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,2947 @@
1
+ """Type and generic-resolution helpers for Java call resolution.
2
+
3
+ This mixin is intentionally policy-free: it keeps the resolver facade's
4
+ existing method contract while isolating generic text manipulation.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from collections import deque
10
+ import re
11
+ from typing import Dict, List, Optional, Set, Tuple
12
+
13
+ from adl_drift.graph.edges import EdgeType
14
+ from adl_drift.graph.node import NodeType
15
+ from adl_drift.lang.java.ids import erased_type
16
+ from adl_drift.lang.java.resolution.constants import (
17
+ ASSERTJ_ASSERT_FACTORY_OWNERS,
18
+ JAVA_LANG_SIMPLE_TYPES,
19
+ JAVA_GENERIC_METHOD_STUB_RETURNS,
20
+ JAVA_GENERIC_TYPE_PARAM_STUBS,
21
+ JAVA_OBJECT_MEMBER_RETURNS,
22
+ JAVA_STATIC_FIELD_STUB_TYPES,
23
+ JAVA_STATIC_METHOD_STUB_RETURNS,
24
+ JAVA_THROWABLE_SIMPLE_TYPES,
25
+ JAVA_TIME_CONVERSION_RETURNS,
26
+ JAVA_TIME_SELF_RETURNING_EXACT,
27
+ JAVA_TIME_SELF_RETURNING_PREFIXES,
28
+ JAVA_TIME_TEMPORAL_TYPES,
29
+ SPRING_BEAN_CONTAINER_TYPES,
30
+ _FLOATING_POINT_LITERAL_TYPES,
31
+ _INTEGER_LITERAL_TYPES,
32
+ _LAMBDA_ARG_TYPE,
33
+ _LITERAL_ARG_TYPE,
34
+ _NULL_ARG_TYPE,
35
+ argument_nodes,
36
+ )
37
+ from adl_drift.lang.java.resolution.model import (
38
+ ImportContext as _ImportContext,
39
+ LocalScopes as _LocalScopes,
40
+ )
41
+
42
+
43
+ # JDK static factories whose result element type is exactly the (single,
44
+ # statically known) argument type: `List.of(a, b)` is `List<A>`; the raw
45
+ # result type is fixed whatever the elements are.
46
+ # `List.of`/`Set.of`/`Stream.of` keep the simple raw name: the owner must be
47
+ # imported at the call site for the call to compile, so the result spelling
48
+ # resolves in the caller's own import context. `Arrays.asList`/`EnumSet.of`
49
+ # are spelled fully qualified instead — a file calling `Arrays.asList(..)`
50
+ # need not import `List` at all, and an unresolvable simple `List` argument
51
+ # would be judged *incompatible* with an `Iterable` parameter (dropping the
52
+ # candidate, a false negative) rather than unknown. `_external_type_name`
53
+ # keeps a dotted spelling as written, so the JDK subtype facts still apply.
54
+ _ELEMENT_PRESERVING_STATIC_FACTORIES = {
55
+ ("java.util.List", "of"): "List",
56
+ ("java.util.stream.Stream", "of"): "Stream",
57
+ ("java.util.Set", "of"): "Set",
58
+ ("java.util.Arrays", "asList"): "java.util.List",
59
+ ("java.util.EnumSet", "of"): "java.util.EnumSet",
60
+ }
61
+ _MATH_PROMOTING_STATICS = frozenset({
62
+ "abs", "absExact", "max", "min", "addExact", "subtractExact", "multiplyExact",
63
+ "negateExact", "incrementExact", "decrementExact",
64
+ })
65
+ _ENUMSET_FACTORIES = frozenset({"of", "allOf", "noneOf", "range", "copyOf", "complementOf"})
66
+
67
+ _BOOLEAN_RESULT_BINARY_OPERATORS = frozenset({"==", "!=", "<", ">", "<=", ">=", "&&", "||"})
68
+ _ARITHMETIC_BINARY_OPERATORS = frozenset({"+", "-", "*", "/", "%"})
69
+ # JLS 5.1.8 unboxing, keyed by simple name (a `java.lang.`-qualified spelling
70
+ # reduces to the same key); primitives map to themselves.
71
+ _UNBOXED_PRIMITIVE = {
72
+ "byte": "byte", "short": "short", "char": "char", "int": "int",
73
+ "long": "long", "float": "float", "double": "double", "boolean": "boolean",
74
+ "Byte": "byte", "Short": "short", "Character": "char", "Integer": "int",
75
+ "Long": "long", "Float": "float", "Double": "double", "Boolean": "boolean",
76
+ }
77
+
78
+
79
+ def _unboxed_primitive(type_text: Optional[str]) -> Optional[str]:
80
+ if not type_text:
81
+ return None
82
+ return _UNBOXED_PRIMITIVE.get(erased_type(type_text).strip().rsplit(".", 1)[-1])
83
+
84
+
85
+ def _unary_numeric_promotion(type_text: Optional[str]) -> Optional[str]:
86
+ """JLS 5.6.1: `byte`/`short`/`char` (and their boxes) promote to `int`;
87
+ `int`/`long`/`float`/`double` keep their type; anything else is unknown."""
88
+ unboxed = _unboxed_primitive(type_text)
89
+ if unboxed in ("byte", "short", "char", "int"):
90
+ return "int"
91
+ if unboxed in ("long", "float", "double"):
92
+ return unboxed
93
+ return None
94
+
95
+
96
+ def _binary_numeric_promotion(left: Optional[str], right: Optional[str]) -> Optional[str]:
97
+ """JLS 5.6.2, only when both operands are statically known numerics."""
98
+ left_promoted = _unary_numeric_promotion(left)
99
+ right_promoted = _unary_numeric_promotion(right)
100
+ if left_promoted is None or right_promoted is None:
101
+ return None
102
+ for wider in ("double", "float", "long"):
103
+ if wider in (left_promoted, right_promoted):
104
+ return wider
105
+ return "int"
106
+
107
+
108
+ _PRIMITIVE_TYPE_NAMES = frozenset({
109
+ "boolean", "byte", "char", "short", "int", "long", "float", "double",
110
+ })
111
+
112
+
113
+ _MEMO_MISS = object()
114
+
115
+
116
+ class TypeResolutionMixin:
117
+ def _infer_arg_type(
118
+ self, node, source: bytes,
119
+ local_types: "_LocalScopes", param_types: Dict[str, str],
120
+ field_types: Dict[str, str], leaf=None,
121
+ ) -> Optional[str]:
122
+ """Best-effort static type of one call argument expression. Returns
123
+ None for anything not statically knowable from a declaration (method
124
+ calls, general `obj.field` access chains, binary/ternary expressions,
125
+ array creation, lambdas, ...) — the caller then treats the *whole*
126
+ arg-type list as unknown rather than guessing (P1.7's precision-first
127
+ spirit).
128
+
129
+ A handful of expression forms *do* have a statically-determined type
130
+ the way a plain literal or bare identifier does, and are unwrapped
131
+ here so a call site using one is type-checked against overloads the
132
+ same way its simpler equivalent is (B-01 fix — a `save(-1)`/`save((1))`/
133
+ `save(this.count)` argument used to fall through to None and, via
134
+ `_select_overload`'s unknown-arg branch, either resolve a sole
135
+ type-incompatible overload at full confidence or fan out to
136
+ incompatible siblings, asymmetric with the `save(1)`/`save(count)`
137
+ forms that resolved correctly):
138
+ - `parenthesized_expression` — `(e)` has the exact type of `e`.
139
+ - `unary_expression` — `!e` is `boolean`; `-e`/`+e`/`~e` take the
140
+ operand's numeric type, with JLS 5.6.1 unary numeric promotion
141
+ (`byte`/`short`/`char` -> `int`) applied so `-b` on a `byte`
142
+ resolves as `int`, never exact-matching a `save(byte)` overload it
143
+ cannot legally reach. Any operand whose type isn't a known numeric
144
+ stays None (never guessed).
145
+ - `field_access` limited to `this.<field>` — resolved from the
146
+ enclosing class's own+inherited `field_types` exactly as a bare
147
+ `<field>` identifier already is. A general `obj.field` needs
148
+ receiver resolution and stays None (unchanged).
149
+ - `leaf` (optional, batch 26) — a callback consulted for a
150
+ `method_invocation`/non-`this` `field_access` operand this
151
+ function itself never types; the argument-typing paths pass
152
+ `_arg_type_leaf` so composite expressions over calls are typed.
153
+ - `array_access` — `arr[i]` has the element type of `arr`'s own
154
+ statically-known type (recursing through this same function, so
155
+ `args[0]` off a `String[] args` parameter resolves to `String`,
156
+ and a nested `matrix[0][1]` peels one `[]` per access). Previously
157
+ fell through to None like any other unhandled expression, which
158
+ made `_infer_arg_types` give up on the *whole* call and fan out
159
+ across every same-arity overload at 0.55 confidence even when the
160
+ indexed array's element type made the call unambiguous (e.g.
161
+ `ApplicationModules.of(args[0])` fanning out to
162
+ `of(CacheKey)`/`of(Class)`/`of(String)` instead of resolving
163
+ `of(String)` alone)."""
164
+ if node.type in _INTEGER_LITERAL_TYPES:
165
+ text = self._text(node, source)
166
+ return "long" if text.endswith(("l", "L")) else "int"
167
+ if node.type in _FLOATING_POINT_LITERAL_TYPES:
168
+ text = self._text(node, source)
169
+ return "float" if text.endswith(("f", "F")) else "double"
170
+ if node.type in _LITERAL_ARG_TYPE:
171
+ return _LITERAL_ARG_TYPE[node.type]
172
+ if node.type == "null_literal":
173
+ return _NULL_ARG_TYPE
174
+ if node.type == "method_reference":
175
+ # A poly expression: compatible with a functional-interface
176
+ # parameter, never directly with Object or a primitive. Keep a
177
+ # sentinel until candidate-parameter compatibility is checked;
178
+ # guessing a concrete Function type here would incorrectly
179
+ # exclude Consumer/Predicate/etc. Unlike a lambda (below), an
180
+ # unbound/bound/static method reference carries no declared
181
+ # parameter list of its own to narrow by, so it stays the bare
182
+ # sentinel.
183
+ return _LAMBDA_ARG_TYPE
184
+ if node.type == "lambda_expression":
185
+ # Same poly-expression sentinel as a method reference, plus two
186
+ # syntactic, JLS-exact facts this lambda's own source already
187
+ # proves about itself — its declared parameter count, and (for a
188
+ # block body only) whether it contains a `return <expr>;` —
189
+ # encoded onto the sentinel so `_param_types_compatible` can
190
+ # exclude a same-arity sibling overload whose SAM arity/return
191
+ # kind this lambda provably cannot satisfy (E1,
192
+ # JAVA_METRICS_SPRING_MODULITH_PRECISION_SAMPLE.md:
193
+ # `Scenario.stimulate(Runnable/Supplier/Function/BiConsumer/
194
+ # BiFunction)` fanned out to all five regardless of the lambda's
195
+ # own arity). An expression body's void/value-compatibility is
196
+ # not attempted (JLS 15.27.2: a non-void statement-expression
197
+ # body is compatible with *both*) — only a block body's return
198
+ # statements are unambiguous, and only in the "found a value
199
+ # return" direction: a block with no `return <expr>;` might
200
+ # still never complete normally (an always-throwing/infinite-loop
201
+ # body is vacuously both void- and value-compatible per JLS), so
202
+ # "no value return found" never excludes a value-compatible
203
+ # sibling — only proven presence of one ever excludes anything.
204
+ return f"{_LAMBDA_ARG_TYPE}:{self._lambda_shape(node, source)}"
205
+ if node.type == "class_literal":
206
+ # `Float.class` has type Class<Float>; erasure is Class. This is
207
+ # sufficient (and required) to prefer `withType(Class)` over the
208
+ # looser `withType(Object)` overload in phase 1.
209
+ return "Class"
210
+ if node.type in ("object_creation_expression", "cast_expression"):
211
+ type_node = node.child_by_field_name("type")
212
+ return self._text(type_node, source) if type_node is not None else None
213
+ if node.type == "array_creation_expression":
214
+ type_node = node.child_by_field_name("type")
215
+ if type_node is None:
216
+ return None
217
+ dimensions = sum(
218
+ 1 for child in node.children
219
+ if child.type in ("dimensions", "dimensions_expr")
220
+ )
221
+ return self._text(type_node, source) + "[]" * max(1, dimensions)
222
+ if node.type == "parenthesized_expression":
223
+ inner = next((c for c in node.children if c.is_named), None)
224
+ if inner is None:
225
+ return None
226
+ return self._infer_arg_type(
227
+ inner, source, local_types, param_types, field_types, leaf=leaf)
228
+ if node.type == "unary_expression":
229
+ op_node = node.child_by_field_name("operator")
230
+ op = self._text(op_node, source) if op_node is not None else ""
231
+ if op == "!":
232
+ return "boolean"
233
+ operand = node.child_by_field_name("operand")
234
+ if op in ("-", "+", "~") and operand is not None:
235
+ inner = self._infer_arg_type(
236
+ operand, source, local_types, param_types, field_types, leaf=leaf)
237
+ if inner in ("byte", "short", "char"):
238
+ return "int" # JLS 5.6.1 unary numeric promotion
239
+ if inner in ("int", "long", "float", "double"):
240
+ return inner
241
+ return None
242
+ if node.type == "instanceof_expression":
243
+ # JLS 15.20.2: `boolean` whatever the operand or tested type.
244
+ return "boolean"
245
+ if node.type == "binary_expression":
246
+ operator = node.child_by_field_name("operator")
247
+ left = node.child_by_field_name("left")
248
+ right = node.child_by_field_name("right")
249
+ if operator is None or left is None or right is None:
250
+ return None
251
+ op = self._text(operator, source)
252
+ if op in _BOOLEAN_RESULT_BINARY_OPERATORS:
253
+ # JLS 15.20 (relational), 15.21 (equality), 15.23/15.24
254
+ # (conditional-and/or): the result is `boolean` regardless
255
+ # of the operand types, so nothing about the operands needs
256
+ # to be known. Batch 24: `assertTrue(quantity > 0)` /
257
+ # `assertTrue(a.isX() || b.isY())` used to stay unknown and
258
+ # fan out to the `BooleanSupplier` sibling at 0.55 whenever
259
+ # an operand was an unresolvable call.
260
+ return "boolean"
261
+ left_type = self._infer_arg_type(left, source, local_types, param_types, field_types, leaf=leaf)
262
+ right_type = self._infer_arg_type(right, source, local_types, param_types, field_types, leaf=leaf)
263
+ if op == "+" and "String" in (left_type, right_type):
264
+ # JLS 15.18.1: if either operand of `+` is String, the result
265
+ # is String regardless of the other operand's type.
266
+ return "String"
267
+ if op in _ARITHMETIC_BINARY_OPERATORS:
268
+ # JLS 5.6.2 binary numeric promotion, applied only when both
269
+ # operand types are statically known (primitive or boxed):
270
+ # `n % 2` off an `int n` is `int`, `1L + n` is `long`.
271
+ # Anything else (an unknown operand, a non-numeric reference)
272
+ # stays None — never guessed.
273
+ return _binary_numeric_promotion(left_type, right_type)
274
+ if op in ("&", "|", "^"):
275
+ if _unboxed_primitive(left_type) == "boolean" and _unboxed_primitive(right_type) == "boolean":
276
+ return "boolean" # JLS 15.22.2
277
+ promoted = _binary_numeric_promotion(left_type, right_type)
278
+ return promoted if promoted in ("int", "long") else None # JLS 15.22.1
279
+ if op in ("<<", ">>", ">>>"):
280
+ # JLS 15.19: unary numeric promotion of the left operand only.
281
+ promoted = _unary_numeric_promotion(left_type)
282
+ return promoted if promoted in ("int", "long") else None
283
+ return None
284
+ if node.type == "field_access":
285
+ obj = node.child_by_field_name("object")
286
+ field_node = node.child_by_field_name("field")
287
+ if obj is not None and obj.type == "this" and field_node is not None:
288
+ fname = self._text(field_node, source)
289
+ if fname in field_types:
290
+ return field_types[fname]
291
+ if obj is not None and field_node is not None and self._text(field_node, source) == "length":
292
+ # JLS 10.7: `array.length` is `int` for every array; only
293
+ # applied when the receiver's own type is statically known
294
+ # to be an array (a declared local/parameter/field), so a
295
+ # `length` field on an unknown reference type is untouched.
296
+ obj_type = self._infer_arg_type(obj, source, local_types, param_types, field_types, leaf=leaf)
297
+ if obj_type is not None and erased_type(obj_type).strip().endswith("[]"):
298
+ return "int"
299
+ return leaf(node) if leaf is not None else None
300
+ if node.type == "array_access":
301
+ array_node = node.child_by_field_name("array")
302
+ if array_node is None:
303
+ return None
304
+ array_type = self._infer_arg_type(
305
+ array_node, source, local_types, param_types, field_types, leaf=leaf)
306
+ if array_type is None:
307
+ return None
308
+ erased_array = erased_type(array_type).strip()
309
+ if not erased_array.endswith("[]"):
310
+ return None
311
+ element_type = erased_array[:-2].strip()
312
+ return element_type or None
313
+ if node.type == "ternary_expression":
314
+ # JLS 15.25: a conditional whose branches are both statically
315
+ # typed is itself typed only in the two exact cases modeled here:
316
+ # identical branch types (`c ? "a" : "b"` is `String`) and a
317
+ # `null` literal against a *reference* branch (`c ? FORMAT :
318
+ # null` is the reference type -- `null` -> reference is a
319
+ # widening reference conversion, so nothing is guessed). A
320
+ # primitive against `null` would box, and differing reference or
321
+ # numeric branches need a least-upper-bound/binary-numeric
322
+ # promotion; those stay None so overload selection keeps its
323
+ # unknown-argument contract instead of fabricating an exact
324
+ # match. Regression: `setShowDeprecated(flag ? FORMAT : null)`
325
+ # used to fan out to the `(boolean)` sibling as well.
326
+ consequence = node.child_by_field_name("consequence")
327
+ alternative = node.child_by_field_name("alternative")
328
+ if consequence is None or alternative is None:
329
+ return None
330
+ branch_types = [
331
+ self._infer_arg_type(
332
+ branch, source, local_types, param_types, field_types, leaf=leaf)
333
+ for branch in (consequence, alternative)
334
+ ]
335
+ if any(
336
+ t is None or t.startswith(_LAMBDA_ARG_TYPE) for t in branch_types
337
+ ):
338
+ return None
339
+ if branch_types[0] == branch_types[1]:
340
+ return branch_types[0]
341
+ non_null = [t for t in branch_types if t != _NULL_ARG_TYPE]
342
+ if len(non_null) == 1 and erased_type(non_null[0]) not in _PRIMITIVE_TYPE_NAMES:
343
+ return non_null[0]
344
+ return None
345
+ if node.type == "identifier":
346
+ name = self._text(node, source)
347
+ local = local_types.type_at(name, node.start_byte)
348
+ if local is not None:
349
+ return local
350
+ if local_types.declared_at(name, node.start_byte):
351
+ # Shadowed by a local/lambda-parameter whose own type
352
+ # couldn't be resolved — the outer param/field of the same
353
+ # name is not what this identifier refers to here.
354
+ return None
355
+ if name in param_types:
356
+ return param_types[name]
357
+ if name in field_types:
358
+ return field_types[name]
359
+ return leaf(node) if leaf is not None else None
360
+ if node.type == "method_invocation" and leaf is not None:
361
+ # Batch 26: `leaf` (see `_arg_type_leaf`) types a call/field
362
+ # operand with the same declared source/JDK return facts the
363
+ # narrowing and exact-ranking argument paths already trust, so
364
+ # a *composite* argument built from one -- `resource.usages()
365
+ # - 3`, `a.size() + b.size()`, `(cond ? x.count() : 0)` --
366
+ # is typed by the JLS rules above instead of collapsing to
367
+ # unknown at the first call operand. Plain `_infer_arg_type`
368
+ # callers pass no leaf and keep the declaration-only contract.
369
+ return leaf(node)
370
+ return None
371
+
372
+ @staticmethod
373
+ def _lambda_shape(node, source: bytes) -> str:
374
+ """`"{param_count}:{shape}"` for a `lambda_expression` node, where
375
+ `shape` is `"value"` when a block body provably contains a value
376
+ `return`, else `"any"` (see `_infer_arg_type`'s lambda branch for
377
+ the safety argument behind never producing `"void"` here)."""
378
+ params_node = node.child_by_field_name("parameters")
379
+ if params_node is None:
380
+ count = 0
381
+ elif params_node.type == "identifier":
382
+ count = 1
383
+ else:
384
+ count = sum(1 for c in params_node.children if c.is_named)
385
+ body = node.child_by_field_name("body")
386
+ shape = "any"
387
+ if body is not None and body.type == "block":
388
+ stack = [body]
389
+ while stack:
390
+ child = stack.pop()
391
+ if child is not body and child.type in (
392
+ "lambda_expression", "method_declaration",
393
+ "constructor_declaration", "class_declaration",
394
+ "interface_declaration", "enum_declaration",
395
+ "anonymous_class_body",
396
+ ):
397
+ continue
398
+ if child.type == "return_statement" and any(
399
+ c.is_named for c in child.children
400
+ ):
401
+ shape = "value"
402
+ break
403
+ stack.extend(child.children)
404
+ return f"{count}:{shape}"
405
+
406
+ def _infer_arg_types(
407
+ self, args_node, source: bytes,
408
+ local_types: "_LocalScopes", param_types: Dict[str, str],
409
+ field_types: Dict[str, str],
410
+ ) -> Optional[List[str]]:
411
+ """`[erased_type, ...]` for every argument iff *all* are statically
412
+ knowable; None (unknown) the moment any single argument isn't —
413
+ overload selection then falls back to the reduced-confidence
414
+ candidate set rather than guessing which unknown-typed arg matches
415
+ which parameter."""
416
+ arg_nodes = argument_nodes(args_node)
417
+ types: List[str] = []
418
+ for n in arg_nodes:
419
+ t = self._infer_arg_type(n, source, local_types, param_types, field_types)
420
+ if t is None:
421
+ return None
422
+ types.append(erased_type(t))
423
+ return types
424
+
425
+ def _infer_known_arg_type_for_narrowing(
426
+ self, node, source: bytes, arg_ctx: "_ImportContext",
427
+ local_types: "_LocalScopes", param_types: Dict[str, str],
428
+ field_types: Dict[str, str],
429
+ ) -> Optional[str]:
430
+ """Best-effort argument type used *only* to narrow an otherwise-total
431
+ overload fan-out (see `_narrow_candidates_by_known_arg_types`).
432
+
433
+ `_infer_arg_type`'s declaration-only contract deliberately excludes
434
+ method calls and cross-class field access (P1.7): guessing a wrong
435
+ *value* type there could pick a single wrong overload at full
436
+ confidence. Narrowing carries no such risk — a wrongly inferred type
437
+ here can only ever *exclude* a same-arity sibling a resolved type
438
+ provably cannot reach; it can never fabricate an edge or select a
439
+ winner (`_narrow_candidates_by_known_arg_types` falls back to the
440
+ full candidate set whenever narrowing would discard everything).
441
+ That asymmetry is why this reuses the same source/JDK return-type
442
+ propagation the `var`/fluent-chain typing already trusts
443
+ (`_generic_method_invocation_return_type`,
444
+ `_source_static_field_arg_type`) instead of staying as conservative
445
+ as `_infer_arg_type`. Batch 26: the same facts are now applied
446
+ through `_arg_type_leaf`, so they also reach a call/field operand
447
+ nested inside a binary/unary/ternary/parenthesized argument."""
448
+ leaf = self._arg_type_leaf(
449
+ source, arg_ctx.module, arg_ctx.enclosing_class_id, arg_ctx.import_map,
450
+ arg_ctx.wildcard_packages, local_types, param_types, field_types, {},
451
+ )
452
+ return self._infer_arg_type(
453
+ node, source, local_types, param_types, field_types, leaf=leaf,
454
+ )
455
+
456
+ def _arg_type_leaf(
457
+ self, source: bytes, module_name: str,
458
+ enclosing_class_id: Optional[str], import_map: Dict[str, str],
459
+ wildcard_packages: List[str], local_types: "_LocalScopes",
460
+ param_types: Dict[str, str], field_types: Dict[str, str],
461
+ type_param_bounds: Dict[str, str],
462
+ ):
463
+ """Callback for `_infer_arg_type(leaf=...)`: the declared source/JDK
464
+ type of one call or field-access *operand* (batch 26).
465
+
466
+ - `method_invocation`: a uniquely selected source method's declared
467
+ return (`_generic_method_invocation_return_type`, incl. the JDK
468
+ fixed-return facts a typed receiver reaches) or an exact external
469
+ static fact (`_external_static_call_return_type`). A source return
470
+ canonicalized to its FQN is re-spelled the way the call site
471
+ spells it when both resolve to the same class, so an exact
472
+ overload match is not hidden by spelling.
473
+ - `field_access`: a source static field (`Type.FIELD`, incl. enum
474
+ constants), an exact JDK constant, a source instance field off a
475
+ typed local/parameter/field, or — new — any field off a receiver
476
+ expression `_declared_receiver_type_text` can type (nested
477
+ `Handler.callCounter.beforeEachCalls`, `tasks.get(k).threadName`,
478
+ `((T) x).field`), see `_field_access_declared_type`.
479
+ - `identifier`: an explicitly static-imported field/enum constant.
480
+
481
+ Every fact here is a declaration (a field's declared type, a
482
+ method's declared return, a JDK contract), never a value guess; an
483
+ unknown operand still yields None so the enclosing expression
484
+ stays untyped."""
485
+ def leaf(node) -> Optional[str]:
486
+ if node.type == "method_invocation":
487
+ source_return = self._generic_method_invocation_return_type(
488
+ node, source, module_name, enclosing_class_id, import_map,
489
+ wildcard_packages, local_types, field_types, param_types,
490
+ type_param_bounds,
491
+ )
492
+ if source_return is not None:
493
+ return self._call_site_spelling(
494
+ source_return, module_name, enclosing_class_id,
495
+ import_map, wildcard_packages,
496
+ )
497
+ return self._external_static_call_return_type(
498
+ node, source, module_name, enclosing_class_id, import_map,
499
+ wildcard_packages, local_types, param_types, field_types,
500
+ )
501
+ if node.type == "field_access":
502
+ field_type = self._source_static_field_arg_type(
503
+ node, source, module_name, enclosing_class_id, import_map,
504
+ wildcard_packages, local_types, param_types, field_types,
505
+ ) or self._external_static_field_arg_type(
506
+ node, source, module_name, enclosing_class_id, import_map,
507
+ wildcard_packages, local_types, param_types, field_types,
508
+ ) or self._source_instance_field_arg_type(
509
+ node, source, module_name, enclosing_class_id, import_map,
510
+ wildcard_packages, local_types, param_types, field_types,
511
+ ) or self._field_access_declared_type(
512
+ node, source, module_name, enclosing_class_id, import_map,
513
+ wildcard_packages, local_types, param_types, field_types,
514
+ type_param_bounds,
515
+ )
516
+ if field_type is None:
517
+ return None
518
+ # A field's declared type is spelled in its *declaring* file
519
+ # (`Namespace GLOBAL` inside `ExtensionContext`); the helpers
520
+ # canonicalize a source type to its exact class id, and the
521
+ # call-site spelling is restored here only when it names the
522
+ # same class, so a caller that never imports the nested
523
+ # `Namespace` still matches `get(ExtensionContext.Namespace)`.
524
+ return self._call_site_spelling(
525
+ field_type, module_name, enclosing_class_id, import_map,
526
+ wildcard_packages,
527
+ )
528
+ if node.type == "identifier":
529
+ return self._static_imported_field_arg_type(
530
+ node, source, module_name, enclosing_class_id, import_map,
531
+ wildcard_packages, local_types, param_types, field_types,
532
+ )
533
+ return None
534
+ return leaf
535
+
536
+ def _call_site_spelling(
537
+ self, type_text: str, module_name: str, enclosing_class_id: Optional[str],
538
+ import_map: Dict[str, str], wildcard_packages: List[str],
539
+ ) -> str:
540
+ """Source-return propagation canonicalizes a return type to its FQN
541
+ for receiver chains. For an overload argument that can hide an
542
+ exact match, restore the call-site simple spelling only when both
543
+ forms resolve to the same class (or, for a JDK owner canonicalized
544
+ by the declaring file, when the caller imports that very class)."""
545
+ erased_arg = erased_type(type_text)
546
+ if "." not in erased_arg:
547
+ return erased_arg
548
+ simple_arg = erased_arg.rsplit(".", 1)[-1]
549
+ qualified_id = self._resolve_declared_type(
550
+ erased_arg, module_name, import_map, wildcard_packages, enclosing_class_id,
551
+ )
552
+ simple_id = self._resolve_declared_type(
553
+ simple_arg, module_name, import_map, wildcard_packages, enclosing_class_id,
554
+ )
555
+ if qualified_id is not None and qualified_id == simple_id:
556
+ return simple_arg
557
+ if (
558
+ qualified_id is None and simple_id is None
559
+ and erased_arg.startswith("java.")
560
+ and (
561
+ import_map.get(simple_arg) == erased_arg
562
+ or erased_arg.rsplit(".", 1)[0] in wildcard_packages
563
+ )
564
+ ):
565
+ return simple_arg
566
+ return erased_arg
567
+
568
+ def _field_access_declared_type(
569
+ self, field_access, source: bytes, module_name: str,
570
+ enclosing_class_id: Optional[str], import_map: Dict[str, str],
571
+ wildcard_packages: List[str], local_types: "_LocalScopes",
572
+ param_types: Dict[str, str], field_types: Dict[str, str],
573
+ type_param_bounds: Dict[str, str],
574
+ ) -> Optional[str]:
575
+ """Declared type of `<expr>.field` for any receiver expression
576
+ `_declared_receiver_type_text` can type (batch 26): a nested field
577
+ chain (`Handler.callCounter.beforeEachCalls`), a call result
578
+ (`tasks.get("a").threadName`), a cast (`((Info) o).count`).
579
+ JLS 15.11.1: the type of a field access is the field's declared
580
+ type in the receiver's *static* type — exact, never a value guess.
581
+ A field declared as a bare class type variable (`T value` on
582
+ `Box<T>`) is substituted from the receiver's own type arguments
583
+ when they are spelled at the site (`Box<String> b; b.value`) and
584
+ left unknown otherwise, so an unbound `T` can never reach overload
585
+ ranking as if it were a class name."""
586
+ obj = field_access.child_by_field_name("object")
587
+ field_node = field_access.child_by_field_name("field")
588
+ if obj is None or field_node is None or field_node.type != "identifier":
589
+ return None
590
+ if obj.type == "this":
591
+ return None # `this.field` is handled by `_infer_arg_type` itself.
592
+ obj_type = self._declared_receiver_type_text(
593
+ obj, source, module_name, enclosing_class_id, import_map,
594
+ wildcard_packages, local_types, field_types, param_types,
595
+ type_param_bounds,
596
+ )
597
+ if obj_type is None:
598
+ return None
599
+ field_name = self._text(field_node, source)
600
+ erased_obj = erased_type(obj_type).strip()
601
+ if erased_obj.endswith("[]"):
602
+ return "int" if field_name == "length" else None # JLS 10.7
603
+ owner_id = self._resolve_declared_type(
604
+ erased_obj, module_name, import_map, wildcard_packages, enclosing_class_id,
605
+ )
606
+ if owner_id is None:
607
+ return None
608
+ declared = self._all_field_types(owner_id).get(field_name)
609
+ if declared is None:
610
+ return None
611
+ bound = self._bind_class_type_variable(owner_id, obj_type, declared)
612
+ return self._canonical_declared_type(owner_id, bound) if bound is not None else None
613
+
614
+ def _infer_contextual_arg_types(
615
+ self, args_node, source: bytes, module_name: str,
616
+ enclosing_class_id: Optional[str], import_map: Dict[str, str],
617
+ wildcard_packages: List[str], local_types: "_LocalScopes",
618
+ param_types: Dict[str, str], field_types: Dict[str, str],
619
+ type_param_bounds: Dict[str, str],
620
+ ) -> Optional[List[str]]:
621
+ """Argument types for the narrow root-new overload path.
622
+
623
+ Extends ordinary literal/declaration inference only with a nested
624
+ source call's declared return type, an exact JDK static/constant
625
+ fact, and (batch 26, via `_arg_type_leaf`) the declared type of a
626
+ field reached through any typable receiver expression — each a
627
+ declaration, never a value guess — including when such a call or
628
+ field is an *operand* of a binary/unary/ternary argument. Any other
629
+ unknown keeps the whole overload unresolved, preventing the
630
+ reduced-confidence fan-out used by general call resolution.
631
+ """
632
+ leaf = self._arg_type_leaf(
633
+ source, module_name, enclosing_class_id, import_map,
634
+ wildcard_packages, local_types, param_types, field_types,
635
+ type_param_bounds,
636
+ )
637
+ inferred: List[str] = []
638
+ for arg in argument_nodes(args_node):
639
+ arg_type = self._infer_arg_type(
640
+ arg, source, local_types, param_types, field_types, leaf=leaf,
641
+ )
642
+ if arg_type is None:
643
+ return None
644
+ inferred.append(erased_type(arg_type))
645
+ return inferred
646
+
647
+ def _nearest_common_source_supertype(
648
+ self, type_texts: List[str], module_name: str, import_map: Dict[str, str],
649
+ wildcard_packages: List[str], enclosing_class_id: Optional[str],
650
+ ) -> Optional[str]:
651
+ """Simple name of the single nearest source supertype shared by every
652
+ type in `type_texts`, or None.
653
+
654
+ Each text must resolve to a source class through the caller's import
655
+ context. The candidate set is the intersection of every class's own
656
+ id plus its INHERITS/IMPLEMENTS ancestors; a candidate survives only
657
+ when no other candidate is its descendant. Exactly one survivor is
658
+ returned, so `enum A implements I` and `enum B implements I` yield
659
+ `I`, while types sharing two unrelated interfaces stay unresolved.
660
+ """
661
+ if len(type_texts) < 2:
662
+ return None
663
+ lineages: List[Set[str]] = []
664
+ for text in type_texts:
665
+ class_id = self._resolve_declared_type(
666
+ text, module_name, import_map, wildcard_packages, enclosing_class_id,
667
+ )
668
+ if class_id is None or class_id not in self.builder._nodes:
669
+ return None
670
+ lineages.append({class_id, *self._class_ancestors(class_id)})
671
+ common = set.intersection(*lineages)
672
+ nearest = [
673
+ candidate for candidate in common
674
+ if not any(
675
+ candidate in self._class_ancestors(other)
676
+ for other in common if other != candidate
677
+ )
678
+ ]
679
+ if len(nearest) != 1:
680
+ return None
681
+ node = self.builder._nodes.get(nearest[0])
682
+ return node.name if node is not None else None
683
+
684
+ def _source_static_field_arg_type(
685
+ self, field_access, source: bytes, module_name: str,
686
+ enclosing_class_id: Optional[str], import_map: Dict[str, str],
687
+ wildcard_packages: List[str], local_types: "_LocalScopes",
688
+ param_types: Dict[str, str], field_types: Dict[str, str],
689
+ ) -> Optional[str]:
690
+ """Declared type of a source static-field argument (`Type.FIELD`).
691
+
692
+ This is intentionally contextual-overload-only. The qualifier must
693
+ be an unshadowed identifier that resolves strictly through the current
694
+ package/import context.
695
+ """
696
+ owner_node = field_access.child_by_field_name("object")
697
+ field_node = field_access.child_by_field_name("field")
698
+ if owner_node is None or owner_node.type != "identifier" or field_node is None:
699
+ return None
700
+ owner_name = self._text(owner_node, source)
701
+ if (
702
+ local_types.type_at(owner_name, owner_node.start_byte) is not None
703
+ or owner_name in param_types
704
+ or owner_name in field_types
705
+ ):
706
+ return None
707
+ owner_id = self._resolve_declared_type(
708
+ owner_name, module_name, import_map, wildcard_packages,
709
+ enclosing_class_id,
710
+ )
711
+ if owner_id is None:
712
+ return None
713
+ field_name = self._text(field_node, source)
714
+ declared_field = self._all_field_types(owner_id).get(field_name)
715
+ if declared_field is not None:
716
+ return self._canonical_declared_type(owner_id, declared_field)
717
+ owner = self.builder._nodes.get(owner_id)
718
+ if (
719
+ owner is not None
720
+ and owner.metadata.get("java_type_kind") == "enum"
721
+ and field_name in owner.metadata.get("enum_constants", ())
722
+ ):
723
+ # Every declared enum constant has exactly its enum type. Keep
724
+ # this exact and metadata-backed: an arbitrary unknown static
725
+ # field on an enum is not assumed to be an enum constant.
726
+ return self._text(owner_node, source)
727
+ return None
728
+
729
+ def _source_instance_field_arg_type(
730
+ self, field_access, source: bytes, module_name: str,
731
+ enclosing_class_id: Optional[str], import_map: Dict[str, str],
732
+ wildcard_packages: List[str], local_types: "_LocalScopes",
733
+ param_types: Dict[str, str], field_types: Dict[str, str],
734
+ ) -> Optional[str]:
735
+ """Declared type of `receiver.field` where `receiver` is a local/
736
+ parameter/field whose own declared type is a resolvable source
737
+ class — e.g. `other.classes` where `other` is a `Classes` parameter
738
+ and `Classes` declares `private final List<JavaClass> classes`
739
+ (E4: `Classes.and(Classes classes) { return and(classes.classes);
740
+ }` fanned out between `and(Classes)`/`and(Collection)` because
741
+ `classes.classes`'s field access, unlike `this.field`, fell through
742
+ `_infer_arg_type` to unknown).
743
+
744
+ Narrowing-only (see `_infer_known_arg_type_for_narrowing`'s
745
+ docstring): a wrong guess here can only ever exclude a same-arity
746
+ sibling a resolved field type provably cannot reach, never select a
747
+ winner on its own. Mirrors `_source_static_field_arg_type`'s
748
+ `Type.FIELD` lookup, but keyed off the receiver's own declared
749
+ instance type instead of a type name qualifier — so it does not
750
+ also need that helper's "unshadowed identifier" guard (a static
751
+ qualifier must not be a local; this one's qualifier must be)."""
752
+ owner_node = field_access.child_by_field_name("object")
753
+ field_node = field_access.child_by_field_name("field")
754
+ if owner_node is None or owner_node.type != "identifier" or field_node is None:
755
+ return None
756
+ owner_name = self._text(owner_node, source)
757
+ owner_type = (
758
+ local_types.type_at(owner_name, owner_node.start_byte)
759
+ or param_types.get(owner_name)
760
+ or field_types.get(owner_name)
761
+ )
762
+ if owner_type is None:
763
+ return None
764
+ owner_id = self._resolve_declared_type(
765
+ owner_type, module_name, import_map, wildcard_packages,
766
+ enclosing_class_id,
767
+ )
768
+ if owner_id is None:
769
+ return None
770
+ field_name = self._text(field_node, source)
771
+ declared = self._all_field_types(owner_id).get(field_name)
772
+ if declared is None:
773
+ return None
774
+ bound = self._bind_class_type_variable(owner_id, owner_type, declared)
775
+ return self._canonical_declared_type(owner_id, bound) if bound is not None else None
776
+
777
+ def _canonical_declared_type(self, owner_id: str, declared: str) -> str:
778
+ """Re-spell a member type declared inside `owner_id`'s file as the
779
+ exact dotted class id when it names a source class there (resolved
780
+ in the *declaring* file's import context, incl. sibling nested
781
+ types in scope without an import), keeping any type arguments as
782
+ written. Primitives, arrays and external/unresolvable names are
783
+ returned unchanged. The consumer restores the call-site simple
784
+ spelling via `_call_site_spelling` when it names the same class."""
785
+ erased = erased_type(declared).strip()
786
+ if not erased or erased.endswith("[]") or erased in _PRIMITIVE_TYPE_NAMES:
787
+ return declared
788
+ owner_node = self.builder._nodes.get(owner_id)
789
+ owner_ctx = self._file_ctx.get(owner_node.file_path) if owner_node is not None else None
790
+ if owner_ctx is None:
791
+ return declared
792
+ class_id = self._resolve_declared_type(
793
+ erased, owner_ctx.module, owner_ctx.import_map, owner_ctx.wildcard_packages,
794
+ owner_id,
795
+ )
796
+ if class_id is None or ":" not in class_id:
797
+ return declared
798
+ owner_module, qualname = class_id.rsplit(":", 1)
799
+ dotted = f"{self.builder._display_module(owner_module)}.{qualname}"
800
+ return dotted + (declared[declared.index("<"):] if "<" in declared else "")
801
+
802
+ def _bind_class_type_variable(
803
+ self, owner_id: str, receiver_type: str, declared: str,
804
+ ) -> Optional[str]:
805
+ """A member declared as a bare class type variable (`T value` on
806
+ `Box<T>`) has, at a use site, the receiver's corresponding type
807
+ argument (`Box<String> b; b.value` is `String`, JLS 4.5.2) — or no
808
+ knowable type at all off a raw/unparameterized receiver. Returning
809
+ the bare `T` would let overload ranking treat it as a class name
810
+ that is compatible with `Object` and nothing else, silently
811
+ selecting an `(Object, ..)` sibling at full confidence (batch 26
812
+ guard, applied to every field-typing path)."""
813
+ erased_field = erased_type(declared).strip()
814
+ owner_params = self._class_type_params.get(owner_id, [])
815
+ if erased_field not in owner_params:
816
+ return declared
817
+ receiver_args = self._generic_type_arguments(receiver_type)
818
+ if len(receiver_args) != len(owner_params):
819
+ return None
820
+ bound = receiver_args[owner_params.index(erased_field)]
821
+ if bound.strip().startswith("?"):
822
+ return self._wildcard_upper_bound(bound)
823
+ return bound
824
+
825
+ def _static_imported_field_arg_type(
826
+ self, node, source: bytes, module_name: str,
827
+ enclosing_class_id: Optional[str], import_map: Dict[str, str],
828
+ wildcard_packages: List[str], local_types: "_LocalScopes",
829
+ param_types: Dict[str, str], field_types: Dict[str, str],
830
+ ) -> Optional[str]:
831
+ """Declared type of a bare identifier that is an explicit static
832
+ field import (`import static a.b.C.FIELD;`), e.g. `assertFalse
833
+ (tag2WasExecuted)` where `tag2WasExecuted` is `import static
834
+ ...TaggedTestCase.tag2WasExecuted`.
835
+
836
+ Mirrors `_source_static_field_arg_type`'s qualified-field lookup,
837
+ but for the unqualified spelling a static import allows -- the
838
+ qualifier check that helper does for `Type.FIELD` is done here by
839
+ requiring the name be absent from every local/param/own-field
840
+ scope first, then present in `import_map` (an explicit static
841
+ import; a wildcard `import static ...*;` is not indexed there and
842
+ stays unresolved, matching every other exact-import-only path in
843
+ this file)."""
844
+ if node.type != "identifier":
845
+ return None
846
+ name = self._text(node, source)
847
+ if (
848
+ local_types.type_at(name, node.start_byte) is not None
849
+ or local_types.declared_at(name, node.start_byte)
850
+ or name in param_types
851
+ or name in field_types
852
+ or name not in import_map
853
+ ):
854
+ return None
855
+ resolved = import_map[name]
856
+ owner_path, _, field_name = resolved.rpartition(".")
857
+ if not owner_path:
858
+ return None
859
+ owner_id = self._resolve_static_import_class(owner_path, module_name)
860
+ if owner_id is None:
861
+ return None
862
+ declared_field = self._all_field_types(owner_id).get(field_name)
863
+ if declared_field is not None:
864
+ return declared_field
865
+ if self._is_enum_constant(owner_id, field_name) and ":" in owner_id:
866
+ # A static-imported enum constant (`import static
867
+ # ...Mode.INCLUDE;`) has exactly its enum type. The enum's
868
+ # simple name need not be imported as a *type* at this call
869
+ # site, so the type is spelled as the exact dotted class id
870
+ # `_resolve_declared_type` accepts, the same way a static-
871
+ # imported source factory's owner is spelled for its fluent
872
+ # continuation (batch 25).
873
+ owner_module, owner_qualname = owner_id.rsplit(":", 1)
874
+ return f"{self.builder._display_module(owner_module)}.{owner_qualname}"
875
+ return None
876
+
877
+ def _is_enum_constant(self, class_id: str, name: str) -> bool:
878
+ """True iff `name` is a declared constant of the source enum
879
+ `class_id` (extractor-recorded metadata; never a guess about an
880
+ arbitrary static field on an enum)."""
881
+ owner = self.builder._nodes.get(class_id)
882
+ return (
883
+ owner is not None
884
+ and owner.metadata.get("java_type_kind") == "enum"
885
+ and name in owner.metadata.get("enum_constants", ())
886
+ )
887
+
888
+ def _external_static_field_arg_type(
889
+ self, field_access, source: bytes, module_name: str,
890
+ enclosing_class_id: Optional[str], import_map: Dict[str, str],
891
+ wildcard_packages: List[str], local_types: "_LocalScopes",
892
+ param_types: Dict[str, str], field_types: Dict[str, str],
893
+ ) -> Optional[str]:
894
+ """Declared type of an *external* (JDK) constant argument such as
895
+ `Integer.MAX_VALUE` or `Level.WARNING`, from the exact
896
+ `JAVA_STATIC_FIELD_STUB_TYPES` table (batch 24).
897
+
898
+ Narrowing-only, and as strict as `_external_static_call_return_type`:
899
+ the qualifier must be an unshadowed identifier that is *not* a
900
+ resolvable source type (a project class named `Level` always wins),
901
+ and must reach a fully-qualified owner through an explicit import or
902
+ the implicit `java.lang` package. `assertEquals(Integer.MAX_VALUE,
903
+ parse(...))` and `listener.stream(Level.WARNING)` (JUnit5's
904
+ `stream(Level)`/`stream(Class)` pair) stayed unknown before."""
905
+ owner_node = field_access.child_by_field_name("object")
906
+ field_node = field_access.child_by_field_name("field")
907
+ if owner_node is None or owner_node.type != "identifier" or field_node is None:
908
+ return None
909
+ owner_name = self._text(owner_node, source)
910
+ if (
911
+ local_types.type_at(owner_name, owner_node.start_byte) is not None
912
+ or local_types.declared_at(owner_name, owner_node.start_byte)
913
+ or owner_name in param_types
914
+ or owner_name in field_types
915
+ ):
916
+ return None
917
+ if self._resolve_declared_type(
918
+ owner_name, module_name, import_map, wildcard_packages,
919
+ enclosing_class_id,
920
+ ) is not None:
921
+ return None
922
+ external_fqn = import_map.get(owner_name)
923
+ if external_fqn is None and owner_name in JAVA_LANG_SIMPLE_TYPES:
924
+ external_fqn = f"java.lang.{owner_name}"
925
+ if external_fqn is None:
926
+ return None
927
+ return JAVA_STATIC_FIELD_STUB_TYPES.get(
928
+ (external_fqn, self._text(field_node, source)),
929
+ )
930
+
931
+ def _external_static_call_return_type(
932
+ self, call_node, source: bytes, module_name: str,
933
+ enclosing_class_id: Optional[str], import_map: Dict[str, str],
934
+ wildcard_packages: List[str], local_types: "_LocalScopes",
935
+ param_types: Dict[str, str], field_types: Dict[str, str],
936
+ ) -> Optional[str]:
937
+ """Return type for a small exact set of external static methods."""
938
+ receiver = call_node.child_by_field_name("object")
939
+ name_node = call_node.child_by_field_name("name")
940
+ if receiver is None or receiver.type != "identifier" or name_node is None:
941
+ return None
942
+ receiver_name = self._text(receiver, source)
943
+ if (
944
+ local_types.type_at(receiver_name, receiver.start_byte) is not None
945
+ or receiver_name in param_types
946
+ or receiver_name in field_types
947
+ ):
948
+ return None
949
+ if self._resolve_declared_type(
950
+ receiver_name, module_name, import_map, wildcard_packages,
951
+ enclosing_class_id,
952
+ ) is not None:
953
+ return None
954
+ external_fqn = import_map.get(receiver_name)
955
+ if external_fqn is None and receiver_name == "Arrays" and "java.util" in wildcard_packages:
956
+ external_fqn = "java.util.Arrays"
957
+ if external_fqn is None and receiver_name in JAVA_LANG_SIMPLE_TYPES:
958
+ # `java.lang` needs no import: `String.valueOf`, `Integer.parseInt`.
959
+ external_fqn = f"java.lang.{receiver_name}"
960
+ method_name = self._text(name_node, source)
961
+ fixed_return = JAVA_STATIC_METHOD_STUB_RETURNS.get((external_fqn or "", method_name))
962
+ if fixed_return is not None:
963
+ return fixed_return
964
+ if external_fqn == "java.lang.Math" and method_name in _MATH_PROMOTING_STATICS:
965
+ # `Math.abs/max/min/addExact/..` are overloaded per primitive
966
+ # and return exactly the promoted operand type (JLS 5.6.1/5.6.2
967
+ # over the declared overload set: `addExact(int,int) -> int`,
968
+ # `max(int,long) -> long`). Only fully known primitive/boxed
969
+ # operands are promoted; anything else stays unknown.
970
+ inferred = self._infer_contextual_arg_types(
971
+ call_node.child_by_field_name("arguments"), source, module_name,
972
+ enclosing_class_id, import_map, wildcard_packages, local_types,
973
+ param_types, field_types, {},
974
+ )
975
+ if inferred and all(_unboxed_primitive(t) in ("int", "long", "float", "double", "short", "byte", "char") for t in inferred):
976
+ if len(inferred) == 1:
977
+ return _unary_numeric_promotion(inferred[0])
978
+ if len(inferred) == 2:
979
+ return _binary_numeric_promotion(inferred[0], inferred[1])
980
+ return None
981
+ # `Arrays.stream(T[])` preserves the array component as the Stream
982
+ # element. Keep the rule to one exact, reference-array shape: the
983
+ # primitive overloads (`int[]`, etc.) return specialized streams and
984
+ # need different lambda parameter types.
985
+ if external_fqn == "java.util.Arrays" and method_name == "stream":
986
+ args = call_node.child_by_field_name("arguments")
987
+ inferred = self._infer_arg_types(
988
+ args, source, local_types, param_types, field_types,
989
+ )
990
+ if inferred is None:
991
+ # The array is frequently the result of a nested call
992
+ # (`Arrays.stream(method.getGenericParameterTypes())`,
993
+ # `Arrays.stream(holder.items())`). Reuse the same declared
994
+ # source/exact-JDK return facts that type a fluent receiver;
995
+ # anything still unknown keeps the Stream element unknown.
996
+ inferred = self._infer_contextual_arg_types(
997
+ args, source, module_name, enclosing_class_id, import_map,
998
+ wildcard_packages, local_types, param_types, field_types, {},
999
+ )
1000
+ if inferred is not None and len(inferred) == 1:
1001
+ component = inferred[0]
1002
+ if component.endswith("..."):
1003
+ component = component[:-3] + "[]"
1004
+ if component.endswith("[]") and component[:-2] not in {
1005
+ "boolean", "byte", "char", "short", "int", "long", "float", "double",
1006
+ }:
1007
+ return f"Stream<{component[:-2]}>"
1008
+ # `Optional.ofNullable(T)` retains the exact static type of its sole
1009
+ # argument. This is needed before an immediately chained `map` can
1010
+ # target-type its lambda parameter, including when that parameter and
1011
+ # a captured local jointly select a source constructor overload.
1012
+ # `null` has no concrete T, so it intentionally remains unknown.
1013
+ if external_fqn == "java.util.Optional" and method_name == "ofNullable":
1014
+ inferred = self._infer_arg_types(
1015
+ call_node.child_by_field_name("arguments"), source,
1016
+ local_types, param_types, field_types,
1017
+ )
1018
+ if inferred is not None and len(inferred) == 1 and inferred[0] != "null":
1019
+ return f"Optional<{inferred[0]}>"
1020
+ # `List.of(a, b, ...)` is a generic static factory. Preserve its
1021
+ # element type only when every argument has the same statically known
1022
+ # type; computing a Java least-upper-bound here would be speculative.
1023
+ # This exact case lets a following `stream().map(x -> x.sourceCall())`
1024
+ # target the lambda parameter without emitting an external call edge.
1025
+ # Batch 25 widens the same exact rule to the other element-preserving
1026
+ # JDK factories that appear as overload arguments' receivers:
1027
+ # `Arrays.asList(a, b).contains(x)`, `Set.of(...)`, `EnumSet.of(...)`.
1028
+ factory_raw = _ELEMENT_PRESERVING_STATIC_FACTORIES.get((external_fqn or "", method_name))
1029
+ if factory_raw is not None:
1030
+ args = call_node.child_by_field_name("arguments")
1031
+ inferred = self._infer_arg_types(
1032
+ args, source, local_types, param_types, field_types,
1033
+ )
1034
+ if inferred is None:
1035
+ inferred = self._infer_contextual_arg_types(
1036
+ args, source, module_name, enclosing_class_id, import_map,
1037
+ wildcard_packages, local_types, param_types, field_types, {},
1038
+ )
1039
+ if inferred and len(inferred) == 1 and inferred[0].endswith("[]"):
1040
+ inferred = [inferred[0][:-2]]
1041
+ raw = factory_raw
1042
+ if inferred and len(set(inferred)) == 1:
1043
+ return f"{raw}<{inferred[0]}>"
1044
+ if inferred and len(set(inferred)) > 1:
1045
+ # Distinct source element types (`Stream.of(A.INSTANCE,
1046
+ # B.INSTANCE)`) share a Java least upper bound. Accept it
1047
+ # only when every argument resolves to a source class and
1048
+ # exactly one nearest common source supertype exists; a JDK
1049
+ # or multiply-bounded lub stays unknown.
1050
+ common = self._nearest_common_source_supertype(
1051
+ sorted(set(inferred)), module_name, import_map,
1052
+ wildcard_packages, enclosing_class_id,
1053
+ )
1054
+ if common is not None:
1055
+ return f"{raw}<{common}>"
1056
+ if external_fqn == "java.util.EnumSet" and method_name in _ENUMSET_FACTORIES:
1057
+ # Every `EnumSet` factory returns an `EnumSet` whatever its
1058
+ # element type; the JDK enum constants it is usually built from
1059
+ # (`EnumSet.of(ChronoUnit.DAYS)`) have no source declaration to
1060
+ # type the element by. The raw type is exact and is enough for
1061
+ # the fixed-return members consulted next (`contains`/`isEmpty`/
1062
+ # `size`); no element-typed (`E`-returning) `EnumSet` stub
1063
+ # exists, so a raw receiver can never leak an unbound `E`.
1064
+ return "java.util.EnumSet"
1065
+ return None
1066
+
1067
+ def _contextual_overload_arg_types(
1068
+ self, candidates: List[str], arg_count: int, arg_types: Optional[List[str]],
1069
+ args_node, source: bytes, module_name: str,
1070
+ enclosing_class_id: Optional[str], import_map: Dict[str, str],
1071
+ wildcard_packages: List[str], local_types: "_LocalScopes",
1072
+ param_types: Dict[str, str], field_types: Dict[str, str],
1073
+ type_param_bounds: Dict[str, str],
1074
+ ) -> Optional[List[str]]:
1075
+ """Refine unknown args only when overload fan-out is otherwise possible."""
1076
+ if arg_types is not None:
1077
+ return arg_types
1078
+ if sum(self._arity_matches(candidate, arg_count) for candidate in candidates) <= 1:
1079
+ return arg_types
1080
+ refined = self._infer_contextual_arg_types(
1081
+ args_node, source, module_name, enclosing_class_id, import_map,
1082
+ wildcard_packages, local_types, param_types, field_types,
1083
+ type_param_bounds,
1084
+ )
1085
+ if refined is None:
1086
+ return arg_types
1087
+ applicable, _ = self._select_overload(
1088
+ candidates, arg_count, refined, lambda _candidate: 0.9,
1089
+ _ImportContext(
1090
+ module_name, import_map, wildcard_packages, enclosing_class_id,
1091
+ ),
1092
+ allow_method_type_inference=True,
1093
+ )
1094
+ # Preserve the established unknown-argument fan-out contract when
1095
+ # contextual inference proves no overload applicable.
1096
+ return refined if applicable is not None else arg_types
1097
+
1098
+ def _resolve_declared_type(
1099
+ self, type_text: str, module_name: str, import_map: Dict[str, str],
1100
+ wildcard_packages: List[str], enclosing_class_id: Optional[str] = None,
1101
+ ) -> Optional[str]:
1102
+ """Declared-type text (`Foo`, `List<User>`, `Box<T>`, `Foo[]`, a
1103
+ Lombok/erasure-normalized varargs `Foo...`) -> CLASS node id.
1104
+
1105
+ Uses `erased_type()` to strip generics/arrays down to the *raw*
1106
+ type — correct for receiver dispatch (`Box<T> b; b.get()` must look
1107
+ up `get` on raw `Box`, not `T`; contrast with
1108
+ `CallGraphBuilder._resolve_annotation`, which Java's TYPE_OF/RETURNS
1109
+ edges use and which deliberately keeps the *inner* generic argument
1110
+ instead, mirroring Python's `List[X]` -> `X` semantics for "what
1111
+ this annotation carries" — the two helpers answer different
1112
+ questions on purpose, see `_resolve_annotation`'s docstring).
1113
+
1114
+ Name resolution itself reuses `CallGraphBuilder._resolve_name` in
1115
+ strict mode: same-package (same-module) -> explicit import -> (a
1116
+ miss falls through to `wildcard_packages`, tried in import order,
1117
+ each via `CallGraphBuilder._lookup_qualified("<pkg>.<t>", ...)` —
1118
+ `import lib.*;` then `Service s;` looks up `lib.Service`) ->
1119
+ unresolved for `java.lang`/external JDK types, which is correct:
1120
+ only in-repo source can be resolved from source. `strict=True`
1121
+ deliberately skips `_resolve_name`'s own non-strict unique-simple-
1122
+ name fallback (over-approximation across every class in the repo,
1123
+ not scoped to an actually-imported package) — `wildcard_packages`
1124
+ is the precise replacement for that approximation, per the P4.4 Fix
1125
+ Applied notes.
1126
+ """
1127
+ memo = self._declared_type_memo
1128
+ key = None
1129
+ if memo is not None and id(import_map) in self._file_import_map_ids:
1130
+ key = (
1131
+ type_text, module_name, id(import_map),
1132
+ tuple(wildcard_packages), enclosing_class_id,
1133
+ )
1134
+ hit = memo.get(key, _MEMO_MISS)
1135
+ if hit is not _MEMO_MISS:
1136
+ return hit
1137
+ result = self._resolve_declared_type_uncached(
1138
+ type_text, module_name, import_map, wildcard_packages, enclosing_class_id,
1139
+ )
1140
+ if key is not None:
1141
+ memo[key] = result
1142
+ return result
1143
+
1144
+ def _resolve_declared_type_uncached(
1145
+ self, type_text: str, module_name: str, import_map: Dict[str, str],
1146
+ wildcard_packages: List[str], enclosing_class_id: Optional[str] = None,
1147
+ ) -> Optional[str]:
1148
+ t = erased_type(type_text).strip()
1149
+ while t.endswith("[]"):
1150
+ t = t[:-2]
1151
+ if not t or not all(c.isalnum() or c in "._$" for c in t):
1152
+ return None
1153
+ namespace = self.builder._module_namespace(module_name)
1154
+ module = self.builder._display_module(module_name)
1155
+
1156
+ # Preserve the declared qualification before falling back to the
1157
+ # shared simple-name index. Java packages commonly contain several
1158
+ # member classes with the same simple name (for example
1159
+ # CommandLine.Builder and HelpFormatter.Builder). The latter index
1160
+ # is intentionally name-keyed and therefore cannot distinguish
1161
+ # those declarations; using it first silently dispatched a typed
1162
+ # local to whichever Builder happened to be indexed last.
1163
+ exact_names: List[str] = []
1164
+ head, dot, tail = t.partition(".")
1165
+ if head in import_map:
1166
+ exact_names.append(import_map[head] + (f".{tail}" if dot else ""))
1167
+ if dot:
1168
+ exact_names.extend((t, f"{module}.{t}"))
1169
+ else:
1170
+ imported = import_map.get(t)
1171
+ if imported:
1172
+ exact_names.append(imported)
1173
+
1174
+ # A member/local type is resolved relative to the enclosing
1175
+ # lexical type before a package-level type of the same name.
1176
+ # Walk inner-to-outer so ordinary Java shadowing is preserved.
1177
+ if enclosing_class_id and ":" in enclosing_class_id:
1178
+ owner_module, owner_qualname = enclosing_class_id.rsplit(":", 1)
1179
+ owner_module = self.builder._display_module(owner_module)
1180
+ owner_parts = owner_qualname.split(".")
1181
+ for end in range(len(owner_parts), 0, -1):
1182
+ exact_names.append(
1183
+ f"{owner_module}.{'.'.join(owner_parts[:end])}.{t}"
1184
+ )
1185
+ exact_names.append(f"{module}.{t}")
1186
+
1187
+ seen_exact: Set[str] = set()
1188
+ for qualified in exact_names:
1189
+ if qualified in seen_exact:
1190
+ continue
1191
+ seen_exact.add(qualified)
1192
+ tid = self.builder._node_for_dotted(qualified, namespace)
1193
+ if tid and self.builder._nodes[tid].node_type == NodeType.CLASS:
1194
+ return tid
1195
+
1196
+ # JLS 6.4.1: a member type declared by a superclass/superinterface is
1197
+ # inherited and visible by simple name in every subclass body, no
1198
+ # import needed (`class AbstractExtensionContext implements
1199
+ # ExtensionContext` writes the bare `Namespace` for the interface's
1200
+ # own nested `ExtensionContext.Namespace`). Only a bare name reaches
1201
+ # here — a dotted reference is already explicitly qualified. Walking
1202
+ # `_class_ancestors` (nearest declared supertype first, matching
1203
+ # ordinary member shadowing) for a nested type actually declared on
1204
+ # that ancestor is exact, so it runs before the global unique-name
1205
+ # fallback below: without it, any second same-simple-name class
1206
+ # anywhere in a large corpus (here, the unrelated top-level
1207
+ # `org.junit.platform.engine.support.store.Namespace`) makes that
1208
+ # fallback ambiguous and silently drops every call whose argument or
1209
+ # receiver carries the inherited type.
1210
+ if not dot and enclosing_class_id:
1211
+ for ancestor_id in self._class_ancestors(enclosing_class_id):
1212
+ nested_id = f"{ancestor_id}.{t}"
1213
+ nested = self.builder._nodes.get(nested_id)
1214
+ if nested is not None and nested.node_type == NodeType.CLASS:
1215
+ return nested_id
1216
+
1217
+ # A dotted type is already explicitly qualified. If every exact
1218
+ # package/import/enclosing-type interpretation above missed, do not
1219
+ # discard its qualifier and retry by simple tail name: resolving
1220
+ # `missing.Service` to an unrelated same-package `Service` is a
1221
+ # global/name-shape guess and turns an unknown cast into a wrong edge.
1222
+ if dot:
1223
+ return None
1224
+
1225
+ tid = self.builder._resolve_name(
1226
+ t, module_name, import_map, {}, self._by_module_symbol, strict=True,
1227
+ )
1228
+ if tid and self.builder._nodes[tid].node_type == NodeType.CLASS:
1229
+ return tid
1230
+ for pkg in wildcard_packages:
1231
+ tid = self.builder._node_for_dotted(f"{pkg}.{t}", namespace)
1232
+ if tid is None:
1233
+ tid = self.builder._lookup_qualified(
1234
+ f"{pkg}.{t}", self._by_module_symbol, namespace
1235
+ )
1236
+ if tid and self.builder._nodes[tid].node_type == NodeType.CLASS:
1237
+ return tid
1238
+ # A functional parameter may carry a nested type name from a
1239
+ # different declaring file's context (for example `Predicate<Bean>`
1240
+ # in a static-imported factory). Preserve strict resolution for
1241
+ # ambiguous names, but recover a project type when the simple name is
1242
+ # globally unique and therefore cannot select a sibling accidentally.
1243
+ # An explicit import that did not resolve above names an external
1244
+ # type (`import java.lang.reflect.Method`), and a bare `java.lang`
1245
+ # name is the JDK type; neither may become an unrelated project class.
1246
+ if t in import_map or t in JAVA_LANG_SIMPLE_TYPES:
1247
+ return None
1248
+ return self.builder._unique_class_by_simple_name(t, namespace)
1249
+
1250
+ def _resolve_receiver(
1251
+ self, obj_node, source: bytes, module_name: str,
1252
+ enclosing_class_id: Optional[str], import_map: Dict[str, str],
1253
+ wildcard_packages: List[str],
1254
+ local_types: "_LocalScopes", field_types: Dict[str, str],
1255
+ param_types: Dict[str, str],
1256
+ type_param_bounds: Optional[Dict[str, str]] = None,
1257
+ ) -> Tuple[Optional[str], float]:
1258
+ """Resolve a `method_invocation`'s `object` field (already known to
1259
+ be neither None/bare nor `super` — those are handled directly in
1260
+ `_resolve_call`) to `(receiver_class_id, confidence)`.
1261
+
1262
+ Handles: `identifier` (local var / parameter / field / same-package
1263
+ static-class-call receiver), parenthesized/cast receivers,
1264
+ `field_access` (recursively, so
1265
+ `this.userService.findById(id)` unwraps `this.userService` to the
1266
+ field's declared type before resolving `findById` — the exact
1267
+ pattern codegraph-java-deep-dive.md §5.3 calls out), and chained
1268
+ `method_invocation` receivers whose selected source method has a
1269
+ declared return type, and `array_access` whose array expression has a
1270
+ statically-known element type. Lambda/method-reference expressions
1271
+ remain unresolved rather than guessed.
1272
+
1273
+ `type_param_bounds` (P4.12 generic narrowing) rewrites a receiver whose
1274
+ declared type is a method type parameter (`T`) to its concrete upper
1275
+ bound (`<T extends StripeGateway> ... g.charge()` resolves `g` as
1276
+ `StripeGateway`) before type resolution.
1277
+ """
1278
+ bounds = type_param_bounds or {}
1279
+ if obj_node.type == "parenthesized_expression":
1280
+ inner = next((child for child in obj_node.children if child.is_named), None)
1281
+ if inner is None:
1282
+ return None, 0.0
1283
+ return self._resolve_receiver(
1284
+ inner, source, module_name, enclosing_class_id, import_map,
1285
+ wildcard_packages, local_types, field_types, param_types,
1286
+ type_param_bounds,
1287
+ )
1288
+
1289
+ if obj_node.type == "ternary_expression":
1290
+ declared = self._declared_receiver_type_text(
1291
+ obj_node, source, module_name, enclosing_class_id, import_map,
1292
+ wildcard_packages, local_types, field_types, param_types,
1293
+ bounds,
1294
+ )
1295
+ if declared is None:
1296
+ return None, 0.0
1297
+ cid = self._resolve_declared_type(
1298
+ declared, module_name, import_map, wildcard_packages,
1299
+ enclosing_class_id,
1300
+ )
1301
+ return (cid, 0.9) if cid else (None, 0.0)
1302
+
1303
+ if obj_node.type == "cast_expression":
1304
+ type_node = obj_node.child_by_field_name("type")
1305
+ if type_node is None:
1306
+ return None, 0.0
1307
+ declared = self._text(type_node, source)
1308
+ cid = self._resolve_declared_type(
1309
+ self._apply_type_bound(declared, bounds), module_name, import_map,
1310
+ wildcard_packages, enclosing_class_id,
1311
+ )
1312
+ return (cid, 0.9) if cid else (None, 0.0)
1313
+
1314
+ if obj_node.type == "identifier":
1315
+ name = self._text(obj_node, source)
1316
+ local = local_types.type_at(name, obj_node.start_byte)
1317
+ if local is not None:
1318
+ cid = self._resolve_declared_type(
1319
+ self._apply_type_bound(local, bounds), module_name, import_map,
1320
+ wildcard_packages, enclosing_class_id,
1321
+ )
1322
+ return (cid, 0.9) if cid else (None, 0.0)
1323
+ if local_types.declared_at(name, obj_node.start_byte):
1324
+ # Shadowed by a local/lambda-parameter of unresolvable type
1325
+ # — must not fall through to an outer param/field/static-type
1326
+ # of the same name, which this identifier no longer names.
1327
+ return None, 0.0
1328
+ if name in param_types:
1329
+ cid = self._resolve_declared_type(
1330
+ self._apply_type_bound(param_types[name], bounds), module_name,
1331
+ import_map, wildcard_packages, enclosing_class_id,
1332
+ )
1333
+ return (cid, 0.9) if cid else (None, 0.0)
1334
+ if name in field_types:
1335
+ cid = self._resolve_declared_type(
1336
+ self._apply_type_bound(field_types[name], bounds), module_name,
1337
+ import_map, wildcard_packages, enclosing_class_id,
1338
+ )
1339
+ return (cid, 0.9) if cid else (None, 0.0)
1340
+ # `import static Owner.FIELD; FIELD.method()` is a value receiver,
1341
+ # not a bare class name. Recover it only from an explicit static
1342
+ # import whose source owner and declared field both resolve.
1343
+ imported = import_map.get(name)
1344
+ if imported and "." in imported:
1345
+ owner_path, field_name = imported.rsplit(".", 1)
1346
+ owner_id = self._resolve_static_import_class(owner_path, module_name)
1347
+ if owner_id is not None:
1348
+ declared = self._all_field_types(owner_id).get(field_name)
1349
+ if declared is not None:
1350
+ cid = self._resolve_declared_type(
1351
+ self._apply_type_bound(declared, bounds), module_name,
1352
+ import_map, wildcard_packages, enclosing_class_id,
1353
+ )
1354
+ return (cid, 0.9) if cid else (None, 0.0)
1355
+ if self._is_enum_constant(owner_id, field_name):
1356
+ # `import static ...Mode.INCLUDE; INCLUDE.select(x)`:
1357
+ # a static-imported enum constant is a value of
1358
+ # exactly its enum (batch 25 — the `select` edge
1359
+ # itself was never emitted before, not only its
1360
+ # return type lost).
1361
+ return owner_id, 0.9
1362
+ # ClassName.staticMethod() — receiver is a type, not a value.
1363
+ cid = self._resolve_declared_type(
1364
+ name, module_name, import_map, wildcard_packages,
1365
+ enclosing_class_id,
1366
+ )
1367
+ if cid:
1368
+ return cid, 0.85
1369
+ return None, 0.0
1370
+
1371
+ if obj_node.type == "field_access":
1372
+ inner_obj = obj_node.child_by_field_name("object")
1373
+ field_node = obj_node.child_by_field_name("field")
1374
+ # Java's qualified enclosing-instance receiver (`Outer.this`)
1375
+ # is represented as a field_access whose field is the `this`
1376
+ # token. It is a value of exactly `Outer`, not a field lookup.
1377
+ # Resolve only a real lexical owner of the current class; this
1378
+ # prevents an arbitrary `Unrelated.this` spelling from becoming
1379
+ # a source receiver.
1380
+ if (
1381
+ inner_obj is not None and inner_obj.type == "identifier"
1382
+ and field_node is not None and field_node.type == "this"
1383
+ and enclosing_class_id is not None
1384
+ ):
1385
+ requested = self._text(inner_obj, source)
1386
+ for outer_id in self._lexical_outer_class_ids(enclosing_class_id):
1387
+ outer = self.builder._nodes.get(outer_id)
1388
+ if outer is not None and outer.name == requested:
1389
+ return outer_id, 0.9
1390
+ # A dotted static type receiver has the same CST shape as value
1391
+ # field access: `Publication.Status.values()` presents
1392
+ # `Publication.Status` as a field_access object. Resolve the
1393
+ # complete qualified text as a source type first. Strict declared
1394
+ # type lookup prevents an ordinary `instance.field` expression
1395
+ # from becoming a type unless that exact qualification exists.
1396
+ qualified_type = self._resolve_declared_type(
1397
+ self._text(obj_node, source), module_name, import_map,
1398
+ wildcard_packages, enclosing_class_id,
1399
+ )
1400
+ if qualified_type is not None:
1401
+ return qualified_type, 0.85
1402
+ if field_node is None:
1403
+ return None, 0.0
1404
+ field_name = self._text(field_node, source)
1405
+ if inner_obj is not None and inner_obj.type == "this":
1406
+ if enclosing_class_id is None:
1407
+ return None, 0.0
1408
+ own_fields = self._all_field_types(enclosing_class_id)
1409
+ if field_name in own_fields:
1410
+ cid = self._resolve_declared_type(
1411
+ self._apply_type_bound(own_fields[field_name], bounds),
1412
+ module_name, import_map, wildcard_packages,
1413
+ enclosing_class_id,
1414
+ )
1415
+ return (cid, 0.9) if cid else (None, 0.0)
1416
+ return None, 0.0
1417
+ if inner_obj is None:
1418
+ return None, 0.0
1419
+ base_id, base_conf = self._resolve_receiver(
1420
+ inner_obj, source, module_name, enclosing_class_id, import_map,
1421
+ wildcard_packages, local_types, field_types, param_types, type_param_bounds,
1422
+ )
1423
+ if base_id is None:
1424
+ return None, 0.0
1425
+ base_fields = self._all_field_types(base_id)
1426
+ if field_name in base_fields:
1427
+ # `bounds` was collected for `enclosing_class_id`'s own
1428
+ # `<T extends ...>` (P4.12 generic narrowing). `base_id` here
1429
+ # is whichever class `inner_obj` resolved to, which may be a
1430
+ # *different* class than `enclosing_class_id` (a chained
1431
+ # field access, e.g. `helper.gateway.charge()`). That other
1432
+ # class may declare its own unrelated type parameter that
1433
+ # happens to share the same bare name (`T` is the idiomatic
1434
+ # single-letter generic name), so blindly reusing `bounds`
1435
+ # here would rebind its field to the caller's binding by
1436
+ # name collision alone rather than its own — a correctness
1437
+ # bug, not just an over-approximation. Bounds only apply
1438
+ # when the field genuinely belongs to the class they were
1439
+ # collected for; a self-referential chain back to the same
1440
+ # class (`base_id == enclosing_class_id`) is the one case
1441
+ # where reusing them is still correct.
1442
+ hop_bounds = bounds if base_id == enclosing_class_id else {}
1443
+ cid = self._resolve_declared_type(
1444
+ self._apply_type_bound(base_fields[field_name], hop_bounds),
1445
+ module_name, import_map, wildcard_packages,
1446
+ enclosing_class_id,
1447
+ )
1448
+ return (cid, min(base_conf, 0.9)) if cid else (None, 0.0)
1449
+ if self._is_enum_constant(base_id, field_name):
1450
+ # `Mode.INCLUDE.select(x)`: a declared enum constant is a
1451
+ # value receiver of exactly its enum type (batch 25).
1452
+ return base_id, min(base_conf, 0.9)
1453
+ return None, 0.0
1454
+
1455
+ if obj_node.type == "array_access":
1456
+ array_node = obj_node.child_by_field_name("array")
1457
+ if array_node is None:
1458
+ return None, 0.0
1459
+ array_type = self._declared_receiver_type_text(
1460
+ array_node, source, module_name, enclosing_class_id, import_map,
1461
+ wildcard_packages, local_types, field_types, param_types, bounds,
1462
+ )
1463
+ if array_type is None:
1464
+ return None, 0.0
1465
+ erased_array = erased_type(array_type).strip()
1466
+ if not erased_array.endswith("[]"):
1467
+ return None, 0.0
1468
+ element_type = erased_array[:-2].strip()
1469
+ if not element_type or element_type.endswith("[]"):
1470
+ # One access into a multidimensional array still yields an
1471
+ # array, not a source class receiver with ordinary methods.
1472
+ return None, 0.0
1473
+ cid = self._resolve_declared_type(
1474
+ self._apply_type_bound(element_type, bounds), module_name,
1475
+ import_map, wildcard_packages, enclosing_class_id,
1476
+ )
1477
+ confidence = 0.75 if array_node.type == "method_invocation" else 0.9
1478
+ return (cid, confidence) if cid else (None, 0.0)
1479
+
1480
+ if obj_node.type == "method_invocation":
1481
+ return_type = self._generic_method_invocation_return_type(
1482
+ obj_node, source, module_name, enclosing_class_id, import_map,
1483
+ wildcard_packages, local_types, field_types, param_types, bounds,
1484
+ )
1485
+ if return_type is None:
1486
+ return None, 0.0
1487
+ return_bounds = (
1488
+ bounds
1489
+ if self._method_return_uses_current_type_bounds(
1490
+ obj_node, return_type, source, module_name, enclosing_class_id,
1491
+ import_map, wildcard_packages, local_types, field_types,
1492
+ param_types, bounds,
1493
+ )
1494
+ else {}
1495
+ )
1496
+ cid = self._resolve_declared_type(
1497
+ self._apply_type_bound(return_type, return_bounds),
1498
+ module_name, import_map, wildcard_packages,
1499
+ enclosing_class_id,
1500
+ )
1501
+ return (cid, 0.75) if cid else (None, 0.0)
1502
+
1503
+ return None, 0.0
1504
+
1505
+ def _method_return_uses_current_type_bounds(
1506
+ self, method_node, return_type: str, source: bytes, module_name: str,
1507
+ enclosing_class_id: Optional[str], import_map: Dict[str, str],
1508
+ wildcard_packages: List[str], local_types: "_LocalScopes",
1509
+ field_types: Dict[str, str], param_types: Dict[str, str],
1510
+ type_param_bounds: Dict[str, str],
1511
+ ) -> bool:
1512
+ """Whether a method-chain return type may use current generic bounds.
1513
+
1514
+ `Box<T> box; box.value()` may bind `T` from the current scope. But
1515
+ `helper.box.value()` must not bind `Helper`'s own `T` using the
1516
+ caller's unrelated `T`, even when the names collide.
1517
+ """
1518
+ returned = erased_type(return_type).strip()
1519
+ while returned.endswith("[]"):
1520
+ returned = returned[:-2]
1521
+ if returned not in type_param_bounds:
1522
+ return False
1523
+ receiver_node = method_node.child_by_field_name("object")
1524
+ if receiver_node is None:
1525
+ return False
1526
+ receiver_type = self._declared_receiver_type_text(
1527
+ receiver_node, source, module_name, enclosing_class_id, import_map,
1528
+ wildcard_packages, local_types, field_types, param_types,
1529
+ type_param_bounds,
1530
+ )
1531
+ if receiver_type is None:
1532
+ return False
1533
+ receiver_args = {
1534
+ erased_type(arg).strip()
1535
+ for arg in self._generic_type_arguments(receiver_type)
1536
+ }
1537
+ if returned not in receiver_args:
1538
+ return False
1539
+ return self._receiver_expression_uses_current_type_bounds(
1540
+ receiver_node, source, module_name, enclosing_class_id, import_map,
1541
+ wildcard_packages, local_types, field_types, param_types,
1542
+ type_param_bounds,
1543
+ )
1544
+
1545
+ def _receiver_expression_uses_current_type_bounds(
1546
+ self, obj_node, source: bytes, module_name: str,
1547
+ enclosing_class_id: Optional[str], import_map: Dict[str, str],
1548
+ wildcard_packages: List[str], local_types: "_LocalScopes",
1549
+ field_types: Dict[str, str], param_types: Dict[str, str],
1550
+ type_param_bounds: Dict[str, str],
1551
+ ) -> bool:
1552
+ if obj_node.type == "identifier":
1553
+ name = self._text(obj_node, source)
1554
+ if local_types.type_at(name, obj_node.start_byte) is not None:
1555
+ return True
1556
+ if local_types.declared_at(name, obj_node.start_byte):
1557
+ # Shadowed by a local/lambda-parameter of unresolvable type
1558
+ # — must not fall through to an outer param/field of the
1559
+ # same name.
1560
+ return False
1561
+ return name in param_types or name in field_types
1562
+ if obj_node.type == "field_access":
1563
+ inner_obj = obj_node.child_by_field_name("object")
1564
+ if inner_obj is None:
1565
+ return False
1566
+ if inner_obj.type == "this":
1567
+ return enclosing_class_id is not None
1568
+ base_id, _ = self._resolve_receiver(
1569
+ inner_obj, source, module_name, enclosing_class_id, import_map,
1570
+ wildcard_packages, local_types, field_types, param_types,
1571
+ type_param_bounds,
1572
+ )
1573
+ return base_id is not None and base_id == enclosing_class_id
1574
+ if obj_node.type == "method_invocation":
1575
+ return_type = self._generic_method_invocation_return_type(
1576
+ obj_node, source, module_name, enclosing_class_id, import_map,
1577
+ wildcard_packages, local_types, field_types, param_types,
1578
+ type_param_bounds,
1579
+ )
1580
+ return bool(
1581
+ return_type is not None
1582
+ and self._method_return_uses_current_type_bounds(
1583
+ obj_node, return_type, source, module_name,
1584
+ enclosing_class_id, import_map, wildcard_packages,
1585
+ local_types, field_types, param_types, type_param_bounds,
1586
+ )
1587
+ )
1588
+ return False
1589
+
1590
+ def _declared_receiver_type_text(
1591
+ self, obj_node, source: bytes, module_name: str,
1592
+ enclosing_class_id: Optional[str], import_map: Dict[str, str],
1593
+ wildcard_packages: List[str], local_types: "_LocalScopes",
1594
+ field_types: Dict[str, str], param_types: Dict[str, str],
1595
+ type_param_bounds: Dict[str, str],
1596
+ ) -> Optional[str]:
1597
+ """Declared type text for a value receiver, preserving generics."""
1598
+ if obj_node.type in ("string_literal", "text_block"):
1599
+ # A string literal (or text block) receiver is a `java.lang.String`
1600
+ # by JLS 3.10.5 — nothing else can ever be the receiver of
1601
+ # `"...".formatted(a, b)` / `"...".trim()`. This was the missing
1602
+ # receiver case behind JUnit5 FP/FN catalog #16476
1603
+ # (`assertTrue(cond, "...".formatted(first, second))`): with no
1604
+ # declared type for the literal, the chained call's return stayed
1605
+ # unknown, so `_narrow_candidates_by_known_arg_types` could not
1606
+ # exclude the `Supplier<String>` sibling and the call fanned out
1607
+ # across `assertTrue(boolean,String)`/`(boolean,Supplier)` at the
1608
+ # capped 0.55 confidence. The `String` member returns themselves
1609
+ # come from `JAVA_GENERIC_METHOD_STUB_RETURNS`.
1610
+ return "String"
1611
+ if obj_node.type == "class_literal":
1612
+ # `Type.class` is a `Class<Type>` receiver. Its element type is
1613
+ # irrelevant to fixed JDK `Class` member returns such as
1614
+ # `getDeclaredMethod`, but recognizing the receiver lets that
1615
+ # exact declaration participate in contextual constructor typing.
1616
+ return "Class"
1617
+ if obj_node.type == "cast_expression":
1618
+ # JLS 15.16: the type of a cast expression is the cast type —
1619
+ # `((MyId) deserialized).getValue()` (batch 26). `_resolve_receiver`
1620
+ # already read it for the call edge; the fluent/argument typing
1621
+ # path lost it.
1622
+ type_node = obj_node.child_by_field_name("type")
1623
+ return self._text(type_node, source) if type_node is not None else None
1624
+ if obj_node.type == "parenthesized_expression":
1625
+ inner = next((child for child in obj_node.children if child.is_named), None)
1626
+ if inner is None:
1627
+ return None
1628
+ return self._declared_receiver_type_text(
1629
+ inner, source, module_name, enclosing_class_id, import_map,
1630
+ wildcard_packages, local_types, field_types, param_types,
1631
+ type_param_bounds,
1632
+ )
1633
+
1634
+ if obj_node.type == "ternary_expression":
1635
+ consequence = obj_node.child_by_field_name("consequence")
1636
+ alternative = obj_node.child_by_field_name("alternative")
1637
+ if consequence is None or alternative is None:
1638
+ return None
1639
+ branch_types = [
1640
+ self._declared_receiver_type_text(
1641
+ branch, source, module_name, enclosing_class_id, import_map,
1642
+ wildcard_packages, local_types, field_types, param_types,
1643
+ type_param_bounds,
1644
+ )
1645
+ for branch in (consequence, alternative)
1646
+ ]
1647
+ if any(branch_type is None for branch_type in branch_types):
1648
+ return None
1649
+ # A ternary whose two branches have precisely the same declared
1650
+ # type needs no least-upper-bound inference. This is especially
1651
+ # useful for external generic receivers such as
1652
+ # `Stream<JavaPackage>`: source identity cannot resolve `Stream`,
1653
+ # but retaining equal text is exact. Differing branch types still
1654
+ # remain unresolved rather than being guessed.
1655
+ if branch_types[0] == branch_types[1]:
1656
+ return branch_types[0]
1657
+ # One branch may spell a JDK owner as its declaring file imports
1658
+ # it (`java.util.stream.Stream<pkg.JavaPackage>` from a source
1659
+ # method's canonicalized return) while the other comes from an
1660
+ # exact JDK stub (`Stream<JavaPackage>`). Both name one type;
1661
+ # unify them structurally without guessing a least upper bound.
1662
+ equivalent = self._equivalent_type_texts(
1663
+ branch_types[0], branch_types[1], module_name, import_map,
1664
+ wildcard_packages, enclosing_class_id,
1665
+ )
1666
+ if equivalent is not None:
1667
+ return equivalent
1668
+ branch_ids = [
1669
+ self._resolve_declared_type(
1670
+ branch_type, module_name, import_map, wildcard_packages,
1671
+ enclosing_class_id,
1672
+ )
1673
+ for branch_type in branch_types
1674
+ ]
1675
+ if branch_ids[0] is None or branch_ids[0] != branch_ids[1]:
1676
+ return None
1677
+ return branch_types[0]
1678
+
1679
+ if obj_node.type == "identifier":
1680
+ name = self._text(obj_node, source)
1681
+ local = local_types.type_at(name, obj_node.start_byte)
1682
+ if local is not None:
1683
+ return self._apply_type_bound(local, type_param_bounds)
1684
+ if local_types.declared_at(name, obj_node.start_byte):
1685
+ # Shadowed by a local/lambda-parameter of unresolvable type
1686
+ # — must not fall through to an outer param/field of the
1687
+ # same name, which this identifier no longer names here.
1688
+ return None
1689
+ if name in param_types:
1690
+ return self._apply_type_bound(param_types[name], type_param_bounds)
1691
+ if name in field_types:
1692
+ return self._apply_type_bound(field_types[name], type_param_bounds)
1693
+ # An explicitly static-imported source field or enum constant
1694
+ # (`import static ...EnumSource.Mode.INCLUDE; INCLUDE.select(x)`)
1695
+ # is a value receiver with a declared type — batch 25: the
1696
+ # chained source call's `boolean` return used to stay unknown,
1697
+ # so `assertTrue(INCLUDE.select(...))` fanned out to the
1698
+ # `BooleanSupplier` sibling. Same exact-import-only lookup the
1699
+ # argument path already trusts; wildcard static imports stay
1700
+ # unresolved.
1701
+ imported = self._static_imported_field_arg_type(
1702
+ obj_node, source, module_name, enclosing_class_id, import_map,
1703
+ wildcard_packages, local_types, param_types, field_types,
1704
+ )
1705
+ if imported is not None:
1706
+ return imported
1707
+ # A value declaration always wins above, but an otherwise-unbound
1708
+ # identifier may be a type receiver (`CommandLine.builder()`).
1709
+ # Preserve that type text so the source method's declared return
1710
+ # type can seed the next fluent receiver. `_resolve_declared_type`
1711
+ # keeps this source-only and import-aware; an unknown/external
1712
+ # type therefore remains unresolved instead of becoming a global
1713
+ # simple-name guess.
1714
+ if self._resolve_declared_type(
1715
+ name, module_name, import_map, wildcard_packages,
1716
+ ) is not None:
1717
+ return name
1718
+ return None
1719
+
1720
+ if obj_node.type == "field_access":
1721
+ inner_obj = obj_node.child_by_field_name("object")
1722
+ field_node = obj_node.child_by_field_name("field")
1723
+ if field_node is None:
1724
+ return None
1725
+ field_name = self._text(field_node, source)
1726
+ if inner_obj is not None and inner_obj.type == "this":
1727
+ if enclosing_class_id is None:
1728
+ return None
1729
+ return self._all_field_types(enclosing_class_id).get(field_name)
1730
+ if inner_obj is None:
1731
+ return None
1732
+ base_id, _ = self._resolve_receiver(
1733
+ inner_obj, source, module_name, enclosing_class_id, import_map,
1734
+ wildcard_packages, local_types, field_types, param_types,
1735
+ type_param_bounds,
1736
+ )
1737
+ if base_id is not None:
1738
+ declared_field = self._all_field_types(base_id).get(field_name)
1739
+ if declared_field is not None:
1740
+ return declared_field
1741
+ if self._is_enum_constant(base_id, field_name):
1742
+ # `Mode.INCLUDE.select(x)`: every declared enum constant
1743
+ # has exactly its enum type (batch 25; same metadata-
1744
+ # backed rule `_source_static_field_arg_type` applies to
1745
+ # an enum-constant *argument*). Spelled as the call site
1746
+ # spells the enum, which already resolved to `base_id`.
1747
+ return self._text(inner_obj, source)
1748
+ # A dotted nested type has the same CST shape as a field access:
1749
+ # `EventPublication.Status.values()` presents
1750
+ # `EventPublication.Status` here. Only accept the complete text
1751
+ # when strict source type resolution succeeds; an ordinary
1752
+ # unresolved `object.field` remains unknown.
1753
+ qualified_type = self._text(obj_node, source)
1754
+ if self._resolve_declared_type(
1755
+ qualified_type, module_name, import_map, wildcard_packages,
1756
+ enclosing_class_id,
1757
+ ) is not None:
1758
+ return qualified_type
1759
+ return None
1760
+
1761
+ if obj_node.type == "array_access":
1762
+ # Preserve the element's declared type for a chained source
1763
+ # return lookup: `visits[0].getPet().getId()`. `_resolve_receiver`
1764
+ # already resolves `visits[0]` well enough to emit `getPet`, but
1765
+ # the outer hop asks this text helper for the inner call's
1766
+ # receiver type. Without the symmetric array case here, that
1767
+ # lookup stopped at the inner edge and silently dropped `getId`.
1768
+ array_node = obj_node.child_by_field_name("array")
1769
+ if array_node is None:
1770
+ return None
1771
+ array_type = self._declared_receiver_type_text(
1772
+ array_node, source, module_name, enclosing_class_id,
1773
+ import_map, wildcard_packages, local_types, field_types,
1774
+ param_types, type_param_bounds,
1775
+ )
1776
+ if array_type is None:
1777
+ return None
1778
+ erased_array = erased_type(array_type).strip()
1779
+ if not erased_array.endswith("[]"):
1780
+ return None
1781
+ return erased_array[:-2].strip() or None
1782
+
1783
+ if obj_node.type == "method_invocation":
1784
+ return self._generic_method_invocation_return_type(
1785
+ obj_node, source, module_name, enclosing_class_id, import_map,
1786
+ wildcard_packages, local_types, field_types, param_types,
1787
+ type_param_bounds,
1788
+ )
1789
+ return None
1790
+
1791
+
1792
+ def _generic_method_invocation_return_type(
1793
+ self, call_node, source: bytes, module_name: str,
1794
+ enclosing_class_id: Optional[str], import_map: Dict[str, str],
1795
+ wildcard_packages: List[str], local_types: "_LocalScopes",
1796
+ field_types: Dict[str, str], param_types: Dict[str, str],
1797
+ type_param_bounds: Dict[str, str],
1798
+ ) -> Optional[str]:
1799
+ """Best-effort generic return type for chained receiver calls.
1800
+
1801
+ Source-declared generic methods are resolved from the receiver's raw
1802
+ class and the target method's persisted return type, then type
1803
+ variables are substituted from the receiver's actual type arguments
1804
+ (`Box<? extends Gateway>.value() -> Gateway`). JDK/container methods
1805
+ whose declarations are outside the analyzed source use a small
1806
+ signature table with the same substitution path.
1807
+ """
1808
+ name_node = call_node.child_by_field_name("name")
1809
+ receiver_node = call_node.child_by_field_name("object")
1810
+ if name_node is None:
1811
+ return None
1812
+ method_name = self._text(name_node, source)
1813
+ if (
1814
+ method_name in JAVA_OBJECT_MEMBER_RETURNS
1815
+ and self._count_args(call_node.child_by_field_name("arguments")) == 0
1816
+ ):
1817
+ # `x.toString()` is a `String` for every `x` (JLS 4.3.2), whether
1818
+ # or not the receiver or an override is resolvable.
1819
+ return JAVA_OBJECT_MEMBER_RETURNS[method_name]
1820
+ if method_name == "assertThat":
1821
+ # AssertJ's `assertThat(actual)` has no source declaration to
1822
+ # resolve structurally (external library); recognize it by name
1823
+ # + owner so a following fluent `hasValueSatisfying`/`satisfies`/
1824
+ # `allSatisfy` lambda still learns `actual`'s real element type
1825
+ # instead of losing inference at this external hop. Applies to
1826
+ # both the bare, statically-imported call and the explicitly
1827
+ # qualified `Assertions.assertThat(actual)` form — both reach
1828
+ # this same early check via `receiver_node` below.
1829
+ assertj_return = self._assertj_assertthat_return_type(
1830
+ call_node, source, module_name, enclosing_class_id, import_map,
1831
+ wildcard_packages, local_types, param_types, field_types,
1832
+ )
1833
+ if assertj_return is not None:
1834
+ return assertj_return
1835
+ if receiver_node is None or receiver_node.type == "this":
1836
+ if receiver_node is None and method_name in import_map:
1837
+ # A bare call can be an explicitly static-imported source
1838
+ # factory. Its result type belongs to the imported class, not
1839
+ # to the caller's enclosing class. Without this branch,
1840
+ # `import static Builder.request; request().step()` resolves
1841
+ # the root call but loses every fluent hop, especially when
1842
+ # the factory lives in another build module.
1843
+ resolved_import = import_map[method_name]
1844
+ owner_path = ".".join(resolved_import.split(".")[:-1])
1845
+ imported_owner = self._resolve_static_import_class(
1846
+ owner_path, module_name,
1847
+ )
1848
+ if imported_owner is not None and ":" in imported_owner:
1849
+ owner_module, owner_qualname = imported_owner.rsplit(":", 1)
1850
+ receiver_type = (
1851
+ f"{self.builder._display_module(owner_module)}."
1852
+ f"{owner_qualname}"
1853
+ )
1854
+ else:
1855
+ receiver_type = None
1856
+ else:
1857
+ receiver_type = None
1858
+ if receiver_type is None:
1859
+ if enclosing_class_id is None or ":" not in enclosing_class_id:
1860
+ return None
1861
+ # A chained receiver may itself be a bare/this source call:
1862
+ # `getOptions().hasOption(x)`. The call's dispatch root is the
1863
+ # enclosing class (plus its ancestors), even though there is no
1864
+ # explicit receiver CST node from which to read a declared type.
1865
+ # Preserve the exact class id as a dotted type so the existing
1866
+ # strict source-method/overload/return-context path can be reused
1867
+ # without a global simple-name fallback.
1868
+ owner_module, owner_qualname = enclosing_class_id.rsplit(":", 1)
1869
+ receiver_type = (
1870
+ f"{self.builder._display_module(owner_module)}.{owner_qualname}"
1871
+ )
1872
+ elif receiver_node.type == "object_creation_expression":
1873
+ # One precise C08 continuation: after `new T(...).step(args)`
1874
+ # has a uniquely selected source method, its declared return may
1875
+ # seed the immediately following hop. The created type is still
1876
+ # resolved strictly; `_generic_return_type_from_source_method`
1877
+ # below refuses ambiguous overload targets, so this does not turn
1878
+ # unknown root-new calls into chain fan-out.
1879
+ type_node = receiver_node.child_by_field_name("type")
1880
+ if type_node is None:
1881
+ return None
1882
+ created_type = self._text(type_node, source)
1883
+ if self._resolve_declared_type(
1884
+ created_type, module_name, import_map, wildcard_packages,
1885
+ enclosing_class_id,
1886
+ ) is None:
1887
+ return None
1888
+ receiver_type = created_type
1889
+ else:
1890
+ receiver_type = self._declared_receiver_type_text(
1891
+ receiver_node, source, module_name, enclosing_class_id, import_map,
1892
+ wildcard_packages, local_types, field_types, param_types,
1893
+ type_param_bounds,
1894
+ )
1895
+ if receiver_type is None:
1896
+ # Static JDK factories have no source class node, hence cannot
1897
+ # supply a declared receiver type. Consult the intentionally
1898
+ # tiny exact table before giving up on a following fluent hop.
1899
+ return self._external_static_call_return_type(
1900
+ call_node, source, module_name, enclosing_class_id, import_map,
1901
+ wildcard_packages, local_types, param_types, field_types,
1902
+ )
1903
+ receiver_args = self._generic_type_arguments(receiver_type)
1904
+ raw_simple = erased_type(receiver_type).strip().split(".")[-1]
1905
+
1906
+ # `BeanFactory.getBean(Class<T>)` (JLS-external, Spring-documented
1907
+ # contract — see `SPRING_BEAN_CONTAINER_TYPES`) returns exactly the
1908
+ # argument's own `Class<T>` literal type. Restricted to the single-
1909
+ # `Class<T>`-literal overload: `getBean(String)`/`getBean(String,
1910
+ # Class<T>)` name a bean by id, not by this argument, and stay
1911
+ # unresolved rather than guessed.
1912
+ if raw_simple in SPRING_BEAN_CONTAINER_TYPES and method_name == "getBean":
1913
+ args_node = call_node.child_by_field_name("arguments")
1914
+ args = argument_nodes(args_node) if args_node is not None else []
1915
+ if len(args) == 1 and args[0].type == "class_literal":
1916
+ literal = next(
1917
+ (child for child in args[0].children if child.is_named), None,
1918
+ )
1919
+ if literal is not None:
1920
+ return self._text(literal, source)
1921
+ return None
1922
+
1923
+ # `Optional<T>.orElse(fallback)` returns T only when the fallback is
1924
+ # assignable to T (or is null). Unlike `get`/`orElseThrow`, blindly
1925
+ # substituting T would hide an invalid or otherwise unknown fallback
1926
+ # and could give a following `var` receiver a wrong source target.
1927
+ # Keep this to one exact argument and the normal overload-compatible
1928
+ # type check; poly expressions remain unresolved.
1929
+ if raw_simple == "Optional" and method_name == "orElse" and len(receiver_args) == 1:
1930
+ args_node = call_node.child_by_field_name("arguments")
1931
+ fallback_types = self._infer_contextual_arg_types(
1932
+ args_node, source, module_name, enclosing_class_id, import_map,
1933
+ wildcard_packages, local_types, param_types, field_types,
1934
+ type_param_bounds,
1935
+ )
1936
+ if fallback_types is not None and len(fallback_types) == 1:
1937
+ element_type = receiver_args[0]
1938
+ fallback_type = fallback_types[0]
1939
+ ctx = _ImportContext(
1940
+ module_name, import_map, wildcard_packages, enclosing_class_id,
1941
+ )
1942
+ if fallback_type == "null" or self._types_compatible(
1943
+ erased_type(element_type), fallback_type, ctx, ctx,
1944
+ ):
1945
+ return element_type
1946
+ return None
1947
+
1948
+ # A directly invoked source method in an Optional mapper has a
1949
+ # declared result even when the mapper's external input type cannot
1950
+ # be reconstructed. Preserve only that exact expression-lambda case
1951
+ # so `optional.map(x -> sourceFactory(x)).orElseThrow()` can type a
1952
+ # following `var`; arbitrary mapper bodies remain unresolved.
1953
+ # The same exact expression-lambda rule types `Stream.map(x ->
1954
+ # sourceCall(x))` as `Stream<R>`, and `flatMap` when the body's
1955
+ # declared result is itself the container type.
1956
+ if raw_simple in {"Optional", "Stream"} and method_name in {"map", "flatMap"}:
1957
+ args_node = call_node.child_by_field_name("arguments")
1958
+ args = argument_nodes(args_node)
1959
+ if len(args) == 1 and args[0].type == "lambda_expression":
1960
+ body = args[0].child_by_field_name("body")
1961
+ if body is not None and body.type in {
1962
+ "method_invocation", "object_creation_expression",
1963
+ }:
1964
+ mapped = self._declared_receiver_type_text(
1965
+ body, source, module_name, enclosing_class_id, import_map,
1966
+ wildcard_packages, local_types, field_types, param_types,
1967
+ type_param_bounds,
1968
+ )
1969
+ if mapped is not None:
1970
+ if method_name == "map":
1971
+ return f"{raw_simple}<{mapped}>"
1972
+ if erased_type(mapped).rsplit(".", 1)[-1] == raw_simple:
1973
+ return mapped
1974
+ # A static method reference (`Type::staticMethod`) as the sole
1975
+ # mapper argument has a declared result the same way an
1976
+ # expression-lambda body does above; `Stream.map`/`.flatMap`
1977
+ # already resolve a method reference argument for the *unbound-
1978
+ # instance* shape (`Type::instanceMethod`, zero extra params,
1979
+ # element itself the receiver) further below, but not this
1980
+ # *static*, one-parameter shape (`Optional.map(Owner::create)`,
1981
+ # the real `AdditionalDiscoverySelectors::selectDirectories`
1982
+ # JUnit5 pattern) for either container -- required one static
1983
+ # candidate, exactly as conservative as the lambda case (an
1984
+ # overloaded or ambiguous reference stays unresolved).
1985
+ if len(args) == 1 and args[0].type == "method_reference":
1986
+ ref_node = args[0]
1987
+ named = [child for child in ref_node.children if child.is_named]
1988
+ terminal = next(
1989
+ (child for child in reversed(ref_node.children)
1990
+ if child.type in {"identifier", "new"}),
1991
+ None,
1992
+ )
1993
+ if named and terminal is not None and terminal.type != "new":
1994
+ owner_id = self._resolve_declared_type(
1995
+ self._text(named[0], source), module_name, import_map,
1996
+ wildcard_packages, enclosing_class_id,
1997
+ )
1998
+ if owner_id is not None:
1999
+ candidates = [
2000
+ candidate for candidate in self._members_named(
2001
+ owner_id, self._text(terminal, source),
2002
+ search_ancestors=True,
2003
+ )
2004
+ if (
2005
+ self._is_static_member(candidate)
2006
+ and len(self._param_type_lists.get(candidate, [])) == 1
2007
+ )
2008
+ ]
2009
+ if len(candidates) == 1:
2010
+ candidate = self.builder._nodes.get(candidates[0])
2011
+ result_type = (
2012
+ candidate.metadata.get("java_return_type")
2013
+ if candidate is not None else None
2014
+ )
2015
+ if isinstance(result_type, str) and result_type != "void":
2016
+ if method_name == "map":
2017
+ return f"{raw_simple}<{result_type}>"
2018
+ if erased_type(result_type).rsplit(".", 1)[-1] == raw_simple:
2019
+ return result_type
2020
+
2021
+ # `Function<T,R>.andThen(f)` consumes R and returns `Function<T,V>`.
2022
+ # The bounded `Class::getName` case is sufficient for the source
2023
+ # chain `toEventType().andThen(Class::getName).andThen(name -> ...)`;
2024
+ # other method references remain unknown instead of guessing V.
2025
+ if raw_simple == "Function" and method_name == "andThen" and len(receiver_args) == 2:
2026
+ args_node = call_node.child_by_field_name("arguments")
2027
+ arg_nodes = argument_nodes(args_node)
2028
+ if len(arg_nodes) == 1 and arg_nodes[0].type == "method_reference":
2029
+ named = [child for child in arg_nodes[0].children if child.is_named]
2030
+ terminal = next(
2031
+ (child for child in reversed(arg_nodes[0].children)
2032
+ if child.type in {"identifier", "new"}),
2033
+ None,
2034
+ )
2035
+ if (
2036
+ len(named) >= 1
2037
+ and terminal is not None
2038
+ and self._text(named[0], source) == "Class"
2039
+ and self._text(terminal, source) == "getName"
2040
+ ):
2041
+ return f"Function<{receiver_args[0]},String>"
2042
+
2043
+ source_return = self._generic_return_type_from_source_method(
2044
+ receiver_type, receiver_args, method_name, call_node, source,
2045
+ module_name, import_map, wildcard_packages, local_types, field_types,
2046
+ param_types, enclosing_class_id,
2047
+ )
2048
+ if source_return is not None:
2049
+ return source_return
2050
+ if raw_simple in JAVA_TIME_TEMPORAL_TYPES:
2051
+ if (
2052
+ method_name in JAVA_TIME_SELF_RETURNING_EXACT
2053
+ or method_name.startswith(JAVA_TIME_SELF_RETURNING_PREFIXES)
2054
+ ):
2055
+ return raw_simple
2056
+ converted = JAVA_TIME_CONVERSION_RETURNS.get((raw_simple, method_name))
2057
+ if converted is not None:
2058
+ return converted
2059
+ if receiver_node is None and enclosing_class_id is not None:
2060
+ # A bare call in a non-static nested class may resolve through an
2061
+ # enclosing instance (`Inner.this` semantics). `_resolve_call`
2062
+ # already emits that root edge, but `var`/fluent propagation used
2063
+ # to inspect only the inner class and therefore lost the outer
2064
+ # method's declared return type. Walk lexical owners nearest-first
2065
+ # after the current class has failed; a static nested boundary has
2066
+ # no enclosing instance beyond itself.
2067
+ #
2068
+ # Batch 25: a *static* nested class has no enclosing instance,
2069
+ # but its bare calls still reach every lexically enclosing
2070
+ # class's `static` methods (JLS 8.1.3 / 15.12.1) —
2071
+ # `assumeTrue(makeReadOnly(dir))` inside `static class
2072
+ # ReadOnlyFileTestCase` calls the outer test class's `private
2073
+ # static boolean makeReadOnly(File)`. The walk used to stop dead
2074
+ # at the static boundary, so the argument's `boolean` return
2075
+ # stayed unknown and the call fanned out to the
2076
+ # `assumeTrue(BooleanSupplier)` sibling at 0.55. Once any class
2077
+ # on the way up is static, only static outer members remain
2078
+ # reachable, so the walk continues in static-only mode instead
2079
+ # of breaking.
2080
+ current = self.builder._nodes.get(enclosing_class_id)
2081
+ static_only = (
2082
+ current is not None
2083
+ and "static" in current.metadata.get("modifiers", ())
2084
+ )
2085
+ for outer_id in self._lexical_outer_class_ids(enclosing_class_id):
2086
+ outer_module, outer_qualname = outer_id.rsplit(":", 1)
2087
+ outer_type = (
2088
+ f"{self.builder._display_module(outer_module)}.{outer_qualname}"
2089
+ )
2090
+ source_return = self._generic_return_type_from_source_method(
2091
+ outer_type, [], method_name, call_node, source,
2092
+ module_name, import_map, wildcard_packages, local_types,
2093
+ field_types, param_types, enclosing_class_id,
2094
+ static_only=static_only,
2095
+ )
2096
+ if source_return is not None:
2097
+ return source_return
2098
+ outer = self.builder._nodes.get(outer_id)
2099
+ if outer is not None and "static" in outer.metadata.get("modifiers", ()):
2100
+ static_only = True
2101
+
2102
+ external_static_return = self._external_static_call_return_type(
2103
+ call_node, source, module_name, enclosing_class_id, import_map,
2104
+ wildcard_packages, local_types, param_types, field_types,
2105
+ )
2106
+ if external_static_return is not None:
2107
+ return external_static_return
2108
+
2109
+ # `Stream.map(Function<T,R>)` changes the element type to the
2110
+ # functional argument's declared result type. Preserve that exact R
2111
+ # for a following fluent hop when the callable is a typed
2112
+ # local/parameter/field; unknown lambdas and overloaded references
2113
+ # remain unresolved rather than guessed.
2114
+ if raw_simple == "Stream" and method_name in {"map", "flatMap"}:
2115
+ args_node = call_node.child_by_field_name("arguments")
2116
+ arg_nodes = argument_nodes(args_node)
2117
+ if len(arg_nodes) == 1 and arg_nodes[0].type == "identifier":
2118
+ argument_name = self._text(arg_nodes[0], source)
2119
+ functional_type = (
2120
+ local_types.type_at(argument_name, arg_nodes[0].start_byte)
2121
+ or param_types.get(argument_name)
2122
+ or field_types.get(argument_name)
2123
+ )
2124
+ functional_args = (
2125
+ self._generic_type_arguments(functional_type)
2126
+ if functional_type is not None else []
2127
+ )
2128
+ if len(functional_args) >= 2:
2129
+ result_type = functional_args[-1]
2130
+ if method_name == "flatMap" and erased_type(result_type).rsplit(".", 1)[-1] == "Stream":
2131
+ return result_type
2132
+ if method_name == "map":
2133
+ return f"Stream<{result_type}>"
2134
+ if len(arg_nodes) == 1 and arg_nodes[0].type == "method_reference":
2135
+ ref_node = arg_nodes[0]
2136
+ named = [child for child in ref_node.children if child.is_named]
2137
+ terminal = next(
2138
+ (child for child in reversed(ref_node.children)
2139
+ if child.type in {"identifier", "new"}),
2140
+ None,
2141
+ )
2142
+ if named and terminal is not None and terminal.type != "new":
2143
+ owner_id = self._resolve_declared_type(
2144
+ self._text(named[0], source), module_name, import_map,
2145
+ wildcard_packages, enclosing_class_id,
2146
+ )
2147
+ if owner_id is not None:
2148
+ candidates = [
2149
+ candidate for candidate in self._members_named(
2150
+ owner_id, self._text(terminal, source),
2151
+ search_ancestors=True,
2152
+ )
2153
+ if len(self._param_type_lists.get(candidate, [])) == 0
2154
+ ]
2155
+ if len(candidates) == 1:
2156
+ candidate = self.builder._nodes.get(candidates[0])
2157
+ result_type = (
2158
+ candidate.metadata.get("java_return_type")
2159
+ if candidate is not None else None
2160
+ )
2161
+ if isinstance(result_type, str) and result_type != "void":
2162
+ if method_name == "flatMap" and (
2163
+ erased_type(result_type).rsplit(".", 1)[-1] == "Stream"
2164
+ ):
2165
+ return result_type
2166
+ if method_name == "map":
2167
+ return f"Stream<{result_type}>"
2168
+
2169
+ # `getClass()` is final on `java.lang.Object`, so every receiver
2170
+ # yields `Class<?>` and no source class can redeclare it.
2171
+ if method_name == "getClass" and self._count_args(
2172
+ call_node.child_by_field_name("arguments")
2173
+ ) == 0:
2174
+ return "java.lang.Class<?>"
2175
+
2176
+ stub_return = JAVA_GENERIC_METHOD_STUB_RETURNS.get((raw_simple, method_name))
2177
+ receiver_class_id = self._resolve_declared_type(
2178
+ receiver_type, module_name, import_map, wildcard_packages,
2179
+ enclosing_class_id,
2180
+ )
2181
+ if receiver_class_id is not None:
2182
+ # The stub table describes JDK declarations. A source class that
2183
+ # merely shares a JDK simple name (`Class`, `Method`) has already
2184
+ # had its own members consulted above and must not inherit JDK
2185
+ # facts *by name*. Two exact, declaration-backed cases remain
2186
+ # (batch 26), both gated on the hierarchy declaring no member of
2187
+ # that name at all:
2188
+ if self._members_named(receiver_class_id, method_name, search_ancestors=True):
2189
+ return None
2190
+ arg_count = self._count_args(call_node.child_by_field_name("arguments"))
2191
+ # JLS 8.10.3: a record's implicit accessor `exitCode()` returns
2192
+ # the component's declared type. The extractor records
2193
+ # components as `record_components`; there is no method node.
2194
+ record_node = self.builder._nodes.get(receiver_class_id)
2195
+ components = record_node.metadata.get("record_components", {}) if record_node else {}
2196
+ if arg_count == 0 and method_name in components:
2197
+ component_type = components[method_name]
2198
+ if erased_type(component_type).strip() in self._class_type_params.get(receiver_class_id, []):
2199
+ return None
2200
+ return component_type
2201
+ # JLS 8.4.8: a source class inherits its external superclass's
2202
+ # members — `exception.getMessage()` on a project exception
2203
+ # extending `RuntimeException` is `Throwable.getMessage()`. Only
2204
+ # the nearest external ancestors the declaring files actually
2205
+ # import (or `java.lang`) are consulted, with the same fixed-
2206
+ # return facts; a generic stub whose result is the JDK class's
2207
+ # own type variable stays unknown (no receiver arguments here).
2208
+ for external_simple in self._external_ancestor_simple_names(receiver_class_id):
2209
+ inherited = JAVA_GENERIC_METHOD_STUB_RETURNS.get((external_simple, method_name))
2210
+ if inherited is None and external_simple in JAVA_THROWABLE_SIMPLE_TYPES:
2211
+ inherited = JAVA_GENERIC_METHOD_STUB_RETURNS.get(("Throwable", method_name))
2212
+ if inherited is None:
2213
+ continue
2214
+ if erased_type(inherited).strip() in JAVA_GENERIC_TYPE_PARAM_STUBS.get(external_simple, []):
2215
+ return None
2216
+ return inherited
2217
+ return None
2218
+ if stub_return is None:
2219
+ return None
2220
+ type_params = JAVA_GENERIC_TYPE_PARAM_STUBS.get(raw_simple, [])
2221
+ return self._substitute_generic_return_type(
2222
+ stub_return, type_params, receiver_args,
2223
+ )
2224
+
2225
+ def _external_ancestor_simple_names(self, class_id: str) -> List[str]:
2226
+ """Simple names of the external (JDK/library) supertypes a source
2227
+ class reaches through its own or any source ancestor's `extends`/
2228
+ `implements` clause, nearest first — each spelling resolved in its
2229
+ declaring file's import context (`_external_type_name`), never by
2230
+ simple name alone."""
2231
+ out: List[str] = []
2232
+ for owner_id in (class_id, *self._class_ancestors(class_id)):
2233
+ owner_node = self.builder._nodes.get(owner_id)
2234
+ owner_ctx = (
2235
+ self._file_ctx.get(owner_node.file_path) if owner_node is not None else None
2236
+ )
2237
+ if owner_node is None or owner_ctx is None:
2238
+ continue
2239
+ declared_supers = (
2240
+ *owner_node.metadata.get("java_superclass_args", ()),
2241
+ *owner_node.metadata.get("java_implements_args", ()),
2242
+ )
2243
+ for declared_super in declared_supers:
2244
+ super_fqn = self._external_type_name(erased_type(declared_super).strip(), owner_ctx)
2245
+ if super_fqn is None:
2246
+ continue
2247
+ simple = super_fqn.rsplit(".", 1)[-1]
2248
+ if simple not in out:
2249
+ out.append(simple)
2250
+ return out
2251
+
2252
+ def _assertj_assertthat_return_type(
2253
+ self, call_node, source: bytes, module_name: str,
2254
+ enclosing_class_id: Optional[str], import_map: Dict[str, str],
2255
+ wildcard_packages: List[str], local_types: "_LocalScopes",
2256
+ param_types: Dict[str, str], field_types: Dict[str, str],
2257
+ ) -> Optional[str]:
2258
+ """Wrapper type for `org.assertj.core.api.Assertions.assertThat
2259
+ (actual)`, derived from `actual`'s own declared type text.
2260
+
2261
+ AssertJ's real `assertThat` overload set (dozens of `Optional`/
2262
+ `Map`/`Iterable`/generic-`Object` overloads, each returning a
2263
+ distinct `*Assert` wrapper) has no source declaration for this
2264
+ resolver to look up structurally. Recognizing just the calling
2265
+ convention -- one argument, whose type is already known here -- is
2266
+ enough to hand a following fluent `hasValueSatisfying`/`satisfies`/
2267
+ `allSatisfy` lambda its real element type one hop later, without
2268
+ emitting a fabricated call edge to AssertJ itself (this function
2269
+ only ever feeds `_invocation_functional_target_type`'s lambda-typing
2270
+ path, never `_resolve_call`).
2271
+ """
2272
+ receiver_node = call_node.child_by_field_name("object")
2273
+ if receiver_node is None:
2274
+ owner_fqn = import_map.get("assertThat")
2275
+ if owner_fqn is not None:
2276
+ owner_path = owner_fqn.rsplit(".", 1)[0]
2277
+ else:
2278
+ # `assertThat` almost always reaches a file through
2279
+ # `import static org.assertj.core.api.Assertions.*;`, not
2280
+ # an explicit single import of the one method — `import_map`
2281
+ # (single imports only) misses this convention entirely,
2282
+ # silently disabling this whole AssertJ lambda-typing path
2283
+ # for every file that uses it (spring-modulith benchmark
2284
+ # finding: 119/~575 files, 0 using the single-name form).
2285
+ # `enclosing_class_id`'s own file carries the same static
2286
+ # wildcard imports as the call site inside it.
2287
+ enclosing = (
2288
+ self.builder._nodes.get(enclosing_class_id)
2289
+ if enclosing_class_id else None
2290
+ )
2291
+ wildcard_classes = (
2292
+ self._static_wildcard_by_file.get(enclosing.file_path, [])
2293
+ if enclosing is not None else []
2294
+ )
2295
+ owner_path = next(
2296
+ (o for o in ASSERTJ_ASSERT_FACTORY_OWNERS if o in wildcard_classes),
2297
+ None,
2298
+ )
2299
+ if owner_path is None:
2300
+ return None
2301
+ elif receiver_node.type == "identifier":
2302
+ owner_path = import_map.get(self._text(receiver_node, source))
2303
+ if owner_path is None:
2304
+ return None
2305
+ else:
2306
+ return None
2307
+ if owner_path not in ASSERTJ_ASSERT_FACTORY_OWNERS:
2308
+ return None
2309
+ args_node = call_node.child_by_field_name("arguments")
2310
+ args = argument_nodes(args_node) if args_node is not None else []
2311
+ if len(args) != 1:
2312
+ return None
2313
+ actual_type = self._declared_receiver_type_text(
2314
+ args[0], source, module_name, enclosing_class_id, import_map,
2315
+ wildcard_packages, local_types, field_types, param_types, {},
2316
+ )
2317
+ if actual_type is None:
2318
+ return None
2319
+ raw = erased_type(actual_type).rsplit(".", 1)[-1]
2320
+ generic_args = self._generic_type_arguments(actual_type)
2321
+ if raw == "Optional" and len(generic_args) == 1:
2322
+ return f"org.assertj.core.api.OptionalAssert<{generic_args[0]}>"
2323
+ if raw in {
2324
+ "Map", "HashMap", "LinkedHashMap", "TreeMap", "ConcurrentMap",
2325
+ } and len(generic_args) == 2:
2326
+ return f"org.assertj.core.api.MapAssert<{generic_args[0]},{generic_args[1]}>"
2327
+ if raw in {
2328
+ "List", "ArrayList", "LinkedList", "Set", "HashSet", "SortedSet",
2329
+ "Collection", "Iterable",
2330
+ } and len(generic_args) == 1:
2331
+ return f"org.assertj.core.api.ListAssert<{generic_args[0]}>"
2332
+ if raw and "." not in raw and raw not in _PRIMITIVE_TYPE_NAMES:
2333
+ # A plain source/JDK object type: AssertJ's generic
2334
+ # `assertThat(T actual)` overload returns `ObjectAssert<T>`.
2335
+ return f"org.assertj.core.api.ObjectAssert<{actual_type}>"
2336
+ return None
2337
+
2338
+ def _generic_return_type_from_source_method(
2339
+ self, receiver_type: str, receiver_args: List[str], method_name: str,
2340
+ call_node, source: bytes, module_name: str, import_map: Dict[str, str],
2341
+ wildcard_packages: List[str], local_types: "_LocalScopes",
2342
+ field_types: Dict[str, str], param_types: Dict[str, str],
2343
+ enclosing_class_id: Optional[str] = None,
2344
+ static_only: bool = False,
2345
+ ) -> Optional[str]:
2346
+ """Declared return type of the single source method a call selects
2347
+ on `receiver_type` (own or inherited), or None when no unique
2348
+ target exists. `static_only` restricts selection to `static`
2349
+ members — used for a bare call walked up to a lexically enclosing
2350
+ class from a *static* nested class, which has no enclosing instance
2351
+ and can therefore only reach the outer class's static methods (JLS
2352
+ 8.1.3 / 15.12.1)."""
2353
+ receiver_class_id = self._resolve_declared_type(
2354
+ receiver_type, module_name, import_map, wildcard_packages,
2355
+ enclosing_class_id,
2356
+ )
2357
+ if receiver_class_id is None:
2358
+ return None
2359
+ args_node = call_node.child_by_field_name("arguments")
2360
+ arg_count = self._count_args(args_node)
2361
+ arg_types = self._infer_arg_types(
2362
+ args_node, source, local_types, param_types, field_types,
2363
+ )
2364
+ candidates = self._members_named(
2365
+ receiver_class_id, method_name, search_ancestors=True,
2366
+ )
2367
+ # A unique zero-argument source method needs neither overload ranking
2368
+ # nor argument/generic binding to choose its declared return. Keep
2369
+ # every non-zero-arg or ambiguous shape on the full resolver below.
2370
+ direct_candidates = [
2371
+ candidate for candidate in candidates
2372
+ if self._arity_matches(candidate, 0)
2373
+ and (not static_only or self._is_static_member(candidate))
2374
+ ] if arg_count == 0 else []
2375
+ if len(direct_candidates) == 1:
2376
+ target_ids = direct_candidates
2377
+ else:
2378
+ arg_types = self._contextual_overload_arg_types(
2379
+ candidates, arg_count, arg_types, args_node, source,
2380
+ module_name, enclosing_class_id, import_map, wildcard_packages,
2381
+ local_types, param_types, field_types, {},
2382
+ )
2383
+ target, _ = self._resolve_own_or_ancestor(
2384
+ receiver_class_id, method_name, arg_count, arg_types,
2385
+ 0.9, 0.85, _ImportContext(
2386
+ module_name, import_map, wildcard_packages, enclosing_class_id,
2387
+ ),
2388
+ allow_method_type_inference=True,
2389
+ narrowing_args=(args_node, source, local_types, param_types, field_types),
2390
+ static_only=static_only,
2391
+ )
2392
+ target_ids = target if isinstance(target, list) else [target] if target else []
2393
+ if len(target_ids) > 1:
2394
+ # Varargs overload selection can conservatively retain several
2395
+ # candidates even when every candidate is declared on the same
2396
+ # class and has exactly the same return type. Dispatch remains
2397
+ # ambiguous, but the invocation's value type is not. Reuse one
2398
+ # representative only for return propagation; emitted call edges
2399
+ # keep the existing fan-out/precision policy unchanged.
2400
+ owners = {self._declaring_class_id(candidate) for candidate in target_ids}
2401
+ returns = {
2402
+ self.builder._nodes[candidate].metadata.get("java_return_type")
2403
+ for candidate in target_ids
2404
+ if candidate in self.builder._nodes
2405
+ }
2406
+ if len(owners) == 1 and None not in owners and len(returns) == 1 and None not in returns:
2407
+ target_ids = [target_ids[0]]
2408
+ if len(target_ids) != 1:
2409
+ return None
2410
+ target_node = self.builder._nodes.get(target_ids[0])
2411
+ if target_node is None:
2412
+ return None
2413
+ return_type = target_node.metadata.get("java_return_type")
2414
+ if not isinstance(return_type, str) or not return_type:
2415
+ return None
2416
+ declaring_class = self._declaring_class_id(target_ids[0]) or (
2417
+ f"{target_node.module}:{target_node.contained_in}"
2418
+ if target_node.contained_in else receiver_class_id
2419
+ )
2420
+ type_params = self._class_type_params.get(declaring_class, [])
2421
+
2422
+ # Most source methods return a concrete type (`String`, `Builder`,
2423
+ # `void`, ...), so invocation argument binding and receiver generic
2424
+ # substitution cannot affect their result. The work below is needed
2425
+ # only when the declared return actually mentions a method or class
2426
+ # type variable. Keep the conservative substring check deliberately
2427
+ # one-sided: a false positive merely takes the old path; it can never
2428
+ # skip a required substitution.
2429
+ method_type_params = self._method_type_param_bounds.get(target_ids[0], {})
2430
+ needs_method_binding = any(name in return_type for name in method_type_params)
2431
+ needs_class_binding = any(name in return_type for name in type_params)
2432
+ # Method type variables are inferred from invocation arguments, not
2433
+ # from the receiver's class arguments. For `<T> T proxy(T bean)`, a
2434
+ # `SomeListener` argument makes the chained receiver type
2435
+ # `SomeListener`; leaving it as bare `T` drops `proxy(bean).run()`.
2436
+ # Keep this bounded to direct parameter variables with statically
2437
+ # known argument types—nested constraints need a full JLS inference
2438
+ # solver and must remain unresolved rather than guessed.
2439
+ method_bindings: Dict[str, str] = {}
2440
+ arg_nodes = argument_nodes(args_node) if needs_method_binding else []
2441
+ # Erased argument types are unknown as a whole whenever any single
2442
+ # argument is not statically knowable (`_infer_arg_types`), which is
2443
+ # the common shape for `getOnlyElement(root.getChildren())`. Keep
2444
+ # the plain `T <- actual` binding gated on a known erased type, but
2445
+ # let the structural bindings below (generic container arguments,
2446
+ # generic arrays, class literals) inspect each argument node on its
2447
+ # own so one unknown sibling argument does not drop every binding.
2448
+ declared_params = self._param_type_lists.get(target_ids[0], [])
2449
+ for index, declared in enumerate(declared_params) if needs_method_binding else ():
2450
+ if arg_types is not None and index >= len(arg_types):
2451
+ break
2452
+ actual = arg_types[index] if arg_types is not None else None
2453
+ parameter = erased_type(declared).strip()
2454
+ if actual is not None and parameter in method_type_params:
2455
+ method_bindings[parameter] = actual
2456
+ # Bind a method variable nested in the same generic raw type:
2457
+ # `<T> T exactlyOne(List<T> xs)` + `List<Filter>` -> Filter,
2458
+ # including the common `Collection<? extends T>` form. The
2459
+ # regular overload pass intentionally erases arguments, so
2460
+ # recover the declared expression type here solely for return
2461
+ # propagation. Require equal raw types and one-to-one generic
2462
+ # arity; no subtype or wildcard-lower-bound guessing.
2463
+ arg_node = arg_nodes[index] if index < len(arg_nodes) else None
2464
+ raw_actual = (
2465
+ self._infer_arg_type(
2466
+ arg_node, source, local_types, param_types, field_types,
2467
+ )
2468
+ if arg_node is not None else None
2469
+ )
2470
+ if raw_actual is None and arg_node is not None and arg_node.type == "method_invocation":
2471
+ # A nested source call carries its declared generic return
2472
+ # (`root.getChildren()` -> `Set<? extends Descriptor>`), so
2473
+ # `<T> T getOnlyElement(Collection<T>)` binds `T` exactly the
2474
+ # way a declared local of that type would. The declared
2475
+ # return is canonicalized by the declaring file's context, so
2476
+ # the raw-type compatibility check below cannot be fooled by
2477
+ # a same-simple-name project class at the call site.
2478
+ raw_actual = self._generic_method_invocation_return_type(
2479
+ arg_node, source, module_name, enclosing_class_id, import_map,
2480
+ wildcard_packages, local_types, field_types, param_types, {},
2481
+ )
2482
+ declared_generic = self._generic_type_arguments(declared)
2483
+ actual_generic = (
2484
+ self._generic_type_arguments(raw_actual)
2485
+ if isinstance(raw_actual, str) else []
2486
+ )
2487
+ if (
2488
+ raw_actual is not None
2489
+ and self._types_compatible(
2490
+ erased_type(declared).strip(), erased_type(raw_actual).strip(),
2491
+ self._param_ctx_for(target_ids[0]),
2492
+ _ImportContext(
2493
+ module_name, import_map, wildcard_packages, enclosing_class_id,
2494
+ ),
2495
+ allow_boxing=False,
2496
+ )
2497
+ and len(declared_generic) == len(actual_generic)
2498
+ ):
2499
+ for declared_arg, actual_arg in zip(declared_generic, actual_generic):
2500
+ variable = self._wildcard_upper_bound(declared_arg) or declared_arg
2501
+ if variable in method_type_params:
2502
+ method_bindings[variable] = (
2503
+ self._wildcard_upper_bound(actual_arg) or actual_arg
2504
+ )
2505
+ # Preserve an element-type binding through generic array
2506
+ # parameters. For `<T> Stream<T> unique(T[] values)`, a
2507
+ # `String[]` argument must make the chained receiver
2508
+ # `Stream<String>`, otherwise downstream Stream method
2509
+ # references lose their functional target type. Require
2510
+ # identical array rank and a reference component: a generic
2511
+ # Java variable cannot stand for a primitive component.
2512
+ dimensions = 0
2513
+ component = parameter
2514
+ while component.endswith("[]"):
2515
+ dimensions += 1
2516
+ component = component[:-2]
2517
+ if dimensions and actual is not None and component in method_type_params:
2518
+ actual_component = actual
2519
+ for _ in range(dimensions):
2520
+ if not actual_component.endswith("[]"):
2521
+ actual_component = ""
2522
+ break
2523
+ actual_component = actual_component[:-2]
2524
+ if actual_component and actual_component not in self._BOXED_PRIMITIVE:
2525
+ method_bindings[component] = actual_component
2526
+ # `Class<E>` carries an exact binding at a class literal call
2527
+ # site. Preserve that binding only when the variable is also
2528
+ # present in this method's declared return type; this avoids
2529
+ # treating an arbitrary generic parameter as a global type
2530
+ # guess.
2531
+ declared_args = self._generic_type_arguments(declared)
2532
+ arg_node = arg_nodes[index] if index < len(arg_nodes) else None
2533
+ variable = declared_args[0] if len(declared_args) == 1 else ""
2534
+ if (
2535
+ arg_node is not None and arg_node.type == "class_literal"
2536
+ and variable and variable.isidentifier()
2537
+ and re.search(rf"\b{re.escape(variable)}\b", return_type)
2538
+ ):
2539
+ literal = next((c for c in arg_node.children if c.is_named), None)
2540
+ if literal is not None:
2541
+ method_bindings[variable] = self._text(literal, source)
2542
+ # A generic fluent source method can receive its result binding from
2543
+ # a method reference. In `Events.<R>map(Function<? super Event,
2544
+ # ? extends R>)`, `Event::getTestDescriptor` makes `R` exactly the
2545
+ # getter's declared return. Preserve only the unbound `Type::method`
2546
+ # shape with one source-owned zero-argument candidate; overloaded,
2547
+ # bound, and external references stay unresolved rather than guessed.
2548
+ for index, declared in enumerate(self._param_type_lists.get(target_ids[0], [])) if needs_method_binding else ():
2549
+ if index >= len(arg_nodes):
2550
+ continue
2551
+ declared_args = self._generic_type_arguments(declared)
2552
+ if len(declared_args) < 2:
2553
+ continue
2554
+ variable = self._wildcard_upper_bound(declared_args[-1]) or declared_args[-1]
2555
+ if (
2556
+ not variable.isidentifier()
2557
+ or variable not in method_type_params
2558
+ or not re.search(rf"\b{re.escape(variable)}\b", return_type)
2559
+ ):
2560
+ continue
2561
+ functional_arg = arg_nodes[index]
2562
+ if functional_arg.type == "lambda_expression":
2563
+ body = functional_arg.child_by_field_name("body")
2564
+ if body is not None and body.type in {
2565
+ "method_invocation", "object_creation_expression",
2566
+ }:
2567
+ inferred = self._declared_receiver_type_text(
2568
+ body, source, module_name, enclosing_class_id,
2569
+ import_map, wildcard_packages, local_types, field_types,
2570
+ param_types, {},
2571
+ )
2572
+ if inferred is not None:
2573
+ method_bindings[variable] = inferred
2574
+ continue
2575
+ if functional_arg.type != "method_reference":
2576
+ continue
2577
+ ref_node = functional_arg
2578
+ named = [child for child in ref_node.children if child.is_named]
2579
+ terminal = next(
2580
+ (child for child in reversed(ref_node.children)
2581
+ if child.type in {"identifier", "new"}),
2582
+ None,
2583
+ )
2584
+ if not named or terminal is None or terminal.type == "new":
2585
+ continue
2586
+ owner_id = self._resolve_declared_type(
2587
+ self._text(named[0], source), module_name, import_map,
2588
+ wildcard_packages, enclosing_class_id,
2589
+ )
2590
+ if owner_id is None:
2591
+ continue
2592
+ candidates = [
2593
+ candidate for candidate in self._members_named(
2594
+ owner_id, self._text(terminal, source), search_ancestors=True,
2595
+ )
2596
+ if len(self._param_type_lists.get(candidate, [])) == 0
2597
+ ]
2598
+ returns = {
2599
+ self.builder._nodes[candidate].metadata.get("java_return_type")
2600
+ for candidate in candidates if candidate in self.builder._nodes
2601
+ }
2602
+ if len(candidates) == 1 and len(returns) == 1:
2603
+ inferred = next(iter(returns))
2604
+ if isinstance(inferred, str) and inferred and inferred != "void":
2605
+ method_bindings[variable] = inferred
2606
+ if method_bindings:
2607
+ return_type = self._replace_type_variables(return_type, method_bindings)
2608
+ # A method binding can itself be a receiver-class variable (for
2609
+ # example `<T> T identity(T value)` called with a `U` field on
2610
+ # `Box<U>`). Recheck after replacement so `U` is still
2611
+ # substituted from the receiver rather than leaked as a
2612
+ # declaration-site variable into the following chain.
2613
+ needs_class_binding = needs_class_binding or any(
2614
+ name in return_type for name in type_params
2615
+ )
2616
+ if needs_class_binding:
2617
+ bindings = self._compose_type_var_bindings(
2618
+ receiver_class_id, receiver_args, declaring_class,
2619
+ )
2620
+ resolved_args = [bindings.get(name, name) for name in type_params]
2621
+ resolved_return = self._substitute_generic_return_type(
2622
+ return_type, type_params, resolved_args,
2623
+ )
2624
+ else:
2625
+ resolved_return = return_type
2626
+ if resolved_return is None:
2627
+ return None
2628
+ # Within the declaring generic class, a raw receiver still knows the
2629
+ # declaration-site upper bound: `class C<T extends Descriptor> { T
2630
+ # get(); }` makes `get().id()` dispatch through `Descriptor`, not a
2631
+ # fictional source class named `T`. Do not apply this to an actual
2632
+ # receiver type argument, which was already substituted above.
2633
+ raw_return = erased_type(resolved_return).strip()
2634
+ if (
2635
+ raw_return in type_params
2636
+ and raw_return not in bindings
2637
+ and (class_bound := self._class_type_param_bounds.get(declaring_class, {}).get(raw_return))
2638
+ ):
2639
+ resolved_return = class_bound
2640
+ # A method inference variable that no argument bound is not a type
2641
+ # named `T`. JLS 18.1.3 resolves such a variable to its declared
2642
+ # bound; an implicit `Object` bound carries no dispatch information
2643
+ # and stays unknown, so a following `var` argument keeps the
2644
+ # unknown-argument overload contract instead of failing every
2645
+ # candidate against a phantom class `T`.
2646
+ unbound = erased_type(resolved_return).strip()
2647
+ unbound_rank = 0
2648
+ while unbound.endswith("[]"):
2649
+ unbound_rank += 1
2650
+ unbound = unbound[:-2]
2651
+ if unbound in method_type_params and unbound not in method_bindings:
2652
+ bound = method_type_params[unbound].strip()
2653
+ if not bound or erased_type(bound).strip() == "Object":
2654
+ return None
2655
+ resolved_return = bound + "[]" * unbound_rank
2656
+ # A source method's return type is declared in the target method's
2657
+ # compilation unit, not at the call site. In particular, a simple
2658
+ # member return type such as ``Builder`` on ``Option.builder()`` must
2659
+ # mean ``Option.Builder`` even when the caller imports an unrelated
2660
+ # ``HelpFormatter.Builder``. Canonicalize it while the declaring
2661
+ # method/file context is still available, then let the outer chain
2662
+ # resolve that qualified text without caller-import aliasing.
2663
+ declaring_ctx = self._file_ctx.get(target_node.file_path)
2664
+ if declaring_ctx is not None:
2665
+ # Preserve the declaration context of source-owned generic
2666
+ # arguments as well as a directly returned source class. A
2667
+ # caller need not import ``Role`` merely because
2668
+ # ``User.getRoles()`` is declared as ``Set<Role>`` in User.java.
2669
+ # Qualifying that argument here lets a later JDK chain such as
2670
+ # ``parallelStream()`` carry ``Stream<pkg.Role>`` into lambda
2671
+ # inference without a global simple-name fallback.
2672
+ generic_bindings: Dict[str, str] = {}
2673
+ for generic_arg in self._generic_type_arguments(resolved_return):
2674
+ bounded_arg = self._wildcard_upper_bound(generic_arg) or generic_arg
2675
+ arg_class_id = self._resolve_declared_type(
2676
+ bounded_arg, declaring_ctx.module,
2677
+ declaring_ctx.import_map, declaring_ctx.wildcard_packages,
2678
+ declaring_class,
2679
+ )
2680
+ if arg_class_id is None or ":" not in arg_class_id:
2681
+ continue
2682
+ arg_module, arg_qualname = arg_class_id.rsplit(":", 1)
2683
+ generic_bindings[erased_type(bounded_arg).strip()] = (
2684
+ f"{self.builder._display_module(arg_module)}.{arg_qualname}"
2685
+ )
2686
+ resolved_return = self._replace_type_variables(
2687
+ resolved_return, generic_bindings,
2688
+ )
2689
+ erased_return = erased_type(resolved_return).strip()
2690
+ array_rank = 0
2691
+ while erased_return.endswith("[]"):
2692
+ array_rank += 1
2693
+ erased_return = erased_return[:-2]
2694
+ return_class_id = self._resolve_declared_type(
2695
+ resolved_return, declaring_ctx.module,
2696
+ declaring_ctx.import_map, declaring_ctx.wildcard_packages,
2697
+ declaring_class,
2698
+ )
2699
+ if return_class_id is not None and ":" in return_class_id:
2700
+ return_module, return_qualname = return_class_id.rsplit(":", 1)
2701
+ # Keep the substituted generic arguments while qualifying
2702
+ # the raw source return owner. Dropping `<Event>` from
2703
+ # `Result<Event>` here makes the next fluent hop see bare
2704
+ # `Result`, so its SAM parameter `E` can never resolve.
2705
+ return_args = self._generic_type_arguments(resolved_return)
2706
+ generic_suffix = (
2707
+ "<" + ", ".join(return_args) + ">"
2708
+ if return_args else ""
2709
+ )
2710
+ return (
2711
+ f"{self.builder._display_module(return_module)}."
2712
+ f"{return_qualname}{generic_suffix}" + "[]" * array_rank
2713
+ )
2714
+ # The same provenance rule for a JDK raw owner: `Set` spelled in
2715
+ # the declaring file means the exact class that file imports, so
2716
+ # a caller that never imports `Set` can still check it against a
2717
+ # `Collection<T>` parameter through the exact-FQN JDK facts.
2718
+ # Only an explicit `java.` import qualifies; a bare or
2719
+ # wildcard-imported name is left as written.
2720
+ if return_class_id is None and "." not in erased_return:
2721
+ imported_owner = declaring_ctx.import_map.get(erased_return)
2722
+ if imported_owner is not None and imported_owner.startswith("java."):
2723
+ return_args = self._generic_type_arguments(resolved_return)
2724
+ generic_suffix = (
2725
+ "<" + ", ".join(return_args) + ">"
2726
+ if return_args else ""
2727
+ )
2728
+ return f"{imported_owner}{generic_suffix}" + "[]" * array_rank
2729
+ return resolved_return
2730
+
2731
+ def _equivalent_type_texts(
2732
+ self, left: str, right: str, module_name: str, import_map: Dict[str, str],
2733
+ wildcard_packages: List[str], enclosing_class_id: Optional[str] = None,
2734
+ ) -> Optional[str]:
2735
+ """Canonical spelling shared by two declared-type texts that name the
2736
+ same type, or None.
2737
+
2738
+ Two spellings are one type when their raw owners resolve to the same
2739
+ source class, or are one external name written bare in one place and
2740
+ `java.`-qualified in the other (an exact JDK stub result versus a
2741
+ declaring-file canonicalized return), and every generic argument is
2742
+ pairwise equivalent by the same rule. Array rank and wildcard shape
2743
+ must agree. Nothing else is unified: genuinely different types stay
2744
+ unresolved rather than guessed. The qualified spelling is preferred
2745
+ so a following hop resolves it without caller imports."""
2746
+ if left == right:
2747
+ return left
2748
+ left_bound = self._wildcard_upper_bound(left)
2749
+ right_bound = self._wildcard_upper_bound(right)
2750
+ if (left_bound is None) != (right_bound is None):
2751
+ return None
2752
+ if left_bound is not None:
2753
+ inner = self._equivalent_type_texts(
2754
+ left_bound, right_bound, module_name, import_map,
2755
+ wildcard_packages, enclosing_class_id,
2756
+ )
2757
+ return f"? extends {inner}" if inner is not None else None
2758
+ left_raw = erased_type(left).strip()
2759
+ right_raw = erased_type(right).strip()
2760
+ rank = 0
2761
+ while left_raw.endswith("[]") and right_raw.endswith("[]"):
2762
+ left_raw, right_raw, rank = left_raw[:-2], right_raw[:-2], rank + 1
2763
+ if left_raw.endswith("[]") or right_raw.endswith("[]"):
2764
+ return None
2765
+ left_args = self._generic_type_arguments(left)
2766
+ right_args = self._generic_type_arguments(right)
2767
+ if len(left_args) != len(right_args):
2768
+ return None
2769
+ raw: Optional[str] = None
2770
+ if left_raw == right_raw:
2771
+ raw = left_raw
2772
+ else:
2773
+ left_id = self._resolve_declared_type(
2774
+ left_raw, module_name, import_map, wildcard_packages, enclosing_class_id,
2775
+ )
2776
+ right_id = self._resolve_declared_type(
2777
+ right_raw, module_name, import_map, wildcard_packages, enclosing_class_id,
2778
+ )
2779
+ if left_id is not None and left_id == right_id:
2780
+ raw = left_raw if "." in left_raw else right_raw
2781
+ elif left_id is None and right_id is None:
2782
+ if left_raw.rsplit(".", 1)[-1] == right_raw.rsplit(".", 1)[-1]:
2783
+ qualified = [t for t in (left_raw, right_raw) if t.startswith("java.")]
2784
+ bare = [t for t in (left_raw, right_raw) if "." not in t]
2785
+ if qualified and bare:
2786
+ raw = qualified[0]
2787
+ if raw is None:
2788
+ return None
2789
+ unified: List[str] = []
2790
+ for left_arg, right_arg in zip(left_args, right_args):
2791
+ same = self._equivalent_type_texts(
2792
+ left_arg, right_arg, module_name, import_map,
2793
+ wildcard_packages, enclosing_class_id,
2794
+ )
2795
+ if same is None:
2796
+ return None
2797
+ unified.append(same)
2798
+ suffix = "<" + ", ".join(unified) + ">" if unified else ""
2799
+ return raw + suffix + "[]" * rank
2800
+
2801
+ def _compose_type_var_bindings(
2802
+ self, receiver_class_id: str, receiver_args: List[str], declaring_class: str,
2803
+ ) -> Dict[str, str]:
2804
+ """Type-variable bindings valid at `declaring_class`'s own scope,
2805
+ given the receiver's declared type arguments at the call site.
2806
+
2807
+ `receiver_args` only binds `receiver_class_id`'s *own* type
2808
+ parameters (`Box<Stripe> box` -> `T := Stripe` on `Box` itself). When
2809
+ the method is declared on an ancestor (`StripeBox extends
2810
+ Box<Stripe>`, receiver typed as bare `StripeBox`), that binding isn't
2811
+ on the receiver at all — it's on the inheritance edge. This walks the
2812
+ INHERITS/IMPLEMENTS path from `receiver_class_id` to `declaring_class`
2813
+ (`_ancestor_path`) and composes `_ancestor_type_args` at each hop,
2814
+ substituting the previous hop's bindings into each edge's argument
2815
+ text before binding the next hop's own type parameters — so a
2816
+ multi-level chain (`Sub<X> extends Mid<X>`, `Mid<T> extends Box<T>`)
2817
+ or an interface hierarchy resolves the same way a class chain does."""
2818
+ own_params = self._class_type_params.get(receiver_class_id, [])
2819
+ bindings = {
2820
+ name: receiver_args[idx]
2821
+ for idx, name in enumerate(own_params)
2822
+ if idx < len(receiver_args)
2823
+ }
2824
+ if receiver_class_id == declaring_class:
2825
+ return bindings
2826
+ path = self._ancestor_path(receiver_class_id, declaring_class)
2827
+ if path is None:
2828
+ return {}
2829
+ cur = receiver_class_id
2830
+ for nxt in path:
2831
+ edge_args = self._ancestor_type_args.get((cur, nxt), [])
2832
+ substituted = [
2833
+ self._replace_type_variables(arg, bindings) for arg in edge_args
2834
+ ]
2835
+ next_params = self._class_type_params.get(nxt, [])
2836
+ bindings = {
2837
+ name: substituted[idx]
2838
+ for idx, name in enumerate(next_params)
2839
+ if idx < len(substituted)
2840
+ }
2841
+ cur = nxt
2842
+ return bindings
2843
+
2844
+ def _ancestor_path(self, start: str, target: str) -> Optional[List[str]]:
2845
+ """BFS over INHERITS ∪ IMPLEMENTS from `start` to `target`, returning
2846
+ the chain of class ids from `start`'s first hop through `target`
2847
+ (nearest ancestor first, mirroring `_class_ancestors`'s own
2848
+ traversal order). `[]` if `start == target`; `None` if unreachable."""
2849
+ if start == target:
2850
+ return []
2851
+ seen: Set[str] = {start}
2852
+ q: deque = deque(
2853
+ (anc, [anc]) for anc in (
2854
+ self._direct_ancestors(start, EdgeType.INHERITS)
2855
+ + self._direct_ancestors(start, EdgeType.IMPLEMENTS)
2856
+ )
2857
+ )
2858
+ while q:
2859
+ cur, path = q.popleft()
2860
+ if cur == target:
2861
+ return path
2862
+ if cur in seen:
2863
+ continue
2864
+ seen.add(cur)
2865
+ for nxt in (
2866
+ self._direct_ancestors(cur, EdgeType.INHERITS)
2867
+ + self._direct_ancestors(cur, EdgeType.IMPLEMENTS)
2868
+ ):
2869
+ q.append((nxt, path + [nxt]))
2870
+ return None
2871
+
2872
+ def _substitute_generic_return_type(
2873
+ self, return_type: str, type_params: List[str], receiver_args: List[str],
2874
+ ) -> Optional[str]:
2875
+ if not type_params or not receiver_args:
2876
+ return self._wildcard_upper_bound(return_type) or return_type
2877
+ bindings = {
2878
+ name: receiver_args[idx]
2879
+ for idx, name in enumerate(type_params)
2880
+ if idx < len(receiver_args)
2881
+ }
2882
+ substituted = self._replace_type_variables(return_type, bindings)
2883
+ return self._wildcard_upper_bound(substituted) or substituted
2884
+
2885
+ @classmethod
2886
+ def _replace_type_variables(cls, type_text: str, bindings: Dict[str, str]) -> str:
2887
+ if not bindings:
2888
+ return type_text
2889
+ out: List[str] = []
2890
+ idx = 0
2891
+ while idx < len(type_text):
2892
+ ch = type_text[idx]
2893
+ if ch.isalpha() or ch in "_$":
2894
+ start = idx
2895
+ idx += 1
2896
+ while idx < len(type_text) and (
2897
+ type_text[idx].isalnum() or type_text[idx] in "_$"
2898
+ ):
2899
+ idx += 1
2900
+ ident = type_text[start:idx]
2901
+ out.append(bindings.get(ident, ident))
2902
+ continue
2903
+ out.append(ch)
2904
+ idx += 1
2905
+ return "".join(out)
2906
+
2907
+ @staticmethod
2908
+ def _generic_type_arguments(type_text: str) -> List[str]:
2909
+ start = type_text.find("<")
2910
+ if start < 0:
2911
+ return []
2912
+ depth = 0
2913
+ end = -1
2914
+ for idx in range(start, len(type_text)):
2915
+ ch = type_text[idx]
2916
+ if ch == "<":
2917
+ depth += 1
2918
+ elif ch == ">":
2919
+ depth -= 1
2920
+ if depth == 0:
2921
+ end = idx
2922
+ break
2923
+ if end < 0:
2924
+ return []
2925
+ args: List[str] = []
2926
+ part_start = start + 1
2927
+ depth = 0
2928
+ for idx in range(start + 1, end):
2929
+ ch = type_text[idx]
2930
+ if ch == "<":
2931
+ depth += 1
2932
+ elif ch == ">":
2933
+ depth -= 1
2934
+ elif ch == "," and depth == 0:
2935
+ arg = type_text[part_start:idx].strip()
2936
+ if arg:
2937
+ args.append(arg)
2938
+ part_start = idx + 1
2939
+ last = type_text[part_start:end].strip()
2940
+ if last:
2941
+ args.append(last)
2942
+ return args
2943
+
2944
+ @staticmethod
2945
+ def _wildcard_upper_bound(type_text: str) -> Optional[str]:
2946
+ match = re.match(r"^\?\s+extends\s+(.+)$", type_text.strip())
2947
+ return match.group(1).strip() if match else None