github.com/podhmo/reflect-shape@v0.4.3/metadata/lookup.go (about)

     1  package metadata
     2  
     3  import (
     4  	"fmt"
     5  	"go/ast"
     6  	"go/parser"
     7  	"go/token"
     8  	"log"
     9  	"os"
    10  	"reflect"
    11  	"runtime"
    12  	"runtime/debug"
    13  	"strconv"
    14  	"strings"
    15  
    16  	"github.com/podhmo/commentof"
    17  	"github.com/podhmo/commentof/collect"
    18  	"github.com/podhmo/reflect-shape/metadata/internal/unsaferuntime"
    19  	"golang.org/x/tools/go/packages"
    20  )
    21  
    22  // ErrNotFound is the error metadata is not found.
    23  var ErrNotFound = fmt.Errorf("not found")
    24  
    25  // ErrNotSupported is the error metadata is not supported, yet
    26  var ErrNotSupported = fmt.Errorf("not supported")
    27  
    28  var DEBUG = false
    29  
    30  func init() {
    31  	if ok, _ := strconv.ParseBool(os.Getenv("DEBUG")); ok {
    32  		DEBUG = true
    33  	}
    34  }
    35  
    36  type Lookup struct {
    37  	Fset     *token.FileSet
    38  	accessor *unsaferuntime.Accessor
    39  
    40  	IncludeGoTestFiles bool
    41  	IncludeUnexported  bool
    42  
    43  	cache map[string]*packageRef // TODO: lock
    44  }
    45  
    46  func NewLookup(fset *token.FileSet) *Lookup {
    47  	return &Lookup{
    48  		Fset:               fset,
    49  		accessor:           unsaferuntime.New(),
    50  		IncludeGoTestFiles: false,
    51  		IncludeUnexported:  false,
    52  		cache:              map[string]*packageRef{},
    53  	}
    54  }
    55  
    56  type Func struct {
    57  	pc   uintptr
    58  	Raw  *collect.Func
    59  	Recv string
    60  }
    61  
    62  func (m *Func) Fullname() string {
    63  	return runtime.FuncForPC(m.pc).Name()
    64  }
    65  
    66  func (m *Func) Name() string {
    67  	return m.Raw.Name
    68  }
    69  
    70  func (m *Func) Doc() string {
    71  	return strings.TrimSpace(m.Raw.Doc)
    72  }
    73  
    74  type Var struct {
    75  	Name string
    76  	Doc  string
    77  }
    78  
    79  func (m *Func) Args() []Var {
    80  	vars := make([]Var, len(m.Raw.ParamNames))
    81  	for i, id := range m.Raw.ParamNames {
    82  		p := m.Raw.Params[id]
    83  		doc := p.Doc
    84  		if doc == "" {
    85  			doc = p.Comment
    86  		}
    87  		vars[i] = Var{Name: m.Raw.Params[id].Name, Doc: strings.TrimSpace(doc)}
    88  	}
    89  	return vars
    90  }
    91  
    92  func (m *Func) Returns() []Var {
    93  	vars := make([]Var, len(m.Raw.ReturnNames))
    94  	for i, id := range m.Raw.ReturnNames {
    95  		p := m.Raw.Returns[id]
    96  		doc := p.Doc
    97  		if doc == "" {
    98  			doc = p.Comment
    99  		}
   100  		vars[i] = Var{Name: m.Raw.Returns[id].Name, Doc: strings.TrimSpace(doc)}
   101  	}
   102  	return vars
   103  }
   104  
   105  func (l *Lookup) LookupFromFunc(fn interface{}) (*Func, error) {
   106  	pc := reflect.ValueOf(fn).Pointer()
   107  	return l.LookupFromFuncForPC(pc)
   108  }
   109  
   110  func (l *Lookup) LookupFromFuncForPC(pc uintptr) (*Func, error) {
   111  	rfunc := l.accessor.FuncForPC(pc)
   112  	if rfunc == nil {
   113  		return nil, fmt.Errorf("cannot find runtime.Func")
   114  	}
   115  
   116  	filename, _ := rfunc.FileLine(rfunc.Entry())
   117  
   118  	// /<pkg name>.<function name>
   119  	// /<pkg name>.<recv>.<method name>
   120  	// /<pkg name>.<recv>.<method name>-fm
   121  
   122  	parts := strings.Split(rfunc.Name(), "/")
   123  	last := parts[len(parts)-1]
   124  	pkgname, name, isFunc := strings.Cut(last, ".")
   125  	_ = pkgname
   126  	if !isFunc {
   127  		return nil, fmt.Errorf("unexpected func: %v", rfunc.Name())
   128  	}
   129  
   130  	recv, name, isMethod := strings.Cut(name, ".")
   131  	if isMethod {
   132  		recv = strings.Trim(recv, "(*)")
   133  	} else {
   134  		name = recv
   135  		recv = ""
   136  	}
   137  	// log.Printf("pkgname:%-15s\trecv:%-10s\tname:%s\tisMethod:%v\n", pkgname, recv, name, isMethod)
   138  
   139  	pkgpath := rfuncPkgpath(rfunc)
   140  	p0, ok := l.cache[pkgpath]
   141  	if ok {
   142  		if p0.fullset {
   143  			if p0.err != nil {
   144  				return nil, p0.err
   145  			}
   146  
   147  			if isMethod {
   148  				ob, ok := p0.Types[recv]
   149  				if !ok {
   150  					// anonymous function? (TODO: correct check)
   151  					if _, ok := p0.Functions[name]; !ok {
   152  						return nil, fmt.Errorf("lookup metadata of anonymous function %s, %w", rfunc.Name(), ErrNotSupported)
   153  					}
   154  					return nil, fmt.Errorf("lookup metadata of method %s, %w", rfunc.Name(), ErrNotFound)
   155  				}
   156  				result, ok := ob.Methods[name]
   157  				if !ok {
   158  					return nil, fmt.Errorf("lookup metadata of method %s, %w", rfunc.Name(), ErrNotFound)
   159  				}
   160  				if DEBUG {
   161  					log.Println("\tOK func cache (full)", rfunc.Name())
   162  				}
   163  				return &Func{pc: pc, Raw: result, Recv: recv}, nil
   164  			} else {
   165  				result, ok := p0.Functions[name]
   166  				if !ok {
   167  					return nil, fmt.Errorf("lookup metadata of %s is failed %w", rfunc.Name(), ErrNotFound)
   168  				}
   169  				if DEBUG {
   170  					log.Println("\tOK func cache (full)", rfunc.Name())
   171  				}
   172  				return &Func{Raw: result}, nil
   173  			}
   174  		}
   175  
   176  		for _, visitedFile := range p0.FileNames {
   177  			if visitedFile == filename {
   178  				f, ok := p0.Files[filename]
   179  				if !ok {
   180  					break
   181  				}
   182  
   183  				if isMethod {
   184  					ob, ok := f.Types[recv]
   185  					if !ok {
   186  						// anonymous function? (TODO: correct check)
   187  						if _, ok := p0.Functions[name]; !ok {
   188  							return nil, fmt.Errorf("lookup metadata of anonymous function %s, %w", rfunc.Name(), ErrNotSupported)
   189  						}
   190  						return nil, fmt.Errorf("lookup metadata of method %s, %w", rfunc.Name(), ErrNotFound)
   191  					}
   192  					result, ok := ob.Methods[name]
   193  					if !ok {
   194  						return nil, fmt.Errorf("lookup metadata of method %s, %w", rfunc.Name(), ErrNotFound)
   195  					}
   196  					if DEBUG {
   197  						log.Println("\tOK func cache", rfunc.Name())
   198  					}
   199  					return &Func{pc: pc, Raw: result, Recv: recv}, nil
   200  				} else {
   201  					result, ok := f.Functions[name]
   202  					if !ok {
   203  						return nil, fmt.Errorf("lookup metadata of %s is failed.. %w", rfunc.Name(), ErrNotFound)
   204  					}
   205  					if DEBUG {
   206  						log.Println("\tOK func cache", rfunc.Name())
   207  					}
   208  					return &Func{pc: pc, Raw: result}, nil
   209  				}
   210  			}
   211  		}
   212  	}
   213  
   214  	f, err := parser.ParseFile(l.Fset, filename, nil, parser.ParseComments)
   215  	if f == nil {
   216  		l.cache[pkgpath] = &packageRef{fullset: false, err: err} // error cache
   217  		return nil, err
   218  	}
   219  
   220  	p, err := commentof.File(l.Fset, f, commentof.WithIncludeUnexported(l.IncludeUnexported), func(b *collect.PackageBuilder) {
   221  		if p0 != nil {
   222  			b.Package = p0.Package // merge
   223  		}
   224  	})
   225  	if !ok && p != nil {
   226  		l.cache[pkgpath] = &packageRef{fullset: false, Package: p}
   227  	}
   228  	if err != nil {
   229  		return nil, err
   230  	}
   231  
   232  	if DEBUG {
   233  		log.Println("\tNG func cache", rfunc.Name())
   234  	}
   235  	if isMethod {
   236  		ob, ok := p.Types[recv]
   237  		if !ok {
   238  			// anonymous function? (TODO: correct check)
   239  			if _, ok := p.Functions[name]; !ok {
   240  				return nil, fmt.Errorf("lookup metadata of anonymous function %s, %w", rfunc.Name(), ErrNotSupported)
   241  			}
   242  			return nil, fmt.Errorf("lookup metadata of method %s, %w", rfunc.Name(), ErrNotFound)
   243  		}
   244  		result, ok := ob.Methods[name]
   245  		if !ok {
   246  			return nil, fmt.Errorf("lookup metadata of method %s, %w", rfunc.Name(), ErrNotFound)
   247  		}
   248  		return &Func{pc: pc, Raw: result, Recv: recv}, nil
   249  	} else {
   250  		result, ok := p.Functions[name]
   251  		if !ok {
   252  			return nil, fmt.Errorf("lookup metadata of function %s, %w", rfunc.Name(), ErrNotFound)
   253  		}
   254  		return &Func{pc: pc, Raw: result}, nil
   255  	}
   256  }
   257  
   258  func rfuncPkgpath(rfunc *runtime.Func) string {
   259  	parts := strings.Split(rfunc.Name(), ".")
   260  	return strings.Join(parts[:len(parts)-1], ".")
   261  }
   262  
   263  type Type struct {
   264  	Raw *collect.Object
   265  }
   266  
   267  func (s *Type) Name() string {
   268  	return s.Raw.Name
   269  }
   270  
   271  func (s *Type) Doc() string {
   272  	doc := s.Raw.Doc
   273  	if doc == "" {
   274  		doc = s.Raw.Comment
   275  	}
   276  	return strings.TrimSpace(doc)
   277  }
   278  
   279  func (s *Type) FieldComments() map[string]string {
   280  	comments := make(map[string]string, len(s.Raw.Fields))
   281  	for _, f := range s.Raw.Fields {
   282  		doc := f.Doc
   283  		if doc == "" {
   284  			doc = f.Comment
   285  		}
   286  		comments[f.Name] = strings.TrimSpace(doc)
   287  	}
   288  	return comments
   289  }
   290  
   291  func (l *Lookup) LookupFromType(ob interface{}) (*Type, error) {
   292  	rt := reflect.TypeOf(ob)
   293  	return l.LookupFromTypeForReflectType(rt)
   294  }
   295  func (l *Lookup) LookupFromTypeForReflectType(rt reflect.Type) (*Type, error) {
   296  	obname, _, _ := strings.Cut(rt.Name(), "[") // for generics
   297  	pkgpath := rt.PkgPath()
   298  
   299  	if pkgpath == "main" {
   300  		binfo, ok := debug.ReadBuildInfo()
   301  		if !ok {
   302  			log.Println("debug.ReadBuildInfo() is failed")
   303  			return nil, ErrNotFound
   304  		}
   305  		pkgpath = binfo.Path
   306  	}
   307  
   308  	if p, ok := l.cache[pkgpath]; ok && p.fullset {
   309  		if p.err != nil {
   310  			return nil, p.err
   311  		}
   312  
   313  		result, ok := p.Types[obname]
   314  		if !ok {
   315  			result, ok = p.Interfaces[obname]
   316  			if !ok {
   317  				return nil, fmt.Errorf("lookup metadata of %v is failed %w", rt, ErrNotFound)
   318  			}
   319  		}
   320  		if DEBUG {
   321  			log.Println("OK package cache", pkgpath)
   322  		}
   323  		return &Type{Raw: result}, nil
   324  	}
   325  
   326  	cfg := &packages.Config{
   327  		Fset:  l.Fset,
   328  		Mode:  packages.NeedName | packages.NeedFiles | packages.NeedSyntax,
   329  		Tests: l.IncludeGoTestFiles, // TODO: support <name>_test package
   330  		ParseFile: func(fset *token.FileSet, filename string, src []byte) (*ast.File, error) {
   331  			// TODO: debug print
   332  			const mode = parser.ParseComments //| parser.AllErrors
   333  			return parser.ParseFile(fset, filename, src, mode)
   334  		},
   335  	}
   336  
   337  	patterns := []string{pkgpath}
   338  	if strings.HasSuffix(pkgpath, "_test") {
   339  		patterns = []string{strings.TrimSuffix(pkgpath, "_test")} // for go test
   340  	}
   341  	pkgs, err := packages.Load(cfg, patterns...)
   342  	if err != nil {
   343  		return nil, fmt.Errorf("packages.Load() %w", err)
   344  	}
   345  
   346  	for _, pkg := range pkgs {
   347  		if len(pkg.Errors) > 0 {
   348  			for _, err := range pkg.Errors {
   349  				log.Printf("lookup package error (%s) %+v", pkg, err)
   350  			}
   351  			continue
   352  		}
   353  
   354  		if pkg.PkgPath != pkgpath {
   355  			continue
   356  		}
   357  		tree := &ast.Package{Name: pkg.Name, Files: map[string]*ast.File{}}
   358  		for _, f := range pkg.Syntax {
   359  			filename := l.Fset.File(f.Pos()).Name()
   360  			tree.Files[filename] = f
   361  		}
   362  
   363  		ref := &packageRef{fullset: true}
   364  		l.cache[pkg.PkgPath] = ref
   365  		p, err := commentof.Package(l.Fset, tree, commentof.WithIncludeUnexported(l.IncludeUnexported))
   366  		if err != nil {
   367  			ref.err = err
   368  			return nil, fmt.Errorf("collect: dir=%s, name=%s, %w", pkg.PkgPath, obname, err)
   369  		}
   370  		ref.Package = p
   371  
   372  		result, ok := p.Types[obname]
   373  		if !ok {
   374  			result, ok = p.Interfaces[obname]
   375  			if !ok {
   376  				continue
   377  			}
   378  		}
   379  		if DEBUG {
   380  			log.Println("NG package cache", pkgpath)
   381  		}
   382  		return &Type{Raw: result}, nil
   383  	}
   384  	return nil, fmt.Errorf("lookup metadata of %v is failed %w", rt, ErrNotFound)
   385  }
   386  
   387  type packageRef struct {
   388  	*collect.Package
   389  
   390  	fullset bool
   391  	err     error
   392  }