|
| 1 | +// Copyright 2018 The Go Authors. All rights reserved. |
| 2 | +// Use of this source code is governed by a BSD-style |
| 3 | +// license that can be found in the LICENSE file. |
| 4 | + |
| 5 | +// Package inspector provides helper functions for traversal over the |
| 6 | +// syntax trees of a package, including node filtering by type, and |
| 7 | +// materialization of the traversal stack. |
| 8 | +// |
| 9 | +// During construction, the inspector does a complete traversal and |
| 10 | +// builds a list of push/pop events and their node type. Subsequent |
| 11 | +// method calls that request a traversal scan this list, rather than walk |
| 12 | +// the AST, and perform type filtering using efficient bit sets. |
| 13 | +// |
| 14 | +// Experiments suggest the inspector's traversals are about 2.5x faster |
| 15 | +// than ast.Inspect, but it may take around 5 traversals for this |
| 16 | +// benefit to amortize the inspector's construction cost. |
| 17 | +// If efficiency is the primary concern, do not use use Inspector for |
| 18 | +// one-off traversals. |
| 19 | +package inspector |
| 20 | + |
| 21 | +// There are four orthogonal features in a traversal: |
| 22 | +// 1 type filtering |
| 23 | +// 2 pruning |
| 24 | +// 3 postorder calls to f |
| 25 | +// 4 stack |
| 26 | +// Rather than offer all of them in the API, |
| 27 | +// only a few combinations are exposed: |
| 28 | +// - Preorder is the fastest and has fewest features, |
| 29 | +// but is the most commonly needed traversal. |
| 30 | +// - Nodes and WithStack both provide pruning and postorder calls, |
| 31 | +// even though few clients need it, because supporting two versions |
| 32 | +// is not justified. |
| 33 | +// More combinations could be supported by expressing them as |
| 34 | +// wrappers around a more generic traversal, but this was measured |
| 35 | +// and found to degrade performance significantly (30%). |
| 36 | + |
| 37 | +import ( |
| 38 | + "go/ast" |
| 39 | +) |
| 40 | + |
| 41 | +// An Inspector provides methods for inspecting |
| 42 | +// (traversing) the syntax trees of a package. |
| 43 | +type Inspector struct { |
| 44 | + events []event |
| 45 | +} |
| 46 | + |
| 47 | +// New returns an Inspector for the specified syntax trees. |
| 48 | +func New(files []*ast.File) *Inspector { |
| 49 | + return &Inspector{traverse(files)} |
| 50 | +} |
| 51 | + |
| 52 | +// An event represents a push or a pop |
| 53 | +// of an ast.Node during a traversal. |
| 54 | +type event struct { |
| 55 | + node ast.Node |
| 56 | + typ uint64 // typeOf(node) |
| 57 | + index int // 1 + index of corresponding pop event, or 0 if this is a pop |
| 58 | +} |
| 59 | + |
| 60 | +// Preorder visits all the nodes of the files supplied to New in |
| 61 | +// depth-first order. It calls f(n) for each node n before it visits |
| 62 | +// n's children. |
| 63 | +// |
| 64 | +// The types argument, if non-empty, enables type-based filtering of |
| 65 | +// events. The function f if is called only for nodes whose type |
| 66 | +// matches an element of the types slice. |
| 67 | +func (in *Inspector) Preorder(types []ast.Node, f func(ast.Node)) { |
| 68 | + // Because it avoids postorder calls to f, and the pruning |
| 69 | + // check, Preorder is almost twice as fast as Nodes. The two |
| 70 | + // features seem to contribute similar slowdowns (~1.4x each). |
| 71 | + |
| 72 | + mask := maskOf(types) |
| 73 | + for i := 0; i < len(in.events); { |
| 74 | + ev := in.events[i] |
| 75 | + if ev.typ&mask != 0 { |
| 76 | + if ev.index > 0 { |
| 77 | + f(ev.node) |
| 78 | + } |
| 79 | + } |
| 80 | + i++ |
| 81 | + } |
| 82 | +} |
| 83 | + |
| 84 | +// Nodes visits the nodes of the files supplied to New in depth-first |
| 85 | +// order. It calls f(n, true) for each node n before it visits n's |
| 86 | +// children. If f returns true, Nodes invokes f recursively for each |
| 87 | +// of the non-nil children of the node, followed by a call of |
| 88 | +// f(n, false). |
| 89 | +// |
| 90 | +// The types argument, if non-empty, enables type-based filtering of |
| 91 | +// events. The function f if is called only for nodes whose type |
| 92 | +// matches an element of the types slice. |
| 93 | +func (in *Inspector) Nodes(types []ast.Node, f func(n ast.Node, push bool) (prune bool)) { |
| 94 | + mask := maskOf(types) |
| 95 | + for i := 0; i < len(in.events); { |
| 96 | + ev := in.events[i] |
| 97 | + if ev.typ&mask != 0 { |
| 98 | + if ev.index > 0 { |
| 99 | + // push |
| 100 | + if !f(ev.node, true) { |
| 101 | + i = ev.index // jump to corresponding pop + 1 |
| 102 | + continue |
| 103 | + } |
| 104 | + } else { |
| 105 | + // pop |
| 106 | + f(ev.node, false) |
| 107 | + } |
| 108 | + } |
| 109 | + i++ |
| 110 | + } |
| 111 | +} |
| 112 | + |
| 113 | +// WithStack visits nodes in a similar manner to Nodes, but it |
| 114 | +// supplies each call to f an additional argument, the current |
| 115 | +// traversal stack. The stack's first element is the outermost node, |
| 116 | +// an *ast.File; its last is the innermost, n. |
| 117 | +func (in *Inspector) WithStack(types []ast.Node, f func(n ast.Node, push bool, stack []ast.Node) (prune bool)) { |
| 118 | + mask := maskOf(types) |
| 119 | + var stack []ast.Node |
| 120 | + for i := 0; i < len(in.events); { |
| 121 | + ev := in.events[i] |
| 122 | + if ev.index > 0 { |
| 123 | + // push |
| 124 | + stack = append(stack, ev.node) |
| 125 | + if ev.typ&mask != 0 { |
| 126 | + if !f(ev.node, true, stack) { |
| 127 | + i = ev.index |
| 128 | + stack = stack[:len(stack)-1] |
| 129 | + continue |
| 130 | + } |
| 131 | + } |
| 132 | + } else { |
| 133 | + // pop |
| 134 | + if ev.typ&mask != 0 { |
| 135 | + f(ev.node, false, stack) |
| 136 | + } |
| 137 | + stack = stack[:len(stack)-1] |
| 138 | + } |
| 139 | + i++ |
| 140 | + } |
| 141 | +} |
| 142 | + |
| 143 | +// traverse builds the table of events representing a traversal. |
| 144 | +func traverse(files []*ast.File) []event { |
| 145 | + // Preallocate approximate number of events |
| 146 | + // based on source file extent. |
| 147 | + // This makes traverse faster by 4x (!). |
| 148 | + var extent int |
| 149 | + for _, f := range files { |
| 150 | + extent += int(f.End() - f.Pos()) |
| 151 | + } |
| 152 | + // This estimate is based on the net/http package. |
| 153 | + events := make([]event, 0, extent*33/100) |
| 154 | + |
| 155 | + var stack []event |
| 156 | + for _, f := range files { |
| 157 | + ast.Inspect(f, func(n ast.Node) bool { |
| 158 | + if n != nil { |
| 159 | + // push |
| 160 | + ev := event{ |
| 161 | + node: n, |
| 162 | + typ: typeOf(n), |
| 163 | + index: len(events), // push event temporarily holds own index |
| 164 | + } |
| 165 | + stack = append(stack, ev) |
| 166 | + events = append(events, ev) |
| 167 | + } else { |
| 168 | + // pop |
| 169 | + ev := stack[len(stack)-1] |
| 170 | + stack = stack[:len(stack)-1] |
| 171 | + |
| 172 | + events[ev.index].index = len(events) + 1 // make push refer to pop |
| 173 | + |
| 174 | + ev.index = 0 // turn ev into a pop event |
| 175 | + events = append(events, ev) |
| 176 | + } |
| 177 | + return true |
| 178 | + }) |
| 179 | + } |
| 180 | + |
| 181 | + return events |
| 182 | +} |
0 commit comments