Skip to content

Commit f4a5dfd

Browse files
fix: fix analysis and test runners (#151)
* fix: fix analysis runner Signed-off-by: Sourya Vatsyayan <sourya@deepsource.io> * fix: fix test runner Signed-off-by: Sourya Vatsyayan <sourya@deepsource.io> * add support for scope tree Signed-off-by: Sourya Vatsyayan <sourya@deepsource.io> * chore: rename lint and rule to checker Signed-off-by: Sourya Vatsyayan <sourya@deepsource.io> --------- Signed-off-by: Sourya Vatsyayan <sourya@deepsource.io>
1 parent 16fb597 commit f4a5dfd

File tree

24 files changed

+1066
-384
lines changed

24 files changed

+1066
-384
lines changed

analysis/analyzer.go

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -70,10 +70,7 @@ func walkTree(node *sitter.Node, f func(*sitter.Node)) {
7070

7171
func Preorder(pass *Pass, fn func(*sitter.Node)) {
7272
// TODO: cache the traversal results to avoid running the traversal for each analyzer
73-
for _, file := range pass.Files {
74-
pass.FileContext = file
75-
walkTree(file.Ast, fn)
76-
}
73+
walkTree(pass.FileContext.Ast, fn)
7774
}
7875

7976
var defaultIgnoreDirs = []string{
@@ -91,7 +88,7 @@ var defaultIgnoreDirs = []string{
9188
".vitepress",
9289
}
9390

94-
func RunAnalyzers(path string, analyzers []*Analyzer) ([]*Issue, error) {
91+
func RunAnalyzers(path string, analyzers []*Analyzer, fileFilter func(string) bool) ([]*Issue, error) {
9592
raisedIssues := []*Issue{}
9693
langAnalyzerMap := make(map[Language][]*Analyzer)
9794
for _, analyzer := range analyzers {
@@ -111,6 +108,10 @@ func RunAnalyzers(path string, analyzers []*Analyzer) ([]*Issue, error) {
111108
return nil
112109
}
113110

111+
if fileFilter != nil && !fileFilter(path) {
112+
return nil
113+
}
114+
114115
file, err := ParseFile(path)
115116
if err != nil {
116117
if err != ErrUnsupportedLanguage {

analysis/language.go

Lines changed: 60 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ type ParseResult struct {
4848
Language Language
4949
// ScopeTree represents the scope hierarchy of the file.
5050
// Can be nil if scope support for this language has not been implemented yet.
51-
// ScopeTree *ScopeTree
51+
ScopeTree *ScopeTree
5252
}
5353

5454
type Language int
@@ -214,14 +214,14 @@ func Parse(filePath string, source []byte, language Language, grammar *sitter.La
214214
return nil, fmt.Errorf("failed to parse %s", filePath)
215215
}
216216

217-
// scopeTree := MakeScopeTree(language, ast, source)
217+
scopeTree := MakeScopeTree(language, ast, source)
218218
parseResult := &ParseResult{
219219
Ast: ast,
220220
Source: source,
221221
FilePath: filePath,
222222
TsLanguage: grammar,
223223
Language: language,
224-
// ScopeTree: scopeTree,
224+
ScopeTree: scopeTree,
225225
}
226226

227227
return parseResult, nil
@@ -263,3 +263,60 @@ func GetEscapedCommentIdentifierFromPath(path string) string {
263263
return ""
264264
}
265265
}
266+
267+
func GetExtFromLanguage(lang Language) string {
268+
switch lang {
269+
case LangPy:
270+
return ".py"
271+
case LangJs:
272+
return ".js"
273+
case LangTs:
274+
return ".ts"
275+
case LangTsx:
276+
return ".tsx"
277+
case LangJava:
278+
return ".java"
279+
case LangRuby:
280+
return ".rb"
281+
case LangRust:
282+
return ".rs"
283+
case LangYaml:
284+
return ".yaml"
285+
case LangCss:
286+
return ".css"
287+
case LangDockerfile:
288+
return ".Dockerfile"
289+
case LangSql:
290+
return ".sql"
291+
case LangKotlin:
292+
return ".kt"
293+
case LangOCaml:
294+
return ".ml"
295+
case LangLua:
296+
return ".lua"
297+
case LangBash:
298+
return ".sh"
299+
case LangCsharp:
300+
return ".cs"
301+
case LangElixir:
302+
return ".ex"
303+
case LangElm:
304+
return ".elm"
305+
case LangGo:
306+
return ".go"
307+
case LangGroovy:
308+
return ".groovy"
309+
case LangHcl:
310+
return ".tf"
311+
case LangHtml:
312+
return ".html"
313+
case LangPhp:
314+
return ".php"
315+
case LangScala:
316+
return ".scala"
317+
case LangSwift:
318+
return ".swift"
319+
default:
320+
return ""
321+
}
322+
}

analysis/scope.go

Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
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

Comments
 (0)