Skip to content

Commit 6efe0f4

Browse files
adonovangopherbot
authored andcommitted
internal/astutil/cursor: Cursor.Ancestors iterator
This CL adds an iterator for the strict ancestors (transitive parents) of the current node. It accepts a type filter. Also, use it in two places. This may slow down Cursor.Stack slightly, but see comments in benchmark for justification. The basic Cursor traversal is still competitive so long as Stack or Ancestors are called sparingly. BenchmarkInspectCalls/Preorder-8 12069 106995 ns/op BenchmarkInspectCalls/WithStack-8 7447 153992 ns/op BenchmarkInspectCalls/CursorStack-8 2545 460433 ns/op (was 178907: 2.6x increase) BenchmarkInspectCalls/Cursor-8 12225 99472 ns/op BenchmarkInspectCalls/CursorAncestors-8 3249 350503 ns/op (=3x WithStack) Change-Id: I79941423888028a622b20b7ab63b37f8435dce33 Reviewed-on: https://go-review.googlesource.com/c/tools/+/641435 Auto-Submit: Alan Donovan <[email protected]> LUCI-TryBot-Result: Go LUCI <[email protected]> Reviewed-by: Robert Findley <[email protected]>
1 parent df3de6a commit 6efe0f4

File tree

4 files changed

+74
-32
lines changed

4 files changed

+74
-32
lines changed

