Skip to content

Commit 0f193ce

Browse files
gkorlandCopilot
andcommitted
fix: address review feedback for JavaScript analyzer
- Fix pyproject.toml: align indentation and add upper bound (<0.24.0) for tree-sitter-javascript - Remove unused variables (heritage, superclass_node) in add_symbols - Switch from query.captures() to self._captures() for correct QueryCursor usage - Filter out node_modules when rglob'ing for *.js files in analyze_sources - Add unit tests (tests/test_javascript_analyzer.py) and fixture (tests/source_files/javascript/sample.js) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 4e44a0a commit 0f193ce

File tree

6 files changed

+154
-10
lines changed

6 files changed

+154
-10
lines changed

api/analyzers/javascript/analyzer.py

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -49,24 +49,18 @@ def get_entity_types(self) -> list[str]:
4949

5050
def add_symbols(self, entity: Entity) -> None:
5151
if entity.node.type == 'class_declaration':
52-
heritage = entity.node.child_by_field_name('body')
53-
if heritage is None:
54-
return
55-
superclass_node = entity.node.child_by_field_name('name')
5652
# Check for `extends` clause via class_heritage
5753
for child in entity.node.children:
5854
if child.type == 'class_heritage':
5955
for heritage_child in child.children:
6056
if heritage_child.type == 'identifier':
6157
entity.add_symbol("base_class", heritage_child)
6258
elif entity.node.type in ['function_declaration', 'method_definition']:
63-
query = self.language.query("(call_expression) @reference.call")
64-
captures = query.captures(entity.node)
59+
captures = self._captures("(call_expression) @reference.call", entity.node)
6560
if 'reference.call' in captures:
6661
for caller in captures['reference.call']:
6762
entity.add_symbol("call", caller)
68-
query = self.language.query("(formal_parameters (identifier) @parameter)")
69-
captures = query.captures(entity.node)
63+
captures = self._captures("(formal_parameters (identifier) @parameter)", entity.node)
7064
if 'parameter' in captures:
7165
for parameter in captures['parameter']:
7266
entity.add_symbol("parameters", parameter)

api/analyzers/source_analyzer.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -177,7 +177,7 @@ def analyze_files(self, files: list[Path], path: Path, graph: Graph) -> None:
177177

