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,278 @@
|
|
|
1
|
+
"""Java object and explicit-constructor resolution."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Dict, List, Optional, Tuple
|
|
6
|
+
|
|
7
|
+
from adl_drift.graph.edges import EdgeType
|
|
8
|
+
from adl_drift.lang.java.ids import signature_key
|
|
9
|
+
from adl_drift.lang.java.resolution.constants import argument_nodes
|
|
10
|
+
from adl_drift.lang.java.resolution.model import (
|
|
11
|
+
ImportContext as _ImportContext,
|
|
12
|
+
LocalScopes as _LocalScopes,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class ConstructorResolutionMixin:
|
|
17
|
+
def _resolve_constructor(
|
|
18
|
+
self, call_node, source: bytes, module_name: str, import_map: Dict[str, str],
|
|
19
|
+
wildcard_packages: List[str],
|
|
20
|
+
local_types: "_LocalScopes", param_types: Dict[str, str],
|
|
21
|
+
enclosing_class_id: Optional[str],
|
|
22
|
+
) -> Tuple[Optional[str], float, Optional[str]]:
|
|
23
|
+
"""Resolve an `object_creation_expression` to `(class_id,
|
|
24
|
+
confidence)`. Always targets the CLASS node — mirrors Python's
|
|
25
|
+
`ClassName(...)` -> CLASS-node INSTANTIATES semantics (P2.8) and the
|
|
26
|
+
existing `_impact_in_edges` fold of constructor callers onto the
|
|
27
|
+
class. Overload disambiguation among multiple declared constructors
|
|
28
|
+
only ever *adjusts confidence*; it never fans out to per-constructor
|
|
29
|
+
targets, since there is nothing else to point INSTANTIATES at
|
|
30
|
+
(a documented simplification vs. plain method overloading — see the
|
|
31
|
+
Fix Applied notes).
|
|
32
|
+
|
|
33
|
+
`local_types`/`param_types`/`enclosing_class_id` (the calling
|
|
34
|
+
method's own locals, parameters, and containing class) feed argument
|
|
35
|
+
type inference exactly like `_resolve_call` does for method calls —
|
|
36
|
+
`new Box(value)` must be able to read `value`'s declared type the
|
|
37
|
+
same way `repo.save(value)` can (P4.4 regression fix: this used to
|
|
38
|
+
call `_infer_arg_types(..., {}, {}, {})` unconditionally, so a
|
|
39
|
+
constructor argument's type was *always* treated as unknown even
|
|
40
|
+
when statically known, which both under-confidently fanned out an
|
|
41
|
+
otherwise-unambiguous call and — worse — let a sole arity-matching
|
|
42
|
+
but type-*incompatible* constructor resolve unconditionally at full
|
|
43
|
+
confidence, since `_select_overload`'s `arg_types is None` branch
|
|
44
|
+
never runs a type check against a single survivor)."""
|
|
45
|
+
type_node = call_node.child_by_field_name("type")
|
|
46
|
+
if type_node is None:
|
|
47
|
+
return None, 0.0, None
|
|
48
|
+
cls_id = self._resolve_declared_type(
|
|
49
|
+
self._text(type_node, source), module_name, import_map, wildcard_packages,
|
|
50
|
+
enclosing_class_id,
|
|
51
|
+
)
|
|
52
|
+
if cls_id is None:
|
|
53
|
+
return None, 0.0, None
|
|
54
|
+
cls_node = self.builder._nodes[cls_id]
|
|
55
|
+
type_kind = cls_node.metadata.get("java_type_kind")
|
|
56
|
+
if type_kind in ("interface", "enum"):
|
|
57
|
+
# Interfaces can't be instantiated with `new`, and enum instances
|
|
58
|
+
# can only come from their declared constants — neither has any
|
|
59
|
+
# constructor a call site could reach.
|
|
60
|
+
return None, 0.0, None
|
|
61
|
+
ctors = self._members_named(
|
|
62
|
+
cls_id, cls_node.name, search_ancestors=False,
|
|
63
|
+
caller_class_id=enclosing_class_id,
|
|
64
|
+
)
|
|
65
|
+
args_node = call_node.child_by_field_name("arguments")
|
|
66
|
+
arg_count = self._count_args(args_node)
|
|
67
|
+
field_types = (
|
|
68
|
+
self._all_field_types(enclosing_class_id) if enclosing_class_id else {}
|
|
69
|
+
)
|
|
70
|
+
arg_types = self._infer_arg_types(
|
|
71
|
+
args_node, source, local_types, param_types, field_types,
|
|
72
|
+
)
|
|
73
|
+
if not ctors:
|
|
74
|
+
# No explicit constructor: the compiler-generated implicit one
|
|
75
|
+
# takes zero arguments for a plain class — a call site with any
|
|
76
|
+
# arguments cannot actually reach it. `record` is the one
|
|
77
|
+
# exception: its implicit canonical constructor takes one arg
|
|
78
|
+
# per component, not zero, so check against the component arity
|
|
79
|
+
# extracted by JavaSymbolExtractor instead.
|
|
80
|
+
if type_kind == "record":
|
|
81
|
+
component_types = cls_node.metadata.get("record_component_types")
|
|
82
|
+
if component_types is not None and self._record_canonical_matches(
|
|
83
|
+
component_types, arg_count, arg_types, _ImportContext(
|
|
84
|
+
module_name, import_map, wildcard_packages,
|
|
85
|
+
),
|
|
86
|
+
):
|
|
87
|
+
return cls_id, 0.95, self._record_canonical_signature(component_types)
|
|
88
|
+
return None, 0.0, None
|
|
89
|
+
if arg_count == 0:
|
|
90
|
+
return cls_id, 0.95, "()"
|
|
91
|
+
return None, 0.0, None
|
|
92
|
+
# `this` has the statically known enclosing-class type, but the
|
|
93
|
+
# generic argument inferencer intentionally has no enclosing-class
|
|
94
|
+
# parameter. Supply that one exact fact here so constructor overloads
|
|
95
|
+
# such as `new ToStringBuilder(this)` select `(Object)` rather than
|
|
96
|
+
# retaining an ambiguous candidate set with no constructor signature.
|
|
97
|
+
args = argument_nodes(args_node)
|
|
98
|
+
if (
|
|
99
|
+
arg_types is None and len(args) == 1 and args[0].type == "this"
|
|
100
|
+
and enclosing_class_id is not None and ":" in enclosing_class_id
|
|
101
|
+
):
|
|
102
|
+
owner_module, owner_qualname = enclosing_class_id.rsplit(":", 1)
|
|
103
|
+
arg_types = [
|
|
104
|
+
f"{self.builder._display_module(owner_module)}.{owner_qualname}"
|
|
105
|
+
]
|
|
106
|
+
if arg_types is None and len(ctors) > 1:
|
|
107
|
+
arg_types = self._infer_contextual_arg_types(
|
|
108
|
+
args_node, source, module_name, enclosing_class_id, import_map,
|
|
109
|
+
wildcard_packages, local_types, param_types, field_types, {},
|
|
110
|
+
)
|
|
111
|
+
caller_ctx = _ImportContext(
|
|
112
|
+
module_name, import_map, wildcard_packages, enclosing_class_id,
|
|
113
|
+
)
|
|
114
|
+
target, conf = self._select_overload(
|
|
115
|
+
ctors, arg_count, arg_types, lambda _c: 0.95, caller_ctx,
|
|
116
|
+
)
|
|
117
|
+
if target is None:
|
|
118
|
+
# Every record has a canonical constructor for its components,
|
|
119
|
+
# even when the source also declares auxiliary constructors. A
|
|
120
|
+
# `record Key(Object event, Id id) { Key(Publication p) {...} }`
|
|
121
|
+
# therefore has a real two-argument construction path although
|
|
122
|
+
# the extracted member list contains only the auxiliary
|
|
123
|
+
# one-argument declaration. Only recover that implicit path
|
|
124
|
+
# when no declared constructor has the requested arity: if a
|
|
125
|
+
# declared same-arity candidate was rejected for incompatible
|
|
126
|
+
# types, preserving that rejection avoids manufacturing an edge.
|
|
127
|
+
component_types = cls_node.metadata.get("record_component_types")
|
|
128
|
+
if (
|
|
129
|
+
type_kind == "record"
|
|
130
|
+
and component_types is not None
|
|
131
|
+
and self._record_canonical_matches(
|
|
132
|
+
component_types, arg_count, arg_types, caller_ctx,
|
|
133
|
+
)
|
|
134
|
+
and not any(self._arity_matches(ctor, arg_count) for ctor in ctors)
|
|
135
|
+
):
|
|
136
|
+
return cls_id, 0.95, self._record_canonical_signature(component_types)
|
|
137
|
+
# No declared constructor matches this call's arg count — an
|
|
138
|
+
# INSTANTIATES edge here would point at a constructor the call
|
|
139
|
+
# site cannot actually reach.
|
|
140
|
+
return None, 0.0, None
|
|
141
|
+
selected = target[0] if isinstance(target, list) and len(target) == 1 else target
|
|
142
|
+
constructor_signature = (
|
|
143
|
+
signature_key(self._param_type_lists.get(selected, []))
|
|
144
|
+
if isinstance(selected, str) else None
|
|
145
|
+
)
|
|
146
|
+
return cls_id, (0.6 if isinstance(target, list) else conf), constructor_signature
|
|
147
|
+
|
|
148
|
+
def _record_canonical_matches(
|
|
149
|
+
self, component_types: List[str], arg_count: int,
|
|
150
|
+
arg_types: Optional[List[str]], arg_ctx: _ImportContext,
|
|
151
|
+
) -> bool:
|
|
152
|
+
"""Whether a record's implicit canonical constructor accepts a call.
|
|
153
|
+
|
|
154
|
+
JLS 8.10.4 gives the canonical constructor the record components'
|
|
155
|
+
parameter list verbatim. Consequently, a final ``T...`` component is
|
|
156
|
+
a real variable-arity parameter, not one array parameter that happens
|
|
157
|
+
to use an ellipsis in the header. Records do not materialize that
|
|
158
|
+
implicit constructor as a function node, so apply the same bounded
|
|
159
|
+
arity and compatibility rules used for an extracted vararg member.
|
|
160
|
+
"""
|
|
161
|
+
if not component_types:
|
|
162
|
+
return arg_count == 0
|
|
163
|
+
is_vararg = self._is_vararg_param(component_types[-1])
|
|
164
|
+
fixed_count = len(component_types) - 1 if is_vararg else len(component_types)
|
|
165
|
+
if arg_count < fixed_count or (not is_vararg and arg_count != fixed_count):
|
|
166
|
+
return False
|
|
167
|
+
# Preserve the established implicit-record behavior for ordinary
|
|
168
|
+
# canonical constructors. This batch only changes the JLS variable-
|
|
169
|
+
# arity form; widening the new argument-type filter to every record
|
|
170
|
+
# would reject existing callers whose source type spelling cannot be
|
|
171
|
+
# fully canonicalized in a multi-module graph.
|
|
172
|
+
if not is_vararg:
|
|
173
|
+
return True
|
|
174
|
+
if arg_types is None:
|
|
175
|
+
return True
|
|
176
|
+
if len(arg_types) != arg_count:
|
|
177
|
+
return False
|
|
178
|
+
# The record header is declared in the class's own source file.
|
|
179
|
+
# There is no function node from which `_param_ctx_for()` could read
|
|
180
|
+
# that context, but source and call contexts are identical for this
|
|
181
|
+
# implicit member.
|
|
182
|
+
if not all(
|
|
183
|
+
self._types_compatible(param, arg, arg_ctx, arg_ctx)
|
|
184
|
+
for param, arg in zip(component_types[:fixed_count], arg_types[:fixed_count])
|
|
185
|
+
):
|
|
186
|
+
return False
|
|
187
|
+
trailing = arg_types[fixed_count:]
|
|
188
|
+
array_type = component_types[-1].replace("...", "[]")
|
|
189
|
+
if len(trailing) == 1 and self._types_compatible(
|
|
190
|
+
array_type, trailing[0], arg_ctx, arg_ctx,
|
|
191
|
+
):
|
|
192
|
+
return True
|
|
193
|
+
component_type = self._vararg_component_type(array_type)
|
|
194
|
+
return all(
|
|
195
|
+
self._types_compatible(component_type, arg, arg_ctx, arg_ctx)
|
|
196
|
+
for arg in trailing
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
@staticmethod
|
|
200
|
+
def _record_canonical_signature(component_types: List[str]) -> str:
|
|
201
|
+
"""Metric identity for a record's implicit canonical constructor.
|
|
202
|
+
|
|
203
|
+
The source-level JavaParser oracle represents a canonical varargs
|
|
204
|
+
component as its element type (``Object``), while ordinary declared
|
|
205
|
+
constructor symbols retain erased array identity (``Object[]``).
|
|
206
|
+
Keep that oracle-compatible projection confined to the implicit
|
|
207
|
+
record member; overload identity for real declared constructors is
|
|
208
|
+
unchanged.
|
|
209
|
+
"""
|
|
210
|
+
if component_types and component_types[-1].strip().endswith("..."):
|
|
211
|
+
return signature_key([*component_types[:-1], component_types[-1].replace("...", "")])
|
|
212
|
+
return signature_key(component_types)
|
|
213
|
+
|
|
214
|
+
def _resolve_explicit_constructor(
|
|
215
|
+
self, call_node, source: bytes, module_name: str, import_map: Dict[str, str],
|
|
216
|
+
wildcard_packages: List[str], local_types: "_LocalScopes",
|
|
217
|
+
param_types: Dict[str, str], enclosing_class_id: Optional[str],
|
|
218
|
+
) -> Tuple[Optional[str], float, Optional[str]]:
|
|
219
|
+
"""Resolve ``this(...)``/``super(...)`` to the constructed class node."""
|
|
220
|
+
if enclosing_class_id is None:
|
|
221
|
+
return None, 0.0, None
|
|
222
|
+
keyword = self._text(call_node.named_children[0], source)
|
|
223
|
+
if keyword == "this":
|
|
224
|
+
cls_id = enclosing_class_id
|
|
225
|
+
elif keyword == "super":
|
|
226
|
+
parents = self._declared_direct_ancestors.get(
|
|
227
|
+
(enclosing_class_id, EdgeType.INHERITS), []
|
|
228
|
+
)
|
|
229
|
+
if not parents:
|
|
230
|
+
return None, 0.0, None
|
|
231
|
+
cls_id = parents[0]
|
|
232
|
+
else:
|
|
233
|
+
return None, 0.0, None
|
|
234
|
+
|
|
235
|
+
cls_node = self.builder._nodes.get(cls_id)
|
|
236
|
+
if cls_node is None:
|
|
237
|
+
return None, 0.0, None
|
|
238
|
+
ctors = self._members_named(
|
|
239
|
+
cls_id, cls_node.name, search_ancestors=False,
|
|
240
|
+
caller_class_id=enclosing_class_id,
|
|
241
|
+
)
|
|
242
|
+
args_node = next(
|
|
243
|
+
(child for child in call_node.named_children if child.type == "argument_list"),
|
|
244
|
+
None,
|
|
245
|
+
)
|
|
246
|
+
arg_count = self._count_args(args_node)
|
|
247
|
+
if not ctors:
|
|
248
|
+
return (cls_id, 0.95, "()") if arg_count == 0 else (None, 0.0, None)
|
|
249
|
+
arg_types = self._infer_arg_types(
|
|
250
|
+
args_node, source, local_types, param_types,
|
|
251
|
+
self._all_field_types(enclosing_class_id),
|
|
252
|
+
)
|
|
253
|
+
if arg_types is None and len(ctors) > 1:
|
|
254
|
+
arg_types = self._infer_contextual_arg_types(
|
|
255
|
+
args_node, source, module_name, enclosing_class_id, import_map,
|
|
256
|
+
wildcard_packages, local_types, param_types,
|
|
257
|
+
self._all_field_types(enclosing_class_id), {},
|
|
258
|
+
)
|
|
259
|
+
target, confidence = self._select_overload(
|
|
260
|
+
ctors, arg_count, arg_types, lambda _candidate: 0.95,
|
|
261
|
+
_ImportContext(
|
|
262
|
+
module_name, import_map, wildcard_packages, enclosing_class_id,
|
|
263
|
+
),
|
|
264
|
+
)
|
|
265
|
+
if target is None:
|
|
266
|
+
return None, 0.0, None
|
|
267
|
+
# Unlike `_resolve_constructor`, `this()`/`super()`'s target class is
|
|
268
|
+
# never in doubt (it's always the current class or its direct
|
|
269
|
+
# ancestor) — an ambiguous overload pick (`target` is a list of
|
|
270
|
+
# arity-matching candidates whose arg types weren't statically known)
|
|
271
|
+
# still means the class itself is reached, just at reduced
|
|
272
|
+
# confidence and with no single constructor signature to report.
|
|
273
|
+
selected = target[0] if isinstance(target, list) and len(target) == 1 else target
|
|
274
|
+
constructor_signature = (
|
|
275
|
+
signature_key(self._param_type_lists.get(selected, []))
|
|
276
|
+
if isinstance(selected, str) else None
|
|
277
|
+
)
|
|
278
|
+
return cls_id, (0.6 if isinstance(target, list) else confidence), constructor_signature
|
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
"""CST collection and lightweight indexes for Java resolution."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections import defaultdict
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Dict, List, Set, Tuple
|
|
8
|
+
|
|
9
|
+
from adl_drift.graph.node import NodeType
|
|
10
|
+
from adl_drift.lang.base import ParseResult
|
|
11
|
+
from adl_drift.lang.java.ids import DEFAULT_PACKAGE
|
|
12
|
+
from adl_drift.lang.java.resolution.constants import (
|
|
13
|
+
_ENCLOSING_TYPE_CST_TYPES,
|
|
14
|
+
_INIT_BLOCK_PARENTS,
|
|
15
|
+
_METHOD_CST_TYPES,
|
|
16
|
+
_NESTED_SCOPE_BOUNDARY,
|
|
17
|
+
)
|
|
18
|
+
from adl_drift.lang.java.resolution.model import MethodFacts
|
|
19
|
+
from adl_drift.utils.types import AnalysisResult
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _node_text(node, source: bytes) -> str:
|
|
23
|
+
return source[node.start_byte:node.end_byte].decode("utf-8", errors="replace")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class ContextResolutionMixin:
|
|
27
|
+
def _collect_annotation_type_names(
|
|
28
|
+
self, parses: Dict[Path, ParseResult]
|
|
29
|
+
) -> Dict[str, Set[str]]:
|
|
30
|
+
"""Simple name -> FQNs of every `@interface Foo {}` (`annotation_
|
|
31
|
+
type_declaration`) declared across the analyzed source — used by
|
|
32
|
+
`_annotation_stub_match` to tell a project's own annotation apart
|
|
33
|
+
from an (always-external) Lombok one. Read straight from the CST
|
|
34
|
+
because `JavaSymbolExtractor` does not emit a graph node for an
|
|
35
|
+
annotation type (it is not in `_CLASS_TYPES`), so there is no
|
|
36
|
+
`_by_module_symbol` entry to consult.
|
|
37
|
+
|
|
38
|
+
FQN = declaring file's package (via `self._file_ctx`, already built
|
|
39
|
+
by the time `resolve_calls()` calls this) + every enclosing
|
|
40
|
+
class/interface/enum/record/annotation-type name (outermost first,
|
|
41
|
+
via `_enclosing_type_chain`) + simple name, or just that chain for a
|
|
42
|
+
default-package file — mirrors how Java itself scopes a (possibly
|
|
43
|
+
nested) `@interface` declaration: `class Marks { @interface Getter
|
|
44
|
+
{} }` in package `app` canonically names as `app.Marks.Getter`,
|
|
45
|
+
never `app.Getter` (F-01 fix — the enclosing chain used to be
|
|
46
|
+
dropped, so a nested project annotation collapsed onto the same key
|
|
47
|
+
as an unrelated top-level declaration and lost its "local"
|
|
48
|
+
status). Keying by simple name alone (pre-F-02) let an unrelated
|
|
49
|
+
package's `@interface Getter` blind the Lombok-stub check for every
|
|
50
|
+
*other* package's `@Getter`/`@lombok.Getter` in the whole
|
|
51
|
+
project."""
|
|
52
|
+
names: Dict[str, Set[str]] = defaultdict(set)
|
|
53
|
+
for file_path, parse in parses.items():
|
|
54
|
+
ctx = self._file_ctx.get(str(file_path))
|
|
55
|
+
package = ctx.module if ctx is not None else DEFAULT_PACKAGE
|
|
56
|
+
stack = [parse.tree.root_node]
|
|
57
|
+
while stack:
|
|
58
|
+
node = stack.pop()
|
|
59
|
+
if node.type == "annotation_type_declaration":
|
|
60
|
+
name_node = node.child_by_field_name("name")
|
|
61
|
+
if name_node is not None:
|
|
62
|
+
simple = self._text(name_node, parse.source_bytes)
|
|
63
|
+
segments = self._enclosing_type_chain(node, parse.source_bytes)
|
|
64
|
+
segments.append(simple)
|
|
65
|
+
fqn = (
|
|
66
|
+
".".join(segments) if package == DEFAULT_PACKAGE
|
|
67
|
+
else f"{package}.{'.'.join(segments)}"
|
|
68
|
+
)
|
|
69
|
+
names[simple].add(fqn)
|
|
70
|
+
stack.extend(node.children)
|
|
71
|
+
return dict(names)
|
|
72
|
+
|
|
73
|
+
@staticmethod
|
|
74
|
+
def _enclosing_type_chain(node, source: bytes) -> List[str]:
|
|
75
|
+
"""Names of `node`'s enclosing class/interface/enum/record/
|
|
76
|
+
annotation-type ancestors, outermost first — mirrors
|
|
77
|
+
`JavaSymbolExtractor._scope_chain`'s type-only case (an annotation
|
|
78
|
+
type can only nest inside another type, never inside a method, so
|
|
79
|
+
no method/anonymous-class segment handling is needed here)."""
|
|
80
|
+
chain: List[str] = []
|
|
81
|
+
p = node.parent
|
|
82
|
+
while p is not None:
|
|
83
|
+
if p.type in _ENCLOSING_TYPE_CST_TYPES:
|
|
84
|
+
nn = p.child_by_field_name("name")
|
|
85
|
+
if nn is not None:
|
|
86
|
+
chain.append(_node_text(nn, source))
|
|
87
|
+
p = p.parent
|
|
88
|
+
chain.reverse()
|
|
89
|
+
return chain
|
|
90
|
+
|
|
91
|
+
# ------------------------------------------------------------------ P4.12 index builders
|
|
92
|
+
|
|
93
|
+
def _build_subtype_index(self) -> Dict[str, Set[str]]:
|
|
94
|
+
"""Reverse of INHERITS ∪ IMPLEMENTS: every CLASS id -> the set of all
|
|
95
|
+
transitive subtype CLASS ids. Built by walking each class's *forward*
|
|
96
|
+
ancestor chain (`_class_ancestors`, already BFS over both edge kinds)
|
|
97
|
+
and recording the class under each ancestor — so an interface maps to
|
|
98
|
+
every class that implements it directly *or* through an intermediate
|
|
99
|
+
abstract class (`StripeGateway extends AuditedGateway implements ...`),
|
|
100
|
+
which is exactly the implementor set the fan-out enumerates."""
|
|
101
|
+
subtypes: Dict[str, Set[str]] = defaultdict(set)
|
|
102
|
+
for nid, node in self.builder._nodes.items():
|
|
103
|
+
if node.node_type != NodeType.CLASS:
|
|
104
|
+
continue
|
|
105
|
+
for anc in self._class_ancestors(nid):
|
|
106
|
+
subtypes[anc].add(nid)
|
|
107
|
+
return subtypes
|
|
108
|
+
|
|
109
|
+
def _iter_method_cst(self, root, file_path_str: str, func_id_by_position):
|
|
110
|
+
"""Yield (method_declaration/constructor_declaration CST node,
|
|
111
|
+
node_id) for every method in the file.
|
|
112
|
+
|
|
113
|
+
Looks the id up by `(file_path, start_line, start_column)` against
|
|
114
|
+
the index built from `results` in `resolve_calls` rather than
|
|
115
|
+
re-deriving the `module:qualname` id from the CST — the id shape
|
|
116
|
+
(scope chain + P4.5 signature_key discriminator) is already computed
|
|
117
|
+
once by `JavaSymbolExtractor`/`CallGraphBuilder._symbol_to_id`;
|
|
118
|
+
duplicating that logic here would be a second, driftable source of
|
|
119
|
+
truth. The column is load-bearing, not decorative: two methods
|
|
120
|
+
routinely share one line (`void a() {} void b() {}`), and a
|
|
121
|
+
line-only key would silently drop one of them from this walk —
|
|
122
|
+
no call-site resolution, no DI indexing, no warning (F-02).
|
|
123
|
+
"""
|
|
124
|
+
# Six index-building passes plus the call-site pass each walk every
|
|
125
|
+
# file's whole CST through here; the (node, id) list is identical on
|
|
126
|
+
# every pass of one resolution run (same tree object, same position
|
|
127
|
+
# index), so build it once per file and replay it. `_method_cst_cache`
|
|
128
|
+
# is reset at the top of `_build_resolution_indexes`, so an
|
|
129
|
+
# incremental rebuild with a re-parsed file never sees a stale tree.
|
|
130
|
+
# tree-sitter's Python binding hands out a fresh `Node` wrapper on
|
|
131
|
+
# every `tree.root_node` access (`is` never matches); `Node.id` is
|
|
132
|
+
# the underlying node's identity within its tree, so key on that.
|
|
133
|
+
cache = self._method_cst_cache
|
|
134
|
+
cached = cache.get(file_path_str) if cache is not None else None
|
|
135
|
+
if cached is not None and cached[0] == root.id:
|
|
136
|
+
yield from cached[1]
|
|
137
|
+
return
|
|
138
|
+
found = []
|
|
139
|
+
for node in self._cst_index(root, file_path_str)["__methods__"]:
|
|
140
|
+
nid = func_id_by_position.get(
|
|
141
|
+
(file_path_str, node.start_point[0] + 1, node.start_point[1]),
|
|
142
|
+
)
|
|
143
|
+
if nid:
|
|
144
|
+
found.append((node, nid))
|
|
145
|
+
if cache is not None:
|
|
146
|
+
cache[file_path_str] = (root.id, found)
|
|
147
|
+
yield from found
|
|
148
|
+
|
|
149
|
+
def _cst_index(self, root, file_path_str: str) -> Dict[str, list]:
|
|
150
|
+
"""One stack-DFS walk of `root` per file per resolution run, shared by
|
|
151
|
+
every index-building pass that used to walk the whole tree itself.
|
|
152
|
+
`"__methods__"`: every `_METHOD_CST_TYPES` node, in the exact order the
|
|
153
|
+
old per-pass walk yielded them. `"__fields__"`: `field_declaration`
|
|
154
|
+
nodes not nested inside another field's initializer — the old field
|
|
155
|
+
walks stopped descending at a field_declaration, so a field declared
|
|
156
|
+
by an anonymous class inside a field initializer was never visited,
|
|
157
|
+
and still isn't. `"__initializer_nodes__"` keeps every declaration
|
|
158
|
+
shape the initializer resolver needs; unlike `__fields__`, it does
|
|
159
|
+
include nested/anonymous-class fields, matching the old fresh walk.
|
|
160
|
+
Cached in `_cst_index_cache` keyed on `Node.id`."""
|
|
161
|
+
cache = self._cst_index_cache
|
|
162
|
+
cached = cache.get(file_path_str) if cache is not None else None
|
|
163
|
+
if cached is not None and cached[0] == root.id:
|
|
164
|
+
return cached[1]
|
|
165
|
+
methods: list = []
|
|
166
|
+
fields: list = []
|
|
167
|
+
initializer_nodes: list = []
|
|
168
|
+
stack = [(root, False)]
|
|
169
|
+
while stack:
|
|
170
|
+
node, inside_field = stack.pop()
|
|
171
|
+
node_type = node.type
|
|
172
|
+
if node_type in _METHOD_CST_TYPES:
|
|
173
|
+
methods.append(node)
|
|
174
|
+
elif node_type == "field_declaration":
|
|
175
|
+
if not inside_field:
|
|
176
|
+
fields.append(node)
|
|
177
|
+
initializer_nodes.append(node)
|
|
178
|
+
inside_field = True
|
|
179
|
+
elif node_type == "static_initializer":
|
|
180
|
+
initializer_nodes.append(node)
|
|
181
|
+
elif node_type == "block" and node.parent is not None \
|
|
182
|
+
and node.parent.type in _INIT_BLOCK_PARENTS:
|
|
183
|
+
initializer_nodes.append(node)
|
|
184
|
+
stack.extend((child, inside_field) for child in node.children)
|
|
185
|
+
index = {
|
|
186
|
+
"__methods__": methods,
|
|
187
|
+
"__fields__": fields,
|
|
188
|
+
"__initializer_nodes__": initializer_nodes,
|
|
189
|
+
}
|
|
190
|
+
if cache is not None:
|
|
191
|
+
cache[file_path_str] = (root.id, index)
|
|
192
|
+
return index
|
|
193
|
+
|
|
194
|
+
def _field_declarations(self, root, file_path_str: str) -> list:
|
|
195
|
+
"""Top-level `field_declaration` nodes of a file (see `_cst_index`)."""
|
|
196
|
+
return self._cst_index(root, file_path_str)["__fields__"]
|
|
197
|
+
|
|
198
|
+
def _initializer_declarations(self, root, file_path_str: str) -> list:
|
|
199
|
+
"""All initializer-owning CST nodes of a file (see `_cst_index`)."""
|
|
200
|
+
return self._cst_index(root, file_path_str)["__initializer_nodes__"]
|
|
201
|
+
|
|
202
|
+
def _method_extractor(self):
|
|
203
|
+
"""Lazily build (and cache) a `JavaSymbolExtractor` used only by
|
|
204
|
+
`_iter_func_cst` to derive Symbols the same way the real graph nodes
|
|
205
|
+
were built. `JavaSymbolExtractor._extract_method` only calls
|
|
206
|
+
`self.parser.node_text`/`node_position` (both `@staticmethod`s on
|
|
207
|
+
`LanguageParser`), so any real Java `LanguageParser` works here —
|
|
208
|
+
there is no per-file state to keep in sync."""
|
|
209
|
+
if self._method_extractor_cache is None:
|
|
210
|
+
from adl_drift.lang.java.extractor import JavaSymbolExtractor
|
|
211
|
+
from adl_drift.lang.java.parser import create_parser
|
|
212
|
+
self._method_extractor_cache = JavaSymbolExtractor(create_parser())
|
|
213
|
+
return self._method_extractor_cache
|
|
214
|
+
|
|
215
|
+
def _iter_func_cst(self, root, source: bytes, module_name: str):
|
|
216
|
+
"""Yield (method/constructor CST node, node_id) for every method in
|
|
217
|
+
the file — P4.6's cross-function taint index (`detectors/taint.py::
|
|
218
|
+
TaintAnalyzer._build_func_index`), mirroring `PythonCallResolver.
|
|
219
|
+
_iter_func_cst`'s contract.
|
|
220
|
+
|
|
221
|
+
Unlike `_iter_method_cst` above (used by `resolve_calls`, which
|
|
222
|
+
looks the id up in a position index built earlier in that same
|
|
223
|
+
pass), this is called standalone by the taint analyzer with only a
|
|
224
|
+
CST root — so it derives each method's `Symbol` itself via
|
|
225
|
+
`JavaSymbolExtractor._extract_method` (the exact function that built
|
|
226
|
+
the real graph nodes) and turns it into an id via
|
|
227
|
+
`CallGraphBuilder._symbol_to_id`. Using the same two functions the
|
|
228
|
+
graph itself used guarantees the taint `function_id` always matches
|
|
229
|
+
the real FUNCTION node id — not a second, independently-derived
|
|
230
|
+
scheme (scope_chain + P4.5 signature_key) that could silently drift
|
|
231
|
+
out of sync with the extractor's own.
|
|
232
|
+
"""
|
|
233
|
+
from adl_drift.lang.java.extractor import _METHOD_TYPES
|
|
234
|
+
|
|
235
|
+
extractor = self._method_extractor()
|
|
236
|
+
anon_index = extractor._index_anonymous_classes(root)
|
|
237
|
+
stack = [root]
|
|
238
|
+
while stack:
|
|
239
|
+
node = stack.pop()
|
|
240
|
+
if node.type in _METHOD_TYPES:
|
|
241
|
+
sym = extractor._extract_method(node, source, Path("."), anon_index)
|
|
242
|
+
if sym is not None:
|
|
243
|
+
nid = self.builder._symbol_to_id(sym, module_name)
|
|
244
|
+
yield node, nid
|
|
245
|
+
stack.extend(node.children)
|
|
246
|
+
|
|
247
|
+
def _method_facts(self, body) -> MethodFacts:
|
|
248
|
+
"""Return one body walk's reusable call/reference CST facts.
|
|
249
|
+
|
|
250
|
+
The cache is deliberately per-resolution-run and keys on ``Node.id``:
|
|
251
|
+
tree-sitter re-wraps a CST node on access, but the underlying id is
|
|
252
|
+
stable for a parsed tree. Facts are tuples so no consumer can mutate
|
|
253
|
+
the shared view; all semantic resolution remains in its existing
|
|
254
|
+
passes. This preserves the old nested-class/method exclusion exactly.
|
|
255
|
+
"""
|
|
256
|
+
cache = self._method_facts_cache
|
|
257
|
+
if cache is not None:
|
|
258
|
+
hit = cache.get(body.id)
|
|
259
|
+
if hit is not None:
|
|
260
|
+
return hit
|
|
261
|
+
call_sites: List[Tuple[object, str]] = []
|
|
262
|
+
method_references: List[object] = []
|
|
263
|
+
assignment_expressions: List[object] = []
|
|
264
|
+
local_scope_nodes: List[object] = []
|
|
265
|
+
lambda_expressions: List[object] = []
|
|
266
|
+
stack = [body]
|
|
267
|
+
while stack:
|
|
268
|
+
node = stack.pop()
|
|
269
|
+
if node.type == "method_invocation":
|
|
270
|
+
call_sites.append((node, "method"))
|
|
271
|
+
elif node.type == "object_creation_expression":
|
|
272
|
+
call_sites.append((node, "object_creation"))
|
|
273
|
+
elif node.type == "explicit_constructor_invocation":
|
|
274
|
+
call_sites.append((node, "explicit_constructor"))
|
|
275
|
+
elif node.type == "method_reference":
|
|
276
|
+
call_sites.append((node, "method_reference"))
|
|
277
|
+
method_references.append(node)
|
|
278
|
+
if node.type == "assignment_expression":
|
|
279
|
+
assignment_expressions.append(node)
|
|
280
|
+
elif node.type in {
|
|
281
|
+
"local_variable_declaration", "enhanced_for_statement",
|
|
282
|
+
"resource", "catch_formal_parameter", "instanceof_expression",
|
|
283
|
+
}:
|
|
284
|
+
local_scope_nodes.append(node)
|
|
285
|
+
elif node.type == "lambda_expression":
|
|
286
|
+
lambda_expressions.append(node)
|
|
287
|
+
if node.type in _NESTED_SCOPE_BOUNDARY:
|
|
288
|
+
continue
|
|
289
|
+
stack.extend(node.children)
|
|
290
|
+
facts = MethodFacts(
|
|
291
|
+
tuple(call_sites), tuple(method_references),
|
|
292
|
+
tuple(assignment_expressions), tuple(local_scope_nodes),
|
|
293
|
+
tuple(lambda_expressions),
|
|
294
|
+
)
|
|
295
|
+
if cache is not None:
|
|
296
|
+
cache[body.id] = facts
|
|
297
|
+
return facts
|
|
298
|
+
|
|
299
|
+
def _collect_call_sites(self, body):
|
|
300
|
+
"""Call-site facts for ``body`` from its shared ``MethodFacts`` walk."""
|
|
301
|
+
return self._method_facts(body).call_sites
|
|
302
|
+
|
|
303
|
+
def _collect_method_references(self, body) -> Tuple[object, ...]:
|
|
304
|
+
"""Method-reference facts for ``body`` from its shared CST walk."""
|
|
305
|
+
return self._method_facts(body).method_references
|