go/ast/inspector: FindByPos: fix bug in FuncDecl.Type

The FuncDecl.{Name,Type} fields are an edge case in many
algorithms because their ranges overlap. That breaks
the assumption of FindByPos that the latest enclosing
node in the traversal is the innermost one.
Add logic to deal with this edge case, and a test.

Fixes golang/go#75997

Change-Id: I4ed2021d304d576a0e8667903c1f40e591631e4c
Reviewed-on: https://go-review.googlesource.com/c/tools/+/713920
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Reviewed-by: Cherry Mui <cherryyz@google.com>
diff --git a/go/ast/inspector/cursor.go b/go/ast/inspector/cursor.go
index 7e72d3c..fc9bbc7 100644
--- a/go/ast/inspector/cursor.go
+++ b/go/ast/inspector/cursor.go
@@ -467,7 +467,9 @@
 	// This algorithm could be implemented using c.Inspect,
 	// but it is about 2.5x slower.
 
-	best := int32(-1) // push index of latest (=innermost) node containing range
+	// best is the push-index of the latest (=innermost) node containing range.
+	// (Beware: latest is not always innermost because FuncDecl.{Name,Type} overlap.)
+	best := int32(-1)
 	for i, limit := c.indices(); i < limit; i++ {
 		ev := events[i]
 		if ev.index > i { // push?
@@ -481,6 +483,19 @@
 					continue
 				}
 			} else {
+				// Edge case: FuncDecl.Name and .Type overlap:
+				// Don't update best from Name to FuncDecl.Type.
+				//
+				// The condition can be read as:
+				// - n is FuncType
+				// - n.parent is FuncDecl
+				// - best is strictly beneath the FuncDecl
+				if ev.typ == 1<<nFuncType &&
+					events[ev.parent].typ == 1<<nFuncDecl &&
+					best > ev.parent {
+					continue
+				}
+
 				nodeEnd = n.End()
 				if n.Pos() > start {
 					break // disjoint, after; stop
diff --git a/go/ast/inspector/cursor_test.go b/go/ast/inspector/cursor_test.go
index c8180d5..b5baada 100644
--- a/go/ast/inspector/cursor_test.go
+++ b/go/ast/inspector/cursor_test.go
@@ -379,6 +379,116 @@
 	}
 }
 
+// Regression test for FuncDecl.Type irregularity in FindByPos (#75997).
+func TestCursor_FindByPos(t *testing.T) {
+	// Observe that the range of FuncType has a hole between
+	// the "func" token and the start of Type.Params.
+	// The hole contains FuncDecl.{Recv,Name}.
+	//
+	//                      ~~~~~~~~~~~~FuncDecl~~~~~~~~~~~~~~~~~~~~~~~~~
+	//                           ~Recv~ ~Name~
+	//                      ~~~~--------------~~~~FuncType~~~~~~
+	//                                        ~Params~ ~Results~
+	const src = `package a; func (recv) method(params) (results) { body }`
+	var (
+		fset    = token.NewFileSet()
+		f, _    = parser.ParseFile(fset, "a.go", src, 0) // ignore parse errors
+		tokFile = fset.File(f.FileStart)
+		inspect = inspector.New([]*ast.File{f})
+	)
+	format := func(start, end token.Pos) string {
+		var (
+			startOffset = tokFile.Offset(start)
+			endOffset   = tokFile.Offset(end)
+		)
+		return fmt.Sprintf("%s<<%s>>%s", src[:startOffset], src[startOffset:endOffset], src[endOffset:])
+	}
+
+	d := f.Decls[0].(*ast.FuncDecl)
+
+	// Each test case specifies a [pos-end) range for
+	// FindByPos and the syntax node it should find.
+	for _, test := range []struct {
+		start, end token.Pos
+		want       ast.Node
+	}{
+		// pure subtrees
+		{d.Pos(), d.End(), d},                // decl
+		{d.Recv.Pos(), d.Recv.End(), d.Recv}, // recv
+		{d.Name.Pos(), d.Name.End(), d.Name}, // name
+		// (A FuncDecl can't have both Recv and TypeParams, so skip this one.)
+		// {d.Type.TypeParams.Pos(), d.Type.TypeParams.End(), d.Type.TypeParams},
+		{d.Type.Params.Pos(), d.Type.Params.End(), d.Type.Params},    // params
+		{d.Type.Results.Pos(), d.Type.Results.End(), d.Type.Results}, // results
+		{d.Body.Pos(), d.Body.End(), d.Body},                         // body
+
+		// single tokens
+		{
+			// "func"
+			d.Type.Func, d.Type.Func + 4,
+			d, // arguably this should be d.Type
+		},
+		{
+			// "(" FieldList
+			d.Recv.Pos(), d.Recv.Pos() + 1,
+			d.Recv,
+		},
+		{
+			// "recv" Ident
+			d.Recv.List[0].Pos(), d.Recv.List[0].Pos() + 1,
+			d.Recv.List[0].Type,
+		},
+		{
+			// "name" Ident
+			d.Name.Pos(), d.Name.Pos() + 1,
+			d.Name,
+		},
+		{
+			// "(" FieldList
+			d.Type.Params.Pos(), d.Type.Params.Pos() + 1,
+			d.Type.Params,
+		},
+		{
+			// "params" Ident
+			d.Type.Params.List[0].Pos(), d.Type.Params.List[0].Pos() + 1,
+			d.Type.Params.List[0].Type,
+		},
+		{
+			// "(" FieldList
+			d.Type.Results.Pos(), d.Type.Results.Pos() + 1,
+			d.Type.Results,
+		},
+		{
+			// "results" Ident
+			d.Type.Results.List[0].Pos(), d.Type.Results.List[0].Pos() + 1,
+			d.Type.Results.List[0].Type,
+		},
+		{
+			// "{" BlockStmt
+			d.Body.Pos(), d.Body.Pos() + 1,
+			d.Body,
+		},
+		{
+			// "body" Ident
+			d.Body.List[0].Pos(), d.Body.List[0].Pos() + 1,
+			d.Body.List[0].(*ast.ExprStmt).X,
+		},
+	} {
+		cur, ok := inspect.Root().FindByPos(test.start, test.end)
+		if !ok || cur.Node() == nil {
+			t.Errorf("%s: FindByPos failed", format(test.start, test.end))
+			continue
+		}
+		got := cur.Node()
+		if got != test.want {
+			t.Errorf("FindByPos:\ninput:\t%s\ngot:\t%s (%T)\nwant:\t%s (%T)",
+				format(test.start, test.end),
+				format(got.Pos(), got.End()), got,
+				format(test.want.Pos(), test.want.End()), test.want)
+		}
+	}
+}
+
 func is[T any](x any) bool {
 	_, ok := x.(T)
 	return ok