adl-toolkit 0.0.0-stage → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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/adl-drift.md +25 -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/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,1318 @@
|
|
|
1
|
+
"""SAM target typing, lambdas, method references, and callable containers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from collections import defaultdict
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Dict, List, Optional, Set, Tuple, Union
|
|
9
|
+
|
|
10
|
+
from adl_drift.graph.edges import EdgeType, GraphEdge
|
|
11
|
+
from adl_drift.graph.node import NodeType
|
|
12
|
+
from adl_drift.lang.base import ParseResult
|
|
13
|
+
from adl_drift.lang.java.ids import erased_type
|
|
14
|
+
from adl_drift.lang.java.resolution.constants import (
|
|
15
|
+
ASSERTJ_CONSUMER_SATISFIES_METHODS,
|
|
16
|
+
ASSERTJ_PREDICATE_METHODS,
|
|
17
|
+
EXTERNAL_SAM_PARAMS,
|
|
18
|
+
EXTERNAL_SAM_RETURNS,
|
|
19
|
+
SPRING_CONTEXT_RUNNER_ASSERTABLE_CONTEXT,
|
|
20
|
+
_ENCLOSING_TYPE_CST_TYPES,
|
|
21
|
+
_JDK_SAM_PARAMS,
|
|
22
|
+
_JDK_SAM_RETURNS,
|
|
23
|
+
_METHOD_CST_TYPES,
|
|
24
|
+
_NESTED_SCOPE_BOUNDARY,
|
|
25
|
+
argument_nodes,
|
|
26
|
+
)
|
|
27
|
+
from adl_drift.lang.java.resolution.model import (
|
|
28
|
+
ImportContext as _ImportContext,
|
|
29
|
+
LocalScopes as _LocalScopes,
|
|
30
|
+
SAMDescriptor as _SAMDescriptor,
|
|
31
|
+
)
|
|
32
|
+
from adl_drift.utils.types import AnalysisResult
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
# Arity of the single functional-interface argument of common JDK Stream and
|
|
36
|
+
# Optional methods, used only as a method-reference fallback when the receiver's
|
|
37
|
+
# generic element type is unavailable (external declaration).
|
|
38
|
+
_JDK_FUNCTIONAL_ARGUMENT_ARITY: Dict[str, int] = {
|
|
39
|
+
"allMatch": 1, "anyMatch": 1, "filter": 1, "flatMap": 1, "forEach": 1,
|
|
40
|
+
"forEachOrdered": 1, "map": 1, "mapMulti": 1, "noneMatch": 1, "peek": 1,
|
|
41
|
+
"orElseGet": 0, "orElseThrow": 0, "or": 0,
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
# Per-argument-index arity of `java.util.stream.Collectors` map-collecting
|
|
45
|
+
# factories' functional parameters: keyMapper/valueMapper are one-arg
|
|
46
|
+
# `Function`s, the optional mergeFunction is a two-arg `BinaryOperator`, and
|
|
47
|
+
# the optional mapSupplier is a zero-arg `Supplier`. Used only as a
|
|
48
|
+
# method/constructor-reference fallback when the collected stream's element
|
|
49
|
+
# type is unavailable to compute a full SAM descriptor by type.
|
|
50
|
+
_JDK_INDEXED_FUNCTIONAL_ARITY: Dict[str, List[int]] = {
|
|
51
|
+
"toMap": [1, 1, 2, 0],
|
|
52
|
+
"toConcurrentMap": [1, 1, 2, 0],
|
|
53
|
+
"toUnmodifiableMap": [1, 1, 2],
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class FunctionalResolutionMixin:
|
|
58
|
+
@classmethod
|
|
59
|
+
def _functional_value_type(cls, type_text: str) -> str:
|
|
60
|
+
"""Peel common callable containers (`Map<K, Runnable>`, arrays)."""
|
|
61
|
+
text = type_text.strip()
|
|
62
|
+
while text.endswith("[]"):
|
|
63
|
+
text = text[:-2].strip()
|
|
64
|
+
for _ in range(4):
|
|
65
|
+
raw = erased_type(text).rsplit(".", 1)[-1]
|
|
66
|
+
if raw not in {"Map", "HashMap", "ConcurrentMap", "List", "Set", "Collection", "Optional"}:
|
|
67
|
+
break
|
|
68
|
+
args = cls._generic_type_arguments(text)
|
|
69
|
+
if not args:
|
|
70
|
+
break
|
|
71
|
+
text = args[-1]
|
|
72
|
+
return text
|
|
73
|
+
|
|
74
|
+
def _functional_target_type(
|
|
75
|
+
self, expression, source: bytes, local_types: "_LocalScopes",
|
|
76
|
+
param_types: Dict[str, str], field_types: Dict[str, str], caller_node,
|
|
77
|
+
) -> Optional[str]:
|
|
78
|
+
"""Target type supplied by assignment/cast/return/container context."""
|
|
79
|
+
cur = expression
|
|
80
|
+
while cur.parent is not None and cur.parent.type not in _METHOD_CST_TYPES:
|
|
81
|
+
parent = cur.parent
|
|
82
|
+
# A callable expression nested in another lambda must not inherit
|
|
83
|
+
# the outer lambda's functional type (e.g. an inner
|
|
84
|
+
# `Predicate<Event>` being mistaken for `Consumer<Scenario>`).
|
|
85
|
+
# Its own target comes from its consuming invocation.
|
|
86
|
+
if parent.type == "lambda_expression" and parent is not expression:
|
|
87
|
+
break
|
|
88
|
+
if parent.type == "cast_expression":
|
|
89
|
+
type_node = parent.child_by_field_name("type")
|
|
90
|
+
if type_node is not None:
|
|
91
|
+
return self._functional_value_type(self._text(type_node, source))
|
|
92
|
+
if parent.type == "variable_declarator":
|
|
93
|
+
declaration = parent.parent
|
|
94
|
+
type_node = declaration.child_by_field_name("type") if declaration else None
|
|
95
|
+
if type_node is not None:
|
|
96
|
+
return self._functional_value_type(self._text(type_node, source))
|
|
97
|
+
if parent.type == "assignment_expression":
|
|
98
|
+
left = parent.child_by_field_name("left")
|
|
99
|
+
if left is not None and left.type == "identifier":
|
|
100
|
+
name = self._text(left, source)
|
|
101
|
+
declared = (
|
|
102
|
+
local_types.type_at(name, expression.start_byte)
|
|
103
|
+
or param_types.get(name) or field_types.get(name)
|
|
104
|
+
)
|
|
105
|
+
if declared:
|
|
106
|
+
return self._functional_value_type(declared)
|
|
107
|
+
if parent.type == "return_statement":
|
|
108
|
+
returned = caller_node.metadata.get("java_return_type")
|
|
109
|
+
if isinstance(returned, str):
|
|
110
|
+
return self._functional_value_type(returned)
|
|
111
|
+
if parent.type == "argument_list" and parent.parent is not None:
|
|
112
|
+
invocation = parent.parent
|
|
113
|
+
if invocation.type == "method_invocation":
|
|
114
|
+
name_node = invocation.child_by_field_name("name")
|
|
115
|
+
obj = invocation.child_by_field_name("object")
|
|
116
|
+
if name_node is not None and self._text(name_node, source) in {
|
|
117
|
+
"put", "add", "offer",
|
|
118
|
+
} and obj is not None and obj.type == "identifier":
|
|
119
|
+
name = self._text(obj, source)
|
|
120
|
+
declared = (
|
|
121
|
+
local_types.type_at(name, expression.start_byte)
|
|
122
|
+
or param_types.get(name) or field_types.get(name)
|
|
123
|
+
)
|
|
124
|
+
if declared:
|
|
125
|
+
return self._functional_value_type(declared)
|
|
126
|
+
# The callable is consumed by this invocation. An outer
|
|
127
|
+
# assignment/return types the invocation's result, not
|
|
128
|
+
# the nested lambda/method reference itself; its SAM type
|
|
129
|
+
# is resolved separately by
|
|
130
|
+
# `_invocation_functional_target_type`.
|
|
131
|
+
break
|
|
132
|
+
cur = parent
|
|
133
|
+
return None
|
|
134
|
+
|
|
135
|
+
@staticmethod
|
|
136
|
+
def _normalize_sam_type(type_text: str) -> str:
|
|
137
|
+
text = type_text.strip()
|
|
138
|
+
match = re.match(r"^\?\s+(?:extends|super)\s+(.+)$", text)
|
|
139
|
+
return erased_type(match.group(1) if match else ("Object" if text == "?" else text))
|
|
140
|
+
|
|
141
|
+
def _functional_descriptor(
|
|
142
|
+
self, type_text: Optional[str], module_name: str,
|
|
143
|
+
import_map: Dict[str, str], wildcard_packages: List[str],
|
|
144
|
+
) -> Optional[_SAMDescriptor]:
|
|
145
|
+
"""SAM parameter types for JDK or source-declared functional interfaces."""
|
|
146
|
+
if not type_text:
|
|
147
|
+
return None
|
|
148
|
+
raw = erased_type(type_text).strip()
|
|
149
|
+
while raw.endswith("[]"):
|
|
150
|
+
# `erased_type()` normalizes a vararg SAM parameter's `...` to
|
|
151
|
+
# `[]`; strip it back to the bare interface name so both the
|
|
152
|
+
# JDK-SAM-table lookup and the project-interface fallback below
|
|
153
|
+
# (keyed on `simple`, unlike `_resolve_declared_type`, which
|
|
154
|
+
# already strips this itself) still match `Executable` for
|
|
155
|
+
# `Executable...` — not the never-declared name `Executable[]`.
|
|
156
|
+
raw = raw[:-2]
|
|
157
|
+
simple = raw.rsplit(".", 1)[-1]
|
|
158
|
+
args = self._generic_type_arguments(type_text)
|
|
159
|
+
spec = _JDK_SAM_PARAMS.get(simple)
|
|
160
|
+
if spec is None:
|
|
161
|
+
# A small, separately-maintained table of well-known *external*
|
|
162
|
+
# (non-JDK) SAMs — see `EXTERNAL_SAM_PARAMS`'s own docstring for
|
|
163
|
+
# why this stays out of `_JDK_SAM_PARAMS` itself.
|
|
164
|
+
spec = EXTERNAL_SAM_PARAMS.get(simple)
|
|
165
|
+
if spec is not None:
|
|
166
|
+
params: List[str] = []
|
|
167
|
+
for item in spec:
|
|
168
|
+
if isinstance(item, str):
|
|
169
|
+
params.append(item)
|
|
170
|
+
elif item < len(args):
|
|
171
|
+
params.append(self._normalize_sam_type(args[item]))
|
|
172
|
+
else:
|
|
173
|
+
return None
|
|
174
|
+
return_spec = _JDK_SAM_RETURNS.get(simple)
|
|
175
|
+
if return_spec is None:
|
|
176
|
+
return_spec = EXTERNAL_SAM_RETURNS.get(simple)
|
|
177
|
+
if isinstance(return_spec, int):
|
|
178
|
+
if return_spec >= len(args):
|
|
179
|
+
return None
|
|
180
|
+
return_type = self._normalize_sam_type(args[return_spec])
|
|
181
|
+
else:
|
|
182
|
+
return_type = return_spec
|
|
183
|
+
return _SAMDescriptor(params, return_type)
|
|
184
|
+
|
|
185
|
+
interface_id = self._resolve_declared_type(
|
|
186
|
+
type_text, module_name, import_map, wildcard_packages,
|
|
187
|
+
)
|
|
188
|
+
if interface_id is None:
|
|
189
|
+
# Candidate parameter types are written in the declaring file's
|
|
190
|
+
# import context, while this helper is called with the caller's
|
|
191
|
+
# context. Recover a nested/project SAM only when its simple name
|
|
192
|
+
# is globally unique; ambiguous names remain unresolved.
|
|
193
|
+
matches = [
|
|
194
|
+
node.id for node in self.builder._nodes.values()
|
|
195
|
+
if node.node_type == NodeType.CLASS
|
|
196
|
+
and node.name == simple
|
|
197
|
+
and node.metadata.get("java_type_kind") == "interface"
|
|
198
|
+
]
|
|
199
|
+
if len(matches) == 1:
|
|
200
|
+
interface_id = matches[0]
|
|
201
|
+
interface = self.builder._nodes.get(interface_id) if interface_id else None
|
|
202
|
+
if interface is None or interface.metadata.get("java_type_kind") != "interface":
|
|
203
|
+
return None
|
|
204
|
+
|
|
205
|
+
abstract: List[str] = []
|
|
206
|
+
seen: Set[Tuple[str, Tuple[str, ...]]] = set()
|
|
207
|
+
for owner in [interface_id, *self._class_ancestors(interface_id)]:
|
|
208
|
+
for member_id in self._contains_children(owner):
|
|
209
|
+
member = self.builder._nodes.get(member_id)
|
|
210
|
+
if member is None or member.node_type != NodeType.FUNCTION:
|
|
211
|
+
continue
|
|
212
|
+
signature = self._erased_signature(member_id)
|
|
213
|
+
key = (member.name, signature)
|
|
214
|
+
if key in seen:
|
|
215
|
+
continue
|
|
216
|
+
seen.add(key)
|
|
217
|
+
if self._is_static_member(member_id) or not member.metadata.get("java_is_abstract"):
|
|
218
|
+
continue
|
|
219
|
+
if key in {("equals", ("Object",)), ("hashCode", ()), ("toString", ())}:
|
|
220
|
+
continue
|
|
221
|
+
abstract.append(member_id)
|
|
222
|
+
if len(abstract) != 1:
|
|
223
|
+
if abstract:
|
|
224
|
+
return None
|
|
225
|
+
# A project interface that only extends a JDK functional
|
|
226
|
+
# interface inherits that interface's single abstract method;
|
|
227
|
+
# describe it through the extends clause's type arguments.
|
|
228
|
+
external = self._external_functional_super(interface_id)
|
|
229
|
+
if external is None:
|
|
230
|
+
return None
|
|
231
|
+
owner_id, super_text = external
|
|
232
|
+
owner_node = self.builder._nodes.get(owner_id)
|
|
233
|
+
owner_ctx = (
|
|
234
|
+
self._file_ctx.get(owner_node.file_path)
|
|
235
|
+
if owner_node is not None else None
|
|
236
|
+
)
|
|
237
|
+
if owner_ctx is None:
|
|
238
|
+
return None
|
|
239
|
+
bindings = self._compose_type_var_bindings(interface_id, args, owner_id)
|
|
240
|
+
return self._functional_descriptor(
|
|
241
|
+
self._replace_type_variables(super_text, bindings),
|
|
242
|
+
owner_ctx.module, owner_ctx.import_map, owner_ctx.wildcard_packages,
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
sam_id = abstract[0]
|
|
246
|
+
declaring = self._declaring_class_id(sam_id) or interface_id
|
|
247
|
+
bindings = self._compose_type_var_bindings(interface_id, args, declaring)
|
|
248
|
+
params = [
|
|
249
|
+
self._normalize_sam_type(self._replace_type_variables(t, bindings))
|
|
250
|
+
for t in self._param_type_lists.get(sam_id, [])
|
|
251
|
+
]
|
|
252
|
+
return_type = self.builder._nodes[sam_id].metadata.get("java_return_type")
|
|
253
|
+
if isinstance(return_type, str):
|
|
254
|
+
return_type = self._normalize_sam_type(
|
|
255
|
+
self._replace_type_variables(return_type, bindings)
|
|
256
|
+
)
|
|
257
|
+
else:
|
|
258
|
+
return_type = None
|
|
259
|
+
return _SAMDescriptor(params, return_type)
|
|
260
|
+
|
|
261
|
+
def _method_reference_return_compatible(
|
|
262
|
+
self, candidate_id: str, expected: Optional[str], expected_ctx: "_ImportContext",
|
|
263
|
+
) -> bool:
|
|
264
|
+
if expected is None or expected == "void":
|
|
265
|
+
return True
|
|
266
|
+
# A consuming generic method can leave its result variable open at
|
|
267
|
+
# this point (`<S> matchingMapped(Function<E,S> extractor, ...)`).
|
|
268
|
+
# The method reference itself infers `S`; a single conventional Java
|
|
269
|
+
# type-variable token is therefore a constraint variable, not a
|
|
270
|
+
# project class that the candidate return must already equal.
|
|
271
|
+
if re.fullmatch(r"[A-Z]", expected):
|
|
272
|
+
return True
|
|
273
|
+
candidate = self.builder._nodes.get(candidate_id)
|
|
274
|
+
if candidate is None:
|
|
275
|
+
return False
|
|
276
|
+
actual = candidate.metadata.get("java_return_type")
|
|
277
|
+
if not isinstance(actual, str) or actual == "void":
|
|
278
|
+
return False
|
|
279
|
+
return self._types_compatible(
|
|
280
|
+
expected, erased_type(actual), expected_ctx,
|
|
281
|
+
self._param_ctx_for(candidate_id), allow_boxing=True,
|
|
282
|
+
)
|
|
283
|
+
|
|
284
|
+
def _invocation_functional_target_type(
|
|
285
|
+
self, expression, source: bytes, module_name: str,
|
|
286
|
+
import_map: Dict[str, str], wildcard_packages: List[str],
|
|
287
|
+
local_types: "_LocalScopes", param_types: Dict[str, str],
|
|
288
|
+
field_types: Dict[str, str], caller_node,
|
|
289
|
+
) -> Optional[str]:
|
|
290
|
+
"""Expected parameter type when callable is passed to one resolvable call."""
|
|
291
|
+
cur = expression
|
|
292
|
+
while cur.parent is not None and cur.parent.type not in _METHOD_CST_TYPES:
|
|
293
|
+
parent = cur.parent
|
|
294
|
+
if parent.type == "argument_list" and parent.parent is not None:
|
|
295
|
+
args = argument_nodes(parent)
|
|
296
|
+
try:
|
|
297
|
+
index = args.index(cur)
|
|
298
|
+
except ValueError:
|
|
299
|
+
return None
|
|
300
|
+
invocation = parent.parent
|
|
301
|
+
candidates: List[str] = []
|
|
302
|
+
enclosing = self._declaring_class_id(caller_node.id)
|
|
303
|
+
ctx = _ImportContext(
|
|
304
|
+
module_name, import_map, wildcard_packages, enclosing,
|
|
305
|
+
)
|
|
306
|
+
receiver_class_id: Optional[str] = None
|
|
307
|
+
receiver_type: Optional[str] = None
|
|
308
|
+
receiver_args: List[str] = []
|
|
309
|
+
if invocation.type == "method_invocation":
|
|
310
|
+
target, _, _ = self._resolve_call(
|
|
311
|
+
invocation, source, module_name, enclosing, import_map,
|
|
312
|
+
wildcard_packages, local_types, param_types,
|
|
313
|
+
self._static_wildcard_by_file.get(caller_node.file_path, []), {},
|
|
314
|
+
)
|
|
315
|
+
candidates = self._target_ids(target)
|
|
316
|
+
receiver = invocation.child_by_field_name("object")
|
|
317
|
+
if receiver is not None:
|
|
318
|
+
receiver_type = self._declared_receiver_type_text(
|
|
319
|
+
receiver, source, module_name, enclosing, import_map,
|
|
320
|
+
wildcard_packages, local_types, field_types,
|
|
321
|
+
param_types, {},
|
|
322
|
+
)
|
|
323
|
+
if receiver_type is not None:
|
|
324
|
+
receiver_class_id = self._resolve_declared_type(
|
|
325
|
+
receiver_type, module_name, import_map,
|
|
326
|
+
wildcard_packages, enclosing,
|
|
327
|
+
)
|
|
328
|
+
receiver_args = self._generic_type_arguments(receiver_type)
|
|
329
|
+
# `Optional<T>.ifPresent(Consumer<? super T>)`/
|
|
330
|
+
# `.ifPresentOrElse` have no source declaration
|
|
331
|
+
# for `_resolve_call` to find (`candidates` stays
|
|
332
|
+
# empty below), so the callable argument would
|
|
333
|
+
# otherwise fall back to a bare per-JDK-method
|
|
334
|
+
# arity placeholder -- indistinguishable between
|
|
335
|
+
# two same-arity overloads of a method reference
|
|
336
|
+
# target (`selectors(List)` vs `selectors
|
|
337
|
+
# (DiscoverySelector...)`, both arity-1). The
|
|
338
|
+
# receiver's own generic argument (already known
|
|
339
|
+
# here, e.g. from a preceding `.map(...)` --
|
|
340
|
+
# including its method-reference-argument case
|
|
341
|
+
# just above) is the exact `T` the JDK contract
|
|
342
|
+
# promises, independent of any candidate lookup.
|
|
343
|
+
if (
|
|
344
|
+
erased_type(receiver_type).rsplit(".", 1)[-1] == "Optional"
|
|
345
|
+
and len(receiver_args) == 1
|
|
346
|
+
and index == 0
|
|
347
|
+
):
|
|
348
|
+
name_node = invocation.child_by_field_name("name")
|
|
349
|
+
jdk_method = (
|
|
350
|
+
self._text(name_node, source)
|
|
351
|
+
if name_node is not None else None
|
|
352
|
+
)
|
|
353
|
+
if jdk_method in {"ifPresent", "ifPresentOrElse"}:
|
|
354
|
+
return (
|
|
355
|
+
"java.util.function.Consumer<"
|
|
356
|
+
f"{receiver_args[0]}>"
|
|
357
|
+
)
|
|
358
|
+
# Spring Boot Test's runner subclasses hard-code
|
|
359
|
+
# their own assertable-context type; see
|
|
360
|
+
# `SPRING_CONTEXT_RUNNER_ASSERTABLE_CONTEXT`'s
|
|
361
|
+
# docstring. Like `Optional.ifPresent` above,
|
|
362
|
+
# `run` has no source declaration to find via
|
|
363
|
+
# `_resolve_call` (`candidates` stays empty), so
|
|
364
|
+
# without this the lambda's own parameter type
|
|
365
|
+
# (e.g. `ctxt` in `.run(ctxt -> ...)`) is unknown
|
|
366
|
+
# and everything inside its body is unresolvable.
|
|
367
|
+
assertable_context = SPRING_CONTEXT_RUNNER_ASSERTABLE_CONTEXT.get(
|
|
368
|
+
erased_type(receiver_type).rsplit(".", 1)[-1],
|
|
369
|
+
)
|
|
370
|
+
if assertable_context is not None and index == 0:
|
|
371
|
+
name_node = invocation.child_by_field_name("name")
|
|
372
|
+
if (
|
|
373
|
+
name_node is not None
|
|
374
|
+
and self._text(name_node, source) == "run"
|
|
375
|
+
):
|
|
376
|
+
return (
|
|
377
|
+
"org.springframework.boot.test.context.runner."
|
|
378
|
+
f"ContextConsumer<{assertable_context}>"
|
|
379
|
+
)
|
|
380
|
+
elif invocation.type == "object_creation_expression":
|
|
381
|
+
type_node = invocation.child_by_field_name("type")
|
|
382
|
+
class_id = self._resolve_declared_type(
|
|
383
|
+
self._text(type_node, source) if type_node is not None else "",
|
|
384
|
+
module_name, import_map, wildcard_packages,
|
|
385
|
+
)
|
|
386
|
+
if class_id:
|
|
387
|
+
cls = self.builder._nodes[class_id]
|
|
388
|
+
ctors = self._members_named(
|
|
389
|
+
class_id, cls.name, search_ancestors=False,
|
|
390
|
+
)
|
|
391
|
+
target, _ = self._select_overload(
|
|
392
|
+
ctors, len(args), None, lambda _c: 0.9, ctx,
|
|
393
|
+
)
|
|
394
|
+
candidates = self._target_ids(target)
|
|
395
|
+
elif invocation.type == "explicit_constructor_invocation":
|
|
396
|
+
# `this(...)`/`super(...)` construct the current class or
|
|
397
|
+
# its direct superclass; mirrors
|
|
398
|
+
# `_resolve_explicit_constructor`'s target-class lookup so
|
|
399
|
+
# a callable argument here (e.g. `this(a, b, Foo::new)`)
|
|
400
|
+
# is target-typed against the delegate constructor's
|
|
401
|
+
# declared parameter, same as any other constructor call.
|
|
402
|
+
keyword_node = next(
|
|
403
|
+
(c for c in invocation.named_children), None,
|
|
404
|
+
)
|
|
405
|
+
keyword = (
|
|
406
|
+
self._text(keyword_node, source) if keyword_node is not None else None
|
|
407
|
+
)
|
|
408
|
+
delegate_class_id: Optional[str] = None
|
|
409
|
+
if keyword == "this":
|
|
410
|
+
delegate_class_id = enclosing
|
|
411
|
+
elif keyword == "super" and enclosing is not None:
|
|
412
|
+
parents = self._declared_direct_ancestors.get(
|
|
413
|
+
(enclosing, EdgeType.INHERITS), [],
|
|
414
|
+
)
|
|
415
|
+
delegate_class_id = parents[0] if parents else None
|
|
416
|
+
if delegate_class_id:
|
|
417
|
+
cls = self.builder._nodes.get(delegate_class_id)
|
|
418
|
+
if cls is not None:
|
|
419
|
+
ctors = self._members_named(
|
|
420
|
+
delegate_class_id, cls.name, search_ancestors=False,
|
|
421
|
+
)
|
|
422
|
+
target, _ = self._select_overload(
|
|
423
|
+
ctors, len(args), None, lambda _c: 0.9, ctx,
|
|
424
|
+
)
|
|
425
|
+
candidates = self._target_ids(target)
|
|
426
|
+
expected: Set[str] = set()
|
|
427
|
+
for candidate in candidates:
|
|
428
|
+
params = self._param_type_lists.get(candidate)
|
|
429
|
+
if not params or not (
|
|
430
|
+
index < len(params) or self._is_vararg_param(params[-1])
|
|
431
|
+
):
|
|
432
|
+
continue
|
|
433
|
+
parameter = params[min(index, len(params) - 1)]
|
|
434
|
+
# A fluent receiver such as `Result<Event>` binds the
|
|
435
|
+
# declaring class's `E` before its `Predicate<E>` or
|
|
436
|
+
# `Function<E,S>` parameter can target-type a lambda or
|
|
437
|
+
# method reference. Candidate parameter texts are stored
|
|
438
|
+
# in declaration form, so substitute the receiver's
|
|
439
|
+
# exact generic arguments here, at the consuming call.
|
|
440
|
+
declaring = self._declaring_class_id(candidate)
|
|
441
|
+
if receiver_class_id is not None and declaring is not None:
|
|
442
|
+
bindings = self._compose_type_var_bindings(
|
|
443
|
+
receiver_class_id, receiver_args, declaring,
|
|
444
|
+
)
|
|
445
|
+
parameter = self._replace_type_variables(parameter, bindings)
|
|
446
|
+
# A sibling class-literal argument can bind a generic
|
|
447
|
+
# method variable used by this functional parameter:
|
|
448
|
+
# `<T,S> filter(..., Class<T>, Function<T,S>)` with
|
|
449
|
+
# `SourceChange.class` target-types the mapper as
|
|
450
|
+
# `Function<SourceChange,S>`. Keep unbound result
|
|
451
|
+
# variables such as S open for method-reference return
|
|
452
|
+
# inference.
|
|
453
|
+
method_bindings: Dict[str, str] = {}
|
|
454
|
+
method_type_params = self._method_type_param_bounds.get(candidate, {})
|
|
455
|
+
for arg_index, arg_node in enumerate(args):
|
|
456
|
+
if arg_node.type != "class_literal" or arg_index >= len(params):
|
|
457
|
+
continue
|
|
458
|
+
declared_args = self._generic_type_arguments(params[arg_index])
|
|
459
|
+
variable = declared_args[0] if len(declared_args) == 1 else ""
|
|
460
|
+
if variable not in method_type_params:
|
|
461
|
+
continue
|
|
462
|
+
literal = next((child for child in arg_node.children if child.is_named), None)
|
|
463
|
+
if literal is not None:
|
|
464
|
+
method_bindings[variable] = self._text(literal, source)
|
|
465
|
+
# A sibling unbound source method reference can bind a
|
|
466
|
+
# generic result variable consumed by this callable. For
|
|
467
|
+
# `where(Descriptor::getUniqueId, uid -> uid.toString())`,
|
|
468
|
+
# the first argument proves V is UniqueId, so the second
|
|
469
|
+
# argument is exactly Predicate<UniqueId>. Keep this to a
|
|
470
|
+
# unique zero-argument source declaration; overloaded,
|
|
471
|
+
# bound, static, and external references retain their
|
|
472
|
+
# existing unresolved behavior.
|
|
473
|
+
for arg_index, arg_node in enumerate(args):
|
|
474
|
+
if arg_node.type != "method_reference" or arg_index >= len(params):
|
|
475
|
+
continue
|
|
476
|
+
declared_args = self._generic_type_arguments(params[arg_index])
|
|
477
|
+
variable = None
|
|
478
|
+
if declared_args:
|
|
479
|
+
variable = self._wildcard_upper_bound(declared_args[-1])
|
|
480
|
+
if variable is None:
|
|
481
|
+
lower = re.match(
|
|
482
|
+
r"^\?\s+super\s+(.+)$", declared_args[-1].strip(),
|
|
483
|
+
)
|
|
484
|
+
variable = lower.group(1).strip() if lower else declared_args[-1]
|
|
485
|
+
if not variable or variable not in method_type_params:
|
|
486
|
+
continue
|
|
487
|
+
named = [child for child in arg_node.children if child.is_named]
|
|
488
|
+
terminal = next(
|
|
489
|
+
(child for child in reversed(arg_node.children)
|
|
490
|
+
if child.type in {"identifier", "new"}),
|
|
491
|
+
None,
|
|
492
|
+
)
|
|
493
|
+
if not named or terminal is None or terminal.type == "new":
|
|
494
|
+
continue
|
|
495
|
+
owner_id = self._resolve_declared_type(
|
|
496
|
+
self._text(named[0], source), module_name, import_map,
|
|
497
|
+
wildcard_packages, enclosing,
|
|
498
|
+
)
|
|
499
|
+
if owner_id is None:
|
|
500
|
+
continue
|
|
501
|
+
referenced = [
|
|
502
|
+
candidate for candidate in self._members_named(
|
|
503
|
+
owner_id, self._text(terminal, source),
|
|
504
|
+
search_ancestors=True,
|
|
505
|
+
)
|
|
506
|
+
if (
|
|
507
|
+
not self._is_static_member(candidate)
|
|
508
|
+
and len(self._param_type_lists.get(candidate, [])) == 0
|
|
509
|
+
and isinstance(
|
|
510
|
+
self.builder._nodes[candidate].metadata.get("java_return_type"),
|
|
511
|
+
str,
|
|
512
|
+
)
|
|
513
|
+
and self.builder._nodes[candidate].metadata.get("java_return_type") != "void"
|
|
514
|
+
)
|
|
515
|
+
]
|
|
516
|
+
if len(referenced) == 1:
|
|
517
|
+
method_bindings[variable] = self.builder._nodes[
|
|
518
|
+
referenced[0]
|
|
519
|
+
].metadata["java_return_type"]
|
|
520
|
+
if method_bindings:
|
|
521
|
+
parameter = self._replace_type_variables(parameter, method_bindings)
|
|
522
|
+
# The parameter is spelled in the candidate's own file.
|
|
523
|
+
# Qualify a source-declared SAM there, so the caller
|
|
524
|
+
# neither needs its import nor falls back to a global
|
|
525
|
+
# simple-name guess (`Executable` is declared by several
|
|
526
|
+
# unrelated project interfaces).
|
|
527
|
+
raw_head = erased_type(parameter).strip()
|
|
528
|
+
while raw_head.endswith("[]"):
|
|
529
|
+
# `erased_type()` normalizes a vararg SAM parameter's
|
|
530
|
+
# `...` to `[]` — strip it back to the bare interface
|
|
531
|
+
# name so a vararg functional-interface parameter
|
|
532
|
+
# (`Executable...`) still gets qualified in its
|
|
533
|
+
# declaring file's own import context instead of
|
|
534
|
+
# skipping straight to the global simple-name guess,
|
|
535
|
+
# which is genuinely ambiguous when — as with
|
|
536
|
+
# `Executable` in the real JUnit5 corpus — several
|
|
537
|
+
# unrelated project interfaces share that name.
|
|
538
|
+
raw_head = raw_head[:-2]
|
|
539
|
+
if raw_head and "." not in raw_head:
|
|
540
|
+
param_ctx = self._param_ctx_for(candidate)
|
|
541
|
+
param_id = self._resolve_declared_type(
|
|
542
|
+
raw_head, param_ctx.module, param_ctx.import_map,
|
|
543
|
+
param_ctx.wildcard_packages, declaring,
|
|
544
|
+
)
|
|
545
|
+
if param_id is not None and ":" in param_id:
|
|
546
|
+
param_module, param_qualname = param_id.rsplit(":", 1)
|
|
547
|
+
generic_args = self._generic_type_arguments(parameter)
|
|
548
|
+
suffix = (
|
|
549
|
+
"<" + ", ".join(generic_args) + ">"
|
|
550
|
+
if generic_args else ""
|
|
551
|
+
)
|
|
552
|
+
parameter = (
|
|
553
|
+
f"{self.builder._display_module(param_module)}."
|
|
554
|
+
f"{param_qualname}{suffix}"
|
|
555
|
+
)
|
|
556
|
+
expected.add(parameter)
|
|
557
|
+
# JDK fluent methods do not have graph nodes, so `_resolve_call`
|
|
558
|
+
# cannot supply their declaration parameter. Their receiver
|
|
559
|
+
# still carries the exact element type, which is sufficient to
|
|
560
|
+
# target-type the first callable argument without inventing an
|
|
561
|
+
# external call edge.
|
|
562
|
+
if (
|
|
563
|
+
not expected
|
|
564
|
+
and invocation.type == "method_invocation"
|
|
565
|
+
and index == 0
|
|
566
|
+
and receiver_type is not None
|
|
567
|
+
and receiver_args
|
|
568
|
+
):
|
|
569
|
+
method_node = invocation.child_by_field_name("name")
|
|
570
|
+
method_name = self._text(method_node, source) if method_node is not None else ""
|
|
571
|
+
raw_receiver = erased_type(receiver_type).rsplit(".", 1)[-1]
|
|
572
|
+
element_type = receiver_args[0]
|
|
573
|
+
if raw_receiver == "Stream":
|
|
574
|
+
if method_name in {"map", "flatMap"}:
|
|
575
|
+
expected.add(f"Function<{element_type},Object>")
|
|
576
|
+
elif method_name in {"filter", "allMatch", "anyMatch", "noneMatch"}:
|
|
577
|
+
expected.add(f"Predicate<{element_type}>")
|
|
578
|
+
elif method_name in {"forEach", "forEachOrdered", "peek"}:
|
|
579
|
+
expected.add(f"Consumer<{element_type}>")
|
|
580
|
+
elif raw_receiver == "Optional":
|
|
581
|
+
if method_name in {"map", "flatMap"}:
|
|
582
|
+
expected.add(f"Function<{element_type},Object>")
|
|
583
|
+
elif method_name == "filter":
|
|
584
|
+
expected.add(f"Predicate<{element_type}>")
|
|
585
|
+
elif method_name == "ifPresent":
|
|
586
|
+
expected.add(f"Consumer<{element_type}>")
|
|
587
|
+
elif raw_receiver in {
|
|
588
|
+
"Iterable", "Collection", "List", "Set", "ArrayList",
|
|
589
|
+
"LinkedList", "HashSet", "SortedSet",
|
|
590
|
+
} and method_name == "forEach":
|
|
591
|
+
expected.add(f"Consumer<{element_type}>")
|
|
592
|
+
elif (
|
|
593
|
+
raw_receiver in {
|
|
594
|
+
"Map", "HashMap", "LinkedHashMap", "TreeMap",
|
|
595
|
+
"ConcurrentMap",
|
|
596
|
+
}
|
|
597
|
+
and method_name == "forEach"
|
|
598
|
+
and len(receiver_args) >= 2
|
|
599
|
+
):
|
|
600
|
+
expected.add(
|
|
601
|
+
f"BiConsumer<{receiver_args[0]},{receiver_args[1]}>"
|
|
602
|
+
)
|
|
603
|
+
elif raw_receiver in {
|
|
604
|
+
"ObjectAssert", "OptionalAssert", "ListAssert", "IterableAssert",
|
|
605
|
+
}:
|
|
606
|
+
# AssertJ assert wrappers derived by
|
|
607
|
+
# `_assertj_assertthat_return_type` from a preceding
|
|
608
|
+
# `assertThat(actual)` -- external declarations, so
|
|
609
|
+
# (like the JDK Stream/Optional/Map cases above) their
|
|
610
|
+
# fluent methods have no source parameter to read
|
|
611
|
+
# from `_resolve_call`. `hasValueSatisfying`/
|
|
612
|
+
# `satisfies`/`allSatisfy`/... take a
|
|
613
|
+
# `Consumer<? super element>`; `filteredOn`/
|
|
614
|
+
# `allMatch`/... take a `Predicate<? super element>`.
|
|
615
|
+
if method_name in ASSERTJ_CONSUMER_SATISFIES_METHODS:
|
|
616
|
+
expected.add(f"Consumer<{element_type}>")
|
|
617
|
+
elif method_name in ASSERTJ_PREDICATE_METHODS:
|
|
618
|
+
expected.add(f"Predicate<{element_type}>")
|
|
619
|
+
if (
|
|
620
|
+
not expected
|
|
621
|
+
and invocation.type == "method_invocation"
|
|
622
|
+
and index == 1
|
|
623
|
+
and receiver_type is not None
|
|
624
|
+
and len(receiver_args) >= 2
|
|
625
|
+
):
|
|
626
|
+
method_node = invocation.child_by_field_name("name")
|
|
627
|
+
method_name = self._text(method_node, source) if method_node is not None else ""
|
|
628
|
+
raw_receiver = erased_type(receiver_type).rsplit(".", 1)[-1]
|
|
629
|
+
if raw_receiver in {
|
|
630
|
+
"Map", "HashMap", "LinkedHashMap", "TreeMap",
|
|
631
|
+
"ConcurrentMap",
|
|
632
|
+
} and method_name == "computeIfAbsent":
|
|
633
|
+
expected.add(
|
|
634
|
+
f"Function<{receiver_args[0]},{receiver_args[1]}>"
|
|
635
|
+
)
|
|
636
|
+
elif raw_receiver == "MapAssert" and method_name == "hasEntrySatisfying":
|
|
637
|
+
# `MapAssert<K,V>.hasEntrySatisfying(K key, Consumer<?
|
|
638
|
+
# super V> valueRequirement)` -- the lambda is the
|
|
639
|
+
# value-side requirement, not a key/value pair.
|
|
640
|
+
expected.add(f"Consumer<{receiver_args[1]}>")
|
|
641
|
+
if (
|
|
642
|
+
not expected
|
|
643
|
+
and invocation.type == "method_invocation"
|
|
644
|
+
and index == 0
|
|
645
|
+
and receiver_class_id is not None
|
|
646
|
+
):
|
|
647
|
+
method_node = invocation.child_by_field_name("name")
|
|
648
|
+
method_name = self._text(method_node, source) if method_node is not None else ""
|
|
649
|
+
receiver_node = self.builder._nodes.get(receiver_class_id)
|
|
650
|
+
iterable_elements: Set[str] = set()
|
|
651
|
+
if receiver_node is not None and method_name == "forEach":
|
|
652
|
+
ancestor_texts = (
|
|
653
|
+
*receiver_node.metadata.get("java_superclass_args", ()),
|
|
654
|
+
*receiver_node.metadata.get("java_implements_args", ()),
|
|
655
|
+
)
|
|
656
|
+
for ancestor_text in ancestor_texts:
|
|
657
|
+
if erased_type(ancestor_text).rsplit(".", 1)[-1] != "Iterable":
|
|
658
|
+
continue
|
|
659
|
+
ancestor_args = self._generic_type_arguments(ancestor_text)
|
|
660
|
+
if len(ancestor_args) == 1:
|
|
661
|
+
iterable_elements.add(ancestor_args[0])
|
|
662
|
+
if len(iterable_elements) == 1:
|
|
663
|
+
expected.add(f"Consumer<{iterable_elements.pop()}>")
|
|
664
|
+
if len(expected) > 1:
|
|
665
|
+
# Every caller of this helper is target-typing a
|
|
666
|
+
# lambda/method-reference expression, which JLS requires
|
|
667
|
+
# to be a functional interface — a plain type like
|
|
668
|
+
# `String` can never be its real target. When sibling
|
|
669
|
+
# overload candidates at this argument position disagree
|
|
670
|
+
# (e.g. `assertAll(Executable...)` vs. `assertAll(String,
|
|
671
|
+
# Executable...)`, both arity-applicable to a call whose
|
|
672
|
+
# own callable arguments aren't yet typed), keep only
|
|
673
|
+
# whichever candidate(s) actually resolve to a functional
|
|
674
|
+
# interface.
|
|
675
|
+
functional_only = {
|
|
676
|
+
candidate for candidate in expected
|
|
677
|
+
if self._functional_descriptor(
|
|
678
|
+
candidate, module_name, import_map, wildcard_packages,
|
|
679
|
+
) is not None
|
|
680
|
+
}
|
|
681
|
+
if len(functional_only) == 1:
|
|
682
|
+
expected = functional_only
|
|
683
|
+
if len(expected) == 1 and receiver_args:
|
|
684
|
+
# A receiver typed by one of the caller's own bounded
|
|
685
|
+
# method type variables (`<W extends Wrapper<?>>
|
|
686
|
+
# List<W>`) target-types the callable as
|
|
687
|
+
# `Function<W,...>`. Inside this method `W` is exactly
|
|
688
|
+
# its declared bound, so substitute it. Only variables
|
|
689
|
+
# that flow from the receiver are rewritten; a callee's
|
|
690
|
+
# own `<T,R>` stays open for inference.
|
|
691
|
+
bounds = self._method_type_param_bounds.get(caller_node.id, {})
|
|
692
|
+
receiver_vars = {
|
|
693
|
+
erased_type(self._wildcard_upper_bound(arg) or arg).strip()
|
|
694
|
+
for arg in receiver_args
|
|
695
|
+
}
|
|
696
|
+
bounded = {
|
|
697
|
+
name: bound for name, bound in bounds.items()
|
|
698
|
+
if bound != "Object" and name in receiver_vars
|
|
699
|
+
}
|
|
700
|
+
if bounded:
|
|
701
|
+
expected = {
|
|
702
|
+
self._replace_type_variables(expected.pop(), bounded)
|
|
703
|
+
}
|
|
704
|
+
return self._functional_value_type(expected.pop()) if len(expected) == 1 else None
|
|
705
|
+
cur = parent
|
|
706
|
+
return None
|
|
707
|
+
|
|
708
|
+
def _add_lambda_param_scopes(
|
|
709
|
+
self, body, source: bytes, module_name: str, import_map: Dict[str, str],
|
|
710
|
+
wildcard_packages: List[str], local_types: "_LocalScopes",
|
|
711
|
+
param_types: Dict[str, str], field_types: Dict[str, str], caller_node,
|
|
712
|
+
) -> None:
|
|
713
|
+
# Method facts use the same nested-scope boundary as the former DFS,
|
|
714
|
+
# so a lambda owned by a nested method/type cannot leak into this
|
|
715
|
+
# caller's lexical scopes.
|
|
716
|
+
for node in self._method_facts(body).lambda_expressions:
|
|
717
|
+
# Keep the explicit guard as a defensive invariant for a future
|
|
718
|
+
# expansion of the shared fact set.
|
|
719
|
+
if node.type == "lambda_expression":
|
|
720
|
+
target_type = self._functional_target_type(
|
|
721
|
+
node, source, local_types, param_types, field_types, caller_node,
|
|
722
|
+
)
|
|
723
|
+
invocation_target = self._invocation_functional_target_type(
|
|
724
|
+
node, source, module_name, import_map, wildcard_packages,
|
|
725
|
+
local_types, param_types, field_types, caller_node,
|
|
726
|
+
)
|
|
727
|
+
# An enclosing assignment/return/container can provide a
|
|
728
|
+
# perfectly real *non-functional* type (for example
|
|
729
|
+
# ``return new Grouping[] { of(..., bean -> ...) }``). It
|
|
730
|
+
# must not mask the parameter type of the consuming factory
|
|
731
|
+
# invocation. Prefer an outer functional target when it
|
|
732
|
+
# actually has a SAM; otherwise try the invocation context.
|
|
733
|
+
descriptor = None
|
|
734
|
+
for candidate in (target_type, invocation_target):
|
|
735
|
+
if candidate is None:
|
|
736
|
+
continue
|
|
737
|
+
descriptor = self._functional_descriptor(
|
|
738
|
+
candidate, module_name, import_map, wildcard_packages,
|
|
739
|
+
)
|
|
740
|
+
if descriptor is not None:
|
|
741
|
+
break
|
|
742
|
+
params_node = node.child_by_field_name("parameters")
|
|
743
|
+
lambda_body = node.child_by_field_name("body")
|
|
744
|
+
if descriptor is not None and params_node is not None and lambda_body is not None:
|
|
745
|
+
names = [
|
|
746
|
+
self._text(child.child_by_field_name("name"), source)
|
|
747
|
+
if child.type == "formal_parameter" and child.child_by_field_name("name")
|
|
748
|
+
else self._text(child, source)
|
|
749
|
+
for child in (
|
|
750
|
+
[params_node] if params_node.type == "identifier"
|
|
751
|
+
else [c for c in params_node.children if c.is_named]
|
|
752
|
+
)
|
|
753
|
+
]
|
|
754
|
+
for name, type_text in zip(names, descriptor.params):
|
|
755
|
+
# JLS 15.27.1 forbids a lambda parameter from reusing
|
|
756
|
+
# the name of a local/parameter/field already in scope
|
|
757
|
+
# at the lambda's own position — such source is
|
|
758
|
+
# invalid but still parses, and an earlier pass
|
|
759
|
+
# (`_collect_lambda_parameter_scopes`) may have
|
|
760
|
+
# already recorded that outer binding as still in
|
|
761
|
+
# effect (typed) or explicitly marked this exact name
|
|
762
|
+
# as shadowed-with-unknown-type specifically so it
|
|
763
|
+
# would *not* resolve through to that outer
|
|
764
|
+
# declaration. Query at `node.start_byte` (the
|
|
765
|
+
# lambda's own start, before its parameter list comes
|
|
766
|
+
# into scope) rather than `lambda_body.start_byte`
|
|
767
|
+
# (already inside the lambda's own scope) to see what
|
|
768
|
+
# `name` means just *outside* the lambda; if it means
|
|
769
|
+
# anything there, this SAM-derived type must not
|
|
770
|
+
# silently override that outer fact with a
|
|
771
|
+
# same-named-but-different binding — leave whatever
|
|
772
|
+
# the earlier pass decided in place ("missing edge
|
|
773
|
+
# over wrong edge").
|
|
774
|
+
if (
|
|
775
|
+
local_types.type_at(name, node.start_byte) is not None
|
|
776
|
+
or local_types.declared_at(name, node.start_byte)
|
|
777
|
+
):
|
|
778
|
+
continue
|
|
779
|
+
# A returned ``Function<Object, R>`` describes the
|
|
780
|
+
# resulting function's input, not an ``andThen``
|
|
781
|
+
# lambda's input. Keep a more precise fluent-call
|
|
782
|
+
# fact in that one lossy fallback case; other target
|
|
783
|
+
# types may still refine placeholders such as ``E``.
|
|
784
|
+
existing = local_types.type_at(name, lambda_body.start_byte)
|
|
785
|
+
if existing is not None and type_text == "Object" and existing != "Object":
|
|
786
|
+
continue
|
|
787
|
+
# SAM descriptors are erased (`Predicate<Map.Entry<K,V>>`
|
|
788
|
+
# -> `Map.Entry`). The fluent-receiver pass may already
|
|
789
|
+
# hold the parameterized element type; its erasure is
|
|
790
|
+
# the same class, so keep the more precise fact.
|
|
791
|
+
if (
|
|
792
|
+
existing is not None and "<" in existing
|
|
793
|
+
and erased_type(existing).strip() == erased_type(type_text).strip()
|
|
794
|
+
):
|
|
795
|
+
continue
|
|
796
|
+
local_types.add(name, type_text, lambda_body.start_byte, node.end_byte)
|
|
797
|
+
|
|
798
|
+
@staticmethod
|
|
799
|
+
def _target_ids(target: Union[str, List[str], None]) -> List[str]:
|
|
800
|
+
return target if isinstance(target, list) else [target] if target else []
|
|
801
|
+
|
|
802
|
+
def _resolve_method_reference(
|
|
803
|
+
self, ref_node, source: bytes, module_name: str,
|
|
804
|
+
enclosing_class_id: Optional[str], import_map: Dict[str, str],
|
|
805
|
+
wildcard_packages: List[str], local_types: "_LocalScopes",
|
|
806
|
+
param_types: Dict[str, str], field_types: Dict[str, str], caller_node,
|
|
807
|
+
) -> Tuple[Union[str, List[str], None], float, EdgeType, Dict[str, object]]:
|
|
808
|
+
named = [child for child in ref_node.children if child.is_named]
|
|
809
|
+
if not named:
|
|
810
|
+
return None, 0.0, EdgeType.CALLABLE_REF, {}
|
|
811
|
+
qualifier = named[0]
|
|
812
|
+
terminal = next((c for c in reversed(ref_node.children) if c.type in {"identifier", "new"}), None)
|
|
813
|
+
if terminal is None:
|
|
814
|
+
return None, 0.0, EdgeType.CALLABLE_REF, {}
|
|
815
|
+
name = self._text(terminal, source)
|
|
816
|
+
target_type = self._functional_target_type(
|
|
817
|
+
ref_node, source, local_types, param_types, field_types, caller_node,
|
|
818
|
+
)
|
|
819
|
+
target_type = target_type or self._invocation_functional_target_type(
|
|
820
|
+
ref_node, source, module_name, import_map, wildcard_packages,
|
|
821
|
+
local_types, param_types, field_types, caller_node,
|
|
822
|
+
)
|
|
823
|
+
descriptor = self._functional_descriptor(
|
|
824
|
+
target_type, module_name, import_map, wildcard_packages,
|
|
825
|
+
)
|
|
826
|
+
fallback_arity = None
|
|
827
|
+
if descriptor is None:
|
|
828
|
+
arity = self._method_reference_context_arity(ref_node, source)
|
|
829
|
+
if arity is not None:
|
|
830
|
+
descriptor = _SAMDescriptor(["Object"] * arity, None)
|
|
831
|
+
fallback_arity = arity
|
|
832
|
+
ctx = _ImportContext(module_name, import_map, wildcard_packages)
|
|
833
|
+
meta: Dict[str, object] = {
|
|
834
|
+
"strategy": "method_reference", "reference_kind": "unknown",
|
|
835
|
+
**({"functional_type": target_type} if target_type else {}),
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
if name == "new":
|
|
839
|
+
if qualifier.type == "array_type":
|
|
840
|
+
# `Type[]::new` allocates an array; it invokes none of
|
|
841
|
+
# `Type`'s instance constructors (JLS 15.13.3), so it must
|
|
842
|
+
# not be attributed to any of them.
|
|
843
|
+
meta["reference_kind"] = "array_constructor"
|
|
844
|
+
return None, 0.0, EdgeType.CALLABLE_REF, meta
|
|
845
|
+
class_id = self._resolve_declared_type(
|
|
846
|
+
self._text(qualifier, source), module_name, import_map, wildcard_packages,
|
|
847
|
+
)
|
|
848
|
+
cls = self.builder._nodes.get(class_id) if class_id else None
|
|
849
|
+
if cls is None or cls.metadata.get("java_type_kind") in {"interface", "enum"}:
|
|
850
|
+
return None, 0.0, EdgeType.CALLABLE_REF, meta
|
|
851
|
+
meta["reference_kind"] = "constructor"
|
|
852
|
+
ctors = self._members_named(class_id, cls.name, search_ancestors=False)
|
|
853
|
+
expected_return = descriptor.return_type if descriptor else None
|
|
854
|
+
if expected_return not in (None, "void") and not self._types_compatible(
|
|
855
|
+
expected_return, cls.name, ctx,
|
|
856
|
+
self._file_ctx.get(cls.file_path, _ImportContext(cls.module, {}, [])),
|
|
857
|
+
allow_boxing=True,
|
|
858
|
+
):
|
|
859
|
+
return None, 0.0, EdgeType.CALLABLE_REF, meta
|
|
860
|
+
if descriptor is None:
|
|
861
|
+
target: Union[str, List[str], None] = ctors[0] if len(ctors) == 1 else ctors or None
|
|
862
|
+
conf = 0.7 if len(ctors) == 1 else 0.4
|
|
863
|
+
elif ctors:
|
|
864
|
+
target, conf = self._select_overload(
|
|
865
|
+
ctors, len(descriptor.params), descriptor.params, lambda _c: 0.95, ctx,
|
|
866
|
+
)
|
|
867
|
+
if target is None and fallback_arity is not None:
|
|
868
|
+
# The JDK fluent/collector call may not supply the
|
|
869
|
+
# referenced constructor's true parameter types either;
|
|
870
|
+
# mirror the static/unbound and bound-instance fallbacks
|
|
871
|
+
# below instead of rejecting a unique-arity candidate
|
|
872
|
+
# because the placeholder Object type is too broad.
|
|
873
|
+
fallback = [c for c in ctors if self._arity_matches(c, fallback_arity)]
|
|
874
|
+
if len(fallback) == 1:
|
|
875
|
+
target, conf = fallback[0], 0.7
|
|
876
|
+
else:
|
|
877
|
+
implicit_arity = len(cls.metadata.get("record_component_types", []))
|
|
878
|
+
valid = len(descriptor.params) == (implicit_arity if cls.metadata.get("java_type_kind") == "record" else 0)
|
|
879
|
+
return (class_id, 0.85, EdgeType.INSTANTIATES, meta) if valid else (
|
|
880
|
+
None, 0.0, EdgeType.INSTANTIATES, meta
|
|
881
|
+
)
|
|
882
|
+
if isinstance(target, list):
|
|
883
|
+
meta["candidate"] = True
|
|
884
|
+
conf = min(conf, 0.45)
|
|
885
|
+
return target, conf, EdgeType.CALLABLE_REF, meta
|
|
886
|
+
|
|
887
|
+
bound_class: Optional[str] = None
|
|
888
|
+
reference_kind = "bound"
|
|
889
|
+
if qualifier.type == "this":
|
|
890
|
+
bound_class = enclosing_class_id
|
|
891
|
+
elif qualifier.type == "super":
|
|
892
|
+
if enclosing_class_id:
|
|
893
|
+
candidates = self._members_named(
|
|
894
|
+
enclosing_class_id, name, search_ancestors=True, search_self=False,
|
|
895
|
+
)
|
|
896
|
+
if descriptor:
|
|
897
|
+
candidates = [
|
|
898
|
+
c for c in candidates
|
|
899
|
+
if self._method_reference_return_compatible(c, descriptor.return_type, ctx)
|
|
900
|
+
]
|
|
901
|
+
target, conf = self._select_overload(
|
|
902
|
+
[c for c in candidates if not self._is_static_member(c)],
|
|
903
|
+
len(descriptor.params), descriptor.params, lambda _c: 0.85, ctx,
|
|
904
|
+
) if descriptor is not None else (
|
|
905
|
+
(candidates[0], 0.7) if len(candidates) == 1 else (candidates or None, 0.4)
|
|
906
|
+
)
|
|
907
|
+
meta["reference_kind"] = "super"
|
|
908
|
+
if isinstance(target, list):
|
|
909
|
+
meta["candidate"] = True
|
|
910
|
+
return target, conf, EdgeType.CALLABLE_REF, meta
|
|
911
|
+
else:
|
|
912
|
+
qtext = self._text(qualifier, source)
|
|
913
|
+
is_value = (
|
|
914
|
+
(qualifier.type == "identifier" and (
|
|
915
|
+
local_types.type_at(qtext, qualifier.start_byte) is not None
|
|
916
|
+
or qtext in param_types or qtext in field_types
|
|
917
|
+
))
|
|
918
|
+
or qualifier.type in {
|
|
919
|
+
"field_access", "object_creation_expression",
|
|
920
|
+
"method_invocation", "parenthesized_expression",
|
|
921
|
+
"cast_expression", "array_access",
|
|
922
|
+
}
|
|
923
|
+
)
|
|
924
|
+
if qualifier.type == "field_access":
|
|
925
|
+
# A dotted qualifier is a value only when its leftmost name
|
|
926
|
+
# is one: `this.delegate::run`, `holder.field::run`. A
|
|
927
|
+
# package/outer-type path (`org.acme.Resource::of`,
|
|
928
|
+
# `Outer.Inner::create`) has the same CST shape but names a
|
|
929
|
+
# type, whose static and unbound members follow below.
|
|
930
|
+
head = qualifier
|
|
931
|
+
while head.type == "field_access":
|
|
932
|
+
head = head.child_by_field_name("object")
|
|
933
|
+
head_is_value = head is None or head.type != "identifier" or (
|
|
934
|
+
local_types.type_at(self._text(head, source), head.start_byte) is not None
|
|
935
|
+
or self._text(head, source) in param_types
|
|
936
|
+
or self._text(head, source) in field_types
|
|
937
|
+
)
|
|
938
|
+
if not head_is_value and self._resolve_declared_type(
|
|
939
|
+
qtext, module_name, import_map, wildcard_packages,
|
|
940
|
+
) is not None:
|
|
941
|
+
is_value = False
|
|
942
|
+
if is_value:
|
|
943
|
+
bound_class, _ = self._resolve_receiver(
|
|
944
|
+
qualifier, source, module_name, enclosing_class_id, import_map,
|
|
945
|
+
wildcard_packages, local_types, field_types, param_types,
|
|
946
|
+
)
|
|
947
|
+
else:
|
|
948
|
+
type_class = self._resolve_declared_type(
|
|
949
|
+
qtext, module_name, import_map, wildcard_packages,
|
|
950
|
+
)
|
|
951
|
+
if type_class is not None:
|
|
952
|
+
candidates = self._members_named(type_class, name, search_ancestors=True)
|
|
953
|
+
static = [c for c in candidates if self._is_static_member(c)]
|
|
954
|
+
instance = [c for c in candidates if not self._is_static_member(c)]
|
|
955
|
+
if descriptor:
|
|
956
|
+
static = [
|
|
957
|
+
c for c in static
|
|
958
|
+
if self._method_reference_return_compatible(c, descriptor.return_type, ctx)
|
|
959
|
+
]
|
|
960
|
+
instance = [
|
|
961
|
+
c for c in instance
|
|
962
|
+
if self._method_reference_return_compatible(c, descriptor.return_type, ctx)
|
|
963
|
+
]
|
|
964
|
+
if descriptor is None:
|
|
965
|
+
all_candidates = static + instance
|
|
966
|
+
target = all_candidates[0] if len(all_candidates) == 1 else all_candidates or None
|
|
967
|
+
conf = 0.65 if len(all_candidates) == 1 else 0.4
|
|
968
|
+
else:
|
|
969
|
+
st, st_conf = self._select_overload(
|
|
970
|
+
static, len(descriptor.params), descriptor.params, lambda _c: 0.9, ctx,
|
|
971
|
+
)
|
|
972
|
+
unbound: Union[str, List[str], None] = None
|
|
973
|
+
unbound_conf = 0.0
|
|
974
|
+
if descriptor.params and (
|
|
975
|
+
target_type is None
|
|
976
|
+
or re.fullmatch(r"[A-Z]", descriptor.params[0])
|
|
977
|
+
or self._types_compatible(
|
|
978
|
+
qtext, descriptor.params[0], ctx, ctx,
|
|
979
|
+
)
|
|
980
|
+
):
|
|
981
|
+
unbound, unbound_conf = self._select_overload(
|
|
982
|
+
instance, len(descriptor.params) - 1, descriptor.params[1:],
|
|
983
|
+
lambda _c: 0.9, ctx,
|
|
984
|
+
)
|
|
985
|
+
winners = self._target_ids(st) + self._target_ids(unbound)
|
|
986
|
+
winners = list(dict.fromkeys(winners))
|
|
987
|
+
if (
|
|
988
|
+
not winners
|
|
989
|
+
and descriptor.params
|
|
990
|
+
and all(re.fullmatch(r"[A-Z]", item) for item in descriptor.params)
|
|
991
|
+
):
|
|
992
|
+
# A generic SAM such as `Function<T,R>` can infer
|
|
993
|
+
# T from a unique static source reference. Its
|
|
994
|
+
# still-open input variable is not an incompatible
|
|
995
|
+
# concrete argument; arity plus uniqueness is the
|
|
996
|
+
# bounded inference proof at this point.
|
|
997
|
+
inferred_static = [
|
|
998
|
+
candidate for candidate in static
|
|
999
|
+
if self._arity_matches(candidate, len(descriptor.params))
|
|
1000
|
+
]
|
|
1001
|
+
if len(inferred_static) == 1:
|
|
1002
|
+
winners = inferred_static
|
|
1003
|
+
if not winners and fallback_arity is not None:
|
|
1004
|
+
# External Stream/Optional declarations may not
|
|
1005
|
+
# provide element generic metadata. Preserve the
|
|
1006
|
+
# method-reference edge when arity identifies a
|
|
1007
|
+
# unique static/instance candidate; parameter
|
|
1008
|
+
# compatibility remains enforced when a real SAM
|
|
1009
|
+
# descriptor is available.
|
|
1010
|
+
fallback = [
|
|
1011
|
+
c for c in static + instance
|
|
1012
|
+
if len(self._param_type_lists.get(c, []))
|
|
1013
|
+
== (fallback_arity if self._is_static_member(c)
|
|
1014
|
+
else fallback_arity - 1)
|
|
1015
|
+
]
|
|
1016
|
+
if len(fallback) == 1:
|
|
1017
|
+
winners = fallback
|
|
1018
|
+
target = winners[0] if len(winners) == 1 else winners or None
|
|
1019
|
+
conf = max(st_conf, unbound_conf) if len(winners) == 1 else 0.45
|
|
1020
|
+
meta["reference_kind"] = (
|
|
1021
|
+
"static" if self._target_ids(target) and all(
|
|
1022
|
+
self._is_static_member(t) for t in self._target_ids(target)
|
|
1023
|
+
) else "unbound"
|
|
1024
|
+
)
|
|
1025
|
+
if isinstance(target, list):
|
|
1026
|
+
meta["candidate"] = True
|
|
1027
|
+
return target, conf, EdgeType.CALLABLE_REF, meta
|
|
1028
|
+
|
|
1029
|
+
if bound_class is None:
|
|
1030
|
+
return None, 0.0, EdgeType.CALLABLE_REF, meta
|
|
1031
|
+
candidates = [
|
|
1032
|
+
c for c in self._members_named(bound_class, name, search_ancestors=True)
|
|
1033
|
+
if not self._is_static_member(c)
|
|
1034
|
+
]
|
|
1035
|
+
if descriptor:
|
|
1036
|
+
candidates = [
|
|
1037
|
+
c for c in candidates
|
|
1038
|
+
if self._method_reference_return_compatible(c, descriptor.return_type, ctx)
|
|
1039
|
+
]
|
|
1040
|
+
if descriptor is None:
|
|
1041
|
+
target = candidates[0] if len(candidates) == 1 else candidates or None
|
|
1042
|
+
conf = 0.7 if len(candidates) == 1 else 0.4
|
|
1043
|
+
else:
|
|
1044
|
+
target, conf = self._select_overload(
|
|
1045
|
+
candidates, len(descriptor.params), descriptor.params, lambda _c: 0.9, ctx,
|
|
1046
|
+
)
|
|
1047
|
+
if (
|
|
1048
|
+
target is None
|
|
1049
|
+
and descriptor.params
|
|
1050
|
+
and all(re.fullmatch(r"[A-Z]", item) for item in descriptor.params)
|
|
1051
|
+
):
|
|
1052
|
+
# Bound `this::method` references passed to `Consumer<T>` or
|
|
1053
|
+
# `Function<T,R>` infer their open input variables from the
|
|
1054
|
+
# referenced method. Keep this to one exact-arity candidate;
|
|
1055
|
+
# overloaded references remain unresolved.
|
|
1056
|
+
inferred = [
|
|
1057
|
+
candidate for candidate in candidates
|
|
1058
|
+
if self._arity_matches(candidate, len(descriptor.params))
|
|
1059
|
+
]
|
|
1060
|
+
if len(inferred) == 1:
|
|
1061
|
+
target, conf = inferred[0], 0.85
|
|
1062
|
+
if target is None and fallback_arity is not None:
|
|
1063
|
+
# The JDK fluent receiver can be external and therefore
|
|
1064
|
+
# unable to supply its element type. For a proven bound
|
|
1065
|
+
# receiver, a unique same-name instance member with the SAM
|
|
1066
|
+
# arity is still unambiguous (`repository::create`); mirror
|
|
1067
|
+
# the static/unbound fallback above instead of rejecting it
|
|
1068
|
+
# because the placeholder Object type is too broad.
|
|
1069
|
+
fallback = [
|
|
1070
|
+
candidate for candidate in candidates
|
|
1071
|
+
if self._arity_matches(candidate, fallback_arity)
|
|
1072
|
+
]
|
|
1073
|
+
if len(fallback) == 1:
|
|
1074
|
+
target, conf = fallback[0], 0.7
|
|
1075
|
+
meta["reference_kind"] = reference_kind
|
|
1076
|
+
if isinstance(target, list):
|
|
1077
|
+
meta["candidate"] = True
|
|
1078
|
+
conf = min(conf, 0.45)
|
|
1079
|
+
return target, conf, EdgeType.CALLABLE_REF, meta
|
|
1080
|
+
|
|
1081
|
+
def _method_reference_context_arity(
|
|
1082
|
+
self, node, source: bytes,
|
|
1083
|
+
) -> Optional[int]:
|
|
1084
|
+
"""Narrow fallback for external JDK calls lacking indexed signatures.
|
|
1085
|
+
|
|
1086
|
+
Maps the enclosing JDK Stream/Optional method to the arity of the
|
|
1087
|
+
functional interface its argument must implement: element consumers
|
|
1088
|
+
(`map`, `filter`, ...) take one parameter; `Supplier` consumers
|
|
1089
|
+
(`orElseGet`, `orElseThrow`, `Optional.or`) take none, so a bound
|
|
1090
|
+
reference such as `formattable::getAbbreviatedFullName` passed to
|
|
1091
|
+
`orElseGet` must select the zero-argument overload rather than fan out
|
|
1092
|
+
across every same-name member. Anything else stays unknown."""
|
|
1093
|
+
parent = node.parent
|
|
1094
|
+
if parent is not None and parent.type == "argument_list":
|
|
1095
|
+
invocation = parent.parent
|
|
1096
|
+
if invocation is not None and invocation.type == "method_invocation":
|
|
1097
|
+
name_node = invocation.child_by_field_name("name")
|
|
1098
|
+
if name_node is not None:
|
|
1099
|
+
name = self._text(name_node, source)
|
|
1100
|
+
arity = _JDK_FUNCTIONAL_ARGUMENT_ARITY.get(name)
|
|
1101
|
+
if arity is not None:
|
|
1102
|
+
return arity
|
|
1103
|
+
indexed = _JDK_INDEXED_FUNCTIONAL_ARITY.get(name)
|
|
1104
|
+
if indexed is not None:
|
|
1105
|
+
args = argument_nodes(parent)
|
|
1106
|
+
try:
|
|
1107
|
+
index = args.index(node)
|
|
1108
|
+
except ValueError:
|
|
1109
|
+
return None
|
|
1110
|
+
if index < len(indexed):
|
|
1111
|
+
return indexed[index]
|
|
1112
|
+
return None
|
|
1113
|
+
|
|
1114
|
+
@staticmethod
|
|
1115
|
+
def _container_name(node, source: bytes) -> Optional[str]:
|
|
1116
|
+
if node is None:
|
|
1117
|
+
return None
|
|
1118
|
+
if node.type == "identifier":
|
|
1119
|
+
return source[node.start_byte:node.end_byte].decode("utf-8", errors="replace")
|
|
1120
|
+
if node.type == "field_access":
|
|
1121
|
+
field = node.child_by_field_name("field")
|
|
1122
|
+
if field is not None:
|
|
1123
|
+
return source[field.start_byte:field.end_byte].decode("utf-8", errors="replace")
|
|
1124
|
+
return None
|
|
1125
|
+
|
|
1126
|
+
def _local_callable_targets(
|
|
1127
|
+
self, body, source: bytes, resolved_refs: Dict[int, Set[str]],
|
|
1128
|
+
) -> Dict[str, Set[str]]:
|
|
1129
|
+
out: Dict[str, Set[str]] = defaultdict(set)
|
|
1130
|
+
for ref in self._collect_method_references(body):
|
|
1131
|
+
targets = resolved_refs.get(ref.start_byte)
|
|
1132
|
+
if not targets:
|
|
1133
|
+
continue
|
|
1134
|
+
cur = ref
|
|
1135
|
+
while cur.parent is not None and cur.parent is not body:
|
|
1136
|
+
parent = cur.parent
|
|
1137
|
+
if parent.type == "variable_declarator":
|
|
1138
|
+
name = parent.child_by_field_name("name")
|
|
1139
|
+
if name is not None:
|
|
1140
|
+
out[self._text(name, source)].update(targets)
|
|
1141
|
+
break
|
|
1142
|
+
if parent.type == "assignment_expression":
|
|
1143
|
+
container = self._container_name(
|
|
1144
|
+
parent.child_by_field_name("left"), source,
|
|
1145
|
+
)
|
|
1146
|
+
if container:
|
|
1147
|
+
out[container].update(targets)
|
|
1148
|
+
break
|
|
1149
|
+
if parent.type == "argument_list" and parent.parent is not None:
|
|
1150
|
+
invocation = parent.parent
|
|
1151
|
+
name_node = invocation.child_by_field_name("name")
|
|
1152
|
+
if name_node is not None and self._text(name_node, source) == "put":
|
|
1153
|
+
container = self._container_name(
|
|
1154
|
+
invocation.child_by_field_name("object"), source,
|
|
1155
|
+
)
|
|
1156
|
+
if container:
|
|
1157
|
+
out[container].update(targets)
|
|
1158
|
+
break
|
|
1159
|
+
cur = parent
|
|
1160
|
+
return out
|
|
1161
|
+
|
|
1162
|
+
def _resolve_callable_container_dispatch(
|
|
1163
|
+
self, call_node, source: bytes, enclosing_class_id: Optional[str],
|
|
1164
|
+
local_targets: Dict[str, Set[str]],
|
|
1165
|
+
local_types: "_LocalScopes", own_param_types: Dict[str, str],
|
|
1166
|
+
) -> Optional[Tuple[List[str], float, Dict[str, object]]]:
|
|
1167
|
+
name_node = call_node.child_by_field_name("name")
|
|
1168
|
+
obj = call_node.child_by_field_name("object")
|
|
1169
|
+
if name_node is None or obj is None or self._text(name_node, source) not in {
|
|
1170
|
+
"run", "call", "apply", "accept", "test",
|
|
1171
|
+
}:
|
|
1172
|
+
return None
|
|
1173
|
+
container_node = obj
|
|
1174
|
+
if obj.type == "method_invocation":
|
|
1175
|
+
inner_name = obj.child_by_field_name("name")
|
|
1176
|
+
if inner_name is None or self._text(inner_name, source) not in {"get", "getOrDefault"}:
|
|
1177
|
+
return None
|
|
1178
|
+
container_node = obj.child_by_field_name("object")
|
|
1179
|
+
container = self._container_name(container_node, source)
|
|
1180
|
+
if not container:
|
|
1181
|
+
return None
|
|
1182
|
+
targets = set(local_targets.get(container, set()))
|
|
1183
|
+
# A local variable or parameter of the same name shadows a
|
|
1184
|
+
# same-named field (Java scoping rules) — the field's callable
|
|
1185
|
+
# table must not be consulted in that case, or a call through an
|
|
1186
|
+
# unrelated locally/parameter-typed receiver gets misattributed to
|
|
1187
|
+
# the shadowed field's bound method reference. Only a *bare*
|
|
1188
|
+
# identifier receiver is subject to shadowing this way — an
|
|
1189
|
+
# explicit field access (`this.task`, or any `x.task`) already
|
|
1190
|
+
# names the field unambiguously and is never shadowed by a
|
|
1191
|
+
# same-named local/parameter.
|
|
1192
|
+
shadowed_by_local = container_node.type == "identifier" and (
|
|
1193
|
+
container in own_param_types
|
|
1194
|
+
or local_types.type_at(container, container_node.start_byte) is not None
|
|
1195
|
+
# A `var` local whose initializer type couldn't be resolved
|
|
1196
|
+
# (e.g. an external factory call) is still a real declaration
|
|
1197
|
+
# that shadows the field — `type_at` alone would miss it
|
|
1198
|
+
# ("missing edge over wrong edge" left it out of `_by_name`).
|
|
1199
|
+
or local_types.declared_at(container, container_node.start_byte)
|
|
1200
|
+
)
|
|
1201
|
+
if enclosing_class_id and not shadowed_by_local:
|
|
1202
|
+
for owner in [enclosing_class_id, *self._class_ancestors(enclosing_class_id)]:
|
|
1203
|
+
targets.update(self._field_callable_targets.get(owner, {}).get(container, set()))
|
|
1204
|
+
if not targets:
|
|
1205
|
+
return None
|
|
1206
|
+
return sorted(targets), (0.8 if len(targets) == 1 else 0.6), {
|
|
1207
|
+
"strategy": "callable_container", "container": container,
|
|
1208
|
+
"candidate": len(targets) > 1,
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1211
|
+
def _index_field_callable_targets(
|
|
1212
|
+
self, parses: Dict[Path, ParseResult],
|
|
1213
|
+
class_id_by_position: Dict[Tuple[str, int, int], str],
|
|
1214
|
+
func_id_by_position: Dict[Tuple[str, int, int], str],
|
|
1215
|
+
) -> Dict[str, Dict[str, Set[str]]]:
|
|
1216
|
+
out: Dict[str, Dict[str, Set[str]]] = defaultdict(lambda: defaultdict(set))
|
|
1217
|
+
for path, parse in parses.items():
|
|
1218
|
+
fp = str(path)
|
|
1219
|
+
ctx = self._file_ctx.get(fp)
|
|
1220
|
+
if ctx is None:
|
|
1221
|
+
continue
|
|
1222
|
+
source = parse.source_bytes
|
|
1223
|
+
for node in self._field_declarations(parse.tree.root_node, fp):
|
|
1224
|
+
owner_node = node.parent
|
|
1225
|
+
while owner_node is not None and owner_node.type not in _ENCLOSING_TYPE_CST_TYPES:
|
|
1226
|
+
owner_node = owner_node.parent
|
|
1227
|
+
if owner_node is None:
|
|
1228
|
+
continue
|
|
1229
|
+
owner_id = class_id_by_position.get(
|
|
1230
|
+
(fp, owner_node.start_point[0] + 1, owner_node.start_point[1]),
|
|
1231
|
+
)
|
|
1232
|
+
owner = self.builder._nodes.get(owner_id) if owner_id else None
|
|
1233
|
+
if owner is None:
|
|
1234
|
+
continue
|
|
1235
|
+
fields = self._all_field_types(owner_id)
|
|
1236
|
+
for declarator in [c for c in node.children if c.type == "variable_declarator"]:
|
|
1237
|
+
name_node = declarator.child_by_field_name("name")
|
|
1238
|
+
value = declarator.child_by_field_name("value")
|
|
1239
|
+
if name_node is None or value is None:
|
|
1240
|
+
continue
|
|
1241
|
+
field_name = self._text(name_node, source)
|
|
1242
|
+
refs: List[object] = []
|
|
1243
|
+
ref_stack = [value]
|
|
1244
|
+
while ref_stack:
|
|
1245
|
+
child = ref_stack.pop()
|
|
1246
|
+
if child.type == "method_reference":
|
|
1247
|
+
refs.append(child)
|
|
1248
|
+
ref_stack.extend(child.children)
|
|
1249
|
+
for ref in refs:
|
|
1250
|
+
target, confidence, etype, meta = self._resolve_method_reference(
|
|
1251
|
+
ref, source, ctx.module, owner_id, ctx.import_map,
|
|
1252
|
+
ctx.wildcard_packages, _LocalScopes(), {}, fields, owner,
|
|
1253
|
+
)
|
|
1254
|
+
target_ids = self._target_ids(target)
|
|
1255
|
+
out[owner_id][field_name].update(target_ids)
|
|
1256
|
+
for tid in target_ids:
|
|
1257
|
+
if tid not in self.builder._nodes or (owner_id, tid, etype) in self.builder._edge_keys:
|
|
1258
|
+
continue
|
|
1259
|
+
self.builder._add_edge(GraphEdge(
|
|
1260
|
+
source=owner_id, target=tid, edge_type=etype,
|
|
1261
|
+
file_path=fp, line=ref.start_point[0] + 1,
|
|
1262
|
+
column=ref.start_point[1], confidence=confidence,
|
|
1263
|
+
metadata=meta, strategy="java_resolver",
|
|
1264
|
+
))
|
|
1265
|
+
# `field = Type::method;` is a reference, not an
|
|
1266
|
+
# invocation — see the matching note in
|
|
1267
|
+
# orchestration.py's _resolve_method_reference_pass.
|
|
1268
|
+
# Leave .calls/.called_by (true-invocation
|
|
1269
|
+
# bookkeeping) untouched.
|
|
1270
|
+
|
|
1271
|
+
# Field containers populated in a constructor/setup method must
|
|
1272
|
+
# be available before any dispatch method is visited; method CST
|
|
1273
|
+
# traversal order is intentionally not source-order-dependent.
|
|
1274
|
+
for func_cst, caller_id in self._iter_method_cst(
|
|
1275
|
+
parse.tree.root_node, fp, func_id_by_position,
|
|
1276
|
+
):
|
|
1277
|
+
owner_id = self._declaring_class_id(caller_id)
|
|
1278
|
+
body = func_cst.child_by_field_name("body")
|
|
1279
|
+
caller = self.builder._nodes.get(caller_id)
|
|
1280
|
+
if owner_id is None or body is None or caller is None:
|
|
1281
|
+
continue
|
|
1282
|
+
fields = self._all_field_types(owner_id)
|
|
1283
|
+
local_types = self._collect_local_scopes(body, source)
|
|
1284
|
+
params = dict(zip(
|
|
1285
|
+
self._param_names(func_cst, source),
|
|
1286
|
+
self._param_type_lists.get(caller_id, []),
|
|
1287
|
+
))
|
|
1288
|
+
for ref in self._collect_method_references(body):
|
|
1289
|
+
container: Optional[str] = None
|
|
1290
|
+
cur = ref
|
|
1291
|
+
while cur.parent is not None and cur.parent is not body:
|
|
1292
|
+
parent = cur.parent
|
|
1293
|
+
if parent.type == "assignment_expression":
|
|
1294
|
+
container = self._container_name(
|
|
1295
|
+
parent.child_by_field_name("left"), source,
|
|
1296
|
+
)
|
|
1297
|
+
break
|
|
1298
|
+
if parent.type == "argument_list" and parent.parent is not None:
|
|
1299
|
+
invocation = parent.parent
|
|
1300
|
+
called = invocation.child_by_field_name("name")
|
|
1301
|
+
if called is not None and self._text(called, source) == "put":
|
|
1302
|
+
container = self._container_name(
|
|
1303
|
+
invocation.child_by_field_name("object"), source,
|
|
1304
|
+
)
|
|
1305
|
+
break
|
|
1306
|
+
cur = parent
|
|
1307
|
+
if (
|
|
1308
|
+
not container or container not in fields
|
|
1309
|
+
or container in params
|
|
1310
|
+
or local_types.type_at(container, ref.start_byte) is not None
|
|
1311
|
+
):
|
|
1312
|
+
continue
|
|
1313
|
+
target, _, _, _ = self._resolve_method_reference(
|
|
1314
|
+
ref, source, ctx.module, owner_id, ctx.import_map,
|
|
1315
|
+
ctx.wildcard_packages, local_types, params, fields, caller,
|
|
1316
|
+
)
|
|
1317
|
+
out[owner_id][container].update(self._target_ids(target))
|
|
1318
|
+
return out
|