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,434 @@
1
+ """TreeSitterJavaParser - Real Tree-sitter based Java AST parser.
2
+
3
+ Uses tree-sitter and tree-sitter-java for precise AST parsing via tree traversal.
4
+ """
5
+ import re
6
+ from pathlib import Path
7
+ from typing import Any
8
+
9
+ from tree_sitter import Language, Parser, Node
10
+ import tree_sitter_java
11
+
12
+ from src.domain.repositories.i_code_parser import ICodeParser, ParseResult
13
+
14
+
15
+ class ParseError(Exception):
16
+ """Exception raised when parsing fails."""
17
+ pass
18
+
19
+
20
+ class TreeSitterJavaParser:
21
+ """
22
+ Java parser implementation using Tree-sitter library.
23
+
24
+ Extracts AST structures from Java source code using tree traversal.
25
+ """
26
+
27
+ # Regex patterns for URL and JDBC detection
28
+ URL_PATTERN = re.compile(r'https?://[^\s"\'<>]+')
29
+ JDBC_PATTERN = re.compile(r'jdbc:([^:]+)://([^:/\s"\']+):?(\d+)?/([^\s"\']+)')
30
+
31
+ def __init__(self):
32
+ """Initialize Tree-sitter parser with Java grammar."""
33
+ # Load Java language
34
+ self.language = Language(tree_sitter_java.language())
35
+ self.parser = Parser(self.language)
36
+
37
+ def parse_file(self, file_path: Path) -> ParseResult:
38
+ """
39
+ Parse a Java file and extract AST structures using real tree-sitter.
40
+
41
+ Args:
42
+ file_path: Path to Java file
43
+
44
+ Returns:
45
+ ParseResult with extracted structures
46
+
47
+ Raises:
48
+ FileNotFoundError: If file doesn't exist
49
+ ParseError: If parsing fails
50
+ """
51
+ if not file_path.exists():
52
+ raise FileNotFoundError(f"File not found: {file_path}")
53
+
54
+ try:
55
+ code_bytes = file_path.read_bytes()
56
+ code_str = code_bytes.decode('utf-8')
57
+ except Exception as e:
58
+ raise ParseError(f"Failed to read file: {e}")
59
+
60
+ # Parse with tree-sitter - REAL AST PARSING
61
+ tree = self.parser.parse(code_bytes)
62
+
63
+ # Check for parse errors
64
+ if tree.root_node.has_error:
65
+ raise ParseError(f"Syntax errors in Java file: {file_path}")
66
+
67
+ # Extract structures via tree traversal
68
+ package = self._extract_package(tree.root_node, code_str)
69
+ classes = self._extract_classes(tree.root_node, code_str, package)
70
+ methods = self._extract_methods(tree.root_node, code_str, package)
71
+ imports = self._extract_imports(tree.root_node, code_str)
72
+ string_literals = self._extract_string_literals(tree.root_node, code_str)
73
+ method_calls = self._extract_method_calls(tree.root_node, code_str)
74
+ endpoints = self._extract_spring_endpoints(tree.root_node, code_str, package)
75
+
76
+ # Attach endpoints as extra_data on method_calls so the pipeline can pick them up
77
+ for ep in endpoints:
78
+ method_calls.append({**ep, "_is_endpoint": True})
79
+
80
+ return ParseResult(
81
+ file_path=file_path,
82
+ classes=classes,
83
+ methods=methods,
84
+ imports=imports,
85
+ method_calls=method_calls,
86
+ string_literals=string_literals
87
+ )
88
+
89
+ def _extract_package(self, root: Node, code: str) -> str:
90
+ """Extract package declaration by traversing AST."""
91
+ for child in root.children:
92
+ if child.type == "package_declaration":
93
+ for subchild in child.children:
94
+ if subchild.type == "scoped_identifier":
95
+ return self._get_node_text(subchild, code)
96
+ return ""
97
+
98
+ def _extract_classes(self, root: Node, code: str, package: str) -> list[dict]:
99
+ """Extract class and interface declarations by traversing AST."""
100
+ classes = []
101
+ self._find_classes_recursive(root, code, package, classes)
102
+ return classes
103
+
104
+ def _find_classes_recursive(self, node: Node, code: str, package: str, results: list):
105
+ """Recursively find class and interface declarations."""
106
+ if node.type == "class_declaration":
107
+ for child in node.children:
108
+ if child.type == "identifier":
109
+ class_name = self._get_node_text(child, code)
110
+ qualified_name = f"{package}.{class_name}" if package else class_name
111
+ results.append({
112
+ "name": class_name,
113
+ "qualified_name": qualified_name,
114
+ "type": "class",
115
+ "line": child.start_point[0] + 1
116
+ })
117
+ break
118
+
119
+ elif node.type == "interface_declaration":
120
+ for child in node.children:
121
+ if child.type == "identifier":
122
+ interface_name = self._get_node_text(child, code)
123
+ qualified_name = f"{package}.{interface_name}" if package else interface_name
124
+ results.append({
125
+ "name": interface_name,
126
+ "qualified_name": qualified_name,
127
+ "type": "interface",
128
+ "line": child.start_point[0] + 1
129
+ })
130
+ break
131
+
132
+ # Recurse into children
133
+ for child in node.children:
134
+ self._find_classes_recursive(child, code, package, results)
135
+
136
+ def _extract_methods(self, root: Node, code: str, package: str) -> list[dict]:
137
+ """Extract method declarations by traversing AST."""
138
+ methods = []
139
+ self._find_methods_recursive(root, code, package, methods)
140
+ return methods
141
+
142
+ def _find_methods_recursive(self, node: Node, code: str, package: str, results: list):
143
+ """Recursively find method declarations and constructors."""
144
+ if node.type == "method_declaration":
145
+ method_info = self._parse_method_node(node, code, package)
146
+ if method_info:
147
+ results.append(method_info)
148
+ elif node.type == "constructor_declaration":
149
+ constructor_info = self._parse_constructor_node(node, code, package)
150
+ if constructor_info:
151
+ results.append(constructor_info)
152
+
153
+ # Recurse into children
154
+ for child in node.children:
155
+ self._find_methods_recursive(child, code, package, results)
156
+
157
+ def _parse_method_node(self, node: Node, code: str, package: str) -> dict | None:
158
+ """Parse a method_declaration node."""
159
+ method_name = None
160
+ return_type = "void"
161
+ parameters = []
162
+
163
+ for child in node.children:
164
+ if child.type == "identifier":
165
+ method_name = self._get_node_text(child, code)
166
+ elif child.type in ("type_identifier", "void_type", "integral_type",
167
+ "floating_point_type", "boolean_type", "generic_type"):
168
+ return_type = self._get_node_text(child, code)
169
+ elif child.type == "formal_parameters":
170
+ parameters = self._parse_parameters(child, code)
171
+
172
+ if not method_name:
173
+ return None
174
+
175
+ # Find containing class
176
+ class_name = self._find_containing_class(node, code)
177
+ qualified_name = f"{package}.{class_name}.{method_name}" if package else f"{class_name}.{method_name}"
178
+
179
+ return {
180
+ "name": method_name,
181
+ "qualified_name": qualified_name,
182
+ "return_type": return_type,
183
+ "parameters": parameters,
184
+ "parent_class": class_name,
185
+ "line": node.start_point[0] + 1
186
+ }
187
+
188
+ def _parse_constructor_node(self, node: Node, code: str, package: str) -> dict | None:
189
+ """Parse a constructor_declaration node."""
190
+ constructor_name = None
191
+ parameters = []
192
+
193
+ for child in node.children:
194
+ if child.type == "identifier":
195
+ constructor_name = self._get_node_text(child, code)
196
+ elif child.type == "formal_parameters":
197
+ parameters = self._parse_parameters(child, code)
198
+
199
+ if not constructor_name:
200
+ return None
201
+
202
+ # Find containing class
203
+ class_name = self._find_containing_class(node, code)
204
+ qualified_name = f"{package}.{class_name}.{constructor_name}" if package else f"{class_name}.{constructor_name}"
205
+
206
+ return {
207
+ "name": constructor_name,
208
+ "qualified_name": qualified_name,
209
+ "return_type": class_name, # Constructors return instance of the class
210
+ "parameters": parameters,
211
+ "parent_class": class_name,
212
+ "line": node.start_point[0] + 1
213
+ }
214
+
215
+ def _parse_parameters(self, params_node: Node, code: str) -> list[dict]:
216
+ """Parse formal_parameters node."""
217
+ parameters = []
218
+
219
+ for child in params_node.children:
220
+ if child.type == "formal_parameter":
221
+ param_type = None
222
+ param_name = None
223
+
224
+ for part in child.children:
225
+ if part.type in ("type_identifier", "integral_type", "generic_type",
226
+ "floating_point_type", "boolean_type"):
227
+ param_type = self._get_node_text(part, code)
228
+ elif part.type == "identifier":
229
+ param_name = self._get_node_text(part, code)
230
+
231
+ if param_type and param_name:
232
+ parameters.append({
233
+ "name": param_name,
234
+ "type": param_type
235
+ })
236
+
237
+ return parameters
238
+
239
+ def _extract_imports(self, root: Node, code: str) -> list[dict]:
240
+ """Extract import statements by traversing AST."""
241
+ imports = []
242
+ self._find_imports_recursive(root, code, imports)
243
+ return imports
244
+
245
+ def _find_imports_recursive(self, node: Node, code: str, results: list):
246
+ """Recursively find import declarations."""
247
+ if node.type == "import_declaration":
248
+ for child in node.children:
249
+ if child.type in ("scoped_identifier", "identifier"):
250
+ import_path = self._get_node_text(child, code)
251
+ results.append({
252
+ "import_path": import_path,
253
+ "line": child.start_point[0] + 1,
254
+ "is_static": False
255
+ })
256
+ break
257
+
258
+ # Recurse
259
+ for child in node.children:
260
+ self._find_imports_recursive(child, code, results)
261
+
262
+ def _extract_string_literals(self, root: Node, code: str) -> list[str]:
263
+ """Extract string literals by traversing AST."""
264
+ literals = []
265
+ self._find_string_literals_recursive(root, code, literals)
266
+ return list(set(literals))
267
+
268
+ def _find_string_literals_recursive(self, node: Node, code: str, results: list):
269
+ """Recursively find string literals."""
270
+ if node.type == "string_literal":
271
+ text = self._get_node_text(node, code)
272
+ # Remove quotes
273
+ if text.startswith('"') and text.endswith('"'):
274
+ text = text[1:-1]
275
+ results.append(text)
276
+
277
+ # Recurse
278
+ for child in node.children:
279
+ self._find_string_literals_recursive(child, code, results)
280
+
281
+ def _extract_method_calls(self, root: Node, code: str) -> list[dict]:
282
+ """Extract method invocations by traversing AST."""
283
+ calls = []
284
+ self._find_method_calls_recursive(root, code, calls)
285
+ return calls
286
+
287
+ def _find_method_calls_recursive(self, node: Node, code: str, results: list):
288
+ """Recursively find method invocations."""
289
+ if node.type == "method_invocation":
290
+ method_name = None
291
+ object_name = "unknown"
292
+
293
+ for child in node.children:
294
+ if child.type == "identifier":
295
+ method_name = self._get_node_text(child, code)
296
+ elif child.type in ("field_access", "identifier"):
297
+ object_name = self._get_node_text(child, code)
298
+
299
+ if method_name:
300
+ results.append({
301
+ "object": object_name,
302
+ "method": method_name,
303
+ "line": node.start_point[0] + 1
304
+ })
305
+
306
+ # Recurse
307
+ for child in node.children:
308
+ self._find_method_calls_recursive(child, code, results)
309
+
310
+ # Spring annotation names that map to HTTP endpoints
311
+ _SPRING_ENDPOINT_ANNOTATIONS = {
312
+ "RequestMapping", "GetMapping", "PostMapping",
313
+ "PutMapping", "PatchMapping", "DeleteMapping",
314
+ }
315
+ _ANNOTATION_HTTP_METHOD = {
316
+ "GetMapping": "GET", "PostMapping": "POST", "PutMapping": "PUT",
317
+ "PatchMapping": "PATCH", "DeleteMapping": "DELETE",
318
+ "RequestMapping": "ANY",
319
+ }
320
+
321
+ def _extract_spring_endpoints(self, root: Node, code: str, package: str) -> list[dict]:
322
+ """
323
+ Extract Spring MVC REST endpoints from class and method annotations.
324
+
325
+ Strategy:
326
+ 1. Find @RequestMapping on a class → base_path
327
+ 2. Find @*Mapping on each method → method_path
328
+ 3. Combine: full_path = base_path + method_path
329
+ """
330
+ endpoints: list[dict] = []
331
+
332
+ def annotation_name_and_path(annotation_node: Node) -> tuple[str | None, str | None]:
333
+ name = None
334
+ path = None
335
+ for child in annotation_node.children:
336
+ if child.type == "identifier":
337
+ name = self._get_node_text(child, code)
338
+ elif child.type == "type_identifier":
339
+ name = self._get_node_text(child, code)
340
+ elif child.type == "annotation_argument_list":
341
+ # value="..." or ("...") or (value="...")
342
+ for arg in child.children:
343
+ if arg.type == "string_literal":
344
+ raw = self._get_node_text(arg, code).strip('"')
345
+ path = raw
346
+ elif arg.type == "element_value_pair":
347
+ key_node = None
348
+ val_node = None
349
+ for sub in arg.children:
350
+ if sub.type == "identifier":
351
+ key_node = self._get_node_text(sub, code)
352
+ elif sub.type == "string_literal":
353
+ val_node = self._get_node_text(sub, code).strip('"')
354
+ if key_node in (None, "value", "path") and val_node:
355
+ path = val_node
356
+ return name, path
357
+
358
+ def collect_class_endpoints(class_node: Node):
359
+ base_path = ""
360
+ class_name = "Unknown"
361
+
362
+ # Get class name
363
+ for child in class_node.children:
364
+ if child.type == "identifier":
365
+ class_name = self._get_node_text(child, code)
366
+
367
+ # Scan annotations before the class body
368
+ for child in class_node.children:
369
+ if child.type == "modifiers":
370
+ for mod in child.children:
371
+ if mod.type == "annotation":
372
+ aname, apath = annotation_name_and_path(mod)
373
+ if aname == "RequestMapping" and apath:
374
+ base_path = apath.rstrip("/")
375
+
376
+ # Scan method declarations for HTTP mappings
377
+ qualified_class = f"{package}.{class_name}" if package else class_name
378
+ for child in class_node.children:
379
+ if child.type == "class_body":
380
+ for body_child in child.children:
381
+ if body_child.type == "method_declaration":
382
+ method_annotations: list[tuple[str, str]] = []
383
+ method_name = None
384
+
385
+ for mc in body_child.children:
386
+ if mc.type == "modifiers":
387
+ for mod in mc.children:
388
+ if mod.type == "annotation":
389
+ aname, apath = annotation_name_and_path(mod)
390
+ if aname in self._SPRING_ENDPOINT_ANNOTATIONS:
391
+ method_annotations.append(
392
+ (aname, apath or "")
393
+ )
394
+ elif mc.type == "identifier":
395
+ method_name = self._get_node_text(mc, code)
396
+
397
+ for aname, method_path in method_annotations:
398
+ full_path = (base_path + "/" + method_path.lstrip("/")).rstrip("/")
399
+ if not full_path.startswith("/"):
400
+ full_path = "/" + full_path
401
+ http_method = self._ANNOTATION_HTTP_METHOD.get(aname, "ANY")
402
+ endpoints.append({
403
+ "path": full_path,
404
+ "http_method": http_method,
405
+ "class_name": qualified_class,
406
+ "method_name": method_name or "",
407
+ "line": body_child.start_point[0] + 1,
408
+ })
409
+
410
+ def traverse(node: Node):
411
+ if node.type == "class_declaration":
412
+ collect_class_endpoints(node)
413
+ for child in node.children:
414
+ traverse(child)
415
+
416
+ traverse(root)
417
+ return endpoints
418
+
419
+ def _find_containing_class(self, node: Node, code: str) -> str:
420
+ """Find the class that contains the given node by walking up the tree."""
421
+ current = node.parent
422
+
423
+ while current:
424
+ if current.type in ("class_declaration", "interface_declaration"):
425
+ for child in current.children:
426
+ if child.type == "identifier":
427
+ return self._get_node_text(child, code)
428
+ current = current.parent
429
+
430
+ return "Unknown"
431
+
432
+ def _get_node_text(self, node: Node, code: str) -> str:
433
+ """Get text content of a tree-sitter node."""
434
+ return code[node.start_byte:node.end_byte]