178178
def analyze_sources(self, path: Path, ignore: list[str], graph: Graph) -> None:
179179
path = path.resolve()
180-
files = list(path.rglob("*.java")) + list(path.rglob("*.py")) + list(path.rglob("*.cs")) + list(path.rglob("*.js"))
180+
files = list(path.rglob("*.java")) + list(path.rglob("*.py")) + list(path.rglob("*.cs")) + [f for f in path.rglob("*.js") if "node_modules" not in f.parts]
181181
# First pass analysis of the source code
182182
self.first_pass(path, files, ignore, graph)
183183

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ dependencies = [
1313
"tree-sitter-c>=0.24.1,<0.25.0",
1414
"tree-sitter-python>=0.25.0,<0.26.0",
1515
"tree-sitter-java>=0.23.5,<0.24.0",
16-
"tree-sitter-javascript>=0.23.0",
16+
"tree-sitter-javascript>=0.23.0,<0.24.0",
1717
"tree-sitter-c-sharp>=0.23.1,<0.24.0",
1818
"fastapi>=0.115.0,<1.0.0",
1919
"uvicorn[standard]>=0.34.0,<1.0.0",
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
/**
2+
* Base class for shapes
3+
*/
4+
class Shape {
5+
constructor(name) {
6+
this.name = name;
7+
}
8+
9+
area() {
10+
return 0;
11+
}
12+
}
13+
14+
class Circle extends Shape {
15+
constructor(radius) {
16+
super(radius);
17+
this.radius = radius;
18+
}
19+
20+
area() {
21+
return Math.PI * this.radius * this.radius;
22+
}
23+
}
24+
25+
function calculateTotal(shapes) {
26+
let total = 0;
27+
for (const shape of shapes) {
28+
total += shape.area();
29+
}
30+
return total;
31+
}

tests/test_javascript_analyzer.py

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
"""Tests for the JavaScript analyzer - extraction only (no DB required)."""
2+
3+
import unittest
4+
from pathlib import Path
5+
6+
from api.analyzers.javascript.analyzer import JavaScriptAnalyzer
7+
from api.entities.entity import Entity
8+
from api.entities.file import File
9+
10+
11+
def _entity_name(analyzer, entity):
12+
"""Get the name of an entity using the analyzer."""
13+
return analyzer.get_entity_name(entity.node)
14+
15+
16+
class TestJavaScriptAnalyzer(unittest.TestCase):
17+
@classmethod
18+
def setUpClass(cls):
19+
cls.analyzer = JavaScriptAnalyzer()
20+
source_dir = Path(__file__).parent / "source_files" / "javascript"
21+
cls.sample_path = source_dir / "sample.js"
22+
source = cls.sample_path.read_bytes()
23+
tree = cls.analyzer.parser.parse(source)
24+
cls.file = File(cls.sample_path, tree)
25+
26+
# Walk AST and extract entities (mirrors create_hierarchy without Graph)
27+
types = cls.analyzer.get_entity_types()
28+
stack = [tree.root_node]
29+
while stack:
30+
node = stack.pop()
31+
if node.type in types:
32+
entity = Entity(node)
33+
cls.analyzer.add_symbols(entity)
34+
cls.file.add_entity(entity)
35+
# Also recurse into entity children (e.g., class body methods)
36+
stack.extend(node.children)
37+
else:
38+
stack.extend(node.children)
39+
40+
def _entity_names(self):
41+
return [_entity_name(self.analyzer, e) for e in self.file.entities.values()]
42+
43+
def test_discovers_js_files(self):
44+
"""SourceAnalyzer should enumerate .js files."""
45+
source_dir = Path(__file__).parent / "source_files" / "javascript"
46+
js_files = list(source_dir.rglob("*.js"))
47+
self.assertTrue(len(js_files) > 0, "Should find .js files")
48+
49+
def test_entity_types(self):
50+
"""Analyzer should recognise JS entity types."""
51+
self.assertEqual(
52+
self.analyzer.get_entity_types(),
53+
['function_declaration', 'class_declaration', 'method_definition'],
54+
)
55+
56+
def test_class_extraction(self):
57+
"""Classes should be extracted from sample.js."""
58+
names = self._entity_names()
59+
self.assertIn("Shape", names)
60+
self.assertIn("Circle", names)
61+
62+
def test_function_extraction(self):
63+
"""Top-level functions should be extracted."""
64+
names = self._entity_names()
65+
self.assertIn("calculateTotal", names)
66+
67+
def test_method_extraction(self):
68+
"""Class methods should be extracted."""
69+
names = self._entity_names()
70+
self.assertIn("area", names)
71+
self.assertIn("constructor", names)
72+
73+
def test_class_labels(self):
74+
"""Classes should get the 'Class' label."""
75+
for entity in self.file.entities.values():
76+
if _entity_name(self.analyzer, entity) in ("Shape", "Circle"):
77+
self.assertEqual(self.analyzer.get_entity_label(entity.node), "Class")
78+
79+
def test_function_label(self):
80+
"""Functions should get the 'Function' label."""
81+
for entity in self.file.entities.values():
82+
if _entity_name(self.analyzer, entity) == "calculateTotal":
83+
self.assertEqual(self.analyzer.get_entity_label(entity.node), "Function")
84+
85+
def test_base_class_symbol(self):
86+
"""Circle should have Shape as a base_class symbol."""
87+
for entity in self.file.entities.values():
88+
if _entity_name(self.analyzer, entity) == "Circle":
89+
base_names = [
90+
s.symbol.text.decode("utf-8")
91+
for s in entity.symbols.get("base_class", [])
92+
]
93+
self.assertIn("Shape", base_names)
94+
95+
def test_is_dependency(self):
96+
"""node_modules paths should be flagged as dependencies."""
97+
self.assertTrue(self.analyzer.is_dependency("foo/node_modules/bar/index.js"))
98+
self.assertFalse(self.analyzer.is_dependency("src/utils.js"))
99+
100+
101+
if __name__ == "__main__":
102+
unittest.main()

uv.lock

Lines changed: 17 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)