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.
Files changed (104) hide show
  1. package/INSTALL_TOOLKIT.md +163 -0
  2. package/README.md +269 -2
  3. package/adl_drift/__init__.py +6 -0
  4. package/adl_drift/cli/__init__.py +1 -0
  5. package/adl_drift/cli/commands.py +993 -0
  6. package/adl_drift/cli/main.py +744 -0
  7. package/adl_drift/cortex/__init__.py +1 -0
  8. package/adl_drift/cortex/embeddings.py +96 -0
  9. package/adl_drift/cortex/memory.py +1749 -0
  10. package/adl_drift/cortex/patterns.py +89 -0
  11. package/adl_drift/cortex/telemetry.py +192 -0
  12. package/adl_drift/detectors/__init__.py +1 -0
  13. package/adl_drift/detectors/async_patterns.py +115 -0
  14. package/adl_drift/detectors/base.py +121 -0
  15. package/adl_drift/detectors/config.py +122 -0
  16. package/adl_drift/detectors/data_access.py +128 -0
  17. package/adl_drift/detectors/decorators.py +122 -0
  18. package/adl_drift/detectors/dynamic_patterns.py +260 -0
  19. package/adl_drift/detectors/errors.py +134 -0
  20. package/adl_drift/detectors/imports.py +132 -0
  21. package/adl_drift/detectors/logging_patterns.py +108 -0
  22. package/adl_drift/detectors/routes.py +257 -0
  23. package/adl_drift/detectors/security.py +236 -0
  24. package/adl_drift/detectors/taint.py +406 -0
  25. package/adl_drift/detectors/tests.py +95 -0
  26. package/adl_drift/detectors/type_conventions.py +81 -0
  27. package/adl_drift/graph/__init__.py +1 -0
  28. package/adl_drift/graph/builder.py +2670 -0
  29. package/adl_drift/graph/cfg.py +461 -0
  30. package/adl_drift/graph/cluster.py +40 -0
  31. package/adl_drift/graph/edges.py +86 -0
  32. package/adl_drift/graph/node.py +90 -0
  33. package/adl_drift/harness/__init__.py +10 -0
  34. package/adl_drift/harness/setup.py +832 -0
  35. package/adl_drift/harness/skills/drift-antigravity.md +19 -0
  36. package/adl_drift/harness/skills/drift-generic.md +19 -0
  37. package/adl_drift/harness/skills/drift.md +25 -0
  38. package/adl_drift/harness/watch.py +84 -0
  39. package/adl_drift/lang/__init__.py +8 -0
  40. package/adl_drift/lang/base.py +464 -0
  41. package/adl_drift/lang/java/__init__.py +6 -0
  42. package/adl_drift/lang/java/backend.py +97 -0
  43. package/adl_drift/lang/java/cfg.py +1424 -0
  44. package/adl_drift/lang/java/config.py +707 -0
  45. package/adl_drift/lang/java/detectors/__init__.py +29 -0
  46. package/adl_drift/lang/java/detectors/additional.py +445 -0
  47. package/adl_drift/lang/java/detectors/base.py +154 -0
  48. package/adl_drift/lang/java/detectors/data_access.py +148 -0
  49. package/adl_drift/lang/java/detectors/imports.py +53 -0
  50. package/adl_drift/lang/java/detectors/routes.py +159 -0
  51. package/adl_drift/lang/java/detectors/security.py +386 -0
  52. package/adl_drift/lang/java/detectors/tests.py +74 -0
  53. package/adl_drift/lang/java/extractor.py +582 -0
  54. package/adl_drift/lang/java/ids.py +99 -0
  55. package/adl_drift/lang/java/parser.py +49 -0
  56. package/adl_drift/lang/java/resolution/__init__.py +1 -0
  57. package/adl_drift/lang/java/resolution/constants.py +1619 -0
  58. package/adl_drift/lang/java/resolution/constructors.py +278 -0
  59. package/adl_drift/lang/java/resolution/context.py +305 -0
  60. package/adl_drift/lang/java/resolution/dispatch.py +540 -0
  61. package/adl_drift/lang/java/resolution/functional.py +1318 -0
  62. package/adl_drift/lang/java/resolution/generated.py +95 -0
  63. package/adl_drift/lang/java/resolution/model.py +153 -0
  64. package/adl_drift/lang/java/resolution/orchestration.py +866 -0
  65. package/adl_drift/lang/java/resolution/overloads.py +1275 -0
  66. package/adl_drift/lang/java/resolution/scopes.py +1111 -0
  67. package/adl_drift/lang/java/resolution/types.py +2947 -0
  68. package/adl_drift/lang/java/resolver.py +1401 -0
  69. package/adl_drift/lang/java/scopes.py +69 -0
  70. package/adl_drift/lang/java/taint.py +500 -0
  71. package/adl_drift/lang/java/type_provenance.py +342 -0
  72. package/adl_drift/lang/python/__init__.py +8 -0
  73. package/adl_drift/lang/python/backend.py +94 -0
  74. package/adl_drift/lang/python/cfg.py +537 -0
  75. package/adl_drift/lang/python/extractor.py +454 -0
  76. package/adl_drift/lang/python/parser.py +41 -0
  77. package/adl_drift/lang/python/resolver.py +1789 -0
  78. package/adl_drift/lang/python/taint.py +365 -0
  79. package/adl_drift/lang/registry.py +117 -0
  80. package/adl_drift/parser/__init__.py +1 -0
  81. package/adl_drift/parser/cache.py +38 -0
  82. package/adl_drift/parser/core.py +33 -0
  83. package/adl_drift/parser/extractor.py +17 -0
  84. package/adl_drift/parser/literal_eval.py +326 -0
  85. package/adl_drift/scanner/__init__.py +1 -0
  86. package/adl_drift/scanner/discovery.py +338 -0
  87. package/adl_drift/scanner/incremental.py +261 -0
  88. package/adl_drift/scanner/parallel.py +224 -0
  89. package/adl_drift/server/__init__.py +1 -0
  90. package/adl_drift/server/handlers.py +2849 -0
  91. package/adl_drift/server/mcp_server.py +127 -0
  92. package/adl_drift/server/mcp_stdio.py +335 -0
  93. package/adl_drift/server/protocol.py +71 -0
  94. package/adl_drift/server/static/index.html +696 -0
  95. package/adl_drift/server/web.py +350 -0
  96. package/adl_drift/utils/__init__.py +1 -0
  97. package/adl_drift/utils/config.py +77 -0
  98. package/adl_drift/utils/fswalk.py +52 -0
  99. package/adl_drift/utils/types.py +345 -0
  100. package/bin/adl-drift.mjs +35 -0
  101. package/bin/adl-toolkit.mjs +488 -0
  102. package/package.json +29 -4
  103. package/pyproject.toml +49 -0
  104. package/scripts/install_toolkit.py +3584 -0
@@ -0,0 +1,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, {}