From 378fedd9b8e5512383b94308a8252641a6c8616a Mon Sep 17 00:00:00 2001 From: visualfc Date: Wed, 16 Sep 2026 11:18:22 +0800 Subject: [PATCH] types: find usages of methods on type aliases go/types keeps receivers such as `type Builder = *aBuilder` as *types.Alias. parserMethod only unwrapped pointers and named types, so find-usages reported the definition and skipped every call site. Unwrap aliases before matching named types, and keep a same-object fallback for method uses. --- types/_testdata/alias/alias.go | 18 +++ types/_testdata/alias/expr.go | 15 +++ types/alias_test.go | 207 +++++++++++++++++++++++++++++++++ types/types.go | 106 +++++++++-------- 4 files changed, 295 insertions(+), 51 deletions(-) create mode 100644 types/_testdata/alias/alias.go create mode 100644 types/_testdata/alias/expr.go create mode 100644 types/alias_test.go diff --git a/types/_testdata/alias/alias.go b/types/_testdata/alias/alias.go new file mode 100644 index 0000000..eb8aecb --- /dev/null +++ b/types/_testdata/alias/alias.go @@ -0,0 +1,18 @@ +package fixture + +type aBuilder struct{} + +// Builder is a pointer alias, matching types such as llgo/ssa.Builder = *aBuilder. +type Builder = *aBuilder + +func (b Builder) ChangeInterface() {} + +func (b Builder) ChangeType() {} + +type aValue struct { + X int +} + +type Value = aValue + +func (v Value) Name() string { return "v" } diff --git a/types/_testdata/alias/expr.go b/types/_testdata/alias/expr.go new file mode 100644 index 0000000..73cbad6 --- /dev/null +++ b/types/_testdata/alias/expr.go @@ -0,0 +1,15 @@ +package fixture + +func UseBuilder(b Builder) { + b.ChangeInterface() + b.ChangeType() +} + +func UseBuilderAgain(b Builder) { + b.ChangeInterface() +} + +func UseValue(v Value) { + _ = v.Name() + _ = v.X +} diff --git a/types/alias_test.go b/types/alias_test.go new file mode 100644 index 0000000..185202e --- /dev/null +++ b/types/alias_test.go @@ -0,0 +1,207 @@ +package types + +import ( + "bytes" + "go/ast" + "go/build" + "go/parser" + "go/token" + "go/types" + "path/filepath" + "sort" + "strconv" + "strings" + "testing" +) + +func TestParserMethodPointerAlias(t *testing.T) { + const src = `package p +type aBuilder struct{} +type Builder = *aBuilder +func (b Builder) ChangeInterface() {} +func use(b Builder) { b.ChangeInterface() } +` + fset := token.NewFileSet() + f, err := parser.ParseFile(fset, "p.go", src, 0) + if err != nil { + t.Fatal(err) + } + info := &types.Info{ + Defs: make(map[*ast.Ident]types.Object), + Uses: make(map[*ast.Ident]types.Object), + Selections: make(map[*ast.SelectorExpr]*types.Selection), + } + if _, err := new(types.Config).Check("p", fset, []*ast.File{f}, info); err != nil { + t.Fatal(err) + } + + var method types.Object + for id, obj := range info.Uses { + if id.Name == "ChangeInterface" && obj != nil { + method = obj + break + } + } + if method == nil { + t.Fatal("ChangeInterface use not found") + } + recv := method.Type().(*types.Signature).Recv().Type() + if _, ok := recv.(*types.Alias); !ok { + t.Fatalf("receiver type = %T, want *types.Alias", recv) + } + + named, name, ok := parserMethod(method) + if !ok { + t.Fatalf("parserMethod failed for alias receiver %T %s", recv, recv) + } + if named.Obj().Name() != "aBuilder" || name != "ChangeInterface" { + t.Fatalf("parserMethod = %s.%s, want aBuilder.ChangeInterface", named.Obj().Name(), name) + } +} + +func TestAliasFindUsages(t *testing.T) { + dir, err := filepath.Abs(filepath.Join("_testdata", "alias")) + if err != nil { + t.Fatal(err) + } + for _, ident := range []string{"ChangeInterface", "ChangeType", "Name", "X"} { + t.Run(ident, func(t *testing.T) { + refs := identRefs(t, dir, ident) + if len(refs) < 2 { + t.Fatalf("testdata needs a definition and at least one use for %s, got %v", ident, refs) + } + want := make([]string, len(refs)) + for i, ref := range refs { + want[i] = ref.key + } + sort.Strings(want) + for _, ref := range refs { + got := lookupUsagePositions(t, dir, ref.file, ref.offset) + if !sameStringSet(got, want) { + t.Errorf("cursor %s:%d\n got %v\nwant %v", ref.file, ref.offset, got, want) + } + } + }) + } +} + +type identRef struct { + file string + offset int + key string +} + +func identRefs(t *testing.T, dir, name string) []identRef { + t.Helper() + entries, err := filepath.Glob(filepath.Join(dir, "*.go")) + if err != nil { + t.Fatal(err) + } + sort.Strings(entries) + var refs []identRef + for _, filename := range entries { + fset := token.NewFileSet() + f, err := parser.ParseFile(fset, filename, nil, 0) + if err != nil { + t.Fatal(err) + } + ast.Inspect(f, func(node ast.Node) bool { + id, ok := node.(*ast.Ident) + if !ok || id.Name != name { + return true + } + pos := fset.Position(id.Pos()) + file := filepath.Base(filename) + refs = append(refs, identRef{ + file: file, + offset: pos.Offset, + key: posKey(file, pos.Line, pos.Column), + }) + return true + }) + } + return refs +} + +func lookupUsagePositions(t *testing.T, dir, file string, offset int) []string { + t.Helper() + var buf bytes.Buffer + w := NewPkgWalker(&build.Default) + w.SetOutput(&buf, &buf) + w.SetFindMode(&FindMode{Usage: true}) + conf := DefaultPkgConfig() + cursor := NewFileCursor(nil, dir, file, offset) + pkg, conf, err := w.Check(dir, conf, cursor) + if err != nil { + t.Fatalf("check %s:%d: %v", file, offset, err) + } + if err := w.LookupCursor(pkg, conf, cursor); err != nil { + t.Fatalf("lookup %s:%d: %v\n%s", file, offset, err, buf.String()) + } + var got []string + for _, line := range strings.Split(strings.TrimSpace(buf.String()), "\n") { + line = strings.TrimSpace(line) + if line == "" { + continue + } + fileName, lineNo, col, ok := parsePositionLine(line) + if !ok { + t.Fatalf("unexpected output %q", line) + } + got = append(got, posKey(fileName, lineNo, col)) + } + sort.Strings(got) + return uniqueStrings(got) +} + +func parsePositionLine(s string) (file string, line, col int, ok bool) { + colIdx := strings.LastIndex(s, ":") + if colIdx < 0 { + return + } + lineIdx := strings.LastIndex(s[:colIdx], ":") + if lineIdx < 0 { + return + } + var err error + line, err = strconv.Atoi(s[lineIdx+1 : colIdx]) + if err != nil { + return + } + col, err = strconv.Atoi(s[colIdx+1:]) + if err != nil { + return + } + return filepath.Base(s[:lineIdx]), line, col, true +} + +func posKey(file string, line, col int) string { + return filepath.Base(file) + ":" + strconv.Itoa(line) + ":" + strconv.Itoa(col) +} + +func sameStringSet(a, b []string) bool { + if len(a) != len(b) { + return false + } + for i := range a { + if a[i] != b[i] { + return false + } + } + return true +} + +func uniqueStrings(in []string) []string { + if len(in) == 0 { + return in + } + out := in[:0] + var last string + for i, s := range in { + if i == 0 || s != last { + out = append(out, s) + last = s + } + } + return out +} diff --git a/types/types.go b/types/types.go index 90a6a9f..a708124 100644 --- a/types/types.go +++ b/types/types.go @@ -82,6 +82,25 @@ func sameNamed(n1, n2 *types.Named) bool { return n1 != nil && n2 != nil && n1.Origin().String() == n2.Origin().String() } +// orgType unwraps aliases and a single pointer so callers can match the +// underlying named type. This is required for type aliases such as +// `type Builder = *aBuilder`, where go/types keeps a *types.Alias. +func orgType(typ types.Type) types.Type { + if typ == nil { + return nil + } + typ = types.Unalias(typ) + if pt, ok := typ.(*types.Pointer); ok { + return types.Unalias(pt.Elem()) + } + return typ +} + +func parseNamed(typ types.Type) (named *types.Named, ok bool) { + named, ok = orgType(typ).(*types.Named) + return +} + // func init func init() { Command.Flag.BoolVar(&typesVerbose, "v", false, "verbose debugging") @@ -1143,7 +1162,11 @@ func (w *PkgWalker) lookupNamedMethod(named *types.Named, name string) (types.Ob } Embedded: for i := 0; i < iface.NumEmbeddeds(); i++ { - if obj, na := w.lookupNamedMethod(iface.Embedded(i), name); obj != nil { + embedded, ok := parseNamed(iface.EmbeddedType(i)) + if !ok { + continue + } + if obj, na := w.lookupNamedMethod(embedded, name); obj != nil { return obj, na } } @@ -1161,10 +1184,12 @@ func (w *PkgWalker) lookupNamedMethod(named *types.Named, name string) (types.Ob if !field.Anonymous() { continue } - if typ, ok := field.Type().(*types.Named); ok { - if obj, na := w.lookupNamedMethod(typ, name); obj != nil { - return obj, na - } + embedded, ok := parseNamed(field.Type()) + if !ok { + continue + } + if obj, na := w.lookupNamedMethod(embedded, name); obj != nil { + return obj, na } } } @@ -1222,13 +1247,6 @@ func IsSameObject(a, b types.Object, kind ObjKind) bool { return a.String() == b.String() } -func orgType(typ types.Type) types.Type { - if pt, ok := typ.(*types.Pointer); ok { - return pt.Elem() - } - return typ -} - func findScope(s *types.Scope, pos token.Pos) *types.Scope { for i := 0; i < s.NumChildren(); i++ { child := s.Child(i) @@ -1240,19 +1258,15 @@ func findScope(s *types.Scope, pos token.Pos) *types.Scope { } func (w *PkgWalker) lookupNamed(obj types.Object, cname string) types.Object { - typ := orgType(obj.Type()) - if typ != nil { - if name, ok := typ.(*types.Named); ok { - obj, na := w.lookupNamedFieldVar(name, cname) - if na != nil { - return obj - } else { - obj, na := w.lookupNamedMethod(name, cname) - if na != nil { - return obj - } - } - } + name, ok := parseNamed(obj.Type()) + if !ok { + return nil + } + if obj, na := w.lookupNamedFieldVar(name, cname); na != nil { + return obj + } + if obj, na := w.lookupNamedMethod(name, cname); na != nil { + return obj } return nil } @@ -1339,35 +1353,15 @@ func (w *PkgWalker) LookupByText(pkgInfo *types.Info, text string) types.Object return cursorObj } -func parseNamed(typ types.Type) (named *types.Named, ok bool) { - if t, ok := typ.(*types.Pointer); ok { - typ = t.Elem() - } - named, ok = typ.(*types.Named) - return -} - func parserMethod(obj types.Object) (named *types.Named, method string, ok bool) { - if obj == nil { - return - } - typ := obj.Type() - if typ == nil { + if obj == nil || obj.Type() == nil { return } - sig, ok := typ.(*types.Signature) - if !ok { + sig, isSig := obj.Type().(*types.Signature) + if !isSig || sig.Recv() == nil { return } - recv := sig.Recv() - if recv == nil { - return - } - typ = recv.Type() - if t, ok := typ.(*types.Pointer); ok { - typ = t.Elem() - } - named, ok = typ.(*types.Named) + named, ok = parseNamed(sig.Recv().Type()) method = obj.Name() return } @@ -1509,6 +1503,13 @@ func (w *PkgWalker) LookupObjects(conf *PkgConfig, cursor *FileCursor) error { } } } + if cursorObj != nil { + for id, obj := range pkgInfo.Uses { + if obj == cursorObj { + usages = append(usages, int(id.Pos())) + } + } + } } else { if enableTypeParams && kind == ObjField && findInfo.fieldTypeObj != nil { named, ok := parseNamed(findInfo.fieldTypeObj.Type()) @@ -2008,7 +2009,7 @@ func (w *PkgWalker) CheckObjectInfo(cursorObj types.Object, cursorSelection *typ if kind == ObjMethod && cursorSelection != nil && cursorSelection.Recv() != nil { sig := cursorObj.(*types.Func).Type().Underlying().(*types.Signature) if _, ok := sig.Recv().Type().Underlying().(*types.Interface); ok { - if named, ok := cursorSelection.Recv().(*types.Named); ok { + if named, ok := parseNamed(cursorSelection.Recv()); ok { obj, na := w.lookupNamedMethod(named, cursorObj.Name()) if obj != nil && na != nil { cursorObj = obj @@ -2056,7 +2057,10 @@ func (w *PkgWalker) CheckObjectInfo(cursorObj types.Object, cursorSelection *typ if cursorIsInterfaceMethod { for k, v := range conf.Info.Defs { if k != nil && v != nil && IsSameObject(v, cursorInterfaceTypeNamed.Obj(), kind) { - named := v.Type().(*types.Named) + named, ok := parseNamed(v.Type()) + if !ok { + continue + } obj, typ := w.lookupNamedMethod(named, cursorObj.Name()) if obj != nil && typ != nil { cursorObj = obj