adl-toolkit 0.0.0-stage → 0.1.1
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/drift-antigravity.md +19 -0
- package/adl_drift/harness/skills/drift-generic.md +19 -0
- package/adl_drift/harness/skills/drift.md +25 -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,540 @@
|
|
|
1
|
+
"""Abstract dispatch fan-out and Spring dependency-injection narrowing."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from typing import Dict, List, Optional, Set, Tuple, Union
|
|
7
|
+
|
|
8
|
+
from adl_drift.graph.node import NodeType
|
|
9
|
+
from adl_drift.lang.java.ids import DEFAULT_PACKAGE
|
|
10
|
+
from adl_drift.lang.java.resolution.constants import (
|
|
11
|
+
JAVA_COMPONENT_ANNOTATIONS,
|
|
12
|
+
JAVA_DI_ANNOTATION_FQNS,
|
|
13
|
+
JAVA_INJECTION_ANNOTATIONS,
|
|
14
|
+
_ANNOTATION_STRING_ARG,
|
|
15
|
+
_CONCRETE_TYPE_KINDS,
|
|
16
|
+
_CONF_DI_SINGLE_COMPONENT,
|
|
17
|
+
_CONF_MULTI_IMPLEMENTOR,
|
|
18
|
+
_CONF_SINGLE_IMPLEMENTOR,
|
|
19
|
+
_CONF_SPRING_PRIMARY,
|
|
20
|
+
_CONF_SPRING_QUALIFIER,
|
|
21
|
+
_MOCK_ALLOCATION_TYPE,
|
|
22
|
+
)
|
|
23
|
+
from adl_drift.lang.java.resolution.model import (
|
|
24
|
+
BeanCandidate as _BeanCandidate,
|
|
25
|
+
DINarrowResult as _DINarrowResult,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
class DispatchMixin:
|
|
29
|
+
def _is_concrete_class(self, class_id: str) -> bool:
|
|
30
|
+
"""True iff `class_id` is a concrete type able to carry a runtime
|
|
31
|
+
method body — a `class`/`record`/`enum` that is not `abstract`. An
|
|
32
|
+
interface or `abstract class` is the *source* of dispatch ambiguity,
|
|
33
|
+
never a fan-out target."""
|
|
34
|
+
node = self.builder._nodes.get(class_id)
|
|
35
|
+
if node is None or node.node_type != NodeType.CLASS:
|
|
36
|
+
return False
|
|
37
|
+
kind = node.metadata.get("java_type_kind")
|
|
38
|
+
if kind not in _CONCRETE_TYPE_KINDS:
|
|
39
|
+
return False
|
|
40
|
+
return not bool(node.metadata.get("java_is_abstract", False))
|
|
41
|
+
|
|
42
|
+
def _is_abstract_method(self, func_id: str) -> bool:
|
|
43
|
+
node = self.builder._nodes.get(func_id)
|
|
44
|
+
return bool(node is not None and node.metadata.get("java_is_abstract", False))
|
|
45
|
+
|
|
46
|
+
def _is_abstract_target(self, target: Union[str, List[str]]) -> bool:
|
|
47
|
+
"""True iff the P4.4-resolved target is (entirely) an abstract method —
|
|
48
|
+
the signal that a concrete runtime implementation is elsewhere. A list
|
|
49
|
+
target (an unresolved interface-level overload tie) counts only when
|
|
50
|
+
*every* candidate is abstract, so a mixed tie with a concrete default
|
|
51
|
+
keeps its direct edges."""
|
|
52
|
+
ids = target if isinstance(target, list) else [target]
|
|
53
|
+
return bool(ids) and all(self._is_abstract_method(t) for t in ids)
|
|
54
|
+
|
|
55
|
+
def _abstract_fan_out(
|
|
56
|
+
self, recv_class_id: str, resolved_target: Union[str, List[str]],
|
|
57
|
+
method_name: str, arg_count: int, arg_types: Optional[List[str]],
|
|
58
|
+
caller_ctx: "_ImportContext", obj_node, source: bytes,
|
|
59
|
+
enclosing_class_id: Optional[str], own_param_types: Dict[str, str],
|
|
60
|
+
local_types: "_LocalScopes",
|
|
61
|
+
) -> Optional[Tuple[Union[str, List[str]], float, Dict[str, object]]]:
|
|
62
|
+
"""The abstract-receiver branch (deep-dive §6.3). Returns a
|
|
63
|
+
`(target(s), confidence, metadata)` triple that *replaces* the P4.4
|
|
64
|
+
interface-method edge, or None to keep it (strict mode, a concrete
|
|
65
|
+
target, or zero concrete implementors — precision-first: never flood).
|
|
66
|
+
|
|
67
|
+
Order of evidence, strongest first: Spring DI narrowing off the
|
|
68
|
+
receiver field's injection annotations, then the raw implementor
|
|
69
|
+
fan-out (single implementor -> high; many -> low). The precision↔recall
|
|
70
|
+
dial (`self._fanout_mode`) governs the raw fan-out: `strict` keeps only
|
|
71
|
+
the interface edge, `balanced` caps emission above `self._fanout_cap`,
|
|
72
|
+
`recall` emits every implementor."""
|
|
73
|
+
if not self._is_abstract_target(resolved_target):
|
|
74
|
+
return None
|
|
75
|
+
if self._fanout_mode == "strict":
|
|
76
|
+
return None
|
|
77
|
+
|
|
78
|
+
# 1. Exact local allocation. Keep the declared receiver type for
|
|
79
|
+
# static resolution; use Interface x = new Impl() only to narrow the
|
|
80
|
+
# runtime expansion. Evidence expires at the first reassignment.
|
|
81
|
+
if obj_node.type == "identifier":
|
|
82
|
+
local_name = self._text(obj_node, source)
|
|
83
|
+
allocation_type = local_types.allocation_type_at(
|
|
84
|
+
local_name, obj_node.start_byte,
|
|
85
|
+
)
|
|
86
|
+
if allocation_type == _MOCK_ALLOCATION_TYPE:
|
|
87
|
+
# A Mockito mock/spy has no real declared implementation to
|
|
88
|
+
# dispatch to — keep the honest abstract-declaration edge
|
|
89
|
+
# rather than guessing an unrelated project class that
|
|
90
|
+
# happens to implement the same interface (P5 finding #2).
|
|
91
|
+
return None
|
|
92
|
+
if allocation_type is not None:
|
|
93
|
+
allocation_class_id = self._resolve_declared_type(
|
|
94
|
+
allocation_type, caller_ctx.module, caller_ctx.import_map,
|
|
95
|
+
caller_ctx.wildcard_packages, enclosing_class_id,
|
|
96
|
+
)
|
|
97
|
+
if (
|
|
98
|
+
allocation_class_id is not None
|
|
99
|
+
and self._is_concrete_class(allocation_class_id)
|
|
100
|
+
and (
|
|
101
|
+
allocation_class_id == recv_class_id
|
|
102
|
+
or recv_class_id in self._class_ancestors(allocation_class_id)
|
|
103
|
+
)
|
|
104
|
+
):
|
|
105
|
+
allocation_target, _ = self._resolve_own_or_ancestor(
|
|
106
|
+
allocation_class_id, method_name, arg_count, arg_types,
|
|
107
|
+
_CONF_SINGLE_IMPLEMENTOR, _CONF_SINGLE_IMPLEMENTOR,
|
|
108
|
+
caller_ctx,
|
|
109
|
+
)
|
|
110
|
+
if (
|
|
111
|
+
isinstance(allocation_target, str)
|
|
112
|
+
and not self._is_abstract_method(allocation_target)
|
|
113
|
+
):
|
|
114
|
+
recv_node = self.builder._nodes.get(recv_class_id)
|
|
115
|
+
declared = (
|
|
116
|
+
recv_node.name if recv_node is not None else recv_class_id
|
|
117
|
+
)
|
|
118
|
+
return allocation_target, _CONF_SINGLE_IMPLEMENTOR, {
|
|
119
|
+
"declared_receiver_type": declared,
|
|
120
|
+
"strategy": "single_implementor",
|
|
121
|
+
"allocation_narrowed": True,
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
# 2. Unanimous own-class field allocation. This never consults
|
|
125
|
+
# sibling subclasses, so an inherited polymorphic test fixture keeps
|
|
126
|
+
# its full runtime expansion.
|
|
127
|
+
field_name: Optional[str] = None
|
|
128
|
+
if enclosing_class_id is not None:
|
|
129
|
+
if obj_node.type == "identifier":
|
|
130
|
+
candidate = self._text(obj_node, source)
|
|
131
|
+
if (
|
|
132
|
+
local_types.type_at(candidate, obj_node.start_byte) is None
|
|
133
|
+
and candidate not in own_param_types
|
|
134
|
+
and candidate in self._all_field_types(enclosing_class_id)
|
|
135
|
+
):
|
|
136
|
+
field_name = candidate
|
|
137
|
+
elif obj_node.type == "field_access":
|
|
138
|
+
inner = obj_node.child_by_field_name("object")
|
|
139
|
+
field = obj_node.child_by_field_name("field")
|
|
140
|
+
if inner is not None and inner.type == "this" and field is not None:
|
|
141
|
+
field_name = self._text(field, source)
|
|
142
|
+
field_allocation_id = (
|
|
143
|
+
self._field_allocation_types.get(enclosing_class_id, {}).get(field_name)
|
|
144
|
+
if enclosing_class_id is not None and field_name is not None else None
|
|
145
|
+
)
|
|
146
|
+
if field_allocation_id == _MOCK_ALLOCATION_TYPE:
|
|
147
|
+
# Same fact as step 1, for a field-declared mock/spy receiver
|
|
148
|
+
# (`ConfigurationParameters configParams = mock();`).
|
|
149
|
+
return None
|
|
150
|
+
if (
|
|
151
|
+
field_allocation_id is not None
|
|
152
|
+
and self._is_concrete_class(field_allocation_id)
|
|
153
|
+
and (
|
|
154
|
+
field_allocation_id == recv_class_id
|
|
155
|
+
or recv_class_id in self._class_ancestors(field_allocation_id)
|
|
156
|
+
)
|
|
157
|
+
):
|
|
158
|
+
field_target, _ = self._resolve_own_or_ancestor(
|
|
159
|
+
field_allocation_id, method_name, arg_count, arg_types,
|
|
160
|
+
_CONF_SINGLE_IMPLEMENTOR, _CONF_SINGLE_IMPLEMENTOR, caller_ctx,
|
|
161
|
+
)
|
|
162
|
+
if isinstance(field_target, str) and not self._is_abstract_method(field_target):
|
|
163
|
+
recv_node = self.builder._nodes.get(recv_class_id)
|
|
164
|
+
declared = recv_node.name if recv_node is not None else recv_class_id
|
|
165
|
+
return field_target, _CONF_SINGLE_IMPLEMENTOR, {
|
|
166
|
+
"declared_receiver_type": declared,
|
|
167
|
+
"strategy": "single_implementor",
|
|
168
|
+
"field_allocation_narrowed": True,
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
# 3. Spring DI narrowing (only meaningful for an injected field).
|
|
172
|
+
field_node_id = self._receiver_field_node_id(
|
|
173
|
+
obj_node, source, enclosing_class_id, local_types, own_param_types,
|
|
174
|
+
)
|
|
175
|
+
if field_node_id is not None and self._is_injected_field(field_node_id):
|
|
176
|
+
di = self._di_narrow(
|
|
177
|
+
recv_class_id, field_node_id, method_name, arg_count, arg_types, caller_ctx,
|
|
178
|
+
)
|
|
179
|
+
if di is not None:
|
|
180
|
+
if di.target is None:
|
|
181
|
+
return None
|
|
182
|
+
recv_node = self.builder._nodes.get(recv_class_id)
|
|
183
|
+
declared = recv_node.name if recv_node is not None else recv_class_id
|
|
184
|
+
return di.target, di.confidence, {
|
|
185
|
+
"declared_receiver_type": declared, "strategy": di.strategy,
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
# 4. Raw implementor fan-out.
|
|
189
|
+
return self._raw_implementor_fan_out(
|
|
190
|
+
recv_class_id, method_name, arg_count, arg_types, caller_ctx,
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
def _raw_implementor_fan_out(
|
|
194
|
+
self, recv_class_id: str, method_name: str, arg_count: int,
|
|
195
|
+
arg_types: Optional[List[str]], caller_ctx: "_ImportContext",
|
|
196
|
+
) -> Optional[Tuple[Union[str, List[str]], float, Dict[str, object]]]:
|
|
197
|
+
"""The mode-gated raw implementor fan-out, shared by the typed-receiver
|
|
198
|
+
branch (via `_abstract_fan_out`, after DI narrowing) and the `this`/bare
|
|
199
|
+
+ `super` branches (which have no injectable field, so they call this
|
|
200
|
+
directly). Resolves `method_name` on every concrete subtype of
|
|
201
|
+
`recv_class_id` and returns `(target(s), confidence, metadata)`, or None
|
|
202
|
+
to keep the honest abstract-declaration edge: `strict` mode, zero
|
|
203
|
+
concrete implementors, or — in `balanced` mode — more implementors than
|
|
204
|
+
`self._fanout_cap` (precision-first: never flood blast radius). Metadata
|
|
205
|
+
carries `declared_receiver_type` + `strategy` so impact/MCP can explain
|
|
206
|
+
the fanned-out edges (deep-dive §8.4). Callers must have already
|
|
207
|
+
established the resolved target is abstract (`_is_abstract_target`)."""
|
|
208
|
+
if self._fanout_mode == "strict":
|
|
209
|
+
return None
|
|
210
|
+
recv_node = self.builder._nodes.get(recv_class_id)
|
|
211
|
+
declared = recv_node.name if recv_node is not None else recv_class_id
|
|
212
|
+
base_meta: Dict[str, object] = {"declared_receiver_type": declared}
|
|
213
|
+
targets = self._fan_out_implementors(
|
|
214
|
+
recv_class_id, method_name, arg_count, arg_types, caller_ctx,
|
|
215
|
+
)
|
|
216
|
+
if not targets:
|
|
217
|
+
return None # interface-only: keep the honest declaration edge.
|
|
218
|
+
telemetry = getattr(self, "fanout_telemetry", None)
|
|
219
|
+
if isinstance(telemetry, dict):
|
|
220
|
+
telemetry.setdefault("candidate_counts", []).append(len(targets))
|
|
221
|
+
if len(targets) == 1:
|
|
222
|
+
if isinstance(telemetry, dict):
|
|
223
|
+
telemetry.setdefault("emitted_counts", []).append(1)
|
|
224
|
+
return targets[0], _CONF_SINGLE_IMPLEMENTOR, {
|
|
225
|
+
**base_meta, "strategy": "single_implementor",
|
|
226
|
+
}
|
|
227
|
+
if self._fanout_mode == "balanced" and len(targets) > self._fanout_cap:
|
|
228
|
+
# Precision-first: too many implementors to name honestly — keep
|
|
229
|
+
# the single interface edge rather than flooding blast radius.
|
|
230
|
+
if isinstance(telemetry, dict):
|
|
231
|
+
telemetry["cap_skipped"] = int(telemetry.get("cap_skipped", 0)) + 1
|
|
232
|
+
telemetry.setdefault("emitted_counts", []).append(0)
|
|
233
|
+
return None
|
|
234
|
+
if isinstance(telemetry, dict):
|
|
235
|
+
telemetry.setdefault("emitted_counts", []).append(len(targets))
|
|
236
|
+
return targets, _CONF_MULTI_IMPLEMENTOR, {
|
|
237
|
+
**base_meta, "strategy": "interface_fanout",
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
def _fan_out_implementors(
|
|
241
|
+
self, root_class_id: str, method_name: str, arg_count: int,
|
|
242
|
+
arg_types: Optional[List[str]], caller_ctx: "_ImportContext",
|
|
243
|
+
) -> List[str]:
|
|
244
|
+
"""Ordered, de-duplicated concrete method targets for the fan-out: for
|
|
245
|
+
every concrete transitive subtype of `root_class_id` (interface or
|
|
246
|
+
abstract class), resolve `method_name` on that subtype the normal P4.4
|
|
247
|
+
way and collect the concrete override it lands on. A subtype that does
|
|
248
|
+
not resolve the method (or resolves it only to another abstract
|
|
249
|
+
declaration) contributes nothing — never a guess."""
|
|
250
|
+
targets: List[str] = []
|
|
251
|
+
seen: Set[str] = set()
|
|
252
|
+
for impl in sorted(self._subtypes.get(root_class_id, ())):
|
|
253
|
+
if not self._is_concrete_class(impl):
|
|
254
|
+
continue
|
|
255
|
+
mt, _ = self._resolve_own_or_ancestor(
|
|
256
|
+
impl, method_name, arg_count, arg_types,
|
|
257
|
+
_CONF_SINGLE_IMPLEMENTOR, _CONF_SINGLE_IMPLEMENTOR, caller_ctx,
|
|
258
|
+
)
|
|
259
|
+
for tid in (mt if isinstance(mt, list) else [mt] if mt else []):
|
|
260
|
+
if tid in seen or self._is_abstract_method(tid):
|
|
261
|
+
continue
|
|
262
|
+
seen.add(tid)
|
|
263
|
+
targets.append(tid)
|
|
264
|
+
return targets
|
|
265
|
+
|
|
266
|
+
# -- DI narrowing (Spring) ------------------------------------------------
|
|
267
|
+
|
|
268
|
+
def _receiver_field_node_id(
|
|
269
|
+
self, obj_node, source: bytes, enclosing_class_id: Optional[str],
|
|
270
|
+
local_types: "_LocalScopes", param_types: Dict[str, str],
|
|
271
|
+
) -> Optional[str]:
|
|
272
|
+
"""The VARIABLE node id of the field a receiver reads, when the receiver
|
|
273
|
+
is genuinely a field of the enclosing class — a bare `field` identifier
|
|
274
|
+
that is neither a local nor a parameter, or an explicit `this.field`.
|
|
275
|
+
Anything else (a local, a parameter, a static-class receiver, a chained
|
|
276
|
+
expression) is not an injectable field and returns None."""
|
|
277
|
+
if enclosing_class_id is None:
|
|
278
|
+
return None
|
|
279
|
+
field_name: Optional[str] = None
|
|
280
|
+
if obj_node.type == "identifier":
|
|
281
|
+
name = self._text(obj_node, source)
|
|
282
|
+
if local_types.type_at(name, obj_node.start_byte) is not None:
|
|
283
|
+
return None # a local, not a field
|
|
284
|
+
if name in param_types:
|
|
285
|
+
return None # a parameter, not a field
|
|
286
|
+
field_name = name
|
|
287
|
+
elif obj_node.type == "field_access":
|
|
288
|
+
inner = obj_node.child_by_field_name("object")
|
|
289
|
+
field_node = obj_node.child_by_field_name("field")
|
|
290
|
+
if inner is not None and inner.type == "this" and field_node is not None:
|
|
291
|
+
field_name = self._text(field_node, source)
|
|
292
|
+
if field_name is None:
|
|
293
|
+
return None
|
|
294
|
+
return self._all_field_node_ids(enclosing_class_id).get(field_name)
|
|
295
|
+
|
|
296
|
+
def _all_field_node_ids(self, class_id: str) -> Dict[str, str]:
|
|
297
|
+
"""`{field_name: field VARIABLE node id}` for `class_id` merged with
|
|
298
|
+
every ancestor's fields, nearer declarations winning — the node-id
|
|
299
|
+
analogue of `_all_field_types` (an injected field may be declared on a
|
|
300
|
+
base class)."""
|
|
301
|
+
merged: Dict[str, str] = {}
|
|
302
|
+
for anc in reversed(self._class_ancestors(class_id)):
|
|
303
|
+
merged.update(self._field_nodes.get(anc, {}))
|
|
304
|
+
merged.update(self._field_nodes.get(class_id, {}))
|
|
305
|
+
return merged
|
|
306
|
+
|
|
307
|
+
def _is_injected_field(self, field_node_id: str) -> bool:
|
|
308
|
+
node = self.builder._nodes.get(field_node_id)
|
|
309
|
+
if node is None:
|
|
310
|
+
return False
|
|
311
|
+
return field_node_id in self._di_injected_fields or any(
|
|
312
|
+
self._di_annotation_simple_name(d, node.file_path) in JAVA_INJECTION_ANNOTATIONS
|
|
313
|
+
for d in node.decorators
|
|
314
|
+
)
|
|
315
|
+
|
|
316
|
+
def _field_qualifier(self, field_node_id: str) -> Optional[str]:
|
|
317
|
+
"""The `@Qualifier("name")` (or `@Resource(name="…")`-style single
|
|
318
|
+
string arg) requested on an injected field, or None."""
|
|
319
|
+
node = self.builder._nodes.get(field_node_id)
|
|
320
|
+
if node is None:
|
|
321
|
+
return None
|
|
322
|
+
for deco in node.decorators:
|
|
323
|
+
simple = self._di_annotation_simple_name(deco, node.file_path)
|
|
324
|
+
if simple == "Resource":
|
|
325
|
+
named = self._annotation_named_string_arg(deco, "name")
|
|
326
|
+
if named:
|
|
327
|
+
return named
|
|
328
|
+
return node.name
|
|
329
|
+
direct = self._qualifier_from_decorators(node.decorators, node.file_path)
|
|
330
|
+
if direct:
|
|
331
|
+
return direct
|
|
332
|
+
injected = self._di_injected_fields.get(field_node_id)
|
|
333
|
+
if injected is not None:
|
|
334
|
+
return injected.qualifier
|
|
335
|
+
return None
|
|
336
|
+
|
|
337
|
+
def _qualifier_from_decorators(
|
|
338
|
+
self, decorators: List[str], file_path: str,
|
|
339
|
+
) -> Optional[str]:
|
|
340
|
+
for deco in decorators:
|
|
341
|
+
simple = self._di_annotation_simple_name(deco, file_path)
|
|
342
|
+
if simple == "Qualifier":
|
|
343
|
+
arg = self._annotation_string_arg(deco)
|
|
344
|
+
if arg:
|
|
345
|
+
return arg
|
|
346
|
+
elif simple == "Resource":
|
|
347
|
+
arg = self._annotation_named_string_arg(deco, "name")
|
|
348
|
+
if arg:
|
|
349
|
+
return arg
|
|
350
|
+
return None
|
|
351
|
+
|
|
352
|
+
def _di_narrow(
|
|
353
|
+
self, interface_id: str, field_node_id: str, method_name: str,
|
|
354
|
+
arg_count: int, arg_types: Optional[List[str]], caller_ctx: "_ImportContext",
|
|
355
|
+
) -> Optional[_DINarrowResult]:
|
|
356
|
+
"""Narrow an injected interface field to one concrete Spring bean
|
|
357
|
+
(deep-dive §7.3). Returns a target when the evidence names exactly one
|
|
358
|
+
bean — a matching `@Qualifier` (0.95), a sole `@Component`-family
|
|
359
|
+
implementor (0.85), or a `@Primary` among several (0.90). An explicit
|
|
360
|
+
but unresolved/ambiguous qualifier returns a blocked result so callers
|
|
361
|
+
do not fall back to unrelated DI rules."""
|
|
362
|
+
candidates = self._bean_candidates(interface_id)
|
|
363
|
+
if not candidates:
|
|
364
|
+
return None
|
|
365
|
+
qualifier = self._field_qualifier(field_node_id)
|
|
366
|
+
if qualifier is not None:
|
|
367
|
+
matches = [
|
|
368
|
+
c for c in candidates
|
|
369
|
+
if qualifier in c.qualifiers or c.bean_name == qualifier
|
|
370
|
+
]
|
|
371
|
+
if len(matches) == 1:
|
|
372
|
+
tgt = self._resolve_bean_method(
|
|
373
|
+
matches[0], method_name, arg_count, arg_types, caller_ctx,
|
|
374
|
+
)
|
|
375
|
+
if tgt is not None:
|
|
376
|
+
return _DINarrowResult(
|
|
377
|
+
tgt, _CONF_SPRING_QUALIFIER, "spring_qualifier",
|
|
378
|
+
)
|
|
379
|
+
if len(matches) == 0:
|
|
380
|
+
return _DINarrowResult(None, 0.0, "spring_qualifier_unresolved")
|
|
381
|
+
return _DINarrowResult(None, 0.0, "spring_qualifier_ambiguous")
|
|
382
|
+
if len(candidates) == 1:
|
|
383
|
+
tgt = self._resolve_bean_method(
|
|
384
|
+
candidates[0], method_name, arg_count, arg_types, caller_ctx,
|
|
385
|
+
)
|
|
386
|
+
if tgt is not None:
|
|
387
|
+
return _DINarrowResult(
|
|
388
|
+
tgt, _CONF_DI_SINGLE_COMPONENT, "di_single_component",
|
|
389
|
+
)
|
|
390
|
+
primaries = [c for c in candidates if c.primary]
|
|
391
|
+
if len(primaries) == 1:
|
|
392
|
+
tgt = self._resolve_bean_method(
|
|
393
|
+
primaries[0], method_name, arg_count, arg_types, caller_ctx,
|
|
394
|
+
)
|
|
395
|
+
if tgt is not None:
|
|
396
|
+
return _DINarrowResult(tgt, _CONF_SPRING_PRIMARY, "spring_primary")
|
|
397
|
+
return None
|
|
398
|
+
|
|
399
|
+
def _resolve_bean_method(
|
|
400
|
+
self, bean: "_BeanCandidate", method_name: str, arg_count: int,
|
|
401
|
+
arg_types: Optional[List[str]], caller_ctx: "_ImportContext",
|
|
402
|
+
) -> Optional[str]:
|
|
403
|
+
"""Resolve `method_name` on a bean's concrete type to a single method
|
|
404
|
+
node id (None if it does not resolve to exactly one concrete method)."""
|
|
405
|
+
mt, _ = self._resolve_own_or_ancestor(
|
|
406
|
+
bean.type_id, method_name, arg_count, arg_types,
|
|
407
|
+
_CONF_SINGLE_IMPLEMENTOR, _CONF_SINGLE_IMPLEMENTOR, caller_ctx,
|
|
408
|
+
)
|
|
409
|
+
if isinstance(mt, str) and not self._is_abstract_method(mt):
|
|
410
|
+
return mt
|
|
411
|
+
return None
|
|
412
|
+
|
|
413
|
+
def _bean_candidates(self, interface_id: str) -> List["_BeanCandidate"]:
|
|
414
|
+
"""Every concrete Spring bean that can satisfy `interface_id`: its
|
|
415
|
+
`@Component`-family concrete implementors, plus any `@Bean` factory
|
|
416
|
+
method producing one of its implementors."""
|
|
417
|
+
subtypes = self._subtypes.get(interface_id, set())
|
|
418
|
+
out: List[_BeanCandidate] = []
|
|
419
|
+
for impl in sorted(subtypes):
|
|
420
|
+
if not self._is_concrete_class(impl):
|
|
421
|
+
continue
|
|
422
|
+
info = self._component_info(impl)
|
|
423
|
+
if info is not None:
|
|
424
|
+
out.append(info)
|
|
425
|
+
for bm in self._bean_method_candidates:
|
|
426
|
+
if bm.type_id in subtypes:
|
|
427
|
+
out.append(bm)
|
|
428
|
+
return out
|
|
429
|
+
|
|
430
|
+
def _component_info(self, class_id: str) -> Optional["_BeanCandidate"]:
|
|
431
|
+
"""A `_BeanCandidate` for a class iff it carries a `@Component`-family
|
|
432
|
+
annotation, else None. Bean name comes from the annotation's string arg
|
|
433
|
+
(`@Service("fast")`) or the decapitalized simple class name; qualifiers
|
|
434
|
+
gather any `@Qualifier` plus the bean name; `@Primary` sets the flag."""
|
|
435
|
+
node = self.builder._nodes.get(class_id)
|
|
436
|
+
if node is None or not node.decorators:
|
|
437
|
+
return None
|
|
438
|
+
names = {self._di_annotation_simple_name(d, node.file_path): d for d in node.decorators}
|
|
439
|
+
if not (JAVA_COMPONENT_ANNOTATIONS & set(names)):
|
|
440
|
+
return None
|
|
441
|
+
bean_name, qualifiers, primary = self._bean_metadata_from_decorators(
|
|
442
|
+
node.decorators, node.name, node.file_path,
|
|
443
|
+
)
|
|
444
|
+
return _BeanCandidate(class_id, bean_name, qualifiers, primary, source="component")
|
|
445
|
+
|
|
446
|
+
def _bean_metadata_from_decorators(
|
|
447
|
+
self, decorators: List[str], type_or_method_name: str, file_path: str,
|
|
448
|
+
) -> Tuple[Optional[str], FrozenSet[str], bool]:
|
|
449
|
+
"""`(bean_name, qualifiers, primary)` distilled from a class's/`@Bean`
|
|
450
|
+
method's annotation list. The explicit bean name is the string arg of a
|
|
451
|
+
component/`@Bean` annotation if given, else the decapitalized name;
|
|
452
|
+
qualifiers include any `@Qualifier` value *and* the bean name itself (a
|
|
453
|
+
field `@Qualifier("stripeGateway")` matches a bean named after its
|
|
454
|
+
class); `@Primary` presence sets the flag."""
|
|
455
|
+
primary = False
|
|
456
|
+
qualifiers: Set[str] = set()
|
|
457
|
+
explicit_name: Optional[str] = None
|
|
458
|
+
for deco in decorators:
|
|
459
|
+
simple = self._di_annotation_simple_name(deco, file_path)
|
|
460
|
+
if simple == "Primary":
|
|
461
|
+
primary = True
|
|
462
|
+
elif simple == "Qualifier":
|
|
463
|
+
arg = self._annotation_string_arg(deco)
|
|
464
|
+
if arg:
|
|
465
|
+
qualifiers.add(arg)
|
|
466
|
+
elif simple in JAVA_COMPONENT_ANNOTATIONS or simple == "Bean":
|
|
467
|
+
arg = self._annotation_string_arg(deco)
|
|
468
|
+
if arg:
|
|
469
|
+
explicit_name = arg
|
|
470
|
+
bean_name = explicit_name or self._decapitalize(type_or_method_name)
|
|
471
|
+
if bean_name:
|
|
472
|
+
qualifiers.add(bean_name)
|
|
473
|
+
return bean_name, frozenset(qualifiers), primary
|
|
474
|
+
|
|
475
|
+
@staticmethod
|
|
476
|
+
def _decapitalize(name: str) -> str:
|
|
477
|
+
"""Spring's default bean name from a class name: the simple name with
|
|
478
|
+
its first letter lowercased (`StripeGateway` -> `stripeGateway`)."""
|
|
479
|
+
return name[:1].lower() + name[1:] if name else name
|
|
480
|
+
|
|
481
|
+
def _di_annotation_simple_name(self, deco: str, file_path: str) -> str:
|
|
482
|
+
"""Simple annotation name from decorator text (`@lombok.Getter` ->
|
|
483
|
+
`Getter`, `@Qualifier("x")` -> `Qualifier`, reusing the builder's own
|
|
484
|
+
dotted-name + argument-stripping parser) iff it is eligible as
|
|
485
|
+
Spring/Jakarta DI evidence — "" when it instead resolves to a
|
|
486
|
+
project-local `@interface` declaration shadowing that well-known name
|
|
487
|
+
(F-02 fix, `_is_local_annotation`), the DI-evidence analogue of the
|
|
488
|
+
Lombok-stub guard `_decorator_is_local_annotation` already applies. A
|
|
489
|
+
project's own `@interface Service {}` (or `Autowired`/`Qualifier`/
|
|
490
|
+
`Primary`/`Bean`/`Component`/...) must never be read as Spring's —
|
|
491
|
+
every call site that treats a decorator as DI/component evidence
|
|
492
|
+
(constructor- and setter-injection indexing, field/param injection
|
|
493
|
+
checks, `@Qualifier`/`@Primary` narrowing, `@Component`-family
|
|
494
|
+
detection, `@Bean` factory candidates) goes through this rather than
|
|
495
|
+
a bare simple-name comparison for that reason."""
|
|
496
|
+
dname = self.builder._decorator_name(deco)
|
|
497
|
+
if not dname:
|
|
498
|
+
return ""
|
|
499
|
+
simple = dname.split(".")[-1]
|
|
500
|
+
if self._is_local_annotation(dname, simple, file_path):
|
|
501
|
+
return ""
|
|
502
|
+
fqn = self._resolved_di_annotation_fqn(dname, simple, file_path)
|
|
503
|
+
if fqn not in JAVA_DI_ANNOTATION_FQNS:
|
|
504
|
+
return ""
|
|
505
|
+
return simple
|
|
506
|
+
|
|
507
|
+
def _resolved_di_annotation_fqn(
|
|
508
|
+
self, dname: str, simple: str, file_path: str,
|
|
509
|
+
) -> Optional[str]:
|
|
510
|
+
if "." in dname:
|
|
511
|
+
return dname
|
|
512
|
+
ctx = self._file_ctx.get(file_path)
|
|
513
|
+
if ctx is None:
|
|
514
|
+
return None
|
|
515
|
+
imported = ctx.import_map.get(simple)
|
|
516
|
+
if imported is not None:
|
|
517
|
+
return imported
|
|
518
|
+
own_fqn = simple if ctx.module == DEFAULT_PACKAGE else f"{ctx.module}.{simple}"
|
|
519
|
+
if own_fqn in JAVA_DI_ANNOTATION_FQNS:
|
|
520
|
+
return own_fqn
|
|
521
|
+
for pkg in ctx.wildcard_packages:
|
|
522
|
+
fqn = f"{pkg}.{simple}"
|
|
523
|
+
if fqn in JAVA_DI_ANNOTATION_FQNS:
|
|
524
|
+
return fqn
|
|
525
|
+
return None
|
|
526
|
+
|
|
527
|
+
@staticmethod
|
|
528
|
+
def _annotation_string_arg(deco: str) -> Optional[str]:
|
|
529
|
+
"""The first string-literal argument of an annotation, or None
|
|
530
|
+
(`@Qualifier("stripe")` -> `stripe`)."""
|
|
531
|
+
m = _ANNOTATION_STRING_ARG.search(deco)
|
|
532
|
+
return m.group(1) if m is not None else None
|
|
533
|
+
|
|
534
|
+
@staticmethod
|
|
535
|
+
def _annotation_named_string_arg(deco: str, arg_name: str) -> Optional[str]:
|
|
536
|
+
"""A named string-literal annotation argument, or None
|
|
537
|
+
(`@Resource(name = "stripe")` -> `stripe`)."""
|
|
538
|
+
pattern = re.compile(rf"\b{re.escape(arg_name)}\s*=\s*\"((?:[^\"\\]|\\.)*)\"")
|
|
539
|
+
m = pattern.search(deco)
|
|
540
|
+
return m.group(1) if m is not None else None
|