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,1401 @@
|
|
|
1
|
+
"""Java call-resolution strategy (P4.4, PROBLEMS.md).
|
|
2
|
+
|
|
3
|
+
Much smaller than Python's `lang/python/resolver.py`: Java declares every
|
|
4
|
+
receiver type, so most Python type-*inference* machinery has no Java
|
|
5
|
+
counterpart — a receiver's class is read straight off the declaration
|
|
6
|
+
(`formal_parameter`/`local_variable_declaration`/`field_declaration` type
|
|
7
|
+
text), while fluent calls propagate source methods' declared return types.
|
|
8
|
+
What Java adds instead is **method overloading**: a class may hold
|
|
9
|
+
several `Symbol`s sharing one name, distinguished only by P4.5's
|
|
10
|
+
`signature_key` id discriminator, so a call site must pick *which* one it
|
|
11
|
+
means. Abstract-type dispatch (interface/DI resolution — "given a declared
|
|
12
|
+
interface type, which concrete implementation runs") is the genuinely hard
|
|
13
|
+
Java problem, deliberately punted to P4.12 — this resolver only walks
|
|
14
|
+
INHERITS/IMPLEMENTS for declared types and their ancestors, it never fans
|
|
15
|
+
out across sibling implementors of an interface.
|
|
16
|
+
|
|
17
|
+
Owns:
|
|
18
|
+
- `JAVA_BUILTINS` — the P2.7-equivalent deny-list: `java.lang.Object`
|
|
19
|
+
methods + common `Collection`/`Map`/`Iterator` method names, replacing
|
|
20
|
+
`PYTHON_BUILTINS`.
|
|
21
|
+
- `JAVA_ANNOTATION_STUB_TABLE` — Lombok-style annotation simple name ->
|
|
22
|
+
name-*prefixes* it generates on the annotated class (`@Getter` -> `get`/
|
|
23
|
+
`is`, `@Data` -> `get`/`is`/`set`/`toString`/`hashCode`/`equals`/
|
|
24
|
+
`canEqual`, ...). Prefixes, not exact names, because Lombok's generated
|
|
25
|
+
method names are field-dependent (`getName()`) — unlike Python's
|
|
26
|
+
`DECORATOR_STUB_TABLE`, which lists the exact fixed attribute names a
|
|
27
|
+
decorator wrapper adds (`delay`, `cache_clear`, ...). Spring AOP/DI
|
|
28
|
+
dispatch is explicitly **not** here — that is P4.12.
|
|
29
|
+
- `JavaCallResolver` — `this`/`super` receivers, typed-receiver direct
|
|
30
|
+
dispatch (local var / parameter / field, with recursive `field_access`
|
|
31
|
+
chains so `this.userService.findById(id)` unwraps through the field's
|
|
32
|
+
declared type per codegraph-java-deep-dive.md §5.3), overload
|
|
33
|
+
disambiguation by arg count then statically-known arg types, generic
|
|
34
|
+
erasure via `lang.java.ids.erased_type()`, and the annotation-stub
|
|
35
|
+
fallback.
|
|
36
|
+
|
|
37
|
+
Grammar-agnostic graph-id resolution (`_resolve_name`, `_contains_child`,
|
|
38
|
+
edge dedupe, `_symbol_to_id`) stays on `CallGraphBuilder`, reused unchanged
|
|
39
|
+
from the Python resolver.
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
from __future__ import annotations
|
|
43
|
+
|
|
44
|
+
import os
|
|
45
|
+
import re
|
|
46
|
+
from collections import defaultdict, deque
|
|
47
|
+
from pathlib import Path
|
|
48
|
+
from typing import (
|
|
49
|
+
TYPE_CHECKING, Callable, Dict, FrozenSet, List, Optional, Set, Tuple, Union,
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
from adl_drift.graph.edges import EdgeType, GraphEdge
|
|
53
|
+
from adl_drift.graph.node import GraphNode, NodeType
|
|
54
|
+
from adl_drift.lang.base import CallResolver, ParseResult, ResolverConfig
|
|
55
|
+
from adl_drift.lang.java.ids import DEFAULT_PACKAGE, erased_type
|
|
56
|
+
from adl_drift.lang.java.resolution.generated import GeneratedMembersMixin
|
|
57
|
+
from adl_drift.lang.java.resolution.functional import FunctionalResolutionMixin
|
|
58
|
+
from adl_drift.lang.java.resolution.scopes import ScopeInferenceMixin
|
|
59
|
+
from adl_drift.lang.java.resolution.types import TypeResolutionMixin
|
|
60
|
+
from adl_drift.lang.java.resolution.overloads import OverloadResolutionMixin
|
|
61
|
+
from adl_drift.lang.java.resolution.constructors import ConstructorResolutionMixin
|
|
62
|
+
from adl_drift.lang.java.resolution.context import ContextResolutionMixin
|
|
63
|
+
from adl_drift.lang.java.resolution.orchestration import ResolutionOrchestrationMixin
|
|
64
|
+
from adl_drift.lang.java.resolution.dispatch import DispatchMixin
|
|
65
|
+
from adl_drift.lang.java.resolution.model import (
|
|
66
|
+
BeanCandidate as _BeanCandidate,
|
|
67
|
+
DINarrowResult as _DINarrowResult,
|
|
68
|
+
ImportContext as _ImportContext,
|
|
69
|
+
InjectedFieldMetadata as _InjectedFieldMetadata,
|
|
70
|
+
LocalScopes as _LocalScopes,
|
|
71
|
+
SAMDescriptor as _SAMDescriptor,
|
|
72
|
+
)
|
|
73
|
+
from adl_drift.utils.types import AnalysisResult
|
|
74
|
+
|
|
75
|
+
if TYPE_CHECKING:
|
|
76
|
+
from adl_drift.graph.builder import CallGraphBuilder
|
|
77
|
+
|
|
78
|
+
__all__ = [
|
|
79
|
+
"JAVA_BUILTINS",
|
|
80
|
+
"JAVA_ANNOTATION_STUB_TABLE",
|
|
81
|
+
"JavaCallResolver",
|
|
82
|
+
"default_java_resolver_config",
|
|
83
|
+
]
|
|
84
|
+
|
|
85
|
+
from adl_drift.lang.java.resolution.constants import (
|
|
86
|
+
JAVA_ANNOTATION_STUB_TABLE,
|
|
87
|
+
JAVA_BUILTINS,
|
|
88
|
+
JAVA_COMPONENT_ANNOTATIONS,
|
|
89
|
+
JAVA_DI_ANNOTATION_FQNS,
|
|
90
|
+
JAVA_INJECTION_ANNOTATIONS,
|
|
91
|
+
JAVA_OPTIONAL_ELEMENT_LAMBDA_METHODS,
|
|
92
|
+
JAVA_STREAM_ELEMENT_LAMBDA_METHODS,
|
|
93
|
+
_ANNOTATION_STRING_ARG,
|
|
94
|
+
_CONCRETE_TYPE_KINDS,
|
|
95
|
+
_CONF_DI_SINGLE_COMPONENT,
|
|
96
|
+
_CONF_MULTI_IMPLEMENTOR,
|
|
97
|
+
_CONF_SINGLE_IMPLEMENTOR,
|
|
98
|
+
_CONF_SPRING_PRIMARY,
|
|
99
|
+
_CONF_SPRING_QUALIFIER,
|
|
100
|
+
_DEFAULT_FANOUT_CAP,
|
|
101
|
+
_DEFAULT_FANOUT_MODE,
|
|
102
|
+
_ENCLOSING_TYPE_CST_TYPES,
|
|
103
|
+
_FANOUT_MODES,
|
|
104
|
+
_JDK_SAM_PARAMS,
|
|
105
|
+
_JDK_SAM_RETURNS,
|
|
106
|
+
_LAMBDA_ARG_TYPE,
|
|
107
|
+
_METHOD_CST_TYPES,
|
|
108
|
+
_NESTED_SCOPE_BOUNDARY,
|
|
109
|
+
_NULL_ARG_TYPE,
|
|
110
|
+
argument_nodes,
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def default_java_resolver_config() -> ResolverConfig:
|
|
115
|
+
"""The real Java `ResolverConfig` — returned by `JavaBackend.resolver_config()`."""
|
|
116
|
+
cfg = ResolverConfig(
|
|
117
|
+
receiver_self_names=("this",),
|
|
118
|
+
receiver_super_names=("super",),
|
|
119
|
+
builtins_deny_list=frozenset(JAVA_BUILTINS),
|
|
120
|
+
annotation_stub_table=dict(JAVA_ANNOTATION_STUB_TABLE),
|
|
121
|
+
)
|
|
122
|
+
cfg.resolver_factory = lambda builder, config: JavaCallResolver(builder, config)
|
|
123
|
+
return cfg
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
class JavaCallResolver(
|
|
127
|
+
ResolutionOrchestrationMixin, ContextResolutionMixin, ConstructorResolutionMixin, OverloadResolutionMixin, TypeResolutionMixin, GeneratedMembersMixin, FunctionalResolutionMixin, ScopeInferenceMixin,
|
|
128
|
+
DispatchMixin, CallResolver
|
|
129
|
+
):
|
|
130
|
+
"""Java's `CallResolver` (P4.4)."""
|
|
131
|
+
|
|
132
|
+
def __init__(self, builder: "CallGraphBuilder", config: ResolverConfig) -> None:
|
|
133
|
+
super().__init__(builder, config)
|
|
134
|
+
# Rebuilt at the start of every resolve_calls() — see there.
|
|
135
|
+
self._by_module_symbol: Dict[str, Dict[str, str]] = {}
|
|
136
|
+
self._field_types: Dict[str, Dict[str, str]] = {}
|
|
137
|
+
self._param_type_lists: Dict[str, List[str]] = {}
|
|
138
|
+
# function id -> the method's own generic type-parameter bounds.
|
|
139
|
+
# A formal `A` in `<A extends Appendable> A append(A value)` is an
|
|
140
|
+
# inference variable, not a project class literally named `A`.
|
|
141
|
+
self._method_type_param_bounds: Dict[str, Dict[str, str]] = {}
|
|
142
|
+
# Per-resolution-run caches, (re)set by `_build_resolution_indexes`;
|
|
143
|
+
# None/empty outside a run so helper calls before/after stay uncached.
|
|
144
|
+
self._method_cst_cache: Optional[Dict[str, tuple]] = None
|
|
145
|
+
self._cst_index_cache: Optional[Dict[str, tuple]] = None
|
|
146
|
+
self._method_facts_cache: Optional[Dict[int, object]] = None
|
|
147
|
+
self._local_scopes_cache: Optional[Dict[tuple, object]] = None
|
|
148
|
+
self._declaring_class_cache: Optional[Dict[str, Optional[str]]] = None
|
|
149
|
+
self._members_by_name_cache: Optional[Dict[str, Dict[str, List[str]]]] = None
|
|
150
|
+
self._declared_type_memo: Optional[Dict[tuple, Optional[str]]] = None
|
|
151
|
+
self._file_import_map_ids: frozenset = frozenset()
|
|
152
|
+
# Keyed by `GraphNode.file_path` (str), *not* module name: Java's
|
|
153
|
+
# module id is the declared `package` (see `module_name_from_parse`),
|
|
154
|
+
# so multiple files routinely share one module — caching by module
|
|
155
|
+
# would pick one arbitrary file's imports for all of them. Every
|
|
156
|
+
# `GraphNode` (including FUNCTION) carries its own `file_path`, so
|
|
157
|
+
# this stays precise down to the file that actually declared each
|
|
158
|
+
# candidate's parameter types.
|
|
159
|
+
self._file_ctx: Dict[str, _ImportContext] = {}
|
|
160
|
+
# {file_path: [fully-qualified class path, ...]} for each file's
|
|
161
|
+
# `import static a.b.C.*;` declarations — the classes a bare call in
|
|
162
|
+
# that file may resolve a static member against (see `_resolve_call`).
|
|
163
|
+
self._static_wildcard_by_file: Dict[str, List[str]] = {}
|
|
164
|
+
# Annotation types (`@interface Foo {}`) declared *in the analyzed
|
|
165
|
+
# source*, keyed by simple name -> the set of FQNs (declaring
|
|
166
|
+
# file's package + simple name) that simple name resolves to
|
|
167
|
+
# somewhere in the project. A real Lombok annotation is always
|
|
168
|
+
# external, so a locally-declared annotation sharing a stub-table
|
|
169
|
+
# name (`@interface Getter {}`) is proof *that specific FQN* is not
|
|
170
|
+
# Lombok — the annotation-stub fallback must skip it (see
|
|
171
|
+
# `_annotation_stub_match`). Keyed by FQN, not bare simple name
|
|
172
|
+
# (F-02 fix): a simple-name-only set can't tell a locally-declared
|
|
173
|
+
# `foo.Getter` from an unrelated `app.Getter` used as
|
|
174
|
+
# `@lombok.Getter` elsewhere, so *any* package declaring
|
|
175
|
+
# `@interface Getter` used to blind the stub fallback project-wide.
|
|
176
|
+
self._local_annotation_type_names: Dict[str, Set[str]] = {}
|
|
177
|
+
# P4.12 reverse hierarchy index: interface/abstract CLASS id -> every
|
|
178
|
+
# transitive subtype CLASS id (reverse of INHERITS ∪ IMPLEMENTS). The
|
|
179
|
+
# implementor fan-out walks this. Rebuilt each resolve_calls().
|
|
180
|
+
self._subtypes: Dict[str, Set[str]] = {}
|
|
181
|
+
# class id -> {field_name: field VARIABLE node id}, for reading a
|
|
182
|
+
# receiver field's injection annotations (@Autowired/@Qualifier) during
|
|
183
|
+
# DI narrowing. Own declarations only; `_all_field_node_ids` merges
|
|
184
|
+
# ancestors on demand.
|
|
185
|
+
self._field_nodes: Dict[str, Dict[str, str]] = {}
|
|
186
|
+
# Enclosing class -> field name -> unanimous concrete allocation.
|
|
187
|
+
self._field_allocation_types: Dict[str, Dict[str, str]] = {}
|
|
188
|
+
# class id -> declared generic type parameter names, e.g. Box<T> -> ["T"].
|
|
189
|
+
self._class_type_params: Dict[str, List[str]] = {}
|
|
190
|
+
# class id -> upper bounds for its declared type parameters. This is
|
|
191
|
+
# distinct from method inference bounds: a raw receiver inside the
|
|
192
|
+
# declaring generic class still has its declaration-site upper bound.
|
|
193
|
+
self._class_type_param_bounds: Dict[str, Dict[str, str]] = {}
|
|
194
|
+
# (class_id, ancestor_class_id) -> the raw type-argument texts used in
|
|
195
|
+
# that class's own `extends`/`implements` clause, e.g.
|
|
196
|
+
# `StripeBox extends Box<Stripe>` -> {(StripeBox, Box): ["Stripe"]}.
|
|
197
|
+
# Lets a generic method's return type resolve through inheritance
|
|
198
|
+
# (F-01), not just when the receiver's own declared type at the call
|
|
199
|
+
# site carries the argument directly.
|
|
200
|
+
self._ancestor_type_args: Dict[Tuple[str, str], List[str]] = {}
|
|
201
|
+
# `@Bean` factory-method candidates: a `@Bean T make()` in a config
|
|
202
|
+
# class registers `T` as an injectable bean even when `T` carries no
|
|
203
|
+
# `@Component` annotation of its own (deep-dive §7.3).
|
|
204
|
+
self._bean_method_candidates: List[_BeanCandidate] = []
|
|
205
|
+
# Fields injected via constructor param assignment or a Spring
|
|
206
|
+
# setter (`@Autowired void setX(T v) { this.x = v; }`) — merged from
|
|
207
|
+
# both indexes in `resolve_calls` since `_is_injected_field`/
|
|
208
|
+
# `_field_qualifier` don't care which classical DI style produced
|
|
209
|
+
# the evidence.
|
|
210
|
+
self._di_injected_fields: Dict[str, _InjectedFieldMetadata] = {}
|
|
211
|
+
# P4.13: callable targets stored in Map-like fields, keyed by
|
|
212
|
+
# declaring class then field name. Used by `handlers.get(k).run()`.
|
|
213
|
+
self._field_callable_targets: Dict[str, Dict[str, Set[str]]] = {}
|
|
214
|
+
# Precision↔recall dial, read once from the environment (see
|
|
215
|
+
# `_FANOUT_MODES`). `DRIFT_RECALL_MODE=1` (P2.21's own flag) forces
|
|
216
|
+
# recall; `DRIFT_JAVA_FANOUT_MODE` selects a mode explicitly and wins
|
|
217
|
+
# over it.
|
|
218
|
+
self._fanout_mode = self._read_fanout_mode()
|
|
219
|
+
self._fanout_cap = self._read_fanout_cap()
|
|
220
|
+
# P4 telemetry: keep decision-level data because a capped call site
|
|
221
|
+
# emits no fan-out edge and cannot be reconstructed from the graph.
|
|
222
|
+
self.fanout_telemetry: Dict[str, object] = {
|
|
223
|
+
"mode": self._fanout_mode, "cap": self._fanout_cap,
|
|
224
|
+
"candidate_counts": [], "emitted_counts": [], "cap_skipped": 0,
|
|
225
|
+
}
|
|
226
|
+
# P4.6: lazily built, cached `JavaSymbolExtractor` used only by
|
|
227
|
+
# `_iter_func_cst` (below) to derive taint function ids the same way
|
|
228
|
+
# the real graph nodes were built.
|
|
229
|
+
self._method_extractor_cache: Optional["object"] = None
|
|
230
|
+
# Exact source-declared hierarchy, resolved with the same qualified
|
|
231
|
+
# type rules as receivers. The shared graph relationship pass keeps
|
|
232
|
+
# a simple-name index for all languages; in a Java package containing
|
|
233
|
+
# many nested classes with the same simple name that index can attach
|
|
234
|
+
# an INHERITS edge to the wrong class. Resolver dispatch must retain
|
|
235
|
+
# the declaration's package/member qualification independently.
|
|
236
|
+
self._declared_direct_ancestors: Dict[Tuple[str, EdgeType], List[str]] = {}
|
|
237
|
+
# BFS-result cache for `_class_ancestors`, reset alongside
|
|
238
|
+
# `_declared_direct_ancestors` above (see orchestration.py's
|
|
239
|
+
# `_build_resolution_indexes`) — see `_class_ancestors`'s docstring.
|
|
240
|
+
self._class_ancestors_cache: Dict[str, List[str]] = {}
|
|
241
|
+
# Result cache for `_contains_children`, reset the same way — see
|
|
242
|
+
# its docstring.
|
|
243
|
+
self._contains_children_cache: Dict[str, List[str]] = {}
|
|
244
|
+
|
|
245
|
+
@staticmethod
|
|
246
|
+
def _read_fanout_mode() -> str:
|
|
247
|
+
explicit = os.environ.get("DRIFT_JAVA_FANOUT_MODE", "").strip().lower()
|
|
248
|
+
if explicit in _FANOUT_MODES:
|
|
249
|
+
return explicit
|
|
250
|
+
if os.environ.get("DRIFT_RECALL_MODE", "0") == "1":
|
|
251
|
+
return "recall"
|
|
252
|
+
return _DEFAULT_FANOUT_MODE
|
|
253
|
+
|
|
254
|
+
@staticmethod
|
|
255
|
+
def _read_fanout_cap() -> int:
|
|
256
|
+
raw = os.environ.get("DRIFT_JAVA_FANOUT_MAX", "").strip()
|
|
257
|
+
if raw.isdigit() and int(raw) > 0:
|
|
258
|
+
return int(raw)
|
|
259
|
+
return _DEFAULT_FANOUT_CAP
|
|
260
|
+
|
|
261
|
+
# ------------------------------------------------------------------ main pass
|
|
262
|
+
|
|
263
|
+
def resolve_calls(
|
|
264
|
+
self,
|
|
265
|
+
parses: Dict[Path, ParseResult],
|
|
266
|
+
results: Dict[Path, AnalysisResult],
|
|
267
|
+
) -> int:
|
|
268
|
+
"""Resolve Java calls through the stable orchestration pass."""
|
|
269
|
+
return self._run_resolution_passes(parses, results)
|
|
270
|
+
|
|
271
|
+
@staticmethod
|
|
272
|
+
def _text(node, source: bytes) -> str:
|
|
273
|
+
return source[node.start_byte:node.end_byte].decode("utf-8", errors="replace")
|
|
274
|
+
|
|
275
|
+
def _build_bean_method_candidates(self) -> List[_BeanCandidate]:
|
|
276
|
+
"""`@Bean`-annotated factory methods (deep-dive §7.3): a
|
|
277
|
+
`@Bean SomeImpl make() { ... }` in a `@Configuration` class registers
|
|
278
|
+
`SomeImpl` as an injectable bean even when `SomeImpl` itself carries no
|
|
279
|
+
`@Component` annotation. Resolves each such method's declared return
|
|
280
|
+
type (persisted as `java_return_type`) to a concrete CLASS node; the
|
|
281
|
+
method's own `@Qualifier`/`@Primary`/bean name feed the same narrowing
|
|
282
|
+
as a component class's would."""
|
|
283
|
+
out: List[_BeanCandidate] = []
|
|
284
|
+
for nid, node in self.builder._nodes.items():
|
|
285
|
+
if node.node_type != NodeType.FUNCTION or not node.decorators:
|
|
286
|
+
continue
|
|
287
|
+
names = {self._di_annotation_simple_name(d, node.file_path) for d in node.decorators}
|
|
288
|
+
if "Bean" not in names:
|
|
289
|
+
continue
|
|
290
|
+
return_type = node.metadata.get("java_return_type")
|
|
291
|
+
if not return_type:
|
|
292
|
+
continue
|
|
293
|
+
ctx = self._file_ctx.get(node.file_path)
|
|
294
|
+
if ctx is None:
|
|
295
|
+
continue
|
|
296
|
+
type_id = self._resolve_declared_type(
|
|
297
|
+
return_type, ctx.module, ctx.import_map, ctx.wildcard_packages,
|
|
298
|
+
)
|
|
299
|
+
if type_id is None or not self._is_concrete_class(type_id):
|
|
300
|
+
continue
|
|
301
|
+
bean_name, qualifiers, primary = self._bean_metadata_from_decorators(
|
|
302
|
+
node.decorators, node.name, node.file_path,
|
|
303
|
+
)
|
|
304
|
+
out.append(_BeanCandidate(
|
|
305
|
+
type_id, bean_name, qualifiers, primary, source="bean_method",
|
|
306
|
+
))
|
|
307
|
+
return out
|
|
308
|
+
|
|
309
|
+
def _build_constructor_injection_index(
|
|
310
|
+
self,
|
|
311
|
+
parses: Dict[Path, ParseResult],
|
|
312
|
+
results: Dict[Path, AnalysisResult],
|
|
313
|
+
func_id_by_position: Dict[Tuple[str, int, int], str],
|
|
314
|
+
) -> Dict[str, _InjectedFieldMetadata]:
|
|
315
|
+
"""Fields injected by constructors via direct `this.field = param`."""
|
|
316
|
+
injected: Dict[str, _InjectedFieldMetadata] = {}
|
|
317
|
+
for file_path, parse in parses.items():
|
|
318
|
+
result = results.get(file_path)
|
|
319
|
+
if result is None:
|
|
320
|
+
continue
|
|
321
|
+
fp_str = str(file_path)
|
|
322
|
+
source = parse.source_bytes
|
|
323
|
+
for func_cst, caller_id in self._iter_method_cst(
|
|
324
|
+
parse.tree.root_node, fp_str, func_id_by_position,
|
|
325
|
+
):
|
|
326
|
+
if func_cst.type not in ("constructor_declaration", "compact_constructor_declaration"):
|
|
327
|
+
continue
|
|
328
|
+
class_id = self._declaring_class_id(caller_id)
|
|
329
|
+
if class_id is None:
|
|
330
|
+
continue
|
|
331
|
+
field_nodes = self._field_nodes.get(class_id, {})
|
|
332
|
+
if not field_nodes:
|
|
333
|
+
continue
|
|
334
|
+
constructor_annotations = self._annotations_from_node(func_cst, source)
|
|
335
|
+
constructor_is_injected = any(
|
|
336
|
+
self._di_annotation_simple_name(d, fp_str) in JAVA_INJECTION_ANNOTATIONS
|
|
337
|
+
for d in constructor_annotations
|
|
338
|
+
)
|
|
339
|
+
params = self._param_injection_metadata(func_cst, source, fp_str)
|
|
340
|
+
if not constructor_is_injected and not any(meta.source for _, meta in params.values()):
|
|
341
|
+
continue
|
|
342
|
+
body = func_cst.child_by_field_name("body")
|
|
343
|
+
if body is None:
|
|
344
|
+
continue
|
|
345
|
+
for field_name, param_name in self._constructor_field_assignments(
|
|
346
|
+
body, source, set(params),
|
|
347
|
+
):
|
|
348
|
+
if param_name not in params or field_name not in field_nodes:
|
|
349
|
+
continue
|
|
350
|
+
param_type, metadata = params[param_name]
|
|
351
|
+
field_id = field_nodes[field_name]
|
|
352
|
+
field_node = self.builder._nodes.get(field_id)
|
|
353
|
+
if field_node is None or not field_node.signature:
|
|
354
|
+
continue
|
|
355
|
+
if not self._declared_types_match(field_node.signature, param_type):
|
|
356
|
+
continue
|
|
357
|
+
if constructor_is_injected or metadata.source:
|
|
358
|
+
injected[field_id] = metadata
|
|
359
|
+
return injected
|
|
360
|
+
|
|
361
|
+
def _build_setter_injection_index(
|
|
362
|
+
self,
|
|
363
|
+
parses: Dict[Path, ParseResult],
|
|
364
|
+
results: Dict[Path, AnalysisResult],
|
|
365
|
+
func_id_by_position: Dict[Tuple[str, int, int], str],
|
|
366
|
+
) -> Dict[str, _InjectedFieldMetadata]:
|
|
367
|
+
"""Fields injected via a Spring setter — `@Autowired void setX(T v)
|
|
368
|
+
{ this.x = v; }` — the third classical injection style alongside
|
|
369
|
+
field- and constructor-injection above. Only single-parameter
|
|
370
|
+
`set<Name>` methods are considered: that is Spring's own
|
|
371
|
+
setter-injection convention, and narrow enough that an unrelated
|
|
372
|
+
single-arg method never gets mistaken for bean wiring."""
|
|
373
|
+
injected: Dict[str, _InjectedFieldMetadata] = {}
|
|
374
|
+
for file_path, parse in parses.items():
|
|
375
|
+
result = results.get(file_path)
|
|
376
|
+
if result is None:
|
|
377
|
+
continue
|
|
378
|
+
fp_str = str(file_path)
|
|
379
|
+
source = parse.source_bytes
|
|
380
|
+
for func_cst, caller_id in self._iter_method_cst(
|
|
381
|
+
parse.tree.root_node, fp_str, func_id_by_position,
|
|
382
|
+
):
|
|
383
|
+
if func_cst.type != "method_declaration":
|
|
384
|
+
continue
|
|
385
|
+
name_node = func_cst.child_by_field_name("name")
|
|
386
|
+
if name_node is None:
|
|
387
|
+
continue
|
|
388
|
+
method_name = self._text(name_node, source)
|
|
389
|
+
if not method_name.startswith("set") or len(method_name) <= len("set"):
|
|
390
|
+
continue
|
|
391
|
+
class_id = self._declaring_class_id(caller_id)
|
|
392
|
+
if class_id is None:
|
|
393
|
+
continue
|
|
394
|
+
field_nodes = self._field_nodes.get(class_id, {})
|
|
395
|
+
if not field_nodes:
|
|
396
|
+
continue
|
|
397
|
+
params = self._param_injection_metadata(func_cst, source, fp_str)
|
|
398
|
+
if len(params) != 1:
|
|
399
|
+
continue # not JavaBeans setter shape - a single (T v) param
|
|
400
|
+
method_annotations = self._annotations_from_node(func_cst, source)
|
|
401
|
+
method_is_injected = any(
|
|
402
|
+
self._di_annotation_simple_name(d, fp_str) in JAVA_INJECTION_ANNOTATIONS
|
|
403
|
+
for d in method_annotations
|
|
404
|
+
)
|
|
405
|
+
# Unlike a constructor (whose parameters are each qualified
|
|
406
|
+
# individually), a setter has exactly one parameter, so Spring
|
|
407
|
+
# allows `@Qualifier` on either the method or the parameter —
|
|
408
|
+
# both mean the same thing here. The parameter's own qualifier
|
|
409
|
+
# (if any) wins on conflict, matching `_field_qualifier`'s own
|
|
410
|
+
# "direct decorator first" precedence for a plain field.
|
|
411
|
+
method_qualifier = self._qualifier_from_decorators(method_annotations, fp_str)
|
|
412
|
+
if not method_is_injected and not method_qualifier and not any(
|
|
413
|
+
meta.source for _, meta in params.values()
|
|
414
|
+
):
|
|
415
|
+
continue
|
|
416
|
+
body = func_cst.child_by_field_name("body")
|
|
417
|
+
if body is None:
|
|
418
|
+
continue
|
|
419
|
+
for field_name, param_name in self._constructor_field_assignments(
|
|
420
|
+
body, source, set(params),
|
|
421
|
+
):
|
|
422
|
+
if param_name not in params or field_name not in field_nodes:
|
|
423
|
+
continue
|
|
424
|
+
param_type, metadata = params[param_name]
|
|
425
|
+
field_id = field_nodes[field_name]
|
|
426
|
+
field_node = self.builder._nodes.get(field_id)
|
|
427
|
+
if field_node is None or not field_node.signature:
|
|
428
|
+
continue
|
|
429
|
+
if not self._declared_types_match(field_node.signature, param_type):
|
|
430
|
+
continue
|
|
431
|
+
if metadata.qualifier is None and method_qualifier is not None:
|
|
432
|
+
metadata = _InjectedFieldMetadata(
|
|
433
|
+
qualifier=method_qualifier, source="method_qualifier",
|
|
434
|
+
)
|
|
435
|
+
if method_is_injected or metadata.source:
|
|
436
|
+
injected[field_id] = metadata
|
|
437
|
+
return injected
|
|
438
|
+
|
|
439
|
+
def _param_injection_metadata(
|
|
440
|
+
self, method_cst, source: bytes, file_path: str,
|
|
441
|
+
) -> Dict[str, Tuple[str, _InjectedFieldMetadata]]:
|
|
442
|
+
"""`{param_name: (declared_type, metadata)}` for every formal
|
|
443
|
+
parameter of a constructor or setter, `metadata.source` set iff that
|
|
444
|
+
parameter itself carries `@Qualifier`/`@Resource(name=...)` or a
|
|
445
|
+
bare injection annotation (shared by `_build_constructor_injection_index`
|
|
446
|
+
and `_build_setter_injection_index` — the evidence a parameter
|
|
447
|
+
carries doesn't depend on which of the two enclosing method shapes
|
|
448
|
+
it is read from)."""
|
|
449
|
+
params_node = method_cst.child_by_field_name("parameters")
|
|
450
|
+
if params_node is None:
|
|
451
|
+
return {}
|
|
452
|
+
out: Dict[str, Tuple[str, _InjectedFieldMetadata]] = {}
|
|
453
|
+
for child in params_node.children:
|
|
454
|
+
if child.type != "formal_parameter":
|
|
455
|
+
continue
|
|
456
|
+
type_node = child.child_by_field_name("type")
|
|
457
|
+
name_node = child.child_by_field_name("name")
|
|
458
|
+
if type_node is None or name_node is None:
|
|
459
|
+
continue
|
|
460
|
+
decorators = self._annotations_from_node(child, source)
|
|
461
|
+
qualifier = self._qualifier_from_decorators(decorators, file_path)
|
|
462
|
+
source_name = ""
|
|
463
|
+
if qualifier is not None:
|
|
464
|
+
source_name = "param_qualifier"
|
|
465
|
+
elif any(
|
|
466
|
+
self._di_annotation_simple_name(d, file_path) in JAVA_INJECTION_ANNOTATIONS
|
|
467
|
+
for d in decorators
|
|
468
|
+
):
|
|
469
|
+
source_name = "param_injection"
|
|
470
|
+
out[self._text(name_node, source)] = (
|
|
471
|
+
self._text(type_node, source),
|
|
472
|
+
_InjectedFieldMetadata(qualifier=qualifier, source=source_name),
|
|
473
|
+
)
|
|
474
|
+
return out
|
|
475
|
+
|
|
476
|
+
def _constructor_field_assignments(
|
|
477
|
+
self, body, source: bytes, param_names: Set[str],
|
|
478
|
+
) -> List[Tuple[str, str]]:
|
|
479
|
+
"""`(field_name, rhs_identifier)` pairs for every direct field
|
|
480
|
+
assignment in a constructor or setter body, whether written
|
|
481
|
+
`this.field = x` or as the bare `field = x` idiom used whenever the
|
|
482
|
+
field and parameter names differ (no shadowing, so no `this.` is
|
|
483
|
+
needed). A bare LHS identifier only counts as a field when it is
|
|
484
|
+
neither a local variable in scope at that point nor one of the
|
|
485
|
+
method's own parameters — either would shadow the field, so an
|
|
486
|
+
unqualified reference there resolves to the local/parameter, not the
|
|
487
|
+
field (same disambiguation `_receiver_field_node_id` does for read
|
|
488
|
+
sites)."""
|
|
489
|
+
assignments: List[Tuple[str, str]] = []
|
|
490
|
+
local_types = self._collect_local_scopes(body, source)
|
|
491
|
+
stack = [body]
|
|
492
|
+
while stack:
|
|
493
|
+
node = stack.pop()
|
|
494
|
+
if node.type in _NESTED_SCOPE_BOUNDARY:
|
|
495
|
+
continue
|
|
496
|
+
if node.type == "assignment_expression":
|
|
497
|
+
children = [c for c in node.children if not c.is_extra]
|
|
498
|
+
if len(children) >= 3 and children[1].type == "=":
|
|
499
|
+
lhs = children[0]
|
|
500
|
+
field_name = self._this_field_name(lhs, source)
|
|
501
|
+
if field_name is None and lhs.type == "identifier":
|
|
502
|
+
name = self._text(lhs, source)
|
|
503
|
+
if (
|
|
504
|
+
name not in param_names
|
|
505
|
+
and local_types.type_at(name, lhs.start_byte) is None
|
|
506
|
+
):
|
|
507
|
+
field_name = name
|
|
508
|
+
if field_name is not None and children[2].type == "identifier":
|
|
509
|
+
assignments.append((field_name, self._text(children[2], source)))
|
|
510
|
+
stack.extend(node.children)
|
|
511
|
+
return assignments
|
|
512
|
+
|
|
513
|
+
def _this_field_name(self, node, source: bytes) -> Optional[str]:
|
|
514
|
+
if node.type != "field_access":
|
|
515
|
+
return None
|
|
516
|
+
inner = node.child_by_field_name("object")
|
|
517
|
+
field_node = node.child_by_field_name("field")
|
|
518
|
+
if inner is not None and inner.type == "this" and field_node is not None:
|
|
519
|
+
return self._text(field_node, source)
|
|
520
|
+
return None
|
|
521
|
+
|
|
522
|
+
def _annotations_from_node(self, node, source: bytes) -> List[str]:
|
|
523
|
+
modifiers = next((c for c in node.children if c.type == "modifiers"), None)
|
|
524
|
+
if modifiers is None:
|
|
525
|
+
return []
|
|
526
|
+
return [
|
|
527
|
+
self._text(child, source).strip()
|
|
528
|
+
for child in modifiers.children
|
|
529
|
+
if child.type in ("annotation", "marker_annotation")
|
|
530
|
+
]
|
|
531
|
+
|
|
532
|
+
@staticmethod
|
|
533
|
+
def _declared_types_match(left: str, right: str) -> bool:
|
|
534
|
+
return erased_type(left).strip() == erased_type(right).strip()
|
|
535
|
+
|
|
536
|
+
@staticmethod
|
|
537
|
+
def _type_param_bounds_from_node(tp_node, source: bytes) -> Dict[str, str]:
|
|
538
|
+
"""`{type_param_name: upper_bound_type_text}` for one `type_parameters`
|
|
539
|
+
CST node — the grammar shape is identical whether it hangs off a
|
|
540
|
+
method or a class/interface/record declaration, so this is shared by
|
|
541
|
+
both `_collect_type_param_bounds` call sites. Only the first
|
|
542
|
+
(leftmost) bound is kept — an intersection bound `T extends A & B`
|
|
543
|
+
has no single erased receiver type this resolver can pick, so it takes
|
|
544
|
+
`A`, the same first-alternative best-effort `_collect_local_scopes`
|
|
545
|
+
uses for a multi-catch. A parameter with no bound (`<T>`) contributes
|
|
546
|
+
nothing (its receiver stays unresolved, never guessed)."""
|
|
547
|
+
bounds: Dict[str, str] = {}
|
|
548
|
+
for child in tp_node.children:
|
|
549
|
+
if child.type != "type_parameter":
|
|
550
|
+
continue
|
|
551
|
+
name_node = next(
|
|
552
|
+
(c for c in child.children if c.type == "type_identifier"), None
|
|
553
|
+
)
|
|
554
|
+
if name_node is None:
|
|
555
|
+
continue
|
|
556
|
+
bound_node = next(
|
|
557
|
+
(c for c in child.children if c.type == "type_bound"), None
|
|
558
|
+
)
|
|
559
|
+
if bound_node is None:
|
|
560
|
+
continue
|
|
561
|
+
first_bound = next((c for c in bound_node.children if c.is_named), None)
|
|
562
|
+
if first_bound is not None:
|
|
563
|
+
bounds[JavaCallResolver._text(name_node, source)] = (
|
|
564
|
+
JavaCallResolver._text(first_bound, source)
|
|
565
|
+
)
|
|
566
|
+
return bounds
|
|
567
|
+
|
|
568
|
+
def _collect_type_param_bounds(self, func_cst, source: bytes) -> Dict[str, str]:
|
|
569
|
+
"""`{type_param_name: upper_bound_type_text}` for a method's own
|
|
570
|
+
`<T extends Bound>` type parameters *plus* every enclosing
|
|
571
|
+
class/interface/record's own `<T extends Bound>` (P4.12 generic
|
|
572
|
+
narrowing, F-04 fix) — `class Client<T extends Stripe> { void
|
|
573
|
+
run(T g) { g.charge(); } }` binds `T` from the class, not `run`,
|
|
574
|
+
which has no type parameters of its own.
|
|
575
|
+
|
|
576
|
+
Merge order is outermost-enclosing-type -> innermost-enclosing-type
|
|
577
|
+
-> method, each later layer overriding an earlier one for the same
|
|
578
|
+
name — mirrors real Java scoping, where a method's own `<T extends
|
|
579
|
+
...>` (or an inner nested class's) shadows an outer class's `T` of
|
|
580
|
+
the same name rather than merging with it."""
|
|
581
|
+
bounds: Dict[str, str] = {}
|
|
582
|
+
enclosing_types: List = []
|
|
583
|
+
cur = func_cst.parent
|
|
584
|
+
while cur is not None:
|
|
585
|
+
if cur.type in _ENCLOSING_TYPE_CST_TYPES:
|
|
586
|
+
enclosing_types.append(cur)
|
|
587
|
+
cur = cur.parent
|
|
588
|
+
for type_node in reversed(enclosing_types):
|
|
589
|
+
tp_node = type_node.child_by_field_name("type_parameters")
|
|
590
|
+
if tp_node is not None:
|
|
591
|
+
bounds.update(self._type_param_bounds_from_node(tp_node, source))
|
|
592
|
+
tp_node = func_cst.child_by_field_name("type_parameters")
|
|
593
|
+
if tp_node is not None:
|
|
594
|
+
bounds.update(self._type_param_bounds_from_node(tp_node, source))
|
|
595
|
+
return bounds
|
|
596
|
+
|
|
597
|
+
def _collect_method_type_param_bounds(
|
|
598
|
+
self,
|
|
599
|
+
parses: Dict[Path, ParseResult],
|
|
600
|
+
func_id_by_position: Dict[Tuple[str, int, int], str],
|
|
601
|
+
) -> Dict[str, Dict[str, str]]:
|
|
602
|
+
"""Type parameters declared by each source method.
|
|
603
|
+
|
|
604
|
+
Class type parameters are deliberately excluded: their binding comes
|
|
605
|
+
from the receiver type. Method parameters are inferred independently
|
|
606
|
+
at every invocation and must not be resolved as ordinary class names.
|
|
607
|
+
"""
|
|
608
|
+
out: Dict[str, Dict[str, str]] = {}
|
|
609
|
+
for file_path, parse in parses.items():
|
|
610
|
+
for method_node, func_id in self._iter_method_cst(
|
|
611
|
+
parse.tree.root_node, str(file_path), func_id_by_position,
|
|
612
|
+
):
|
|
613
|
+
tp_node = method_node.child_by_field_name("type_parameters")
|
|
614
|
+
if tp_node is None:
|
|
615
|
+
continue
|
|
616
|
+
names = self._type_param_names_from_node(tp_node, parse.source_bytes)
|
|
617
|
+
if not names:
|
|
618
|
+
continue
|
|
619
|
+
bounds = self._type_param_bounds_from_node(tp_node, parse.source_bytes)
|
|
620
|
+
out[func_id] = {name: bounds.get(name, "Object") for name in names}
|
|
621
|
+
return out
|
|
622
|
+
|
|
623
|
+
def _collect_class_type_params(
|
|
624
|
+
self, parses: Dict[Path, ParseResult], class_id_by_position: Dict[Tuple[str, int, int], str],
|
|
625
|
+
) -> Dict[str, List[str]]:
|
|
626
|
+
"""`{class_id: [type parameter names]}` for source-declared generic
|
|
627
|
+
types. The resolver uses this to substitute a receiver's actual type
|
|
628
|
+
arguments into declared method return types (`Box<T>.value() -> T`)."""
|
|
629
|
+
out: Dict[str, List[str]] = {}
|
|
630
|
+
for file_path, parse in parses.items():
|
|
631
|
+
fp_str = str(file_path)
|
|
632
|
+
stack = [parse.tree.root_node]
|
|
633
|
+
while stack:
|
|
634
|
+
node = stack.pop()
|
|
635
|
+
if node.type in _ENCLOSING_TYPE_CST_TYPES:
|
|
636
|
+
class_id = class_id_by_position.get(
|
|
637
|
+
(fp_str, node.start_point[0] + 1, node.start_point[1]),
|
|
638
|
+
)
|
|
639
|
+
if class_id is not None:
|
|
640
|
+
tp_node = node.child_by_field_name("type_parameters")
|
|
641
|
+
if tp_node is not None:
|
|
642
|
+
params = self._type_param_names_from_node(
|
|
643
|
+
tp_node, parse.source_bytes,
|
|
644
|
+
)
|
|
645
|
+
if params:
|
|
646
|
+
out[class_id] = params
|
|
647
|
+
stack.extend(node.children)
|
|
648
|
+
return out
|
|
649
|
+
|
|
650
|
+
def _collect_class_type_param_bounds(
|
|
651
|
+
self, parses: Dict[Path, ParseResult],
|
|
652
|
+
class_id_by_position: Dict[Tuple[str, int, int], str],
|
|
653
|
+
) -> Dict[str, Dict[str, str]]:
|
|
654
|
+
"""Upper bounds declared directly by each source generic type."""
|
|
655
|
+
out: Dict[str, Dict[str, str]] = {}
|
|
656
|
+
for file_path, parse in parses.items():
|
|
657
|
+
fp_str = str(file_path)
|
|
658
|
+
stack = [parse.tree.root_node]
|
|
659
|
+
while stack:
|
|
660
|
+
node = stack.pop()
|
|
661
|
+
if node.type in _ENCLOSING_TYPE_CST_TYPES:
|
|
662
|
+
class_id = class_id_by_position.get(
|
|
663
|
+
(fp_str, node.start_point[0] + 1, node.start_point[1]),
|
|
664
|
+
)
|
|
665
|
+
tp_node = node.child_by_field_name("type_parameters")
|
|
666
|
+
if class_id is not None and tp_node is not None:
|
|
667
|
+
bounds = self._type_param_bounds_from_node(
|
|
668
|
+
tp_node, parse.source_bytes,
|
|
669
|
+
)
|
|
670
|
+
if bounds:
|
|
671
|
+
out[class_id] = bounds
|
|
672
|
+
stack.extend(node.children)
|
|
673
|
+
return out
|
|
674
|
+
|
|
675
|
+
def _collect_ancestor_type_args(
|
|
676
|
+
self, results: Dict[Path, AnalysisResult],
|
|
677
|
+
class_id_by_position: Dict[Tuple[str, int, int], str],
|
|
678
|
+
file_ctx: Dict[str, "_ImportContext"],
|
|
679
|
+
) -> Dict[Tuple[str, str], List[str]]:
|
|
680
|
+
"""`{(class_id, ancestor_class_id): [type-argument texts]}` read off
|
|
681
|
+
each class/interface's own `extends`/`implements` clause.
|
|
682
|
+
|
|
683
|
+
`Symbol.superclasses`/`Symbol.implements` are erased (`extractor.py::
|
|
684
|
+
_extract_class`) to a plain declared name, matching what the shared
|
|
685
|
+
`graph/builder.py::resolve_relationships` INHERITS/IMPLEMENTS lookup
|
|
686
|
+
needs. The raw clause text with generics intact (`Box<Stripe>`) is
|
|
687
|
+
kept alongside in `Symbol.metadata["java_superclass_args"]`/
|
|
688
|
+
`["java_implements_args"]` — this is the one place that argument
|
|
689
|
+
text survives, keyed by the edge it belongs to so
|
|
690
|
+
`_compose_type_var_bindings` can walk it per hop."""
|
|
691
|
+
out: Dict[Tuple[str, str], List[str]] = {}
|
|
692
|
+
for path, ar in results.items():
|
|
693
|
+
ctx = file_ctx.get(str(path))
|
|
694
|
+
if ctx is None:
|
|
695
|
+
continue
|
|
696
|
+
for sym in ar.symbols:
|
|
697
|
+
if sym.kind != "class":
|
|
698
|
+
continue
|
|
699
|
+
class_id = class_id_by_position.get(
|
|
700
|
+
(str(sym.file_path), sym.position.line, sym.position.column),
|
|
701
|
+
)
|
|
702
|
+
if class_id is None:
|
|
703
|
+
continue
|
|
704
|
+
parent_texts = (
|
|
705
|
+
*sym.metadata.get("java_superclass_args", ()),
|
|
706
|
+
*sym.metadata.get("java_implements_args", ()),
|
|
707
|
+
)
|
|
708
|
+
for parent_text in parent_texts:
|
|
709
|
+
args = self._generic_type_arguments(parent_text)
|
|
710
|
+
if not args:
|
|
711
|
+
continue
|
|
712
|
+
ancestor_id = self._resolve_declared_type(
|
|
713
|
+
parent_text, ctx.module, ctx.import_map, ctx.wildcard_packages,
|
|
714
|
+
)
|
|
715
|
+
if ancestor_id is None or ancestor_id == class_id:
|
|
716
|
+
continue
|
|
717
|
+
out[(class_id, ancestor_id)] = args
|
|
718
|
+
return out
|
|
719
|
+
|
|
720
|
+
@staticmethod
|
|
721
|
+
def _type_param_names_from_node(tp_node, source: bytes) -> List[str]:
|
|
722
|
+
names: List[str] = []
|
|
723
|
+
for child in tp_node.children:
|
|
724
|
+
if child.type != "type_parameter":
|
|
725
|
+
continue
|
|
726
|
+
name_node = next(
|
|
727
|
+
(c for c in child.children if c.type == "type_identifier"), None
|
|
728
|
+
)
|
|
729
|
+
if name_node is not None:
|
|
730
|
+
names.append(JavaCallResolver._text(name_node, source))
|
|
731
|
+
return names
|
|
732
|
+
|
|
733
|
+
@staticmethod
|
|
734
|
+
def _count_args(args_node) -> int:
|
|
735
|
+
return len(argument_nodes(args_node))
|
|
736
|
+
|
|
737
|
+
# ------------------------------------------------------------------ member lookup + overload selection
|
|
738
|
+
|
|
739
|
+
def _is_static_member(self, func_id: str) -> bool:
|
|
740
|
+
"""Whether `func_id` is a `static` method. Fail-safe: a node whose
|
|
741
|
+
metadata carries no `java_is_static` key (a graph built before the
|
|
742
|
+
extractor recorded it) returns True, so the static-import filter in
|
|
743
|
+
`_resolve_own_or_ancestor` never *drops* an edge on missing data —
|
|
744
|
+
it only excludes members positively known to be instance methods."""
|
|
745
|
+
node = self.builder._nodes.get(func_id)
|
|
746
|
+
if node is None:
|
|
747
|
+
return False
|
|
748
|
+
return bool(node.metadata.get("java_is_static", True))
|
|
749
|
+
|
|
750
|
+
def _resolve_own_or_ancestor(
|
|
751
|
+
self, class_id: str, method_name: str, arg_count: int,
|
|
752
|
+
arg_types: Optional[List[str]], self_conf: float, ancestor_conf: float,
|
|
753
|
+
arg_ctx: "_ImportContext", static_only: bool = False,
|
|
754
|
+
allow_method_type_inference: bool = False,
|
|
755
|
+
prefer_unique_own: bool = False,
|
|
756
|
+
narrowing_args: Optional[
|
|
757
|
+
Tuple[object, bytes, "_LocalScopes", Dict[str, str], Dict[str, str]]
|
|
758
|
+
] = None,
|
|
759
|
+
) -> Tuple[Union[str, List[str], None], float]:
|
|
760
|
+
"""Candidate set is own-class overloads *plus* any inherited
|
|
761
|
+
overload not overridden by one of them (see `_members_named`) — a
|
|
762
|
+
same-name method on the child no longer hides the parent's other
|
|
763
|
+
overloads, only the one it actually overrides. Structural lookup
|
|
764
|
+
(own class, then the full INHERITS/IMPLEMENTS ancestor chain) always
|
|
765
|
+
runs, regardless of whether `method_name` is in
|
|
766
|
+
`builtins_deny_list`: unlike Python's deny-list, which guards a
|
|
767
|
+
whole-project unique-simple-name fallback, the Java ancestor walk
|
|
768
|
+
here has no such fuzzy fallback to guard — a match found via
|
|
769
|
+
`_members_named`'s ancestor search is a real INHERITS/IMPLEMENTS
|
|
770
|
+
edge to a real declaration, not a guess, so a base class that
|
|
771
|
+
genuinely declares `toString()` resolves the same way any other
|
|
772
|
+
inherited method does (P4.4 regression fix; previously this
|
|
773
|
+
early-returned unresolved whenever the receiver's own class didn't
|
|
774
|
+
declare a denied name itself, even when an ancestor did — see
|
|
775
|
+
PROBLEMS.md's P4.4 notes for the superseded rationale and
|
|
776
|
+
`TestDenyList`'s renamed test).
|
|
777
|
+
|
|
778
|
+
`narrowing_args` (`(args_node, source, local_types, param_types,
|
|
779
|
+
field_types)`), when given and `arg_types` is `None`, shrinks the
|
|
780
|
+
candidate set via `_narrow_candidates_by_known_arg_types` before
|
|
781
|
+
selection: a call whose arguments are only partially resolvable
|
|
782
|
+
still excludes a same-arity sibling that a *known* argument
|
|
783
|
+
provably cannot reach, instead of fanning out across every one.
|
|
784
|
+
Optional so every pre-existing call site (no known-argument
|
|
785
|
+
narrowing context) keeps its exact prior behavior."""
|
|
786
|
+
# This method resolves `method_invocation` dispatch targets only —
|
|
787
|
+
# never a constructor — so a member named after its own class (an
|
|
788
|
+
# interface method implementation sharing the class's simple name)
|
|
789
|
+
# must not be confused with the constructor of the same name.
|
|
790
|
+
own = self._members_named(
|
|
791
|
+
class_id, method_name, search_ancestors=False, exclude_constructors=True,
|
|
792
|
+
caller_class_id=arg_ctx.enclosing_class_id,
|
|
793
|
+
)
|
|
794
|
+
members = self._members_named(
|
|
795
|
+
class_id, method_name, search_ancestors=True, exclude_constructors=True,
|
|
796
|
+
caller_class_id=arg_ctx.enclosing_class_id,
|
|
797
|
+
)
|
|
798
|
+
if static_only:
|
|
799
|
+
# A static import (`import static a.b.C.name;`, or `C.*`) can only
|
|
800
|
+
# bind a *static* member, so an instance method of the same name
|
|
801
|
+
# is not a legal target here (F-02). Skip nodes whose metadata
|
|
802
|
+
# lacks `java_is_static` (graphs built before this flag existed):
|
|
803
|
+
# absent info must not silently drop a real edge.
|
|
804
|
+
own = [c for c in own if self._is_static_member(c)]
|
|
805
|
+
members = [c for c in members if self._is_static_member(c)]
|
|
806
|
+
if not members:
|
|
807
|
+
return None, 0.0
|
|
808
|
+
if arg_types is None and narrowing_args is not None:
|
|
809
|
+
args_node, source, local_types, param_types, field_types = narrowing_args
|
|
810
|
+
own = self._narrow_candidates_by_known_arg_types(
|
|
811
|
+
own, arg_count, args_node, source, local_types, param_types,
|
|
812
|
+
field_types, arg_ctx,
|
|
813
|
+
)
|
|
814
|
+
members = self._narrow_candidates_by_known_arg_types(
|
|
815
|
+
members, arg_count, args_node, source, local_types, param_types,
|
|
816
|
+
field_types, arg_ctx,
|
|
817
|
+
)
|
|
818
|
+
own_ids = set(own)
|
|
819
|
+
if prefer_unique_own and own:
|
|
820
|
+
own_target, own_conf = self._select_overload(
|
|
821
|
+
own, arg_count, arg_types, lambda _c: self_conf, arg_ctx,
|
|
822
|
+
allow_method_type_inference,
|
|
823
|
+
)
|
|
824
|
+
if isinstance(own_target, str):
|
|
825
|
+
return own_target, own_conf
|
|
826
|
+
if isinstance(own_target, list):
|
|
827
|
+
return None, 0.0
|
|
828
|
+
inherited = [candidate for candidate in members if candidate not in own_ids]
|
|
829
|
+
inherited_target, inherited_conf = self._select_overload(
|
|
830
|
+
inherited, arg_count, arg_types, lambda _c: ancestor_conf, arg_ctx,
|
|
831
|
+
allow_method_type_inference,
|
|
832
|
+
)
|
|
833
|
+
if isinstance(inherited_target, str):
|
|
834
|
+
return inherited_target, inherited_conf
|
|
835
|
+
return None, 0.0
|
|
836
|
+
return self._select_overload(
|
|
837
|
+
members, arg_count, arg_types,
|
|
838
|
+
lambda c: self_conf if c in own_ids else ancestor_conf,
|
|
839
|
+
arg_ctx, allow_method_type_inference,
|
|
840
|
+
)
|
|
841
|
+
|
|
842
|
+
def _annotation_stub_match(
|
|
843
|
+
self, class_id: str, method_name: str, search_self: bool = True,
|
|
844
|
+
) -> bool:
|
|
845
|
+
"""True iff `class_id` (when `search_self`) or any INHERITS/
|
|
846
|
+
IMPLEMENTS ancestor carries a stub-table annotation covering
|
|
847
|
+
`method_name` — Lombok generates its methods on the annotated class
|
|
848
|
+
itself, so a subclass with no annotation of its own still inherits
|
|
849
|
+
the real (compiler-generated) method and must resolve through it
|
|
850
|
+
the same way. `search_self=False` mirrors `_members_named`'s
|
|
851
|
+
`super.m()` semantics: `super.getX()` must reach an ancestor's
|
|
852
|
+
Lombok-generated method, never one implied by the child's own
|
|
853
|
+
annotation.
|
|
854
|
+
|
|
855
|
+
A decorator that resolves — by FQN if already qualified
|
|
856
|
+
(`@lombok.Getter`), else through the *declaring class's own file's*
|
|
857
|
+
package/import context if bare (`@Getter`) — to an annotation
|
|
858
|
+
declared *in the analyzed source* as an `@interface`
|
|
859
|
+
(`self._local_annotation_type_names`) is the project's own
|
|
860
|
+
annotation, never Lombok (whose annotations are always external), so
|
|
861
|
+
the stub is skipped for it — otherwise a local `@interface Getter
|
|
862
|
+
{}` would fabricate a synthetic `getX()` edge. Resolved per-file
|
|
863
|
+
(F-02 fix), not by simple name alone: an unrelated package
|
|
864
|
+
declaring `@interface Getter` must not blind `@lombok.Getter` used
|
|
865
|
+
anywhere else in the project — only a decorator that actually
|
|
866
|
+
resolves to *that* declaration is local."""
|
|
867
|
+
chain = (
|
|
868
|
+
(class_id, *self._class_ancestors(class_id))
|
|
869
|
+
if search_self else self._class_ancestors(class_id)
|
|
870
|
+
)
|
|
871
|
+
for cid in chain:
|
|
872
|
+
node = self.builder._nodes.get(cid)
|
|
873
|
+
if node is None or not node.decorators:
|
|
874
|
+
continue
|
|
875
|
+
for deco in node.decorators:
|
|
876
|
+
dname = self.builder._decorator_name(deco)
|
|
877
|
+
if not dname:
|
|
878
|
+
continue
|
|
879
|
+
simple = dname.split(".")[-1]
|
|
880
|
+
if self._decorator_is_local_annotation(dname, simple, node):
|
|
881
|
+
continue # project's own annotation, not Lombok
|
|
882
|
+
prefixes = self.config.annotation_stub_table.get(simple)
|
|
883
|
+
if prefixes and any(method_name.startswith(p) for p in prefixes):
|
|
884
|
+
return True
|
|
885
|
+
return False
|
|
886
|
+
|
|
887
|
+
def _decorator_is_local_annotation(
|
|
888
|
+
self, dname: str, simple: str, node,
|
|
889
|
+
) -> bool:
|
|
890
|
+
"""`_is_local_annotation` for a decorator found on a graph `node`
|
|
891
|
+
(Lombok-stub guard); see that method for the core resolution."""
|
|
892
|
+
return self._is_local_annotation(dname, simple, node.file_path)
|
|
893
|
+
|
|
894
|
+
def _is_local_annotation(self, dname: str, simple: str, file_path: str) -> bool:
|
|
895
|
+
"""True iff decorator `dname` (simple name `simple`) declared in
|
|
896
|
+
`file_path` resolves to an `@interface` declared in the analyzed
|
|
897
|
+
source, per `self._local_annotation_type_names` (simple name ->
|
|
898
|
+
declared FQNs). Shared by the Lombok-stub guard
|
|
899
|
+
(`_decorator_is_local_annotation`, F-01/F-02) and the DI-evidence
|
|
900
|
+
guard (`_di_annotation_simple_name`, F-02 follow-up) — same question
|
|
901
|
+
("does this well-known simple name actually resolve to *this
|
|
902
|
+
project's own* declaration, not the external framework's?") asked
|
|
903
|
+
for two different well-known-name tables.
|
|
904
|
+
|
|
905
|
+
A dotted `dname` is checked directly against the declared-FQN set
|
|
906
|
+
first (`@lombok.Getter` already *is* its own resolved FQN — that
|
|
907
|
+
direct-FQN path is the F-02 fix: it is what lets `@lombok.Getter`
|
|
908
|
+
survive an unrelated package's `@interface Getter`). Unlike a
|
|
909
|
+
top-level package-qualified name, though, a dotted `dname` is not
|
|
910
|
+
always an absolute FQN: `@Marks.Getter` on a *nested* annotation
|
|
911
|
+
type (`class Marks { @interface Getter {} }`) is a type-qualified
|
|
912
|
+
reference relative to `node`'s own package/import context, the same
|
|
913
|
+
way `Marks.Getter` would resolve as an ordinary (non-annotation)
|
|
914
|
+
member-type reference. So on a direct-match miss, the leading
|
|
915
|
+
segment is resolved as a declared type via `_resolve_declared_type`
|
|
916
|
+
(same import-map/wildcard-package machinery as any other receiver
|
|
917
|
+
type, no separate resolution path to drift) and the remaining
|
|
918
|
+
segment(s) are re-joined onto *that* type's own FQN (F-01 fix — the
|
|
919
|
+
old code trusted every dotted name as already-absolute and could
|
|
920
|
+
never match a nested declaration's `package.Enclosing.Simple` key).
|
|
921
|
+
Because a member annotation type is an *inherited* member, the
|
|
922
|
+
qualifier may be a subclass that inherits the declaration
|
|
923
|
+
(`@Child.Getter`, `class Child extends Marks`, `Marks` declaring
|
|
924
|
+
`Getter` -> canonical `package.Marks.Getter`); the candidate is
|
|
925
|
+
therefore tried against the resolved qualifier *and* each of its
|
|
926
|
+
`_class_ancestors`, mirroring Java's own member-type-inheritance
|
|
927
|
+
lookup (F-01 residual fix).
|
|
928
|
+
A bare `dname` (`@Getter`) is resolved through `node`'s own
|
|
929
|
+
declaring file's import map / package (`self._file_ctx`, mirroring
|
|
930
|
+
`_param_ctx_for`'s "declaring file's own context, not the call
|
|
931
|
+
site's" rule) before comparing — an explicit single-type import or
|
|
932
|
+
same-package sibling `@interface Getter` still counts as local,
|
|
933
|
+
matching Java's own name-resolution scoping.
|
|
934
|
+
|
|
935
|
+
Missing file context (e.g. a partial incremental rescan not passing
|
|
936
|
+
every file) falls back to treating any known local FQN for `simple`
|
|
937
|
+
as a match — "missing edge over wrong edge", same precision stance
|
|
938
|
+
as `_param_ctx_for` — for both the dotted and bare cases."""
|
|
939
|
+
local_fqns = self._local_annotation_type_names.get(simple)
|
|
940
|
+
if not local_fqns:
|
|
941
|
+
return False
|
|
942
|
+
ctx = self._file_ctx.get(file_path)
|
|
943
|
+
if "." in dname:
|
|
944
|
+
if dname in local_fqns:
|
|
945
|
+
return True
|
|
946
|
+
if ctx is None:
|
|
947
|
+
return True
|
|
948
|
+
head, _, rest = dname.partition(".")
|
|
949
|
+
head_id = self._resolve_declared_type(
|
|
950
|
+
head, ctx.module, ctx.import_map, ctx.wildcard_packages,
|
|
951
|
+
)
|
|
952
|
+
if head_id is None:
|
|
953
|
+
return False
|
|
954
|
+
# A member annotation type is an *inherited* member: `@Child.Getter`
|
|
955
|
+
# where `class Child extends Marks` and `Marks` declares `Getter`
|
|
956
|
+
# canonically names `<pkg>.Marks.Getter`, not `<pkg>.Child.Getter`
|
|
957
|
+
# (F-01 residual — the declaring type may be an ancestor of the
|
|
958
|
+
# qualifier, so re-joining `rest` onto the qualifier's own FQN alone
|
|
959
|
+
# misses it). Try the qualifier itself, then each ancestor, mirroring
|
|
960
|
+
# Java's own member-type-inheritance lookup. `@lombok.Getter` never
|
|
961
|
+
# reaches here (head `lombok` resolves to no CLASS node), so this
|
|
962
|
+
# cannot over-suppress a genuine external annotation.
|
|
963
|
+
for type_id in (head_id, *self._class_ancestors(head_id)):
|
|
964
|
+
type_node = self.builder._nodes[type_id]
|
|
965
|
+
type_chain = type_node.qualified_name
|
|
966
|
+
type_prefix = f"{type_node.module}."
|
|
967
|
+
if type_chain.startswith(type_prefix):
|
|
968
|
+
type_chain = type_chain[len(type_prefix):]
|
|
969
|
+
candidate = (
|
|
970
|
+
f"{type_chain}.{rest}" if type_node.module == DEFAULT_PACKAGE
|
|
971
|
+
else f"{type_node.module}.{type_chain}.{rest}"
|
|
972
|
+
)
|
|
973
|
+
if candidate in local_fqns:
|
|
974
|
+
return True
|
|
975
|
+
return False
|
|
976
|
+
if ctx is None:
|
|
977
|
+
return True
|
|
978
|
+
imported = ctx.import_map.get(simple)
|
|
979
|
+
if imported is not None:
|
|
980
|
+
return imported in local_fqns
|
|
981
|
+
own_fqn = simple if ctx.module == DEFAULT_PACKAGE else f"{ctx.module}.{simple}"
|
|
982
|
+
if own_fqn in local_fqns:
|
|
983
|
+
return True
|
|
984
|
+
return any(f"{pkg}.{simple}" in local_fqns for pkg in ctx.wildcard_packages)
|
|
985
|
+
|
|
986
|
+
# ------------------------------------------------------------------ call-site resolution
|
|
987
|
+
|
|
988
|
+
def _resolve_static_import_class(
|
|
989
|
+
self, cls_path: str, module_name: str,
|
|
990
|
+
) -> Optional[str]:
|
|
991
|
+
"""The CLASS node id a static import's class path (`a.b.C`) names, or
|
|
992
|
+
None. Tries `_lookup_qualified` first — the direct fully-qualified
|
|
993
|
+
lookup that a static import's already-resolved dotted path
|
|
994
|
+
(`a.b.C.method` minus its last segment) needs, and which
|
|
995
|
+
`_resolve_name(..., strict=True)` cannot do with an empty import map
|
|
996
|
+
(it only checks head-in-import-map or tail-in-same-module) — then
|
|
997
|
+
falls back to `_resolve_name` for a single-segment path (a same-module
|
|
998
|
+
or default-package class the qualified lookup, which needs >= 2 dotted
|
|
999
|
+
parts, can't handle)."""
|
|
1000
|
+
if not cls_path:
|
|
1001
|
+
return None
|
|
1002
|
+
namespace = self.builder._module_namespace(module_name)
|
|
1003
|
+
# Static imports carry the exact owner path. Prefer the dotted index
|
|
1004
|
+
# so nested owners (`Documenter.CanvasOptions.Grouping`) cannot be
|
|
1005
|
+
# replaced by another same-simple-name class in the package-level
|
|
1006
|
+
# simple-name map.
|
|
1007
|
+
cls_id = self.builder._node_for_dotted(cls_path, namespace)
|
|
1008
|
+
if cls_id is None:
|
|
1009
|
+
cls_id = self.builder._lookup_qualified(
|
|
1010
|
+
cls_path, self._by_module_symbol, namespace,
|
|
1011
|
+
)
|
|
1012
|
+
if cls_id is None:
|
|
1013
|
+
cls_id = self.builder._resolve_name(
|
|
1014
|
+
cls_path, module_name, {}, {}, self._by_module_symbol, strict=True,
|
|
1015
|
+
)
|
|
1016
|
+
if cls_id and self.builder._nodes[cls_id].node_type == NodeType.CLASS:
|
|
1017
|
+
return cls_id
|
|
1018
|
+
return None
|
|
1019
|
+
|
|
1020
|
+
# ------------------------------------------------------------------ P4.12 abstract-receiver fan-out
|
|
1021
|
+
|
|
1022
|
+
@staticmethod
|
|
1023
|
+
def _apply_type_bound(type_text: str, bounds: Dict[str, str]) -> str:
|
|
1024
|
+
"""Rewrite a receiver type that is a method type parameter to its
|
|
1025
|
+
concrete upper bound (`T` -> `StripeGateway` given
|
|
1026
|
+
`<T extends StripeGateway>`); pass anything else through unchanged. The
|
|
1027
|
+
raw type is erased and de-arrayed first so `T[]`/`List<T>`-shaped names
|
|
1028
|
+
still key on the bare parameter."""
|
|
1029
|
+
if not bounds:
|
|
1030
|
+
return type_text
|
|
1031
|
+
base = erased_type(type_text).strip()
|
|
1032
|
+
while base.endswith("[]"):
|
|
1033
|
+
base = base[:-2]
|
|
1034
|
+
return bounds.get(base, type_text)
|
|
1035
|
+
|
|
1036
|
+
|
|
1037
|
+
def _resolve_call(
|
|
1038
|
+
self, call_node, source: bytes, module_name: str,
|
|
1039
|
+
enclosing_class_id: Optional[str], import_map: Dict[str, str],
|
|
1040
|
+
wildcard_packages: List[str],
|
|
1041
|
+
local_types: "_LocalScopes", own_param_types: Dict[str, str],
|
|
1042
|
+
static_wildcard_classes: List[str],
|
|
1043
|
+
type_param_bounds: Optional[Dict[str, str]] = None,
|
|
1044
|
+
) -> Tuple[Union[str, List[str], None], float, Dict[str, object]]:
|
|
1045
|
+
"""Resolve a `method_invocation` node to `(target_id_or_candidates,
|
|
1046
|
+
confidence, edge_metadata)`. Cascade: `super.m()` -> ancestor walk
|
|
1047
|
+
(0.85), then the ancestor-only annotation-stub fallback (0.6);
|
|
1048
|
+
bare/`this.m()` -> own class then ancestors (0.95 / 0.85), bare calls
|
|
1049
|
+
additionally try an explicit static import then each static wildcard
|
|
1050
|
+
import (0.85 / 0.75), then the own+ancestor annotation-stub fallback
|
|
1051
|
+
(0.6); typed receiver (local/param/field/static-class) -> own class
|
|
1052
|
+
then ancestors at the receiver's confidence, then the same
|
|
1053
|
+
annotation-stub fallback (0.6). The stub fallback is reachable from
|
|
1054
|
+
every branch: Lombok generates real methods on the annotated class,
|
|
1055
|
+
so a call site calling its *own* class's Lombok-generated accessor
|
|
1056
|
+
(`this.getX()`/bare `getX()`) is exactly as legitimate as calling
|
|
1057
|
+
one through an external typed receiver — the fallback must not be
|
|
1058
|
+
exclusive to the latter.
|
|
1059
|
+
|
|
1060
|
+
`edge_metadata` is empty for every branch except the P4.12
|
|
1061
|
+
abstract-receiver fan-out (a typed receiver whose declared type is an
|
|
1062
|
+
interface/abstract class and whose P4.4 target is therefore an
|
|
1063
|
+
abstract method): there it carries `strategy` + `declared_receiver_type`
|
|
1064
|
+
so impact/MCP can explain the fanned-out (or DI-narrowed) edges
|
|
1065
|
+
(deep-dive §8.4). `type_param_bounds` feeds generic narrowing — see
|
|
1066
|
+
`_resolve_receiver`.
|
|
1067
|
+
"""
|
|
1068
|
+
caller_ctx = _ImportContext(
|
|
1069
|
+
module_name, import_map, wildcard_packages, enclosing_class_id,
|
|
1070
|
+
)
|
|
1071
|
+
name_node = call_node.child_by_field_name("name")
|
|
1072
|
+
if name_node is None:
|
|
1073
|
+
return None, 0.0, {}
|
|
1074
|
+
method_name = self._text(name_node, source)
|
|
1075
|
+
args_node = call_node.child_by_field_name("arguments")
|
|
1076
|
+
arg_count = self._count_args(args_node)
|
|
1077
|
+
field_types_own = (
|
|
1078
|
+
self._visible_bare_field_types(enclosing_class_id)
|
|
1079
|
+
if enclosing_class_id else {}
|
|
1080
|
+
)
|
|
1081
|
+
arg_types = self._infer_arg_types(
|
|
1082
|
+
args_node, source, local_types, own_param_types, field_types_own,
|
|
1083
|
+
)
|
|
1084
|
+
obj_node = call_node.child_by_field_name("object")
|
|
1085
|
+
arg_nodes = argument_nodes(args_node)
|
|
1086
|
+
bare_creation_args = bool(arg_nodes) and all(
|
|
1087
|
+
child.type == "object_creation_expression" for child in arg_nodes
|
|
1088
|
+
)
|
|
1089
|
+
|
|
1090
|
+
if obj_node is not None and obj_node.type == "super":
|
|
1091
|
+
if enclosing_class_id is None:
|
|
1092
|
+
return None, 0.0, {}
|
|
1093
|
+
cands = self._members_named(
|
|
1094
|
+
enclosing_class_id, method_name, search_ancestors=True, search_self=False,
|
|
1095
|
+
exclude_constructors=True, caller_class_id=enclosing_class_id,
|
|
1096
|
+
)
|
|
1097
|
+
if not cands:
|
|
1098
|
+
if self._annotation_stub_match(enclosing_class_id, method_name, search_self=False):
|
|
1099
|
+
return enclosing_class_id, 0.6, {}
|
|
1100
|
+
return None, 0.0, {}
|
|
1101
|
+
target, conf = self._select_overload(
|
|
1102
|
+
cands, arg_count, arg_types, lambda _c: 0.85, caller_ctx,
|
|
1103
|
+
)
|
|
1104
|
+
# `super.<abstract>()` is illegal Java, so this rarely fires, but
|
|
1105
|
+
# keep the branch consistent with `this`/bare: if the ancestor walk
|
|
1106
|
+
# somehow lands on an abstract declaration, fan out over the
|
|
1107
|
+
# enclosing class's concrete implementors rather than emit a
|
|
1108
|
+
# bodiless edge.
|
|
1109
|
+
if target is not None and self._is_abstract_target(target):
|
|
1110
|
+
fan = self._raw_implementor_fan_out(
|
|
1111
|
+
enclosing_class_id, method_name, arg_count, arg_types,
|
|
1112
|
+
caller_ctx,
|
|
1113
|
+
)
|
|
1114
|
+
if fan is not None:
|
|
1115
|
+
return fan
|
|
1116
|
+
return target, conf, {}
|
|
1117
|
+
|
|
1118
|
+
if obj_node is None or obj_node.type == "this":
|
|
1119
|
+
if enclosing_class_id is None:
|
|
1120
|
+
return None, 0.0, {}
|
|
1121
|
+
implicit_candidates = self._members_named(
|
|
1122
|
+
enclosing_class_id, method_name, search_ancestors=True,
|
|
1123
|
+
exclude_constructors=True, caller_class_id=enclosing_class_id,
|
|
1124
|
+
)
|
|
1125
|
+
overload_arg_types = self._contextual_overload_arg_types(
|
|
1126
|
+
implicit_candidates, arg_count, arg_types, args_node, source,
|
|
1127
|
+
module_name, enclosing_class_id, import_map, wildcard_packages,
|
|
1128
|
+
local_types, own_param_types, field_types_own,
|
|
1129
|
+
type_param_bounds or {},
|
|
1130
|
+
)
|
|
1131
|
+
target, conf = self._resolve_own_or_ancestor(
|
|
1132
|
+
enclosing_class_id, method_name, arg_count, overload_arg_types,
|
|
1133
|
+
0.95, 0.85,
|
|
1134
|
+
caller_ctx,
|
|
1135
|
+
# C04 direct-root scope: nested bare calls have a separate
|
|
1136
|
+
# scoring/propagation identity and must remain unchanged.
|
|
1137
|
+
allow_method_type_inference=(
|
|
1138
|
+
obj_node is None and call_node.parent.type != "arguments"
|
|
1139
|
+
),
|
|
1140
|
+
# C04: for `helper(new T<>())`, require one applicable method
|
|
1141
|
+
# on the own class before considering inherited declarations.
|
|
1142
|
+
# A tied own overload is unresolved, never a C06-style fan-out.
|
|
1143
|
+
prefer_unique_own=(obj_node is None and bare_creation_args),
|
|
1144
|
+
narrowing_args=(
|
|
1145
|
+
args_node, source, local_types, own_param_types,
|
|
1146
|
+
field_types_own,
|
|
1147
|
+
),
|
|
1148
|
+
)
|
|
1149
|
+
if target is not None:
|
|
1150
|
+
# P4.12: a `this`/bare call landing on an *abstract* method is
|
|
1151
|
+
# the Template Method / interface-default-method pattern — the
|
|
1152
|
+
# abstract sibling declared on this very (abstract/interface)
|
|
1153
|
+
# class has no body, so fan out to the concrete implementors,
|
|
1154
|
+
# exactly like the typed-receiver branch, rooted at the
|
|
1155
|
+
# enclosing class (a concrete class cannot resolve one of its
|
|
1156
|
+
# own calls to an abstract method, so `_is_abstract_target`
|
|
1157
|
+
# implies the enclosing type is abstract/interface). No DI
|
|
1158
|
+
# narrowing: `this` is never an injected field.
|
|
1159
|
+
if self._is_abstract_target(target):
|
|
1160
|
+
fan = self._raw_implementor_fan_out(
|
|
1161
|
+
enclosing_class_id, method_name, arg_count,
|
|
1162
|
+
overload_arg_types,
|
|
1163
|
+
caller_ctx,
|
|
1164
|
+
)
|
|
1165
|
+
if fan is not None:
|
|
1166
|
+
# The abstract declaration has no executable body.
|
|
1167
|
+
# Once concrete implementors are known, emit only the
|
|
1168
|
+
# runtime targets — the typed-receiver branch follows
|
|
1169
|
+
# the same rule, and retaining the declaration creates
|
|
1170
|
+
# a false-positive call edge for template methods.
|
|
1171
|
+
return fan
|
|
1172
|
+
return target, conf, {}
|
|
1173
|
+
if obj_node is None:
|
|
1174
|
+
# An unqualified call in a nested class may target a member
|
|
1175
|
+
# of a lexical outer class through the implicit enclosing
|
|
1176
|
+
# instance (`Inner.this` semantics). Search only the exact
|
|
1177
|
+
# lexical owner chain, nearest first. An explicitly static
|
|
1178
|
+
# nested class has no enclosing instance, so retain only
|
|
1179
|
+
# static outer members in that case.
|
|
1180
|
+
current_class = self.builder._nodes.get(enclosing_class_id)
|
|
1181
|
+
static_context = bool(
|
|
1182
|
+
current_class
|
|
1183
|
+
and "static" in current_class.metadata.get("modifiers", ())
|
|
1184
|
+
)
|
|
1185
|
+
for outer_id in self._lexical_outer_class_ids(enclosing_class_id):
|
|
1186
|
+
outer_candidates = self._members_named(
|
|
1187
|
+
outer_id, method_name, search_ancestors=True,
|
|
1188
|
+
exclude_constructors=True, caller_class_id=enclosing_class_id,
|
|
1189
|
+
)
|
|
1190
|
+
if static_context:
|
|
1191
|
+
outer_candidates = [
|
|
1192
|
+
candidate for candidate in outer_candidates
|
|
1193
|
+
if self._is_static_member(candidate)
|
|
1194
|
+
]
|
|
1195
|
+
outer_arg_types = self._contextual_overload_arg_types(
|
|
1196
|
+
outer_candidates, arg_count, arg_types, args_node, source,
|
|
1197
|
+
module_name, outer_id, import_map, wildcard_packages,
|
|
1198
|
+
local_types, own_param_types,
|
|
1199
|
+
self._all_field_types(outer_id), type_param_bounds or {},
|
|
1200
|
+
)
|
|
1201
|
+
outer_target, outer_conf = self._resolve_own_or_ancestor(
|
|
1202
|
+
outer_id, method_name, arg_count, outer_arg_types,
|
|
1203
|
+
0.90, 0.80, caller_ctx,
|
|
1204
|
+
prefer_unique_own=True,
|
|
1205
|
+
narrowing_args=(
|
|
1206
|
+
args_node, source, local_types, own_param_types,
|
|
1207
|
+
self._all_field_types(outer_id),
|
|
1208
|
+
),
|
|
1209
|
+
)
|
|
1210
|
+
if outer_target is not None:
|
|
1211
|
+
return outer_target, outer_conf, {}
|
|
1212
|
+
# A bare call unresolved on the own class/ancestors may be a
|
|
1213
|
+
# statically-imported method invoked without a receiver.
|
|
1214
|
+
if method_name in import_map:
|
|
1215
|
+
# explicit static import: `import static a.b.C.method;`
|
|
1216
|
+
resolved = import_map[method_name]
|
|
1217
|
+
cls_path = ".".join(resolved.split(".")[:-1])
|
|
1218
|
+
cls_id = self._resolve_static_import_class(cls_path, module_name)
|
|
1219
|
+
if cls_id is not None:
|
|
1220
|
+
candidates = [
|
|
1221
|
+
candidate for candidate in self._members_named(
|
|
1222
|
+
cls_id, method_name, search_ancestors=True,
|
|
1223
|
+
exclude_constructors=True, caller_class_id=enclosing_class_id,
|
|
1224
|
+
)
|
|
1225
|
+
if self._is_static_member(candidate)
|
|
1226
|
+
]
|
|
1227
|
+
static_arg_types = self._contextual_overload_arg_types(
|
|
1228
|
+
candidates, arg_count, arg_types, args_node, source,
|
|
1229
|
+
module_name, enclosing_class_id, import_map,
|
|
1230
|
+
wildcard_packages, local_types, own_param_types,
|
|
1231
|
+
field_types_own, type_param_bounds or {},
|
|
1232
|
+
)
|
|
1233
|
+
st = self._resolve_own_or_ancestor(
|
|
1234
|
+
cls_id, method_name, arg_count, static_arg_types, 0.85, 0.75,
|
|
1235
|
+
caller_ctx, static_only=True,
|
|
1236
|
+
# Static generic utilities such as
|
|
1237
|
+
# `Preconditions.notNull(T, String)` infer their
|
|
1238
|
+
# method type variable from the supplied source
|
|
1239
|
+
# argument just as typed-receiver calls do.
|
|
1240
|
+
allow_method_type_inference=True,
|
|
1241
|
+
narrowing_args=(
|
|
1242
|
+
args_node, source, local_types, own_param_types,
|
|
1243
|
+
field_types_own,
|
|
1244
|
+
),
|
|
1245
|
+
)
|
|
1246
|
+
if st[0] is not None:
|
|
1247
|
+
return st[0], st[1], {}
|
|
1248
|
+
# static wildcard import: `import static a.b.C.*;` — no
|
|
1249
|
+
# binding in `import_map` (the wildcard carries no name to
|
|
1250
|
+
# bind), so every statically-wildcard-imported class's
|
|
1251
|
+
# candidates must be gathered *before* overload resolution
|
|
1252
|
+
# runs (F-03 fix): resolving one class at a time and
|
|
1253
|
+
# returning on the first hit let an earlier `import static`
|
|
1254
|
+
# line shadow a later one that declares an equally-
|
|
1255
|
+
# applicable overload (`run(int)` in both `A` and `B`),
|
|
1256
|
+
# silently picking whichever happened to be imported first
|
|
1257
|
+
# instead of surfacing the real ambiguity. Collecting every
|
|
1258
|
+
# class's own+ancestor candidates into one pool and running
|
|
1259
|
+
# a single `_select_overload` over it reuses the same
|
|
1260
|
+
# exact/strict/loose + `_most_specific` ranking (and its
|
|
1261
|
+
# fan-out-at-reduced-confidence behavior) that already
|
|
1262
|
+
# disambiguates overloads within a single class.
|
|
1263
|
+
wildcard_conf: Dict[str, float] = {}
|
|
1264
|
+
for cls_path in static_wildcard_classes:
|
|
1265
|
+
cls_id = self._resolve_static_import_class(cls_path, module_name)
|
|
1266
|
+
if cls_id is None:
|
|
1267
|
+
continue
|
|
1268
|
+
own = [
|
|
1269
|
+
c for c in self._members_named(
|
|
1270
|
+
cls_id, method_name, search_ancestors=False,
|
|
1271
|
+
exclude_constructors=True, caller_class_id=enclosing_class_id,
|
|
1272
|
+
)
|
|
1273
|
+
if self._is_static_member(c)
|
|
1274
|
+
]
|
|
1275
|
+
own_ids = set(own)
|
|
1276
|
+
members = [
|
|
1277
|
+
c for c in self._members_named(
|
|
1278
|
+
cls_id, method_name, search_ancestors=True,
|
|
1279
|
+
exclude_constructors=True, caller_class_id=enclosing_class_id,
|
|
1280
|
+
)
|
|
1281
|
+
if self._is_static_member(c)
|
|
1282
|
+
]
|
|
1283
|
+
for c in members:
|
|
1284
|
+
wildcard_conf[c] = 0.85 if c in own_ids else 0.75
|
|
1285
|
+
if wildcard_conf:
|
|
1286
|
+
wildcard_candidates = list(wildcard_conf)
|
|
1287
|
+
wildcard_arg_types = self._contextual_overload_arg_types(
|
|
1288
|
+
wildcard_candidates, arg_count, arg_types, args_node, source,
|
|
1289
|
+
module_name, enclosing_class_id, import_map,
|
|
1290
|
+
wildcard_packages, local_types, own_param_types,
|
|
1291
|
+
field_types_own, type_param_bounds or {},
|
|
1292
|
+
)
|
|
1293
|
+
if wildcard_arg_types is None:
|
|
1294
|
+
wildcard_candidates = self._narrow_candidates_by_known_arg_types(
|
|
1295
|
+
wildcard_candidates, arg_count, args_node, source,
|
|
1296
|
+
local_types, own_param_types, field_types_own,
|
|
1297
|
+
caller_ctx,
|
|
1298
|
+
)
|
|
1299
|
+
st = self._select_overload(
|
|
1300
|
+
wildcard_candidates, arg_count, wildcard_arg_types,
|
|
1301
|
+
lambda c: wildcard_conf[c], caller_ctx,
|
|
1302
|
+
allow_method_type_inference=True,
|
|
1303
|
+
)
|
|
1304
|
+
if st[0] is not None:
|
|
1305
|
+
return st[0], st[1], {}
|
|
1306
|
+
if self._annotation_stub_match(enclosing_class_id, method_name):
|
|
1307
|
+
return enclosing_class_id, 0.6, {}
|
|
1308
|
+
return None, 0.0, {}
|
|
1309
|
+
|
|
1310
|
+
if obj_node.type == "object_creation_expression":
|
|
1311
|
+
# Deliberately narrow C08 slice: resolve only a method invoked
|
|
1312
|
+
# directly on `new T(...)`. Do not teach return propagation how
|
|
1313
|
+
# to derive a type from object creation here, so an outer
|
|
1314
|
+
# `new T().step().finish()` remains out of scope. Likewise,
|
|
1315
|
+
# require one structural same-name candidate before overload
|
|
1316
|
+
# selection; overloaded root-new calls remain unresolved for
|
|
1317
|
+
# their own follow-up task.
|
|
1318
|
+
type_node = obj_node.child_by_field_name("type")
|
|
1319
|
+
if type_node is None:
|
|
1320
|
+
return None, 0.0, {}
|
|
1321
|
+
created_type = self._text(type_node, source)
|
|
1322
|
+
recv_class_id = self._resolve_declared_type(
|
|
1323
|
+
created_type, module_name, import_map, wildcard_packages,
|
|
1324
|
+
enclosing_class_id,
|
|
1325
|
+
)
|
|
1326
|
+
if recv_class_id is None:
|
|
1327
|
+
return None, 0.0, {}
|
|
1328
|
+
candidates = self._members_named(
|
|
1329
|
+
recv_class_id, method_name, search_ancestors=True,
|
|
1330
|
+
exclude_constructors=True, caller_class_id=enclosing_class_id,
|
|
1331
|
+
)
|
|
1332
|
+
if not candidates:
|
|
1333
|
+
return None, 0.0, {}
|
|
1334
|
+
root_new_arg_types = arg_types
|
|
1335
|
+
if len(candidates) > 1:
|
|
1336
|
+
root_new_arg_types = self._infer_contextual_arg_types(
|
|
1337
|
+
args_node, source, module_name, enclosing_class_id,
|
|
1338
|
+
import_map, wildcard_packages, local_types,
|
|
1339
|
+
own_param_types, field_types_own, type_param_bounds or {},
|
|
1340
|
+
)
|
|
1341
|
+
if root_new_arg_types is None:
|
|
1342
|
+
return None, 0.0, {}
|
|
1343
|
+
target, conf = self._select_overload(
|
|
1344
|
+
candidates, arg_count, root_new_arg_types,
|
|
1345
|
+
lambda _candidate: 0.9,
|
|
1346
|
+
caller_ctx,
|
|
1347
|
+
)
|
|
1348
|
+
if isinstance(target, str):
|
|
1349
|
+
return target, conf, {}
|
|
1350
|
+
return None, 0.0, {}
|
|
1351
|
+
|
|
1352
|
+
recv_class_id, recv_conf = self._resolve_receiver(
|
|
1353
|
+
obj_node, source, module_name, enclosing_class_id, import_map,
|
|
1354
|
+
wildcard_packages, local_types, field_types_own, own_param_types,
|
|
1355
|
+
type_param_bounds,
|
|
1356
|
+
)
|
|
1357
|
+
if recv_class_id is None:
|
|
1358
|
+
return None, 0.0, {}
|
|
1359
|
+
typed_candidates = self._members_named(
|
|
1360
|
+
recv_class_id, method_name, search_ancestors=True,
|
|
1361
|
+
exclude_constructors=True, caller_class_id=enclosing_class_id,
|
|
1362
|
+
)
|
|
1363
|
+
overload_arg_types = self._contextual_overload_arg_types(
|
|
1364
|
+
typed_candidates, arg_count, arg_types, args_node, source,
|
|
1365
|
+
module_name, enclosing_class_id, import_map, wildcard_packages,
|
|
1366
|
+
local_types, own_param_types, field_types_own,
|
|
1367
|
+
type_param_bounds or {},
|
|
1368
|
+
)
|
|
1369
|
+
target, conf = self._resolve_own_or_ancestor(
|
|
1370
|
+
recv_class_id, method_name, arg_count, overload_arg_types,
|
|
1371
|
+
recv_conf, min(recv_conf, 0.85), caller_ctx,
|
|
1372
|
+
# C07: once the explicit receiver class is resolved strictly,
|
|
1373
|
+
# method-scoped type variables (`T`, `<A extends Appendable>`)
|
|
1374
|
+
# participate in applicability instead of being mistaken for
|
|
1375
|
+
# source class names. Exact concrete/Supplier overloads still win
|
|
1376
|
+
# in `_select_overload`'s existing phase ordering.
|
|
1377
|
+
allow_method_type_inference=True,
|
|
1378
|
+
narrowing_args=(
|
|
1379
|
+
args_node, source, local_types, own_param_types, field_types_own,
|
|
1380
|
+
),
|
|
1381
|
+
)
|
|
1382
|
+
if target is not None:
|
|
1383
|
+
# P4.12: a typed receiver whose declared type is an interface/
|
|
1384
|
+
# abstract class resolves (per P4.4) to an *abstract* method — the
|
|
1385
|
+
# interface declaration, which has no body and is useless for blast
|
|
1386
|
+
# radius. Fan out to the concrete implementors instead (or narrow
|
|
1387
|
+
# via Spring DI). Only fires when the resolved target is actually
|
|
1388
|
+
# abstract; a concrete method on an abstract class (a template
|
|
1389
|
+
# method) or an interface `default` method keeps its direct edge.
|
|
1390
|
+
fan = self._abstract_fan_out(
|
|
1391
|
+
recv_class_id, target, method_name, arg_count,
|
|
1392
|
+
overload_arg_types,
|
|
1393
|
+
caller_ctx, obj_node, source, enclosing_class_id, own_param_types,
|
|
1394
|
+
local_types,
|
|
1395
|
+
)
|
|
1396
|
+
if fan is not None:
|
|
1397
|
+
return fan
|
|
1398
|
+
return target, conf, {}
|
|
1399
|
+
if self._annotation_stub_match(recv_class_id, method_name):
|
|
1400
|
+
return recv_class_id, min(recv_conf, 0.6), {}
|
|
1401
|
+
return None, 0.0, {}
|