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),
+	}
+}