Skip to content

Commit 9cef8b1

Browse files
John Dethridgegopherbot
authored andcommitted
go/callgraph/cha: more precise resolution of unexported methods
Use (*go/types.Func).Id as the key for looking up which methods are potential matches for a selector. If a selector refers to an unexported method, it can only be a method defined in the current package. Previously the algorithm would consider all methods of the same name to be potential matches. SSA functions created by the user with (*ssa.Program).NewFunction may not have a link to a go/types.Func, and so this would not work, but it is unlikely that users of the cha library would also use NewFunction to create a synthetic method on some type. Fixes golang/go#66689 Change-Id: Ia7d4fcae56987f1d23fbefbf02251d76e5e951c3 Reviewed-on: https://go-review.googlesource.com/c/tools/+/574955 LUCI-TryBot-Result: Go LUCI <[email protected]> Reviewed-by: Tim King <[email protected]> Auto-Submit: Alan Donovan <[email protected]> Reviewed-by: Alan Donovan <[email protected]>
1 parent 0cc2ffd commit 9cef8b1

File tree

2 files changed

+83
-7
lines changed

2 files changed

+83
-7
lines changed

go/callgraph/cha/cha.go

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -93,10 +93,14 @@ func lazyCallees(fns map[*ssa.Function]bool) func(site ssa.CallInstruction) []*s
9393
// a dynamic call of a particular signature.
9494
var funcsBySig typeutil.Map // value is []*ssa.Function
9595

96-
// methodsByName contains all methods,
97-
// grouped by name for efficient lookup.
98-
// (methodsById would be better but not every SSA method has a go/types ID.)
99-
methodsByName := make(map[string][]*ssa.Function)
96+
// methodsByID contains all methods, grouped by ID for efficient
97+
// lookup.
98+
//
99+
// We must key by ID, not name, for correct resolution of interface
100+
// calls to a type with two (unexported) methods spelled the same but
101+
// from different packages. The fact that the concrete type implements
102+
// the interface does not mean the call dispatches to both methods.
103+
methodsByID := make(map[string][]*ssa.Function)
100104

101105
// An imethod represents an interface method I.m.
102106
// (There's no go/types object for it;
@@ -118,7 +122,7 @@ func lazyCallees(fns map[*ssa.Function]bool) func(site ssa.CallInstruction) []*s
118122
id := m.Id()
119123
methods, ok := methodsMemo[imethod{I, id}]
120124
if !ok {
121-
for _, f := range methodsByName[m.Name()] {
125+
for _, f := range methodsByID[id] {
122126
C := f.Signature.Recv().Type() // named or *named
123127
if types.Implements(C, I) {
124128
methods = append(methods, f)
@@ -138,8 +142,9 @@ func lazyCallees(fns map[*ssa.Function]bool) func(site ssa.CallInstruction) []*s
138142
funcs, _ := funcsBySig.At(f.Signature).([]*ssa.Function)
139143
funcs = append(funcs, f)
140144
funcsBySig.Set(f.Signature, funcs)
141-
} else {
142-
methodsByName[f.Name()] = append(methodsByName[f.Name()], f)
145+
} else if obj := f.Object(); obj != nil {
146+
id := obj.(*types.Func).Id()
147+
methodsByID[id] = append(methodsByID[id], f)
143148
}
144149
}
145150

go/callgraph/cha/cha_test.go

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313
"bytes"
1414
"fmt"
1515
"go/ast"
16+
"go/build"
1617
"go/parser"
1718
"go/token"
1819
"go/types"
@@ -21,6 +22,7 @@ import (
2122
"strings"
2223
"testing"
2324

25+
"golang.org/x/tools/go/buildutil"
2426
"golang.org/x/tools/go/callgraph"
2527
"golang.org/x/tools/go/callgraph/cha"
2628
"golang.org/x/tools/go/loader"
@@ -92,6 +94,75 @@ func TestCHAGenerics(t *testing.T) {
9294
}
9395
}
9496

97+
// TestCHAUnexported tests call resolution for unexported methods.
98+
func TestCHAUnexported(t *testing.T) {
99+
// The two packages below each have types with methods called "m".
100+
// Each of these methods should only be callable by functions in their
101+
// own package, because they are unexported.
102+
//
103+
// In particular:
104+
// - main.main can call (main.S1).m
105+
// - p2.Foo can call (p2.S2).m
106+
// - main.main cannot call (p2.S2).m
107+
// - p2.Foo cannot call (main.S1).m
108+
//
109+
// We use CHA to build a callgraph, then check that it has the
110+
// appropriate set of edges.
111+
112+
main := `package main
113+
import "p2"
114+
type I1 interface { m() }
115+
type S1 struct { p2.I2 }
116+
func (s S1) m() { }
117+
func main() {
118+
var s S1
119+
var o I1 = s
120+
o.m()
121+
p2.Foo(s)
122+
}`
123+
124+
p2 := `package p2
125+
type I2 interface { m() }
126+
type S2 struct { }
127+
func (s S2) m() { }
128+
func Foo(i I2) { i.m() }`
129+
130+
want := `All calls
131+
main.init --> p2.init
132+
main.main --> (main.S1).m
133+
main.main --> p2.Foo
134+
p2.Foo --> (p2.S2).m`
135+
136+
conf := loader.Config{
137+
Build: fakeContext(map[string]string{"main": main, "p2": p2}),
138+
}
139+
conf.Import("main")
140+
iprog, err := conf.Load()
141+
if err != nil {
142+
t.Fatalf("Load failed: %v", err)
143+
}
144+
prog := ssautil.CreateProgram(iprog, ssa.InstantiateGenerics)
145+
prog.Build()
146+
147+
cg := cha.CallGraph(prog)
148+
149+
// The graph is easier to read without synthetic nodes.
150+
cg.DeleteSyntheticNodes()
151+
152+
if got := printGraph(cg, nil, "", "All calls"); got != want {
153+
t.Errorf("cha.CallGraph: got:\n%s\nwant:\n%s", got, want)
154+
}
155+
}
156+
157+
// Simplifying wrapper around buildutil.FakeContext for single-file packages.
158+
func fakeContext(pkgs map[string]string) *build.Context {
159+
pkgs2 := make(map[string]map[string]string)
160+
for path, content := range pkgs {
161+
pkgs2[path] = map[string]string{"x.go": content}
162+
}
163+
return buildutil.FakeContext(pkgs2)
164+
}
165+
95166
func loadProgInfo(filename string, mode ssa.BuilderMode) (*ssa.Program, *ast.File, *ssa.Package, error) {
96167
content, err := os.ReadFile(filename)
97168
if err != nil {

0 commit comments

Comments
 (0)