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,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]
|