Skip to content

Commit dd86fef

Browse files
authored
feat: add scope and data-flow analysis for JS (#205)
1 parent e576dc7 commit dd86fef

File tree

12 files changed

+1486
-15
lines changed

12 files changed

+1486
-15
lines changed

analysis/scope.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ const (
2929
VarKindFunction
3030
VarKindVariable
3131
VarKindParameter
32+
VarKindClass
3233
)
3334

3435
type Variable struct {
@@ -39,6 +40,8 @@ type Variable struct {
3940
DeclNode *sitter.Node
4041
// Refs is a list of references to this variable throughout the file
4142
Refs []*Reference
43+
// Exported tracks if the variable is valid to be exported
44+
Exported bool
4245
}
4346

4447
// ScopeBuilder is an interface that has to be implemented
@@ -144,7 +147,7 @@ func buildScopeTree(
144147
if builder.NodeCreatesScope(node) {
145148
nextScope = NewScope(scope)
146149
scopeOfNode[node] = nextScope
147-
150+
scope.AstNode = node
148151
if scope != nil {
149152
scope.Children = append(scope.Children, nextScope)
150153
} else {

analysis/ts_scope.go

Lines changed: 147 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,10 @@ var ScopeNodes = []string{
3131
"for_in_statement",
3232
"for_of_statement",
3333
"program",
34+
"arrow_function",
35+
"class_body",
36+
// "class_declaration",
37+
"method_definition",
3438
}
3539

3640
func (ts *TsScopeBuilder) NodeCreatesScope(node *sitter.Node) bool {
@@ -39,7 +43,14 @@ func (ts *TsScopeBuilder) NodeCreatesScope(node *sitter.Node) bool {
3943

4044
func (ts *TsScopeBuilder) DeclaresVariable(node *sitter.Node) bool {
4145
typ := node.Type()
42-
return typ == "variable_declarator" || typ == "import_clause" || typ == "import_specifier"
46+
return typ == "variable_declarator" ||
47+
typ == "import_clause" ||
48+
typ == "import_specifier" ||
49+
typ == "formal_parameters" ||
50+
typ == "function_declaration" ||
51+
typ == "method_definition" ||
52+
typ == "class_declaration" ||
53+
typ == "export_statement" || typ == "assignment_expression" || typ == "public_field_definition"
4354
}
4455

4556
func (ts *TsScopeBuilder) scanDecl(idOrPattern, declarator *sitter.Node, decls []*Variable) []*Variable {
@@ -135,6 +146,10 @@ func (ts *TsScopeBuilder) CollectVariables(node *sitter.Node) []*Variable {
135146
lhs := node.ChildByFieldName("name")
136147
return ts.scanDecl(lhs, node, declaredVars)
137148

149+
case "assignment_expression":
150+
lhs := node.ChildByFieldName("left")
151+
return ts.scanDecl(lhs, node, declaredVars)
152+
138153
case "function_declaration":
139154
name := node.ChildByFieldName("name")
140155
// skipcq: TCV-001
@@ -150,6 +165,40 @@ func (ts *TsScopeBuilder) CollectVariables(node *sitter.Node) []*Variable {
150165

151166
case "formal_parameters":
152167
// TODO
168+
for i := 0; i < int(node.NamedChildCount()); i++ {
169+
param := node.NamedChild(i)
170+
if param == nil {
171+
continue
172+
}
173+
174+
var identifier *sitter.Node
175+
if param.Type() == "identifier" {
176+
identifier = param
177+
} else if param.Type() == "required_parameter" || param.Type() == "optional_parameter" {
178+
// Look for pattern which might be identifier or destructuring
179+
pattern := param.ChildByFieldName("pattern")
180+
if pattern != nil && pattern.Type() == "identifier" {
181+
identifier = pattern
182+
}
183+
// TODO: Handle destructuring patterns within parameters if needed by calling scanDecl
184+
} else if param.Type() == "assignment_pattern" {
185+
// Parameter with default value: function foo(x = 1)
186+
left := param.ChildByFieldName("left")
187+
if left != nil && left.Type() == "identifier" {
188+
identifier = left
189+
}
190+
// TODO: Handle destructuring patterns within parameters if needed by calling scanDecl
191+
}
192+
// TODO: Handle rest parameter (...)+
193+
if identifier != nil {
194+
declaredVars = append(declaredVars, &Variable{
195+
Kind: VarKindParameter,
196+
Name: identifier.Content(ts.source),
197+
DeclNode: param, // Use the parameter node itself (or identifier) as DeclNode
198+
})
199+
}
200+
// Add handling for destructuring patterns here if necessary using scanDecl
201+
}
153202

154203
case "import_specifier":
155204
// import { <name> } from ...
@@ -166,6 +215,36 @@ func (ts *TsScopeBuilder) CollectVariables(node *sitter.Node) []*Variable {
166215
DeclNode: defaultImport,
167216
})
168217
}
218+
219+
case "class_declaration":
220+
className := node.ChildByFieldName("name")
221+
if className != nil {
222+
declaredVars = append(declaredVars, &Variable{
223+
Kind: VarKindClass,
224+
Name: className.Content(ts.source),
225+
DeclNode: className,
226+
})
227+
}
228+
229+
case "method_definition":
230+
methodName := node.ChildByFieldName("name")
231+
if methodName != nil {
232+
declaredVars = append(declaredVars, &Variable{
233+
Kind: VarKindFunction,
234+
Name: methodName.Content(ts.source),
235+
DeclNode: methodName,
236+
})
237+
}
238+
239+
case "public_field_definition":
240+
fieldName := node.ChildByFieldName("name")
241+
if fieldName != nil {
242+
declaredVars = append(declaredVars, &Variable{
243+
Kind: VarKindVariable,
244+
Name: fieldName.Content(ts.source),
245+
DeclNode: fieldName,
246+
})
247+
}
169248
}
170249

171250
return declaredVars
@@ -232,6 +311,7 @@ func (ts *TsScopeBuilder) OnNodeEnter(node *sitter.Node, scope *Scope) {
232311
}
233312
variable.Refs = append(variable.Refs, ref)
234313
}
314+
235315
}
236316

237317
func (ts *TsScopeBuilder) OnNodeExit(node *sitter.Node, scope *Scope) {
@@ -254,4 +334,70 @@ func (ts *TsScopeBuilder) OnNodeExit(node *sitter.Node, scope *Scope) {
254334
variable.Refs = append(variable.Refs, ref)
255335
}
256336
}
337+
if node.Type() == "export_statement" {
338+
// Handle named exports: export { foo, bar as baz };
339+
exportClause := ChildrenOfType(node, "export_clause")
340+
341+
for _, clause := range exportClause {
342+
var varName string
343+
exportSpecifier := ChildrenOfType(clause, "export_specifier")
344+
if len(exportSpecifier) == 0 {
345+
continue
346+
}
347+
for _, specifier := range exportSpecifier {
348+
name := specifier.ChildByFieldName("name")
349+
if name == nil {
350+
continue
351+
}
352+
if name.Type() == "identifier" {
353+
varName = name.Content(ts.source)
354+
}
355+
variable := scope.Lookup(varName)
356+
if variable != nil {
357+
variable.Exported = true
358+
}
359+
}
360+
361+
}
362+
363+
// Handle direct exports: export const foo = 123;
364+
declaration := node.ChildByFieldName("declaration")
365+
if declaration != nil {
366+
if declaration.Type() == "lexical_declaration" {
367+
// Handle variable declarations: export const foo = 123, bar = 456;
368+
declarators := ChildrenOfType(declaration, "variable_declarator")
369+
for _, declarator := range declarators {
370+
name := declarator.ChildByFieldName("name")
371+
if name != nil && name.Type() == "identifier" {
372+
varName := name.Content(ts.source)
373+
374+
variable := scope.Lookup(varName)
375+
if variable != nil {
376+
variable.Exported = true
377+
}
378+
}
379+
}
380+
} else if declaration.Type() == "function_declaration" || declaration.Type() == "class_declaration" {
381+
// Handle direct function/class exports: export function foo() {}
382+
name := declaration.ChildByFieldName("name")
383+
if name != nil {
384+
varName := name.Content(ts.source)
385+
variable := scope.Lookup(varName)
386+
if variable != nil {
387+
variable.Exported = true
388+
}
389+
}
390+
}
391+
}
392+
393+
// Handle default exports: export default foo;
394+
defaultExport := node.ChildByFieldName("value")
395+
if defaultExport != nil && defaultExport.Type() == "identifier" {
396+
varName := defaultExport.Content(ts.source)
397+
variable := scope.Lookup(varName)
398+
if variable != nil {
399+
variable.Exported = true
400+
}
401+
}
402+
}
257403
}

0 commit comments

Comments
 (0)