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 }