cl: tighten funcName wrapper fallback and add regressions

This commit is contained in:
Li Jie
2026-03-03 11:33:12 +08:00
parent bd4b1894ae
commit f0145a6443
2 changed files with 123 additions and 34 deletions
+112 -21
View File
@@ -9,6 +9,7 @@ import (
"go/token"
"go/types"
"sort"
"strings"
"testing"
"github.com/goplus/gogen/packages"
@@ -16,6 +17,63 @@ import (
"golang.org/x/tools/go/ssa/ssautil"
)
func buildSSAPackage(t *testing.T, src string) *ssa.Package {
t.Helper()
fset := token.NewFileSet()
f, err := parser.ParseFile(fset, "foo.go", src, 0)
if err != nil {
t.Fatal(err)
}
files := []*ast.File{f}
pkg := types.NewPackage("foo", "foo")
imp := packages.NewImporter(fset)
mode := ssa.SanityCheckFunctions | ssa.InstantiateGenerics
ssapkg, _, err := ssautil.BuildPackage(&types.Config{Importer: imp}, fset, pkg, files, mode)
if err != nil {
t.Fatal(err)
}
return ssapkg
}
func collectFuncNamesBySSAName(ssapkg *ssa.Package, ssaName string) []string {
var got []string
for fn := range ssautil.AllFunctions(ssapkg.Prog) {
if fn == nil || fn.Name() != ssaName {
continue
}
got = append(got, funcName(ssapkg.Pkg, fn, false))
}
sort.Strings(got)
return got
}
func expectTwoDistinctNamesForTypes(t *testing.T, got []string, suffix string) {
t.Helper()
if len(got) != 2 {
t.Fatalf("got %d function names, want 2: %v", len(got), got)
}
if got[0] == got[1] {
t.Fatalf("got duplicate names: %v", got)
}
var hasA, hasB bool
for _, name := range got {
if !strings.Contains(name, suffix) {
t.Fatalf("name %q missing %q", name, suffix)
}
if strings.Contains(name, "A") {
hasA = true
}
if strings.Contains(name, "B") {
hasB = true
}
}
if !hasA || !hasB {
t.Fatalf("expected names for both A and B, got %v", got)
}
}
func TestFuncName_NestedClosureInMethodIncludesRecv(t *testing.T) {
const src = `package foo
@@ -39,28 +97,9 @@ func (b *B) marshal() int {
}
`
fset := token.NewFileSet()
f, err := parser.ParseFile(fset, "foo.go", src, 0)
if err != nil {
t.Fatal(err)
}
files := []*ast.File{f}
pkg := types.NewPackage("foo", "foo")
imp := packages.NewImporter(fset)
mode := ssa.SanityCheckFunctions | ssa.InstantiateGenerics
ssapkg, _, err := ssautil.BuildPackage(&types.Config{Importer: imp}, fset, pkg, files, mode)
if err != nil {
t.Fatal(err)
}
ssapkg := buildSSAPackage(t, src)
var got []string
for fn := range ssautil.AllFunctions(ssapkg.Prog) {
if fn == nil || fn.Name() != "marshal$1$1" {
continue
}
got = append(got, funcName(ssapkg.Pkg, fn, false))
}
sort.Strings(got)
got := collectFuncNamesBySSAName(ssapkg, "marshal$1$1")
want := []string{
"foo.(*A).marshal$1$1",
@@ -77,3 +116,55 @@ func (b *B) marshal() int {
}
}
}
func TestFuncName_TopLevelAndPlainClosureNoRecv(t *testing.T) {
const src = `package foo
func top() {}
func outer() {
f := func() {}
f()
}
`
ssapkg := buildSSAPackage(t, src)
topFn := ssapkg.Func("top")
if topFn == nil {
t.Fatal("top function not found")
}
if got, want := funcName(ssapkg.Pkg, topFn, false), "foo.top"; got != want {
t.Fatalf("top-level func name = %q, want %q", got, want)
}
closures := collectFuncNamesBySSAName(ssapkg, "outer$1")
if len(closures) != 1 {
t.Fatalf("got %d closure name(s) for outer$1: %v, want 1", len(closures), closures)
}
if got, want := closures[0], "foo.outer$1"; got != want {
t.Fatalf("closure name = %q, want %q", got, want)
}
}
func TestFuncName_ThunkAndBoundStillCarryRecv(t *testing.T) {
const src = `package foo
type A int
type B int
func (A) f() int { return 1 }
func (B) f() int { return 2 }
var ta = A.f
var tb = B.f
var ba = A(0).f
var bb = B(0).f
`
ssapkg := buildSSAPackage(t, src)
thunks := collectFuncNamesBySSAName(ssapkg, "f$thunk")
expectTwoDistinctNamesForTypes(t, thunks, "f$thunk")
bounds := collectFuncNamesBySSAName(ssapkg, "f$bound")
expectTwoDistinctNamesForTypes(t, bounds, "f$bound")
}
+11 -13
View File
@@ -415,26 +415,24 @@ func funcName(pkg *types.Package, fn *ssa.Function, org bool) string {
// Closures in methods can be nested (closure inside closure inside method).
// Walking only one Parent() loses the receiver for deeper nests, producing
// names like "pkg.marshal$1$1" that can collide across receiver types.
// Walk parents until we find a receiver or a thunk/bound pattern.
// Walk parents until we find a receiver.
var recv *types.Var
for f := fn; f != nil; f = f.Parent() {
recv = f.Signature.Recv()
if recv != nil {
break
}
name := f.Name()
// check $thunk and $bound
if strings.HasSuffix(name, "$thunk") {
// For thunks, extract receiver from first parameter.
if params := f.Signature.Params(); params.Len() > 0 {
recv = params.At(0)
break
}
} else if strings.HasSuffix(name, "$bound") && len(f.FreeVars) == 1 {
// For bound method wrappers, synthesize receiver var from free var type.
recv = types.NewVar(token.NoPos, nil, "", f.FreeVars[0].Type())
break
}
// For wrappers, fall back to metadata available on fn itself.
name := fn.Name()
if recv == nil && strings.HasSuffix(name, "$thunk") {
// For thunks, extract receiver from first parameter.
if params := fn.Signature.Params(); params.Len() > 0 {
recv = params.At(0)
}
} else if recv == nil && strings.HasSuffix(name, "$bound") && len(fn.FreeVars) == 1 {
// For bound method wrappers, synthesize receiver var from free var type.
recv = types.NewVar(token.NoPos, nil, "", fn.FreeVars[0].Type())
}
var fnName string
if org := fn.Origin(); org != nil {