codemap-java 0.2.0__py3-none-any.whl → 0.3.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.
- codemap_java/indexer.py +341 -6
- codemap_java/resolver.py +285 -0
- {codemap_java-0.2.0.dist-info → codemap_java-0.3.0.dist-info}/METADATA +2 -2
- codemap_java-0.3.0.dist-info/RECORD +7 -0
- codemap_java-0.3.0.dist-info/entry_points.txt +5 -0
- codemap_java-0.2.0.dist-info/RECORD +0 -6
- codemap_java-0.2.0.dist-info/entry_points.txt +0 -2
- {codemap_java-0.2.0.dist-info → codemap_java-0.3.0.dist-info}/WHEEL +0 -0
codemap_java/indexer.py
CHANGED
|
@@ -5,8 +5,17 @@ declarations. Package declarations are honoured as a namespace prefix
|
|
|
5
5
|
under the file path. Nested types track a class stack to produce the
|
|
6
6
|
correct ``Cls#Inner#m()`` chain.
|
|
7
7
|
|
|
8
|
-
The indexer is single-file by design; cross-file
|
|
9
|
-
resolution lives in
|
|
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.
|
|
10
19
|
"""
|
|
11
20
|
|
|
12
21
|
from __future__ import annotations
|
|
@@ -17,7 +26,7 @@ from typing import ClassVar
|
|
|
17
26
|
import tree_sitter
|
|
18
27
|
import tree_sitter_java
|
|
19
28
|
|
|
20
|
-
from codemap.core.models import Diagnostic, Edge, IndexResult, Range, Symbol
|
|
29
|
+
from codemap.core.models import Annotation, Diagnostic, Edge, IndexResult, Range, Symbol
|
|
21
30
|
from codemap.core.symbol import Descriptor, DescriptorKind, SymbolID
|
|
22
31
|
from codemap.indexers.base import IndexContext
|
|
23
32
|
|
|
@@ -100,12 +109,19 @@ class _Visitor:
|
|
|
100
109
|
self.edges: list[Edge] = []
|
|
101
110
|
self.diagnostics: list[Diagnostic] = []
|
|
102
111
|
self._class_stack: list[str] = []
|
|
112
|
+
self._class_annos_stack: list[list[Annotation]] = []
|
|
103
113
|
self._package: str = ""
|
|
114
|
+
self._file_imports: list[str] = []
|
|
104
115
|
|
|
105
116
|
def visit(self, node: tree_sitter.Node) -> None:
|
|
106
117
|
if node.type == "package_declaration":
|
|
107
118
|
self._package = _node_text(node.children[1]) if node.child_count > 1 else ""
|
|
108
119
|
return
|
|
120
|
+
if node.type == "import_declaration":
|
|
121
|
+
imp = _parse_import(node)
|
|
122
|
+
if imp:
|
|
123
|
+
self._file_imports.append(imp)
|
|
124
|
+
return
|
|
109
125
|
if node.type in _TYPE_DECLS:
|
|
110
126
|
self._visit_type(node)
|
|
111
127
|
return
|
|
@@ -128,7 +144,16 @@ class _Visitor:
|
|
|
128
144
|
if name is None:
|
|
129
145
|
return
|
|
130
146
|
java_kind = node.type.removesuffix("_declaration")
|
|
147
|
+
is_top_level = not self._class_stack
|
|
131
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)
|
|
132
157
|
self.symbols.append(
|
|
133
158
|
Symbol(
|
|
134
159
|
id=sid,
|
|
@@ -136,20 +161,21 @@ class _Visitor:
|
|
|
136
161
|
language=LANG,
|
|
137
162
|
file=self.relative_path,
|
|
138
163
|
range=_node_range(node),
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
else {},
|
|
164
|
+
annotations=annotations,
|
|
165
|
+
extra=extra,
|
|
142
166
|
)
|
|
143
167
|
)
|
|
144
168
|
body = node.child_by_field_name("body")
|
|
145
169
|
if body is None:
|
|
146
170
|
return
|
|
147
171
|
self._class_stack.append(name)
|
|
172
|
+
self._class_annos_stack.append(annotations)
|
|
148
173
|
try:
|
|
149
174
|
for child in body.children:
|
|
150
175
|
self.visit(child)
|
|
151
176
|
finally:
|
|
152
177
|
self._class_stack.pop()
|
|
178
|
+
self._class_annos_stack.pop()
|
|
153
179
|
|
|
154
180
|
# ----------------------------------------------------------- members
|
|
155
181
|
|
|
@@ -164,6 +190,21 @@ class _Visitor:
|
|
|
164
190
|
display = name
|
|
165
191
|
sid = self._make_id(name, kind=DescriptorKind.METHOD)
|
|
166
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
|
|
167
208
|
self.symbols.append(
|
|
168
209
|
Symbol(
|
|
169
210
|
id=sid,
|
|
@@ -172,10 +213,14 @@ class _Visitor:
|
|
|
172
213
|
file=self.relative_path,
|
|
173
214
|
range=_node_range(node),
|
|
174
215
|
signature=signature,
|
|
216
|
+
annotations=method_annos,
|
|
217
|
+
extra=extra,
|
|
175
218
|
)
|
|
176
219
|
)
|
|
177
220
|
|
|
178
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 ""
|
|
179
224
|
for child in node.children:
|
|
180
225
|
if child.type != "variable_declarator":
|
|
181
226
|
continue
|
|
@@ -186,6 +231,9 @@ class _Visitor:
|
|
|
186
231
|
if not name:
|
|
187
232
|
continue
|
|
188
233
|
sid = self._make_id(name, kind=DescriptorKind.TERM)
|
|
234
|
+
extra: dict[str, object] = {}
|
|
235
|
+
if type_str:
|
|
236
|
+
extra["type"] = type_str
|
|
189
237
|
self.symbols.append(
|
|
190
238
|
Symbol(
|
|
191
239
|
id=sid,
|
|
@@ -193,6 +241,7 @@ class _Visitor:
|
|
|
193
241
|
language=LANG,
|
|
194
242
|
file=self.relative_path,
|
|
195
243
|
range=_node_range(child),
|
|
244
|
+
extra=extra,
|
|
196
245
|
)
|
|
197
246
|
)
|
|
198
247
|
|
|
@@ -252,3 +301,289 @@ def _method_signature(
|
|
|
252
301
|
return_type = node.child_by_field_name("type")
|
|
253
302
|
rt_text = _node_text(return_type) + " " if return_type is not None else ""
|
|
254
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 "/"
|
codemap_java/resolver.py
ADDED
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
"""JavaCallResolverBridge — cross-file FQN-based call/extends/implements resolver.
|
|
2
|
+
|
|
3
|
+
Consumes the metadata that :class:`codemap_java.indexer.JavaIndexer` attaches
|
|
4
|
+
to ``Symbol.extra``:
|
|
5
|
+
|
|
6
|
+
* top-level class symbols carry ``package`` / ``imports`` / ``supertypes``
|
|
7
|
+
* method symbols carry ``params`` (list of ``{name, type}``) and
|
|
8
|
+
``pending_calls`` (raw invocation records — receiver text, method name,
|
|
9
|
+
argument arity, location)
|
|
10
|
+
* field symbols carry ``type``
|
|
11
|
+
|
|
12
|
+
Algorithm (single bridge pass, ADR-0013):
|
|
13
|
+
|
|
14
|
+
1. Build a project-wide FQN table from all top-level Java classes
|
|
15
|
+
(``package + simple_name``).
|
|
16
|
+
2. Index each class's methods (by name → list of (arity, sid)) and fields
|
|
17
|
+
(by name → type string), so we can dispatch later by arity and follow
|
|
18
|
+
field receivers to their declared types.
|
|
19
|
+
3. Resolve each class's ``supertypes`` against ``imports`` + same-package
|
|
20
|
+
+ java.lang implicit imports, emit ``extends`` / ``implements`` edges.
|
|
21
|
+
4. For each pending invocation, infer the receiver class's FQN
|
|
22
|
+
(``""``/``"this"``/``"super"`` → caller class; capitalised → resolve as
|
|
23
|
+
type; lowercase → look up caller's field of that name and take its
|
|
24
|
+
declared type), then emit a ``calls`` edge iff exactly one method on
|
|
25
|
+
the target class matches by name + arity. Ambiguous / unresolved
|
|
26
|
+
invocations silently drop — they aren't crashes.
|
|
27
|
+
|
|
28
|
+
All edges are emitted with ``confidence="medium"`` per ADR-0013's accepted
|
|
29
|
+
trade-off versus full semantic resolution.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
from __future__ import annotations
|
|
33
|
+
|
|
34
|
+
from collections import defaultdict
|
|
35
|
+
from pathlib import PurePosixPath
|
|
36
|
+
from typing import Any, ClassVar
|
|
37
|
+
|
|
38
|
+
from codemap.core.models import BridgeResult, Edge
|
|
39
|
+
from codemap.core.store import ReadOnlyStore
|
|
40
|
+
from codemap.core.symbol import DescriptorKind, SymbolID
|
|
41
|
+
|
|
42
|
+
__all__ = ["JavaCallResolverBridge"]
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class JavaCallResolverBridge:
|
|
46
|
+
name: ClassVar[str] = "java_calls"
|
|
47
|
+
version: ClassVar[str] = "0.1.0"
|
|
48
|
+
requires: ClassVar[list[str]] = []
|
|
49
|
+
|
|
50
|
+
def resolve(self, store: ReadOnlyStore) -> BridgeResult:
|
|
51
|
+
# One pass over the store to bucket java symbols.
|
|
52
|
+
classes: list[Any] = []
|
|
53
|
+
methods: list[Any] = []
|
|
54
|
+
fields: list[Any] = []
|
|
55
|
+
for sym in store.iter_symbols():
|
|
56
|
+
if sym.language != "java":
|
|
57
|
+
continue
|
|
58
|
+
if sym.kind == "class" and "imports" in sym.extra:
|
|
59
|
+
# Top-level only. (Nested classes don't carry imports; the
|
|
60
|
+
# current resolver doesn't follow them — see ADR-0013.)
|
|
61
|
+
classes.append(sym)
|
|
62
|
+
elif sym.kind == "method":
|
|
63
|
+
methods.append(sym)
|
|
64
|
+
elif sym.kind == "field":
|
|
65
|
+
fields.append(sym)
|
|
66
|
+
|
|
67
|
+
# Build FQN table + per-class info.
|
|
68
|
+
fqn_to_sid: dict[str, SymbolID] = {}
|
|
69
|
+
simple_to_fqn: dict[str, list[str]] = defaultdict(list)
|
|
70
|
+
info_by_sid: dict[SymbolID, _ClassInfo] = {}
|
|
71
|
+
file_to_classes: dict[PurePosixPath, list[_ClassInfo]] = defaultdict(list)
|
|
72
|
+
|
|
73
|
+
for cls in classes:
|
|
74
|
+
pkg = str(cls.extra.get("package", ""))
|
|
75
|
+
simple = cls.id.descriptors[-1].name
|
|
76
|
+
fqn = f"{pkg}.{simple}" if pkg else simple
|
|
77
|
+
fqn_to_sid[fqn] = cls.id
|
|
78
|
+
simple_to_fqn[simple].append(fqn)
|
|
79
|
+
info = _ClassInfo(
|
|
80
|
+
sid=cls.id,
|
|
81
|
+
fqn=fqn,
|
|
82
|
+
pkg=pkg,
|
|
83
|
+
simple_name=simple,
|
|
84
|
+
file=cls.file,
|
|
85
|
+
imports=list(cls.extra.get("imports", [])),
|
|
86
|
+
supertypes=list(cls.extra.get("supertypes", [])),
|
|
87
|
+
)
|
|
88
|
+
info_by_sid[cls.id] = info
|
|
89
|
+
file_to_classes[cls.file].append(info)
|
|
90
|
+
|
|
91
|
+
# Attach methods + fields to their owner class.
|
|
92
|
+
for m in methods:
|
|
93
|
+
owner = _owner_class(m, file_to_classes)
|
|
94
|
+
if owner is None:
|
|
95
|
+
continue
|
|
96
|
+
arity = len(m.extra.get("params", []))
|
|
97
|
+
owner.methods[m.id.descriptors[-1].name].append(
|
|
98
|
+
_MethodRecord(
|
|
99
|
+
sid=m.id,
|
|
100
|
+
arity=arity,
|
|
101
|
+
pending_calls=list(m.extra.get("pending_calls", [])),
|
|
102
|
+
)
|
|
103
|
+
)
|
|
104
|
+
for f in fields:
|
|
105
|
+
owner = _owner_class(f, file_to_classes)
|
|
106
|
+
if owner is None:
|
|
107
|
+
continue
|
|
108
|
+
ftype = str(f.extra.get("type", "")).strip()
|
|
109
|
+
if ftype:
|
|
110
|
+
owner.fields[f.id.descriptors[-1].name] = ftype
|
|
111
|
+
|
|
112
|
+
# Resolver helpers in closure.
|
|
113
|
+
def resolve_type(name: str, ctx: _ClassInfo) -> str | None:
|
|
114
|
+
if "." in name:
|
|
115
|
+
return name if name in fqn_to_sid else None
|
|
116
|
+
same_pkg = f"{ctx.pkg}.{name}" if ctx.pkg else name
|
|
117
|
+
if same_pkg in fqn_to_sid:
|
|
118
|
+
return same_pkg
|
|
119
|
+
for imp in ctx.imports:
|
|
120
|
+
if imp.endswith(".*"):
|
|
121
|
+
candidate = f"{imp[:-2]}.{name}"
|
|
122
|
+
if candidate in fqn_to_sid:
|
|
123
|
+
return candidate
|
|
124
|
+
elif imp.rsplit(".", 1)[-1] == name and imp in fqn_to_sid:
|
|
125
|
+
return imp
|
|
126
|
+
# java.lang implicit
|
|
127
|
+
implicit = f"java.lang.{name}"
|
|
128
|
+
if implicit in fqn_to_sid:
|
|
129
|
+
return implicit
|
|
130
|
+
# Last resort: unique simple-name match across the project.
|
|
131
|
+
candidates = simple_to_fqn.get(name, [])
|
|
132
|
+
if len(candidates) == 1:
|
|
133
|
+
return candidates[0]
|
|
134
|
+
return None
|
|
135
|
+
|
|
136
|
+
edges: list[Edge] = []
|
|
137
|
+
|
|
138
|
+
# extends / implements edges.
|
|
139
|
+
for info in info_by_sid.values():
|
|
140
|
+
for sup in info.supertypes:
|
|
141
|
+
tgt_fqn = resolve_type(sup["name"], info)
|
|
142
|
+
if tgt_fqn is None:
|
|
143
|
+
continue
|
|
144
|
+
edges.append(
|
|
145
|
+
Edge(
|
|
146
|
+
source=info.sid,
|
|
147
|
+
target=fqn_to_sid[tgt_fqn],
|
|
148
|
+
kind=sup["relation"], # "extends" | "implements"
|
|
149
|
+
confidence="medium",
|
|
150
|
+
)
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
# calls edges.
|
|
154
|
+
for info in info_by_sid.values():
|
|
155
|
+
for records in info.methods.values():
|
|
156
|
+
for rec in records:
|
|
157
|
+
for call in rec.pending_calls:
|
|
158
|
+
tgt_sid = _resolve_call(call, info, resolve_type, fqn_to_sid, info_by_sid)
|
|
159
|
+
if tgt_sid is None:
|
|
160
|
+
continue
|
|
161
|
+
edges.append(
|
|
162
|
+
Edge(
|
|
163
|
+
source=rec.sid,
|
|
164
|
+
target=tgt_sid,
|
|
165
|
+
kind="calls",
|
|
166
|
+
confidence="medium",
|
|
167
|
+
)
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
return BridgeResult(edges=edges)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
# ---------------------------------------------------------------------------
|
|
174
|
+
# Internal types
|
|
175
|
+
# ---------------------------------------------------------------------------
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
class _ClassInfo:
|
|
179
|
+
__slots__ = (
|
|
180
|
+
"fields",
|
|
181
|
+
"file",
|
|
182
|
+
"fqn",
|
|
183
|
+
"imports",
|
|
184
|
+
"methods",
|
|
185
|
+
"pkg",
|
|
186
|
+
"sid",
|
|
187
|
+
"simple_name",
|
|
188
|
+
"supertypes",
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
def __init__(
|
|
192
|
+
self,
|
|
193
|
+
*,
|
|
194
|
+
sid: SymbolID,
|
|
195
|
+
fqn: str,
|
|
196
|
+
pkg: str,
|
|
197
|
+
simple_name: str,
|
|
198
|
+
file: PurePosixPath,
|
|
199
|
+
imports: list[str],
|
|
200
|
+
supertypes: list[dict[str, str]],
|
|
201
|
+
) -> None:
|
|
202
|
+
self.sid = sid
|
|
203
|
+
self.fqn = fqn
|
|
204
|
+
self.pkg = pkg
|
|
205
|
+
self.simple_name = simple_name
|
|
206
|
+
self.file = file
|
|
207
|
+
self.imports = imports
|
|
208
|
+
self.supertypes = supertypes
|
|
209
|
+
self.methods: dict[str, list[_MethodRecord]] = defaultdict(list)
|
|
210
|
+
self.fields: dict[str, str] = {}
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
class _MethodRecord:
|
|
214
|
+
__slots__ = ("arity", "pending_calls", "sid")
|
|
215
|
+
|
|
216
|
+
def __init__(
|
|
217
|
+
self,
|
|
218
|
+
*,
|
|
219
|
+
sid: SymbolID,
|
|
220
|
+
arity: int,
|
|
221
|
+
pending_calls: list[dict[str, Any]],
|
|
222
|
+
) -> None:
|
|
223
|
+
self.sid = sid
|
|
224
|
+
self.arity = arity
|
|
225
|
+
self.pending_calls = pending_calls
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
# ---------------------------------------------------------------------------
|
|
229
|
+
# Helpers
|
|
230
|
+
# ---------------------------------------------------------------------------
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def _owner_class(
|
|
234
|
+
sym: Any,
|
|
235
|
+
file_to_classes: dict[PurePosixPath, list[_ClassInfo]],
|
|
236
|
+
) -> _ClassInfo | None:
|
|
237
|
+
"""Locate the top-level class that owns ``sym`` (a method or field).
|
|
238
|
+
|
|
239
|
+
Owner is the class whose simple name matches the second-to-last
|
|
240
|
+
descriptor of ``sym.id`` and lives in the same file.
|
|
241
|
+
"""
|
|
242
|
+
descs = sym.id.descriptors
|
|
243
|
+
if len(descs) < 2 or descs[-2].kind is not DescriptorKind.TYPE:
|
|
244
|
+
return None
|
|
245
|
+
owner_name = descs[-2].name
|
|
246
|
+
for info in file_to_classes.get(sym.file, []):
|
|
247
|
+
if info.simple_name == owner_name:
|
|
248
|
+
return info
|
|
249
|
+
return None
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def _resolve_call(
|
|
253
|
+
call: dict[str, Any],
|
|
254
|
+
caller: _ClassInfo,
|
|
255
|
+
resolve_type, # type: ignore[no-untyped-def]
|
|
256
|
+
fqn_to_sid: dict[str, SymbolID],
|
|
257
|
+
info_by_sid: dict[SymbolID, _ClassInfo],
|
|
258
|
+
) -> SymbolID | None:
|
|
259
|
+
receiver = str(call.get("receiver", ""))
|
|
260
|
+
name = str(call.get("name", ""))
|
|
261
|
+
arity = int(call.get("arity", 0))
|
|
262
|
+
if not name:
|
|
263
|
+
return None
|
|
264
|
+
|
|
265
|
+
if receiver in {"", "this", "super"}:
|
|
266
|
+
target_fqn: str | None = caller.fqn
|
|
267
|
+
elif receiver and receiver[0].isupper():
|
|
268
|
+
target_fqn = resolve_type(receiver, caller)
|
|
269
|
+
else:
|
|
270
|
+
ftype = caller.fields.get(receiver)
|
|
271
|
+
target_fqn = resolve_type(ftype, caller) if ftype else None
|
|
272
|
+
|
|
273
|
+
if target_fqn is None:
|
|
274
|
+
return None
|
|
275
|
+
target_cls_sid = fqn_to_sid.get(target_fqn)
|
|
276
|
+
if target_cls_sid is None:
|
|
277
|
+
return None
|
|
278
|
+
target_info = info_by_sid.get(target_cls_sid)
|
|
279
|
+
if target_info is None:
|
|
280
|
+
return None
|
|
281
|
+
|
|
282
|
+
candidates = [m.sid for m in target_info.methods.get(name, []) if m.arity == arity]
|
|
283
|
+
if len(candidates) == 1:
|
|
284
|
+
return candidates[0]
|
|
285
|
+
return None
|
|
@@ -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
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
codemap_java/__init__.py,sha256=CfrzKPuFCV4SA9ly2O8K84G96OIBVTCWrDUFafKIOgY,170
|
|
2
|
+
codemap_java/indexer.py,sha256=BLhd-j8P3Y3YbXhbss8CFyzA9heBjgtiYaBAkMo2EOw,21415
|
|
3
|
+
codemap_java/resolver.py,sha256=niktAfVmautbbV6nVAWkLGzz2dtu6GYEzhFuWaQyJYA,9897
|
|
4
|
+
codemap_java-0.3.0.dist-info/METADATA,sha256=RfUsDQh4-S2x8WcBHGw-HCcmXTaM9oaZYJ8bF86BEhw,2250
|
|
5
|
+
codemap_java-0.3.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
|
|
6
|
+
codemap_java-0.3.0.dist-info/entry_points.txt,sha256=ffms-JC_vd26AXl4105KAWYteM0J1a5xw7hMqyIrWBo,128
|
|
7
|
+
codemap_java-0.3.0.dist-info/RECORD,,
|
|
@@ -1,6 +0,0 @@
|
|
|
1
|
-
codemap_java/__init__.py,sha256=CfrzKPuFCV4SA9ly2O8K84G96OIBVTCWrDUFafKIOgY,170
|
|
2
|
-
codemap_java/indexer.py,sha256=exyqk2mvj-BAEli9Fi9beUcQIPu_MTXoL9h_QqLjGX0,8461
|
|
3
|
-
codemap_java-0.2.0.dist-info/METADATA,sha256=7r9Lbl65HmdoeiOgPJZVGvQAoKSGWJTSrvkqO7oz0AQ,2250
|
|
4
|
-
codemap_java-0.2.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
|
|
5
|
-
codemap_java-0.2.0.dist-info/entry_points.txt,sha256=nc_YzUZs5Nwz3H_qPVY1k1n4A1VtpA8F6Tj0AE6XnNc,51
|
|
6
|
-
codemap_java-0.2.0.dist-info/RECORD,,
|
|
File without changes
|