blob: 6c8c54698b5eb9bd613eaea67e3f9e59bfdf162c [file] [log] [blame]
// Copyright 2023 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package typerefs_test
import (
"context"
"fmt"
"go/token"
"sort"
"testing"
"github.com/google/go-cmp/cmp"
"golang.org/x/tools/gopls/internal/lsp/cache"
"golang.org/x/tools/gopls/internal/lsp/source"
"golang.org/x/tools/gopls/internal/lsp/source/typerefs"
"golang.org/x/tools/gopls/internal/span"
"golang.org/x/tools/internal/testenv"
)
func TestRefs(t *testing.T) {
ctx := context.Background()
tests := []struct {
label string
srcs []string // source for the local package; package name must be p
imports map[string]string // for simplicity: importPath -> pkgID/pkgName (we set pkgName == pkgID)
want map[string][]string // decl name -> id.<decl name>
go118 bool // test uses generics
allowErrs bool // whether we expect parsing errors
}{
{
label: "empty package",
want: map[string][]string{},
},
{
label: "fields",
srcs: []string{`
package p
type A struct{ b B }
type B func(c C) (d D)
type C int
type D int
// Should not be referenced by field names.
type b int
type c int
type d int
`},
want: map[string][]string{
"A": {"p.B"},
"B": {"p.C", "p.D"},
},
},
{
label: "embedding",
srcs: []string{`
package p
type A struct{
B
_ struct {
C
}
D
}
type B int
type C int
type D interface{
B
}
`},
want: map[string][]string{
"A": {"p.B", "p.C", "p.D"},
"D": {"p.B"},
},
},
{
label: "constraint embedding",
srcs: []string{`
package p
type A interface{
int | B | ~C
struct{D}
}
type B int
type C int
type D int
`},
want: map[string][]string{
"A": {"p.B", "p.C", "p.D"},
},
go118: true,
},
{
label: "funcs",
srcs: []string{`
package p
type A int
type B int
const C B = 2
func F(A) B {
return C
}
var V = F(W)
var W A
`},
want: map[string][]string{
"C": {"p.B"},
"F": {"p.A", "p.B"},
"V": {"p.F", "p.W"}, // p.W edge can't be eliminated: F could be builtin or generic
"W": {"p.A"},
},
},
{
label: "methods",
srcs: []string{`package p
type A int
type B int
`, `package p
func (A) M(B)
func (*B) M(A)
`},
want: map[string][]string{
"A": {"p.B"},
"B": {"p.A"},
},
},
{
label: "initializers",
srcs: []string{`
package p
var a b = c // type does not depend on c
type b int
var c = d // type does depend on d
var d b
var e = d + a
var f = func() b { return e }
var g = struct{
a b
_ [unsafe.Sizeof(g)]int
}{}
var h = (d + a + c*c)
var i = (a+c).f
`},
want: map[string][]string{
"a": {"p.b"},
"c": {"p.d"},
"d": {"p.b"},
"e": {"p.a", "p.d"},
"f": {"p.b"},
"g": {"p.b", "p.g"}, // p.g edge could be skipped
"h": {"p.a", "p.c", "p.d"},
"i": {"p.a", "p.c"},
},
},
{
label: "builtins",
srcs: []string{`package p
var A = new(B)
type B struct{}
type C chan D
type D int
type S []T
type T int
var U = append(([]*S)(nil), new(T))
type X map[K]V
type K int
type V int
var Z = make(map[K]A)
// close, delete, and panic cannot occur outside of statements
`},
want: map[string][]string{
"A": {"p.B"},
"C": {"p.D"},
"S": {"p.T"},
"U": {"p.S", "p.T"}, // p.T edge could be eliminated
"X": {"p.K", "p.V"},
"Z": {"p.A", "p.K"},
},
},
{
label: "builtin shadowing",
srcs: []string{`package p
var A = new(B)
func new() c
type c int
`},
want: map[string][]string{
"A": {"p.new"},
"new": {"p.c"},
},
},
{
label: "named forwarding",
srcs: []string{`package p
type A B
type B C
type C int
`},
want: map[string][]string{
"A": {"p.B"},
"B": {"p.C"},
},
},
{
label: "aliases",
srcs: []string{`package p
type A = B
type B = C
type C = int
`},
want: map[string][]string{
"A": {"p.B"},
"B": {"p.C"},
},
},
{
label: "array length",
srcs: []string{`package p
import "unsafe"
type A [unsafe.Sizeof(B{C})]int
type A2 [unsafe.Sizeof(B{f:C})]int // use a KeyValueExpr
type B struct{ f int }
var C = 0
type D [unsafe.Sizeof(struct{ f E })]int
type E int
type F [3]G
type G [C]int
`},
want: map[string][]string{
"A": {"p.B"},
"A2": {"p.B"},
"D": {"p.E"},
"F": {"p.G"},
"G": {"p.C"},
},
},
{
label: "imports",
srcs: []string{`package p
import (
"q"
r2 "r"
"s" // note: package name is t
"z"
)
type A struct {
q.Q
r2.R
s.S // invalid ref
z.Z // note: shadowed below
}
type B struct {
r.R // invalid ref
t.T
}
var x int = q.V
var y = q.V.W
type z interface{}
`},
imports: map[string]string{"q": "q", "r": "r", "s": "t", "z": "z"},
want: map[string][]string{
"A": {"p.z", "q.Q", "r.R", "z.Z"},
"B": {"t.T"},
"y": {"q.V"},
},
},
{
label: "import blank",
srcs: []string{`package p
import _ "q"
type A q.Q
`},
imports: map[string]string{"q": "q"},
want: map[string][]string{},
},
{
label: "import dot",
srcs: []string{`package p
import . "q"
type A q.Q // not actually an edge, since q is imported .
type B struct {
C // assumed to be an edge to q
D // resolved to package decl
}
type E error // unexported, therefore must be universe.error
type F Field
var G = Field.X
`, `package p
type D interface{}
`},
imports: map[string]string{"q": "q"},
want: map[string][]string{
"B": {"p.D", "q.C"},
"F": {"q.Field"},
"G": {"q.Field"},
},
},
{
label: "typeparams",
srcs: []string{`package p
type A[T any] struct {
t T
b B
}
type B int
func F1[T any](T, B)
func F2[T C]()(T, B)
type T int
type C any
func F3[T1 ~[]T2, T2 ~[]T3](t1 T1, t2 T2)
type T3 any
`, `package p
func (A[B]) M(C) {}
`},
want: map[string][]string{
"A": {"p.B", "p.C"},
"F1": {"p.B"},
"F2": {"p.B", "p.C"},
"F3": {"p.T3"},
},
go118: true,
},
{
label: "instances",
srcs: []string{`package p
type A[T any] struct{}
type B[T1, T2 any] struct{}
type C A[int]
type D B[int, A[E]]
type E int
`},
want: map[string][]string{
"C": {"p.A"},
"D": {"p.A", "p.B", "p.E"},
},
go118: true,
},
{
label: "duplicate decls",
srcs: []string{`package p
import "a"
type a a.A
type b int
type C a.A
func (C) Foo(x) {} // invalid parameter, but that does not matter
type C b
func (C) Bar(y) {} // invalid parameter, but that does not matter
var x, y int
`},
imports: map[string]string{"a": "a", "b": "b"}, // "b" import should not matter, since it isn't in this file
want: map[string][]string{
"a": {"a.A", "p.a"},
"C": {"a.A", "p.a", "p.b", "p.x", "p.y"},
},
},
{
label: "invalid decls",
srcs: []string{`package p
type A B
func () Foo(B){}
var B
`},
want: map[string][]string{
"A": {"p.B"},
"Foo": {"p.B"},
},
allowErrs: true,
},
}
for _, test := range tests {
t.Run(test.label, func(t *testing.T) {
if test.go118 {
testenv.NeedsGo1Point(t, 18)
}
var pgfs []*source.ParsedGoFile
for i, src := range test.srcs {
uri := span.URI(fmt.Sprintf("file:///%d.go", i))
pgf, _ := cache.ParseGoSrc(ctx, token.NewFileSet(), uri, []byte(src), source.ParseFull)
if !test.allowErrs && pgf.ParseErr != nil {
t.Fatalf("ParseGoSrc(...) returned parse errors: %v", pgf.ParseErr)
}
pgfs = append(pgfs, pgf)
}
imports := make(map[source.ImportPath]*source.Metadata)
for path, m := range test.imports {
imports[source.ImportPath(path)] = &source.Metadata{
ID: source.PackageID(m),
Name: source.PackageName(m),
}
}
index := typerefs.NewPackageIndex()
refs := typerefs.Refs(pgfs, "p", imports, index)
got := make(map[string][]string)
for name, refs := range refs {
var srefs []string
for _, ref := range refs {
id, name := ref.Unpack(index)
srefs = append(srefs, fmt.Sprintf("%s.%s", id, name))
}
sort.Strings(srefs)
got[name] = srefs
}
if diff := cmp.Diff(test.want, got); diff != "" {
t.Errorf("Refs(...) returned unexpected refs (-want +got):\n%s", diff)
}
})
}
}