Skip to content
84 changes: 60 additions & 24 deletions cl/import.go
Original file line number Diff line number Diff line change
Expand Up @@ -382,26 +382,39 @@ func (p *context) initLink(line string, prefix int, export bool, f func(inPkgNam
}
}

// recvTypeName asserts the post-typecheck receiver invariant. Syntax preload
// uses recvTypeNameInfo so malformed declarations can be skipped safely.
func recvTypeName(typ ast.Expr) string {
retry:
name, _, ok := recvTypeNameInfo(typ)
if !ok {
panic("unreachable")
}
return name
}

// recvTypeNameInfo normalizes the parentheses permitted in receiver
// declarations and reports the uninstantiated base name and pointer form.
func recvTypeNameInfo(typ ast.Expr) (name string, pointer bool, ok bool) {
typ = ast.Unparen(typ)
if star, isPointer := typ.(*ast.StarExpr); isPointer {
pointer = true
typ = ast.Unparen(star.X)
}
switch t := typ.(type) {
case *ast.Ident:
return t.Name
return t.Name, pointer, true
case *ast.IndexExpr:
return trecvTypeName(t.X, t.Index)
typ = t.X
case *ast.IndexListExpr:
Comment thread
cpunion marked this conversation as resolved.
return trecvTypeName(t.X, t.Indices...)
case *ast.ParenExpr:
typ = t.X
goto retry
default:
return "", false, false
}
panic("unreachable")
}

// TODO(xsw): support generic type
func trecvTypeName(t ast.Expr, indices ...ast.Expr) string {
_ = indices
return t.(*ast.Ident).Name
base, valid := ast.Unparen(typ).(*ast.Ident)
if !valid {
return "", false, false
}
return base.Name, pointer, true
}

