|
| 1 | +// A language agnostic interface for scope handling which |
| 2 | +// also handles forward declarations and references (e.g: hoisting). |
| 3 | +// BUT, references aren't tracked across files in a language like Golang or C++ (macros/extern/using namespace) |
| 4 | + |
| 5 | +package analysis |
| 6 | + |
| 7 | +import sitter "github.com/smacker/go-tree-sitter" |
| 8 | + |
| 9 | +// Reference represents a variable reference inside a source file |
| 10 | +// Cross-file references like those in Golang and C++ (macros/extern) are NOT supported, |
| 11 | +// so this shouldn't be used for checkers like "unused-variable", but is safe to use for checkers like |
| 12 | +// "unused-import" |
| 13 | +type Reference struct { |
| 14 | + // IsWriteRef determines if this reference is a write reference. |
| 15 | + // For write refs, only the expression being assigned is stored. |
| 16 | + // i.e: for `a = 3`, this list will store the `3` node, not the assignment node |
| 17 | + IsWriteRef bool |
| 18 | + // Variable stores the variable being referenced |
| 19 | + Variable *Variable |
| 20 | + // Node stores the node that references the variable |
| 21 | + Node *sitter.Node |
| 22 | +} |
| 23 | + |
| 24 | +type VarKind int32 |
| 25 | + |
| 26 | +const ( |
| 27 | + VarKindError VarKind = iota |
| 28 | + VarKindImport |
| 29 | + VarKindFunction |
| 30 | + VarKindVariable |
| 31 | + VarKindParameter |
| 32 | +) |
| 33 | + |
| 34 | +type Variable struct { |
| 35 | + Kind VarKind |
| 36 | + // Stores the name of the variable |
| 37 | + Name string |
| 38 | + // DeclNode is the AST node that declares this variable |
| 39 | + DeclNode *sitter.Node |
| 40 | + // Refs is a list of references to this variable throughout the file |
| 41 | + Refs []*Reference |
| 42 | +} |
| 43 | + |
| 44 | +// ScopeBuilder is an interface that has to be implemented |
| 45 | +// once for every supported language. |
| 46 | +// Languages that don't implement a `ScopeBuilder` can still have checkers, just |
| 47 | +// not any that require scope resolution. |
| 48 | +type ScopeBuilder interface { |
| 49 | + GetLanguage() Language |
| 50 | + // NodeCreatesScope returns true if the node introduces a new scope |
| 51 | + // into the scope tree |
| 52 | + NodeCreatesScope(node *sitter.Node) bool |
| 53 | + // DeclaresVariable determines if we can extract new variables out of this AST node |
| 54 | + DeclaresVariable(node *sitter.Node) bool |
| 55 | + // CollectVariables extracts variables from the node and adds them to the scope |
| 56 | + CollectVariables(node *sitter.Node) []*Variable |
| 57 | + // OnNodeEnter is called when the scope builder enters a node |
| 58 | + // for the first time, and hasn't scanned its children decls just yet |
| 59 | + // can be used to handle language specific scoping rules, if any |
| 60 | + // If `node` is smth like a block statement, `currentScope` corresponds |
| 61 | + // to the scope introduced by the block statement. |
| 62 | + OnNodeEnter(node *sitter.Node, currentScope *Scope) |
| 63 | + // OnNodeExit is called when the scope builder exits a node |
| 64 | + // can be used to handle language specific scoping rules, if any |
| 65 | + // If `node` is smth like a block statement, `currentScope` corresponds |
| 66 | + // to the scope introduced by the block statement. |
| 67 | + OnNodeExit(node *sitter.Node, currentScope *Scope) |
| 68 | +} |
| 69 | + |
| 70 | +type Scope struct { |
| 71 | + // AstNode is the AST node that introduces this scope into the scope tree |
| 72 | + AstNode *sitter.Node |
| 73 | + // Variables is a map of variable name to an object representing it |
| 74 | + Variables map[string]*Variable |
| 75 | + // Upper is the parent scope of this scope |
| 76 | + Upper *Scope |
| 77 | + // Children is a list of scopes that are children of this scope |
| 78 | + Children []*Scope |
| 79 | +} |
| 80 | + |
| 81 | +func NewScope(upper *Scope) *Scope { |
| 82 | + return &Scope{ |
| 83 | + Variables: map[string]*Variable{}, |
| 84 | + Upper: upper, |
| 85 | + } |
| 86 | +} |
| 87 | + |
| 88 | +// Lookup searches for a variable in the current scope and its parents |
| 89 | +func (s *Scope) Lookup(name string) *Variable { |
| 90 | + if v, exists := s.Variables[name]; exists { |
| 91 | + return v |
| 92 | + } |
| 93 | + |
| 94 | + if s.Upper != nil { |
| 95 | + return s.Upper.Lookup(name) |
| 96 | + } |
| 97 | + |
| 98 | + return nil |
| 99 | +} |
| 100 | + |
| 101 | +type ScopeTree struct { |
| 102 | + Language Language |
| 103 | + // ScopeOfNode maps every scope-having node to its corresponding scope. |
| 104 | + // E.g: a block statement is mapped to the scope it introduces. |
| 105 | + ScopeOfNode map[*sitter.Node]*Scope |
| 106 | + // Root is the top-level scope in the program, |
| 107 | + // usually associated with the `program` or `module` node |
| 108 | + Root *Scope |
| 109 | +} |
| 110 | + |
| 111 | +// BuildScopeTree constructs a scope tree from the AST for a program |
| 112 | +func BuildScopeTree(builder ScopeBuilder, ast *sitter.Node, source []byte) *ScopeTree { |
| 113 | + root := NewScope(nil) |
| 114 | + root.AstNode = ast |
| 115 | + |
| 116 | + scopeOfNode := make(map[*sitter.Node]*Scope) |
| 117 | + buildScopeTree(builder, source, ast, root, scopeOfNode) |
| 118 | + |
| 119 | + return &ScopeTree{ |
| 120 | + Language: builder.GetLanguage(), |
| 121 | + ScopeOfNode: scopeOfNode, |
| 122 | + Root: root, |
| 123 | + } |
| 124 | +} |
| 125 | + |
| 126 | +func buildScopeTree( |
| 127 | + builder ScopeBuilder, |
| 128 | + source []byte, |
| 129 | + node *sitter.Node, |
| 130 | + scope *Scope, |
| 131 | + scopeOfNode map[*sitter.Node]*Scope, |
| 132 | +) *Scope { |
| 133 | + builder.OnNodeEnter(node, scope) |
| 134 | + defer builder.OnNodeExit(node, scope) |
| 135 | + |
| 136 | + if builder.DeclaresVariable(node) { |
| 137 | + decls := builder.CollectVariables(node) |
| 138 | + for _, decl := range decls { |
| 139 | + scope.Variables[decl.Name] = decl |
| 140 | + } |
| 141 | + } |
| 142 | + |
| 143 | + nextScope := scope |
| 144 | + if builder.NodeCreatesScope(node) { |
| 145 | + nextScope = NewScope(scope) |
| 146 | + scopeOfNode[node] = nextScope |
| 147 | + |
| 148 | + if scope != nil { |
| 149 | + scope.Children = append(scope.Children, nextScope) |
| 150 | + } else { |
| 151 | + scope = nextScope // root |
| 152 | + } |
| 153 | + } |
| 154 | + |
| 155 | + for i := 0; i < int(node.NamedChildCount()); i++ { |
| 156 | + child := node.NamedChild(i) |
| 157 | + buildScopeTree(builder, source, child, nextScope, scopeOfNode) |
| 158 | + } |
| 159 | + |
| 160 | + return scope |
| 161 | +} |
| 162 | + |
| 163 | +// GetScope finds the nearest surrounding scope of an AST node |
| 164 | +func (st *ScopeTree) GetScope(node *sitter.Node) *Scope { |
| 165 | + if scope, exists := st.ScopeOfNode[node]; exists { |
| 166 | + return scope |
| 167 | + } |
| 168 | + |
| 169 | + if parent := node.Parent(); parent != nil { |
| 170 | + return st.GetScope(parent) |
| 171 | + } |
| 172 | + |
| 173 | + return nil |
| 174 | +} |
| 175 | + |
| 176 | +func MakeScopeTree(lang Language, ast *sitter.Node, source []byte) *ScopeTree { |
| 177 | + switch lang { |
| 178 | + case LangPy: |
| 179 | + return nil |
| 180 | + case LangTs, LangJs, LangTsx: |
| 181 | + builder := &TsScopeBuilder{ |
| 182 | + ast: ast, |
| 183 | + source: source, |
| 184 | + } |
| 185 | + return BuildScopeTree(builder, ast, source) |
| 186 | + default: |
| 187 | + return nil |
| 188 | + } |
| 189 | +} |
0 commit comments