gopls/internal/analysis/hostport/hostport.go

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -164,8 +164,7 @@ func run(pass *analysis.Pass) (any, error) {
164164
// Search for decl of addrVar within common ancestor of addrVar and Dial call.
165165
if addrVar, ok := info.Uses[address].(*types.Var); ok {
166166
pos := addrVar.Pos()
167-
// TODO(adonovan): use Cursor.Ancestors iterator when available.
168-
for _, curAncestor := range curCall.Stack(nil) {
167+
for curAncestor := range curCall.Ancestors() {
169168
if curIdent, ok := curAncestor.FindPos(pos, pos); ok {
170169
// curIdent is the declaring ast.Ident of addr.
171170
switch parent := curIdent.Parent().Node().(type) {

gopls/internal/analysis/modernize/bloop.go

Lines changed: 7 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -36,12 +36,12 @@ func bloop(pass *analysis.Pass) {
3636
// edits computes the text edits for a matched for/range loop
3737
// at the specified cursor. b is the *testing.B value, and
3838
// (start, end) is the portion using b.N to delete.
39-
edits := func(cur cursor.Cursor, b ast.Expr, start, end token.Pos) (edits []analysis.TextEdit) {
39+
edits := func(curLoop cursor.Cursor, b ast.Expr, start, end token.Pos) (edits []analysis.TextEdit) {
40+
curFn, _ := enclosingFunc(curLoop)
4041
// Within the same function, delete all calls to
4142
// b.{Start,Stop,Timer} that precede the loop.
4243
filter := []ast.Node{(*ast.ExprStmt)(nil), (*ast.FuncLit)(nil)}
43-
fn, _ := enclosingFunc(cur)
44-
fn.Inspect(filter, func(cur cursor.Cursor, push bool) (descend bool) {
44+
curFn.Inspect(filter, func(cur cursor.Cursor, push bool) (descend bool) {
4545
if push {
4646
node := cur.Node()
4747
if is[*ast.FuncLit](node) {
@@ -162,22 +162,10 @@ func uses(info *types.Info, cur cursor.Cursor, obj types.Object) bool {
162162
}
163163

164164
// enclosingFunc returns the cursor for the innermost Func{Decl,Lit}
165-
// that encloses (or is) c, if any.
166-
//
167-
// TODO(adonovan): consider adding:
168-
//
169-
// func (Cursor) AnyEnclosing(filter ...ast.Node) (Cursor bool)
170-
// func (Cursor) Enclosing[N ast.Node]() (Cursor, bool)
171-
//
172-
// See comments at [cursor.Cursor.Stack].
165+
// that encloses c, if any.
173166
func enclosingFunc(c cursor.Cursor) (cursor.Cursor, bool) {
174-
for {
175-
switch c.Node().(type) {
176-
case *ast.FuncLit, *ast.FuncDecl:
177-
return c, true
178-
case nil:
179-
return cursor.Cursor{}, false
180-
}
181-
c = c.Parent()
167+
for curAncestor := range c.Ancestors((*ast.FuncLit)(nil), (*ast.FuncDecl)(nil)) {
168+
return curAncestor, true
182169
}
170+
return cursor.Cursor{}, false
183171
}

internal/astutil/cursor/cursor.go

Lines changed: 27 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -157,13 +157,6 @@ func (c Cursor) Inspect(types []ast.Node, f func(c Cursor, push bool) (descend b
157157
// must be empty.
158158
//
159159
// Stack must not be called on the Root node.
160-
//
161-
// TODO(adonovan): perhaps this should be replaced by:
162-
//
163-
// func (Cursor) Ancestors(filter []ast.Node) iter.Seq[Cursor]
164-
//
165-
// returning a filtering iterator up the parent chain.
166-
// This finesses the question of allocation entirely.
167160
func (c Cursor) Stack(stack []Cursor) []Cursor {
168161
if len(stack) > 0 {
169162
panic("stack is non-empty")
@@ -172,14 +165,37 @@ func (c Cursor) Stack(stack []Cursor) []Cursor {
172165
panic("Cursor.Stack called on Root node")
173166
}
174167

175-
events := c.events()
176-
for i := c.index; i >= 0; i = events[i].parent {
177-
stack = append(stack, Cursor{c.in, i})
178-
}
168+
stack = append(stack, c)
169+
stack = slices.AppendSeq(stack, c.Ancestors())
179170
slices.Reverse(stack)
180171
return stack
181172
}
182173

174+
// Ancestors returns an iterator over the ancestors of the current
175+
// node, starting with [Cursor.Parent].
176+
//
177+
// Ancestors must not be called on the Root node (whose [Cursor.Node] returns nil).
178+
//
179+
// The types argument, if non-empty, enables type-based filtering of
180+
// events: the sequence includes only ancestors whose type matches an
181+
// element of the types slice.
182+
func (c Cursor) Ancestors(types ...ast.Node) iter.Seq[Cursor] {
183+
if c.index < 0 {
184+
panic("Cursor.Ancestors called on Root node")
185+
}
186+
187+
mask := maskOf(types)
188+
189+
return func(yield func(Cursor) bool) {
190+
events := c.events()
191+
for i := events[c.index].parent; i >= 0; i = events[i].parent {
192+
if events[i].typ&mask != 0 && !yield(Cursor{c.in, i}) {
193+
break
194+
}
195+
}
196+
}
197+
}
198+
183199
// Parent returns the parent of the current node.
184200
//
185201
// Parent must not be called on the Root node (whose [Cursor.Node] returns nil).

internal/astutil/cursor/cursor_test.go

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import (
1515
"iter"
1616
"log"
1717
"path/filepath"
18+
"reflect"
1819
"slices"
1920
"strings"
2021
"testing"
@@ -152,6 +153,13 @@ func g() {
152153
if got, want := fmt.Sprint(stack), "[*ast.File *ast.FuncDecl *ast.BlockStmt *ast.ExprStmt *ast.CallExpr]"; got != want {
153154
t.Errorf("curCall.Stack() = %q, want %q", got, want)
154155
}
156+
157+
// Ancestors = Reverse(Stack[:last]).
158+
stack = stack[:len(stack)-1]
159+
slices.Reverse(stack)
160+
if got, want := slices.Collect(curCall.Ancestors()), stack; !reflect.DeepEqual(got, want) {
161+
t.Errorf("Ancestors = %v, Reverse(Stack - last element) = %v", got, want)
162+
}
155163
}
156164

157165
// nested Inspect traversal
@@ -381,6 +389,15 @@ func BenchmarkInspectCalls(b *testing.B) {
381389
// And if the calls to Stack are very selective,
382390
// or are replaced by 2 calls to Parent, it runs
383391
// 27% faster than WithStack.
392+
//
393+
// But the purpose of inspect.WithStack is not to obtain the
394+
// stack on every node, but to perform a traversal in which it
395+
// one as the _option_ to access the stack if it should be
396+
// needed, but the need is rare and usually only for a small
397+
// portion. Arguably, because Cursor traversals always
398+
// provide, at no extra cost, the option to access the
399+
// complete stack, the right comparison is the plain Cursor
400+
// benchmark below.
384401
b.Run("CursorStack", func(b *testing.B) {
385402
var ncalls int
386403
for range b.N {
@@ -392,6 +409,28 @@ func BenchmarkInspectCalls(b *testing.B) {
392409
}
393410
}
394411
})
412+
413+
b.Run("Cursor", func(b *testing.B) {
414+
var ncalls int
415+
for range b.N {
416+
for cur := range cursor.Root(inspect).Preorder(callExprs...) {
417+
_ = cur.Node().(*ast.CallExpr)
418+
ncalls++
419+
}
420+
}
421+
})
422+
423+
b.Run("CursorAncestors", func(b *testing.B) {
424+
var ncalls int
425+
for range b.N {
426+
for cur := range cursor.Root(inspect).Preorder(callExprs...) {
427+
_ = cur.Node().(*ast.CallExpr)
428+
for range cur.Ancestors() {
429+
}
430+
ncalls++
431+
}
432+
}
433+
})
395434
}
396435

397436
// This benchmark compares methods for finding a known node in a tree.

0 commit comments

Comments
 (0)