graph-dependency-analyzer 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- graph_dependency_analyzer-0.2.0.dist-info/METADATA +158 -0
- graph_dependency_analyzer-0.2.0.dist-info/RECORD +86 -0
- graph_dependency_analyzer-0.2.0.dist-info/WHEEL +5 -0
- graph_dependency_analyzer-0.2.0.dist-info/entry_points.txt +2 -0
- graph_dependency_analyzer-0.2.0.dist-info/top_level.txt +1 -0
- src/__init__.py +0 -0
- src/application/__init__.py +0 -0
- src/application/dtos/__init__.py +0 -0
- src/application/dtos/affected_component.py +59 -0
- src/application/dtos/batch_processing_result.py +17 -0
- src/application/dtos/impact_analysis_response.py +56 -0
- src/application/dtos/impact_query_request.py +38 -0
- src/application/dtos/index_batch_request.py +53 -0
- src/application/dtos/index_batch_response.py +72 -0
- src/application/dtos/index_result.py +20 -0
- src/application/dtos/repository_index_result.py +79 -0
- src/application/exceptions.py +41 -0
- src/application/services/__init__.py +0 -0
- src/application/services/file_scanner_service.py +258 -0
- src/application/services/progress_tracker.py +132 -0
- src/application/use_cases/__init__.py +0 -0
- src/application/use_cases/clear_all_data_use_case.py +180 -0
- src/application/use_cases/index_batch_use_case.py +153 -0
- src/application/use_cases/index_repository_use_case.py +683 -0
- src/application/use_cases/indexing_orchestrator.py +181 -0
- src/application/use_cases/list_repositories_use_case.py +84 -0
- src/cli.py +512 -0
- src/config/__init__.py +5 -0
- src/config/container.py +211 -0
- src/config/logging_config.py +123 -0
- src/config/settings.py +165 -0
- src/domain/__init__.py +0 -0
- src/domain/entities/__init__.py +20 -0
- src/domain/entities/ast_node.py +32 -0
- src/domain/entities/code_chunk.py +45 -0
- src/domain/entities/code_file.py +44 -0
- src/domain/entities/dependency_graph.py +110 -0
- src/domain/entities/repository.py +46 -0
- src/domain/exceptions.py +36 -0
- src/domain/repositories/__init__.py +22 -0
- src/domain/repositories/i_code_parser.py +50 -0
- src/domain/repositories/i_dependency_extractor.py +35 -0
- src/domain/repositories/i_embedding_provider.py +31 -0
- src/domain/repositories/i_graph_repository.py +78 -0
- src/domain/repositories/i_vector_repository.py +55 -0
- src/domain/services/__init__.py +8 -0
- src/domain/services/dependency_extractor.py +962 -0
- src/domain/services/semantic_chunk_builder.py +719 -0
- src/domain/value_objects/__init__.py +12 -0
- src/domain/value_objects/chunk_type.py +24 -0
- src/domain/value_objects/dependency_type.py +22 -0
- src/domain/value_objects/node_type.py +22 -0
- src/domain/value_objects/qualified_name.py +78 -0
- src/infrastructure/__init__.py +0 -0
- src/infrastructure/exceptions.py +41 -0
- src/infrastructure/external_apis/__init__.py +0 -0
- src/infrastructure/external_apis/ollama_embedding_provider.py +134 -0
- src/infrastructure/external_apis/openai_embedding_provider.py +231 -0
- src/infrastructure/parsing/__init__.py +20 -0
- src/infrastructure/parsing/config_file_parser.py +347 -0
- src/infrastructure/parsing/gradle_dependency_parser.py +159 -0
- src/infrastructure/parsing/groovy_parser.py +198 -0
- src/infrastructure/parsing/maven_dependency_parser.py +147 -0
- src/infrastructure/parsing/microfrontend_config_parser.py +242 -0
- src/infrastructure/parsing/npm_package_dependency_parser.py +255 -0
- src/infrastructure/parsing/python_parser.py +211 -0
- src/infrastructure/parsing/sql_schema_parser.py +658 -0
- src/infrastructure/parsing/tree_sitter_java_parser.py +434 -0
- src/infrastructure/parsing/tree_sitter_typescript_parser.py +600 -0
- src/infrastructure/persistence/__init__.py +0 -0
- src/infrastructure/persistence/graph_export_repository.py +443 -0
- src/infrastructure/persistence/neo4j_dependency_repository.py +657 -0
- src/infrastructure/persistence/qdrant_vector_repository.py +357 -0
- src/infrastructure/persistence/repository_relationship_repository.py +1312 -0
- src/presentation/__init__.py +0 -0
- src/presentation/api/__init__.py +0 -0
- src/presentation/api/rest/__init__.py +0 -0
- src/presentation/api/rest/exception_handlers.py +174 -0
- src/presentation/api/rest/main.py +80 -0
- src/presentation/api/rest/routers/__init__.py +5 -0
- src/presentation/api/rest/routers/admin.py +103 -0
- src/presentation/api/rest/routers/analysis.py +980 -0
- src/presentation/api/rest/routers/export.py +442 -0
- src/presentation/api/rest/routers/health.py +117 -0
- src/presentation/api/rest/routers/indexing.py +148 -0
- src/presentation/api/rest/routers/relationships.py +1089 -0
|
@@ -0,0 +1,600 @@
|
|
|
1
|
+
"""TreeSitterTypeScriptParser - Tree-sitter based TypeScript/JavaScript AST parser.
|
|
2
|
+
|
|
3
|
+
Uses tree-sitter and tree-sitter-typescript for precise AST parsing via tree traversal.
|
|
4
|
+
Supports dual grammars: TypeScript (.ts, .js) and TSX (.tsx, .jsx).
|
|
5
|
+
"""
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from tree_sitter import Language, Parser, Node
|
|
9
|
+
|
|
10
|
+
from src.domain.repositories.i_code_parser import ICodeParser, ParseResult
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ParseError(Exception):
|
|
14
|
+
"""Exception raised when parsing fails."""
|
|
15
|
+
pass
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class TreeSitterTypeScriptParser:
|
|
19
|
+
"""
|
|
20
|
+
TypeScript/JavaScript parser implementation using Tree-sitter library.
|
|
21
|
+
|
|
22
|
+
Implements ICodeParser interface for consistency with Java parser.
|
|
23
|
+
Uses two internal parser instances: one for TypeScript (.ts, .js),
|
|
24
|
+
one for TSX (.tsx, .jsx).
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
# File extensions mapped to grammars
|
|
28
|
+
TS_EXTENSIONS = {'.ts', '.js'}
|
|
29
|
+
TSX_EXTENSIONS = {'.tsx', '.jsx'}
|
|
30
|
+
|
|
31
|
+
def __init__(self):
|
|
32
|
+
"""Initialize Tree-sitter parsers with TypeScript and TSX grammars."""
|
|
33
|
+
self.ts_language = self._load_language('typescript')
|
|
34
|
+
self.ts_parser = Parser(self.ts_language)
|
|
35
|
+
|
|
36
|
+
self.tsx_language = self._load_language('tsx')
|
|
37
|
+
self.tsx_parser = Parser(self.tsx_language)
|
|
38
|
+
|
|
39
|
+
def _load_language(self, grammar_type: str) -> Language:
|
|
40
|
+
"""
|
|
41
|
+
Load Tree-sitter language grammar.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
grammar_type: 'typescript' or 'tsx'
|
|
45
|
+
|
|
46
|
+
Returns:
|
|
47
|
+
Language instance
|
|
48
|
+
|
|
49
|
+
Raises:
|
|
50
|
+
ParseError: If grammar loading fails
|
|
51
|
+
"""
|
|
52
|
+
try:
|
|
53
|
+
import tree_sitter_typescript
|
|
54
|
+
|
|
55
|
+
if grammar_type == 'typescript':
|
|
56
|
+
return Language(tree_sitter_typescript.language_typescript())
|
|
57
|
+
elif grammar_type == 'tsx':
|
|
58
|
+
return Language(tree_sitter_typescript.language_tsx())
|
|
59
|
+
else:
|
|
60
|
+
raise ValueError(f"Unknown grammar type: {grammar_type}")
|
|
61
|
+
|
|
62
|
+
except ImportError as e:
|
|
63
|
+
raise ParseError(
|
|
64
|
+
f"Failed to load tree-sitter-typescript: {e}. "
|
|
65
|
+
"Install with: pip install tree-sitter-typescript"
|
|
66
|
+
)
|
|
67
|
+
except Exception as e:
|
|
68
|
+
raise ParseError(f"Failed to load {grammar_type} grammar: {e}")
|
|
69
|
+
|
|
70
|
+
def parse_file(self, file_path: Path) -> ParseResult:
|
|
71
|
+
"""
|
|
72
|
+
Parse TypeScript/JavaScript file and extract AST structures.
|
|
73
|
+
|
|
74
|
+
Args:
|
|
75
|
+
file_path: Path to .ts/.tsx/.js/.jsx file
|
|
76
|
+
|
|
77
|
+
Returns:
|
|
78
|
+
ParseResult with extracted structures
|
|
79
|
+
|
|
80
|
+
Raises:
|
|
81
|
+
FileNotFoundError: If file doesn't exist
|
|
82
|
+
ParseError: If parsing fails
|
|
83
|
+
"""
|
|
84
|
+
if not file_path.exists():
|
|
85
|
+
raise FileNotFoundError(f"File not found: {file_path}")
|
|
86
|
+
|
|
87
|
+
# Read file content
|
|
88
|
+
try:
|
|
89
|
+
code_bytes = file_path.read_bytes()
|
|
90
|
+
code_str = code_bytes.decode('utf-8')
|
|
91
|
+
except UnicodeDecodeError as e:
|
|
92
|
+
raise ParseError(f"Failed to read file {file_path}: Invalid UTF-8 encoding")
|
|
93
|
+
except Exception as e:
|
|
94
|
+
raise ParseError(f"Failed to read file {file_path}: {e}")
|
|
95
|
+
|
|
96
|
+
# Select appropriate parser based on file extension
|
|
97
|
+
parser = self._select_parser(file_path)
|
|
98
|
+
|
|
99
|
+
# Parse with tree-sitter
|
|
100
|
+
tree = parser.parse(code_bytes)
|
|
101
|
+
|
|
102
|
+
# Check for parse errors
|
|
103
|
+
if tree.root_node.has_error:
|
|
104
|
+
raise ParseError(f"Syntax errors in TypeScript/JavaScript file: {file_path}")
|
|
105
|
+
|
|
106
|
+
# Extract structures via tree traversal
|
|
107
|
+
imports = self._extract_imports(tree.root_node, code_str)
|
|
108
|
+
methods = self._extract_components(tree.root_node, code_str) # Tasks 1.3-1.5
|
|
109
|
+
method_calls = self._extract_function_calls(tree.root_node, code_str) # Task 1.6
|
|
110
|
+
string_literals = self._extract_string_literals(tree.root_node, code_str) # Task 1.6
|
|
111
|
+
|
|
112
|
+
return ParseResult(
|
|
113
|
+
file_path=file_path,
|
|
114
|
+
classes=[], # TypeScript doesn't use classes in Java sense
|
|
115
|
+
methods=methods,
|
|
116
|
+
imports=imports,
|
|
117
|
+
method_calls=method_calls,
|
|
118
|
+
string_literals=string_literals
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
def _select_parser(self, file_path: Path) -> Parser:
|
|
122
|
+
"""
|
|
123
|
+
Select appropriate parser based on file extension.
|
|
124
|
+
|
|
125
|
+
Args:
|
|
126
|
+
file_path: Path to file
|
|
127
|
+
|
|
128
|
+
Returns:
|
|
129
|
+
Parser instance (ts_parser or tsx_parser)
|
|
130
|
+
"""
|
|
131
|
+
extension = file_path.suffix.lower()
|
|
132
|
+
|
|
133
|
+
if extension in self.TSX_EXTENSIONS:
|
|
134
|
+
return self.tsx_parser
|
|
135
|
+
else:
|
|
136
|
+
# Default to TypeScript parser for .ts, .js, and unknown extensions
|
|
137
|
+
return self.ts_parser
|
|
138
|
+
|
|
139
|
+
def _extract_imports(self, root_node: Node, code: str) -> list[dict]:
|
|
140
|
+
"""
|
|
141
|
+
Extract import statements from AST.
|
|
142
|
+
|
|
143
|
+
Args:
|
|
144
|
+
root_node: Root AST node
|
|
145
|
+
code: Source code string
|
|
146
|
+
|
|
147
|
+
Returns:
|
|
148
|
+
List of import dictionaries with metadata
|
|
149
|
+
"""
|
|
150
|
+
imports = []
|
|
151
|
+
self._find_imports_recursive(root_node, code, imports)
|
|
152
|
+
return imports
|
|
153
|
+
|
|
154
|
+
def _find_imports_recursive(self, node: Node, code: str, results: list):
|
|
155
|
+
"""Recursively find import_statement nodes."""
|
|
156
|
+
if node.type == "import_statement":
|
|
157
|
+
import_data = self._parse_import_statement(node, code)
|
|
158
|
+
if import_data:
|
|
159
|
+
results.append(import_data)
|
|
160
|
+
|
|
161
|
+
for child in node.children:
|
|
162
|
+
self._find_imports_recursive(child, code, results)
|
|
163
|
+
|
|
164
|
+
def _parse_import_statement(self, node: Node, code: str) -> dict | None:
|
|
165
|
+
"""
|
|
166
|
+
Parse an import_statement node.
|
|
167
|
+
|
|
168
|
+
Args:
|
|
169
|
+
node: import_statement AST node
|
|
170
|
+
code: Source code string
|
|
171
|
+
|
|
172
|
+
Returns:
|
|
173
|
+
Dictionary with import metadata or None
|
|
174
|
+
"""
|
|
175
|
+
module_path = None
|
|
176
|
+
default_import = None
|
|
177
|
+
imported_names = []
|
|
178
|
+
namespace_import = None
|
|
179
|
+
import_type = None
|
|
180
|
+
|
|
181
|
+
# Find module path (string node)
|
|
182
|
+
for child in node.children:
|
|
183
|
+
if child.type == "string":
|
|
184
|
+
module_path = self._extract_string_value(child, code)
|
|
185
|
+
elif child.type == "import_clause":
|
|
186
|
+
# Parse import_clause to determine type and bindings
|
|
187
|
+
default_import, imported_names, namespace_import = self._parse_import_clause(child, code)
|
|
188
|
+
|
|
189
|
+
if not module_path:
|
|
190
|
+
return None
|
|
191
|
+
|
|
192
|
+
# Determine import type
|
|
193
|
+
if default_import and imported_names:
|
|
194
|
+
import_type = "mixed"
|
|
195
|
+
elif default_import:
|
|
196
|
+
import_type = "default"
|
|
197
|
+
elif namespace_import:
|
|
198
|
+
import_type = "namespace"
|
|
199
|
+
elif imported_names:
|
|
200
|
+
import_type = "named"
|
|
201
|
+
else:
|
|
202
|
+
import_type = "side-effect"
|
|
203
|
+
|
|
204
|
+
return {
|
|
205
|
+
"module_path": module_path,
|
|
206
|
+
"import_type": import_type,
|
|
207
|
+
"default_import": default_import,
|
|
208
|
+
"imported_names": imported_names,
|
|
209
|
+
"namespace_import": namespace_import,
|
|
210
|
+
"line": node.start_point[0] + 1
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
def _parse_import_clause(self, node: Node, code: str) -> tuple[str | None, list[str], str | None]:
|
|
214
|
+
"""
|
|
215
|
+
Parse import_clause node to extract bindings.
|
|
216
|
+
|
|
217
|
+
Args:
|
|
218
|
+
node: import_clause AST node
|
|
219
|
+
code: Source code string
|
|
220
|
+
|
|
221
|
+
Returns:
|
|
222
|
+
Tuple of (default_import, named_imports_list, namespace_import)
|
|
223
|
+
"""
|
|
224
|
+
default_import = None
|
|
225
|
+
named_imports = []
|
|
226
|
+
namespace_import = None
|
|
227
|
+
|
|
228
|
+
for child in node.children:
|
|
229
|
+
if child.type == "identifier":
|
|
230
|
+
# Direct identifier = default import
|
|
231
|
+
default_import = self._get_node_text(child, code)
|
|
232
|
+
elif child.type == "named_imports":
|
|
233
|
+
# Named imports: { foo, bar }
|
|
234
|
+
named_imports = self._parse_named_imports(child, code)
|
|
235
|
+
elif child.type == "namespace_import":
|
|
236
|
+
# Namespace import: * as utils
|
|
237
|
+
namespace_import = self._parse_namespace_import(child, code)
|
|
238
|
+
|
|
239
|
+
return default_import, named_imports, namespace_import
|
|
240
|
+
|
|
241
|
+
def _parse_named_imports(self, node: Node, code: str) -> list[str]:
|
|
242
|
+
"""Extract names from named_imports node."""
|
|
243
|
+
names = []
|
|
244
|
+
for child in node.children:
|
|
245
|
+
if child.type == "import_specifier":
|
|
246
|
+
# import_specifier contains identifier
|
|
247
|
+
for subchild in child.children:
|
|
248
|
+
if subchild.type == "identifier":
|
|
249
|
+
names.append(self._get_node_text(subchild, code))
|
|
250
|
+
break # Only take first identifier (imported name)
|
|
251
|
+
return names
|
|
252
|
+
|
|
253
|
+
def _parse_namespace_import(self, node: Node, code: str) -> str | None:
|
|
254
|
+
"""Extract namespace name from namespace_import node (* as name)."""
|
|
255
|
+
for child in node.children:
|
|
256
|
+
if child.type == "identifier":
|
|
257
|
+
return self._get_node_text(child, code)
|
|
258
|
+
return None
|
|
259
|
+
|
|
260
|
+
def _extract_string_value(self, node: Node, code: str) -> str:
|
|
261
|
+
"""Extract string value without quotes."""
|
|
262
|
+
text = self._get_node_text(node, code)
|
|
263
|
+
# Remove surrounding quotes
|
|
264
|
+
if text.startswith('"') and text.endswith('"'):
|
|
265
|
+
return text[1:-1]
|
|
266
|
+
if text.startswith("'") and text.endswith("'"):
|
|
267
|
+
return text[1:-1]
|
|
268
|
+
return text
|
|
269
|
+
|
|
270
|
+
def _get_node_text(self, node: Node, code: str) -> str:
|
|
271
|
+
"""Get text content of a node."""
|
|
272
|
+
return code[node.start_byte:node.end_byte]
|
|
273
|
+
|
|
274
|
+
def _extract_components(self, root_node: Node, code: str) -> list[dict]:
|
|
275
|
+
"""
|
|
276
|
+
Extract React/Angular/Vue components from AST.
|
|
277
|
+
|
|
278
|
+
Args:
|
|
279
|
+
root_node: Root AST node
|
|
280
|
+
code: Source code string
|
|
281
|
+
|
|
282
|
+
Returns:
|
|
283
|
+
List of component dictionaries
|
|
284
|
+
"""
|
|
285
|
+
components = []
|
|
286
|
+
self._find_components_recursive(root_node, code, components)
|
|
287
|
+
return components
|
|
288
|
+
|
|
289
|
+
def _find_components_recursive(self, node: Node, code: str, results: list):
|
|
290
|
+
"""Recursively find component declarations."""
|
|
291
|
+
# React function components (arrow functions with JSX)
|
|
292
|
+
if node.type == "variable_declarator":
|
|
293
|
+
component = self._parse_react_function_component(node, code)
|
|
294
|
+
if component:
|
|
295
|
+
results.append(component)
|
|
296
|
+
|
|
297
|
+
# React class components
|
|
298
|
+
elif node.type == "class_declaration":
|
|
299
|
+
component = self._parse_react_class_component(node, code)
|
|
300
|
+
if component:
|
|
301
|
+
results.append(component)
|
|
302
|
+
|
|
303
|
+
# Angular components (export with decorator)
|
|
304
|
+
elif node.type == "export_statement":
|
|
305
|
+
component = self._parse_angular_component(node, code)
|
|
306
|
+
if component:
|
|
307
|
+
results.append(component)
|
|
308
|
+
|
|
309
|
+
for child in node.children:
|
|
310
|
+
self._find_components_recursive(child, code, results)
|
|
311
|
+
|
|
312
|
+
def _parse_react_function_component(self, node: Node, code: str) -> dict | None:
|
|
313
|
+
"""Parse React function component from variable_declarator."""
|
|
314
|
+
name = None
|
|
315
|
+
has_jsx = False
|
|
316
|
+
jsx_children = []
|
|
317
|
+
|
|
318
|
+
for child in node.children:
|
|
319
|
+
if child.type == "identifier":
|
|
320
|
+
name = self._get_node_text(child, code)
|
|
321
|
+
elif child.type == "arrow_function":
|
|
322
|
+
has_jsx, jsx_children = self._check_jsx_in_function(child, code)
|
|
323
|
+
|
|
324
|
+
if name and has_jsx:
|
|
325
|
+
return {
|
|
326
|
+
"name": name,
|
|
327
|
+
"type": "function_component",
|
|
328
|
+
"framework": "react",
|
|
329
|
+
"has_jsx": True,
|
|
330
|
+
"jsx_children": jsx_children,
|
|
331
|
+
"line": node.start_point[0] + 1
|
|
332
|
+
}
|
|
333
|
+
return None
|
|
334
|
+
|
|
335
|
+
def _parse_react_class_component(self, node: Node, code: str) -> dict | None:
|
|
336
|
+
"""Parse React class component."""
|
|
337
|
+
name = None
|
|
338
|
+
extends = None
|
|
339
|
+
|
|
340
|
+
for child in node.children:
|
|
341
|
+
if child.type == "type_identifier":
|
|
342
|
+
name = self._get_node_text(child, code)
|
|
343
|
+
elif child.type == "class_heritage":
|
|
344
|
+
# Extract extends clause
|
|
345
|
+
for subchild in child.children:
|
|
346
|
+
if subchild.type == "extends_clause":
|
|
347
|
+
for expr_child in subchild.children:
|
|
348
|
+
if expr_child.type == "member_expression":
|
|
349
|
+
extends = self._get_node_text(expr_child, code)
|
|
350
|
+
|
|
351
|
+
# Only treat as React component if extends React.Component or React.PureComponent
|
|
352
|
+
if name and extends and "React." in extends:
|
|
353
|
+
return {
|
|
354
|
+
"name": name,
|
|
355
|
+
"type": "class_component",
|
|
356
|
+
"framework": "react",
|
|
357
|
+
"extends": extends,
|
|
358
|
+
"line": node.start_point[0] + 1
|
|
359
|
+
}
|
|
360
|
+
return None
|
|
361
|
+
|
|
362
|
+
def _check_jsx_in_function(self, node: Node, code: str) -> tuple[bool, list[str]]:
|
|
363
|
+
"""Check if function body contains JSX and extract child components."""
|
|
364
|
+
has_jsx = False
|
|
365
|
+
jsx_children = set()
|
|
366
|
+
|
|
367
|
+
def traverse(n):
|
|
368
|
+
nonlocal has_jsx
|
|
369
|
+
if n.type == "jsx_element" or n.type == "jsx_self_closing_element":
|
|
370
|
+
has_jsx = True
|
|
371
|
+
# Extract JSX element name
|
|
372
|
+
for child in n.children:
|
|
373
|
+
if child.type == "jsx_opening_element":
|
|
374
|
+
for subchild in child.children:
|
|
375
|
+
if subchild.type == "identifier":
|
|
376
|
+
tag_name = self._get_node_text(subchild, code)
|
|
377
|
+
# Custom components start with uppercase
|
|
378
|
+
if tag_name and tag_name[0].isupper():
|
|
379
|
+
jsx_children.add(tag_name)
|
|
380
|
+
elif child.type == "identifier":
|
|
381
|
+
tag_name = self._get_node_text(child, code)
|
|
382
|
+
if tag_name and tag_name[0].isupper():
|
|
383
|
+
jsx_children.add(tag_name)
|
|
384
|
+
for c in n.children:
|
|
385
|
+
traverse(c)
|
|
386
|
+
|
|
387
|
+
traverse(node)
|
|
388
|
+
return has_jsx, list(jsx_children)
|
|
389
|
+
|
|
390
|
+
def _parse_angular_component(self, node: Node, code: str) -> dict | None:
|
|
391
|
+
"""Parse Angular @Component decorator."""
|
|
392
|
+
has_component_decorator = False
|
|
393
|
+
class_name = None
|
|
394
|
+
selector = None
|
|
395
|
+
standalone = False
|
|
396
|
+
imports = []
|
|
397
|
+
|
|
398
|
+
# Check for @Component decorator
|
|
399
|
+
for child in node.children:
|
|
400
|
+
if child.type == "decorator":
|
|
401
|
+
decorator_name, metadata = self._parse_decorator(child, code)
|
|
402
|
+
if decorator_name == "Component":
|
|
403
|
+
has_component_decorator = True
|
|
404
|
+
selector = metadata.get("selector")
|
|
405
|
+
standalone = metadata.get("standalone", False)
|
|
406
|
+
imports = metadata.get("imports", [])
|
|
407
|
+
|
|
408
|
+
elif child.type == "class_declaration":
|
|
409
|
+
for subchild in child.children:
|
|
410
|
+
if subchild.type == "type_identifier":
|
|
411
|
+
class_name = self._get_node_text(subchild, code)
|
|
412
|
+
|
|
413
|
+
if has_component_decorator and class_name:
|
|
414
|
+
result = {
|
|
415
|
+
"name": class_name,
|
|
416
|
+
"type": "angular_component",
|
|
417
|
+
"framework": "angular",
|
|
418
|
+
"selector": selector,
|
|
419
|
+
"line": node.start_point[0] + 1
|
|
420
|
+
}
|
|
421
|
+
if standalone:
|
|
422
|
+
result["standalone"] = True
|
|
423
|
+
result["imports"] = imports
|
|
424
|
+
return result
|
|
425
|
+
return None
|
|
426
|
+
|
|
427
|
+
def _parse_decorator(self, node: Node, code: str) -> tuple[str | None, dict]:
|
|
428
|
+
"""Parse decorator and extract metadata."""
|
|
429
|
+
decorator_name = None
|
|
430
|
+
metadata = {}
|
|
431
|
+
|
|
432
|
+
for child in node.children:
|
|
433
|
+
if child.type == "call_expression":
|
|
434
|
+
# Get decorator name
|
|
435
|
+
for subchild in child.children:
|
|
436
|
+
if subchild.type == "identifier":
|
|
437
|
+
decorator_name = self._get_node_text(subchild, code)
|
|
438
|
+
elif subchild.type == "arguments":
|
|
439
|
+
# Extract metadata from object argument
|
|
440
|
+
metadata = self._parse_decorator_metadata(subchild, code)
|
|
441
|
+
|
|
442
|
+
return decorator_name, metadata
|
|
443
|
+
|
|
444
|
+
def _parse_decorator_metadata(self, node: Node, code: str) -> dict:
|
|
445
|
+
"""Parse decorator metadata object."""
|
|
446
|
+
metadata = {}
|
|
447
|
+
|
|
448
|
+
for child in node.children:
|
|
449
|
+
if child.type == "object":
|
|
450
|
+
for obj_child in child.children:
|
|
451
|
+
if obj_child.type == "pair":
|
|
452
|
+
key, value = self._parse_object_pair(obj_child, code)
|
|
453
|
+
if key:
|
|
454
|
+
metadata[key] = value
|
|
455
|
+
|
|
456
|
+
return metadata
|
|
457
|
+
|
|
458
|
+
def _parse_object_pair(self, node: Node, code: str) -> tuple[str | None, any]:
|
|
459
|
+
"""Parse object key-value pair."""
|
|
460
|
+
key = None
|
|
461
|
+
value = None
|
|
462
|
+
|
|
463
|
+
children = list(node.children)
|
|
464
|
+
for i, child in enumerate(children):
|
|
465
|
+
if child.type == "property_identifier":
|
|
466
|
+
key = self._get_node_text(child, code)
|
|
467
|
+
elif child.type == "string":
|
|
468
|
+
value = self._extract_string_value(child, code)
|
|
469
|
+
elif child.type == "true":
|
|
470
|
+
value = True
|
|
471
|
+
elif child.type == "false":
|
|
472
|
+
value = False
|
|
473
|
+
elif child.type == "array":
|
|
474
|
+
value = self._parse_array(child, code)
|
|
475
|
+
|
|
476
|
+
return key, value
|
|
477
|
+
|
|
478
|
+
def _parse_array(self, node: Node, code: str) -> list:
|
|
479
|
+
"""Parse array literal."""
|
|
480
|
+
items = []
|
|
481
|
+
for child in node.children:
|
|
482
|
+
if child.type == "identifier":
|
|
483
|
+
items.append(self._get_node_text(child, code))
|
|
484
|
+
return items
|
|
485
|
+
|
|
486
|
+
def _extract_function_calls(self, root_node: Node, code: str) -> list[dict]:
|
|
487
|
+
"""
|
|
488
|
+
Extract function calls from AST (Task 1.6).
|
|
489
|
+
|
|
490
|
+
Args:
|
|
491
|
+
root_node: Root AST node
|
|
492
|
+
code: Source code string
|
|
493
|
+
|
|
494
|
+
Returns:
|
|
495
|
+
List of function call dictionaries
|
|
496
|
+
"""
|
|
497
|
+
calls = []
|
|
498
|
+
self._find_function_calls_recursive(root_node, code, calls)
|
|
499
|
+
return calls
|
|
500
|
+
|
|
501
|
+
def _find_function_calls_recursive(self, node: Node, code: str, results: list):
|
|
502
|
+
"""Recursively find call_expression nodes."""
|
|
503
|
+
if node.type == "call_expression":
|
|
504
|
+
call_data = self._parse_call_expression(node, code)
|
|
505
|
+
if call_data:
|
|
506
|
+
results.append(call_data)
|
|
507
|
+
|
|
508
|
+
for child in node.children:
|
|
509
|
+
self._find_function_calls_recursive(child, code, results)
|
|
510
|
+
|
|
511
|
+
# Known HTTP client wrapper function names to detect as API calls
|
|
512
|
+
HTTP_WRAPPERS = {
|
|
513
|
+
"getJSON", "postJSON", "putJSON", "patchJSON", "deleteJSON",
|
|
514
|
+
"requestJSON", "getBlob", "requestBlob", "postFormDataJSON",
|
|
515
|
+
"fetch", "get", "post", "put", "patch", "delete", "request",
|
|
516
|
+
}
|
|
517
|
+
# Member-expression prefixes that indicate HTTP clients (e.g. axios.get)
|
|
518
|
+
HTTP_PREFIXES = {"axios", "http", "httpClient", "api", "client", "this.http", "this.client"}
|
|
519
|
+
|
|
520
|
+
def _parse_call_expression(self, node: Node, code: str) -> dict | None:
|
|
521
|
+
"""Parse a call_expression node, detecting HTTP wrapper calls."""
|
|
522
|
+
callee = None
|
|
523
|
+
first_string_arg: str | None = None
|
|
524
|
+
|
|
525
|
+
for child in node.children:
|
|
526
|
+
if child.type == "identifier":
|
|
527
|
+
callee = self._get_node_text(child, code)
|
|
528
|
+
elif child.type == "member_expression":
|
|
529
|
+
callee = self._get_node_text(child, code)
|
|
530
|
+
elif child.type == "arguments" and first_string_arg is None:
|
|
531
|
+
first_string_arg = self._extract_first_string_arg(child, code)
|
|
532
|
+
|
|
533
|
+
if not callee:
|
|
534
|
+
return None
|
|
535
|
+
|
|
536
|
+
# Determine if this is an HTTP wrapper call
|
|
537
|
+
is_api_call = False
|
|
538
|
+
callee_name = callee.split(".")[-1] # last segment: axios.get -> get
|
|
539
|
+
callee_prefix = callee.split(".")[0] if "." in callee else ""
|
|
540
|
+
|
|
541
|
+
if callee_name in self.HTTP_WRAPPERS or callee_prefix in self.HTTP_PREFIXES:
|
|
542
|
+
is_api_call = True
|
|
543
|
+
|
|
544
|
+
result: dict = {"callee": callee, "line": node.start_point[0] + 1}
|
|
545
|
+
if is_api_call and first_string_arg and self._is_api_path(first_string_arg):
|
|
546
|
+
result["is_api_call"] = True
|
|
547
|
+
result["api_path"] = first_string_arg
|
|
548
|
+
return result
|
|
549
|
+
|
|
550
|
+
# HTTP method verbs that are NOT API paths
|
|
551
|
+
_HTTP_VERBS = {"GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"}
|
|
552
|
+
|
|
553
|
+
def _is_api_path(self, value: str) -> bool:
|
|
554
|
+
"""Return True if the string looks like an API path, not an HTTP verb or other arg."""
|
|
555
|
+
if not value:
|
|
556
|
+
return False
|
|
557
|
+
if value.upper() in self._HTTP_VERBS:
|
|
558
|
+
return False
|
|
559
|
+
# Must contain at least one slash or letter (e.g. 'user/profile', '/api/v1/items')
|
|
560
|
+
if "/" not in value and not value.replace("-", "").replace("_", "").isalpha():
|
|
561
|
+
return False
|
|
562
|
+
# Reject content-type strings
|
|
563
|
+
if "content-type" in value.lower() or "application/" in value.lower():
|
|
564
|
+
return False
|
|
565
|
+
return True
|
|
566
|
+
|
|
567
|
+
def _extract_first_string_arg(self, arguments_node: Node, code: str) -> str | None:
|
|
568
|
+
"""Extract the first string literal from a call's arguments node."""
|
|
569
|
+
for child in arguments_node.children:
|
|
570
|
+
if child.type == "string":
|
|
571
|
+
return self._extract_string_value(child, code)
|
|
572
|
+
# Template literal — skip (dynamic)
|
|
573
|
+
if child.type == "template_string":
|
|
574
|
+
return None
|
|
575
|
+
return None
|
|
576
|
+
|
|
577
|
+
def _extract_string_literals(self, root_node: Node, code: str) -> list[str]:
|
|
578
|
+
"""
|
|
579
|
+
Extract string literals from AST (Task 1.6).
|
|
580
|
+
|
|
581
|
+
Args:
|
|
582
|
+
root_node: Root AST node
|
|
583
|
+
code: Source code string
|
|
584
|
+
|
|
585
|
+
Returns:
|
|
586
|
+
List of string literal values
|
|
587
|
+
"""
|
|
588
|
+
literals = []
|
|
589
|
+
self._find_string_literals_recursive(root_node, code, literals)
|
|
590
|
+
return literals
|
|
591
|
+
|
|
592
|
+
def _find_string_literals_recursive(self, node: Node, code: str, results: list):
|
|
593
|
+
"""Recursively find string nodes."""
|
|
594
|
+
if node.type == "string":
|
|
595
|
+
value = self._extract_string_value(node, code)
|
|
596
|
+
if value:
|
|
597
|
+
results.append(value)
|
|
598
|
+
|
|
599
|
+
for child in node.children:
|
|
600
|
+
self._find_string_literals_recursive(child, code, results)
|
|
File without changes
|