blob: a703cdfcf9092e28f421f81b8314dcd6f4130133 [file] [log] [blame]
// Copyright 2018 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 inspector provides helper functions for traversal over the
// syntax trees of a package, including node filtering by type, and
// materialization of the traversal stack.
//
// During construction, the inspector does a complete traversal and
// builds a list of push/pop events and their node type. Subsequent
// method calls that request a traversal scan this list, rather than walk
// the AST, and perform type filtering using efficient bit sets.
// This representation is sometimes called a "balanced parenthesis tree."
//
// Experiments suggest the inspector's traversals are about 2.5x faster
// than [ast.Inspect], but it may take around 5 traversals for this
// benefit to amortize the inspector's construction cost.
// If efficiency is the primary concern, do not use Inspector for
// one-off traversals.
//
// The [Cursor] type provides a more flexible API for efficient
// navigation of syntax trees in all four "cardinal directions". For
// example, traversals may be nested, so you can find each node of
// type A and then search within it for nodes of type B. Or you can
// traverse from a node to its immediate neighbors: its parent, its
// previous and next sibling, or its first and last child. We
// recommend using methods of Cursor in preference to Inspector where
// possible.
package inspector
// There are four orthogonal features in a traversal:
// 1 type filtering
// 2 pruning
// 3 postorder calls to f
// 4 stack
// Rather than offer all of them in the API,
// only a few combinations are exposed:
// - Preorder is the fastest and has fewest features,
// but is the most commonly needed traversal.
// - Nodes and WithStack both provide pruning and postorder calls,
// even though few clients need it, because supporting two versions
// is not justified.
// More combinations could be supported by expressing them as
// wrappers around a more generic traversal, but this was measured
// and found to degrade performance significantly (30%).
import (
"go/ast"
"golang.org/x/tools/go/ast/edge"
)
// An Inspector provides methods for inspecting
// (traversing) the syntax trees of a package.
type Inspector struct {
events []event
}
func packEdgeKindAndIndex(ek edge.Kind, index int) int32 {
return int32(uint32(index+1)<<7 | uint32(ek))
}
// unpackEdgeKindAndIndex unpacks the edge kind and edge index (within
// an []ast.Node slice) from the parent field of a pop event.
func unpackEdgeKindAndIndex(x int32) (edge.Kind, int) {
// The "parent" field of a pop node holds the
// edge Kind in the lower 7 bits and the index+1
// in the upper 25.
return edge.Kind(x & 0x7f), int(x>>7) - 1
}
// New returns an Inspector for the specified syntax trees.
func New(files []*ast.File) *Inspector {
return &Inspector{traverse(files)}
}
// An event represents a push or a pop
// of an ast.Node during a traversal.
type event struct {
node ast.Node
typ uint64 // typeOf(node) on push event, or union of typ strictly between push and pop events on pop events
index int32 // index of corresponding push or pop event
parent int32 // index of parent's push node (push nodes only), or packed edge kind/index (pop nodes only)
}
// TODO: Experiment with storing only the second word of event.node (unsafe.Pointer).
// Type can be recovered from the sole bit in typ.
// [Tried this, wasn't faster. --adonovan]
// Preorder visits all the nodes of the files supplied to New in
// depth-first order. It calls f(n) for each node n before it visits
// n's children.
//
// The complete traversal sequence is determined by [ast.Inspect].
// The types argument, if non-empty, enables type-based filtering of
// events. The function f is called only for nodes whose type
// matches an element of the types slice.
//
// The [Cursor.Preorder] method provides a richer alternative interface.
// Example:
//
// for c := range in.Root().Preorder(types) { ... }
func (in *Inspector) Preorder(types []ast.Node, f func(ast.Node)) {
// Because it avoids postorder calls to f, and the pruning
// check, Preorder is almost twice as fast as Nodes. The two
// features seem to contribute similar slowdowns (~1.4x each).
// This function is equivalent to the PreorderSeq call below,
// but to avoid the additional dynamic call (which adds 13-35%
// to the benchmarks), we expand it out.
//
// in.PreorderSeq(types...)(func(n ast.Node) bool {
// f(n)
// return true
// })
mask := maskOf(types)
for i := int32(0); i < int32(len(in.events)); {
ev := in.events[i]
if ev.index > i {
// push
if ev.typ&mask != 0 {
f(ev.node)
}
pop := ev.index
if in.events[pop].typ&mask == 0 {
// Subtrees do not contain types: skip them and pop.
i = pop + 1
continue
}
}
i++
}
}
// Nodes visits the nodes of the files supplied to New in depth-first
// order. It calls f(n, true) for each node n before it visits n's
// children. If f returns true, Nodes invokes f recursively for each
// of the non-nil children of the node, followed by a call of
// f(n, false).
//
// The complete traversal sequence is determined by [ast.Inspect].
// The types argument, if non-empty, enables type-based filtering of
// events. The function f if is called only for nodes whose type
// matches an element of the types slice.
//
// The [Cursor.Inspect] method provides a richer alternative interface.
// Example:
//
// in.Root().Inspect(types, func(c Cursor) bool {
// ...
// return true
// }
func (in *Inspector) Nodes(types []ast.Node, f func(n ast.Node, push bool) (proceed bool)) {
mask := maskOf(types)
for i := int32(0); i < int32(len(in.events)); {
ev := in.events[i]
if ev.index > i {
// push
pop := ev.index
if ev.typ&mask != 0 {
if !f(ev.node, true) {
i = pop + 1 // jump to corresponding pop + 1
continue
}
}
if in.events[pop].typ&mask == 0 {
// Subtrees do not contain types: skip them.
i = pop
continue
}
} else {
// pop
push := ev.index
if in.events[push].typ&mask != 0 {
f(ev.node, false)
}
}
i++
}
}
// WithStack visits nodes in a similar manner to Nodes, but it
// supplies each call to f an additional argument, the current
// traversal stack. The stack's first element is the outermost node,
// an *ast.File; its last is the innermost, n.
//
// The [Cursor.Inspect] method provides a richer alternative interface.
// Example:
//
// in.Root().Inspect(types, func(c Cursor) bool {
// stack := slices.Collect(c.Enclosing())
// ...
// return true
// })
func (in *Inspector) WithStack(types []ast.Node, f func(n ast.Node, push bool, stack []ast.Node) (proceed bool)) {
mask := maskOf(types)
var stack []ast.Node
for i := int32(0); i < int32(len(in.events)); {
ev := in.events[i]
if ev.index > i {
// push
pop := ev.index
stack = append(stack, ev.node)
if ev.typ&mask != 0 {
if !f(ev.node, true, stack) {
i = pop + 1
stack = stack[:len(stack)-1]
continue
}
}
if in.events[pop].typ&mask == 0 {
// Subtrees does not contain types: skip them.
i = pop
continue
}
} else {
// pop
push := ev.index
if in.events[push].typ&mask != 0 {
f(ev.node, false, stack)
}
stack = stack[:len(stack)-1]
}
i++
}
}
// traverse builds the table of events representing a traversal.
func traverse(files []*ast.File) []event {
// Preallocate approximate number of events
// based on source file extent of the declarations.
// (We use End-Pos not FileStart-FileEnd to neglect
// the effect of long doc comments.)
// This makes traverse faster by 4x (!).
var extent int
for _, f := range files {
extent += int(f.End() - f.Pos())
}
// This estimate is based on the net/http package.
capacity := min(extent*33/100, 1e6) // impose some reasonable maximum (1M)
v := &visitor{
events: make([]event, 0, capacity),
stack: []item{{index: -1}}, // include an extra event so file nodes have a parent
}
for _, file := range files {
walk(v, edge.Invalid, -1, file)
}
return v.events
}
type visitor struct {
events []event
stack []item
}
type item struct {
index int32 // index of current node's push event
parentIndex int32 // index of parent node's push event
typAccum uint64 // accumulated type bits of current node's descendants
edgeKindAndIndex int32 // edge.Kind and index, bit packed
}
func (v *visitor) push(ek edge.Kind, eindex int, node ast.Node) {
var (
index = int32(len(v.events))
parentIndex = v.stack[len(v.stack)-1].index
)
v.events = append(v.events, event{
node: node,
parent: parentIndex,
typ: typeOf(node),
index: 0, // (pop index is set later by visitor.pop)
})
v.stack = append(v.stack, item{
index: index,
parentIndex: parentIndex,
edgeKindAndIndex: packEdgeKindAndIndex(ek, eindex),
})
// 2B nodes ought to be enough for anyone!
if int32(len(v.events)) < 0 {
panic("event index exceeded int32")
}
// 32M elements in an []ast.Node ought to be enough for anyone!
if ek2, eindex2 := unpackEdgeKindAndIndex(packEdgeKindAndIndex(ek, eindex)); ek2 != ek || eindex2 != eindex {
panic("Node slice index exceeded uint25")
}
}
func (v *visitor) pop(node ast.Node) {
top := len(v.stack) - 1
current := v.stack[top]
push := &v.events[current.index]
parent := &v.stack[top-1]
push.index = int32(len(v.events)) // make push event refer to pop
parent.typAccum |= current.typAccum | push.typ // accumulate type bits into parent
v.stack = v.stack[:top]
v.events = append(v.events, event{
node: node,
typ: current.typAccum,
index: current.index,
parent: current.edgeKindAndIndex, // see [unpackEdgeKindAndIndex]
})
}