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,1111 @@
|
|
|
1
|
+
"""Lexical scope collection and local type propagation for Java calls."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections import defaultdict
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Dict, List, Optional, Tuple
|
|
8
|
+
|
|
9
|
+
from adl_drift.graph.node import NodeType
|
|
10
|
+
from adl_drift.lang.java.ids import erased_type
|
|
11
|
+
from adl_drift.lang.java.resolution.constants import (
|
|
12
|
+
JAVA_OPTIONAL_ELEMENT_LAMBDA_METHODS,
|
|
13
|
+
JAVA_STREAM_ELEMENT_LAMBDA_METHODS,
|
|
14
|
+
_ENCLOSING_TYPE_CST_TYPES,
|
|
15
|
+
_METHOD_CST_TYPES,
|
|
16
|
+
_MOCK_ALLOCATION_TYPE,
|
|
17
|
+
_NESTED_SCOPE_BOUNDARY,
|
|
18
|
+
argument_nodes,
|
|
19
|
+
)
|
|
20
|
+
from adl_drift.lang.java.resolution.model import (
|
|
21
|
+
ImportContext as _ImportContext,
|
|
22
|
+
LocalScopes as _LocalScopes,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
_PRIMITIVE_STREAM_ELEMENT = {"IntStream": "int", "LongStream": "long", "DoubleStream": "double"}
|
|
26
|
+
|
|
27
|
+
class ScopeInferenceMixin:
|
|
28
|
+
@staticmethod
|
|
29
|
+
def _enclosing_scope_end(node, scope_types: Tuple[str, ...], fallback: int) -> int:
|
|
30
|
+
"""End byte of the nearest ancestor of `node` whose type is in
|
|
31
|
+
`scope_types` — the byte offset a declaration made in that scope stops
|
|
32
|
+
being visible at. `fallback` (the method body's end) is used if no such
|
|
33
|
+
ancestor is found."""
|
|
34
|
+
cur = node.parent
|
|
35
|
+
while cur is not None:
|
|
36
|
+
if cur.type in scope_types:
|
|
37
|
+
return cur.end_byte
|
|
38
|
+
cur = cur.parent
|
|
39
|
+
return fallback
|
|
40
|
+
|
|
41
|
+
@staticmethod
|
|
42
|
+
def _enclosing_scope_start(node, scope_types: Tuple[str, ...], fallback: int) -> int:
|
|
43
|
+
cur = node.parent
|
|
44
|
+
while cur is not None:
|
|
45
|
+
if cur.type in scope_types:
|
|
46
|
+
return cur.start_byte
|
|
47
|
+
cur = cur.parent
|
|
48
|
+
return fallback
|
|
49
|
+
|
|
50
|
+
def _build_field_allocation_index(
|
|
51
|
+
self, parses: Dict[Path, ParseResult],
|
|
52
|
+
func_id_by_position: Dict[Tuple[str, int, int], str],
|
|
53
|
+
class_id_by_position: Dict[Tuple[str, int, int], str],
|
|
54
|
+
) -> Dict[str, Dict[str, str]]:
|
|
55
|
+
"""Return exact field types only when all own-class assignments agree."""
|
|
56
|
+
evidence: Dict[Tuple[str, str], Set[str]] = defaultdict(set)
|
|
57
|
+
invalid: Set[Tuple[str, str]] = set()
|
|
58
|
+
for file_path, parse in parses.items():
|
|
59
|
+
ctx = self._file_ctx.get(str(file_path))
|
|
60
|
+
if ctx is None:
|
|
61
|
+
continue
|
|
62
|
+
source = parse.source_bytes
|
|
63
|
+
# A field's own declaration-site initializer (`Type field =
|
|
64
|
+
# mock();`), as opposed to a later `this.field = ...`
|
|
65
|
+
# assignment inside a method body (the only shape the walk
|
|
66
|
+
# below covers). Scoped to the Mockito `mock()`/`spy()` sentinel
|
|
67
|
+
# only — not a general declaration-site allocation pass — since
|
|
68
|
+
# that is the one fact this index needs to support
|
|
69
|
+
# (`_abstract_fan_out`'s mock/spy fan-out suppression, P5
|
|
70
|
+
# finding #2); a general `Type field = new Impl();` pass would
|
|
71
|
+
# be a separate, broader enhancement.
|
|
72
|
+
for node in self._field_declarations(parse.tree.root_node, str(file_path)):
|
|
73
|
+
owner_node = node.parent
|
|
74
|
+
while owner_node is not None and owner_node.type not in _ENCLOSING_TYPE_CST_TYPES:
|
|
75
|
+
owner_node = owner_node.parent
|
|
76
|
+
if owner_node is None:
|
|
77
|
+
continue
|
|
78
|
+
owner_id = class_id_by_position.get(
|
|
79
|
+
(str(file_path), owner_node.start_point[0] + 1, owner_node.start_point[1]),
|
|
80
|
+
)
|
|
81
|
+
if owner_id is None:
|
|
82
|
+
continue
|
|
83
|
+
for declarator in [c for c in node.children if c.type == "variable_declarator"]:
|
|
84
|
+
name_node = declarator.child_by_field_name("name")
|
|
85
|
+
value_node = declarator.child_by_field_name("value")
|
|
86
|
+
if (
|
|
87
|
+
name_node is not None and value_node is not None
|
|
88
|
+
and self._is_mockito_factory_call(value_node, source)
|
|
89
|
+
):
|
|
90
|
+
key = (owner_id, self._text(name_node, source))
|
|
91
|
+
if key not in invalid:
|
|
92
|
+
evidence[key].add(_MOCK_ALLOCATION_TYPE)
|
|
93
|
+
for func_cst, caller_id in self._iter_method_cst(
|
|
94
|
+
parse.tree.root_node, str(file_path), func_id_by_position,
|
|
95
|
+
):
|
|
96
|
+
class_id = self._declaring_class_id(caller_id)
|
|
97
|
+
body = func_cst.child_by_field_name("body")
|
|
98
|
+
if class_id is None or body is None:
|
|
99
|
+
continue
|
|
100
|
+
field_types = self._all_field_types(class_id)
|
|
101
|
+
local_types = self._collect_local_scopes(body, source)
|
|
102
|
+
param_types = dict(zip(
|
|
103
|
+
self._param_names(func_cst, source),
|
|
104
|
+
self._param_type_lists.get(caller_id, []),
|
|
105
|
+
))
|
|
106
|
+
type_bounds = self._collect_type_param_bounds(func_cst, source)
|
|
107
|
+
# Assignment facts already exclude nested method/type bodies,
|
|
108
|
+
# exactly as the former per-method walk did.
|
|
109
|
+
for node in self._method_facts(body).assignment_expressions:
|
|
110
|
+
if node.type == "assignment_expression":
|
|
111
|
+
children = [c for c in node.children if not c.is_extra]
|
|
112
|
+
if len(children) >= 3 and children[1].type == "=":
|
|
113
|
+
lhs, rhs = children[0], children[2]
|
|
114
|
+
field_name = self._this_field_name(lhs, source)
|
|
115
|
+
if lhs.type == "identifier":
|
|
116
|
+
candidate = self._text(lhs, source)
|
|
117
|
+
if (
|
|
118
|
+
candidate in field_types
|
|
119
|
+
and candidate not in param_types
|
|
120
|
+
and local_types.type_at(candidate, lhs.start_byte) is None
|
|
121
|
+
):
|
|
122
|
+
field_name = candidate
|
|
123
|
+
if field_name in field_types:
|
|
124
|
+
type_text: Optional[str] = None
|
|
125
|
+
if rhs.type == "object_creation_expression":
|
|
126
|
+
type_text = self._var_initializer_type(rhs, source)
|
|
127
|
+
elif rhs.type == "method_invocation":
|
|
128
|
+
type_text = self._generic_method_invocation_return_type(
|
|
129
|
+
rhs, source, ctx.module, class_id,
|
|
130
|
+
ctx.import_map, ctx.wildcard_packages,
|
|
131
|
+
local_types, field_types, param_types, type_bounds,
|
|
132
|
+
)
|
|
133
|
+
key = (class_id, field_name)
|
|
134
|
+
allocation_id = (
|
|
135
|
+
self._resolve_declared_type(
|
|
136
|
+
type_text, ctx.module, ctx.import_map,
|
|
137
|
+
ctx.wildcard_packages, class_id,
|
|
138
|
+
)
|
|
139
|
+
if type_text is not None else None
|
|
140
|
+
)
|
|
141
|
+
if allocation_id is None or not self._is_concrete_class(allocation_id):
|
|
142
|
+
invalid.add(key)
|
|
143
|
+
else:
|
|
144
|
+
evidence[key].add(allocation_id)
|
|
145
|
+
out: Dict[str, Dict[str, str]] = defaultdict(dict)
|
|
146
|
+
for (class_id, field_name), allocation_ids in evidence.items():
|
|
147
|
+
if (class_id, field_name) not in invalid and len(allocation_ids) == 1:
|
|
148
|
+
out[class_id][field_name] = next(iter(allocation_ids))
|
|
149
|
+
return dict(out)
|
|
150
|
+
|
|
151
|
+
def _collect_local_scopes(
|
|
152
|
+
self, body, source: bytes, *, field_types: Optional[Dict[str, str]] = None,
|
|
153
|
+
param_types: Optional[Dict[str, str]] = None,
|
|
154
|
+
) -> "_LocalScopes":
|
|
155
|
+
# Three index-building passes ask for the plain (no field/param
|
|
156
|
+
# types) scopes of every method body and only read the result; the
|
|
157
|
+
# call-site pass asks with types and then mutates its copy (lambda
|
|
158
|
+
# params, var inference) — so only the plain form is memoized, per
|
|
159
|
+
# run, keyed on the source buffer and the body node (`Node.id` is
|
|
160
|
+
# unique within a tree, `source_bytes` lives as long as the parse).
|
|
161
|
+
if field_types is None and param_types is None:
|
|
162
|
+
cache = self._local_scopes_cache
|
|
163
|
+
if cache is not None:
|
|
164
|
+
key = (id(source), body.id)
|
|
165
|
+
hit = cache.get(key)
|
|
166
|
+
if hit is None:
|
|
167
|
+
hit = cache[key] = self._collect_local_scopes_uncached(body, source)
|
|
168
|
+
return hit
|
|
169
|
+
return self._collect_local_scopes_uncached(
|
|
170
|
+
body, source, field_types=field_types, param_types=param_types,
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
def _collect_local_scopes_uncached(
|
|
174
|
+
self, body, source: bytes, *, field_types: Optional[Dict[str, str]] = None,
|
|
175
|
+
param_types: Optional[Dict[str, str]] = None,
|
|
176
|
+
) -> "_LocalScopes":
|
|
177
|
+
"""Lexically-scoped local-variable types for `body` (see
|
|
178
|
+
`_LocalScopes`). Covers, besides the plain `local_variable_declaration`
|
|
179
|
+
(scoped to its enclosing `block` — or, when the declaration is a
|
|
180
|
+
classic `for (Type v = ...; ...; ...) { ... }` loop's init clause, to
|
|
181
|
+
the enclosing `for_statement` instead: that declaration's direct
|
|
182
|
+
parent is the `for_statement`, not a `block`, so without this case
|
|
183
|
+
`_enclosing_scope_end` would keep climbing to whatever `block`
|
|
184
|
+
*contains* the loop — often the whole method body — silently
|
|
185
|
+
extending `v`'s visibility past the loop's closing brace and letting
|
|
186
|
+
it shadow a same-named field/parameter used afterward; C-01 fix,
|
|
187
|
+
`enhanced_for_statement`/`resource`/`catch_formal_parameter` below
|
|
188
|
+
never had this gap since each already names its own enclosing
|
|
189
|
+
statement type as the boundary), the three other statement forms
|
|
190
|
+
that introduce a *typed* local by declaration:
|
|
191
|
+
`enhanced_for_statement`'s loop variable (`for (User u : users)`,
|
|
192
|
+
scoped to the loop *body* only — not the iterable expression, per
|
|
193
|
+
JLS 14.14.2; a receiver in `users` there still resolves against the
|
|
194
|
+
enclosing scope, e.g. a same-named field), `catch_formal_parameter`'s exception
|
|
195
|
+
variable (`catch (Ex e)`, scoped to its `catch_clause`), and a
|
|
196
|
+
try-with-resources `resource`'s variable (`try (Res r = open())`,
|
|
197
|
+
scoped to its `try_statement`) — each declares its type inline exactly
|
|
198
|
+
like a local var (P4.4 recall fix). A multi-catch (`catch (A | B e)`)
|
|
199
|
+
takes its *first* alternative's type (best-effort: the exact effective
|
|
200
|
+
type is the alternatives' least upper bound, which this resolver has
|
|
201
|
+
no JDK hierarchy to compute — a method on the first alternative still
|
|
202
|
+
resolves, the same precision-first approximation used elsewhere here).
|
|
203
|
+
A resource that reuses an already-declared variable (`try (existing)`,
|
|
204
|
+
no inline type) declares nothing new and is skipped."""
|
|
205
|
+
scopes = _LocalScopes()
|
|
206
|
+
field_types = field_types or {}
|
|
207
|
+
param_types = param_types or {}
|
|
208
|
+
body_end = body.end_byte
|
|
209
|
+
facts = self._method_facts(body)
|
|
210
|
+
assignments: Dict[str, List[Tuple[int, Optional[str], int]]] = defaultdict(list)
|
|
211
|
+
for assignment in facts.assignment_expressions:
|
|
212
|
+
children = [c for c in assignment.children if not c.is_extra]
|
|
213
|
+
if len(children) >= 3 and children[1].type == "=":
|
|
214
|
+
lhs = children[0]
|
|
215
|
+
if lhs.type == "identifier":
|
|
216
|
+
statement = assignment.parent
|
|
217
|
+
direct_scope = statement.parent if statement is not None else None
|
|
218
|
+
# Only a direct statement in one lexical scope creates
|
|
219
|
+
# a linear exact-allocation interval. Assignments under
|
|
220
|
+
# branch/loop nodes remain conservative.
|
|
221
|
+
if (
|
|
222
|
+
statement is not None
|
|
223
|
+
and statement.type == "expression_statement"
|
|
224
|
+
and direct_scope is not None
|
|
225
|
+
and direct_scope.type in ("block", "for_statement", "switch_block")
|
|
226
|
+
):
|
|
227
|
+
rhs = children[2]
|
|
228
|
+
allocation_type = (
|
|
229
|
+
self._var_initializer_type(rhs, source)
|
|
230
|
+
if rhs.type == "object_creation_expression" else None
|
|
231
|
+
)
|
|
232
|
+
assignments[self._text(lhs, source)].append(
|
|
233
|
+
(assignment.start_byte, allocation_type, direct_scope.start_byte)
|
|
234
|
+
)
|
|
235
|
+
for node in facts.local_scope_nodes:
|
|
236
|
+
if node.type == "local_variable_declaration":
|
|
237
|
+
type_node = node.child_by_field_name("type")
|
|
238
|
+
if type_node is not None:
|
|
239
|
+
declared_type_text = self._text(type_node, source)
|
|
240
|
+
# S-01 fix: `"switch_block"` added to the boundary set —
|
|
241
|
+
# a `local_variable_declaration` written directly inside a
|
|
242
|
+
# colon-style `switch` case (`case 1: Item cursor = ...;`)
|
|
243
|
+
# is a child of `switch_block_statement_group`, not of a
|
|
244
|
+
# `block`, so without this the search climbed straight
|
|
245
|
+
# past the whole `switch` to whatever `block` contains
|
|
246
|
+
# it (often the entire method body), letting the
|
|
247
|
+
# case-local's declared type silently leak into, and
|
|
248
|
+
# shadow same-named receivers used in, code that runs
|
|
249
|
+
# after the switch closes. `switch_block` (not the
|
|
250
|
+
# narrower `switch_block_statement_group`) is used
|
|
251
|
+
# deliberately: per JLS 6.3 a switch-block-scoped local's
|
|
252
|
+
# scope runs to the end of the *whole* switch block, not
|
|
253
|
+
# just its own case group, so a same-named receiver in a
|
|
254
|
+
# later case reached via fallthrough (no `break`) must
|
|
255
|
+
# still see it.
|
|
256
|
+
scope_end = self._enclosing_scope_end(
|
|
257
|
+
node, ("block", "for_statement", "switch_block"), body_end,
|
|
258
|
+
)
|
|
259
|
+
scope_start = self._enclosing_scope_start(
|
|
260
|
+
node, ("block", "for_statement", "switch_block"), body.start_byte,
|
|
261
|
+
)
|
|
262
|
+
for child in node.children:
|
|
263
|
+
if child.type != "variable_declarator":
|
|
264
|
+
continue
|
|
265
|
+
name_node = child.child_by_field_name("name")
|
|
266
|
+
if name_node is None:
|
|
267
|
+
continue
|
|
268
|
+
name = self._text(name_node, source)
|
|
269
|
+
assignment_events = sorted(
|
|
270
|
+
(
|
|
271
|
+
(offset, allocation_type)
|
|
272
|
+
for offset, allocation_type, assignment_scope_start
|
|
273
|
+
in assignments.get(name, ())
|
|
274
|
+
if (
|
|
275
|
+
child.end_byte < offset <= scope_end
|
|
276
|
+
and assignment_scope_start == scope_start
|
|
277
|
+
)
|
|
278
|
+
),
|
|
279
|
+
key=lambda event: event[0],
|
|
280
|
+
)
|
|
281
|
+
type_text = declared_type_text
|
|
282
|
+
value_node = child.child_by_field_name("value")
|
|
283
|
+
if type_text == "var":
|
|
284
|
+
# V-01 fix: JLS 14.4 local-variable type
|
|
285
|
+
# inference (`var x = ...;`). tree-sitter-java
|
|
286
|
+
# parses `var` as an ordinary `type_identifier`
|
|
287
|
+
# (its text is literally `"var"`), indistinguishable
|
|
288
|
+
# at the grammar level from a real type name in
|
|
289
|
+
# that position — without this branch,
|
|
290
|
+
# `_resolve_declared_type("var", ...)` always
|
|
291
|
+
# failed (no project class is ever legally named
|
|
292
|
+
# `var`, a reserved type name per JLS 3.9), so
|
|
293
|
+
# every `var`-typed receiver silently dropped its
|
|
294
|
+
# CALLS edges. Best-effort only, mirroring the
|
|
295
|
+
# exact two initializer forms `_infer_arg_type`
|
|
296
|
+
# already reads a type off of directly
|
|
297
|
+
# (`new Foo()` / `(Foo) x`) — a `var` initialized
|
|
298
|
+
# from a method call, literal, or other
|
|
299
|
+
# expression stays unresolved rather than
|
|
300
|
+
# guessed, same "missing edge over wrong edge"
|
|
301
|
+
# stance as everywhere else in this file. `var`
|
|
302
|
+
# in an `enhanced_for_statement` header
|
|
303
|
+
# (`for (var x : list)`) or a try-with-resources
|
|
304
|
+
# `resource` is a separate, harder case — the
|
|
305
|
+
# element/resource type there would need the
|
|
306
|
+
# iterable/resource expression's own generic
|
|
307
|
+
# argument, not just an initializer's own type
|
|
308
|
+
# token — and is deliberately left as a known,
|
|
309
|
+
# documented gap, not folded into this fix.
|
|
310
|
+
type_text = self._var_initializer_type(
|
|
311
|
+
value_node, source, scopes, param_types, field_types,
|
|
312
|
+
)
|
|
313
|
+
# A bare field is another exact, bounded source
|
|
314
|
+
# of a `var` declaration's type: `var alias =
|
|
315
|
+
# gateway`. A same-named parameter shadows the
|
|
316
|
+
# field, and is itself an exact alias source.
|
|
317
|
+
if (
|
|
318
|
+
type_text is None
|
|
319
|
+
and value_node is not None
|
|
320
|
+
and value_node.type == "identifier"
|
|
321
|
+
):
|
|
322
|
+
initializer_name = self._text(value_node, source)
|
|
323
|
+
type_text = (
|
|
324
|
+
param_types.get(initializer_name)
|
|
325
|
+
or field_types.get(initializer_name)
|
|
326
|
+
)
|
|
327
|
+
if type_text is None:
|
|
328
|
+
# Type stays unknown ("missing edge over wrong
|
|
329
|
+
# edge") but the name is still a genuine local
|
|
330
|
+
# declaration in this scope — record it apart
|
|
331
|
+
# from `add()` so shadow checks that only need
|
|
332
|
+
# "is this name locally declared here" (not
|
|
333
|
+
# its type) still see it.
|
|
334
|
+
scopes.add_declared(name, node.start_byte, scope_end)
|
|
335
|
+
continue
|
|
336
|
+
elif value_node is not None and value_node.type == "object_creation_expression":
|
|
337
|
+
allocation_type = self._var_initializer_type(value_node, source)
|
|
338
|
+
if allocation_type is not None:
|
|
339
|
+
first_reassignment = next(
|
|
340
|
+
(offset for offset, _ in assignment_events),
|
|
341
|
+
None,
|
|
342
|
+
)
|
|
343
|
+
scopes.add_allocation(
|
|
344
|
+
name, allocation_type, node.start_byte,
|
|
345
|
+
first_reassignment - 1
|
|
346
|
+
if first_reassignment is not None else scope_end,
|
|
347
|
+
)
|
|
348
|
+
elif value_node is not None and self._is_mockito_factory_call(value_node, source):
|
|
349
|
+
first_reassignment = next(
|
|
350
|
+
(offset for offset, _ in assignment_events),
|
|
351
|
+
None,
|
|
352
|
+
)
|
|
353
|
+
scopes.add_allocation(
|
|
354
|
+
name, _MOCK_ALLOCATION_TYPE, node.start_byte,
|
|
355
|
+
first_reassignment - 1
|
|
356
|
+
if first_reassignment is not None else scope_end,
|
|
357
|
+
)
|
|
358
|
+
for index, (offset, allocation_type) in enumerate(assignment_events):
|
|
359
|
+
if allocation_type is None:
|
|
360
|
+
continue
|
|
361
|
+
next_offset = (
|
|
362
|
+
assignment_events[index + 1][0]
|
|
363
|
+
if index + 1 < len(assignment_events) else None
|
|
364
|
+
)
|
|
365
|
+
scopes.add_allocation(
|
|
366
|
+
name, allocation_type, offset,
|
|
367
|
+
next_offset - 1 if next_offset is not None else scope_end,
|
|
368
|
+
)
|
|
369
|
+
scopes.add(
|
|
370
|
+
name, type_text,
|
|
371
|
+
node.start_byte, scope_end,
|
|
372
|
+
)
|
|
373
|
+
elif node.type == "enhanced_for_statement":
|
|
374
|
+
type_node = node.child_by_field_name("type")
|
|
375
|
+
name_node = node.child_by_field_name("name")
|
|
376
|
+
body_node = node.child_by_field_name("body")
|
|
377
|
+
if type_node is not None and name_node is not None and body_node is not None:
|
|
378
|
+
# loop variable is visible only inside the loop body
|
|
379
|
+
# (JLS 14.14.2) — NOT the iterable expression, which
|
|
380
|
+
# resolves in the enclosing scope (F-01 fix: this used
|
|
381
|
+
# to start at the whole statement's start_byte, so a
|
|
382
|
+
# loop var shadowed a same-named field/local inside its
|
|
383
|
+
# own iterable expression, e.g. `for (A x : x.items())`
|
|
384
|
+
# where `x` before `:` must stay the outer `x`).
|
|
385
|
+
scopes.add(
|
|
386
|
+
self._text(name_node, source), self._text(type_node, source),
|
|
387
|
+
body_node.start_byte, node.end_byte,
|
|
388
|
+
)
|
|
389
|
+
elif node.type == "resource":
|
|
390
|
+
type_node = node.child_by_field_name("type")
|
|
391
|
+
name_node = node.child_by_field_name("name")
|
|
392
|
+
if type_node is not None and name_node is not None:
|
|
393
|
+
type_text: Optional[str] = self._text(type_node, source)
|
|
394
|
+
if type_text == "var":
|
|
395
|
+
# `try (var ctx = new Context(...))`: the resource's
|
|
396
|
+
# type is exactly its initializer's type token; a
|
|
397
|
+
# source-call initializer is typed by the context-
|
|
398
|
+
# aware second pass, and anything else stays unknown
|
|
399
|
+
# (the literal token `var` is never a class name).
|
|
400
|
+
type_text = self._var_initializer_type(
|
|
401
|
+
node.child_by_field_name("value"), source,
|
|
402
|
+
)
|
|
403
|
+
if type_text is not None:
|
|
404
|
+
scope_end = self._enclosing_scope_end(
|
|
405
|
+
node, ("try_with_resources_statement",), node.end_byte,
|
|
406
|
+
)
|
|
407
|
+
scopes.add(
|
|
408
|
+
self._text(name_node, source), type_text,
|
|
409
|
+
node.start_byte, scope_end,
|
|
410
|
+
)
|
|
411
|
+
elif node.type == "catch_formal_parameter":
|
|
412
|
+
name_node = node.child_by_field_name("name")
|
|
413
|
+
catch_type = next(
|
|
414
|
+
(c for c in node.children if c.type == "catch_type"), None
|
|
415
|
+
)
|
|
416
|
+
type_node = (
|
|
417
|
+
next((c for c in catch_type.children if c.is_named), None)
|
|
418
|
+
if catch_type is not None else None
|
|
419
|
+
)
|
|
420
|
+
if type_node is not None and name_node is not None:
|
|
421
|
+
scope_end = self._enclosing_scope_end(
|
|
422
|
+
node, ("catch_clause",), node.end_byte,
|
|
423
|
+
)
|
|
424
|
+
scopes.add(
|
|
425
|
+
self._text(name_node, source), self._text(type_node, source),
|
|
426
|
+
node.start_byte, scope_end,
|
|
427
|
+
)
|
|
428
|
+
elif node.type == "instanceof_expression":
|
|
429
|
+
# JLS 6.3 flow scoping for a type pattern. The grammar keeps
|
|
430
|
+
# the matched type in `right` and the introduced identifier
|
|
431
|
+
# in `name` (`value instanceof Item item`). Add the binding
|
|
432
|
+
# only where the condition proves the match:
|
|
433
|
+
#
|
|
434
|
+
# * the RHS of `pattern && rhs`;
|
|
435
|
+
# * a positive `if` consequence; or
|
|
436
|
+
# * after a negated guard whose consequence exits directly.
|
|
437
|
+
#
|
|
438
|
+
# Other boolean shapes deliberately remain unresolved rather
|
|
439
|
+
# than leaking a pattern variable into a path where it may not
|
|
440
|
+
# exist.
|
|
441
|
+
type_node = node.child_by_field_name("right")
|
|
442
|
+
name_node = node.child_by_field_name("name")
|
|
443
|
+
if type_node is not None and name_node is not None:
|
|
444
|
+
pattern_type = self._text(type_node, source)
|
|
445
|
+
pattern_name = self._text(name_node, source)
|
|
446
|
+
ancestor = node.parent
|
|
447
|
+
while ancestor is not None and ancestor != body:
|
|
448
|
+
if ancestor.type == "binary_expression":
|
|
449
|
+
operator = ancestor.child_by_field_name("operator")
|
|
450
|
+
left = ancestor.child_by_field_name("left")
|
|
451
|
+
right = ancestor.child_by_field_name("right")
|
|
452
|
+
if (
|
|
453
|
+
operator is not None
|
|
454
|
+
and self._text(operator, source) == "&&"
|
|
455
|
+
and left is not None and right is not None
|
|
456
|
+
and left.start_byte <= node.start_byte
|
|
457
|
+
and node.end_byte <= left.end_byte
|
|
458
|
+
):
|
|
459
|
+
scopes.add(
|
|
460
|
+
pattern_name, pattern_type,
|
|
461
|
+
right.start_byte, right.end_byte,
|
|
462
|
+
)
|
|
463
|
+
break
|
|
464
|
+
if ancestor.type == "if_statement":
|
|
465
|
+
condition = ancestor.child_by_field_name("condition")
|
|
466
|
+
consequence = ancestor.child_by_field_name("consequence")
|
|
467
|
+
alternative = ancestor.child_by_field_name("alternative")
|
|
468
|
+
if condition is None or consequence is None:
|
|
469
|
+
break
|
|
470
|
+
condition_text = self._text(condition, source).strip()
|
|
471
|
+
if condition_text.lstrip("(").startswith("!"):
|
|
472
|
+
abrupt_consequence = consequence.type in {
|
|
473
|
+
"return_statement", "throw_statement",
|
|
474
|
+
"break_statement", "continue_statement",
|
|
475
|
+
}
|
|
476
|
+
if consequence.type == "block":
|
|
477
|
+
statements = [
|
|
478
|
+
child for child in consequence.children
|
|
479
|
+
if child.is_named
|
|
480
|
+
]
|
|
481
|
+
abrupt_consequence = bool(
|
|
482
|
+
statements
|
|
483
|
+
and statements[-1].type in {
|
|
484
|
+
"return_statement", "throw_statement",
|
|
485
|
+
"break_statement", "continue_statement",
|
|
486
|
+
}
|
|
487
|
+
)
|
|
488
|
+
if (
|
|
489
|
+
alternative is None
|
|
490
|
+
and abrupt_consequence
|
|
491
|
+
):
|
|
492
|
+
scope_end = self._enclosing_scope_end(
|
|
493
|
+
ancestor, ("block",), body.end_byte,
|
|
494
|
+
)
|
|
495
|
+
scopes.add(
|
|
496
|
+
pattern_name, pattern_type,
|
|
497
|
+
ancestor.end_byte, scope_end,
|
|
498
|
+
)
|
|
499
|
+
else:
|
|
500
|
+
scopes.add(
|
|
501
|
+
pattern_name, pattern_type,
|
|
502
|
+
consequence.start_byte, consequence.end_byte,
|
|
503
|
+
)
|
|
504
|
+
break
|
|
505
|
+
ancestor = ancestor.parent
|
|
506
|
+
return scopes
|
|
507
|
+
|
|
508
|
+
def _collect_lambda_parameter_scopes(
|
|
509
|
+
self, body, source: bytes, module_name: str,
|
|
510
|
+
enclosing_class_id: Optional[str], import_map: Dict[str, str],
|
|
511
|
+
wildcard_packages: List[str], local_types: "_LocalScopes",
|
|
512
|
+
param_types: Dict[str, str], type_param_bounds: Dict[str, str],
|
|
513
|
+
static_wildcard_classes: List[str],
|
|
514
|
+
) -> None:
|
|
515
|
+
"""Add contextually typed lambda parameters to lexical locals.
|
|
516
|
+
|
|
517
|
+
Covers source functional-interface parameters (for example
|
|
518
|
+
``Comparator<Option>``) and the common ``Stream<T>.map`` pipeline.
|
|
519
|
+
Unknown or multiply-resolved contexts add nothing.
|
|
520
|
+
"""
|
|
521
|
+
for node in self._method_facts(body).lambda_expressions:
|
|
522
|
+
params_node = node.child_by_field_name("parameters")
|
|
523
|
+
lambda_body = node.child_by_field_name("body")
|
|
524
|
+
args_node = node.parent
|
|
525
|
+
call_node = args_node.parent if args_node is not None else None
|
|
526
|
+
inferred: Optional[str] = None
|
|
527
|
+
# Assignment context: `Consumer<Scenario> consumer = it -> ...`.
|
|
528
|
+
# For the single implicit parameter shape, the functional
|
|
529
|
+
# interface's first generic argument is its contextual type.
|
|
530
|
+
declarator = node.parent
|
|
531
|
+
declaration = declarator.parent if declarator is not None else None
|
|
532
|
+
if (
|
|
533
|
+
params_node is not None
|
|
534
|
+
and params_node.type == "identifier"
|
|
535
|
+
and declarator is not None
|
|
536
|
+
and declarator.type == "variable_declarator"
|
|
537
|
+
and declaration is not None
|
|
538
|
+
and declaration.type == "local_variable_declaration"
|
|
539
|
+
):
|
|
540
|
+
declared_type = declaration.child_by_field_name("type")
|
|
541
|
+
generic_args = (
|
|
542
|
+
self._generic_type_arguments(self._text(declared_type, source))
|
|
543
|
+
if declared_type is not None else []
|
|
544
|
+
)
|
|
545
|
+
inferred = generic_args[0] if generic_args else None
|
|
546
|
+
# Return context: `Predicate<Bean> factory() { return bean ->
|
|
547
|
+
# ...; }` and `Function<P, R> f() { return p -> ...; }`.
|
|
548
|
+
# The enclosing method's declared functional type provides
|
|
549
|
+
# the implicit lambda parameter even though there is no
|
|
550
|
+
# assignment or call-argument target at the lambda node.
|
|
551
|
+
if inferred is None and params_node is not None:
|
|
552
|
+
ancestor = node.parent
|
|
553
|
+
returned = False
|
|
554
|
+
while ancestor is not None and ancestor.type not in _METHOD_CST_TYPES:
|
|
555
|
+
if ancestor.type == "return_statement":
|
|
556
|
+
returned = True
|
|
557
|
+
ancestor = ancestor.parent
|
|
558
|
+
if returned and ancestor is not None:
|
|
559
|
+
return_type_node = ancestor.child_by_field_name("type")
|
|
560
|
+
generic_args = (
|
|
561
|
+
self._generic_type_arguments(
|
|
562
|
+
self._text(return_type_node, source),
|
|
563
|
+
)
|
|
564
|
+
if return_type_node is not None else []
|
|
565
|
+
)
|
|
566
|
+
inferred = generic_args[0] if generic_args else None
|
|
567
|
+
if inferred is None and return_type_node is not None:
|
|
568
|
+
functional_type = self._text(return_type_node, source)
|
|
569
|
+
functional_id = self._resolve_declared_type(
|
|
570
|
+
functional_type, module_name, import_map,
|
|
571
|
+
wildcard_packages, enclosing_class_id,
|
|
572
|
+
)
|
|
573
|
+
if functional_id is not None:
|
|
574
|
+
abstract_methods = [
|
|
575
|
+
child for child in self._contains_children(functional_id)
|
|
576
|
+
if self.builder._nodes[child].node_type == NodeType.FUNCTION
|
|
577
|
+
and self.builder._nodes[child].metadata.get("java_is_abstract")
|
|
578
|
+
and not self.builder._nodes[child].metadata.get("java_is_static")
|
|
579
|
+
]
|
|
580
|
+
if len(abstract_methods) == 1:
|
|
581
|
+
sam_params = self._param_type_lists.get(
|
|
582
|
+
abstract_methods[0], [],
|
|
583
|
+
)
|
|
584
|
+
inferred = sam_params[0] if sam_params else None
|
|
585
|
+
if (
|
|
586
|
+
params_node is not None and lambda_body is not None
|
|
587
|
+
and args_node is not None and args_node.type == "argument_list"
|
|
588
|
+
and call_node is not None and call_node.type == "method_invocation"
|
|
589
|
+
):
|
|
590
|
+
args = argument_nodes(args_node)
|
|
591
|
+
arg_index = next((i for i, c in enumerate(args) if c == node), -1)
|
|
592
|
+
name_node = call_node.child_by_field_name("name")
|
|
593
|
+
receiver_node = call_node.child_by_field_name("object")
|
|
594
|
+
if arg_index >= 0 and name_node is not None:
|
|
595
|
+
method_name = self._text(name_node, source)
|
|
596
|
+
receiver_text = (
|
|
597
|
+
self._declared_receiver_type_text(
|
|
598
|
+
receiver_node, source, module_name, enclosing_class_id,
|
|
599
|
+
import_map, wildcard_packages, local_types,
|
|
600
|
+
self._all_field_types(enclosing_class_id)
|
|
601
|
+
if enclosing_class_id else {},
|
|
602
|
+
param_types, type_param_bounds,
|
|
603
|
+
)
|
|
604
|
+
if receiver_node is not None else None
|
|
605
|
+
)
|
|
606
|
+
receiver_simple = (
|
|
607
|
+
erased_type(receiver_text).strip().split(".")[-1]
|
|
608
|
+
if receiver_text is not None else None
|
|
609
|
+
)
|
|
610
|
+
external_element_lambda = (
|
|
611
|
+
receiver_simple == "Stream"
|
|
612
|
+
and method_name in JAVA_STREAM_ELEMENT_LAMBDA_METHODS
|
|
613
|
+
) or (
|
|
614
|
+
receiver_simple == "Optional"
|
|
615
|
+
and method_name in JAVA_OPTIONAL_ELEMENT_LAMBDA_METHODS
|
|
616
|
+
)
|
|
617
|
+
primitive_element = _PRIMITIVE_STREAM_ELEMENT.get(receiver_simple or "")
|
|
618
|
+
if (
|
|
619
|
+
arg_index == 0 and primitive_element is not None
|
|
620
|
+
and method_name in JAVA_STREAM_ELEMENT_LAMBDA_METHODS
|
|
621
|
+
):
|
|
622
|
+
# `IntStream.iterate(0, n -> n + 2).mapToObj(n ->
|
|
623
|
+
# ...)`: a primitive stream's element-consuming
|
|
624
|
+
# lambda parameter is exactly `int`/`long`/
|
|
625
|
+
# `double` (batch 26) — no type argument to read.
|
|
626
|
+
inferred = primitive_element
|
|
627
|
+
elif arg_index == 0 and external_element_lambda:
|
|
628
|
+
generic_args = self._generic_type_arguments(receiver_text)
|
|
629
|
+
inferred = generic_args[0] if generic_args else None
|
|
630
|
+
elif (
|
|
631
|
+
params_node.type == "identifier"
|
|
632
|
+
and arg_index == 0
|
|
633
|
+
and method_name == "groupingBy"
|
|
634
|
+
and "java.util.stream.Collectors" in static_wildcard_classes
|
|
635
|
+
):
|
|
636
|
+
# `Stream<T>.collect(groupingBy(t -> ...))` gives
|
|
637
|
+
# the static-imported collector's first Function
|
|
638
|
+
# argument the stream element type. Require the
|
|
639
|
+
# exact JDK static wildcard plus the enclosing
|
|
640
|
+
# `collect` call, so a project-local `groupingBy`
|
|
641
|
+
# cannot acquire a guessed lambda type.
|
|
642
|
+
outer_args = call_node.parent
|
|
643
|
+
outer_call = (
|
|
644
|
+
outer_args.parent if outer_args is not None
|
|
645
|
+
and outer_args.type == "argument_list" else None
|
|
646
|
+
)
|
|
647
|
+
outer_name = (
|
|
648
|
+
outer_call.child_by_field_name("name")
|
|
649
|
+
if outer_call is not None
|
|
650
|
+
and outer_call.type == "method_invocation" else None
|
|
651
|
+
)
|
|
652
|
+
outer_receiver = (
|
|
653
|
+
outer_call.child_by_field_name("object")
|
|
654
|
+
if outer_call is not None else None
|
|
655
|
+
)
|
|
656
|
+
if (
|
|
657
|
+
outer_name is not None
|
|
658
|
+
and self._text(outer_name, source) == "collect"
|
|
659
|
+
and outer_receiver is not None
|
|
660
|
+
):
|
|
661
|
+
outer_type = self._declared_receiver_type_text(
|
|
662
|
+
outer_receiver, source, module_name,
|
|
663
|
+
enclosing_class_id, import_map,
|
|
664
|
+
wildcard_packages, local_types,
|
|
665
|
+
self._all_field_types(enclosing_class_id)
|
|
666
|
+
if enclosing_class_id else {},
|
|
667
|
+
param_types, type_param_bounds,
|
|
668
|
+
)
|
|
669
|
+
if (
|
|
670
|
+
outer_type is not None
|
|
671
|
+
and erased_type(outer_type).strip().split(".")[-1] == "Stream"
|
|
672
|
+
):
|
|
673
|
+
generic_args = self._generic_type_arguments(outer_type)
|
|
674
|
+
inferred = generic_args[0] if generic_args else None
|
|
675
|
+
elif (
|
|
676
|
+
params_node.type == "identifier"
|
|
677
|
+
and arg_index == 0
|
|
678
|
+
and receiver_simple == "Function"
|
|
679
|
+
and method_name in {"andThen", "compose"}
|
|
680
|
+
):
|
|
681
|
+
generic_args = self._generic_type_arguments(receiver_text)
|
|
682
|
+
if len(generic_args) == 2:
|
|
683
|
+
inferred = generic_args[1 if method_name == "andThen" else 0]
|
|
684
|
+
elif (
|
|
685
|
+
params_node.type == "identifier"
|
|
686
|
+
and arg_index == 1
|
|
687
|
+
and method_name == "computeIfAbsent"
|
|
688
|
+
and receiver_simple in {
|
|
689
|
+
"Map", "HashMap", "LinkedHashMap", "TreeMap",
|
|
690
|
+
"ConcurrentHashMap",
|
|
691
|
+
}
|
|
692
|
+
):
|
|
693
|
+
# JDK Map.computeIfAbsent(K, Function<K,V>) gives
|
|
694
|
+
# its second argument an exact key type. This is
|
|
695
|
+
# deliberately not generalized to compute/merge:
|
|
696
|
+
# their BiFunction parameters have distinct K/V
|
|
697
|
+
# positions and need a multi-parameter model.
|
|
698
|
+
generic_args = self._generic_type_arguments(receiver_text)
|
|
699
|
+
inferred = generic_args[0] if len(generic_args) == 2 else None
|
|
700
|
+
else:
|
|
701
|
+
receiver_id = enclosing_class_id if receiver_node is None else None
|
|
702
|
+
if receiver_node is not None:
|
|
703
|
+
receiver_id, _ = self._resolve_receiver(
|
|
704
|
+
receiver_node, source, module_name, enclosing_class_id,
|
|
705
|
+
import_map, wildcard_packages, local_types,
|
|
706
|
+
self._all_field_types(enclosing_class_id)
|
|
707
|
+
if enclosing_class_id else {},
|
|
708
|
+
param_types, type_param_bounds,
|
|
709
|
+
)
|
|
710
|
+
if receiver_id is not None:
|
|
711
|
+
candidates = self._members_named(
|
|
712
|
+
receiver_id, method_name, search_ancestors=True,
|
|
713
|
+
)
|
|
714
|
+
arity = self._count_args(args_node)
|
|
715
|
+
argument_types = self._infer_arg_types(
|
|
716
|
+
args_node, source, local_types, param_types,
|
|
717
|
+
self._all_field_types(enclosing_class_id)
|
|
718
|
+
if enclosing_class_id else {},
|
|
719
|
+
)
|
|
720
|
+
target, _ = self._select_overload(
|
|
721
|
+
candidates, arity, argument_types,
|
|
722
|
+
lambda _candidate: 0.9,
|
|
723
|
+
_ImportContext(
|
|
724
|
+
module_name, import_map, wildcard_packages,
|
|
725
|
+
enclosing_class_id,
|
|
726
|
+
),
|
|
727
|
+
)
|
|
728
|
+
if isinstance(target, str):
|
|
729
|
+
# A lambda past the fixed-arity prefix of a
|
|
730
|
+
# varargs overload (`run(a -> .., b -> ..)`
|
|
731
|
+
# into `run(Consumer<T>... steps)`) has no
|
|
732
|
+
# positional parameter of its own: it is
|
|
733
|
+
# typed by the vararg component, never by
|
|
734
|
+
# a raw index that would overrun the list.
|
|
735
|
+
params = self._param_type_lists.get(target, [])
|
|
736
|
+
if arg_index < len(params):
|
|
737
|
+
declared = params[arg_index]
|
|
738
|
+
elif params and self._is_vararg_param(params[-1]):
|
|
739
|
+
declared = params[-1]
|
|
740
|
+
else:
|
|
741
|
+
declared = ""
|
|
742
|
+
generic_args = self._generic_type_arguments(declared)
|
|
743
|
+
inferred = generic_args[0] if generic_args else None
|
|
744
|
+
elif params_node.type == "identifier":
|
|
745
|
+
# Overloaded functional factories often
|
|
746
|
+
# cannot be selected until the lambda is
|
|
747
|
+
# typed, while typing previously required
|
|
748
|
+
# selecting the overload—a cycle. Break it
|
|
749
|
+
# only when every arity-compatible source
|
|
750
|
+
# candidate declares the same functional
|
|
751
|
+
# input type at this argument position.
|
|
752
|
+
contextual: Set[str] = set()
|
|
753
|
+
for candidate in candidates:
|
|
754
|
+
if not self._arity_matches(candidate, arity):
|
|
755
|
+
continue
|
|
756
|
+
declared_params = self._param_type_lists.get(
|
|
757
|
+
candidate, [],
|
|
758
|
+
)
|
|
759
|
+
if arg_index >= len(declared_params):
|
|
760
|
+
continue
|
|
761
|
+
generic_args = self._generic_type_arguments(
|
|
762
|
+
declared_params[arg_index],
|
|
763
|
+
)
|
|
764
|
+
if generic_args:
|
|
765
|
+
contextual.add(generic_args[0])
|
|
766
|
+
if len(contextual) == 1:
|
|
767
|
+
inferred = next(iter(contextual))
|
|
768
|
+
if inferred:
|
|
769
|
+
names = (
|
|
770
|
+
[self._text(params_node, source)]
|
|
771
|
+
if params_node.type == "identifier"
|
|
772
|
+
else [self._text(c, source) for c in params_node.children if c.type == "identifier"]
|
|
773
|
+
)
|
|
774
|
+
for name in names:
|
|
775
|
+
local_types.add(name, inferred, lambda_body.start_byte, lambda_body.end_byte)
|
|
776
|
+
elif params_node is not None and lambda_body is not None:
|
|
777
|
+
# The lambda genuinely has parameters, but every
|
|
778
|
+
# inference path above failed — most commonly a lambda
|
|
779
|
+
# passed to a JDK method outside the small allow-lists
|
|
780
|
+
# above (`List<T>.forEach`, etc.), whose declaring type
|
|
781
|
+
# isn't in source so its SAM parameter type can't be
|
|
782
|
+
# read. The parameter name is still a real declaration
|
|
783
|
+
# that shadows any same-named outer local/param/field
|
|
784
|
+
# for the lambda body's extent, even though its type
|
|
785
|
+
# stays unknown ("missing edge over wrong edge") — record
|
|
786
|
+
# it the same way an unresolvable `var` local is (P4.4
|
|
787
|
+
# shadow-detection fix), so callers checking
|
|
788
|
+
# `declared_at` don't fall through to the outer scope.
|
|
789
|
+
names = (
|
|
790
|
+
[self._text(params_node, source)]
|
|
791
|
+
if params_node.type == "identifier"
|
|
792
|
+
else [self._text(c, source) for c in params_node.children if c.type == "identifier"]
|
|
793
|
+
)
|
|
794
|
+
for name in names:
|
|
795
|
+
local_types.add_declared(name, lambda_body.start_byte, lambda_body.end_byte)
|
|
796
|
+
|
|
797
|
+
def _var_initializer_type(
|
|
798
|
+
self, value_node, source: bytes,
|
|
799
|
+
local_types: Optional["_LocalScopes"] = None,
|
|
800
|
+
param_types: Optional[Dict[str, str]] = None,
|
|
801
|
+
field_types: Optional[Dict[str, str]] = None,
|
|
802
|
+
) -> Optional[str]:
|
|
803
|
+
"""Best-effort declared type for a `var` local's initializer (JLS
|
|
804
|
+
14.4), used only by `_collect_local_scopes`'s `var` branch (V-01
|
|
805
|
+
fix). Limited to the initializer forms that already carry an
|
|
806
|
+
explicit type token the same way `_infer_arg_type` reads them for a
|
|
807
|
+
method-call argument — `new Foo()` (`object_creation_expression`),
|
|
808
|
+
`(Foo) x` (`cast_expression`), both exposing a `type` CST field
|
|
809
|
+
directly, and `arr[i]` (`array_access`), delegated to
|
|
810
|
+
`_infer_arg_type`'s own array-element-type peeling so `var first =
|
|
811
|
+
args[0]` off a `String[] args` parameter resolves the same way
|
|
812
|
+
`save(args[0])` already does (9c77bb7's direct-argument case, one
|
|
813
|
+
assignment hop further). The `array_access` branch is a no-op unless
|
|
814
|
+
a caller supplies at least one of `local_types`/`param_types`/
|
|
815
|
+
`field_types` — every other `_var_initializer_type` call site keeps
|
|
816
|
+
passing none of them, since none can ever reach an `array_access`
|
|
817
|
+
value (each already gates on `object_creation_expression`
|
|
818
|
+
specifically). Anything else (a method call's declared return type,
|
|
819
|
+
a literal, a binary/ternary expression, a chained call, ...) returns
|
|
820
|
+
None — deliberately not a general type-inference pass; the caller
|
|
821
|
+
then leaves the local out of the scope map entirely rather than
|
|
822
|
+
guessing, so it resolves the same "unknown, no edge" way an
|
|
823
|
+
already-unresolvable receiver does elsewhere in this file."""
|
|
824
|
+
if value_node is None:
|
|
825
|
+
return None
|
|
826
|
+
if value_node.type in ("true", "false"):
|
|
827
|
+
# JLS 3.10.3: the two boolean literals have type `boolean`.
|
|
828
|
+
# This is as declaration-backed as a typed local and lets a
|
|
829
|
+
# `var enabled = false` argument select a boolean overload.
|
|
830
|
+
return "boolean"
|
|
831
|
+
if value_node.type in ("object_creation_expression", "cast_expression"):
|
|
832
|
+
type_node = value_node.child_by_field_name("type")
|
|
833
|
+
return (
|
|
834
|
+
source[type_node.start_byte:type_node.end_byte].decode("utf8")
|
|
835
|
+
if type_node is not None else None
|
|
836
|
+
)
|
|
837
|
+
if value_node.type == "array_access" and (
|
|
838
|
+
local_types is not None or param_types is not None or field_types is not None
|
|
839
|
+
):
|
|
840
|
+
return self._infer_arg_type(
|
|
841
|
+
value_node, source, local_types or _LocalScopes(),
|
|
842
|
+
param_types or {}, field_types or {},
|
|
843
|
+
)
|
|
844
|
+
return None
|
|
845
|
+
|
|
846
|
+
@staticmethod
|
|
847
|
+
def _is_mockito_factory_call(value_node, source: bytes) -> bool:
|
|
848
|
+
"""True iff `value_node` is a call to Mockito's `mock(...)`/
|
|
849
|
+
`spy(...)` factory (bare, statically imported, or qualified
|
|
850
|
+
`Mockito.mock(...)`/`BDDMockito.mock(...)`). A mock/spy receiver has
|
|
851
|
+
no real declared implementation to dispatch to at runtime — this is
|
|
852
|
+
an exact fact about a ubiquitous, unambiguous third-party API, not a
|
|
853
|
+
guess, so `_abstract_fan_out` can safely treat it as "no concrete
|
|
854
|
+
target" instead of fanning out across unrelated project code that
|
|
855
|
+
happens to implement the same interface."""
|
|
856
|
+
if value_node.type != "method_invocation":
|
|
857
|
+
return False
|
|
858
|
+
name_node = value_node.child_by_field_name("name")
|
|
859
|
+
if name_node is None or source[name_node.start_byte:name_node.end_byte].decode(
|
|
860
|
+
"utf8", errors="replace",
|
|
861
|
+
) not in ("mock", "spy"):
|
|
862
|
+
return False
|
|
863
|
+
receiver = value_node.child_by_field_name("object")
|
|
864
|
+
if receiver is None:
|
|
865
|
+
return True # bare call: `mock(...)`, statically imported.
|
|
866
|
+
return receiver.type == "identifier" and source[
|
|
867
|
+
receiver.start_byte:receiver.end_byte
|
|
868
|
+
].decode("utf8", errors="replace") in ("Mockito", "BDDMockito")
|
|
869
|
+
|
|
870
|
+
def _name_reassigned_in_range(
|
|
871
|
+
self, assignments, source: bytes, name: str, after: int, before: int,
|
|
872
|
+
) -> bool:
|
|
873
|
+
"""True iff a plain `name = ...;` assignment appears anywhere in
|
|
874
|
+
one of the already-collected assignment expressions strictly after
|
|
875
|
+
byte `after` and at/before `before`. Method facts exclude nested
|
|
876
|
+
method/type bodies, so a same-named local or field there cannot be a
|
|
877
|
+
reassignment of this one. Used to keep a declaration-site allocation fact (a typed
|
|
878
|
+
local's method-call initializer) from silently outliving a later,
|
|
879
|
+
different assignment to the same name in the same scope."""
|
|
880
|
+
for node in assignments:
|
|
881
|
+
children = [c for c in node.children if not c.is_extra]
|
|
882
|
+
if (
|
|
883
|
+
len(children) >= 3 and children[1].type == "="
|
|
884
|
+
and children[0].type == "identifier"
|
|
885
|
+
and after < children[0].start_byte <= before
|
|
886
|
+
and self._text(children[0], source) == name
|
|
887
|
+
):
|
|
888
|
+
return True
|
|
889
|
+
return False
|
|
890
|
+
|
|
891
|
+
def _collect_var_method_call_scopes(
|
|
892
|
+
self, body, source: bytes, module_name: str,
|
|
893
|
+
enclosing_class_id: Optional[str], import_map: Dict[str, str],
|
|
894
|
+
wildcard_packages: List[str], local_types: "_LocalScopes",
|
|
895
|
+
param_types: Dict[str, str], type_param_bounds: Dict[str, str],
|
|
896
|
+
) -> None:
|
|
897
|
+
"""Infer context-dependent ``var`` locals from resolvable source types.
|
|
898
|
+
|
|
899
|
+
The first local-scope pass intentionally handles syntax-only types.
|
|
900
|
+
This context-aware second pass can reuse source method selection and
|
|
901
|
+
its declared return type without introducing a name guess. It also
|
|
902
|
+
covers enhanced-for variables when the iterable has an exact array or
|
|
903
|
+
single-element generic type (for example ``Status.values()`` yielding
|
|
904
|
+
``Status[]``); the variable remains scoped to the loop body.
|
|
905
|
+
"""
|
|
906
|
+
field_types = (
|
|
907
|
+
self._all_field_types(enclosing_class_id) if enclosing_class_id else {}
|
|
908
|
+
)
|
|
909
|
+
facts = self._method_facts(body)
|
|
910
|
+
# `_method_facts` collects its DFS in reverse source order (a LIFO
|
|
911
|
+
# stack pushes natural-order children). This pass depends on earlier
|
|
912
|
+
# declarations, so reverse the immutable subset to retain the old
|
|
913
|
+
# source-order traversal exactly.
|
|
914
|
+
for node in reversed(facts.local_scope_nodes):
|
|
915
|
+
if node.type == "local_variable_declaration":
|
|
916
|
+
type_node = node.child_by_field_name("type")
|
|
917
|
+
if type_node is not None and self._text(type_node, source) == "var":
|
|
918
|
+
scope_end = self._enclosing_scope_end(
|
|
919
|
+
node, ("block", "for_statement", "switch_block"), body.end_byte,
|
|
920
|
+
)
|
|
921
|
+
for child in node.children:
|
|
922
|
+
if child.type != "variable_declarator":
|
|
923
|
+
continue
|
|
924
|
+
name_node = child.child_by_field_name("name")
|
|
925
|
+
value_node = child.child_by_field_name("value")
|
|
926
|
+
if name_node is None or value_node is None:
|
|
927
|
+
continue
|
|
928
|
+
inferred: Optional[str] = None
|
|
929
|
+
if value_node.type == "method_invocation":
|
|
930
|
+
inferred = self._generic_method_invocation_return_type(
|
|
931
|
+
value_node, source, module_name, enclosing_class_id,
|
|
932
|
+
import_map, wildcard_packages, local_types, field_types,
|
|
933
|
+
param_types, type_param_bounds,
|
|
934
|
+
)
|
|
935
|
+
elif value_node.type == "field_access":
|
|
936
|
+
# `var folder = options.outputFolder` has no
|
|
937
|
+
# explicit type token, but the selected receiver
|
|
938
|
+
# class owns an exact declared field type. This
|
|
939
|
+
# is a bounded local propagation step, not a
|
|
940
|
+
# global name fallback: unresolved receivers or
|
|
941
|
+
# fields still leave the `var` unknown.
|
|
942
|
+
receiver = value_node.child_by_field_name("object")
|
|
943
|
+
field = value_node.child_by_field_name("field")
|
|
944
|
+
if receiver is not None and field is not None:
|
|
945
|
+
receiver_id, _ = self._resolve_receiver(
|
|
946
|
+
receiver, source, module_name, enclosing_class_id,
|
|
947
|
+
import_map, wildcard_packages, local_types,
|
|
948
|
+
field_types, param_types, type_param_bounds,
|
|
949
|
+
)
|
|
950
|
+
if receiver_id is not None:
|
|
951
|
+
inferred = self._all_field_types(receiver_id).get(
|
|
952
|
+
self._text(field, source)
|
|
953
|
+
)
|
|
954
|
+
elif value_node.type == "ternary_expression":
|
|
955
|
+
inferred = self._declared_receiver_type_text(
|
|
956
|
+
value_node, source, module_name,
|
|
957
|
+
enclosing_class_id, import_map,
|
|
958
|
+
wildcard_packages, local_types, field_types,
|
|
959
|
+
param_types, type_param_bounds,
|
|
960
|
+
)
|
|
961
|
+
if inferred is not None:
|
|
962
|
+
local_types.add(
|
|
963
|
+
self._text(name_node, source), inferred,
|
|
964
|
+
node.start_byte, scope_end,
|
|
965
|
+
)
|
|
966
|
+
elif type_node is not None:
|
|
967
|
+
# A typed (non-`var`) local's initializer may be a
|
|
968
|
+
# source method call whose *declared* return type is
|
|
969
|
+
# more concrete than the local's own declared type
|
|
970
|
+
# reveals -- e.g. `ConfigurationParameters configParams
|
|
971
|
+
# = fromMapAndParent(...)`, where `fromMapAndParent` is
|
|
972
|
+
# declared to return the concrete
|
|
973
|
+
# `LauncherConfigurationParameters`, not the interface
|
|
974
|
+
# `configParams` is declared as. Dispatch on
|
|
975
|
+
# `configParams` still uses its *declared* interface
|
|
976
|
+
# type (Java static-dispatch semantics, preserved
|
|
977
|
+
# elsewhere in this file), but `_abstract_fan_out` can
|
|
978
|
+
# use this as allocation evidence the same way a direct
|
|
979
|
+
# `new Impl()` initializer already narrows it (P5
|
|
980
|
+
# finding #2) -- reusing the exact return-type
|
|
981
|
+
# propagation this pass already trusts for `var`
|
|
982
|
+
# locals, never a new inference. Skipped when the local
|
|
983
|
+
# is reassigned anywhere later in its own scope, since a
|
|
984
|
+
# stale allocation recorded only at the declaration site
|
|
985
|
+
# would otherwise silently outlive a later, different
|
|
986
|
+
# assignment.
|
|
987
|
+
scope_end = self._enclosing_scope_end(
|
|
988
|
+
node, ("block", "for_statement", "switch_block"), body.end_byte,
|
|
989
|
+
)
|
|
990
|
+
for child in node.children:
|
|
991
|
+
if child.type != "variable_declarator":
|
|
992
|
+
continue
|
|
993
|
+
name_node = child.child_by_field_name("name")
|
|
994
|
+
value_node = child.child_by_field_name("value")
|
|
995
|
+
if (
|
|
996
|
+
name_node is None or value_node is None
|
|
997
|
+
or value_node.type != "method_invocation"
|
|
998
|
+
):
|
|
999
|
+
continue
|
|
1000
|
+
inferred = self._generic_method_invocation_return_type(
|
|
1001
|
+
value_node, source, module_name, enclosing_class_id,
|
|
1002
|
+
import_map, wildcard_packages, local_types,
|
|
1003
|
+
field_types, param_types, type_param_bounds,
|
|
1004
|
+
)
|
|
1005
|
+
if inferred is None:
|
|
1006
|
+
continue
|
|
1007
|
+
allocation_id = self._resolve_declared_type(
|
|
1008
|
+
inferred, module_name, import_map,
|
|
1009
|
+
wildcard_packages, enclosing_class_id,
|
|
1010
|
+
)
|
|
1011
|
+
if (
|
|
1012
|
+
allocation_id is None
|
|
1013
|
+
or not self._is_concrete_class(allocation_id)
|
|
1014
|
+
):
|
|
1015
|
+
continue
|
|
1016
|
+
name = self._text(name_node, source)
|
|
1017
|
+
if self._name_reassigned_in_range(
|
|
1018
|
+
facts.assignment_expressions, source, name,
|
|
1019
|
+
node.end_byte, scope_end,
|
|
1020
|
+
):
|
|
1021
|
+
continue
|
|
1022
|
+
local_types.add_allocation(
|
|
1023
|
+
name, inferred, node.start_byte, scope_end,
|
|
1024
|
+
)
|
|
1025
|
+
elif node.type == "resource":
|
|
1026
|
+
type_node = node.child_by_field_name("type")
|
|
1027
|
+
name_node = node.child_by_field_name("name")
|
|
1028
|
+
value_node = node.child_by_field_name("value")
|
|
1029
|
+
if (
|
|
1030
|
+
type_node is not None
|
|
1031
|
+
and self._text(type_node, source) == "var"
|
|
1032
|
+
and name_node is not None
|
|
1033
|
+
and value_node is not None
|
|
1034
|
+
and value_node.type == "method_invocation"
|
|
1035
|
+
):
|
|
1036
|
+
# `try (var ctx = factory.open())`: same bounded source
|
|
1037
|
+
# return propagation as a `var` local initializer.
|
|
1038
|
+
inferred = self._generic_method_invocation_return_type(
|
|
1039
|
+
value_node, source, module_name, enclosing_class_id,
|
|
1040
|
+
import_map, wildcard_packages, local_types, field_types,
|
|
1041
|
+
param_types, type_param_bounds,
|
|
1042
|
+
)
|
|
1043
|
+
if inferred is not None:
|
|
1044
|
+
local_types.add(
|
|
1045
|
+
self._text(name_node, source), inferred, node.start_byte,
|
|
1046
|
+
self._enclosing_scope_end(
|
|
1047
|
+
node, ("try_with_resources_statement",), node.end_byte,
|
|
1048
|
+
),
|
|
1049
|
+
)
|
|
1050
|
+
elif node.type == "enhanced_for_statement":
|
|
1051
|
+
type_node = node.child_by_field_name("type")
|
|
1052
|
+
name_node = node.child_by_field_name("name")
|
|
1053
|
+
value_node = node.child_by_field_name("value")
|
|
1054
|
+
body_node = node.child_by_field_name("body")
|
|
1055
|
+
if (
|
|
1056
|
+
type_node is not None
|
|
1057
|
+
and self._text(type_node, source) == "var"
|
|
1058
|
+
and name_node is not None
|
|
1059
|
+
and value_node is not None
|
|
1060
|
+
and body_node is not None
|
|
1061
|
+
):
|
|
1062
|
+
iterable_type = self._declared_receiver_type_text(
|
|
1063
|
+
value_node, source, module_name, enclosing_class_id,
|
|
1064
|
+
import_map, wildcard_packages, local_types, field_types,
|
|
1065
|
+
param_types, type_param_bounds,
|
|
1066
|
+
)
|
|
1067
|
+
element_type: Optional[str] = None
|
|
1068
|
+
if iterable_type is not None:
|
|
1069
|
+
erased_iterable = erased_type(iterable_type).strip()
|
|
1070
|
+
if erased_iterable.endswith("[]"):
|
|
1071
|
+
element_type = erased_iterable[:-2].strip() or None
|
|
1072
|
+
else:
|
|
1073
|
+
generic_args = self._generic_type_arguments(iterable_type)
|
|
1074
|
+
if len(generic_args) == 1:
|
|
1075
|
+
element_type = generic_args[0]
|
|
1076
|
+
if element_type is not None:
|
|
1077
|
+
local_types.add(
|
|
1078
|
+
self._text(name_node, source), element_type,
|
|
1079
|
+
body_node.start_byte, node.end_byte,
|
|
1080
|
+
)
|
|
1081
|
+
|
|
1082
|
+
@staticmethod
|
|
1083
|
+
def _param_names(func_cst, source: bytes) -> List[str]:
|
|
1084
|
+
"""Ordered parameter names of a method/constructor CST node —
|
|
1085
|
+
zipped against `_param_type_lists` (built from `Symbol.param_types`,
|
|
1086
|
+
which preserves declaration order) to get a `{name: type}` map for
|
|
1087
|
+
the *current* method's own parameters."""
|
|
1088
|
+
params_node = func_cst.child_by_field_name("parameters")
|
|
1089
|
+
if params_node is None:
|
|
1090
|
+
return []
|
|
1091
|
+
names: List[str] = []
|
|
1092
|
+
for child in params_node.children:
|
|
1093
|
+
if child.type == "formal_parameter":
|
|
1094
|
+
name_node = child.child_by_field_name("name")
|
|
1095
|
+
if name_node is not None:
|
|
1096
|
+
names.append(
|
|
1097
|
+
source[name_node.start_byte:name_node.end_byte].decode(
|
|
1098
|
+
"utf-8", errors="replace"
|
|
1099
|
+
)
|
|
1100
|
+
)
|
|
1101
|
+
elif child.type == "spread_parameter":
|
|
1102
|
+
for sub in child.children:
|
|
1103
|
+
if sub.type == "variable_declarator":
|
|
1104
|
+
name_node = sub.child_by_field_name("name")
|
|
1105
|
+
if name_node is not None:
|
|
1106
|
+
names.append(
|
|
1107
|
+
source[name_node.start_byte:name_node.end_byte].decode(
|
|
1108
|
+
"utf-8", errors="replace"
|
|
1109
|
+
)
|
|
1110
|
+
)
|
|
1111
|
+
return names
|