codemap-java 0.2.2__tar.gz → 0.3.0__tar.gz
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.
- {codemap_java-0.2.2 → codemap_java-0.3.0}/PKG-INFO +2 -2
- {codemap_java-0.2.2 → codemap_java-0.3.0}/pyproject.toml +5 -2
- codemap_java-0.3.0/src/codemap_java/indexer.py +589 -0
- codemap_java-0.3.0/src/codemap_java/resolver.py +285 -0
- codemap_java-0.3.0/tests/test_annotations.py +155 -0
- codemap_java-0.3.0/tests/test_indexer_extras.py +257 -0
- codemap_java-0.3.0/tests/test_indexer_types.py +161 -0
- codemap_java-0.3.0/tests/test_resolver.py +321 -0
- codemap_java-0.3.0/tests/test_spring_routes.py +163 -0
- codemap_java-0.2.2/src/codemap_java/indexer.py +0 -254
- {codemap_java-0.2.2 → codemap_java-0.3.0}/.gitignore +0 -0
- {codemap_java-0.2.2 → codemap_java-0.3.0}/README.md +0 -0
- {codemap_java-0.2.2 → codemap_java-0.3.0}/src/codemap_java/__init__.py +0 -0
- {codemap_java-0.2.2 → codemap_java-0.3.0}/tests/__init__.py +0 -0
- {codemap_java-0.2.2 → codemap_java-0.3.0}/tests/test_indexer.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: codemap-java
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: Java indexer plugin for CodeMap
|
|
5
5
|
Project-URL: Homepage, https://github.com/qxbyte/codemap
|
|
6
6
|
Author: CodeMap Contributors
|
|
@@ -11,7 +11,7 @@ Classifier: Programming Language :: Java
|
|
|
11
11
|
Classifier: Programming Language :: Python :: 3
|
|
12
12
|
Classifier: Topic :: Software Development
|
|
13
13
|
Requires-Python: >=3.11
|
|
14
|
-
Requires-Dist: codemap-core<0.
|
|
14
|
+
Requires-Dist: codemap-core<0.4,>=0.3.0
|
|
15
15
|
Requires-Dist: tree-sitter-java>=0.23
|
|
16
16
|
Requires-Dist: tree-sitter>=0.25
|
|
17
17
|
Provides-Extra: dev
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "codemap-java"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.3.0"
|
|
8
8
|
description = "Java indexer plugin for CodeMap"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.11"
|
|
@@ -18,7 +18,7 @@ classifiers = [
|
|
|
18
18
|
"Topic :: Software Development",
|
|
19
19
|
]
|
|
20
20
|
dependencies = [
|
|
21
|
-
"codemap-core>=0.
|
|
21
|
+
"codemap-core>=0.3.0,<0.4",
|
|
22
22
|
"tree-sitter>=0.25",
|
|
23
23
|
"tree-sitter-java>=0.23",
|
|
24
24
|
]
|
|
@@ -29,6 +29,9 @@ dev = ["pytest>=8.0"]
|
|
|
29
29
|
[project.entry-points."codemap.indexers"]
|
|
30
30
|
java = "codemap_java:JavaIndexer"
|
|
31
31
|
|
|
32
|
+
[project.entry-points."codemap.bridges"]
|
|
33
|
+
java_calls = "codemap_java.resolver:JavaCallResolverBridge"
|
|
34
|
+
|
|
32
35
|
[project.urls]
|
|
33
36
|
Homepage = "https://github.com/qxbyte/codemap"
|
|
34
37
|
|
|
@@ -0,0 +1,589 @@
|
|
|
1
|
+
"""Java indexer built on tree-sitter-java.
|
|
2
|
+
|
|
3
|
+
Covers class / interface / enum / record / method / constructor / field
|
|
4
|
+
declarations. Package declarations are honoured as a namespace prefix
|
|
5
|
+
under the file path. Nested types track a class stack to produce the
|
|
6
|
+
correct ``Cls#Inner#m()`` chain.
|
|
7
|
+
|
|
8
|
+
The indexer is single-file by design; cross-file ``extends`` / ``implements``
|
|
9
|
+
and call-graph resolution lives in :class:`codemap.core.bridge.java_calls
|
|
10
|
+
.JavaCallResolverBridge`. To enable that resolver, the indexer attaches three
|
|
11
|
+
metadata keys to ``Symbol.extra`` (ADR-0013):
|
|
12
|
+
|
|
13
|
+
* top-level type symbols carry ``imports`` (list[str], fully qualified)
|
|
14
|
+
* top-level type symbols carry ``supertypes`` (list of
|
|
15
|
+
``{"name": str, "relation": "extends"|"implements"}``)
|
|
16
|
+
* method / constructor symbols carry ``pending_calls`` — a list of raw
|
|
17
|
+
invocation records ``{"receiver", "name", "arity", "line", "col"}`` for
|
|
18
|
+
the bridge to FQN-resolve.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
from pathlib import Path, PurePosixPath
|
|
24
|
+
from typing import ClassVar
|
|
25
|
+
|
|
26
|
+
import tree_sitter
|
|
27
|
+
import tree_sitter_java
|
|
28
|
+
|
|
29
|
+
from codemap.core.models import Annotation, Diagnostic, Edge, IndexResult, Range, Symbol
|
|
30
|
+
from codemap.core.symbol import Descriptor, DescriptorKind, SymbolID
|
|
31
|
+
from codemap.indexers.base import IndexContext
|
|
32
|
+
|
|
33
|
+
SCHEME = "scip-java"
|
|
34
|
+
LANG = "java"
|
|
35
|
+
|
|
36
|
+
_JAVA_LANG = tree_sitter.Language(tree_sitter_java.language())
|
|
37
|
+
|
|
38
|
+
_TYPE_DECLS = frozenset(
|
|
39
|
+
{
|
|
40
|
+
"class_declaration",
|
|
41
|
+
"interface_declaration",
|
|
42
|
+
"enum_declaration",
|
|
43
|
+
"record_declaration",
|
|
44
|
+
}
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class JavaIndexer:
|
|
49
|
+
name: ClassVar[str] = "java"
|
|
50
|
+
version: ClassVar[str] = "0.1.0"
|
|
51
|
+
file_patterns: ClassVar[list[str]] = ["*.java"]
|
|
52
|
+
languages: ClassVar[list[str]] = [LANG]
|
|
53
|
+
|
|
54
|
+
def supports(self, path: Path) -> bool:
|
|
55
|
+
return path.suffix == ".java"
|
|
56
|
+
|
|
57
|
+
def index_file(
|
|
58
|
+
self,
|
|
59
|
+
path: Path,
|
|
60
|
+
source: bytes,
|
|
61
|
+
ctx: IndexContext,
|
|
62
|
+
) -> IndexResult:
|
|
63
|
+
try:
|
|
64
|
+
source.decode("utf-8")
|
|
65
|
+
except UnicodeDecodeError as exc:
|
|
66
|
+
return IndexResult(
|
|
67
|
+
diagnostics=[
|
|
68
|
+
Diagnostic(
|
|
69
|
+
severity="error",
|
|
70
|
+
file=ctx.relative_path,
|
|
71
|
+
code="JAVA002",
|
|
72
|
+
message=f"not valid UTF-8: {exc}",
|
|
73
|
+
producer=self.name,
|
|
74
|
+
)
|
|
75
|
+
]
|
|
76
|
+
)
|
|
77
|
+
parser = tree_sitter.Parser(_JAVA_LANG)
|
|
78
|
+
tree = parser.parse(source)
|
|
79
|
+
visitor = _Visitor(ctx.relative_path)
|
|
80
|
+
visitor.visit(tree.root_node)
|
|
81
|
+
diagnostics = list(visitor.diagnostics)
|
|
82
|
+
if tree.root_node.has_error:
|
|
83
|
+
diagnostics.append(
|
|
84
|
+
Diagnostic(
|
|
85
|
+
severity="warning",
|
|
86
|
+
file=ctx.relative_path,
|
|
87
|
+
range=Range(start_line=1, end_line=1),
|
|
88
|
+
code="JAVA001",
|
|
89
|
+
message="tree-sitter reported parse errors; symbols may be incomplete",
|
|
90
|
+
producer=self.name,
|
|
91
|
+
)
|
|
92
|
+
)
|
|
93
|
+
return IndexResult(
|
|
94
|
+
symbols=visitor.symbols,
|
|
95
|
+
edges=visitor.edges,
|
|
96
|
+
diagnostics=diagnostics,
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
# ---------------------------------------------------------------------------
|
|
101
|
+
# AST visitor
|
|
102
|
+
# ---------------------------------------------------------------------------
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
class _Visitor:
|
|
106
|
+
def __init__(self, relative_path: PurePosixPath) -> None:
|
|
107
|
+
self.relative_path = relative_path
|
|
108
|
+
self.symbols: list[Symbol] = []
|
|
109
|
+
self.edges: list[Edge] = []
|
|
110
|
+
self.diagnostics: list[Diagnostic] = []
|
|
111
|
+
self._class_stack: list[str] = []
|
|
112
|
+
self._class_annos_stack: list[list[Annotation]] = []
|
|
113
|
+
self._package: str = ""
|
|
114
|
+
self._file_imports: list[str] = []
|
|
115
|
+
|
|
116
|
+
def visit(self, node: tree_sitter.Node) -> None:
|
|
117
|
+
if node.type == "package_declaration":
|
|
118
|
+
self._package = _node_text(node.children[1]) if node.child_count > 1 else ""
|
|
119
|
+
return
|
|
120
|
+
if node.type == "import_declaration":
|
|
121
|
+
imp = _parse_import(node)
|
|
122
|
+
if imp:
|
|
123
|
+
self._file_imports.append(imp)
|
|
124
|
+
return
|
|
125
|
+
if node.type in _TYPE_DECLS:
|
|
126
|
+
self._visit_type(node)
|
|
127
|
+
return
|
|
128
|
+
if node.type == "method_declaration" and self._class_stack:
|
|
129
|
+
self._visit_method(node, is_constructor=False)
|
|
130
|
+
return
|
|
131
|
+
if node.type == "constructor_declaration" and self._class_stack:
|
|
132
|
+
self._visit_method(node, is_constructor=True)
|
|
133
|
+
return
|
|
134
|
+
if node.type == "field_declaration" and self._class_stack:
|
|
135
|
+
self._visit_field(node)
|
|
136
|
+
return
|
|
137
|
+
for child in node.children:
|
|
138
|
+
self.visit(child)
|
|
139
|
+
|
|
140
|
+
# ------------------------------------------------------------- types
|
|
141
|
+
|
|
142
|
+
def _visit_type(self, node: tree_sitter.Node) -> None:
|
|
143
|
+
name = _name_child(node)
|
|
144
|
+
if name is None:
|
|
145
|
+
return
|
|
146
|
+
java_kind = node.type.removesuffix("_declaration")
|
|
147
|
+
is_top_level = not self._class_stack
|
|
148
|
+
sid = self._make_id(name, kind=DescriptorKind.TYPE)
|
|
149
|
+
annotations = _parse_annotations(node)
|
|
150
|
+
extra: dict[str, object] = {}
|
|
151
|
+
if self._package or java_kind != "class":
|
|
152
|
+
extra["java_kind"] = java_kind
|
|
153
|
+
extra["package"] = self._package
|
|
154
|
+
if is_top_level:
|
|
155
|
+
extra["imports"] = list(self._file_imports)
|
|
156
|
+
extra["supertypes"] = _parse_supertypes(node)
|
|
157
|
+
self.symbols.append(
|
|
158
|
+
Symbol(
|
|
159
|
+
id=sid,
|
|
160
|
+
kind="class", # Symbol schema has no separate interface/enum kind
|
|
161
|
+
language=LANG,
|
|
162
|
+
file=self.relative_path,
|
|
163
|
+
range=_node_range(node),
|
|
164
|
+
annotations=annotations,
|
|
165
|
+
extra=extra,
|
|
166
|
+
)
|
|
167
|
+
)
|
|
168
|
+
body = node.child_by_field_name("body")
|
|
169
|
+
if body is None:
|
|
170
|
+
return
|
|
171
|
+
self._class_stack.append(name)
|
|
172
|
+
self._class_annos_stack.append(annotations)
|
|
173
|
+
try:
|
|
174
|
+
for child in body.children:
|
|
175
|
+
self.visit(child)
|
|
176
|
+
finally:
|
|
177
|
+
self._class_stack.pop()
|
|
178
|
+
self._class_annos_stack.pop()
|
|
179
|
+
|
|
180
|
+
# ----------------------------------------------------------- members
|
|
181
|
+
|
|
182
|
+
def _visit_method(self, node: tree_sitter.Node, *, is_constructor: bool) -> None:
|
|
183
|
+
name = _name_child(node)
|
|
184
|
+
if name is None:
|
|
185
|
+
return
|
|
186
|
+
if is_constructor:
|
|
187
|
+
display = "<init>"
|
|
188
|
+
sid = self._make_id(display, kind=DescriptorKind.METHOD)
|
|
189
|
+
else:
|
|
190
|
+
display = name
|
|
191
|
+
sid = self._make_id(name, kind=DescriptorKind.METHOD)
|
|
192
|
+
signature = _method_signature(node, name, is_constructor=is_constructor)
|
|
193
|
+
body = node.child_by_field_name("body")
|
|
194
|
+
pending_calls = _collect_invocations(body) if body is not None else []
|
|
195
|
+
params = _parse_formal_parameters(node.child_by_field_name("parameters"))
|
|
196
|
+
method_annos = _parse_annotations(node)
|
|
197
|
+
extra: dict[str, object] = {"params": params}
|
|
198
|
+
if not is_constructor:
|
|
199
|
+
ret = node.child_by_field_name("type")
|
|
200
|
+
if ret is not None:
|
|
201
|
+
extra["return_type"] = _strip_generics(_node_text(ret))
|
|
202
|
+
if pending_calls:
|
|
203
|
+
extra["pending_calls"] = pending_calls
|
|
204
|
+
class_annos = self._class_annos_stack[-1] if self._class_annos_stack else []
|
|
205
|
+
route = _http_route_meta(class_annos, method_annos)
|
|
206
|
+
if route is not None:
|
|
207
|
+
extra["http_route"] = route
|
|
208
|
+
self.symbols.append(
|
|
209
|
+
Symbol(
|
|
210
|
+
id=sid,
|
|
211
|
+
kind="method",
|
|
212
|
+
language=LANG,
|
|
213
|
+
file=self.relative_path,
|
|
214
|
+
range=_node_range(node),
|
|
215
|
+
signature=signature,
|
|
216
|
+
annotations=method_annos,
|
|
217
|
+
extra=extra,
|
|
218
|
+
)
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
def _visit_field(self, node: tree_sitter.Node) -> None:
|
|
222
|
+
type_node = node.child_by_field_name("type")
|
|
223
|
+
type_str = _strip_generics(_node_text(type_node)) if type_node is not None else ""
|
|
224
|
+
for child in node.children:
|
|
225
|
+
if child.type != "variable_declarator":
|
|
226
|
+
continue
|
|
227
|
+
name_node = child.child_by_field_name("name")
|
|
228
|
+
if name_node is None:
|
|
229
|
+
continue
|
|
230
|
+
name = _node_text(name_node)
|
|
231
|
+
if not name:
|
|
232
|
+
continue
|
|
233
|
+
sid = self._make_id(name, kind=DescriptorKind.TERM)
|
|
234
|
+
extra: dict[str, object] = {}
|
|
235
|
+
if type_str:
|
|
236
|
+
extra["type"] = type_str
|
|
237
|
+
self.symbols.append(
|
|
238
|
+
Symbol(
|
|
239
|
+
id=sid,
|
|
240
|
+
kind="field",
|
|
241
|
+
language=LANG,
|
|
242
|
+
file=self.relative_path,
|
|
243
|
+
range=_node_range(child),
|
|
244
|
+
extra=extra,
|
|
245
|
+
)
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
# ----------------------------------------------------------- helpers
|
|
249
|
+
|
|
250
|
+
def _make_id(self, name: str, *, kind: DescriptorKind) -> SymbolID:
|
|
251
|
+
descriptors = list(_path_namespaces(self.relative_path))
|
|
252
|
+
descriptors.extend(
|
|
253
|
+
Descriptor(name=cls, kind=DescriptorKind.TYPE) for cls in self._class_stack
|
|
254
|
+
)
|
|
255
|
+
descriptors.append(Descriptor(name=name, kind=kind))
|
|
256
|
+
return SymbolID(scheme=SCHEME, descriptors=tuple(descriptors))
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
# ---------------------------------------------------------------------------
|
|
260
|
+
# Pure helpers
|
|
261
|
+
# ---------------------------------------------------------------------------
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def _path_namespaces(path: PurePosixPath) -> list[Descriptor]:
|
|
265
|
+
return [Descriptor(name=part, kind=DescriptorKind.NAMESPACE) for part in path.parts]
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def _node_range(node: tree_sitter.Node) -> Range:
|
|
269
|
+
sr, sc = node.start_point
|
|
270
|
+
er, ec = node.end_point
|
|
271
|
+
return Range(
|
|
272
|
+
start_line=sr + 1,
|
|
273
|
+
start_col=sc,
|
|
274
|
+
end_line=max(er + 1, sr + 1),
|
|
275
|
+
end_col=ec,
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def _node_text(node: tree_sitter.Node) -> str:
|
|
280
|
+
return node.text.decode("utf-8") if node.text is not None else ""
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def _name_child(node: tree_sitter.Node) -> str | None:
|
|
284
|
+
name_node = node.child_by_field_name("name")
|
|
285
|
+
if name_node is None or name_node.text is None:
|
|
286
|
+
return None
|
|
287
|
+
text = _node_text(name_node).strip()
|
|
288
|
+
return text or None
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def _method_signature(
|
|
292
|
+
node: tree_sitter.Node,
|
|
293
|
+
name: str,
|
|
294
|
+
*,
|
|
295
|
+
is_constructor: bool,
|
|
296
|
+
) -> str:
|
|
297
|
+
params = node.child_by_field_name("parameters")
|
|
298
|
+
params_text = _node_text(params) if params is not None else "()"
|
|
299
|
+
if is_constructor:
|
|
300
|
+
return f"{name}{params_text}"
|
|
301
|
+
return_type = node.child_by_field_name("type")
|
|
302
|
+
rt_text = _node_text(return_type) + " " if return_type is not None else ""
|
|
303
|
+
return f"{rt_text}{name}{params_text}"
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
# ---------------------------------------------------------------------------
|
|
307
|
+
# Metadata extractors for the JavaCallResolverBridge (ADR-0013)
|
|
308
|
+
# ---------------------------------------------------------------------------
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
def _parse_import(node: tree_sitter.Node) -> str:
|
|
312
|
+
"""Return the imported FQN. ``import static x.y.Z.m;`` → ``x.y.Z.m``;
|
|
313
|
+
``import java.util.*;`` → ``java.util.*``. Empty string on malformed
|
|
314
|
+
input (returned to the caller, who drops empties)."""
|
|
315
|
+
parts: list[str] = []
|
|
316
|
+
saw_asterisk = False
|
|
317
|
+
for child in node.children:
|
|
318
|
+
ttype = child.type
|
|
319
|
+
if ttype in {"import", "static", ";"}:
|
|
320
|
+
continue
|
|
321
|
+
if ttype == "asterisk":
|
|
322
|
+
saw_asterisk = True
|
|
323
|
+
continue
|
|
324
|
+
if ttype in {"identifier", "scoped_identifier"}:
|
|
325
|
+
parts.append(_node_text(child))
|
|
326
|
+
if not parts:
|
|
327
|
+
return ""
|
|
328
|
+
path = ".".join(parts)
|
|
329
|
+
return f"{path}.*" if saw_asterisk else path
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
def _parse_supertypes(type_node: tree_sitter.Node) -> list[dict[str, str]]:
|
|
333
|
+
"""Extract ``extends`` / ``implements`` relations off a type declaration.
|
|
334
|
+
|
|
335
|
+
Handles ``class X extends A``, ``class X implements I, J``,
|
|
336
|
+
``class X extends A implements I``, and ``interface I extends J, K``.
|
|
337
|
+
Generic type arguments (``Box<String>``) are stripped — bridge resolves
|
|
338
|
+
by raw name, not parameterized type.
|
|
339
|
+
"""
|
|
340
|
+
out: list[dict[str, str]] = []
|
|
341
|
+
for child in type_node.children:
|
|
342
|
+
ttype = child.type
|
|
343
|
+
# `class` declarations: superclass / super_interfaces fields.
|
|
344
|
+
if ttype == "superclass":
|
|
345
|
+
out.extend({"name": name, "relation": "extends"} for name in _supertype_names(child))
|
|
346
|
+
elif ttype == "super_interfaces":
|
|
347
|
+
out.extend({"name": name, "relation": "implements"} for name in _supertype_names(child))
|
|
348
|
+
# `interface` declarations: extends_interfaces.
|
|
349
|
+
elif ttype == "extends_interfaces":
|
|
350
|
+
out.extend({"name": name, "relation": "extends"} for name in _supertype_names(child))
|
|
351
|
+
return out
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
def _supertype_names(container: tree_sitter.Node) -> list[str]:
|
|
355
|
+
out: list[str] = []
|
|
356
|
+
for child in container.children:
|
|
357
|
+
ttype = child.type
|
|
358
|
+
if ttype in {"type_identifier", "scoped_type_identifier"}:
|
|
359
|
+
out.append(_node_text(child))
|
|
360
|
+
elif ttype == "generic_type":
|
|
361
|
+
# take the head type, drop ``<...>``
|
|
362
|
+
head = child.child(0)
|
|
363
|
+
if head is not None and head.type in {
|
|
364
|
+
"type_identifier",
|
|
365
|
+
"scoped_type_identifier",
|
|
366
|
+
}:
|
|
367
|
+
out.append(_node_text(head))
|
|
368
|
+
elif ttype == "type_list":
|
|
369
|
+
out.extend(_supertype_names(child))
|
|
370
|
+
return out
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
def _collect_invocations(body: tree_sitter.Node) -> list[dict[str, object]]:
|
|
374
|
+
"""Walk ``body`` collecting every ``method_invocation`` node as a raw
|
|
375
|
+
record. The bridge does FQN resolution; here we only capture the syntactic
|
|
376
|
+
shape (receiver text, name, arity, location)."""
|
|
377
|
+
records: list[dict[str, object]] = []
|
|
378
|
+
|
|
379
|
+
def walk(node: tree_sitter.Node) -> None:
|
|
380
|
+
if node.type == "method_invocation":
|
|
381
|
+
name_node = node.child_by_field_name("name")
|
|
382
|
+
obj_node = node.child_by_field_name("object")
|
|
383
|
+
args_node = node.child_by_field_name("arguments")
|
|
384
|
+
if name_node is not None:
|
|
385
|
+
receiver = _receiver_text(obj_node)
|
|
386
|
+
name = _node_text(name_node)
|
|
387
|
+
arity = _argument_arity(args_node)
|
|
388
|
+
sr, sc = node.start_point
|
|
389
|
+
records.append(
|
|
390
|
+
{
|
|
391
|
+
"receiver": receiver,
|
|
392
|
+
"name": name,
|
|
393
|
+
"arity": arity,
|
|
394
|
+
"line": sr + 1,
|
|
395
|
+
"col": sc,
|
|
396
|
+
}
|
|
397
|
+
)
|
|
398
|
+
for child in node.children:
|
|
399
|
+
walk(child)
|
|
400
|
+
|
|
401
|
+
walk(body)
|
|
402
|
+
return records
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
def _receiver_text(obj: tree_sitter.Node | None) -> str:
|
|
406
|
+
"""Best-effort textual receiver. Empty for unqualified calls; the inner
|
|
407
|
+
method's name for chained calls (``foo.bar().baz()`` → ``"bar"`` is the
|
|
408
|
+
receiver of ``baz``)."""
|
|
409
|
+
if obj is None:
|
|
410
|
+
return ""
|
|
411
|
+
ttype = obj.type
|
|
412
|
+
if ttype in {"identifier", "scoped_identifier", "this", "super"}:
|
|
413
|
+
return _node_text(obj)
|
|
414
|
+
if ttype == "field_access":
|
|
415
|
+
field = obj.child_by_field_name("field")
|
|
416
|
+
return _node_text(field) if field is not None else ""
|
|
417
|
+
if ttype == "method_invocation":
|
|
418
|
+
inner = obj.child_by_field_name("name")
|
|
419
|
+
return _node_text(inner) if inner is not None else ""
|
|
420
|
+
return ""
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
def _argument_arity(args: tree_sitter.Node | None) -> int:
|
|
424
|
+
if args is None:
|
|
425
|
+
return 0
|
|
426
|
+
# named_child_count skips punctuation tokens (`(`, `)`, `,`).
|
|
427
|
+
return int(args.named_child_count)
|
|
428
|
+
|
|
429
|
+
|
|
430
|
+
def _parse_formal_parameters(node: tree_sitter.Node | None) -> list[dict[str, str]]:
|
|
431
|
+
"""Return ``[{"name": str, "type": str}]`` for each formal parameter.
|
|
432
|
+
|
|
433
|
+
Generic type arguments are stripped (``List<String>`` → ``List``) so the
|
|
434
|
+
FQN resolver can match by raw type name.
|
|
435
|
+
"""
|
|
436
|
+
if node is None:
|
|
437
|
+
return []
|
|
438
|
+
out: list[dict[str, str]] = []
|
|
439
|
+
for child in node.children:
|
|
440
|
+
if child.type not in {"formal_parameter", "spread_parameter"}:
|
|
441
|
+
continue
|
|
442
|
+
name_node = child.child_by_field_name("name")
|
|
443
|
+
type_node = child.child_by_field_name("type")
|
|
444
|
+
if name_node is None or type_node is None:
|
|
445
|
+
# Spread parameters wrap the actual variable_declarator differently.
|
|
446
|
+
for sub in child.children:
|
|
447
|
+
if sub.type == "variable_declarator" and name_node is None:
|
|
448
|
+
name_node = sub.child_by_field_name("name")
|
|
449
|
+
if name_node is None or type_node is None:
|
|
450
|
+
continue
|
|
451
|
+
out.append(
|
|
452
|
+
{
|
|
453
|
+
"name": _node_text(name_node),
|
|
454
|
+
"type": _strip_generics(_node_text(type_node)),
|
|
455
|
+
}
|
|
456
|
+
)
|
|
457
|
+
return out
|
|
458
|
+
|
|
459
|
+
|
|
460
|
+
def _strip_generics(t: str) -> str:
|
|
461
|
+
"""``Box<String, Integer>`` → ``Box``; arrays / primitives untouched."""
|
|
462
|
+
t = t.strip()
|
|
463
|
+
if "<" in t:
|
|
464
|
+
return t.split("<", 1)[0].rstrip()
|
|
465
|
+
return t
|
|
466
|
+
|
|
467
|
+
|
|
468
|
+
# ---------------------------------------------------------------------------
|
|
469
|
+
# Annotation extraction (Plan 3 Task 1)
|
|
470
|
+
# ---------------------------------------------------------------------------
|
|
471
|
+
|
|
472
|
+
|
|
473
|
+
def _parse_annotations(decl_node: tree_sitter.Node) -> list[Annotation]:
|
|
474
|
+
"""Walk a class/method/constructor declaration's ``modifiers`` child and
|
|
475
|
+
return every ``annotation`` / ``marker_annotation`` it carries as an
|
|
476
|
+
:class:`Annotation`."""
|
|
477
|
+
out: list[Annotation] = []
|
|
478
|
+
for child in decl_node.children:
|
|
479
|
+
if child.type != "modifiers":
|
|
480
|
+
continue
|
|
481
|
+
for m in child.children:
|
|
482
|
+
if m.type == "marker_annotation":
|
|
483
|
+
name = _annotation_name(m)
|
|
484
|
+
if name:
|
|
485
|
+
out.append(Annotation(name=name, arguments={}))
|
|
486
|
+
elif m.type == "annotation":
|
|
487
|
+
name = _annotation_name(m)
|
|
488
|
+
args = _annotation_arguments(m)
|
|
489
|
+
if name:
|
|
490
|
+
out.append(Annotation(name=name, arguments=args))
|
|
491
|
+
return out
|
|
492
|
+
|
|
493
|
+
|
|
494
|
+
def _annotation_name(ann_node: tree_sitter.Node) -> str:
|
|
495
|
+
for child in ann_node.children:
|
|
496
|
+
if child.type in {"identifier", "scoped_identifier"}:
|
|
497
|
+
return _node_text(child)
|
|
498
|
+
return ""
|
|
499
|
+
|
|
500
|
+
|
|
501
|
+
def _annotation_arguments(ann_node: tree_sitter.Node) -> dict[str, str]:
|
|
502
|
+
"""``@RequestMapping("/x")`` → ``{"value": "/x"}``;
|
|
503
|
+
``@X(name="a", v=1)`` → ``{"name": "a", "v": "1"}``;
|
|
504
|
+
``@Override`` → ``{}``."""
|
|
505
|
+
args: dict[str, str] = {}
|
|
506
|
+
arglist = None
|
|
507
|
+
for child in ann_node.children:
|
|
508
|
+
if child.type == "annotation_argument_list":
|
|
509
|
+
arglist = child
|
|
510
|
+
break
|
|
511
|
+
if arglist is None:
|
|
512
|
+
return args
|
|
513
|
+
for child in arglist.children:
|
|
514
|
+
if child.type == "string_literal":
|
|
515
|
+
args["value"] = _strip_string_literal(_node_text(child))
|
|
516
|
+
elif child.type == "element_value_pair":
|
|
517
|
+
key_node = child.child_by_field_name("key")
|
|
518
|
+
val_node = child.child_by_field_name("value")
|
|
519
|
+
if key_node is None or val_node is None:
|
|
520
|
+
# fall back: first two non-`=` children
|
|
521
|
+
kids = [c for c in child.children if c.type not in {"="}]
|
|
522
|
+
if len(kids) < 2:
|
|
523
|
+
continue
|
|
524
|
+
key_node, val_node = kids[0], kids[1]
|
|
525
|
+
key = _node_text(key_node)
|
|
526
|
+
value = _strip_string_literal(_node_text(val_node))
|
|
527
|
+
if key:
|
|
528
|
+
args[key] = value
|
|
529
|
+
elif child.type not in {"(", ")", ","} and "value" not in args:
|
|
530
|
+
# bare non-string single value (e.g. enum member, integer)
|
|
531
|
+
args["value"] = _strip_string_literal(_node_text(child))
|
|
532
|
+
return args
|
|
533
|
+
|
|
534
|
+
|
|
535
|
+
def _strip_string_literal(s: str) -> str:
|
|
536
|
+
if len(s) >= 2 and s[0] == s[-1] and s[0] in {'"', "'"}:
|
|
537
|
+
return s[1:-1]
|
|
538
|
+
return s
|
|
539
|
+
|
|
540
|
+
|
|
541
|
+
# ---------------------------------------------------------------------------
|
|
542
|
+
# Spring http_route metadata (Plan 3 Task 2)
|
|
543
|
+
# ---------------------------------------------------------------------------
|
|
544
|
+
|
|
545
|
+
_VERB_ANNO = {
|
|
546
|
+
"GetMapping": "GET",
|
|
547
|
+
"PostMapping": "POST",
|
|
548
|
+
"PutMapping": "PUT",
|
|
549
|
+
"DeleteMapping": "DELETE",
|
|
550
|
+
"PatchMapping": "PATCH",
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
|
|
554
|
+
def _http_route_meta(
|
|
555
|
+
class_annos: list[Annotation],
|
|
556
|
+
method_annos: list[Annotation],
|
|
557
|
+
) -> dict[str, str] | None:
|
|
558
|
+
"""Combine class-level ``@RequestMapping`` prefix with the method's verb
|
|
559
|
+
mapping annotation. Returns ``{"method", "path"}`` or ``None`` when the
|
|
560
|
+
method has no mapping annotation."""
|
|
561
|
+
prefix = ""
|
|
562
|
+
for a in class_annos:
|
|
563
|
+
if a.name == "RequestMapping":
|
|
564
|
+
prefix = a.arguments.get("value", a.arguments.get("path", ""))
|
|
565
|
+
break
|
|
566
|
+
|
|
567
|
+
verb: str | None = None
|
|
568
|
+
path = ""
|
|
569
|
+
for a in method_annos:
|
|
570
|
+
if a.name in _VERB_ANNO:
|
|
571
|
+
verb = _VERB_ANNO[a.name]
|
|
572
|
+
path = a.arguments.get("value", a.arguments.get("path", ""))
|
|
573
|
+
break
|
|
574
|
+
|
|
575
|
+
if verb is None:
|
|
576
|
+
for a in method_annos:
|
|
577
|
+
if a.name == "RequestMapping":
|
|
578
|
+
verb = "GET"
|
|
579
|
+
path = a.arguments.get("value", a.arguments.get("path", ""))
|
|
580
|
+
break
|
|
581
|
+
|
|
582
|
+
if verb is None:
|
|
583
|
+
return None
|
|
584
|
+
return {"method": verb, "path": _join_route(prefix, path)}
|
|
585
|
+
|
|
586
|
+
|
|
587
|
+
def _join_route(prefix: str, path: str) -> str:
|
|
588
|
+
parts = [seg for seg in (prefix + "/" + path).split("/") if seg]
|
|
589
|
+
return "/" + "/".join(parts) if parts else "/"
|