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.
- package/INSTALL_TOOLKIT.md +163 -0
- package/README.md +269 -2
- package/adl_drift/__init__.py +6 -0
- package/adl_drift/cli/__init__.py +1 -0
- package/adl_drift/cli/commands.py +993 -0
- package/adl_drift/cli/main.py +744 -0
- package/adl_drift/cortex/__init__.py +1 -0
- package/adl_drift/cortex/embeddings.py +96 -0
- package/adl_drift/cortex/memory.py +1749 -0
- package/adl_drift/cortex/patterns.py +89 -0
- package/adl_drift/cortex/telemetry.py +192 -0
- package/adl_drift/detectors/__init__.py +1 -0
- package/adl_drift/detectors/async_patterns.py +115 -0
- package/adl_drift/detectors/base.py +121 -0
- package/adl_drift/detectors/config.py +122 -0
- package/adl_drift/detectors/data_access.py +128 -0
- package/adl_drift/detectors/decorators.py +122 -0
- package/adl_drift/detectors/dynamic_patterns.py +260 -0
- package/adl_drift/detectors/errors.py +134 -0
- package/adl_drift/detectors/imports.py +132 -0
- package/adl_drift/detectors/logging_patterns.py +108 -0
- package/adl_drift/detectors/routes.py +257 -0
- package/adl_drift/detectors/security.py +236 -0
- package/adl_drift/detectors/taint.py +406 -0
- package/adl_drift/detectors/tests.py +95 -0
- package/adl_drift/detectors/type_conventions.py +81 -0
- package/adl_drift/graph/__init__.py +1 -0
- package/adl_drift/graph/builder.py +2670 -0
- package/adl_drift/graph/cfg.py +461 -0
- package/adl_drift/graph/cluster.py +40 -0
- package/adl_drift/graph/edges.py +86 -0
- package/adl_drift/graph/node.py +90 -0
- package/adl_drift/harness/__init__.py +10 -0
- package/adl_drift/harness/setup.py +832 -0
- package/adl_drift/harness/skills/adl-drift.md +25 -0
- package/adl_drift/harness/skills/drift-antigravity.md +19 -0
- package/adl_drift/harness/skills/drift-generic.md +19 -0
- package/adl_drift/harness/watch.py +84 -0
- package/adl_drift/lang/__init__.py +8 -0
- package/adl_drift/lang/base.py +464 -0
- package/adl_drift/lang/java/__init__.py +6 -0
- package/adl_drift/lang/java/backend.py +97 -0
- package/adl_drift/lang/java/cfg.py +1424 -0
- package/adl_drift/lang/java/config.py +707 -0
- package/adl_drift/lang/java/detectors/__init__.py +29 -0
- package/adl_drift/lang/java/detectors/additional.py +445 -0
- package/adl_drift/lang/java/detectors/base.py +154 -0
- package/adl_drift/lang/java/detectors/data_access.py +148 -0
- package/adl_drift/lang/java/detectors/imports.py +53 -0
- package/adl_drift/lang/java/detectors/routes.py +159 -0
- package/adl_drift/lang/java/detectors/security.py +386 -0
- package/adl_drift/lang/java/detectors/tests.py +74 -0
- package/adl_drift/lang/java/extractor.py +582 -0
- package/adl_drift/lang/java/ids.py +99 -0
- package/adl_drift/lang/java/parser.py +49 -0
- package/adl_drift/lang/java/resolution/__init__.py +1 -0
- package/adl_drift/lang/java/resolution/constants.py +1619 -0
- package/adl_drift/lang/java/resolution/constructors.py +278 -0
- package/adl_drift/lang/java/resolution/context.py +305 -0
- package/adl_drift/lang/java/resolution/dispatch.py +540 -0
- package/adl_drift/lang/java/resolution/functional.py +1318 -0
- package/adl_drift/lang/java/resolution/generated.py +95 -0
- package/adl_drift/lang/java/resolution/model.py +153 -0
- package/adl_drift/lang/java/resolution/orchestration.py +866 -0
- package/adl_drift/lang/java/resolution/overloads.py +1275 -0
- package/adl_drift/lang/java/resolution/scopes.py +1111 -0
- package/adl_drift/lang/java/resolution/types.py +2947 -0
- package/adl_drift/lang/java/resolver.py +1401 -0
- package/adl_drift/lang/java/scopes.py +69 -0
- package/adl_drift/lang/java/taint.py +500 -0
- package/adl_drift/lang/java/type_provenance.py +342 -0
- package/adl_drift/lang/python/__init__.py +8 -0
- package/adl_drift/lang/python/backend.py +94 -0
- package/adl_drift/lang/python/cfg.py +537 -0
- package/adl_drift/lang/python/extractor.py +454 -0
- package/adl_drift/lang/python/parser.py +41 -0
- package/adl_drift/lang/python/resolver.py +1789 -0
- package/adl_drift/lang/python/taint.py +365 -0
- package/adl_drift/lang/registry.py +117 -0
- package/adl_drift/parser/__init__.py +1 -0
- package/adl_drift/parser/cache.py +38 -0
- package/adl_drift/parser/core.py +33 -0
- package/adl_drift/parser/extractor.py +17 -0
- package/adl_drift/parser/literal_eval.py +326 -0
- package/adl_drift/scanner/__init__.py +1 -0
- package/adl_drift/scanner/discovery.py +338 -0
- package/adl_drift/scanner/incremental.py +261 -0
- package/adl_drift/scanner/parallel.py +224 -0
- package/adl_drift/server/__init__.py +1 -0
- package/adl_drift/server/handlers.py +2849 -0
- package/adl_drift/server/mcp_server.py +127 -0
- package/adl_drift/server/mcp_stdio.py +335 -0
- package/adl_drift/server/protocol.py +71 -0
- package/adl_drift/server/static/index.html +696 -0
- package/adl_drift/server/web.py +350 -0
- package/adl_drift/utils/__init__.py +1 -0
- package/adl_drift/utils/config.py +77 -0
- package/adl_drift/utils/fswalk.py +52 -0
- package/adl_drift/utils/types.py +345 -0
- package/bin/adl-drift.mjs +35 -0
- package/bin/adl-toolkit.mjs +488 -0
- package/package.json +29 -4
- package/pyproject.toml +49 -0
- 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)})"
|