internal/lsp/source: add a unit test for searchForEnclosing The logic to resolve the enclosing type for an identifier is somewhat tricky. Add a unit test to exercise a few edge cases. This would probably be easier to read and write using a hybrid approach that extracts markers from the source. This test uncovered a bug, that on the SelectorExpr branch we were accidentally returning a nil *Named types.Type, rather than a nil types.Type. Change-Id: I43e096f51999b2a6e109c09d3805ad70a4780398 Reviewed-on: https://go-review.googlesource.com/c/tools/+/244841 Reviewed-by: Heschi Kreinick <heschi@google.com>
diff --git a/internal/lsp/source/identifier.go b/internal/lsp/source/identifier.go index acb3bfd..d4f9591 100644 --- a/internal/lsp/source/identifier.go +++ b/internal/lsp/source/identifier.go
@@ -135,7 +135,7 @@ qf: qf, pkg: pkg, ident: ident, - enclosing: searchForEnclosing(pkg, path), + enclosing: searchForEnclosing(pkg.GetTypesInfo(), path), } var wasEmbeddedField bool @@ -226,15 +226,15 @@ return result, nil } -func searchForEnclosing(pkg Package, path []ast.Node) types.Type { +func searchForEnclosing(info *types.Info, path []ast.Node) types.Type { for _, n := range path { switch n := n.(type) { case *ast.SelectorExpr: - if sel, ok := pkg.GetTypesInfo().Selections[n]; ok { + if sel, ok := info.Selections[n]; ok { recv := deref(sel.Recv()) // Keep track of the last exported type seen. - var exported *types.Named + var exported types.Type if named, ok := recv.(*types.Named); ok && named.Obj().Exported() { exported = named } @@ -251,12 +251,12 @@ return exported } case *ast.CompositeLit: - if t, ok := pkg.GetTypesInfo().Types[n]; ok { + if t, ok := info.Types[n]; ok { return t.Type } case *ast.TypeSpec: if _, ok := n.Type.(*ast.StructType); ok { - if t, ok := pkg.GetTypesInfo().Defs[n.Name]; ok { + if t, ok := info.Defs[n.Name]; ok { return t.Type() } }
diff --git a/internal/lsp/source/identifier_test.go b/internal/lsp/source/identifier_test.go new file mode 100644 index 0000000..f07e401 --- /dev/null +++ b/internal/lsp/source/identifier_test.go
@@ -0,0 +1,128 @@ +// Copyright 2020 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 source + +import ( + "bytes" + "go/ast" + "go/parser" + "go/token" + "go/types" + "testing" +) + +func TestSearchForEnclosing(t *testing.T) { + tests := []struct { + desc string + // For convenience, consider the first occurence of the identifier "X" in + // src. + src string + // By convention, "" means no type found. + wantTypeName string + }{ + { + desc: "self enclosing", + src: `package a; type X struct {}`, + wantTypeName: "X", + }, + { + // TODO(rFindley): is this correct, or do we want to resolve I2 here? + desc: "embedded interface in interface", + src: `package a; var y = i1.X; type i1 interface {I2}; type I2 interface{X()}`, + wantTypeName: "", + }, + { + desc: "embedded interface in struct", + src: `package a; var y = t.X; type t struct {I}; type I interface{X()}`, + wantTypeName: "I", + }, + { + desc: "double embedding", + src: `package a; var y = t1.X; type t1 struct {t2}; type t2 struct {I}; type I interface{X()}`, + wantTypeName: "I", + }, + { + desc: "struct field", + src: `package a; type T struct { X int }`, + wantTypeName: "T", + }, + { + desc: "nested struct field", + src: `package a; type T struct { E struct { X int } }`, + wantTypeName: "T", + }, + { + desc: "slice entry", + src: `package a; type T []int; var S = T{X}; var X int = 2`, + wantTypeName: "T", + }, + { + desc: "struct pointer literal", + src: `package a; type T struct {i int}; var L = &T{X}; const X = 2`, + wantTypeName: "T", + }, + } + + for _, test := range tests { + test := test + t.Run(test.desc, func(t *testing.T) { + fset := token.NewFileSet() + file, err := parser.ParseFile(fset, "a.go", test.src, parser.AllErrors) + if err != nil { + t.Fatal(err) + } + column := 1 + bytes.IndexRune([]byte(test.src), 'X') + pos := posAt(1, column, fset, "a.go") + path := pathEnclosingObjNode(file, pos) + if path == nil { + t.Fatalf("no ident found at (1, %d)", column) + } + info := newInfo() + if _, err = (*types.Config)(nil).Check("p", fset, []*ast.File{file}, info); err != nil { + t.Fatal(err) + } + typ := searchForEnclosing(info, path) + if typ == nil { + if test.wantTypeName != "" { + t.Errorf("searchForEnclosing(...) = <nil>, want %q", test.wantTypeName) + } + return + } + if got := typ.(*types.Named).Obj().Name(); got != test.wantTypeName { + t.Errorf("searchForEnclosing(...) = %q, want %q", got, test.wantTypeName) + } + }) + } +} + +// posAt returns the token.Pos corresponding to the 1-based (line, column) +// coordinates in the file fname of fset. +func posAt(line, column int, fset *token.FileSet, fname string) token.Pos { + var tok *token.File + fset.Iterate(func(f *token.File) bool { + if f.Name() == fname { + tok = f + return false + } + return true + }) + if tok == nil { + return token.NoPos + } + start := tok.LineStart(line) + return start + token.Pos(column-1) +} + +// newInfo returns a types.Info with all maps populated. +func newInfo() *types.Info { + return &types.Info{ + Types: make(map[ast.Expr]types.TypeAndValue), + Defs: make(map[*ast.Ident]types.Object), + Uses: make(map[*ast.Ident]types.Object), + Implicits: make(map[ast.Node]types.Object), + Selections: make(map[*ast.SelectorExpr]*types.Selection), + Scopes: make(map[ast.Node]*types.Scope), + } +}