adl-toolkit 0.0.0-stage → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/INSTALL_TOOLKIT.md +163 -0
- package/README.md +269 -2
- package/adl_drift/__init__.py +6 -0
- package/adl_drift/cli/__init__.py +1 -0
- package/adl_drift/cli/commands.py +993 -0
- package/adl_drift/cli/main.py +744 -0
- package/adl_drift/cortex/__init__.py +1 -0
- package/adl_drift/cortex/embeddings.py +96 -0
- package/adl_drift/cortex/memory.py +1749 -0
- package/adl_drift/cortex/patterns.py +89 -0
- package/adl_drift/cortex/telemetry.py +192 -0
- package/adl_drift/detectors/__init__.py +1 -0
- package/adl_drift/detectors/async_patterns.py +115 -0
- package/adl_drift/detectors/base.py +121 -0
- package/adl_drift/detectors/config.py +122 -0
- package/adl_drift/detectors/data_access.py +128 -0
- package/adl_drift/detectors/decorators.py +122 -0
- package/adl_drift/detectors/dynamic_patterns.py +260 -0
- package/adl_drift/detectors/errors.py +134 -0
- package/adl_drift/detectors/imports.py +132 -0
- package/adl_drift/detectors/logging_patterns.py +108 -0
- package/adl_drift/detectors/routes.py +257 -0
- package/adl_drift/detectors/security.py +236 -0
- package/adl_drift/detectors/taint.py +406 -0
- package/adl_drift/detectors/tests.py +95 -0
- package/adl_drift/detectors/type_conventions.py +81 -0
- package/adl_drift/graph/__init__.py +1 -0
- package/adl_drift/graph/builder.py +2670 -0
- package/adl_drift/graph/cfg.py +461 -0
- package/adl_drift/graph/cluster.py +40 -0
- package/adl_drift/graph/edges.py +86 -0
- package/adl_drift/graph/node.py +90 -0
- package/adl_drift/harness/__init__.py +10 -0
- package/adl_drift/harness/setup.py +832 -0
- package/adl_drift/harness/skills/drift-antigravity.md +19 -0
- package/adl_drift/harness/skills/drift-generic.md +19 -0
- package/adl_drift/harness/skills/drift.md +25 -0
- package/adl_drift/harness/watch.py +84 -0
- package/adl_drift/lang/__init__.py +8 -0
- package/adl_drift/lang/base.py +464 -0
- package/adl_drift/lang/java/__init__.py +6 -0
- package/adl_drift/lang/java/backend.py +97 -0
- package/adl_drift/lang/java/cfg.py +1424 -0
- package/adl_drift/lang/java/config.py +707 -0
- package/adl_drift/lang/java/detectors/__init__.py +29 -0
- package/adl_drift/lang/java/detectors/additional.py +445 -0
- package/adl_drift/lang/java/detectors/base.py +154 -0
- package/adl_drift/lang/java/detectors/data_access.py +148 -0
- package/adl_drift/lang/java/detectors/imports.py +53 -0
- package/adl_drift/lang/java/detectors/routes.py +159 -0
- package/adl_drift/lang/java/detectors/security.py +386 -0
- package/adl_drift/lang/java/detectors/tests.py +74 -0
- package/adl_drift/lang/java/extractor.py +582 -0
- package/adl_drift/lang/java/ids.py +99 -0
- package/adl_drift/lang/java/parser.py +49 -0
- package/adl_drift/lang/java/resolution/__init__.py +1 -0
- package/adl_drift/lang/java/resolution/constants.py +1619 -0
- package/adl_drift/lang/java/resolution/constructors.py +278 -0
- package/adl_drift/lang/java/resolution/context.py +305 -0
- package/adl_drift/lang/java/resolution/dispatch.py +540 -0
- package/adl_drift/lang/java/resolution/functional.py +1318 -0
- package/adl_drift/lang/java/resolution/generated.py +95 -0
- package/adl_drift/lang/java/resolution/model.py +153 -0
- package/adl_drift/lang/java/resolution/orchestration.py +866 -0
- package/adl_drift/lang/java/resolution/overloads.py +1275 -0
- package/adl_drift/lang/java/resolution/scopes.py +1111 -0
- package/adl_drift/lang/java/resolution/types.py +2947 -0
- package/adl_drift/lang/java/resolver.py +1401 -0
- package/adl_drift/lang/java/scopes.py +69 -0
- package/adl_drift/lang/java/taint.py +500 -0
- package/adl_drift/lang/java/type_provenance.py +342 -0
- package/adl_drift/lang/python/__init__.py +8 -0
- package/adl_drift/lang/python/backend.py +94 -0
- package/adl_drift/lang/python/cfg.py +537 -0
- package/adl_drift/lang/python/extractor.py +454 -0
- package/adl_drift/lang/python/parser.py +41 -0
- package/adl_drift/lang/python/resolver.py +1789 -0
- package/adl_drift/lang/python/taint.py +365 -0
- package/adl_drift/lang/registry.py +117 -0
- package/adl_drift/parser/__init__.py +1 -0
- package/adl_drift/parser/cache.py +38 -0
- package/adl_drift/parser/core.py +33 -0
- package/adl_drift/parser/extractor.py +17 -0
- package/adl_drift/parser/literal_eval.py +326 -0
- package/adl_drift/scanner/__init__.py +1 -0
- package/adl_drift/scanner/discovery.py +338 -0
- package/adl_drift/scanner/incremental.py +261 -0
- package/adl_drift/scanner/parallel.py +224 -0
- package/adl_drift/server/__init__.py +1 -0
- package/adl_drift/server/handlers.py +2849 -0
- package/adl_drift/server/mcp_server.py +127 -0
- package/adl_drift/server/mcp_stdio.py +335 -0
- package/adl_drift/server/protocol.py +71 -0
- package/adl_drift/server/static/index.html +696 -0
- package/adl_drift/server/web.py +350 -0
- package/adl_drift/utils/__init__.py +1 -0
- package/adl_drift/utils/config.py +77 -0
- package/adl_drift/utils/fswalk.py +52 -0
- package/adl_drift/utils/types.py +345 -0
- package/bin/adl-drift.mjs +35 -0
- package/bin/adl-toolkit.mjs +488 -0
- package/package.json +29 -4
- package/pyproject.toml +49 -0
- package/scripts/install_toolkit.py +3584 -0
|
@@ -0,0 +1,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
|