sesamy-cli/internal/reflect.go
Kevin Franklin Kim ab48d6326a
wip: need added
2024-05-17 17:36:32 +02:00

434 lines
9.6 KiB
Go

package internal
import (
"context"
"fmt"
"go/ast"
"go/token"
"go/types"
"maps"
"slices"
"github.com/foomo/sesamy-cli/pkg/config"
"github.com/pkg/errors"
"github.com/pterm/pterm"
"github.com/stoewer/go-strcase"
"golang.org/x/tools/go/packages"
)
func GetStructTypes(ctx context.Context, cfg config.Packages) (map[string]*types.Struct, error) {
ret := map[string]*types.Struct{}
fset := token.NewFileSet()
pkgs, err := packages.Load(&packages.Config{
Mode: packages.NeedName | packages.NeedTypesInfo |
packages.NeedFiles | packages.NeedImports | packages.NeedDeps |
packages.NeedModule | packages.NeedTypes | packages.NeedSyntax,
Context: ctx,
Logf: func(format string, args ...any) {
pterm.Debug.Printfln(format, args...)
},
Fset: fset,
}, cfg.PackageNames()...)
if err != nil {
return nil, err
}
for _, pkg := range pkgs {
if len(pkg.Errors) > 0 {
return nil, errors.Wrap(pkg.Errors[0], "packages contain errors")
}
packageStructs, err := getPackageStructs(cfg, pkg)
if err != nil {
return nil, err
}
maps.Copy(ret, packageStructs)
}
return ret, nil
}
func GetStructTypeParameters(ctx context.Context, cfg config.Packages) (map[string][]string, error) {
typs, err := GetStructTypes(ctx, cfg)
if err != nil {
return nil, err
}
ret := map[string][]string{}
for name, t := range typs {
var fields []string
for i := range t.NumFields() {
tag, err := ParseTag(t.Tag(i))
if err != nil {
return nil, err
}
if tag != "" {
fields = append(fields, tag)
}
}
ret[strcase.SnakeCase(name)] = fields
}
return ret, nil
}
type Struct struct {
Name string `json:"name,omitempty"`
Attributes []Attribute `json:"attributes,omitempty"`
}
type Attribute struct {
Name string `json:"name,omitempty"`
Tags map[string]string `json:"tags,omitempty"`
}
type (
Scanner struct {
cfg *Config
pkgs []*packages.Package
Packages map[string]*ScannerPackage
}
ScannerPackage struct {
pkg *packages.Package
Name string
PkgPath string
Imports map[*types.Package][]string
Scope ScannerScope
Values ScannerValues
}
ScannerScope map[string]types.Type
)
func (s ScannerScope) LookupUnderlyingTypeName(name string) map[string]types.Type {
ret := map[string]types.Type{}
for i, i2 := range s {
if x, ok := i2.(*types.Named); ok && i != name && x.Obj().Name() == name {
ret[i] = i2
}
}
return ret
}
type (
Config struct {
Packages []*ConfigPackage
}
ConfigPackage struct {
Path string
Names []string
}
)
func (c *Config) PackageNames(path string) []string {
for _, configPackage := range c.Packages {
if configPackage.Path == path {
return configPackage.Names
}
}
return nil
}
func (c *Config) PackagePaths() []string {
ret := make([]string, len(c.Packages))
for i, p := range c.Packages {
ret[i] = p.Path
}
return ret
}
func NewScanner(cfg *Config) *Scanner {
return &Scanner{
cfg: cfg,
Packages: map[string]*ScannerPackage{},
}
}
func (s *Scanner) Scan(ctx context.Context) error {
var err error
// load packages
s.pkgs, err = packages.Load(&packages.Config{
Mode: packages.NeedName | packages.NeedTypesInfo |
packages.NeedFiles | packages.NeedImports | packages.NeedDeps |
packages.NeedModule | packages.NeedTypes | packages.NeedSyntax,
Context: ctx,
// Fset: token.NewFileSet(),
}, s.cfg.PackagePaths()...)
if err != nil {
return err
}
// iterate packages
for _, pkg := range s.pkgs {
// retrieve requested packages names
if names := s.cfg.PackageNames(pkg.PkgPath); len(names) > 0 {
s.pkgPackage(pkg, names...)
}
}
return nil
}
// ------------------------------------------------------------------------------------------------
// ~ Private methods
// ------------------------------------------------------------------------------------------------
func (s *Scanner) pkgPackage(pkg *packages.Package, names ...string) {
if _, ok := s.Packages[pkg.PkgPath]; !ok {
s.Packages[pkg.PkgPath] = NewScannerPackage(pkg)
}
// add request scopes
added := s.Packages[pkg.PkgPath].typesScope(names)
// check underlying added scopes
for _, name := range added {
s.typesType(pkg, s.Packages[pkg.PkgPath].Scope[name].Underlying())
}
}
func (s *Scanner) typesType(pkg *packages.Package, v types.Type) {
switch t := v.(type) {
case *types.Struct:
// iterate fields
for i := range t.NumFields() {
s.typesVar(pkg, t.Field(i))
}
default:
fmt.Println(t)
}
}
func (s *Scanner) typesVar(pkg *packages.Package, v *types.Var) {
if !v.Exported() {
return
}
switch t := v.Type().(type) {
case *types.Named:
if p, ok := pkg.Imports[v.Pkg().Path()]; ok {
s.pkgPackage(p, t.Obj().Name())
}
}
}
type ScannerValues map[string]*ast.ValueSpec
func (s ScannerValues) Lookup(name string) *ast.ValueSpec {
return s[name]
}
func NewScannerPackage(pkg *packages.Package) *ScannerPackage {
values := ScannerValues{}
for _, file := range pkg.Syntax {
for _, decl := range file.Decls {
if genDecl, ok := decl.(*ast.GenDecl); ok {
for _, spec := range genDecl.Specs {
if valueSpec, ok := spec.(*ast.ValueSpec); ok && len(valueSpec.Names) > 0 {
values[valueSpec.Names[0].Name] = valueSpec
}
}
}
}
}
return &ScannerPackage{
pkg: pkg,
Name: pkg.Name,
PkgPath: pkg.PkgPath,
Imports: map[*types.Package][]string{},
Scope: ScannerScope{},
Values: values,
}
}
func (s *ScannerPackage) typesScope(names []string) []string {
var added []string
scope := s.pkg.Types.Scope()
// iterate requested names
for _, name := range names {
// check if already within the local scope
if _, ok := s.Scope[name]; !ok {
// lookup scope object
if obj := scope.Lookup(name); obj != nil {
// add type to local scope
s.Scope[name] = obj.Type()
// scan the underlying type
s.typesType(obj.Type().Underlying())
// append to added scopes
added = append(added, name)
}
// FIXME find underlying type usages e.g. `const Foo <name>`
for _, i := range scope.Names() {
child := scope.Lookup(i)
if x, ok := child.Type().(*types.Named); ok && x.Obj().Name() == name {
s.Scope[i] = x
added = append(added, i)
}
}
}
}
return added
}
func (s *ScannerPackage) typesType(v types.Type) {
switch t := v.(type) {
case *types.Struct:
for i := range t.NumFields() {
s.typesVar(t.Field(i))
}
}
}
func (s *ScannerPackage) typesVar(v *types.Var) {
if v.Exported() {
if p, ok := v.Type().(*types.Named); ok {
pkg := p.Obj().Pkg().Path()
if s.pkg.PkgPath != pkg {
if tv, ok := v.Type().(*types.Named); ok {
typeName := tv.Obj().Name()
if !slices.Contains(s.Imports[v.Pkg()], typeName) {
s.Imports[v.Pkg()] = append(s.Imports[v.Pkg()], typeName)
}
}
}
} else {
pterm.Debug.Println("unhandled typeVar")
}
}
}
// --------------------
type Package struct {
pkg *packages.Package
Types []*Type `json:"s,omitempty"`
}
type Type struct {
Spec *ast.TypeSpec
TypeInfo types.TypeAndValue
}
func NewPackage(pkg *packages.Package) *Package {
inst := &Package{
pkg: pkg,
}
for _, file := range pkg.Syntax {
ast.Inspect(file, inst.astNode)
}
return inst
}
func (s *Package) Name() string {
return s.pkg.Name
}
func (s *Package) Path() string {
return s.pkg.PkgPath
}
// ------------------------------------------------------------------------------------------------
// ~ Private methods
// ------------------------------------------------------------------------------------------------
func (s *Package) astNode(n ast.Node) bool {
switch t := n.(type) {
case *ast.File:
for _, d := range t.Decls {
s.astDecl(d)
}
}
return false
}
func (s *Package) astDecl(v ast.Decl) {
switch t := v.(type) {
case *ast.GenDecl:
s.astGenDecl(t)
}
}
func (s *Package) astGenDecl(v *ast.GenDecl) {
switch v.Tok {
case token.IMPORT:
return
default:
for _, spec := range v.Specs {
s.astSpec(spec)
}
}
}
func (s *Package) astSpec(v ast.Spec) {
switch t := v.(type) {
case *ast.TypeSpec:
if t.Name.IsExported() {
s.astTypeSpec(t)
}
}
}
func (s *Package) astTypeSpec(v *ast.TypeSpec) {
r := &Type{Spec: v}
ast.Inspect(v, s.astNode)
switch t := v.Type.(type) {
case *ast.IndexExpr:
if value, ok := s.typeInfo(t.Index); ok {
r.TypeInfo = value
}
default:
return
}
s.Types = append(s.Types, r)
}
func (s *Package) typeInfo(n ast.Expr) (types.TypeAndValue, bool) {
v, ok := s.pkg.TypesInfo.Types[n]
return v, ok
}
func getPackageStructs(cfg config.Packages, pkg *packages.Package) (map[string]*types.Struct, error) {
ret := map[string]*types.Struct{}
if len(pkg.GoFiles) == 0 {
return nil, fmt.Errorf("no input go files for package index")
}
pkgCfg, err := cfg.PackageConfig(pkg.ID)
if err != nil {
return nil, err
}
for _, file := range pkg.Syntax {
ast.Inspect(file, func(n ast.Node) bool {
// GenDecl can be an import, type, var, or const expression
if genDecl, ok := n.(*ast.GenDecl); ok {
if genDecl.Tok == token.IMPORT {
return false
}
for _, spec := range genDecl.Specs {
// e.g. "type Foo struct {}" or "type Bar = string"
if typeSpec, ok := spec.(*ast.TypeSpec); ok && typeSpec.Name.IsExported() {
if t, ok := typeSpec.Type.(*ast.IndexExpr); ok {
x := pkg.TypesInfo.Types[t]
fmt.Println(x)
}
if !pkgCfg.ExportEvent(typeSpec.Name.String()) {
continue
}
if indexExpr, ok := typeSpec.Type.(*ast.IndexExpr); ok {
if indexType, ok := pkg.TypesInfo.Types[indexExpr.Index]; ok {
if s, ok := indexType.Type.Underlying().(*types.Struct); ok {
ret[typeSpec.Name.String()] = s
}
}
}
}
}
return false
}
return true
})
}
return ret, nil
}