adl-toolkit 0.0.0-stage → 0.1.1

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