adl-toolkit 0.0.0-stage → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (104) hide show
  1. package/INSTALL_TOOLKIT.md +163 -0
  2. package/README.md +269 -2
  3. package/adl_drift/__init__.py +6 -0
  4. package/adl_drift/cli/__init__.py +1 -0
  5. package/adl_drift/cli/commands.py +993 -0
  6. package/adl_drift/cli/main.py +744 -0
  7. package/adl_drift/cortex/__init__.py +1 -0
  8. package/adl_drift/cortex/embeddings.py +96 -0
  9. package/adl_drift/cortex/memory.py +1749 -0
  10. package/adl_drift/cortex/patterns.py +89 -0
  11. package/adl_drift/cortex/telemetry.py +192 -0
  12. package/adl_drift/detectors/__init__.py +1 -0
  13. package/adl_drift/detectors/async_patterns.py +115 -0
  14. package/adl_drift/detectors/base.py +121 -0
  15. package/adl_drift/detectors/config.py +122 -0
  16. package/adl_drift/detectors/data_access.py +128 -0
  17. package/adl_drift/detectors/decorators.py +122 -0
  18. package/adl_drift/detectors/dynamic_patterns.py +260 -0
  19. package/adl_drift/detectors/errors.py +134 -0
  20. package/adl_drift/detectors/imports.py +132 -0
  21. package/adl_drift/detectors/logging_patterns.py +108 -0
  22. package/adl_drift/detectors/routes.py +257 -0
  23. package/adl_drift/detectors/security.py +236 -0
  24. package/adl_drift/detectors/taint.py +406 -0
  25. package/adl_drift/detectors/tests.py +95 -0
  26. package/adl_drift/detectors/type_conventions.py +81 -0
  27. package/adl_drift/graph/__init__.py +1 -0
  28. package/adl_drift/graph/builder.py +2670 -0
  29. package/adl_drift/graph/cfg.py +461 -0
  30. package/adl_drift/graph/cluster.py +40 -0
  31. package/adl_drift/graph/edges.py +86 -0
  32. package/adl_drift/graph/node.py +90 -0
  33. package/adl_drift/harness/__init__.py +10 -0
  34. package/adl_drift/harness/setup.py +832 -0
  35. package/adl_drift/harness/skills/adl-drift.md +25 -0
  36. package/adl_drift/harness/skills/drift-antigravity.md +19 -0
  37. package/adl_drift/harness/skills/drift-generic.md +19 -0
  38. package/adl_drift/harness/watch.py +84 -0
  39. package/adl_drift/lang/__init__.py +8 -0
  40. package/adl_drift/lang/base.py +464 -0
  41. package/adl_drift/lang/java/__init__.py +6 -0
  42. package/adl_drift/lang/java/backend.py +97 -0
  43. package/adl_drift/lang/java/cfg.py +1424 -0
  44. package/adl_drift/lang/java/config.py +707 -0
  45. package/adl_drift/lang/java/detectors/__init__.py +29 -0
  46. package/adl_drift/lang/java/detectors/additional.py +445 -0
  47. package/adl_drift/lang/java/detectors/base.py +154 -0
  48. package/adl_drift/lang/java/detectors/data_access.py +148 -0
  49. package/adl_drift/lang/java/detectors/imports.py +53 -0
  50. package/adl_drift/lang/java/detectors/routes.py +159 -0
  51. package/adl_drift/lang/java/detectors/security.py +386 -0
  52. package/adl_drift/lang/java/detectors/tests.py +74 -0
  53. package/adl_drift/lang/java/extractor.py +582 -0
  54. package/adl_drift/lang/java/ids.py +99 -0
  55. package/adl_drift/lang/java/parser.py +49 -0
  56. package/adl_drift/lang/java/resolution/__init__.py +1 -0
  57. package/adl_drift/lang/java/resolution/constants.py +1619 -0
  58. package/adl_drift/lang/java/resolution/constructors.py +278 -0
  59. package/adl_drift/lang/java/resolution/context.py +305 -0
  60. package/adl_drift/lang/java/resolution/dispatch.py +540 -0
  61. package/adl_drift/lang/java/resolution/functional.py +1318 -0
  62. package/adl_drift/lang/java/resolution/generated.py +95 -0
  63. package/adl_drift/lang/java/resolution/model.py +153 -0
  64. package/adl_drift/lang/java/resolution/orchestration.py +866 -0
  65. package/adl_drift/lang/java/resolution/overloads.py +1275 -0
  66. package/adl_drift/lang/java/resolution/scopes.py +1111 -0
  67. package/adl_drift/lang/java/resolution/types.py +2947 -0
  68. package/adl_drift/lang/java/resolver.py +1401 -0
  69. package/adl_drift/lang/java/scopes.py +69 -0
  70. package/adl_drift/lang/java/taint.py +500 -0
  71. package/adl_drift/lang/java/type_provenance.py +342 -0
  72. package/adl_drift/lang/python/__init__.py +8 -0
  73. package/adl_drift/lang/python/backend.py +94 -0
  74. package/adl_drift/lang/python/cfg.py +537 -0
  75. package/adl_drift/lang/python/extractor.py +454 -0
  76. package/adl_drift/lang/python/parser.py +41 -0
  77. package/adl_drift/lang/python/resolver.py +1789 -0
  78. package/adl_drift/lang/python/taint.py +365 -0
  79. package/adl_drift/lang/registry.py +117 -0
  80. package/adl_drift/parser/__init__.py +1 -0
  81. package/adl_drift/parser/cache.py +38 -0
  82. package/adl_drift/parser/core.py +33 -0
  83. package/adl_drift/parser/extractor.py +17 -0
  84. package/adl_drift/parser/literal_eval.py +326 -0
  85. package/adl_drift/scanner/__init__.py +1 -0
  86. package/adl_drift/scanner/discovery.py +338 -0
  87. package/adl_drift/scanner/incremental.py +261 -0
  88. package/adl_drift/scanner/parallel.py +224 -0
  89. package/adl_drift/server/__init__.py +1 -0
  90. package/adl_drift/server/handlers.py +2849 -0
  91. package/adl_drift/server/mcp_server.py +127 -0
  92. package/adl_drift/server/mcp_stdio.py +335 -0
  93. package/adl_drift/server/protocol.py +71 -0
  94. package/adl_drift/server/static/index.html +696 -0
  95. package/adl_drift/server/web.py +350 -0
  96. package/adl_drift/utils/__init__.py +1 -0
  97. package/adl_drift/utils/config.py +77 -0
  98. package/adl_drift/utils/fswalk.py +52 -0
  99. package/adl_drift/utils/types.py +345 -0
  100. package/bin/adl-drift.mjs +35 -0
  101. package/bin/adl-toolkit.mjs +488 -0
  102. package/package.json +29 -4
  103. package/pyproject.toml +49 -0
  104. package/scripts/install_toolkit.py +3584 -0
