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.
Files changed (86) hide show
  1. graph_dependency_analyzer-0.2.0.dist-info/METADATA +158 -0
  2. graph_dependency_analyzer-0.2.0.dist-info/RECORD +86 -0
  3. graph_dependency_analyzer-0.2.0.dist-info/WHEEL +5 -0
  4. graph_dependency_analyzer-0.2.0.dist-info/entry_points.txt +2 -0
  5. graph_dependency_analyzer-0.2.0.dist-info/top_level.txt +1 -0
  6. src/__init__.py +0 -0
  7. src/application/__init__.py +0 -0
  8. src/application/dtos/__init__.py +0 -0
  9. src/application/dtos/affected_component.py +59 -0
  10. src/application/dtos/batch_processing_result.py +17 -0
  11. src/application/dtos/impact_analysis_response.py +56 -0
  12. src/application/dtos/impact_query_request.py +38 -0
  13. src/application/dtos/index_batch_request.py +53 -0
  14. src/application/dtos/index_batch_response.py +72 -0
  15. src/application/dtos/index_result.py +20 -0
  16. src/application/dtos/repository_index_result.py +79 -0
  17. src/application/exceptions.py +41 -0
  18. src/application/services/__init__.py +0 -0
  19. src/application/services/file_scanner_service.py +258 -0
  20. src/application/services/progress_tracker.py +132 -0
  21. src/application/use_cases/__init__.py +0 -0
  22. src/application/use_cases/clear_all_data_use_case.py +180 -0
  23. src/application/use_cases/index_batch_use_case.py +153 -0
  24. src/application/use_cases/index_repository_use_case.py +683 -0
  25. src/application/use_cases/indexing_orchestrator.py +181 -0
  26. src/application/use_cases/list_repositories_use_case.py +84 -0
  27. src/cli.py +512 -0
  28. src/config/__init__.py +5 -0
  29. src/config/container.py +211 -0
  30. src/config/logging_config.py +123 -0
  31. src/config/settings.py +165 -0
  32. src/domain/__init__.py +0 -0
  33. src/domain/entities/__init__.py +20 -0
  34. src/domain/entities/ast_node.py +32 -0
  35. src/domain/entities/code_chunk.py +45 -0
  36. src/domain/entities/code_file.py +44 -0
  37. src/domain/entities/dependency_graph.py +110 -0
  38. src/domain/entities/repository.py +46 -0
  39. src/domain/exceptions.py +36 -0
  40. src/domain/repositories/__init__.py +22 -0
  41. src/domain/repositories/i_code_parser.py +50 -0
  42. src/domain/repositories/i_dependency_extractor.py +35 -0
  43. src/domain/repositories/i_embedding_provider.py +31 -0
  44. src/domain/repositories/i_graph_repository.py +78 -0
  45. src/domain/repositories/i_vector_repository.py +55 -0
  46. src/domain/services/__init__.py +8 -0
  47. src/domain/services/dependency_extractor.py +962 -0
  48. src/domain/services/semantic_chunk_builder.py +719 -0
  49. src/domain/value_objects/__init__.py +12 -0
  50. src/domain/value_objects/chunk_type.py +24 -0
  51. src/domain/value_objects/dependency_type.py +22 -0
  52. src/domain/value_objects/node_type.py +22 -0
  53. src/domain/value_objects/qualified_name.py +78 -0
  54. src/infrastructure/__init__.py +0 -0
  55. src/infrastructure/exceptions.py +41 -0
  56. src/infrastructure/external_apis/__init__.py +0 -0
  57. src/infrastructure/external_apis/ollama_embedding_provider.py +134 -0
  58. src/infrastructure/external_apis/openai_embedding_provider.py +231 -0
  59. src/infrastructure/parsing/__init__.py +20 -0
  60. src/infrastructure/parsing/config_file_parser.py +347 -0
  61. src/infrastructure/parsing/gradle_dependency_parser.py +159 -0
  62. src/infrastructure/parsing/groovy_parser.py +198 -0
  63. src/infrastructure/parsing/maven_dependency_parser.py +147 -0
  64. src/infrastructure/parsing/microfrontend_config_parser.py +242 -0
  65. src/infrastructure/parsing/npm_package_dependency_parser.py +255 -0
  66. src/infrastructure/parsing/python_parser.py +211 -0
  67. src/infrastructure/parsing/sql_schema_parser.py +658 -0
  68. src/infrastructure/parsing/tree_sitter_java_parser.py +434 -0
  69. src/infrastructure/parsing/tree_sitter_typescript_parser.py +600 -0
  70. src/infrastructure/persistence/__init__.py +0 -0
  71. src/infrastructure/persistence/graph_export_repository.py +443 -0
  72. src/infrastructure/persistence/neo4j_dependency_repository.py +657 -0
  73. src/infrastructure/persistence/qdrant_vector_repository.py +357 -0
  74. src/infrastructure/persistence/repository_relationship_repository.py +1312 -0
  75. src/presentation/__init__.py +0 -0
  76. src/presentation/api/__init__.py +0 -0
  77. src/presentation/api/rest/__init__.py +0 -0
  78. src/presentation/api/rest/exception_handlers.py +174 -0
  79. src/presentation/api/rest/main.py +80 -0
  80. src/presentation/api/rest/routers/__init__.py +5 -0
  81. src/presentation/api/rest/routers/admin.py +103 -0
  82. src/presentation/api/rest/routers/analysis.py +980 -0
  83. src/presentation/api/rest/routers/export.py +442 -0
  84. src/presentation/api/rest/routers/health.py +117 -0
  85. src/presentation/api/rest/routers/indexing.py +148 -0
  86. 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