// inPkgName:
Expand All @@ -410,19 +423,36 @@ func trecvTypeName(t ast.Expr, indices ...ast.Expr) string {
// fullName:
// - func: pkg.name
// - method: pkg.(T).name, pkg.(*T).name
// astFuncName asserts the post-typecheck declaration invariant. Syntax preload
// uses astFuncNameOK so malformed declarations can be skipped safely.
func astFuncName(pkgPath string, fn *ast.FuncDecl) (fullName, inPkgName string) {
fullName, inPkgName, ok := astFuncNameOK(pkgPath, fn)
if !ok {
panic("unreachable")
}
return fullName, inPkgName
}

func astFuncNameOK(pkgPath string, fn *ast.FuncDecl) (fullName, inPkgName string, ok bool) {
if fn == nil || fn.Name == nil {
return "", "", false
}
name := fn.Name.Name
if recv := fn.Recv; recv != nil && len(recv.List) == 1 {
var method string
t := recv.List[0].Type
if tp, ok := t.(*ast.StarExpr); ok {
method = "(*" + recvTypeName(tp.X) + ")." + name
} else {
method = recvTypeName(t) + "." + name
}
return pkgPath + "." + method, method
if fn.Recv == nil {
return pkgPath + "." + name, name, true
}
return pkgPath + "." + name, name
if len(fn.Recv.List) != 1 || fn.Recv.List[0] == nil {
return "", "", false
}
receiverName, pointer, ok := recvTypeNameInfo(fn.Recv.List[0].Type)
if !ok {
return "", "", false
}
method := receiverName + "." + name
if pointer {
method = "(*" + receiverName + ")." + name
}
return pkgPath + "." + method, method, true
}

func typesFuncName(pkgPath string, fn *types.Func) (fullName, inPkgName string) {
Expand Down Expand Up @@ -772,7 +802,13 @@ func ParsePkgSyntax(prog llssa.Program, fset *token.FileSet, pkg *types.Package,
if err := locality.ValidateFuncBody(fset, decl.Body); err != nil {
return err
}
fullName, inPkgName := astFuncName(pkgPath, decl)
// A syntactically valid declaration may still have an invalid
// receiver type. The type checker reports that error; this
// syntax-only pass must not assume its receiver shape.
fullName, inPkgName, ok := astFuncNameOK(pkgPath, decl)
if !ok {
continue
}
collectLinknameByDoc(prog, decl.Doc, fullName, inPkgName)
ctx.processNoInterfaceByDoc(decl.Doc, fullName)
case *ast.GenDecl:
Expand Down
114 changes: 114 additions & 0 deletions cl/import_coverage_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,57 @@ func TestParsePkgSyntaxReportsLocalityErrors(t *testing.T) {
}
}

func TestParsePkgSyntaxSkipsInvalidReceiver(t *testing.T) {
const source = `package p
//go:linkname m C.invalid
func ([]int) m() {}

type T struct{}

//go:linkname (*T).ParenPointer C.parenPointer
func ((*T)) ParenPointer() {}

//go:linkname (*T).InnerParenPointer C.innerParenPointer
func (*(T)) InnerParenPointer() {}

type G[P any] struct{}

//go:linkname G.Value C.genericValue
func (G[P]) Value() {}

//go:linkname (*G).Pointer C.genericPointer
func ((*G[P])) Pointer() {}

//go:linkname F C.f
func F()
`
fset := token.NewFileSet()
file, err := parser.ParseFile(fset, "p.go", source, parser.ParseComments)
if err != nil {
t.Fatalf("ParseFile failed: %v", err)
}
prog := llssa.NewProgram(nil)
pkg := types.NewPackage("example.com/p", "p")
if err := ParsePkgSyntax(prog, fset, pkg, []*ast.File{file}); err != nil {
t.Fatal(err)
}
if _, ok := prog.Linkname(pkg.Path() + ".m"); ok {
t.Fatal("linkname was collected for an invalid receiver")
}
want := map[string]string{
pkg.Path() + ".(*T).ParenPointer": "C.parenPointer",
pkg.Path() + ".(*T).InnerParenPointer": "C.innerParenPointer",
pkg.Path() + ".G.Value": "C.genericValue",
pkg.Path() + ".(*G).Pointer": "C.genericPointer",
pkg.Path() + ".F": "C.f",
}
for fullName, target := range want {
if got, ok := prog.Linkname(fullName); !ok || got != target {
t.Errorf("linkname %q = (%q,%v), want (%q,%v)", fullName, got, ok, target, true)
}
}
}

func TestPkgSymInfoAddSymAndInitLinknamesCoverage(t *testing.T) {
dir := t.TempDir()
srcPath := filepath.Join(dir, "p.go")
Expand Down Expand Up @@ -221,6 +272,69 @@ func TestAstAndTypesFuncNameCoverage(t *testing.T) {
}
}

func TestReceiverNameHelpersRejectMalformedSyntax(t *testing.T) {
invalidBase := &ast.IndexExpr{
X: &ast.SelectorExpr{X: &ast.Ident{Name: "pkg"}, Sel: &ast.Ident{Name: "T"}},
Index: &ast.Ident{Name: "P"},
}
if name, pointer, ok := recvTypeNameInfo(invalidBase); ok || name != "" || pointer {
t.Fatalf("recvTypeNameInfo(invalid indexed base) = (%q, %v, %v), want empty false false", name, pointer, ok)
}
if name, pointer, ok := recvTypeNameInfo(&ast.ArrayType{Elt: &ast.Ident{Name: "int"}}); ok || name != "" || pointer {
t.Fatalf("recvTypeNameInfo(array) = (%q, %v, %v), want empty false false", name, pointer, ok)
}
indexed := &ast.IndexListExpr{
X: &ast.ParenExpr{X: &ast.Ident{Name: "G"}},
Indices: []ast.Expr{&ast.Ident{Name: "P"}, &ast.Ident{Name: "Q"}},
}
if name, pointer, ok := recvTypeNameInfo(indexed); !ok || name != "G" || pointer {
t.Fatalf("recvTypeNameInfo(index list) = (%q, %v, %v), want G false true", name, pointer, ok)
}

invalidDecls := []struct {
name string
fn *ast.FuncDecl
}{
{name: "nil declaration"},
{name: "nil name", fn: &ast.FuncDecl{}},
{name: "empty receiver list", fn: &ast.FuncDecl{
Name: &ast.Ident{Name: "M"}, Recv: &ast.FieldList{},
}},
{name: "nil receiver field", fn: &ast.FuncDecl{
Name: &ast.Ident{Name: "M"}, Recv: &ast.FieldList{List: []*ast.Field{nil}},
}},
{name: "invalid receiver type", fn: &ast.FuncDecl{
Name: &ast.Ident{Name: "M"},
Recv: &ast.FieldList{List: []*ast.Field{{Type: &ast.ArrayType{
Elt: &ast.Ident{Name: "int"},
}}}},
}},
}
for _, tt := range invalidDecls {
t.Run(tt.name, func(t *testing.T) {
if full, inPkg, ok := astFuncNameOK("example.com/p", tt.fn); ok || full != "" || inPkg != "" {
t.Fatalf("astFuncNameOK = (%q, %q, %v), want empty empty false", full, inPkg, ok)
}
})
}

expectPanic := func(t *testing.T, call func()) {
t.Helper()
defer func() {
if recover() == nil {
t.Fatal("call did not panic")
}
}()
call()
}
t.Run("receiver invariant wrapper", func(t *testing.T) {
expectPanic(t, func() { recvTypeName(invalidBase) })
})
t.Run("function invariant wrapper", func(t *testing.T) {
expectPanic(t, func() { astFuncName("example.com/p", nil) })
})
}

func TestParsePkgSyntaxCollectsLinknames(t *testing.T) {
cases := []struct {
name string
Expand Down
Loading
Loading