@@ -0,0 +1,582 @@
1
+ """Symbol table extractor — walks the Java CST and produces Symbol records
2
+ (P4.2, PROBLEMS.md).
3
+
4
+ Java mapping (see PROBLEMS.md P4.2 for the full writeup):
5
+ - `method_declaration` / constructor declarations -> `Symbol(kind="function")`,
6
+ param types read from `formal_parameters` (Java types are mandatory, so
7
+ `param_types`/`return_type` are always populated, unlike Python's optional
8
+ annotations), plus a `signature_key` overload discriminator (P4.5).
9
+ - `class_declaration` / `interface_declaration` / `enum_declaration` /
10
+ `record_declaration` -> `Symbol(kind="class")`; `superclasses` from
11
+ `extends`, `implements` from `implements` (P4.9).
12
+ - `field_declaration` / `constant_declaration` -> `Symbol(kind="attribute",
13
+ parent=<class>)` — a direct extraction (Java has explicit field
14
+ declarations; no Python-style `self.X` heuristic needed). An interface
15
+ body spells its implicitly `public static final` fields (JLS 9.3) as
16
+ `constant_declaration` nodes with the same `type` + `variable_declarator`
17
+ shape; without them `Config DEFAULT = ...` on an interface was invisible
18
+ to every field-typed receiver chain (batch 25).
19
+ - `annotation` / `marker_annotation` nodes -> decorator-equivalent strings on
20
+ the symbol's existing `decorators` field.
21
+ - `import_declaration` -> `ImportRecord`, covering plain, `static`, and
22
+ wildcard (`*`) imports.
23
+
24
+ Deliberately not implemented (no Java equivalent, per P4.2's own scope):
25
+ `async`, and `exec`/`eval` literal-template expansion (P2.16, Python-only).
26
+ Also deliberately not implemented here: `Symbol.is_test` — unlike Python's
27
+ own `_looks_like_test`, JUnit/TestNG detection is P4.3's job (`lang/java/
28
+ detectors/tests.py::JavaTestDetector`), which already owns the shared,
29
+ provenance-checked annotation-import matching (`lang/java/type_provenance.py`).
30
+ `Symbol.is_test` is backfilled from that detector's `java_test` pattern by
31
+ `graph/builder.py::add_file_result`, the same way `ROUTES` patterns backfill
32
+ `is_entry_point` — see PROBLEMS.md P4.2's "Fix applied" note. A second,
33
+ extractor-local reimplementation previously existed here and was removed:
34
+ it duplicated (and could silently drift from) the detector's logic.
35
+ """
36
+
37
+ from __future__ import annotations
38
+
39
+ from pathlib import Path
40
+ from typing import Any, Dict, List, Optional
41
+
42
+ from tree_sitter import Node
43
+
44
+ from adl_drift.lang.base import ParseResult
45
+ from adl_drift.lang.base import SymbolExtractor as _BaseSymbolExtractor
46
+ from adl_drift.lang.java.ids import DEFAULT_PACKAGE, erased_type, signature_key
47
+ from adl_drift.utils.types import AnalysisResult, ImportRecord, Symbol
48
+
49
+ _CLASS_TYPES = (
50
+ "class_declaration",
51
+ "interface_declaration",
52
+ "enum_declaration",
53
+ "record_declaration",
54
+ "annotation_type_declaration",
55
+ )
56
+ _SCOPE_CLASS_TYPES = _CLASS_TYPES
57
+ _CONSTRUCTOR_TYPES = ("constructor_declaration", "compact_constructor_declaration")
58
+ _METHOD_TYPES = ("method_declaration", *_CONSTRUCTOR_TYPES)
59
+ _ANNOTATION_TYPES = ("annotation", "marker_annotation")
60
+ _CLASS_KEYWORD = {
61
+ "class_declaration": "class",
62
+ "interface_declaration": "interface",
63
+ "enum_declaration": "enum",
64
+ "record_declaration": "record",
65
+ "annotation_type_declaration": "annotation",
66
+ }
67
+
68
+
69
+ class JavaSymbolExtractor(_BaseSymbolExtractor):
70
+ """Extracts method/class/field symbols + import records from a parsed Java file."""
71
+
72
+ def _build(self, parse: ParseResult, file_path: Path) -> AnalysisResult:
73
+ symbols: List[Symbol] = []
74
+ imports: List[str] = []
75
+ import_records: List[ImportRecord] = []
76
+ seen_field_keys: set = set() # (scope_chain, parent or "", name) — dedupe fields only;
77
+ # overloaded methods legitimately share this key and must NOT be deduped here (P4.5's
78
+ # signature_key is what disambiguates them downstream, not this pass).
79
+
80
+ anon_index = self._index_anonymous_classes(parse.tree.root_node)
81
+
82
+ for node in self._walk(parse.tree.root_node):
83
+ if node.type in _METHOD_TYPES:
84
+ sym = self._extract_method(node, parse.source_bytes, file_path, anon_index)
85
+ if sym:
86
+ symbols.append(sym)
87
+ elif node.type in _CLASS_TYPES:
88
+ sym = self._extract_class(node, parse.source_bytes, file_path, anon_index)
89
+ if sym:
90
+ symbols.append(sym)
91
+ elif self._is_anonymous_class_body(node):
92
+ symbols.append(self._extract_anonymous_class(
93
+ node, parse.source_bytes, file_path, anon_index,
94
+ ))
95
+ elif node.type in ("field_declaration", "constant_declaration"):
96
+ for sym in self._extract_fields(node, parse.source_bytes, file_path, anon_index):
97
+ key = self._dedupe_key(sym)
98
+ if key in seen_field_keys:
99
+ continue
100
+ seen_field_keys.add(key)
101
+ symbols.append(sym)
102
+ elif node.type == "import_declaration":
103
+ imports.append(self.parser.node_text(node, parse.source_bytes).strip())
104
+ import_records.extend(
105
+ self._extract_import_records(node, parse.source_bytes)
106
+ )
107
+
108
+ return self._finalize(
109
+ parse,
110
+ file_path,
111
+ symbols,
112
+ imports,
113
+ import_records,
114
+ language="java",
115
+ module_name=self.module_name_from_parse(parse),
116
+ )
117
+
118
+ def module_name_for_file(self, file_path: Path) -> str:
119
+ return self.module_name_from_parse(self.parser.parse_file(file_path))
120
+
121
+ def module_name_from_parse(self, parse: ParseResult) -> str:
122
+ """Return the declared Java package, or a stable default-package id."""
123
+ for node in parse.tree.root_node.named_children:
124
+ if node.type != "package_declaration":
125
+ continue
126
+ for child in reversed(node.named_children):
127
+ if child.type in ("identifier", "scoped_identifier"):
128
+ return self.parser.node_text(child, parse.source_bytes).strip()
129
+ return DEFAULT_PACKAGE
130
+
131
+ # -- imports ----------------------------------------------------------
132
+
133
+ def _extract_import_records(self, node: Node, source: bytes) -> List[ImportRecord]:
134
+ """Handles `import a.b.C;`, `import static a.b.C.m;`, and
135
+ `import a.b.*;` (plain or static wildcard)."""
136
+ is_static = any(c.type == "static" for c in node.children)
137
+ has_wildcard = any(c.type == "asterisk" for c in node.children)
138
+
139
+ scoped: Optional[Node] = None
140
+ for child in node.children:
141
+ if child.type in ("scoped_identifier", "identifier"):
142
+ scoped = child
143
+ break
144
+ if scoped is None:
145
+ return []
146
+
147
+ if has_wildcard:
148
+ return [ImportRecord(
149
+ module=self.parser.node_text(scoped, source),
150
+ name="*",
151
+ is_wildcard=True,
152
+ is_static=is_static,
153
+ )]
154
+
155
+ name_node = scoped.child_by_field_name("name")
156
+ if name_node is None:
157
+ # bare `import Foo;` (default-package type — legal, rare)
158
+ return [ImportRecord(
159
+ module="", name=self.parser.node_text(scoped, source), is_static=is_static,
160
+ )]
161
+ scope_node = scoped.child_by_field_name("scope")
162
+ module = self.parser.node_text(scope_node, source) if scope_node is not None else ""
163
+ return [ImportRecord(
164
+ module=module,
165
+ name=self.parser.node_text(name_node, source),
166
+ is_static=is_static,
167
+ )]
168
+
169
+ # -- methods / constructors --------------------------------------------
170
+
171
+ def _extract_method(
172
+ self, node: Node, source: bytes, file_path: Path, anon_index: Dict[int, int],
173
+ ) -> Optional[Symbol]:
174
+ name_node = node.child_by_field_name("name")
175
+ if name_node is None:
176
+ return None
177
+ name = self.parser.node_text(name_node, source)
178
+ params_node = self._method_parameters(node)
179
+ param_types = self._extract_param_types(params_node, source) if params_node else {}
180
+ params_text = self.parser.node_text(params_node, source) if params_node else "()"
181
+
182
+ return_type_node = node.child_by_field_name("type") # absent for constructors
183
+ return_type = (
184
+ self.parser.node_text(return_type_node, source).strip() if return_type_node else None
185
+ )
186
+ return_dims = node.child_by_field_name("dimensions")
187
+ if return_dims is not None and return_type is not None:
188
+ return_type += self.parser.node_text(return_dims, source).strip()
189
+ signature = f"{return_type} {name}{params_text}" if return_type else f"{name}{params_text}"
190
+
191
+ decorators = self._extract_annotations(node, source)
192
+ parent = self._find_parent_class(node, source, anon_index)
193
+ scope_chain = self._scope_chain(node, source, anon_index)
194
+ modifiers = self._extract_modifiers(node, source)
195
+
196
+ # Persist static-ness into metadata (not just `modifiers`): the graph
197
+ # builder copies `symbol.metadata` onto the GraphNode but drops
198
+ # `symbol.modifiers`, so the resolver — which sees only GraphNodes —
199
+ # can read `java_is_static` but never `modifiers`. Needed so the
200
+ # static-import call path can reject an equally-named *instance*
201
+ # method that `import static a.b.C.name;` cannot legally bind (F-02).
202
+ metadata: Dict[str, object] = (
203
+ {"constructor": True} if node.type in _CONSTRUCTOR_TYPES else {}
204
+ )
205
+ metadata["java_is_static"] = "static" in modifiers
206
+ # P4.12 abstract-dispatch fan-out needs to tell an *abstract* method
207
+ # (interface method with no body, or `abstract`-modified method on an
208
+ # abstract class — the receiver whose runtime target is unknown) from a
209
+ # concrete one. `native` methods also have no Java body, but still have
210
+ # a valid runtime target, so they must keep their direct edge.
211
+ # `java_return_type` is persisted for the same issue's `@Bean`
212
+ # factory-method bean index (a `@Bean T make()` registers `T` as an
213
+ # injectable candidate — see the resolver).
214
+ metadata["java_is_abstract"] = self._is_java_abstract_method(node, modifiers)
215
+ if return_type is not None:
216
+ metadata["java_return_type"] = return_type
217
+
218
+ return Symbol(
219
+ name=name,
220
+ kind="function",
221
+ file_path=file_path,
222
+ position=self.parser.node_position(node),
223
+ decorators=decorators,
224
+ signature=signature,
225
+ parent=parent,
226
+ param_types=param_types,
227
+ return_type=return_type,
228
+ throws=self._extract_throws(node, source),
229
+ modifiers=modifiers,
230
+ scope_chain=scope_chain,
231
+ signature_key=signature_key(param_types.values()),
232
+ metadata=metadata,
233
+ )
234
+
235
+ @staticmethod
236
+ def _is_java_abstract_method(node: Node, modifiers: List[str]) -> bool:
237
+ if node.type != "method_declaration":
238
+ return False
239
+ if node.child_by_field_name("body") is not None:
240
+ return False
241
+ if "native" in modifiers:
242
+ return False
243
+ if "abstract" in modifiers:
244
+ return True
245
+
246
+ parent = node.parent
247
+ while parent is not None:
248
+ if parent.type in (
249
+ "class_declaration",
250
+ "interface_declaration",
251
+ "enum_declaration",
252
+ "record_declaration",
253
+ "annotation_type_declaration",
254
+ ):
255
+ return parent.type in ("interface_declaration", "annotation_type_declaration")
256
+ parent = parent.parent
257
+ return False
258
+
259
+ @staticmethod
260
+ def _method_parameters(node: Node) -> Optional[Node]:
261
+ params = node.child_by_field_name("parameters")
262
+ if params is not None or node.type != "compact_constructor_declaration":
263
+ return params
264
+ record = node.parent.parent if node.parent is not None else None
265
+ if record is not None and record.type == "record_declaration":
266
+ return record.child_by_field_name("parameters")
267
+ return None
268
+
269
+ def _extract_param_types(self, params_node: Node, source: bytes) -> Dict[str, str]:
270
+ """Walk `formal_parameters` children, return {param_name: type_text}.
271
+
272
+ Handles plain `formal_parameter` and varargs `spread_parameter`
273
+ (`String... args` -> type text `String...`, which `ids.erased_type`
274
+ normalizes to `String[]`).
275
+ """
276
+ out: Dict[str, str] = {}
277
+ for child in params_node.children:
278
+ if child.type == "formal_parameter":
279
+ type_node = child.child_by_field_name("type")
280
+ name_node = child.child_by_field_name("name")
281
+ if type_node is not None and name_node is not None:
282
+ type_text = self.parser.node_text(type_node, source).strip()
283
+ dimensions = child.child_by_field_name("dimensions")
284
+ if dimensions is not None:
285
+ type_text += self.parser.node_text(dimensions, source).strip()
286
+ out[self.parser.node_text(name_node, source)] = type_text
287
+ elif child.type == "spread_parameter":
288
+ type_parts: List[str] = []
289
+ var_decl: Optional[Node] = None
290
+ for sub in child.children:
291
+ if sub.type == "variable_declarator":
292
+ var_decl = sub
293
+ elif sub.type == "modifiers":
294
+ continue
295
+ else:
296
+ type_parts.append(self.parser.node_text(sub, source))
297
+ if var_decl is not None:
298
+ name_node = var_decl.child_by_field_name("name")
299
+ if name_node is not None:
300
+ out[self.parser.node_text(name_node, source)] = "".join(type_parts).strip()
301
+ return out
302
+
303
+ # -- classes / interfaces / enums / records ----------------------------
304
+
305
+ def _extract_class(
306
+ self, node: Node, source: bytes, file_path: Path, anon_index: Dict[int, int]
307
+ ) -> Optional[Symbol]:
308
+ name_node = node.child_by_field_name("name")
309
+ if name_node is None:
310
+ return None
311
+ name = self.parser.node_text(name_node, source)
312
+ decorators = self._extract_annotations(node, source)
313
+ parent = self._find_parent_class(node, source, anon_index)
314
+ scope_chain = self._scope_chain(node, source, anon_index)
315
+ superclasses_raw = self._extract_superclasses(node, source)
316
+ implements_raw = self._extract_implements(node, source)
317
+ modifiers = self._extract_modifiers(node, source)
318
+
319
+ metadata: Dict[str, Any] = {"java_type_kind": _CLASS_KEYWORD[node.type]}
320
+ if node.type == "enum_declaration":
321
+ body = node.child_by_field_name("body")
322
+ metadata["enum_constants"] = [
323
+ self.parser.node_text(name_node, source)
324
+ for child in (body.children if body is not None else [])
325
+ if child.type == "enum_constant"
326
+ for name_node in [child.child_by_field_name("name")]
327
+ if name_node is not None
328
+ ]
329
+ # `Symbol.superclasses`/`implements` feed the shared, language-agnostic
330
+ # `graph/builder.py::resolve_relationships` INHERITS/IMPLEMENTS lookup,
331
+ # which expects a plain declared name the same way the Python
332
+ # extractor's own `_extract_superclasses` already reduces `Generic[T]`
333
+ # to `Generic` — an un-erased `Box<Stripe>` never matches the by-name
334
+ # index there, so the edge (and everything built on it: ancestor
335
+ # method lookup, OVERRIDES, P4.12's fan-out/DI narrowing) was silently
336
+ # never created for any class with a parameterized supertype. The raw,
337
+ # generic-preserving text is kept separately in metadata for
338
+ # `lang/java/resolver.py`'s generic-return-through-inheritance
339
+ # substitution (F-01), which is the one place that argument text is
340
+ # still needed.
341
+ try:
342
+ superclasses = [erased_type(s) for s in superclasses_raw]
343
+ implements = [erased_type(s) for s in implements_raw]
344
+ except ValueError:
345
+ superclasses, implements = superclasses_raw, implements_raw
346
+ metadata["java_superclass_args"] = superclasses_raw
347
+ metadata["java_implements_args"] = implements_raw
348
+ # P4.12: a receiver typed as an interface *or* an `abstract class` has
349
+ # an unknown runtime implementation and drives the fan-out; a plain
350
+ # concrete class does not. `interface` is already covered by
351
+ # `java_type_kind`; `abstract` on a class needs its own flag.
352
+ metadata["java_is_abstract"] = "abstract" in modifiers
353
+ if node.type == "record_declaration":
354
+ # Records have an implicit canonical constructor whose arity
355
+ # equals the component count — resolver._resolve_constructor
356
+ # (P4.4 P1 fix) needs this to reject calls with the wrong
357
+ # argument count instead of accepting any `new Point(...)`.
358
+ params_node = node.child_by_field_name("parameters")
359
+ if params_node is not None:
360
+ components = self._extract_param_types(params_node, source)
361
+ metadata["record_component_types"] = list(components.values())
362
+ metadata["record_components"] = dict(components)
363
+ else:
364
+ metadata["record_component_types"] = []
365
+ metadata["record_components"] = {}
366
+
367
+ return Symbol(
368
+ name=name,
369
+ kind="class",
370
+ file_path=file_path,
371
+ position=self.parser.node_position(node),
372
+ decorators=decorators,
373
+ signature=f"{_CLASS_KEYWORD[node.type]} {name}",
374
+ parent=parent,
375
+ superclasses=superclasses,
376
+ implements=implements,
377
+ modifiers=modifiers,
378
+ scope_chain=scope_chain,
379
+ metadata=metadata,
380
+ )
381
+
382
+ def _extract_anonymous_class(
383
+ self, node: Node, source: bytes, file_path: Path, anon_index: Dict[int, int],
384
+ ) -> Symbol:
385
+ """Materialize the synthetic owner already used by anonymous members.
386
+
387
+ Methods and fields inside an anonymous class carry a `$N` scope segment.
388
+ Without a matching CLASS node their CONTAINS edge is structurally
389
+ dangling even though call resolution remains possible.
390
+ """
391
+ name = f"${anon_index[node.start_byte]}"
392
+ return Symbol(
393
+ name=name,
394
+ kind="class",
395
+ file_path=file_path,
396
+ position=self.parser.node_position(node),
397
+ signature=f"anonymous class {name}",
398
+ parent=self._find_parent_class(node, source, anon_index),
399
+ scope_chain=self._scope_chain(node, source, anon_index),
400
+ metadata={"java_type_kind": "anonymous", "synthetic": "anonymous_class"},
401
+ )
402
+
403
+ def _extract_type_list(self, node: Node, source: bytes) -> List[str]:
404
+ """Collect raw type text from a `type_list` (or a single bare type
405
+ child, e.g. a class's single `extends Base`)."""
406
+ out: List[str] = []
407
+ for child in node.children:
408
+ if child.type == "type_list":
409
+ out.extend(self._extract_type_list(child, source))
410
+ elif child.type in ("type_identifier", "scoped_type_identifier", "generic_type"):
411
+ out.append(self.parser.node_text(child, source).strip())
412
+ return out
413
+
414
+ def _extract_superclasses(self, node: Node, source: bytes) -> List[str]:
415
+ """`extends Base` on a class (field `superclass`) or `extends A, B`
416
+ on an interface (positional `extends_interfaces` — interfaces permit
417
+ multiple, so it isn't a named single-value field like `superclass`)."""
418
+ superclass_node = node.child_by_field_name("superclass")
419
+ if superclass_node is not None:
420
+ return self._extract_type_list(superclass_node, source)
421
+ for child in node.children:
422
+ if child.type == "extends_interfaces":
423
+ return self._extract_type_list(child, source)
424
+ return []
425
+
426
+ def _extract_implements(self, node: Node, source: bytes) -> List[str]:
427
+ interfaces_node = node.child_by_field_name("interfaces")
428
+ if interfaces_node is None:
429
+ return []
430
+ return self._extract_type_list(interfaces_node, source)
431
+
432
+ # -- fields --------------------------------------------------------------
433
+
434
+ def _extract_fields(
435
+ self, node: Node, source: bytes, file_path: Path, anon_index: Dict[int, int]
436
+ ) -> List[Symbol]:
437
+ type_node = node.child_by_field_name("type")
438
+ base_type_text = self.parser.node_text(type_node, source).strip() if type_node else None
439
+ decorators = self._extract_annotations(node, source)
440
+ modifiers = self._extract_modifiers(node, source)
441
+ parent = self._find_parent_class(node, source, anon_index)
442
+ scope_chain = self._scope_chain(node, source, anon_index)
443
+
444
+ out: List[Symbol] = []
445
+ for child in node.children:
446
+ if child.type != "variable_declarator":
447
+ continue
448
+ name_node = child.child_by_field_name("name")
449
+ if name_node is None:
450
+ continue
451
+ type_text = base_type_text
452
+ dimensions = child.child_by_field_name("dimensions")
453
+ if dimensions is not None and type_text is not None:
454
+ type_text += self.parser.node_text(dimensions, source).strip()
455
+ out.append(Symbol(
456
+ name=self.parser.node_text(name_node, source),
457
+ kind="attribute",
458
+ file_path=file_path,
459
+ position=self.parser.node_position(child),
460
+ decorators=decorators,
461
+ signature=type_text,
462
+ parent=parent,
463
+ modifiers=modifiers,
464
+ scope_chain=scope_chain,
465
+ ))
466
+ return out
467
+
468
+ # -- annotations (decorator-equivalent) -----------------------------------
469
+
470
+ def _extract_annotations(self, node: Node, source: bytes) -> List[str]:
471
+ modifiers_node: Optional[Node] = None
472
+ for child in node.children:
473
+ if child.type == "modifiers":
474
+ modifiers_node = child
475
+ break
476
+ if modifiers_node is None:
477
+ return []
478
+ return [
479
+ self.parser.node_text(child, source).strip()
480
+ for child in modifiers_node.children
481
+ if child.type in _ANNOTATION_TYPES
482
+ ]
483
+
484
+ def _extract_modifiers(self, node: Node, source: bytes) -> List[str]:
485
+ for child in node.children:
486
+ if child.type == "modifiers":
487
+ return [
488
+ self.parser.node_text(part, source).strip()
489
+ for part in child.children
490
+ if part.type not in _ANNOTATION_TYPES and not part.is_extra
491
+ ]
492
+ return []
493
+
494
+ def _extract_throws(self, node: Node, source: bytes) -> List[str]:
495
+ for child in node.children:
496
+ if child.type == "throws":
497
+ return [
498
+ self.parser.node_text(exception, source).strip()
499
+ for exception in child.named_children
500
+ if not exception.is_extra
501
+ ]
502
+ return []
503
+
504
+ # -- scope chain: nested/inner/local/anonymous classes --------------------
505
+
506
+ def _index_anonymous_classes(self, root: Node) -> Dict[int, int]:
507
+ """Assign anonymous class bodies an ordinal within their callable.
508
+
509
+ Enum constants share their enclosing enum as the scope. This keeps
510
+ IDs stable when an unrelated earlier callable gains an anonymous
511
+ class while still distinguishing enum-constant subclasses.
512
+ """
513
+ index: Dict[int, int] = {}
514
+ counts: Dict[int, int] = {}
515
+ for node in self._walk(root):
516
+ if not self._is_anonymous_class_body(node):
517
+ continue
518
+ owner = node.parent
519
+ while owner is not None:
520
+ if owner.type in _METHOD_TYPES or owner.type in _CLASS_TYPES:
521
+ break
522
+ owner = owner.parent
523
+ owner_start = owner.start_byte if owner is not None else root.start_byte
524
+ counts[owner_start] = counts.get(owner_start, 0) + 1
525
+ index[node.start_byte] = counts[owner_start]
526
+ return index
527
+
528
+ @staticmethod
529
+ def _is_anonymous_class_body(node: Node) -> bool:
530
+ return node.type == "class_body" and node.parent is not None \
531
+ and node.parent.type in ("object_creation_expression", "enum_constant")
532
+
533
+ def _scope_chain(
534
+ self, node: Node, source: bytes, anon_index: Dict[int, int]
535
+ ) -> List[str]:
536
+ """Java's own scope chain (P2.17/P1.10's `<locals>` semantics don't
537
+ apply): nested/inner/static-nested classes contribute their name;
538
+ a local class's enclosing method also contributes its name (methods
539
+ can't nest in Java, so a method only ever shows up here when it
540
+ encloses a local or anonymous class); an anonymous class contributes
541
+ a synthetic `$N` segment. `symbol.parent` (nearest enclosing
542
+ class-like ancestor) stays the separate, coarser field used for
543
+ display/self-reference; this is the uniqueness key."""
544
+ chain: List[str] = []
545
+ p = node.parent
546
+ while p is not None:
547
+ if p.type in _SCOPE_CLASS_TYPES:
548
+ nn = p.child_by_field_name("name")
549
+ if nn is not None:
550
+ chain.append(self.parser.node_text(nn, source))
551
+ elif p.type in _METHOD_TYPES:
552
+ nn = p.child_by_field_name("name")
553
+ if nn is not None:
554
+ params = self._method_parameters(p)
555
+ param_types = self._extract_param_types(params, source) if params else {}
556
+ discriminator = signature_key(param_types.values())
557
+ if p.type in _CONSTRUCTOR_TYPES:
558
+ chain.append(discriminator)
559
+ else:
560
+ chain.append(self.parser.node_text(nn, source) + discriminator)
561
+ elif self._is_anonymous_class_body(p):
562
+ chain.append(f"${anon_index[p.start_byte]}")
563
+ p = p.parent
564
+ chain.reverse()
565
+ return chain
566
+
567
+ def _find_parent_class(
568
+ self, node: Node, source: bytes, anon_index: Dict[int, int]
569
+ ) -> Optional[str]:
570
+ """Nearest enclosing class-like ancestor's name (real or synthetic
571
+ anonymous-class segment), skipping over intervening methods —
572
+ mirrors `lang.python.extractor`'s `_find_parent_class`."""
573
+ p = node.parent
574
+ while p is not None:
575
+ if p.type in _SCOPE_CLASS_TYPES:
576
+ nn = p.child_by_field_name("name")
577
+ if nn is not None:
578
+ return self.parser.node_text(nn, source)
579
+ elif self._is_anonymous_class_body(p):
580
+ return f"${anon_index[p.start_byte]}"
581
+ p = p.parent
582
+ return None
@@ -0,0 +1,99 @@
1
+ """Java overload identity helpers (P4.5).
2
+
3
+ Kept parser-free so P4.1 and P4.2 can use it independently.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import re
9
+ from collections.abc import Iterable
10
+ from functools import lru_cache
11
+
12
+
13
+ DEFAULT_PACKAGE = "<default>"
14
+
15
+
16
+ _TYPE_ANNOTATION = re.compile(r"@[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)*")
17
+
18
+
19
+ def _strip_type_annotations(text: str) -> str:
20
+ """Remove Java type-use annotations, including nested/quoted arguments."""
21
+ out: list[str] = []
22
+ i = 0
23
+ while i < len(text):
24
+ match = _TYPE_ANNOTATION.match(text, i)
25
+ if match is None:
26
+ out.append(text[i])
27
+ i += 1
28
+ continue
29
+
30
+ i = match.end()
31
+ while i < len(text) and text[i].isspace():
32
+ i += 1
33
+ if i < len(text) and text[i] == "(":
34
+ depth = 0
35
+ quote: str | None = None
36
+ escaped = False
37
+ while i < len(text):
38
+ char = text[i]
39
+ i += 1
40
+ if quote:
41
+ if escaped:
42
+ escaped = False
43
+ elif char == "\\":
44
+ escaped = True
45
+ elif char == quote:
46
+ quote = None
47
+ elif char in ('"', "'"):
48
+ quote = char
49
+ elif char == "(":
50
+ depth += 1
51
+ elif char == ")":
52
+ depth -= 1
53
+ if depth == 0:
54
+ break
55
+ if depth:
56
+ raise ValueError(f"unbalanced type annotation: {text!r}")
57
+ while i < len(text) and text[i].isspace():
58
+ i += 1
59
+ return "".join(out)
60
+
61
+
62
+ @lru_cache(maxsize=None)
63
+ def erased_type(type_text: str) -> str:
64
+ """Return stable source-level Java type erasure for node identity.
65
+
66
+ Pure function of `type_text` alone (no graph/analysis state), so caching
67
+ is unconditionally safe and unbounded cache growth is bounded in
68
+ practice by the number of distinct type-strings in a corpus (thousands,
69
+ not millions). Memoized because the same handful of type strings
70
+ ("String", "int", "List<String>", ...) recur millions of times across a
71
+ large corpus's overload/param-type matching (§8 bước 7 profiling: 3.25M
72
+ calls on JUnit5, ~9% of resolve_calls time between this and its helper
73
+ `_strip_type_annotations`, which this is the only caller of).
74
+ """
75
+ text = _strip_type_annotations(type_text.strip())
76
+ out: list[str] = []
77
+ depth = 0
78
+ for char in text:
79
+ if char == "<":
80
+ depth += 1
81
+ elif char == ">":
82
+ depth -= 1
83
+ if depth < 0:
84
+ raise ValueError(f"unbalanced generic type: {type_text!r}")
85
+ elif depth == 0:
86
+ out.append(char)
87
+ if depth:
88
+ raise ValueError(f"unbalanced generic type: {type_text!r}")
89
+
90
+ erased = "".join(out).replace("...", "[]")
91
+ erased = "".join(erased.split())
92
+ if not erased:
93
+ raise ValueError("Java type cannot be empty")
94
+ return erased
95
+
96
+
97
+ def signature_key(parameter_types: Iterable[str]) -> str:
98
+ """Return erased overload suffix, e.g. ``(String,int[])``."""
99
+ return f"({','.join(erased_type(t) for t in parameter_types)})"