github.com/cockroachdb/tools@v0.0.0-20230222021103-a6d27438930d/go/ast/inspector/inspector.go (about)

     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 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) on push event, or union of typ strictly between push and pop events on pop events
    57  	index int    // index of corresponding push or pop event
    58  }
    59  
    60  // TODO: Experiment with storing only the second word of event.node (unsafe.Pointer).
    61  // Type can be recovered from the sole bit in typ.
    62  
    63  // Preorder visits all the nodes of the files supplied to New in
    64  // depth-first order. It calls f(n) for each node n before it visits
    65  // n's children.
    66  //
    67  // The types argument, if non-empty, enables type-based filtering of
    68  // events. The function f if is called only for nodes whose type
    69  // matches an element of the types slice.
    70  func (in *Inspector) Preorder(types []ast.Node, f func(ast.Node)) {
    71  	// Because it avoids postorder calls to f, and the pruning
    72  	// check, Preorder is almost twice as fast as Nodes. The two
    73  	// features seem to contribute similar slowdowns (~1.4x each).
    74  
    75  	mask := maskOf(types)
    76  	for i := 0; i < len(in.events); {
    77  		ev := in.events[i]
    78  		if ev.index > i {
    79  			// push
    80  			if ev.typ&mask != 0 {
    81  				f(ev.node)
    82  			}
    83  			pop := ev.index
    84  			if in.events[pop].typ&mask == 0 {
    85  				// Subtrees do not contain types: skip them and pop.
    86  				i = pop + 1
    87  				continue
    88  			}
    89  		}
    90  		i++
    91  	}
    92  }
    93  
    94  // Nodes visits the nodes of the files supplied to New in depth-first
    95  // order. It calls f(n, true) for each node n before it visits n's
    96  // children. If f returns true, Nodes invokes f recursively for each
    97  // of the non-nil children of the node, followed by a call of
    98  // f(n, false).
    99  //
   100  // The types argument, if non-empty, enables type-based filtering of
   101  // events. The function f if is called only for nodes whose type
   102  // matches an element of the types slice.
   103  func (in *Inspector) Nodes(types []ast.Node, f func(n ast.Node, push bool) (proceed bool)) {
   104  	mask := maskOf(types)
   105  	for i := 0; i < len(in.events); {
   106  		ev := in.events[i]
   107  		if ev.index > i {
   108  			// push
   109  			pop := ev.index
   110  			if ev.typ&mask != 0 {
   111  				if !f(ev.node, true) {
   112  					i = pop + 1 // jump to corresponding pop + 1
   113  					continue
   114  				}
   115  			}
   116  			if in.events[pop].typ&mask == 0 {
   117  				// Subtrees do not contain types: skip them.
   118  				i = pop
   119  				continue
   120  			}
   121  		} else {
   122  			// pop
   123  			push := ev.index
   124  			if in.events[push].typ&mask != 0 {
   125  				f(ev.node, false)
   126  			}
   127  		}
   128  		i++
   129  	}
   130  }
   131  
   132  // WithStack visits nodes in a similar manner to Nodes, but it
   133  // supplies each call to f an additional argument, the current
   134  // traversal stack. The stack's first element is the outermost node,
   135  // an *ast.File; its last is the innermost, n.
   136  func (in *Inspector) WithStack(types []ast.Node, f func(n ast.Node, push bool, stack []ast.Node) (proceed bool)) {
   137  	mask := maskOf(types)
   138  	var stack []ast.Node
   139  	for i := 0; i < len(in.events); {
   140  		ev := in.events[i]
   141  		if ev.index > i {
   142  			// push
   143  			pop := ev.index
   144  			stack = append(stack, ev.node)
   145  			if ev.typ&mask != 0 {
   146  				if !f(ev.node, true, stack) {
   147  					i = pop + 1
   148  					stack = stack[:len(stack)-1]
   149  					continue
   150  				}
   151  			}
   152  			if in.events[pop].typ&mask == 0 {
   153  				// Subtrees does not contain types: skip them.
   154  				i = pop
   155  				continue
   156  			}
   157  		} else {
   158  			// pop
   159  			push := ev.index
   160  			if in.events[push].typ&mask != 0 {
   161  				f(ev.node, false, stack)
   162  			}
   163  			stack = stack[:len(stack)-1]
   164  		}
   165  		i++
   166  	}
   167  }
   168  
   169  // traverse builds the table of events representing a traversal.
   170  func traverse(files []*ast.File) []event {
   171  	// Preallocate approximate number of events
   172  	// based on source file extent.
   173  	// This makes traverse faster by 4x (!).
   174  	var extent int
   175  	for _, f := range files {
   176  		extent += int(f.End() - f.Pos())
   177  	}
   178  	// This estimate is based on the net/http package.
   179  	capacity := extent * 33 / 100
   180  	if capacity > 1e6 {
   181  		capacity = 1e6 // impose some reasonable maximum
   182  	}
   183  	events := make([]event, 0, capacity)
   184  
   185  	var stack []event
   186  	stack = append(stack, event{}) // include an extra event so file nodes have a parent
   187  	for _, f := range files {
   188  		ast.Inspect(f, func(n ast.Node) bool {
   189  			if n != nil {
   190  				// push
   191  				ev := event{
   192  					node:  n,
   193  					typ:   0,           // temporarily used to accumulate type bits of subtree
   194  					index: len(events), // push event temporarily holds own index
   195  				}
   196  				stack = append(stack, ev)
   197  				events = append(events, ev)
   198  			} else {
   199  				// pop
   200  				top := len(stack) - 1
   201  				ev := stack[top]
   202  				typ := typeOf(ev.node)
   203  				push := ev.index
   204  				parent := top - 1
   205  
   206  				events[push].typ = typ            // set type of push
   207  				stack[parent].typ |= typ | ev.typ // parent's typ contains push and pop's typs.
   208  				events[push].index = len(events)  // make push refer to pop
   209  
   210  				stack = stack[:top]
   211  				events = append(events, ev)
   212  			}
   213  			return true
   214  		})
   215  	}
   216  
   217  	return events
   218  }