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 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 `extends` / `implements`
9
- resolution lives in a future bridge so the indexer surface stays narrow.
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
- extra={"java_kind": java_kind, "package": self._package}
140
- if self._package or java_kind != "class"
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 "/"
@@ -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.2.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.3,>=0.2.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,,
@@ -0,0 +1,5 @@
1
+ [codemap.bridges]
2
+ java_calls = codemap_java.resolver:JavaCallResolverBridge
3
+
4
+ [codemap.indexers]
5
+ java = codemap_java:JavaIndexer
@@ -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,,
@@ -1,2 +0,0 @@
1
- [codemap.indexers]
2
- java = codemap_java:JavaIndexer