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,866 @@
1
+ """Execution-order-preserving orchestration for Java call resolution."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import copy
6
+ import logging
7
+ import multiprocessing
8
+ import os
9
+ import sys
10
+ from collections import defaultdict
11
+ from concurrent.futures import ProcessPoolExecutor
12
+ from concurrent.futures.process import BrokenProcessPool
13
+ from pathlib import Path
14
+ from typing import Dict, List, Set, Tuple
15
+
16
+ from adl_drift.graph.edges import EdgeType, GraphEdge
17
+ from adl_drift.graph.node import GraphNode, NodeType
18
+ from adl_drift.lang.base import ParseResult
19
+ from adl_drift.lang.java.resolution.constants import (
20
+ _ENCLOSING_TYPE_CST_TYPES,
21
+ _INIT_BLOCK_PARENTS,
22
+ )
23
+ from adl_drift.lang.java.resolution.model import ImportContext as _ImportContext
24
+ from adl_drift.utils.types import AnalysisResult
25
+
26
+ logger = logging.getLogger(__name__)
27
+
28
+
29
+ class _ProcessEdgeCollector:
30
+ """Worker-local edge sink over the parent's read-only graph snapshot."""
31
+
32
+ def __init__(self, base) -> None:
33
+ self._base = base
34
+ self._nodes = base._nodes
35
+ self.graph = base.graph
36
+ # The worker must observe edges it has already emitted. Reading the
37
+ # parent's key set alone would emit duplicate dependency edges from
38
+ # two call sites in the same file and make `edges_added` inaccurate.
39
+ self._edge_keys = set(base._edge_keys)
40
+ self._occurrence_keys = set(base._occurrence_keys)
41
+ self.edges: List[GraphEdge] = []
42
+ self.occurrences: List[GraphEdge] = []
43
+
44
+ def _add_edge(self, edge: GraphEdge) -> None:
45
+ key = (edge.source, edge.target, edge.edge_type)
46
+ if key in self._edge_keys:
47
+ return
48
+ self._edge_keys.add(key)
49
+ self.edges.append(edge)
50
+
51
+ def _record_edge_occurrence(self, edge: GraphEdge) -> None:
52
+ key = (
53
+ edge.source, edge.target, edge.edge_type, edge.file_path,
54
+ edge.start_byte if edge.start_byte is not None else edge.line,
55
+ edge.end_byte if edge.end_byte is not None else edge.column,
56
+ )
57
+ if key in self._occurrence_keys:
58
+ return
59
+ self._occurrence_keys.add(key)
60
+ self.occurrences.append(edge)
61
+
62
+ def __getattr__(self, name: str):
63
+ return getattr(self._base, name)
64
+
65
+
66
+ def _edge_order(edge: GraphEdge) -> tuple:
67
+ return (
68
+ edge.file_path or "", edge.line if edge.line is not None else -1,
69
+ edge.column if edge.column is not None else -1,
70
+ edge.start_byte if edge.start_byte is not None else -1,
71
+ edge.end_byte if edge.end_byte is not None else -1,
72
+ edge.source, edge.target, edge.edge_type.name,
73
+ )
74
+
75
+
76
+ # Set immediately before forking. ParseResult/Tree objects are intentionally
77
+ # not sent through the process queue (they are not pickleable); Linux fork gives
78
+ # workers a copy-on-write read-only view of the completed resolution index.
79
+ _PROCESS_RESOLVER = None
80
+ _PROCESS_PARSES = None
81
+ _PROCESS_RESULTS = None
82
+ _PROCESS_FUNC_IDS = None
83
+
84
+ # A failed pool has not mutated the parent graph: workers return data only.
85
+ # Fall back to the existing sequential path rather than making an optional
86
+ # speed-up a new availability requirement.
87
+ _PROCESS_POOL_FAILURES = (BrokenProcessPool, OSError, RuntimeError)
88
+
89
+
90
+ def _resolve_one_forked(file_path: Path):
91
+ resolver = copy.copy(_PROCESS_RESOLVER)
92
+ collector = _ProcessEdgeCollector(_PROCESS_RESOLVER.builder)
93
+ resolver.builder = collector
94
+ resolver._method_cst_cache = {}
95
+ resolver._cst_index_cache = {}
96
+ resolver._method_facts_cache = {}
97
+ resolver._local_scopes_cache = {}
98
+ resolver._declaring_class_cache = {}
99
+ resolver._members_by_name_cache = {}
100
+ resolver._erased_signature_cache = {}
101
+ resolver._param_ctx_for_cache = {}
102
+ resolver._declared_type_memo = {}
103
+ resolver._class_ancestors_cache = {}
104
+ resolver._contains_children_cache = {}
105
+ resolver.fanout_telemetry = {
106
+ "mode": resolver._fanout_mode, "cap": resolver._fanout_cap,
107
+ "candidate_counts": [], "emitted_counts": [], "cap_skipped": 0,
108
+ }
109
+ parse = _PROCESS_PARSES[file_path]
110
+ result = _PROCESS_RESULTS.get(file_path)
111
+ if result is not None:
112
+ resolver._resolve_project_call_sites_sequential(
113
+ {file_path: parse}, {file_path: result}, _PROCESS_FUNC_IDS, 0,
114
+ )
115
+ return collector.edges, collector.occurrences, resolver.fanout_telemetry
116
+
117
+
118
+ class ResolutionOrchestrationMixin:
119
+ def _run_resolution_passes(
120
+ self,
121
+ parses: Dict[Path, ParseResult],
122
+ results: Dict[Path, AnalysisResult],
123
+ ) -> int:
124
+ """Emit CALLS (method_invocation) / INSTANTIATES
125
+ (object_creation_expression) edges for every call site.
126
+
127
+ No AWAITS (Java has no `await` expression) and no P1.8 REFERENCES
128
+ pass here — field/variable *reads* outside of a call are not part of
129
+ this issue's scope (P4.4 is call resolution; a Java REFERENCES pass
130
+ would be an additive follow-up, not required by PROBLEMS.md's "what
131
+ the fix looks like" list for this issue).
132
+ """
133
+ func_id_by_position, edges_added = self._build_resolution_indexes(
134
+ parses, results,
135
+ )
136
+ return self._resolve_project_call_sites(
137
+ parses, results, func_id_by_position, edges_added,
138
+ )
139
+
140
+ def _build_resolution_indexes(
141
+ self, parses: Dict[Path, ParseResult], results: Dict[Path, AnalysisResult],
142
+ ) -> Tuple[Dict[Tuple[str, int, int], str], int]:
143
+ # Per-run caches (see resolver.__init__): the graph's node set and
144
+ # CONTAINS edges are fixed for the whole run, the method-CST index
145
+ # keys on the tree object, and the declared-type memo is armed only
146
+ # at the end of this method once every index it reads exists.
147
+ self._method_cst_cache = {}
148
+ self._cst_index_cache = {}
149
+ self._method_facts_cache = {}
150
+ self._local_scopes_cache = {}
151
+ self._declaring_class_cache = {}
152
+ self._members_by_name_cache = {}
153
+ self._erased_signature_cache = {}
154
+ self._param_ctx_for_cache = {}
155
+ self._declared_type_memo = None
156
+ self._file_import_map_ids = frozenset()
157
+ by_module_symbol: Dict[str, Dict[str, str]] = defaultdict(dict)
158
+ field_types: Dict[str, Dict[str, str]] = defaultdict(dict)
159
+ field_nodes: Dict[str, Dict[str, str]] = defaultdict(dict)
160
+ # Keyed by (file, line, column) — not just (file, line). Two type
161
+ # declarations routinely start on the same *line* (`} class Box<T> {`
162
+ # closing a preceding class, or compact/generated/minified source),
163
+ # and a line-only key silently collides, dropping whichever
164
+ # declaration loses the last-write-wins race (F-02: `Box`'s own
165
+ # `<T>` type parameters attributed to an unrelated class sharing its
166
+ # line, breaking generic narrowing with no warning). Two
167
+ # declarations can never share a start *column* too, so the triple
168
+ # is collision-free.
169
+ class_id_by_position: Dict[Tuple[str, int, int], str] = {}
170
+ for nid, node in self.builder._nodes.items():
171
+ if node.language != "java":
172
+ continue
173
+ if node.node_type == NodeType.CLASS:
174
+ by_module_symbol[node.module][node.name] = nid
175
+ class_id_by_position[(node.file_path, node.line, node.column)] = nid
176
+ elif node.node_type == NodeType.VARIABLE and node.contained_in and node.signature:
177
+ class_id = self._declaring_class_id(nid)
178
+ if class_id is None:
179
+ continue
180
+ field_types[class_id][node.name] = node.signature
181
+ field_nodes[class_id][node.name] = nid
182
+ self._by_module_symbol = by_module_symbol
183
+ self._field_types = field_types
184
+ self._field_nodes = field_nodes
185
+ # P4.12 reverse hierarchy index, built off the now-complete node/edge
186
+ # graph. The `@Bean` factory list also needs `self._file_ctx` (a
187
+ # `@Bean` return type resolves against its declaring file's imports),
188
+ # so it is built lower down, once `file_ctx` exists.
189
+ self._subtypes = self._build_subtype_index()
190
+
191
+ param_type_lists: Dict[str, List[str]] = {}
192
+ # Same (file, line, column) reasoning as `class_id_by_position`
193
+ # above: two methods routinely share a line (`void a() {} void b()
194
+ # {}`), and a line-only key drops one of them from `_iter_method_cst`
195
+ # entirely — not a narrowing miss but the caller's whole call-site
196
+ # scan (and constructor/setter DI indexing) silently skipped.
197
+ func_id_by_position: Dict[Tuple[str, int, int], str] = {}
198
+ file_ctx: Dict[str, _ImportContext] = {}
199
+ static_wildcard_by_file: Dict[str, List[str]] = {}
200
+ for path, ar in results.items():
201
+ mod = self.builder._module_for_result(ar)
202
+ # Each candidate method's parameter types must resolve against
203
+ # *its own declaring file's* imports, not whichever call site
204
+ # happens to be resolving against it right now — see
205
+ # `_ImportContext`. Keyed by file path, not by `mod`: Java's
206
+ # module id is the declared `package`, so several files
207
+ # routinely share one module, each with its own import list.
208
+ file_ctx[str(path)] = _ImportContext(
209
+ mod,
210
+ self.builder._import_map_for(ar, mod),
211
+ self.builder._wildcard_packages_for(ar, mod),
212
+ )
213
+ static_wildcard_by_file[str(path)] = (
214
+ self.builder._static_wildcard_classes_for(ar, mod)
215
+ )
216
+ for sym in ar.symbols:
217
+ if sym.kind != "function":
218
+ continue
219
+ fid = self.builder._symbol_to_id(sym, mod)
220
+ param_type_lists[fid] = list(sym.param_types.values())
221
+ func_id_by_position[
222
+ (str(sym.file_path), sym.position.line, sym.position.column)
223
+ ] = fid
224
+ param_type_lists.update(self._ensure_generated_member_stubs())
225
+ self._param_type_lists = param_type_lists
226
+ self._class_id_by_position = class_id_by_position
227
+ # Bug group 4 fix: materialize the synthetic `<init>`/`<clinit>`
228
+ # FUNCTION each class needs *before* `_resolve_project_call_sites`
229
+ # runs, the same way `_ensure_generated_member_stubs` above
230
+ # materializes `values()`/`valueOf()`/record accessors — those are
231
+ # real call *targets* the extractor never emits a Symbol for; these
232
+ # are real call *sources* (a field initializer or `static { }` block
233
+ # invoking something) it never emits one for either, so without a
234
+ # matching FUNCTION node here `_resolve_initializer_call_sites`
235
+ # would have nothing to attribute the edge to.
236
+ self._synthesize_initializer_members(parses, class_id_by_position)
237
+ self._method_type_param_bounds = self._collect_method_type_param_bounds(
238
+ parses, func_id_by_position,
239
+ )
240
+ self._file_ctx = file_ctx
241
+ self._static_wildcard_by_file = static_wildcard_by_file
242
+ declared_ancestors: Dict[Tuple[str, EdgeType], List[str]] = defaultdict(list)
243
+ for path, ar in results.items():
244
+ ctx = file_ctx.get(str(path))
245
+ if ctx is None:
246
+ continue
247
+ for sym in ar.symbols:
248
+ if sym.kind != "class":
249
+ continue
250
+ class_id = class_id_by_position.get(
251
+ (str(sym.file_path), sym.position.line, sym.position.column)
252
+ )
253
+ if class_id is None:
254
+ continue
255
+ for edge_type, parent_names in (
256
+ (EdgeType.INHERITS, sym.superclasses),
257
+ (EdgeType.IMPLEMENTS, sym.implements),
258
+ ):
259
+ for parent_name in parent_names:
260
+ parent_id = self._resolve_declared_type(
261
+ parent_name, ctx.module, ctx.import_map,
262
+ ctx.wildcard_packages, class_id,
263
+ )
264
+ if parent_id and parent_id != class_id:
265
+ declared_ancestors[(class_id, edge_type)].append(parent_id)
266
+ self._declared_direct_ancestors = {
267
+ key: list(dict.fromkeys(values))
268
+ for key, values in declared_ancestors.items()
269
+ }
270
+ # `_class_ancestors`/`_contains_children`'s memoization caches must
271
+ # not outlive the graph state they were computed against (see
272
+ # resolution/overloads.py) — reset them here, alongside
273
+ # `_declared_direct_ancestors`, on every resolution pass. This runs
274
+ # after `_ensure_generated_member_stubs()` above, so the reset
275
+ # covers any CONTAINS edges that call added too.
276
+ self._class_ancestors_cache = {}
277
+ self._contains_children_cache = {}
278
+ # Rebuild from the exact resolver hierarchy (plus graph edges) now
279
+ # that the per-file import contexts exist.
280
+ self._subtypes = self._build_subtype_index()
281
+ self._local_annotation_type_names = self._collect_annotation_type_names(parses)
282
+ self._class_type_params = self._collect_class_type_params(
283
+ parses, class_id_by_position,
284
+ )
285
+ self._class_type_param_bounds = self._collect_class_type_param_bounds(
286
+ parses, class_id_by_position,
287
+ )
288
+ self._ancestor_type_args = self._collect_ancestor_type_args(
289
+ results, class_id_by_position, file_ctx,
290
+ )
291
+ self._field_allocation_types = self._build_field_allocation_index(
292
+ parses, func_id_by_position, class_id_by_position,
293
+ )
294
+ # `@Bean` factory beans (P4.12 DI): resolves each factory method's
295
+ # return type against its declaring file's imports, so it must run
296
+ # after `self._file_ctx` is populated above.
297
+ self._bean_method_candidates = self._build_bean_method_candidates()
298
+ self._di_injected_fields = {
299
+ **self._build_constructor_injection_index(
300
+ parses, results, func_id_by_position,
301
+ ),
302
+ **self._build_setter_injection_index(
303
+ parses, results, func_id_by_position,
304
+ ),
305
+ }
306
+ edge_count_before_callable_fields = len(self.builder._edges)
307
+ self._field_callable_targets = self._index_field_callable_targets(
308
+ parses, class_id_by_position, func_id_by_position,
309
+ )
310
+
311
+ self._file_import_map_ids = frozenset(
312
+ id(ctx.import_map) for ctx in self._file_ctx.values()
313
+ )
314
+ self._declared_type_memo = {}
315
+ return (
316
+ func_id_by_position,
317
+ len(self.builder._edges) - edge_count_before_callable_fields,
318
+ )
319
+
320
+ def _resolve_project_call_sites(
321
+ self, parses: Dict[Path, ParseResult], results: Dict[Path, AnalysisResult],
322
+ func_id_by_position: Dict[Tuple[str, int, int], str], edges_added: int,
323
+ ) -> int:
324
+ """Resolve calls, optionally using a Linux fork process pool.
325
+
326
+ This experimental path is opt-in because it relies on ``fork`` to
327
+ share the non-pickleable Tree-sitter parse trees copy-on-write. Each
328
+ process has private resolver caches and edge buffers; only this main
329
+ process changes the real graph after deterministic sorting.
330
+ """
331
+ if self._should_process_resolve(parses):
332
+ raw_workers = os.environ.get("DRIFT_JAVA_RESOLVE_WORKERS", "")
333
+ try:
334
+ workers = int(raw_workers) if raw_workers else min(os.cpu_count() or 1, 8)
335
+ except ValueError:
336
+ workers = min(os.cpu_count() or 1, 8)
337
+ workers = min(workers, len(parses))
338
+ if workers > 1:
339
+ global _PROCESS_RESOLVER, _PROCESS_PARSES, _PROCESS_RESULTS, _PROCESS_FUNC_IDS
340
+ _PROCESS_RESOLVER = self
341
+ _PROCESS_PARSES = parses
342
+ _PROCESS_RESULTS = results
343
+ _PROCESS_FUNC_IDS = func_id_by_position
344
+ try:
345
+ context = multiprocessing.get_context("fork")
346
+ with ProcessPoolExecutor(max_workers=workers, mp_context=context) as pool:
347
+ resolved = list(pool.map(_resolve_one_forked, sorted(parses)))
348
+ except _PROCESS_POOL_FAILURES as exc:
349
+ logger.warning(
350
+ "Java resolve process pool unavailable (%s: %s); "
351
+ "falling back to sequential resolution.",
352
+ type(exc).__name__, exc,
353
+ )
354
+ return self._resolve_project_call_sites_sequential(
355
+ parses, results, func_id_by_position, edges_added,
356
+ )
357
+ finally:
358
+ _PROCESS_RESOLVER = _PROCESS_PARSES = _PROCESS_RESULTS = _PROCESS_FUNC_IDS = None
359
+ for edges, occurrences, telemetry in resolved:
360
+ for edge in sorted(occurrences, key=_edge_order):
361
+ self.builder._record_edge_occurrence(edge)
362
+ for edge in sorted(edges, key=_edge_order):
363
+ key = (edge.source, edge.target, edge.edge_type)
364
+ if key in self.builder._edge_keys:
365
+ continue
366
+ self.builder._add_edge(edge)
367
+ # Workers mutate copy-on-write GraphNodes only. Keep
368
+ # the public node-level adjacency sets equivalent to
369
+ # the sequential resolver while the parent remains
370
+ # the sole writer of the real graph.
371
+ self.builder._nodes[edge.source].calls.add(edge.target)
372
+ self.builder._nodes[edge.target].called_by.add(edge.source)
373
+ self.fanout_telemetry["candidate_counts"].extend(telemetry["candidate_counts"])
374
+ self.fanout_telemetry["emitted_counts"].extend(telemetry["emitted_counts"])
375
+ self.fanout_telemetry["cap_skipped"] += telemetry["cap_skipped"]
376
+ return edges_added + sum(len(edges) for edges, _, _ in resolved)
377
+ return self._resolve_project_call_sites_sequential(
378
+ parses, results, func_id_by_position, edges_added,
379
+ )
380
+
381
+ @staticmethod
382
+ def _should_process_resolve(parses: Dict[Path, ParseResult]) -> bool:
383
+ """Whether the opt-in fork implementation is usable for this run.
384
+
385
+ `fork` is deliberately Linux-only. On spawn-only platforms a child
386
+ cannot inherit Tree-sitter trees or the resolver's read-only indexes,
387
+ and serialising that state is a different implementation strategy.
388
+ Never nest this pool inside another multiprocessing worker either.
389
+ """
390
+ return (
391
+ os.environ.get("DRIFT_JAVA_PROCESS_RESOLVE") == "1"
392
+ and sys.platform.startswith("linux")
393
+ and "fork" in multiprocessing.get_all_start_methods()
394
+ and multiprocessing.parent_process() is None
395
+ and len(parses) >= 12
396
+ )
397
+
398
+ def _resolve_project_call_sites_sequential(
399
+ self, parses: Dict[Path, ParseResult], results: Dict[Path, AnalysisResult],
400
+ func_id_by_position: Dict[Tuple[str, int, int], str], edges_added: int,
401
+ ) -> int:
402
+ for file_path, parse in parses.items():
403
+ result = results.get(file_path)
404
+ if result is None:
405
+ continue
406
+ module_name = self.builder._module_for_result(result)
407
+ caller_ctx = self._file_ctx[str(file_path)]
408
+ import_map = caller_ctx.import_map
409
+ wildcard_packages = caller_ctx.wildcard_packages
410
+ static_wildcard_classes = self._static_wildcard_by_file.get(str(file_path), [])
411
+ source = parse.source_bytes
412
+ fp_str = str(file_path)
413
+
414
+ for func_cst, caller_id in self._iter_method_cst(
415
+ parse.tree.root_node, fp_str, func_id_by_position
416
+ ):
417
+ if caller_id not in self.builder._nodes:
418
+ continue
419
+ caller_node = self.builder._nodes[caller_id]
420
+ enclosing_class_id = self._declaring_class_id(caller_id)
421
+ field_types_own = (
422
+ self._visible_bare_field_types(enclosing_class_id)
423
+ if enclosing_class_id else {}
424
+ )
425
+ body = func_cst.child_by_field_name("body")
426
+ if body is None:
427
+ continue
428
+
429
+ own_param_types = dict(
430
+ zip(
431
+ self._param_names(func_cst, source),
432
+ self._param_type_lists.get(caller_id, []),
433
+ )
434
+ )
435
+ # A method declared in an anonymous/local class may read an
436
+ # effectively-final parameter of its lexical enclosing method.
437
+ # Add only that enclosing method's declared parameters; its
438
+ # locals and fields remain governed by their ordinary scopes.
439
+ captured_param_types = self._captured_param_types(
440
+ func_cst, source, fp_str, func_id_by_position,
441
+ )
442
+ captured_param_types.update(own_param_types)
443
+ own_param_types = captured_param_types
444
+ # P4.12 generic narrowing: `<T extends StripeGateway>` binds a
445
+ # method type parameter to a concrete upper bound, so a receiver
446
+ # declared `T` resolves to that bound (narrowing the fan-out, or
447
+ # pinning it to one concrete class) instead of dropping to no
448
+ # edge at all (no project class is named `T`).
449
+ type_param_bounds = self._collect_type_param_bounds(func_cst, source)
450
+
451
+ edges_added = self._resolve_caller_body(
452
+ body, source, module_name, enclosing_class_id, import_map,
453
+ wildcard_packages, static_wildcard_classes, field_types_own,
454
+ own_param_types, type_param_bounds, caller_node, caller_id,
455
+ fp_str, edges_added,
456
+ )
457
+
458
+ edges_added = self._resolve_initializer_call_sites(
459
+ parse, source, module_name, fp_str, func_id_by_position,
460
+ import_map, wildcard_packages, static_wildcard_classes, edges_added,
461
+ )
462
+ return edges_added
463
+
464
+ def _resolve_caller_body(
465
+ self, body, source: bytes, module_name: str, enclosing_class_id,
466
+ import_map, wildcard_packages, static_wildcard_classes, field_types_own,
467
+ own_param_types, type_param_bounds, caller_node, caller_id: str,
468
+ fp_str: str, edges_added: int,
469
+ ) -> int:
470
+ """Shared per-caller resolution tail: scope inference over `body`
471
+ (a method/constructor's `block`, or — for a synthetic `<init>`/
472
+ `<clinit>` caller, see `_resolve_initializer_call_sites` — a bare
473
+ field-initializer expression or a `static`/instance-initializer
474
+ `block`) followed by method-reference and call-site resolution.
475
+ Factored out of `_resolve_project_call_sites` so both real methods
476
+ and synthetic initializer callers run through exactly one call
477
+ resolution path (PROBLEMS.md bug group 4)."""
478
+ local_types = self._collect_local_scopes(
479
+ body, source, field_types=field_types_own,
480
+ param_types=own_param_types,
481
+ )
482
+ self._collect_var_method_call_scopes(
483
+ body, source, module_name, enclosing_class_id, import_map,
484
+ wildcard_packages, local_types, own_param_types,
485
+ type_param_bounds,
486
+ )
487
+ self._collect_lambda_parameter_scopes(
488
+ body, source, module_name, enclosing_class_id, import_map,
489
+ wildcard_packages, local_types, own_param_types,
490
+ type_param_bounds, static_wildcard_classes,
491
+ )
492
+
493
+ # P4.13 target typing augments the existing receiver scopes:
494
+ # preserve the current branch's var/lambda inference, then add
495
+ # SAM-derived parameter scopes used by functional references.
496
+ self._add_lambda_param_scopes(
497
+ body, source, module_name, import_map, wildcard_packages,
498
+ local_types, own_param_types, field_types_own, caller_node,
499
+ )
500
+
501
+ resolved_refs, edges_added = self._resolve_method_reference_pass(
502
+ body, source, module_name, enclosing_class_id, import_map,
503
+ wildcard_packages, local_types, own_param_types, field_types_own,
504
+ caller_node, caller_id, fp_str, edges_added,
505
+ )
506
+
507
+ local_callable_targets = self._local_callable_targets(
508
+ body, source, resolved_refs,
509
+ )
510
+
511
+ return self._resolve_call_site_pass(
512
+ body, source, module_name, enclosing_class_id, import_map,
513
+ wildcard_packages, static_wildcard_classes, local_types,
514
+ own_param_types, type_param_bounds, local_callable_targets,
515
+ caller_node, caller_id, fp_str, edges_added,
516
+ )
517
+
518
+ # ------------------------------------------------------------ bug group 4:
519
+ # field initializers / static+instance initializer blocks
520
+
521
+ def _iter_initializer_sites(
522
+ self, root, file_path_str: str, source: bytes, extractor,
523
+ ):
524
+ """Yield `(owner_node, body_node, kind)` for every field initializer
525
+ expression, `static { }` block, and bare instance-initializer `{ }`
526
+ block anywhere in the file — the three CST shapes JLS 8.6/8.7 fold
527
+ into a class's implicit instance-initializer (`kind="init"`) or
528
+ class-initializer (`kind="clinit"`). `owner_node` is the enclosing
529
+ declaration (`field_declaration`/`static_initializer`/the block
530
+ itself) used to find the nearest enclosing class and to anchor
531
+ `_captured_param_types`; `body_node` is what actually gets walked
532
+ for call sites (a field's initializer *expression*, not the whole
533
+ `field_declaration`, so a multi-declarator `int a = f(), b = g();`
534
+ yields one entry per initializer).
535
+
536
+ Deliberately not gated by `_NESTED_SCOPE_BOUNDARY`: unlike
537
+ `_collect_call_sites` (which walks *within* an already-found
538
+ caller's body and must stop at a nested method/class), this is a
539
+ fresh top-level scan for these three node types anywhere in the
540
+ file, including inside local/anonymous classes — exactly how
541
+ `JavaSymbolExtractor._build`'s own top-level `_walk` finds nested
542
+ `field_declaration`s today.
543
+ """
544
+ for node in self._initializer_declarations(root, file_path_str):
545
+ if node.type == "field_declaration":
546
+ is_static = "static" in extractor._extract_modifiers(node, source)
547
+ kind = "clinit" if is_static else "init"
548
+ for child in node.children:
549
+ if child.type != "variable_declarator":
550
+ continue
551
+ value = child.child_by_field_name("value")
552
+ if value is not None:
553
+ yield node, value, kind
554
+ elif node.type == "static_initializer":
555
+ block = next((c for c in node.children if c.type == "block"), None)
556
+ if block is not None:
557
+ yield node, block, "clinit"
558
+ elif node.type == "block" and node.parent is not None \
559
+ and node.parent.type in _INIT_BLOCK_PARENTS:
560
+ yield node, node, "init"
561
+
562
+ @staticmethod
563
+ def _nearest_enclosing_class_cst(node):
564
+ """Nearest enclosing class-like CST node (real declaration, or an
565
+ anonymous class's own `class_body`) — the node-object counterpart of
566
+ `JavaSymbolExtractor._find_parent_class`, which returns only the
567
+ *name*. Needed here to key into `class_id_by_position` (itself keyed
568
+ by the exact CST node `JavaSymbolExtractor` positioned the CLASS
569
+ Symbol on) rather than a second, driftable name-based lookup."""
570
+ p = node.parent
571
+ while p is not None:
572
+ if p.type in _ENCLOSING_TYPE_CST_TYPES:
573
+ return p
574
+ if p.type == "class_body" and p.parent is not None \
575
+ and p.parent.type in ("object_creation_expression", "enum_constant"):
576
+ return p
577
+ p = p.parent
578
+ return None
579
+
580
+ def _ensure_initializer_member_stub(self, class_node, kind: str) -> str:
581
+ """Materialize the synthetic `<init>`/`<clinit>` FUNCTION `class_node`
582
+ needs, mirroring `GeneratedMembersMixin._add_generated_member_stub`'s
583
+ direct-`GraphNode` pattern (no CST node backs either, so there is no
584
+ `Symbol` to route through `JavaSymbolExtractor`). `<`/`>` are illegal
585
+ in a Java identifier, so `f"{class_node.id}.<init>"` can never collide
586
+ with a real member's id (a no-arg constructor's own id is
587
+ `f"{class_node.id}.()"`, never `.{name}"` for any declared name)."""
588
+ name = "<clinit>" if kind == "clinit" else "<init>"
589
+ member_id = f"{class_node.id}.{name}"
590
+ if member_id not in self.builder._nodes:
591
+ self.builder._add_node(GraphNode(
592
+ id=member_id,
593
+ name=name,
594
+ node_type=NodeType.FUNCTION,
595
+ module=class_node.module,
596
+ file_path=class_node.file_path,
597
+ line=class_node.line,
598
+ column=class_node.column,
599
+ contained_in=class_node.name,
600
+ language="java",
601
+ metadata={
602
+ "java_is_static": kind == "clinit",
603
+ "java_is_abstract": False,
604
+ "synthetic": "class_initializer" if kind == "clinit" else "instance_initializer",
605
+ },
606
+ ))
607
+ self.builder._add_edge(GraphEdge(
608
+ source=class_node.id,
609
+ target=member_id,
610
+ edge_type=EdgeType.CONTAINS,
611
+ file_path=class_node.file_path,
612
+ line=class_node.line,
613
+ column=class_node.column,
614
+ strategy="java_resolver",
615
+ ))
616
+ return member_id
617
+
618
+ def _synthesize_initializer_members(
619
+ self, parses: Dict[Path, ParseResult],
620
+ class_id_by_position: Dict[Tuple[str, int, int], str],
621
+ ) -> None:
622
+ extractor = self._method_extractor()
623
+ for file_path, parse in parses.items():
624
+ source = parse.source_bytes
625
+ fp_str = str(file_path)
626
+ for owner_node, _body, kind in self._iter_initializer_sites(
627
+ parse.tree.root_node, fp_str, source, extractor,
628
+ ):
629
+ class_cst = self._nearest_enclosing_class_cst(owner_node)
630
+ if class_cst is None:
631
+ continue
632
+ class_id = class_id_by_position.get(
633
+ (fp_str, class_cst.start_point[0] + 1, class_cst.start_point[1]),
634
+ )
635
+ if class_id is None or class_id not in self.builder._nodes:
636
+ continue
637
+ self._ensure_initializer_member_stub(
638
+ self.builder._nodes[class_id], kind,
639
+ )
640
+
641
+ def _resolve_initializer_call_sites(
642
+ self, parse: ParseResult, source: bytes, module_name: str, fp_str: str,
643
+ func_id_by_position: Dict[Tuple[str, int, int], str],
644
+ import_map, wildcard_packages, static_wildcard_classes,
645
+ edges_added: int,
646
+ ) -> int:
647
+ """Route call sites found inside field initializers / static+instance
648
+ initializer blocks to the synthetic `<init>`/`<clinit>` FUNCTION
649
+ `_synthesize_initializer_members` already created for their class —
650
+ the fix for PROBLEMS.md bug group 4 (these call sites previously had
651
+ no caller CST node `_iter_method_cst` could ever yield, so the edge
652
+ was silently dropped)."""
653
+ extractor = self._method_extractor()
654
+ for owner_node, body, kind in self._iter_initializer_sites(
655
+ parse.tree.root_node, fp_str, source, extractor,
656
+ ):
657
+ class_cst = self._nearest_enclosing_class_cst(owner_node)
658
+ if class_cst is None:
659
+ continue
660
+ class_id = self._class_id_by_position.get(
661
+ (fp_str, class_cst.start_point[0] + 1, class_cst.start_point[1]),
662
+ )
663
+ if class_id is None:
664
+ continue
665
+ name = "<clinit>" if kind == "clinit" else "<init>"
666
+ caller_id = f"{class_id}.{name}"
667
+ if caller_id not in self.builder._nodes:
668
+ continue
669
+ caller_node = self.builder._nodes[caller_id]
670
+ field_types_own = self._visible_bare_field_types(class_id)
671
+ # Neither a field initializer nor a static/instance-initializer
672
+ # block declares parameters of its own; an anonymous/local
673
+ # class's initializer may still read an effectively-final local
674
+ # of its lexically enclosing method, same as a real method there
675
+ # would (`_captured_param_types` walks up from `owner_node`
676
+ # exactly as it would from that method's own CST node).
677
+ own_param_types = self._captured_param_types(
678
+ owner_node, source, fp_str, func_id_by_position,
679
+ )
680
+ edges_added = self._resolve_caller_body(
681
+ body, source, module_name, class_id, import_map,
682
+ wildcard_packages, static_wildcard_classes, field_types_own,
683
+ own_param_types, {}, caller_node, caller_id, fp_str, edges_added,
684
+ )
685
+ return edges_added
686
+
687
+ def _captured_param_types(
688
+ self, func_cst, source: bytes, file_path: str,
689
+ func_id_by_position: Dict[Tuple[str, int, int], str],
690
+ ) -> Dict[str, str]:
691
+ """Declared parameters captured from the nearest lexical method.
692
+
693
+ Java permits an anonymous or local class to use effectively-final
694
+ parameters from the method that contains its declaration. A member
695
+ class has no enclosing method in its CST ancestry, so this contributes
696
+ no facts there. The current method's parameters are overlaid by the
697
+ caller and therefore correctly shadow captured names.
698
+ """
699
+ parent = func_cst.parent
700
+ while parent is not None:
701
+ if parent.type in (
702
+ "method_declaration", "constructor_declaration",
703
+ "compact_constructor_declaration",
704
+ ):
705
+ outer_id = func_id_by_position.get(
706
+ (file_path, parent.start_point[0] + 1, parent.start_point[1]),
707
+ )
708
+ if outer_id is None:
709
+ return {}
710
+ return dict(zip(
711
+ self._param_names(parent, source),
712
+ self._param_type_lists.get(outer_id, []),
713
+ ))
714
+ parent = parent.parent
715
+ return {}
716
+
717
+ def _resolve_method_reference_pass(
718
+ self, body, source: bytes, module_name: str, enclosing_class_id,
719
+ import_map, wildcard_packages, local_types, own_param_types,
720
+ field_types_own, caller_node, caller_id: str, fp_str: str, edges_added: int,
721
+ ):
722
+ resolved_refs: Dict[int, Set[str]] = {}
723
+ for ref_node in self._collect_method_references(body):
724
+ target_id, confidence, ref_etype, ref_meta = self._resolve_method_reference(
725
+ ref_node, source, module_name, enclosing_class_id,
726
+ import_map, wildcard_packages, local_types,
727
+ own_param_types, field_types_own, caller_node,
728
+ )
729
+ if not target_id:
730
+ caller_node.metadata.setdefault(
731
+ "unresolved_method_references", []
732
+ ).append({
733
+ "text": self._text(ref_node, source),
734
+ "line": ref_node.start_point[0] + 1,
735
+ "column": ref_node.start_point[1],
736
+ })
737
+ continue
738
+ targets = target_id if isinstance(target_id, list) else [target_id]
739
+ resolved_refs[ref_node.start_byte] = set(targets)
740
+ for tid in targets:
741
+ # A recursive reference (`this::walk` inside `walk`,
742
+ # `Node::contains` inside `contains`) still gets its
743
+ # CALLABLE_REF/REFERENCES edge, exactly as a recursive bare
744
+ # call already does.
745
+ if tid not in self.builder._nodes:
746
+ continue
747
+ edge = GraphEdge(
748
+ source=caller_id, target=tid, edge_type=ref_etype,
749
+ file_path=fp_str, line=ref_node.start_point[0] + 1,
750
+ column=ref_node.start_point[1],
751
+ start_byte=ref_node.start_byte, end_byte=ref_node.end_byte,
752
+ confidence=float(confidence), metadata=dict(ref_meta),
753
+ strategy="java_resolver",
754
+ )
755
+ self.builder._record_edge_occurrence(edge)
756
+ if (caller_id, tid, ref_etype) in self.builder._edge_keys:
757
+ continue
758
+ self.builder._add_edge(edge)
759
+ # Backward-compatible semantic view: pre-P4.13
760
+ # consumers query method references as REFERENCES,
761
+ # while the richer CALLABLE_REF edge carries SAM
762
+ # target-typing metadata for newer consumers.
763
+ if ref_etype == EdgeType.CALLABLE_REF:
764
+ compat_edge = GraphEdge(
765
+ source=caller_id, target=tid,
766
+ edge_type=EdgeType.REFERENCES,
767
+ file_path=fp_str,
768
+ line=ref_node.start_point[0] + 1,
769
+ column=ref_node.start_point[1],
770
+ start_byte=ref_node.start_byte,
771
+ end_byte=ref_node.end_byte,
772
+ confidence=float(confidence),
773
+ metadata=dict(ref_meta),
774
+ strategy="java_resolver",
775
+ )
776
+ self.builder._record_edge_occurrence(compat_edge)
777
+ compat_key = (caller_id, tid, EdgeType.REFERENCES)
778
+ if compat_key not in self.builder._edge_keys:
779
+ self.builder._add_edge(compat_edge)
780
+ # `.calls`/`.called_by` model actual invocation (mirrored by
781
+ # memory.py's persistence reload, which only repopulates
782
+ # them from EdgeType.CALLS rows). A method reference
783
+ # (`Type::method`) never invokes anything by itself, so it
784
+ # must not inflate caller/callee bookkeeping here — the
785
+ # CALLABLE_REF/REFERENCES edges above already make it
786
+ # visible to blast-radius/impact analysis via
787
+ # IMPACT_EDGE_TYPES.
788
+ edges_added += 1
789
+
790
+ return resolved_refs, edges_added
791
+
792
+
793
+ def _resolve_call_site_pass(
794
+ self, body, source: bytes, module_name: str, enclosing_class_id,
795
+ import_map, wildcard_packages, static_wildcard_classes, local_types,
796
+ own_param_types, type_param_bounds, local_callable_targets, caller_node,
797
+ caller_id: str, fp_str: str, edges_added: int,
798
+ ) -> int:
799
+ for call_node, call_kind in self._collect_call_sites(body):
800
+ if call_kind != "method":
801
+ resolver = (
802
+ self._resolve_explicit_constructor
803
+ if call_kind == "explicit_constructor"
804
+ else self._resolve_constructor
805
+ )
806
+ target_id, confidence, constructor_signature = resolver(
807
+ call_node, source, module_name, import_map, wildcard_packages,
808
+ local_types, own_param_types, enclosing_class_id,
809
+ )
810
+ etype = EdgeType.INSTANTIATES
811
+ edge_meta: Dict[str, object] = {
812
+ "constructor_signature": constructor_signature,
813
+ }
814
+ else:
815
+ indirect = self._resolve_callable_container_dispatch(
816
+ call_node, source, enclosing_class_id,
817
+ local_callable_targets, local_types, own_param_types,
818
+ )
819
+ if indirect:
820
+ target_id, confidence, edge_meta = indirect
821
+ else:
822
+ target_id, confidence, edge_meta = self._resolve_call(
823
+ call_node, source, module_name, enclosing_class_id,
824
+ import_map, wildcard_packages, local_types, own_param_types,
825
+ static_wildcard_classes, type_param_bounds,
826
+ )
827
+ etype = EdgeType.CALLS
828
+ if not target_id:
829
+ continue
830
+ targets = target_id if isinstance(target_id, list) else [target_id]
831
+ target_overrides = edge_meta.get("_target_overrides", {})
832
+ shared_edge_meta = {
833
+ key: value for key, value in edge_meta.items()
834
+ if key != "_target_overrides"
835
+ }
836
+ for tid in targets:
837
+ if tid not in self.builder._nodes:
838
+ continue
839
+ override = (
840
+ target_overrides.get(tid, {})
841
+ if isinstance(target_overrides, dict) else {}
842
+ )
843
+ target_confidence = override.get("confidence", confidence)
844
+ target_metadata = override.get("metadata", shared_edge_meta)
845
+ edge = GraphEdge(
846
+ source=caller_id,
847
+ target=tid,
848
+ edge_type=etype,
849
+ file_path=fp_str,
850
+ line=call_node.start_point[0] + 1,
851
+ column=call_node.start_point[1],
852
+ start_byte=call_node.start_byte,
853
+ end_byte=call_node.end_byte,
854
+ confidence=float(target_confidence),
855
+ metadata=dict(target_metadata) if target_metadata else {},
856
+ strategy="java_resolver",
857
+ )
858
+ self.builder._record_edge_occurrence(edge)
859
+ key = (caller_id, tid, etype)
860
+ if key in self.builder._edge_keys:
861
+ continue
862
+ self.builder._add_edge(edge)
863
+ caller_node.calls.add(tid)
864
+ self.builder._nodes[tid].called_by.add(caller_id)
865
+ edges_added += 1
866
+ return edges_added