Skip to content

Commit 0c3a1fe

Browse files
h9jianggopherbot
authored andcommitted
go/ast/inspector: FindByPos returns the first innermost node
Fix golang/go#76872 Change-Id: I35d9c6cb0abfe2d380bf326bdd1e547a3f1abc89 Reviewed-on: https://go-review.googlesource.com/c/tools/+/732521 Auto-Submit: Hongxiang Jiang <hxjiang@golang.org> Reviewed-by: Alan Donovan <adonovan@google.com> LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
1 parent ca281cf commit 0c3a1fe

File tree

3 files changed

+86
-7
lines changed

3 files changed

+86
-7
lines changed

go/ast/inspector/cursor.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -453,6 +453,9 @@ func (c Cursor) FindNode(n ast.Node) (Cursor, bool) {
453453
// rooted at c such that n.Pos() <= start && end <= n.End().
454454
// (For an *ast.File, it uses the bounds n.FileStart-n.FileEnd.)
455455
//
456+
// An empty range (start == end) between two adjacent nodes is
457+
// considered to belong to the first node.
458+
//
456459
// It returns zero if none is found.
457460
// Precondition: start <= end.
458461
//
@@ -501,10 +504,17 @@ func (c Cursor) FindByPos(start, end token.Pos) (Cursor, bool) {
501504
break // disjoint, after; stop
502505
}
503506
}
507+
504508
// Inv: node.{Pos,FileStart} <= start
505509
if end <= nodeEnd {
506510
// node fully contains target range
507511
best = i
512+
513+
// Don't search beyond end of the first match.
514+
// This is important only for an empty range (start=end)
515+
// between two adjoining nodes, which would otherwise
516+
// match both nodes; we want to match only the first.
517+
limit = ev.index
508518
} else if nodeEnd < start {
509519
i = ev.index // disjoint, before; skip forward
510520
}

go/ast/inspector/cursor_test.go

Lines changed: 60 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -225,7 +225,7 @@ func TestCursor_FindNode(t *testing.T) {
225225
inspect := netInspect
226226

227227
// Enumerate all nodes of a particular type,
228-
// then check that FindPos can find them,
228+
// then check that FindByPos can find them,
229229
// starting at the root.
230230
//
231231
// (We use BasicLit because they are numerous.)
@@ -263,15 +263,15 @@ func TestCursor_FindNode(t *testing.T) {
263263
}
264264
}
265265

266-
// TestCursor_FindPos_order ensures that FindPos does not assume files are in Pos order.
267-
func TestCursor_FindPos_order(t *testing.T) {
266+
// TestCursor_FindByPos_order ensures that FindByPos does not assume files are in Pos order.
267+
func TestCursor_FindByPos_order(t *testing.T) {
268268
// Pick an arbitrary decl.
269269
target := netFiles[7].Decls[0]
270270

271271
// Find the target decl by its position.
272272
cur, ok := netInspect.Root().FindByPos(target.Pos(), target.End())
273273
if !ok || cur.Node() != target {
274-
t.Fatalf("unshuffled: FindPos(%T) = (%v, %t)", target, cur, ok)
274+
t.Fatalf("unshuffled: FindByPos(%T) = (%v, %t)", target, cur, ok)
275275
}
276276

277277
// Shuffle the files out of Pos order.
@@ -284,7 +284,7 @@ func TestCursor_FindPos_order(t *testing.T) {
284284
inspect := inspector.New(files)
285285
cur, ok = inspect.Root().FindByPos(target.Pos(), target.End())
286286
if !ok || cur.Node() != target {
287-
t.Fatalf("shuffled: FindPos(%T) = (%v, %t)", target, cur, ok)
287+
t.Fatalf("shuffled: FindByPos(%T) = (%v, %t)", target, cur, ok)
288288
}
289289
}
290290

@@ -379,6 +379,59 @@ func TestCursor_Edge(t *testing.T) {
379379
}
380380
}
381381

382+
// Regression test for mutilple matching nodes in FindByPos (#76872).
383+
func TestCursor_FindByPos_Boundary(t *testing.T) {
384+
// This test verifies that when a cursor position is on the boundary of two
385+
// adjacent nodes (e.g. "foo|("), FindByPos returns the first node
386+
// encountered in traversal order (which is usually the node "to the left").
387+
//
388+
// Note: The source is intentionally unformatted (no space between ')' and
389+
// '{') to ensure the nodes are strictly adjacent at the boundary.
390+
const src = `package p; func foo(a int){}`
391+
var (
392+
fset = token.NewFileSet()
393+
f, _ = parser.ParseFile(fset, "p.go", src, 0)
394+
tokFile = fset.File(f.FileStart)
395+
inspect = inspector.New([]*ast.File{f})
396+
)
397+
398+
d := f.Decls[0].(*ast.FuncDecl)
399+
400+
format := func(pos token.Pos) string {
401+
off := tokFile.Offset(pos)
402+
return fmt.Sprintf("...%s<<>>%s...", src[off-1:off], src[off:off+1])
403+
}
404+
405+
for _, test := range []struct {
406+
name string
407+
pos token.Pos
408+
want ast.Node
409+
}{
410+
{
411+
// "foo|(" Ident
412+
pos: d.Type.Params.Opening,
413+
want: d.Name,
414+
},
415+
{
416+
// ")|{" FieldList
417+
pos: d.Body.Pos(),
418+
want: d.Type.Params,
419+
},
420+
} {
421+
cur, ok := inspect.Root().FindByPos(test.pos, test.pos)
422+
if !ok {
423+
t.Fatalf("FindByPos(%d) %s found nothing", test.pos, format(test.pos))
424+
}
425+
426+
if cur.Node() != test.want {
427+
t.Errorf("FindByPos(%d) %s:\ngot %T (%v)\nwant %T (%v)",
428+
test.pos, format(test.pos),
429+
cur.Node(), cur.Node(),
430+
test.want, test.want)
431+
}
432+
}
433+
}
434+
382435
// Regression test for FuncDecl.Type irregularity in FindByPos (#75997).
383436
func TestCursor_FindByPos(t *testing.T) {
384437
// Observe that the range of FuncType has a hole between
@@ -615,12 +668,12 @@ func BenchmarkCursor_FindNode(b *testing.B) {
615668
})
616669

617670
// This method is about 100x (!) faster than Cursor.Preorder.
618-
b.Run("Cursor.FindPos", func(b *testing.B) {
671+
b.Run("Cursor.FindByPos", func(b *testing.B) {
619672
needleNode := needle.Node()
620673
for b.Loop() {
621674
found, ok := root.FindByPos(needleNode.Pos(), needleNode.End())
622675
if !ok || found != needle {
623-
b.Errorf("FindPos search failed: got %v, want %v", found, needle)
676+
b.Errorf("FindByPos search failed: got %v, want %v", found, needle)
624677
}
625678
}
626679
})
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
This test exercises a references query at a position where an empty selection
2+
might be considered enclosed both by the node before (Ident) and the node after
3+
(FuncType). It ensures that we find the reference of the syntax node before the
4+
cursor.
5+
6+
See https://github.com/golang/go/issues/76872.
7+
8+
-- main.go --
9+
package main
10+
11+
// re"foo()" matches the zero-width position after "foo", not the call parens.
12+
func foo() {} //@ loc(decl, "foo"), refs(re"foo()", decl, call)
13+
14+
func _() {
15+
foo() //@ loc(call, "foo")
16+
}

0 commit comments

Comments
 (0)