]> Cypherpunks repositories - gostls13.git/commitdiff
go/ast: add PreorderStack, a variant of Inspect that builds a stack
authorAlan Donovan <adonovan@google.com>
Wed, 14 May 2025 20:15:40 +0000 (16:15 -0400)
committerGopher Robot <gobot@golang.org>
Mon, 19 May 2025 17:54:45 +0000 (10:54 -0700)
+ doc, test, relnote

Fixes #73319

Change-Id: Ib7c9d0d7107cd62dc7f09120dfb475c4a469ddc9
Reviewed-on: https://go-review.googlesource.com/c/go/+/672696
Reviewed-by: Robert Findley <rfindley@google.com>
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Auto-Submit: Alan Donovan <adonovan@google.com>

api/next/73319.txt [new file with mode: 0644]
doc/next/6-stdlib/99-minor/go/ast/73319.md [new file with mode: 0644]
src/go/ast/walk.go
src/go/ast/walk_test.go

diff --git a/api/next/73319.txt b/api/next/73319.txt
new file mode 100644 (file)
index 0000000..39a3ece
--- /dev/null
@@ -0,0 +1 @@
+pkg go/ast, func PreorderStack(Node, []Node, func(Node, []Node) bool) #73319
diff --git a/doc/next/6-stdlib/99-minor/go/ast/73319.md b/doc/next/6-stdlib/99-minor/go/ast/73319.md
new file mode 100644 (file)
index 0000000..b99e20e
--- /dev/null
@@ -0,0 +1,4 @@
+The new [PreorderStack] function, like [Inspect], traverses a syntax
+tree and provides control over descent into subtrees, but as a
+convenience it also provides the stack of enclosing nodes at each
+point.
index ec9a8901c4aa8f9808587d3868cd71d150c26dad..24cdc60d73d5f2d626699b5f54c2ac4ecfebd59f 100644 (file)
@@ -368,6 +368,11 @@ func (f inspector) Visit(node Node) Visitor {
 // f(node); node must not be nil. If f returns true, Inspect invokes f
 // recursively for each of the non-nil children of node, followed by a
 // call of f(nil).
+//
+// In many cases it may be more convenient to use [Preorder], which
+// returns an iterator over the sqeuence of nodes, or [PreorderStack],
+// which (like [Inspect]) provides control over descent into subtrees,
+// but additionally reports the stack of enclosing nodes.
 func Inspect(node Node, f func(Node) bool) {
        Walk(inspector(f), node)
 }
@@ -376,7 +381,8 @@ func Inspect(node Node, f func(Node) bool) {
 // beneath (and including) the specified root, in depth-first
 // preorder.
 //
-// For greater control over the traversal of each subtree, use [Inspect].
+// For greater control over the traversal of each subtree, use
+// [Inspect] or [PreorderStack].
 func Preorder(root Node) iter.Seq[Node] {
        return func(yield func(Node) bool) {
                ok := true
@@ -389,3 +395,34 @@ func Preorder(root Node) iter.Seq[Node] {
                })
        }
 }
+
+// PreorderStack traverses the tree rooted at root,
+// calling f before visiting each node.
+//
+// Each call to f provides the current node and traversal stack,
+// consisting of the original value of stack appended with all nodes
+// from root to n, excluding n itself. (This design allows calls
+// to PreorderStack to be nested without double counting.)
+//
+// If f returns false, the traversal skips over that subtree. Unlike
+// [Inspect], no second call to f is made after visiting node n.
+// (In practice, the second call is nearly always used only to pop the
+// stack, and it is surprisingly tricky to do this correctly.)
+func PreorderStack(root Node, stack []Node, f func(n Node, stack []Node) bool) {
+       before := len(stack)
+       Inspect(root, func(n Node) bool {
+               if n != nil {
+                       if !f(n, stack) {
+                               // Do not push, as there will be no corresponding pop.
+                               return false
+                       }
+                       stack = append(stack, n) // push
+               } else {
+                       stack = stack[:len(stack)-1] // pop
+               }
+               return true
+       })
+       if len(stack) != before {
+               panic("push/pop mismatch")
+       }
+}
index b8b4f958ec762f0d055095c13e4bb6afceeeac19..172b2e3f5dd2698e68e3597c208936f1ce1a197c 100644 (file)
@@ -8,10 +8,13 @@ import (
        "go/ast"
        "go/parser"
        "go/token"
+       "reflect"
+       "slices"
+       "strings"
        "testing"
 )
 
-func TestPreorderBreak(t *testing.T) {
+func TestPreorder_Break(t *testing.T) {
        // This test checks that Preorder correctly handles a break statement while
        // in the middle of walking a node. Previously, incorrect handling of the
        // boolean returned by the yield function resulted in the iterator calling
@@ -31,3 +34,54 @@ func TestPreorderBreak(t *testing.T) {
                }
        }
 }
+
+func TestPreorderStack(t *testing.T) {
+       const src = `package a
+func f() {
+       print("hello")
+}
+func g() {
+       print("goodbye")
+       panic("oops")
+}
+`
+       fset := token.NewFileSet()
+       f, _ := parser.ParseFile(fset, "a.go", src, 0)
+
+       str := func(n ast.Node) string {
+               return strings.TrimPrefix(reflect.TypeOf(n).String(), "*ast.")
+       }
+
+       var events []string
+       var gotStack []string
+       ast.PreorderStack(f, nil, func(n ast.Node, stack []ast.Node) bool {
+               events = append(events, str(n))
+               if decl, ok := n.(*ast.FuncDecl); ok && decl.Name.Name == "f" {
+                       return false // skip subtree of f()
+               }
+               if lit, ok := n.(*ast.BasicLit); ok && lit.Value == `"oops"` {
+                       for _, n := range stack {
+                               gotStack = append(gotStack, str(n))
+                       }
+               }
+               return true
+       })
+
+       // Check sequence of events.
+       wantEvents := []string{
+               "File", "Ident", // package a
+               "FuncDecl",                                                // func f()  [pruned]
+               "FuncDecl", "Ident", "FuncType", "FieldList", "BlockStmt", // func g()
+               "ExprStmt", "CallExpr", "Ident", "BasicLit", // print...
+               "ExprStmt", "CallExpr", "Ident", "BasicLit", // panic...
+       }
+       if !slices.Equal(events, wantEvents) {
+               t.Errorf("PreorderStack events:\ngot:  %s\nwant: %s", events, wantEvents)
+       }
+
+       // Check captured stack.
+       wantStack := []string{"File", "FuncDecl", "BlockStmt", "ExprStmt", "CallExpr"}
+       if !slices.Equal(gotStack, wantStack) {
+               t.Errorf("PreorderStack stack:\ngot:  %s\nwant: %s", gotStack, wantStack)
+       }
+}