adl-toolkit 0.0.0-stage → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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/adl-drift.md +25 -0
  36. package/adl_drift/harness/skills/drift-antigravity.md +19 -0
  37. package/adl_drift/harness/skills/drift-generic.md +19 -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,1275 @@
1
+ """Java overload selection: member lookup, applicability, and ranking."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections import deque
6
+ from typing import Callable, Dict, FrozenSet, List, Optional, Set, Tuple, Union
7
+
8
+ from adl_drift.graph.edges import EdgeType
9
+ from adl_drift.graph.node import NodeType
10
+ from adl_drift.lang.java.ids import erased_type
11
+ from adl_drift.lang.java.resolution.constants import (
12
+ JAVA_EXTERNAL_SUBTYPE_STUBS,
13
+ JAVA_EXTERNAL_TYPES,
14
+ JAVA_LANG_SIMPLE_TYPES,
15
+ _JAVA_FUNCTIONAL_INTERFACE_SHAPE,
16
+ _JAVA_FUNCTIONAL_INTERFACES,
17
+ _LAMBDA_ARG_TYPE,
18
+ _NULL_ARG_TYPE,
19
+ _UNKNOWN_ARG_TYPE,
20
+ argument_nodes,
21
+ )
22
+ from adl_drift.lang.java.resolution.model import (
23
+ ImportContext as _ImportContext,
24
+ LocalScopes as _LocalScopes,
25
+ )
26
+
27
+
28
+ class OverloadResolutionMixin:
29
+ def _lexical_outer_class_ids(self, class_id: str) -> List[str]:
30
+ """Nearest-to-farthest source classes lexically enclosing `class_id`."""
31
+ if ":" not in class_id:
32
+ return []
33
+ module, qualname = class_id.rsplit(":", 1)
34
+ parts = qualname.split(".")
35
+ out: List[str] = []
36
+ for end in range(len(parts) - 1, 0, -1):
37
+ candidate = f"{module}:{'.'.join(parts[:end])}"
38
+ node = self.builder._nodes.get(candidate)
39
+ if node is not None and node.node_type == NodeType.CLASS:
40
+ out.append(candidate)
41
+ return out
42
+
43
+ def _top_level_class_id(self, class_id: str) -> str:
44
+ """The outermost lexically-enclosing class id for `class_id`
45
+ (itself, if already top-level). JLS 6.6.1: a `private` member is
46
+ accessible anywhere within the body of the *top level* class that
47
+ encloses its declaration — not merely from its own literal
48
+ declaring class — so a nested class may freely call a `private`
49
+ method of its enclosing class (and vice versa, and sibling nested
50
+ classes of one another) as long as both share the same top-level
51
+ ancestor. `_lexical_outer_class_ids` is nearest-to-farthest, so the
52
+ last entry is the farthest (top-level) one."""
53
+ outers = self._lexical_outer_class_ids(class_id)
54
+ return outers[-1] if outers else class_id
55
+
56
+ def _contains_children(self, node_id: str) -> List[str]:
57
+ """Every node directly `CONTAINS`-ed by `node_id` (class members,
58
+ module classes, ...). Memoized per `node_id` (`_contains_children_cache`,
59
+ reset alongside `_declared_direct_ancestors`/`_class_ancestors_cache`
60
+ at the start of every resolution pass): every CONTAINS edge, including
61
+ generated member stubs, is fully established before call-site
62
+ resolution starts (`_build_resolution_indexes`) and never changes
63
+ mid-pass, yet this was called 480K times on JUnit5 with a fresh
64
+ NetworkX `out_edges` traversal every time — fix #5, same shape as
65
+ fix #4's `_direct_ancestors` (§8 bước 7 profiling: this was the
66
+ largest remaining self-time entry after fix #3+#4 landed)."""
67
+ cached = self._contains_children_cache.get(node_id)
68
+ if cached is not None:
69
+ return cached
70
+ if node_id not in self.builder.graph:
71
+ result: List[str] = []
72
+ else:
73
+ result = [
74
+ t for _, t, d in self.builder.graph.out_edges(node_id, data=True)
75
+ if d.get("type") == EdgeType.CONTAINS.name
76
+ ]
77
+ self._contains_children_cache[node_id] = result
78
+ return result
79
+
80
+ def _declaring_class_id(self, member_id: str) -> Optional[str]:
81
+ """The fully-qualified id of `member_id`'s immediate declaring
82
+ class, via the reverse CONTAINS edge `graph/builder.py` wires from
83
+ `Symbol.scope_chain` (`f"{module}:{'.'.join(chain)}"`).
84
+
85
+ Unlike `GraphNode.contained_in` — which `extractor.py::
86
+ _find_parent_class` deliberately keeps as the coarser *nearest
87
+ enclosing simple name* ("Box"), by its own docstring a separate,
88
+ coarser field than the scope-chain uniqueness key — this is
89
+ qualified even for a nested class ("Outer.Box"), so it actually
90
+ matches how `_class_type_params`/INHERITS/IMPLEMENTS class ids are
91
+ keyed (both built off the same scope-chain-qualified node ids).
92
+ A bare `f"{module}:{contained_in}"` lookup silently misses for any
93
+ nested class member (F-01)."""
94
+ cache = self._declaring_class_cache
95
+ if cache is not None and member_id in cache:
96
+ return cache[member_id]
97
+ result = self._declaring_class_id_uncached(member_id)
98
+ if cache is not None:
99
+ cache[member_id] = result
100
+ return result
101
+
102
+ def _declaring_class_id_uncached(self, member_id: str) -> Optional[str]:
103
+ if member_id not in self.builder.graph:
104
+ return None
105
+ for source, _, data in self.builder.graph.in_edges(member_id, data=True):
106
+ if data.get("type") != EdgeType.CONTAINS.name:
107
+ continue
108
+ source_node = self.builder._nodes.get(source)
109
+ if source_node is not None and source_node.node_type == NodeType.CLASS:
110
+ return source
111
+ # Methods inside an anonymous class have a synthetic scope segment
112
+ # (`Outer.$1.method()`), but no standalone CLASS node for `$1`, so the
113
+ # shared builder cannot attach a CONTAINS edge to that immediate
114
+ # scope. Recover the nearest real lexical type from the qualified
115
+ # member id. This is also what Java uses to resolve an unqualified
116
+ # nested type such as `Keys` inside an enum-constant body.
117
+ member = self.builder._nodes.get(member_id)
118
+ if member is None:
119
+ return None
120
+ prefix = f"{member.module}:"
121
+ if not member_id.startswith(prefix) or "(" not in member_id:
122
+ return None
123
+ method_head = member_id[len(prefix):].rsplit("(", 1)[0]
124
+ if "." not in method_head:
125
+ return None
126
+ owner = method_head.rsplit(".", 1)[0]
127
+ while owner:
128
+ candidate = f"{member.module}:{owner}"
129
+ node = self.builder._nodes.get(candidate)
130
+ if node is not None and node.node_type == NodeType.CLASS:
131
+ return candidate
132
+ if "." not in owner:
133
+ break
134
+ owner = owner.rsplit(".", 1)[0]
135
+ return None
136
+
137
+ def _direct_ancestors(self, class_id: str, edge_type: EdgeType) -> List[str]:
138
+ # `_declared_direct_ancestors` is fully populated before any call site
139
+ # resolution runs (orchestration.py's `_build_resolution_indexes`), so
140
+ # checking it first never changes which branch below is taken — it
141
+ # only skips the expensive NetworkX out_edges traversal on every one
142
+ # of the millions of calls this makes where the cache already has the
143
+ # answer (P0 profiling: this line was ~22% of JUnit5 resolve_calls
144
+ # time before the reorder, almost entirely wasted work on cache hits).
145
+ key = (class_id, edge_type)
146
+ if key in self._declared_direct_ancestors:
147
+ return self._declared_direct_ancestors[key]
148
+ # Graph-only fallback supports partial/incremental graphs whose
149
+ # declaring AnalysisResult was unavailable to this resolver pass —
150
+ # in practice this is every external/JDK class_id, since only
151
+ # project source classes get an entry above. Without a supporting
152
+ # AnalysisResult these edges cannot change mid-pass, so the result is
153
+ # written back into the same dict once computed: bottleneck #4
154
+ # profiling (JAVA_PERFORMANCE_OPTIMIZATION_PLAN.md §8 bước 7) found
155
+ # ~79% of all _direct_ancestors calls on JUnit5 missed the cache and
156
+ # re-walked NetworkX's out_edges for the same handful of common
157
+ # external ancestors (Object, Comparable, ...) millions of times.
158
+ if class_id not in self.builder.graph:
159
+ result: List[str] = []
160
+ else:
161
+ edge_type_name = edge_type.name
162
+ result = [
163
+ t for _, t, d in self.builder.graph.out_edges(class_id, data=True)
164
+ if d.get("type") == edge_type_name
165
+ ]
166
+ self._declared_direct_ancestors[key] = result
167
+ return result
168
+
169
+ def _class_ancestors(self, class_id: str) -> List[str]:
170
+ """BFS over INHERITS ∪ IMPLEMENTS, nearest first — a class's own
171
+ superclass and interfaces before their own ancestors in turn.
172
+
173
+ Memoized per class_id (`_class_ancestors_cache`, reset alongside
174
+ `_declared_direct_ancestors` at the start of every resolution pass in
175
+ `_build_resolution_indexes`): the INHERITS/IMPLEMENTS edges this BFS
176
+ walks are fully established before call-site resolution starts and
177
+ never change mid-pass, yet this was called 748K times on JUnit5 for a
178
+ graph of only ~3.7K classes — the same class's ancestor chain
179
+ recomputed from scratch on almost every call (§8 bước 7 profiling).
180
+ """
181
+ cached = self._class_ancestors_cache.get(class_id)
182
+ if cached is not None:
183
+ return cached
184
+ seen: Set[str] = {class_id}
185
+ q: deque = deque(
186
+ self._direct_ancestors(class_id, EdgeType.INHERITS)
187
+ + self._direct_ancestors(class_id, EdgeType.IMPLEMENTS)
188
+ )
189
+ out: List[str] = []
190
+ while q:
191
+ cur = q.popleft()
192
+ if cur in seen:
193
+ continue
194
+ seen.add(cur)
195
+ out.append(cur)
196
+ q.extend(self._direct_ancestors(cur, EdgeType.INHERITS))
197
+ q.extend(self._direct_ancestors(cur, EdgeType.IMPLEMENTS))
198
+ self._class_ancestors_cache[class_id] = out
199
+ return out
200
+
201
+ def _all_field_types(self, class_id: str) -> Dict[str, str]:
202
+ """`{field_name: declared_type_text}` for `class_id`, merged with
203
+ every INHERITS/IMPLEMENTS ancestor's fields not shadowed by a
204
+ closer declaration — the field analogue of `_members_named`'s
205
+ own+ancestor merge, needed because `self._field_types` is keyed per
206
+ declaring class only (built once in `resolve_calls()` from each
207
+ field's own `contained_in`), so a field declared on an ancestor was
208
+ otherwise invisible to a subclass's `this.field.method()` chain."""
209
+ def declared_fields(owner_id: str) -> Dict[str, str]:
210
+ fields = dict(self._field_types.get(owner_id, {}))
211
+ owner = self.builder._nodes.get(owner_id)
212
+ if owner is not None:
213
+ # A record component declares a private final field even
214
+ # though there is no `field_declaration` CST node for the
215
+ # extractor to emit. Treat compiler-mandated component
216
+ # fields exactly like explicit fields for bare/`this` and
217
+ # pattern-variable receiver chains.
218
+ fields.update(dict(owner.metadata.get("record_components", {})))
219
+ return fields
220
+
221
+ merged: Dict[str, str] = {}
222
+ for anc in reversed(self._class_ancestors(class_id)):
223
+ fields = declared_fields(anc)
224
+ # A field declared as `T ctx` on `Base<T>` is a `ClassCtx` on
225
+ # `Sub extends Base<ClassCtx>` (JLS 8.1.4 / 4.5): substitute the
226
+ # ancestor's class variables through the inheritance edge the
227
+ # same way method return types already are.
228
+ bindings = self._compose_type_var_bindings(class_id, [], anc)
229
+ if bindings:
230
+ fields = {
231
+ name: self._replace_type_variables(text, bindings)
232
+ for name, text in fields.items()
233
+ }
234
+ merged.update(fields)
235
+ merged.update(declared_fields(class_id))
236
+ return merged
237
+
238
+ def _visible_bare_field_types(self, class_id: str) -> Dict[str, str]:
239
+ """Fields visible to an unqualified name inside ``class_id``.
240
+
241
+ A non-static nested/local class may refer to fields of its lexical
242
+ enclosing instances without an explicit ``Outer.this`` qualifier.
243
+ Merge farthest-to-nearest lexical owners, then the current class, so
244
+ normal Java shadowing keeps the closest declaration. Explicit
245
+ ``this.field`` resolution deliberately continues to use
246
+ ``_all_field_types(class_id)`` and therefore cannot accidentally bind
247
+ an outer-instance field.
248
+ """
249
+ current = self.builder._nodes.get(class_id)
250
+ if current is not None and "static" in current.metadata.get("modifiers", ()):
251
+ return self._all_field_types(class_id)
252
+ accessible_outers: List[str] = []
253
+ for outer_id in self._lexical_outer_class_ids(class_id):
254
+ accessible_outers.append(outer_id)
255
+ outer = self.builder._nodes.get(outer_id)
256
+ if outer is not None and "static" in outer.metadata.get("modifiers", ()):
257
+ break
258
+ merged: Dict[str, str] = {}
259
+ for outer_id in reversed(accessible_outers):
260
+ merged.update(self._all_field_types(outer_id))
261
+ merged.update(self._all_field_types(class_id))
262
+ return merged
263
+
264
+ def _erased_signature(self, func_id: str) -> Tuple[str, ...]:
265
+ """Erased positional parameter-type tuple, used to tell a true
266
+ override (same name + same erased signature) apart from an overload
267
+ (same name, different signature) when merging inherited members.
268
+
269
+ Memoized per `func_id` (`_erased_signature_cache`, reset alongside
270
+ the other per-run caches in `_build_resolution_indexes`): purely a
271
+ function of `_param_type_lists[func_id]`, which is fixed for the
272
+ whole resolution pass, yet `_members_named` recomputes it for every
273
+ candidate on every lookup — a large overload family (e.g. a
274
+ project's own `assertEquals`-shaped API with dozens of siblings)
275
+ gets looked up at every call site sharing that name, so the same
276
+ handful of func_ids' signatures were being re-erased tens of
277
+ thousands of times before this cache."""
278
+ cache = getattr(self, "_erased_signature_cache", None)
279
+ if cache is not None:
280
+ cached = cache.get(func_id)
281
+ if cached is not None:
282
+ return cached
283
+ sig = tuple(erased_type(t) for t in self._param_type_lists.get(func_id, []))
284
+ if cache is not None:
285
+ cache[func_id] = sig
286
+ return sig
287
+
288
+ def _members_named(
289
+ self, class_id: str, name: str, search_ancestors: bool, search_self: bool = True,
290
+ exclude_constructors: bool = False, caller_class_id: Optional[str] = None,
291
+ ) -> List[str]:
292
+ """Every FUNCTION child of `class_id` named `name`, plus — iff
293
+ `search_ancestors` — every same-named overload across the *entire*
294
+ INHERITS/IMPLEMENTS ancestor chain, not just the nearest ancestor
295
+ tier that declares one: a name declared two-or-more levels up
296
+ (`Child extends Parent extends GrandParent`) stays a candidate as
297
+ long as no *closer* class (own class, or a nearer ancestor already
298
+ walked) overrides it with an identical erased signature — that's a
299
+ true override and hides it; a differently-signed overload does not,
300
+ and both must stay candidates for `_select_overload`. When
301
+ `search_self` is False, the own class is never consulted, not even
302
+ for override exclusion — this is `super.m()`'s path, which must
303
+ always reach an ancestor's method regardless of the child's own
304
+ override.
305
+
306
+ A Java constructor's node is named after its class's own simple
307
+ name, so a plain `method_invocation` lookup for a method that
308
+ happens to share the class's name (e.g. implementing an interface
309
+ method named `Foo` inside a class also named `Foo`) would otherwise
310
+ be indistinguishable from the constructor. `exclude_constructors`
311
+ drops any candidate flagged `metadata["constructor"]`; callers that
312
+ deliberately look up a class's own name to find its constructor(s)
313
+ (`new T(...)`, `this(...)`, `super(...)`) leave it False."""
314
+ def is_visible(candidate_id: str) -> bool:
315
+ if caller_class_id is None:
316
+ return True
317
+ node = self.builder._nodes.get(candidate_id)
318
+ if node is None or "private" not in node.metadata.get("modifiers", ()):
319
+ return True
320
+ declaring = self._declaring_class_id(candidate_id)
321
+ if declaring is None:
322
+ return True
323
+ return self._top_level_class_id(declaring) == self._top_level_class_id(
324
+ caller_class_id,
325
+ )
326
+
327
+ own = [
328
+ c for c in self._function_members_named(class_id, name)
329
+ if not (exclude_constructors and self.builder._nodes[c].metadata.get("constructor"))
330
+ and is_visible(c)
331
+ ]
332
+ if not search_ancestors:
333
+ return own if search_self else []
334
+ collected: List[str] = list(own) if search_self else []
335
+ shadowed = {self._erased_signature(c) for c in collected}
336
+ for anc in self._class_ancestors(class_id):
337
+ for c in self._function_members_named(anc, name):
338
+ n = self.builder._nodes[c]
339
+ if exclude_constructors and n.metadata.get("constructor"):
340
+ continue
341
+ if not is_visible(c):
342
+ continue
343
+ sig = self._erased_signature(c)
344
+ if sig in shadowed:
345
+ continue
346
+ collected.append(c)
347
+ shadowed.add(sig)
348
+ return collected
349
+
350
+ def _function_members_named(self, class_id: str, name: str) -> List[str]:
351
+ """FUNCTION children of `class_id` named `name`, in `_contains_children`
352
+ order. Built once per class from `_contains_children` (same source of
353
+ truth `_members_named` scanned linearly on every call) and memoized in
354
+ `_members_by_name_cache`, reset with the other per-run caches."""
355
+ cache = self._members_by_name_cache
356
+ index = cache.get(class_id) if cache is not None else None
357
+ if index is None:
358
+ index = {}
359
+ for c in self._contains_children(class_id):
360
+ n = self.builder._nodes.get(c)
361
+ if n is not None and n.node_type == NodeType.FUNCTION:
362
+ index.setdefault(n.name, []).append(c)
363
+ if cache is not None:
364
+ cache[class_id] = index
365
+ return index.get(name, ())
366
+
367
+ def _arity_matches(self, func_id: str, arg_count: int) -> bool:
368
+ types = self._param_type_lists.get(func_id, [])
369
+ if len(types) == arg_count:
370
+ return True
371
+ if types and self._is_vararg_param(types[-1]):
372
+ # varargs-shaped last param accepts >= the fixed-arity prefix.
373
+ return arg_count >= len(types) - 1
374
+ return False
375
+
376
+ def _is_vararg_candidate(self, func_id: str) -> bool:
377
+ """True iff `func_id`'s *last* declared parameter is a true vararg
378
+ (`T...`), i.e. this candidate can only be reached in JLS's phase-3
379
+ "variable arity invocation" — never in phase 1/2 fixed-arity form
380
+ except the single "array form" case `_param_types_match`/
381
+ `_param_types_compatible` already special-case. Used by
382
+ `_select_overload` to keep phase-3-only candidates from competing
383
+ with a phase-1/2-applicable fixed-arity sibling — see there."""
384
+ raw_types = self._param_type_lists.get(func_id, [])
385
+ return bool(raw_types) and self._is_vararg_param(raw_types[-1])
386
+
387
+ def _is_phase3_only(
388
+ self, func_id: str, arg_types: List[str], arg_ctx: "_ImportContext",
389
+ ) -> bool:
390
+ """True iff, *for this particular call*, `func_id` is reachable only
391
+ through JLS §15.12.2 phase 3 (variable-arity, element-wise vararg
392
+ expansion) — and so must not compete with a phase-1/2-applicable
393
+ sibling. Unlike `_is_vararg_candidate`, which classifies purely by
394
+ declaration shape (`T...`), this is invocation-form-aware: a
395
+ vararg-declaring method still has a phase-1/2 *array form* (JLS
396
+ §15.12.2.1) when the caller passes exactly the declared arity with a
397
+ final argument assignable to the vararg parameter's array type
398
+ (`m(String[] arr)` against `m(String...)`), and in that form it is a
399
+ legitimate phase-1/2 candidate, not a phase-3-only one.
400
+
401
+ A fixed-arity candidate is never phase-3-only. A vararg candidate is
402
+ phase-3-only unless the call matches its array form — i.e. the arg
403
+ count differs from the declared parameter count (element-wise
404
+ expansion is the only fit), or the single trailing argument is not
405
+ `_types_compatible` with the vararg array type (`save(1)` against
406
+ `save(int...)`: `int` is not assignable to `int[]`, so array form
407
+ fails and this stays phase 3, preserving the P4.4 regression fix that
408
+ lets a `save(long)` sibling win). The array-form gate mirrors the
409
+ array-form branch `_param_types_match`/`_param_types_compatible`
410
+ already special-case, using the loosest phase (`allow_boxing=True`)
411
+ so classification never *under*-includes a candidate those two
412
+ methods would still accept in a tier."""
413
+ if not self._is_vararg_candidate(func_id):
414
+ return False
415
+ types = [erased_type(t) for t in self._param_type_lists.get(func_id, [])]
416
+ if len(arg_types) != len(types):
417
+ return True
418
+ param_ctx = self._param_ctx_for(func_id)
419
+ vararg_type = types[-1]
420
+ trailing = arg_types[len(types) - 1:]
421
+ if len(trailing) == 1 and self._types_compatible(
422
+ vararg_type, trailing[0], param_ctx, arg_ctx, allow_boxing=True,
423
+ ):
424
+ return False # array form: a phase-1/2 candidate for this call.
425
+ return True
426
+
427
+ @staticmethod
428
+ def _is_vararg_param(raw_type_text: str) -> bool:
429
+ """True iff the *source* declaration used `T...`, not a plain `T[]`.
430
+
431
+ Must check the raw (pre-erasure) text: `erased_type()` normalizes
432
+ `...` to `[]` for type-identity purposes, which would otherwise make
433
+ a plain array-typed last parameter (`Object[] rows`, exactly one
434
+ argument required) indistinguishable from true varargs
435
+ (`Object... rows`, zero-or-more arguments) — the two accept
436
+ different call-site arities and must not be conflated here.
437
+ """
438
+ return raw_type_text.strip().endswith("...")
439
+
440
+ # JLS 5.1.2 widening primitive conversion: a value of the key type may be
441
+ # passed where any type in the value set is declared.
442
+ _PRIMITIVE_WIDENING: Dict[str, FrozenSet[str]] = {
443
+ "byte": frozenset({"short", "int", "long", "float", "double"}),
444
+ "short": frozenset({"int", "long", "float", "double"}),
445
+ "char": frozenset({"int", "long", "float", "double"}),
446
+ "int": frozenset({"long", "float", "double"}),
447
+ "long": frozenset({"float", "double"}),
448
+ "float": frozenset({"double"}),
449
+ }
450
+ # JLS 5.1.7/5.1.8 boxing/unboxing conversion, checked in both directions.
451
+ _BOXED_PRIMITIVE: Dict[str, str] = {
452
+ "int": "Integer", "long": "Long", "double": "Double", "float": "Float",
453
+ "boolean": "Boolean", "char": "Character", "byte": "Byte", "short": "Short",
454
+ }
455
+ # Reverse of `_BOXED_PRIMITIVE` (wrapper -> primitive), used to unbox an
456
+ # arg type before checking primitive widening — see `_types_compatible`.
457
+ _UNBOXED_PRIMITIVE: Dict[str, str] = {v: k for k, v in _BOXED_PRIMITIVE.items()}
458
+
459
+ @staticmethod
460
+ def _external_type_name(type_name: str, ctx: "_ImportContext") -> Optional[str]:
461
+ """Exact FQN of an external (JDK) type spelled in `ctx`, or None.
462
+
463
+ An explicit import, a wildcard import matching an exact-FQN hierarchy
464
+ fact, and the implicit `java.lang` names qualify; an already dotted
465
+ spelling is kept as written. A name that resolves to nothing here is
466
+ *not* guessed."""
467
+ wildcard_matches = {
468
+ f"{package}.{type_name}" for package in ctx.wildcard_packages
469
+ if f"{package}.{type_name}" in JAVA_EXTERNAL_TYPES
470
+ }
471
+ if len(wildcard_matches) == 1:
472
+ return next(iter(wildcard_matches))
473
+ if "." in type_name:
474
+ head, tail = type_name.split(".", 1)
475
+ imported_head = ctx.import_map.get(head)
476
+ if imported_head is not None:
477
+ return f"{imported_head}.{tail}"
478
+ return type_name
479
+ imported = ctx.import_map.get(type_name)
480
+ if imported is not None:
481
+ return imported
482
+ if "java.util" in ctx.wildcard_packages and type_name in {
483
+ "ArrayList", "Collection", "HashMap", "HashSet", "List", "Map", "Set", "TreeMap",
484
+ }:
485
+ return f"java.util.{type_name}"
486
+ if type_name in JAVA_LANG_SIMPLE_TYPES:
487
+ return f"java.lang.{type_name}"
488
+ return None
489
+
490
+ def _types_compatible(
491
+ self, param_type: str, arg_type: str,
492
+ param_ctx: "_ImportContext", arg_ctx: "_ImportContext",
493
+ allow_boxing: bool = True,
494
+ ) -> bool:
495
+ """True iff a value statically typed `arg_type` (erased, per
496
+ `_infer_arg_types`) may be passed where `param_type` (also erased)
497
+ is declared: exact match, `Object` (every reference/boxed-primitive
498
+ type is Object-compatible), JLS 5.1.2 primitive widening, JLS
499
+ 5.1.7/5.1.8 boxing/unboxing, or — for two types that resolve to
500
+ in-repo CLASS nodes — an INHERITS/IMPLEMENTS subtype relationship
501
+ (a subclass argument passed where a superclass/interface parameter
502
+ is declared, the single most common source of "known type, but not
503
+ an exact match" in real Java). Reference arrays follow JLS widening
504
+ reference conversion component-wise (`String[]` -> `Object[]`, and
505
+ source `Child[]` -> `Parent[]`). Array-ness must agree before that
506
+ recursion; primitive array components never box or widen into a
507
+ different component type (`int[]` is not `Object[]` or `long[]`).
508
+
509
+ `param_type` and `arg_type` are resolved against *separate*
510
+ contexts on purpose: `param_type` was written in the candidate
511
+ method's own declaring file and must resolve against that file's
512
+ imports/wildcard-imports (`param_ctx`), while `arg_type` was
513
+ written at the call site and resolves against the caller's
514
+ (`arg_ctx`) — using the caller's context for both, or dropping
515
+ wildcard-imported packages entirely, silently fails to resolve a
516
+ wildcard-imported or cross-file-imported type and drops an
517
+ otherwise-valid candidate (P4.4 regression fix).
518
+
519
+ `allow_boxing` splits JLS §15.12.2's phases: with it False (phase 1,
520
+ strict invocation) only identity + primitive widening + reference
521
+ widening/subtyping apply — *no* boxing/unboxing; with it True (phase
522
+ 2, loose invocation) boxing/unboxing is additionally allowed.
523
+ `_select_overload` tries the strict phase across all candidates before
524
+ the loose one, so an `int` argument picks a `save(long)` (widening,
525
+ phase 1) over a `save(Integer)` (boxing, phase 2) sibling the way
526
+ real javac does, instead of fanning out to both.
527
+
528
+ The `null` literal (`_NULL_ARG_TYPE`) is compatible with every
529
+ *reference* parameter type (class, interface, array, boxed
530
+ primitive) and no primitive — independent of `allow_boxing`, since
531
+ `null` -> reference is a widening reference conversion (phase 1).
532
+
533
+ JLS 5.1.8 unboxing, optionally followed by 5.1.2 widening, is also
534
+ modeled: a wrapper arg (`Integer`) unboxes to its primitive
535
+ (`int`) and, if that alone doesn't match `param_type`, is checked
536
+ against `_PRIMITIVE_WIDENING` from there — `Integer` -> `long`
537
+ (F-05 fix). The reverse chain (box, then widen as a *primitive*) is
538
+ not attempted because JLS doesn't allow it: once boxed, only
539
+ widening *reference* conversion applies, which the
540
+ `_resolve_declared_type`/`_class_ancestors` path below already
541
+ covers.
542
+
543
+ Still not modeled (documented simplification): external/JDK
544
+ supertypes (`Comparable` param + `String` arg needs a JDK hierarchy
545
+ this issue doesn't fabricate). A pair neither rule proves compatible
546
+ is treated as a real mismatch,
547
+ not an unknown — the caller drops the candidate. When more than one
548
+ candidate is compatible, `_select_overload` calls `_most_specific`
549
+ to rank them (JLS §15.12.2.5) before falling back to a fan-out —
550
+ see that method."""
551
+ if arg_type == _NULL_ARG_TYPE:
552
+ # null type: assignable to any reference (anything not a
553
+ # primitive), never to a primitive parameter.
554
+ return param_type not in self._BOXED_PRIMITIVE
555
+ if arg_type.startswith(_LAMBDA_ARG_TYPE):
556
+ simple = erased_type(param_type).split(".")[-1]
557
+ if simple in _JAVA_FUNCTIONAL_INTERFACES:
558
+ shape = _JAVA_FUNCTIONAL_INTERFACE_SHAPE.get(simple)
559
+ lambda_shape = arg_type[len(_LAMBDA_ARG_TYPE) + 1:] if (
560
+ len(arg_type) > len(_LAMBDA_ARG_TYPE)
561
+ ) else None
562
+ if shape is not None and lambda_shape is not None:
563
+ count_text, _, value_shape = lambda_shape.partition(":")
564
+ if count_text.isdigit() and int(count_text) != shape[0]:
565
+ # JLS 15.27.3: a lambda's declared parameter count
566
+ # must exactly equal its target SAM's arity — not
567
+ # merely "compatible" the way widening/boxing works
568
+ # for an ordinary value argument.
569
+ return False
570
+ if value_shape == "value" and shape[1]:
571
+ # A block body proven to contain a value `return` is
572
+ # not void-compatible (JLS 15.27.2): it cannot
573
+ # target a void-returning SAM like Runnable/Consumer.
574
+ return False
575
+ return True
576
+ # Project-declared SAM interfaces are valid lambda targets too
577
+ # (`Converter<T>` in Commons CLI). Resolve strictly in the
578
+ # candidate parameter's declaring context and require exactly
579
+ # one abstract instance-method signature across its hierarchy.
580
+ param_id = self._resolve_declared_type(
581
+ param_type, param_ctx.module, param_ctx.import_map,
582
+ param_ctx.wildcard_packages, param_ctx.enclosing_class_id,
583
+ )
584
+ if param_id is None:
585
+ return False
586
+ param_node = self.builder._nodes.get(param_id)
587
+ if param_node is None or param_node.metadata.get("java_type_kind") != "interface":
588
+ return False
589
+ abstract_signatures: Set[Tuple[str, str]] = set()
590
+ for owner_id in (param_id, *self._class_ancestors(param_id)):
591
+ if owner_id not in self.builder.graph:
592
+ continue
593
+ for _, member_id, data in self.builder.graph.out_edges(owner_id, data=True):
594
+ if data.get("type") != EdgeType.CONTAINS.name:
595
+ continue
596
+ member = self.builder._nodes.get(member_id)
597
+ if (
598
+ member is not None
599
+ and member.node_type == NodeType.FUNCTION
600
+ and member.metadata.get("java_is_abstract")
601
+ and not member.metadata.get("java_is_static")
602
+ ):
603
+ abstract_signatures.add((member.name, member.signature or ""))
604
+ if len(abstract_signatures) == 1:
605
+ return True
606
+ if abstract_signatures:
607
+ return False
608
+ # `interface TimeoutProvider extends Function<Config, R> {}`
609
+ # declares nothing itself; it is functional exactly when its
610
+ # single external super-interface is a JDK functional one.
611
+ return self._external_functional_super(param_id) is not None
612
+ if param_type == arg_type:
613
+ return True
614
+ arg_is_primitive = arg_type in self._BOXED_PRIMITIVE
615
+ if param_type == "Object":
616
+ # `String` -> `Object` is a widening reference conversion
617
+ # (phase 1); `int` -> `Object` needs boxing first (phase 2).
618
+ return (not arg_is_primitive) or allow_boxing
619
+ param_is_array = param_type.endswith("[]")
620
+ arg_is_array = arg_type.endswith("[]")
621
+ if param_is_array or arg_is_array:
622
+ if not (param_is_array and arg_is_array):
623
+ return False
624
+ param_component = param_type[:-2]
625
+ arg_component = arg_type[:-2]
626
+ primitive_types = set(self._BOXED_PRIMITIVE)
627
+ if param_component in primitive_types or arg_component in primitive_types:
628
+ return param_component == arg_component
629
+ return self._types_compatible(
630
+ param_component, arg_component, param_ctx, arg_ctx,
631
+ allow_boxing=False,
632
+ )
633
+ if param_type in self._PRIMITIVE_WIDENING.get(arg_type, ()):
634
+ return True
635
+ if allow_boxing:
636
+ boxed_arg = self._BOXED_PRIMITIVE.get(arg_type)
637
+ boxed_param = self._BOXED_PRIMITIVE.get(param_type)
638
+ if boxed_arg == param_type or boxed_param == arg_type:
639
+ return True
640
+ unboxed_arg = self._UNBOXED_PRIMITIVE.get(arg_type)
641
+ if unboxed_arg is not None and param_type in self._PRIMITIVE_WIDENING.get(unboxed_arg, ()):
642
+ return True # unboxing (5.1.8) then widening (5.1.2): Integer -> int -> long.
643
+ external_name = self._external_type_name
644
+
645
+ param_external = external_name(param_type, param_ctx)
646
+ arg_external = external_name(arg_type, arg_ctx)
647
+ if param_external is not None and param_external == arg_external:
648
+ return True
649
+ if (
650
+ param_external is not None
651
+ and arg_external is not None
652
+ and (arg_external, param_external) in JAVA_EXTERNAL_SUBTYPE_STUBS
653
+ ):
654
+ return True
655
+ param_id = self._resolve_declared_type(
656
+ param_type, param_ctx.module, param_ctx.import_map, param_ctx.wildcard_packages,
657
+ param_ctx.enclosing_class_id,
658
+ )
659
+ arg_id = self._resolve_declared_type(
660
+ arg_type, arg_ctx.module, arg_ctx.import_map, arg_ctx.wildcard_packages,
661
+ arg_ctx.enclosing_class_id,
662
+ )
663
+ if param_id is not None and arg_id is not None:
664
+ return param_id == arg_id or param_id in self._class_ancestors(arg_id)
665
+ if param_external is not None and arg_id is not None:
666
+ # A source class reaches an external supertype through its own
667
+ # `extends` clause or through any source ancestor's: the exact
668
+ # JDK facts apply to the nearest external superclass on that
669
+ # chain (`NoTestsDiscoveredException extends JUnitException
670
+ # extends RuntimeException` may be passed as `Throwable`). Each
671
+ # `extends` spelling resolves in its own declaring file.
672
+ for owner_id in (arg_id, *self._class_ancestors(arg_id)):
673
+ owner_node = self.builder._nodes.get(owner_id)
674
+ owner_ctx = (
675
+ self._file_ctx.get(owner_node.file_path)
676
+ if owner_node is not None else None
677
+ )
678
+ if owner_node is None or owner_ctx is None:
679
+ continue
680
+ declared_supers = (
681
+ *owner_node.metadata.get("java_superclass_args", ()),
682
+ *owner_node.metadata.get("java_implements_args", ()),
683
+ )
684
+ for declared_super in declared_supers:
685
+ super_name = external_name(erased_type(declared_super), owner_ctx)
686
+ if super_name is None:
687
+ continue
688
+ if (
689
+ super_name == param_external
690
+ or (super_name, param_external) in JAVA_EXTERNAL_SUBTYPE_STUBS
691
+ ):
692
+ return True
693
+ return False
694
+
695
+ def _external_functional_super(self, interface_id: str) -> Optional[Tuple[str, str]]:
696
+ """`(owner_id, extends_text)` when `interface_id` (or a source ancestor)
697
+ extends exactly one external JDK functional interface, else None.
698
+
699
+ The extends clause is resolved in its own declaring file: the name
700
+ must not be a source class there, and its exact FQN must be the JDK
701
+ one (`java.util.function.Function`, `java.lang.Runnable`, ...). A
702
+ project interface that declares its own abstract method is handled
703
+ by the caller before this is consulted."""
704
+ found: List[Tuple[str, str]] = []
705
+ for owner_id in (interface_id, *self._class_ancestors(interface_id)):
706
+ owner_node = self.builder._nodes.get(owner_id)
707
+ owner_ctx = (
708
+ self._file_ctx.get(owner_node.file_path)
709
+ if owner_node is not None else None
710
+ )
711
+ if owner_node is None or owner_ctx is None:
712
+ continue
713
+ if owner_node.metadata.get("java_type_kind") != "interface":
714
+ continue
715
+ for declared_super in owner_node.metadata.get("java_superclass_args", ()):
716
+ raw = erased_type(declared_super).strip()
717
+ if raw.rsplit(".", 1)[-1] not in _JAVA_FUNCTIONAL_INTERFACES:
718
+ continue
719
+ if self._resolve_declared_type(
720
+ raw, owner_ctx.module, owner_ctx.import_map,
721
+ owner_ctx.wildcard_packages,
722
+ ) is not None:
723
+ continue
724
+ external = self._external_type_name(raw, owner_ctx)
725
+ if external is None or not external.startswith("java."):
726
+ continue
727
+ found.append((owner_id, declared_super))
728
+ return found[0] if len(found) == 1 else None
729
+
730
+ def _narrower_or_equal(
731
+ self, a_type: str, b_type: str,
732
+ a_ctx: "_ImportContext", b_ctx: "_ImportContext",
733
+ ) -> bool:
734
+ """True iff every value accepted where `a_type` is declared is also
735
+ accepted where `b_type` is declared — i.e. `a_type` is at least as
736
+ specific as `b_type`. Used only to rank *pairs of already-compatible
737
+ candidates'* declared parameter types against each other (see
738
+ `_most_specific`), not to re-derive compatibility with a concrete
739
+ argument — that is `_types_compatible`'s job. Primitive widening
740
+ via the existing `_PRIMITIVE_WIDENING` lattice; reference types via
741
+ an INHERITS/IMPLEMENTS subtype check, each side resolved against
742
+ its *own* declaring context (`a_ctx`/`b_ctx` — two overloads being
743
+ ranked against each other may be declared in two different files,
744
+ same reasoning as `_types_compatible`'s `param_ctx`/`arg_ctx`
745
+ split). Array types and primitive/reference mixed pairs are
746
+ deliberately not ordered (same "not modeled" boundary
747
+ `_types_compatible` documents for boxing/unboxing chains) — an
748
+ unordered pair returns `False` in both directions, which only
749
+ costs a tie-break (both stay fan-out candidates), never a wrong
750
+ pick."""
751
+ if a_type == b_type:
752
+ return True
753
+ if a_type.endswith("[]") or b_type.endswith("[]"):
754
+ return False
755
+ a_is_prim = a_type in self._BOXED_PRIMITIVE
756
+ b_is_prim = b_type in self._BOXED_PRIMITIVE
757
+ if a_is_prim and b_is_prim:
758
+ return b_type in self._PRIMITIVE_WIDENING.get(a_type, ())
759
+ if a_is_prim or b_is_prim:
760
+ return False
761
+ if b_type == "Object":
762
+ # Mirrors `_types_compatible`'s own `Object`-is-universal-
763
+ # supertype shortcut: `Object` is a JDK type this resolver
764
+ # never fabricates a CLASS node for, so `_resolve_declared_type`
765
+ # below would always return `None` for it and this domination
766
+ # would incorrectly read as "unorderable" instead of "any
767
+ # reference type is narrower than Object".
768
+ return True
769
+ a_id = self._resolve_declared_type(
770
+ a_type, a_ctx.module, a_ctx.import_map, a_ctx.wildcard_packages,
771
+ )
772
+ b_id = self._resolve_declared_type(
773
+ b_type, b_ctx.module, b_ctx.import_map, b_ctx.wildcard_packages,
774
+ )
775
+ if a_id is not None and b_id is not None:
776
+ return a_id == b_id or b_id in self._class_ancestors(a_id)
777
+ if a_id is None and b_id is None:
778
+ # Two external JDK parameter types are ordered by the same
779
+ # exact-FQN facts `_types_compatible` accepts, so a nested
780
+ # source return canonicalized to `java.util.Set` still prefers
781
+ # `m(Set)` over `m(Collection)`.
782
+ a_external = self._external_type_name(a_type, a_ctx)
783
+ b_external = self._external_type_name(b_type, b_ctx)
784
+ if a_external is not None and b_external is not None:
785
+ return (
786
+ a_external == b_external
787
+ or (a_external, b_external) in JAVA_EXTERNAL_SUBTYPE_STUBS
788
+ )
789
+ return False
790
+
791
+ def _dominates(self, c1: str, c2: str) -> bool:
792
+ """True iff `c1` is strictly more specific than `c2` — every one of
793
+ `c1`'s declared parameter types is narrower-or-equal to `c2`'s at
794
+ the same position, and at least one position is strictly narrower
795
+ (not just equal). Positional only: candidates with differing
796
+ parameter-list lengths (a fixed-arity/vararg shape mismatch between
797
+ two same-arg-count-compatible overloads) are treated as
798
+ incomparable rather than guessed at."""
799
+ types1 = [erased_type(t) for t in self._param_type_lists.get(c1, [])]
800
+ types2 = [erased_type(t) for t in self._param_type_lists.get(c2, [])]
801
+ if len(types1) != len(types2):
802
+ return False
803
+ ctx1 = self._param_ctx_for(c1)
804
+ ctx2 = self._param_ctx_for(c2)
805
+ strictly_narrower_somewhere = False
806
+ for t1, t2 in zip(types1, types2):
807
+ if not self._narrower_or_equal(t1, t2, ctx1, ctx2):
808
+ return False
809
+ if not self._narrower_or_equal(t2, t1, ctx2, ctx1):
810
+ strictly_narrower_somewhere = True
811
+ return strictly_narrower_somewhere
812
+
813
+ def _most_specific(self, candidates: List[str]) -> List[str]:
814
+ """Narrow a `compatible`-tier candidate set down to the maximally
815
+ specific ones — JLS §15.12.2.5's "most specific method" tie-break,
816
+ run only among candidates `_param_types_compatible` already
817
+ accepted (this never promotes an otherwise-incompatible candidate,
818
+ it only prunes redundant *compatible* ones). A candidate dominated
819
+ by another (see `_dominates`) is dropped; a pair `_dominates` can't
820
+ order — two interfaces neither extending the other, or a type this
821
+ resolver can't resolve — is a genuine tie and both survive, exactly
822
+ the fan-out behavior `_select_overload` had before this method
823
+ existed. `save(long)`/`save(float)` both declared, called with a
824
+ statically-known `int`: `save(long)` dominates `save(float)`
825
+ (`int` widens to both, but `long` also widens to `float` while
826
+ `float` does not widen to `long` — `long` is strictly narrower) and
827
+ survives alone."""
828
+ dominated: Set[str] = set()
829
+ for c1 in candidates:
830
+ for c2 in candidates:
831
+ if c1 != c2 and self._dominates(c1, c2):
832
+ dominated.add(c2)
833
+ return [c for c in candidates if c not in dominated]
834
+
835
+ @staticmethod
836
+ def _vararg_component_type(vararg_array_type: str) -> str:
837
+ """`int[]` -> `int`, `String[]` -> `String` — the type each
838
+ variable-arity *element* must match, as opposed to the array type
839
+ itself (which only a caller passing the array directly, "array
840
+ form", needs to match — see `_param_types_match`/
841
+ `_param_types_compatible`)."""
842
+ return (
843
+ vararg_array_type[:-2]
844
+ if vararg_array_type.endswith("[]")
845
+ else vararg_array_type
846
+ )
847
+
848
+ def _param_types_match(self, func_id: str, arg_types: List[str]) -> bool:
849
+ """Exact erased-type match, positionally — JLS §15.12.2 "phase 1,
850
+ strict invocation" (no widening/boxing/subtyping). Preferred over
851
+ `_param_types_compatible` whenever it has any survivor: an argument
852
+ that exactly matches one overload's declared parameter type must
853
+ pick that overload even when a *different* overload would also
854
+ accept it via widening (`save(int)` and `save(long)` both declared,
855
+ `save(1)` must resolve only to `save(int)`, never fan out to
856
+ `save(long)` too) — mirrors javac's phase ordering, where a phase-1
857
+ applicable method always wins over a phase-2-only one.
858
+
859
+ A vararg-declaring candidate is checked as vararg-or-nothing, never
860
+ as a fixed-arity candidate first (P4.4 regression fix): `void
861
+ save(int... values)` erases to a single `int[]` parameter, so
862
+ checking `len(types) == len(arg_types)` first and comparing
863
+ `save(1)`'s `int` argument against the *array* type `int[]` fails
864
+ and used to `return` immediately — the vararg branch, which must
865
+ compare against the *component* type `int` instead, never ran.
866
+ Separately, the old vararg branch only checked the fixed prefix
867
+ length/content and never compared the vararg elements' types at
868
+ all, so any trailing argument type — `String...` called with two
869
+ `int`s — was silently accepted."""
870
+ raw_types = self._param_type_lists.get(func_id, [])
871
+ types = [erased_type(t) for t in raw_types]
872
+ method_type_params = self._method_type_param_bounds.get(func_id, {})
873
+
874
+ def exact_or_generic_array(param_type: str, arg_type: str) -> bool:
875
+ if param_type == arg_type:
876
+ return True
877
+ dimensions = 0
878
+ component = param_type
879
+ while component.endswith("[]"):
880
+ dimensions += 1
881
+ component = component[:-2]
882
+ if not dimensions or component not in method_type_params:
883
+ return False
884
+ arg_component = arg_type
885
+ for _ in range(dimensions):
886
+ if not arg_component.endswith("[]"):
887
+ return False
888
+ arg_component = arg_component[:-2]
889
+ # `T` cannot be a primitive, so `T[]` never matches `int[]`.
890
+ return arg_component not in self._BOXED_PRIMITIVE
891
+
892
+ is_vararg = bool(raw_types) and self._is_vararg_param(raw_types[-1])
893
+ if not is_vararg:
894
+ return len(types) == len(arg_types) and all(
895
+ exact_or_generic_array(p, a) for p, a in zip(types, arg_types)
896
+ )
897
+ fixed = types[:-1]
898
+ if len(arg_types) < len(fixed) or arg_types[: len(fixed)] != fixed:
899
+ return False
900
+ trailing = arg_types[len(fixed):]
901
+ vararg_type = types[-1]
902
+ if len(trailing) == 1 and trailing[0] == vararg_type:
903
+ return True # array form: caller passed the array itself.
904
+ component_type = self._vararg_component_type(vararg_type)
905
+ return all(a == component_type for a in trailing)
906
+
907
+ def _param_ctx_for(self, func_id: str) -> "_ImportContext":
908
+ """The `_ImportContext` of the file that declares `func_id` — its
909
+ parameter types were written against *that* file's imports and
910
+ wildcard-imports, not whichever call site is currently resolving
911
+ against it. Keyed by `GraphNode.file_path`, not `.module`: several
912
+ files commonly share one Java module (module id = declared
913
+ `package`), each with its own import list, so a module-keyed lookup
914
+ would silently pick an arbitrary same-package file's imports instead
915
+ of the declaring file's own. Falls back to an empty context (no
916
+ import binds, no wildcard packages, `module` still correct so
917
+ same-package resolution still works) if the file is somehow missing
918
+ from `self._file_ctx` — e.g. a single-file incremental rescan that
919
+ doesn't re-pass every file `results` normally covers — which only
920
+ drops cross-package import resolution for that one lookup, never
921
+ raises, matching this resolver's "missing edge over wrong edge"
922
+ precision stance.
923
+
924
+ Memoized per `func_id` (`_param_ctx_for_cache`, reset alongside the
925
+ other per-run caches): every input (`self._file_ctx`, `node.module`,
926
+ `_declaring_class_id(func_id)`) is fixed for the whole resolution
927
+ pass, but this is called once per candidate on every overload
928
+ applicability check, so the same heavily-overloaded method gets its
929
+ context rebuilt on every call site that names it."""
930
+ cache = getattr(self, "_param_ctx_for_cache", None)
931
+ if cache is not None:
932
+ cached = cache.get(func_id)
933
+ if cached is not None:
934
+ return cached
935
+ node = self.builder._nodes.get(func_id)
936
+ if node is None:
937
+ ctx = _ImportContext("", {}, [])
938
+ else:
939
+ ctx = self._file_ctx.get(node.file_path, _ImportContext(node.module, {}, []))
940
+ ctx = ctx._replace(enclosing_class_id=self._declaring_class_id(func_id))
941
+ if cache is not None:
942
+ cache[func_id] = ctx
943
+ return ctx
944
+
945
+ def _param_types_compatible(
946
+ self, func_id: str, arg_types: List[str], arg_ctx: "_ImportContext",
947
+ allow_boxing: bool = True, allow_method_type_inference: bool = False,
948
+ ) -> bool:
949
+ """Positional argument/parameter compatibility via
950
+ `_types_compatible`. `allow_boxing` selects the JLS §15.12.2 phase:
951
+ False = "phase 1, strict invocation" (identity, primitive widening,
952
+ reference subtyping — no boxing); True = "phase 2, loose invocation"
953
+ (adds boxing/unboxing). `_select_overload` tries the strict phase
954
+ across all candidates before the loose one — see there for why phase 1
955
+ must be exhausted before phase 2 is attempted.
956
+
957
+ `func_id`'s own declaring-file context (`_param_ctx_for`) resolves
958
+ its parameter types; `arg_ctx` (the call site's context) resolves
959
+ the argument types — see `_types_compatible` for why these must not
960
+ be conflated.
961
+
962
+ Vararg handling mirrors `_param_types_match` (P4.4 regression fix,
963
+ same bug, same shape): checked as vararg-or-nothing rather than
964
+ trying fixed-arity first, and every trailing ("variadic") argument
965
+ is checked against the vararg's *component* type via
966
+ `_types_compatible` — not silently accepted, and not compared
967
+ against the array type unless the caller actually passed exactly
968
+ one argument that is itself array-compatible (array form)."""
969
+ param_ctx = self._param_ctx_for(func_id)
970
+ raw_types = self._param_type_lists.get(func_id, [])
971
+ types = [erased_type(t) for t in raw_types]
972
+ method_type_params = self._method_type_param_bounds.get(func_id, {})
973
+
974
+ def compatible(param_type: str, arg_type: str) -> bool:
975
+ # `_UNKNOWN_ARG_TYPE` means "no information about this argument,"
976
+ # not a real type — it must never rule a candidate in or out, on
977
+ # any parameter shape (array, generic, primitive). Checked before
978
+ # every other branch below, each of which assumes `arg_type` is a
979
+ # real (possibly sentinel-but-known) type.
980
+ if arg_type == _UNKNOWN_ARG_TYPE:
981
+ return True
982
+ # A method inference variable accepts a reference argument without
983
+ # treating its short name as a source class. Primitive arguments
984
+ # require the loose-invocation (boxing) phase. This remains scoped
985
+ # to an already-found own/inherited candidate; it adds no global
986
+ # name lookup or candidate fan-out.
987
+ if allow_method_type_inference and param_type in method_type_params:
988
+ # A lambda needs a functional-interface target type. An
989
+ # unbounded inference variable (`<T> ... T value`) has the
990
+ # implicit bound Object and is not itself a lambda target.
991
+ # A variable explicitly bounded by a functional interface is
992
+ # valid; reuse the existing JDK/project SAM check for it.
993
+ if arg_type.startswith(_LAMBDA_ARG_TYPE):
994
+ bound = method_type_params[param_type]
995
+ if erased_type(bound) == "Object":
996
+ return False
997
+ return self._types_compatible(
998
+ bound, arg_type, param_ctx, arg_ctx, allow_boxing,
999
+ )
1000
+ return arg_type not in self._BOXED_PRIMITIVE or allow_boxing
1001
+ # Class-scoped variables are instantiated by the receiver
1002
+ # (`Store<String>` makes `N` a reference type). The resolver does
1003
+ # not yet carry that substitution into this overload helper, but
1004
+ # treating the declared class variable as a reference slot is
1005
+ # sound for applicability and lets generic field calls inside a
1006
+ # lambda resolve without guessing a concrete source class.
1007
+ owner = self._declaring_class_id(func_id)
1008
+ if (
1009
+ owner is not None
1010
+ and param_type in self._class_type_params.get(owner, [])
1011
+ ):
1012
+ return arg_type not in self._BOXED_PRIMITIVE or allow_boxing
1013
+ # Method type variables can occur inside an array type (`T[]`),
1014
+ # not only as the complete parameter spelling. Treat an array
1015
+ # argument as an instantiation of that variable only when its
1016
+ # dimensions agree and its component is a reference type. This
1017
+ # lets `T[] copy(T[] value)` win over a sibling `T copy(T value)`
1018
+ # for a `Widget[]` argument; accepting the latter alone loses a
1019
+ # real Java overload edge. Primitive arrays are excluded because
1020
+ # a Java generic variable cannot be instantiated with `int`.
1021
+ array_dims = 0
1022
+ component = param_type
1023
+ while component.endswith("[]"):
1024
+ array_dims += 1
1025
+ component = component[:-2]
1026
+ if allow_method_type_inference and array_dims and component in method_type_params:
1027
+ arg_component = arg_type
1028
+ for _ in range(array_dims):
1029
+ if not arg_component.endswith("[]"):
1030
+ return False
1031
+ arg_component = arg_component[:-2]
1032
+ if arg_component in self._BOXED_PRIMITIVE:
1033
+ return False
1034
+ bound = method_type_params[component]
1035
+ return self._types_compatible(
1036
+ bound, arg_component, param_ctx, arg_ctx, allow_boxing,
1037
+ )
1038
+ return self._types_compatible(
1039
+ param_type, arg_type, param_ctx, arg_ctx, allow_boxing,
1040
+ )
1041
+
1042
+ is_vararg = bool(raw_types) and self._is_vararg_param(raw_types[-1])
1043
+ if not is_vararg:
1044
+ if len(types) != len(arg_types):
1045
+ return False
1046
+ return all(
1047
+ compatible(p, a)
1048
+ for p, a in zip(types, arg_types)
1049
+ )
1050
+ fixed = types[:-1]
1051
+ if len(arg_types) < len(fixed):
1052
+ return False
1053
+ if not all(
1054
+ compatible(p, a)
1055
+ for p, a in zip(fixed, arg_types[: len(fixed)])
1056
+ ):
1057
+ return False
1058
+ trailing = arg_types[len(fixed):]
1059
+ vararg_type = types[-1]
1060
+ if len(trailing) == 1 and compatible(vararg_type, trailing[0]):
1061
+ return True # array form: caller passed the array itself.
1062
+ component_type = self._vararg_component_type(vararg_type)
1063
+ return all(
1064
+ compatible(component_type, a)
1065
+ for a in trailing
1066
+ )
1067
+
1068
+ def _narrow_candidates_by_known_arg_types(
1069
+ self, candidates: List[str], arg_count: int, args_node,
1070
+ source: bytes, local_types: "_LocalScopes", param_types: Dict[str, str],
1071
+ field_types: Dict[str, str], arg_ctx: "_ImportContext",
1072
+ ) -> List[str]:
1073
+ """Shrink an otherwise-total arity-only fan-out using whichever
1074
+ argument positions *are* statically known, leaving a genuinely
1075
+ unresolvable position as a wildcard rather than guessing it.
1076
+
1077
+ `_infer_arg_types`'s all-or-nothing contract (by design: never guess
1078
+ an unknown argument) means one unresolvable position — commonly an
1079
+ external/JDK method-call result with no exact-return fact, such as
1080
+ `exception.getMessage()` — makes `_select_overload` fan out across
1081
+ *every* same-arity candidate, even ones a known sibling argument
1082
+ rules out entirely. `assertEquals(expectedMessage, exception.getMessage())`
1083
+ (`expectedMessage` a plain `String`) is a real example: no
1084
+ `assertEquals(byte,byte)`/`(int,int)`/…/`(Double,Double)` sibling can
1085
+ ever accept a `String` first argument, so fanning out to all of
1086
+ JUnit's ~20 primitive/boxed overloads is not "unresolved" for any of
1087
+ them — it is a wrong edge for every one but the `(Object,Object)`
1088
+ family. This reuses `_param_types_compatible`'s own vararg/generic/
1089
+ boxing-aware applicability check with `_UNKNOWN_ARG_TYPE` standing in
1090
+ for each position this function cannot determine, so an unresolvable
1091
+ position can never disqualify a candidate — only a position with a
1092
+ *real* known type can.
1093
+
1094
+ Per-position typing first tries `_infer_arg_type` (literals/
1095
+ declarations only) and, only when that is None, falls back to
1096
+ `_infer_known_arg_type_for_narrowing` — the same source/JDK
1097
+ return-type propagation the `var`/fluent-chain typing already
1098
+ trusts (batch 16 P5 finding #1: `exception.getMessage()` itself
1099
+ stayed unknown before this, so a *known* sibling argument's type
1100
+ could never exclude anything). This asymmetry with `_infer_arg_type`
1101
+ is safe specifically because narrowing can only ever *exclude* a
1102
+ same-arity sibling a resolved type provably cannot reach — it can
1103
+ never fabricate an edge or select a sole winner from a chain-typed
1104
+ guess the way using it for `_infer_arg_types`' exact-match path
1105
+ could.
1106
+
1107
+ Returns `candidates` unchanged whenever no position is statically
1108
+ known, arity is inconsistent with the call, or narrowing would
1109
+ otherwise discard every candidate (a correctness backstop, not
1110
+ expected in practice): this never changes behavior for a call this
1111
+ helper cannot actually inform."""
1112
+ if len(candidates) <= 1:
1113
+ return candidates
1114
+ arg_nodes = argument_nodes(args_node)
1115
+ if len(arg_nodes) != arg_count:
1116
+ return candidates
1117
+ partial = [
1118
+ self._infer_arg_type(node, source, local_types, param_types, field_types)
1119
+ or self._infer_known_arg_type_for_narrowing(
1120
+ node, source, arg_ctx, local_types, param_types, field_types,
1121
+ )
1122
+ for node in arg_nodes
1123
+ ]
1124
+ if all(t is None for t in partial):
1125
+ return candidates
1126
+ known_types = [
1127
+ erased_type(t) if t is not None else _UNKNOWN_ARG_TYPE for t in partial
1128
+ ]
1129
+ survivors = [
1130
+ candidate for candidate in candidates
1131
+ if not self._arity_matches(candidate, arg_count)
1132
+ or self._param_types_compatible(
1133
+ candidate, known_types, arg_ctx, allow_method_type_inference=True,
1134
+ )
1135
+ ]
1136
+ return survivors if survivors else candidates
1137
+
1138
+ def _select_overload(
1139
+ self, candidates: List[str], arg_count: int,
1140
+ arg_types: Optional[List[str]], conf_for: Callable[[str], float],
1141
+ arg_ctx: "_ImportContext", allow_method_type_inference: bool = False,
1142
+ ) -> Tuple[Union[str, List[str], None], float]:
1143
+ """P4.4 overload selection: arg count first — arity is a hard Java
1144
+ constraint, checked even for a single candidate, never skipped. When
1145
+ arg types are statically known, arity-matching candidates are then
1146
+ ranked in two tiers mirroring JLS §15.12.2's phases: exact erased-type
1147
+ match (`_param_types_match`) first, and only when *no* candidate
1148
+ exact-matches does the wider widening/boxing/subtype-aware
1149
+ `_param_types_compatible` apply — and when more than one candidate
1150
+ passes that check, `_most_specific` (JLS §15.12.2.5) prunes it down
1151
+ to whichever candidate's parameter types are strictly narrower than
1152
+ every other survivor's before falling back to a fan-out, so e.g.
1153
+ `save(long)`/`save(float)` called with a statically-known `int`
1154
+ resolves to `save(long)` alone rather than both. Each tier applies
1155
+ the same rule after that ranking — one survivor resolves at full
1156
+ confidence, more than one (a genuine tie `_most_specific` couldn't
1157
+ order) fans out at reduced confidence, per-candidate confidence
1158
+ still coming from `conf_for` — but a lone *exact* match is never
1159
+ diluted by a widening-only sibling overload, and a lone *arity*
1160
+ match is no longer accepted unconditionally the way it was before this fix: a
1161
+ single candidate whose declared parameter types are provably
1162
+ incompatible with the call's statically-known argument types (e.g.
1163
+ `repo.save(1)` against a sole `Repo.save(String)`) is a real
1164
+ mismatch, not an unresolved ambiguity, and resolves to nothing
1165
+ rather than a wrong edge. No candidate matching the call's arg count
1166
+ (or, when types are known, matching neither tier) is a resolution
1167
+ failure: it must never fall back to a broader candidate set, which
1168
+ would emit an edge to a same-name overload the call site cannot
1169
+ actually reach. `conf_for` looks up the confidence tied to *where* a
1170
+ given candidate was declared (own class vs. inherited), so a merged
1171
+ own+ancestor candidate set (see `_members_named`) reports the right
1172
+ confidence for whichever tier the winner actually came from.
1173
+
1174
+ **Non-vararg candidates are ranked, and given a chance to resolve
1175
+ alone, before any vararg-declaring candidate is even considered**
1176
+ (P4.4 regression fix): JLS §15.12.2 tries phase 1 (strict, no
1177
+ boxing, no varargs) and phase 2 (loose — boxing/widening allowed,
1178
+ still no varargs) across *every* applicable method — fixed-arity
1179
+ and vararg-declared alike, the vararg one only in its unexpanded
1180
+ "array form" — before ever attempting phase 3 (variable arity,
1181
+ element-wise vararg expansion). A vararg-declared candidate reached
1182
+ only via expansion is a phase-3-only candidate, and phase 3 must
1183
+ never even be attempted while a phase-1/2-applicable method exists.
1184
+ The two-tier exact-then-compatible ranking above conflated this:
1185
+ `_param_types_match`'s vararg branch matches a *component* type
1186
+ exactly (`save(int... v)` vs. `save(1)` matches `int` == `int`),
1187
+ landing it in the same "exact" tier as a true fixed-arity exact
1188
+ match, while a fixed-arity sibling reachable only through widening
1189
+ (`save(long)` vs. the same `save(1)`) is relegated to the
1190
+ "compatible" tier — `exact`'s existing short-circuit then resolved
1191
+ to the vararg candidate alone, or fanned out to both, never letting
1192
+ `save(long)` win the way real javac does. **Fix:** partition
1193
+ `by_arity` into a phase-1/2 pool and a phase-3-only pool first, keyed
1194
+ on `_is_phase3_only` — which is *invocation-form-aware*, not a
1195
+ declaration-shape test: a vararg-declaring method reached in its
1196
+ phase-1/2 *array form* for this call (`m(String[])` against
1197
+ `m(String...)`) stays in the phase-1/2 pool and competes with a
1198
+ fixed-arity sibling, while one reachable only through element-wise
1199
+ expansion (`save(1)` against `save(int...)`, `int` not assignable to
1200
+ `int[]`) is deferred to phase 3. This corrects the earlier
1201
+ declaration-only `_is_vararg_candidate` partition, which demoted
1202
+ *every* vararg candidate — array form and all — out of phase 1/2,
1203
+ so an array-form call could never beat a widening-compatible
1204
+ fixed-arity overload (`m(Object)`). Run the existing
1205
+ exact-then-compatible-then-`_most_specific` ranking against the
1206
+ phase-1/2 pool alone, and only fall through to ranking the
1207
+ phase-3-only pool the same way if the first produced zero survivors
1208
+ (nothing phase-1/2-applicable at all) — never merging the two pools'
1209
+ results, and never touching `_param_types_match`/
1210
+ `_param_types_compatible`/`_most_specific` themselves, which stay
1211
+ correct within each pool."""
1212
+ if not candidates:
1213
+ return None, 0.0
1214
+ by_arity = [c for c in candidates if self._arity_matches(c, arg_count)]
1215
+ if not by_arity:
1216
+ return None, 0.0
1217
+ if arg_types is None:
1218
+ if len(by_arity) == 1:
1219
+ return by_arity[0], conf_for(by_arity[0])
1220
+ return by_arity, min(min(conf_for(c) for c in by_arity), 0.55)
1221
+
1222
+ def _resolve_tier(hits: List[str]) -> Tuple[Union[str, List[str], None], float]:
1223
+ """One survivor -> full confidence; >1 (a genuine tie
1224
+ `_most_specific` couldn't order) -> fan out at reduced
1225
+ confidence; 0 -> unresolved. Applied per phase tier below."""
1226
+ if not hits:
1227
+ return None, 0.0
1228
+ if len(hits) > 1:
1229
+ hits = self._most_specific(hits)
1230
+ if len(hits) == 1:
1231
+ return hits[0], conf_for(hits[0])
1232
+ return hits, min(min(conf_for(c) for c in hits), 0.55)
1233
+
1234
+ def _rank(pool: List[str]) -> Tuple[Union[str, List[str], None], float]:
1235
+ # JLS §15.12.2 phase ordering, stopping at the first phase with
1236
+ # any applicable candidate: exact (identity) first, then phase 1
1237
+ # strict (widening/subtyping, no boxing), then phase 2 loose
1238
+ # (adds boxing/unboxing). A widening-applicable candidate thus
1239
+ # always wins over a boxing-only sibling — javac's behavior —
1240
+ # instead of both surviving one merged `compatible` tier.
1241
+ exact = [c for c in pool if self._param_types_match(c, arg_types)]
1242
+ if exact:
1243
+ target, conf = _resolve_tier(exact)
1244
+ if target is not None:
1245
+ return target, conf
1246
+ strict = [
1247
+ c for c in pool
1248
+ if self._param_types_compatible(
1249
+ c, arg_types, arg_ctx, allow_boxing=False,
1250
+ allow_method_type_inference=allow_method_type_inference,
1251
+ )
1252
+ ]
1253
+ if strict:
1254
+ target, conf = _resolve_tier(strict)
1255
+ if target is not None:
1256
+ return target, conf
1257
+ loose = [
1258
+ c for c in pool
1259
+ if self._param_types_compatible(
1260
+ c, arg_types, arg_ctx,
1261
+ allow_method_type_inference=allow_method_type_inference,
1262
+ )
1263
+ ]
1264
+ return _resolve_tier(loose)
1265
+
1266
+ non_vararg = [
1267
+ c for c in by_arity if not self._is_phase3_only(c, arg_types, arg_ctx)
1268
+ ]
1269
+ phase3 = [
1270
+ c for c in by_arity if self._is_phase3_only(c, arg_types, arg_ctx)
1271
+ ]
1272
+ target, conf = _rank(non_vararg)
1273
+ if target is not None:
1274
+ return target, conf
1275
+ return _rank(phase3)