adl-toolkit 0.0.0-stage → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (104) hide show
  1. package/INSTALL_TOOLKIT.md +163 -0
  2. package/README.md +269 -2
  3. package/adl_drift/__init__.py +6 -0
  4. package/adl_drift/cli/__init__.py +1 -0
  5. package/adl_drift/cli/commands.py +993 -0
  6. package/adl_drift/cli/main.py +744 -0
  7. package/adl_drift/cortex/__init__.py +1 -0
  8. package/adl_drift/cortex/embeddings.py +96 -0
  9. package/adl_drift/cortex/memory.py +1749 -0
  10. package/adl_drift/cortex/patterns.py +89 -0
  11. package/adl_drift/cortex/telemetry.py +192 -0
  12. package/adl_drift/detectors/__init__.py +1 -0
  13. package/adl_drift/detectors/async_patterns.py +115 -0
  14. package/adl_drift/detectors/base.py +121 -0
  15. package/adl_drift/detectors/config.py +122 -0
  16. package/adl_drift/detectors/data_access.py +128 -0
  17. package/adl_drift/detectors/decorators.py +122 -0
  18. package/adl_drift/detectors/dynamic_patterns.py +260 -0
  19. package/adl_drift/detectors/errors.py +134 -0
  20. package/adl_drift/detectors/imports.py +132 -0
  21. package/adl_drift/detectors/logging_patterns.py +108 -0
  22. package/adl_drift/detectors/routes.py +257 -0
  23. package/adl_drift/detectors/security.py +236 -0
  24. package/adl_drift/detectors/taint.py +406 -0
  25. package/adl_drift/detectors/tests.py +95 -0
  26. package/adl_drift/detectors/type_conventions.py +81 -0
  27. package/adl_drift/graph/__init__.py +1 -0
  28. package/adl_drift/graph/builder.py +2670 -0
  29. package/adl_drift/graph/cfg.py +461 -0
  30. package/adl_drift/graph/cluster.py +40 -0
  31. package/adl_drift/graph/edges.py +86 -0
  32. package/adl_drift/graph/node.py +90 -0
  33. package/adl_drift/harness/__init__.py +10 -0
  34. package/adl_drift/harness/setup.py +832 -0
  35. package/adl_drift/harness/skills/adl-drift.md +25 -0
  36. package/adl_drift/harness/skills/drift-antigravity.md +19 -0
  37. package/adl_drift/harness/skills/drift-generic.md +19 -0
  38. package/adl_drift/harness/watch.py +84 -0
  39. package/adl_drift/lang/__init__.py +8 -0
  40. package/adl_drift/lang/base.py +464 -0
  41. package/adl_drift/lang/java/__init__.py +6 -0
  42. package/adl_drift/lang/java/backend.py +97 -0
  43. package/adl_drift/lang/java/cfg.py +1424 -0
  44. package/adl_drift/lang/java/config.py +707 -0
  45. package/adl_drift/lang/java/detectors/__init__.py +29 -0
  46. package/adl_drift/lang/java/detectors/additional.py +445 -0
  47. package/adl_drift/lang/java/detectors/base.py +154 -0
  48. package/adl_drift/lang/java/detectors/data_access.py +148 -0
  49. package/adl_drift/lang/java/detectors/imports.py +53 -0
  50. package/adl_drift/lang/java/detectors/routes.py +159 -0
  51. package/adl_drift/lang/java/detectors/security.py +386 -0
  52. package/adl_drift/lang/java/detectors/tests.py +74 -0
  53. package/adl_drift/lang/java/extractor.py +582 -0
  54. package/adl_drift/lang/java/ids.py +99 -0
  55. package/adl_drift/lang/java/parser.py +49 -0
  56. package/adl_drift/lang/java/resolution/__init__.py +1 -0
  57. package/adl_drift/lang/java/resolution/constants.py +1619 -0
  58. package/adl_drift/lang/java/resolution/constructors.py +278 -0
  59. package/adl_drift/lang/java/resolution/context.py +305 -0
  60. package/adl_drift/lang/java/resolution/dispatch.py +540 -0
  61. package/adl_drift/lang/java/resolution/functional.py +1318 -0
  62. package/adl_drift/lang/java/resolution/generated.py +95 -0
  63. package/adl_drift/lang/java/resolution/model.py +153 -0
  64. package/adl_drift/lang/java/resolution/orchestration.py +866 -0
  65. package/adl_drift/lang/java/resolution/overloads.py +1275 -0
  66. package/adl_drift/lang/java/resolution/scopes.py +1111 -0
  67. package/adl_drift/lang/java/resolution/types.py +2947 -0
  68. package/adl_drift/lang/java/resolver.py +1401 -0
  69. package/adl_drift/lang/java/scopes.py +69 -0
  70. package/adl_drift/lang/java/taint.py +500 -0
  71. package/adl_drift/lang/java/type_provenance.py +342 -0
  72. package/adl_drift/lang/python/__init__.py +8 -0
  73. package/adl_drift/lang/python/backend.py +94 -0
  74. package/adl_drift/lang/python/cfg.py +537 -0
  75. package/adl_drift/lang/python/extractor.py +454 -0
  76. package/adl_drift/lang/python/parser.py +41 -0
  77. package/adl_drift/lang/python/resolver.py +1789 -0
  78. package/adl_drift/lang/python/taint.py +365 -0
  79. package/adl_drift/lang/registry.py +117 -0
  80. package/adl_drift/parser/__init__.py +1 -0
  81. package/adl_drift/parser/cache.py +38 -0
  82. package/adl_drift/parser/core.py +33 -0
  83. package/adl_drift/parser/extractor.py +17 -0
  84. package/adl_drift/parser/literal_eval.py +326 -0
  85. package/adl_drift/scanner/__init__.py +1 -0
  86. package/adl_drift/scanner/discovery.py +338 -0
  87. package/adl_drift/scanner/incremental.py +261 -0
  88. package/adl_drift/scanner/parallel.py +224 -0
  89. package/adl_drift/server/__init__.py +1 -0
  90. package/adl_drift/server/handlers.py +2849 -0
  91. package/adl_drift/server/mcp_server.py +127 -0
  92. package/adl_drift/server/mcp_stdio.py +335 -0
  93. package/adl_drift/server/protocol.py +71 -0
  94. package/adl_drift/server/static/index.html +696 -0
  95. package/adl_drift/server/web.py +350 -0
  96. package/adl_drift/utils/__init__.py +1 -0
  97. package/adl_drift/utils/config.py +77 -0
  98. package/adl_drift/utils/fswalk.py +52 -0
  99. package/adl_drift/utils/types.py +345 -0
  100. package/bin/adl-drift.mjs +35 -0
  101. package/bin/adl-toolkit.mjs +488 -0
  102. package/package.json +29 -4
  103. package/pyproject.toml +49 -0
  104. package/scripts/install_toolkit.py +3584 -0
@@ -0,0 +1,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