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,278 @@
1
+ """Java object and explicit-constructor resolution."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Dict, List, Optional, Tuple
6
+
7
+ from adl_drift.graph.edges import EdgeType
8
+ from adl_drift.lang.java.ids import signature_key
9
+ from adl_drift.lang.java.resolution.constants import argument_nodes
10
+ from adl_drift.lang.java.resolution.model import (
11
+ ImportContext as _ImportContext,
12
+ LocalScopes as _LocalScopes,
13
+ )
14
+
15
+
16
+ class ConstructorResolutionMixin:
17
+ def _resolve_constructor(
18
+ self, call_node, source: bytes, module_name: str, import_map: Dict[str, str],
19
+ wildcard_packages: List[str],
20
+ local_types: "_LocalScopes", param_types: Dict[str, str],
21
+ enclosing_class_id: Optional[str],
22
+ ) -> Tuple[Optional[str], float, Optional[str]]:
23
+ """Resolve an `object_creation_expression` to `(class_id,
24
+ confidence)`. Always targets the CLASS node — mirrors Python's
25
+ `ClassName(...)` -> CLASS-node INSTANTIATES semantics (P2.8) and the
26
+ existing `_impact_in_edges` fold of constructor callers onto the
27
+ class. Overload disambiguation among multiple declared constructors
28
+ only ever *adjusts confidence*; it never fans out to per-constructor
29
+ targets, since there is nothing else to point INSTANTIATES at
30
+ (a documented simplification vs. plain method overloading — see the
31
+ Fix Applied notes).
32
+
33
+ `local_types`/`param_types`/`enclosing_class_id` (the calling
34
+ method's own locals, parameters, and containing class) feed argument
35
+ type inference exactly like `_resolve_call` does for method calls —
36
+ `new Box(value)` must be able to read `value`'s declared type the
37
+ same way `repo.save(value)` can (P4.4 regression fix: this used to
38
+ call `_infer_arg_types(..., {}, {}, {})` unconditionally, so a
39
+ constructor argument's type was *always* treated as unknown even
40
+ when statically known, which both under-confidently fanned out an
41
+ otherwise-unambiguous call and — worse — let a sole arity-matching
42
+ but type-*incompatible* constructor resolve unconditionally at full
43
+ confidence, since `_select_overload`'s `arg_types is None` branch
44
+ never runs a type check against a single survivor)."""
45
+ type_node = call_node.child_by_field_name("type")
46
+ if type_node is None:
47
+ return None, 0.0, None
48
+ cls_id = self._resolve_declared_type(
49
+ self._text(type_node, source), module_name, import_map, wildcard_packages,
50
+ enclosing_class_id,
51
+ )
52
+ if cls_id is None:
53
+ return None, 0.0, None
54
+ cls_node = self.builder._nodes[cls_id]
55
+ type_kind = cls_node.metadata.get("java_type_kind")
56
+ if type_kind in ("interface", "enum"):
57
+ # Interfaces can't be instantiated with `new`, and enum instances
58
+ # can only come from their declared constants — neither has any
59
+ # constructor a call site could reach.
60
+ return None, 0.0, None
61
+ ctors = self._members_named(
62
+ cls_id, cls_node.name, search_ancestors=False,
63
+ caller_class_id=enclosing_class_id,
64
+ )
65
+ args_node = call_node.child_by_field_name("arguments")
66
+ arg_count = self._count_args(args_node)
67
+ field_types = (
68
+ self._all_field_types(enclosing_class_id) if enclosing_class_id else {}
69
+ )
70
+ arg_types = self._infer_arg_types(
71
+ args_node, source, local_types, param_types, field_types,
72
+ )
73
+ if not ctors:
74
+ # No explicit constructor: the compiler-generated implicit one
75
+ # takes zero arguments for a plain class — a call site with any
76
+ # arguments cannot actually reach it. `record` is the one
77
+ # exception: its implicit canonical constructor takes one arg
78
+ # per component, not zero, so check against the component arity
79
+ # extracted by JavaSymbolExtractor instead.
80
+ if type_kind == "record":
81
+ component_types = cls_node.metadata.get("record_component_types")
82
+ if component_types is not None and self._record_canonical_matches(
83
+ component_types, arg_count, arg_types, _ImportContext(
84
+ module_name, import_map, wildcard_packages,
85
+ ),
86
+ ):
87
+ return cls_id, 0.95, self._record_canonical_signature(component_types)
88
+ return None, 0.0, None
89
+ if arg_count == 0:
90
+ return cls_id, 0.95, "()"
91
+ return None, 0.0, None
92
+ # `this` has the statically known enclosing-class type, but the
93
+ # generic argument inferencer intentionally has no enclosing-class
94
+ # parameter. Supply that one exact fact here so constructor overloads
95
+ # such as `new ToStringBuilder(this)` select `(Object)` rather than
96
+ # retaining an ambiguous candidate set with no constructor signature.
97
+ args = argument_nodes(args_node)
98
+ if (
99
+ arg_types is None and len(args) == 1 and args[0].type == "this"
100
+ and enclosing_class_id is not None and ":" in enclosing_class_id
101
+ ):
102
+ owner_module, owner_qualname = enclosing_class_id.rsplit(":", 1)
103
+ arg_types = [
104
+ f"{self.builder._display_module(owner_module)}.{owner_qualname}"
105
+ ]
106
+ if arg_types is None and len(ctors) > 1:
107
+ arg_types = self._infer_contextual_arg_types(
108
+ args_node, source, module_name, enclosing_class_id, import_map,
109
+ wildcard_packages, local_types, param_types, field_types, {},
110
+ )
111
+ caller_ctx = _ImportContext(
112
+ module_name, import_map, wildcard_packages, enclosing_class_id,
113
+ )
114
+ target, conf = self._select_overload(
115
+ ctors, arg_count, arg_types, lambda _c: 0.95, caller_ctx,
116
+ )
117
+ if target is None:
118
+ # Every record has a canonical constructor for its components,
119
+ # even when the source also declares auxiliary constructors. A
120
+ # `record Key(Object event, Id id) { Key(Publication p) {...} }`
121
+ # therefore has a real two-argument construction path although
122
+ # the extracted member list contains only the auxiliary
123
+ # one-argument declaration. Only recover that implicit path
124
+ # when no declared constructor has the requested arity: if a
125
+ # declared same-arity candidate was rejected for incompatible
126
+ # types, preserving that rejection avoids manufacturing an edge.
127
+ component_types = cls_node.metadata.get("record_component_types")
128
+ if (
129
+ type_kind == "record"
130
+ and component_types is not None
131
+ and self._record_canonical_matches(
132
+ component_types, arg_count, arg_types, caller_ctx,
133
+ )
134
+ and not any(self._arity_matches(ctor, arg_count) for ctor in ctors)
135
+ ):
136
+ return cls_id, 0.95, self._record_canonical_signature(component_types)
137
+ # No declared constructor matches this call's arg count — an
138
+ # INSTANTIATES edge here would point at a constructor the call
139
+ # site cannot actually reach.
140
+ return None, 0.0, None
141
+ selected = target[0] if isinstance(target, list) and len(target) == 1 else target
142
+ constructor_signature = (
143
+ signature_key(self._param_type_lists.get(selected, []))
144
+ if isinstance(selected, str) else None
145
+ )
146
+ return cls_id, (0.6 if isinstance(target, list) else conf), constructor_signature
147
+
148
+ def _record_canonical_matches(
149
+ self, component_types: List[str], arg_count: int,
150
+ arg_types: Optional[List[str]], arg_ctx: _ImportContext,
151
+ ) -> bool:
152
+ """Whether a record's implicit canonical constructor accepts a call.
153
+
154
+ JLS 8.10.4 gives the canonical constructor the record components'
155
+ parameter list verbatim. Consequently, a final ``T...`` component is
156
+ a real variable-arity parameter, not one array parameter that happens
157
+ to use an ellipsis in the header. Records do not materialize that
158
+ implicit constructor as a function node, so apply the same bounded
159
+ arity and compatibility rules used for an extracted vararg member.
160
+ """
161
+ if not component_types:
162
+ return arg_count == 0
163
+ is_vararg = self._is_vararg_param(component_types[-1])
164
+ fixed_count = len(component_types) - 1 if is_vararg else len(component_types)
165
+ if arg_count < fixed_count or (not is_vararg and arg_count != fixed_count):
166
+ return False
167
+ # Preserve the established implicit-record behavior for ordinary
168
+ # canonical constructors. This batch only changes the JLS variable-
169
+ # arity form; widening the new argument-type filter to every record
170
+ # would reject existing callers whose source type spelling cannot be
171
+ # fully canonicalized in a multi-module graph.
172
+ if not is_vararg:
173
+ return True
174
+ if arg_types is None:
175
+ return True
176
+ if len(arg_types) != arg_count:
177
+ return False
178
+ # The record header is declared in the class's own source file.
179
+ # There is no function node from which `_param_ctx_for()` could read
180
+ # that context, but source and call contexts are identical for this
181
+ # implicit member.
182
+ if not all(
183
+ self._types_compatible(param, arg, arg_ctx, arg_ctx)
184
+ for param, arg in zip(component_types[:fixed_count], arg_types[:fixed_count])
185
+ ):
186
+ return False
187
+ trailing = arg_types[fixed_count:]
188
+ array_type = component_types[-1].replace("...", "[]")
189
+ if len(trailing) == 1 and self._types_compatible(
190
+ array_type, trailing[0], arg_ctx, arg_ctx,
191
+ ):
192
+ return True
193
+ component_type = self._vararg_component_type(array_type)
194
+ return all(
195
+ self._types_compatible(component_type, arg, arg_ctx, arg_ctx)
196
+ for arg in trailing
197
+ )
198
+
199
+ @staticmethod
200
+ def _record_canonical_signature(component_types: List[str]) -> str:
201
+ """Metric identity for a record's implicit canonical constructor.
202
+
203
+ The source-level JavaParser oracle represents a canonical varargs
204
+ component as its element type (``Object``), while ordinary declared
205
+ constructor symbols retain erased array identity (``Object[]``).
206
+ Keep that oracle-compatible projection confined to the implicit
207
+ record member; overload identity for real declared constructors is
208
+ unchanged.
209
+ """
210
+ if component_types and component_types[-1].strip().endswith("..."):
211
+ return signature_key([*component_types[:-1], component_types[-1].replace("...", "")])
212
+ return signature_key(component_types)
213
+
214
+ def _resolve_explicit_constructor(
215
+ self, call_node, source: bytes, module_name: str, import_map: Dict[str, str],
216
+ wildcard_packages: List[str], local_types: "_LocalScopes",
217
+ param_types: Dict[str, str], enclosing_class_id: Optional[str],
218
+ ) -> Tuple[Optional[str], float, Optional[str]]:
219
+ """Resolve ``this(...)``/``super(...)`` to the constructed class node."""
220
+ if enclosing_class_id is None:
221
+ return None, 0.0, None
222
+ keyword = self._text(call_node.named_children[0], source)
223
+ if keyword == "this":
224
+ cls_id = enclosing_class_id
225
+ elif keyword == "super":
226
+ parents = self._declared_direct_ancestors.get(
227
+ (enclosing_class_id, EdgeType.INHERITS), []
228
+ )
229
+ if not parents:
230
+ return None, 0.0, None
231
+ cls_id = parents[0]
232
+ else:
233
+ return None, 0.0, None
234
+
235
+ cls_node = self.builder._nodes.get(cls_id)
236
+ if cls_node is None:
237
+ return None, 0.0, None
238
+ ctors = self._members_named(
239
+ cls_id, cls_node.name, search_ancestors=False,
240
+ caller_class_id=enclosing_class_id,
241
+ )
242
+ args_node = next(
243
+ (child for child in call_node.named_children if child.type == "argument_list"),
244
+ None,
245
+ )
246
+ arg_count = self._count_args(args_node)
247
+ if not ctors:
248
+ return (cls_id, 0.95, "()") if arg_count == 0 else (None, 0.0, None)
249
+ arg_types = self._infer_arg_types(
250
+ args_node, source, local_types, param_types,
251
+ self._all_field_types(enclosing_class_id),
252
+ )
253
+ if arg_types is None and len(ctors) > 1:
254
+ arg_types = self._infer_contextual_arg_types(
255
+ args_node, source, module_name, enclosing_class_id, import_map,
256
+ wildcard_packages, local_types, param_types,
257
+ self._all_field_types(enclosing_class_id), {},
258
+ )
259
+ target, confidence = self._select_overload(
260
+ ctors, arg_count, arg_types, lambda _candidate: 0.95,
261
+ _ImportContext(
262
+ module_name, import_map, wildcard_packages, enclosing_class_id,
263
+ ),
264
+ )
265
+ if target is None:
266
+ return None, 0.0, None
267
+ # Unlike `_resolve_constructor`, `this()`/`super()`'s target class is
268
+ # never in doubt (it's always the current class or its direct
269
+ # ancestor) — an ambiguous overload pick (`target` is a list of
270
+ # arity-matching candidates whose arg types weren't statically known)
271
+ # still means the class itself is reached, just at reduced
272
+ # confidence and with no single constructor signature to report.
273
+ selected = target[0] if isinstance(target, list) and len(target) == 1 else target
274
+ constructor_signature = (
275
+ signature_key(self._param_type_lists.get(selected, []))
276
+ if isinstance(selected, str) else None
277
+ )
278
+ return cls_id, (0.6 if isinstance(target, list) else confidence), constructor_signature
@@ -0,0 +1,305 @@
1
+ """CST collection and lightweight indexes for Java resolution."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections import defaultdict
6
+ from pathlib import Path
7
+ from typing import Dict, List, Set, Tuple
8
+
9
+ from adl_drift.graph.node import NodeType
10
+ from adl_drift.lang.base import ParseResult
11
+ from adl_drift.lang.java.ids import DEFAULT_PACKAGE
12
+ from adl_drift.lang.java.resolution.constants import (
13
+ _ENCLOSING_TYPE_CST_TYPES,
14
+ _INIT_BLOCK_PARENTS,
15
+ _METHOD_CST_TYPES,
16
+ _NESTED_SCOPE_BOUNDARY,
17
+ )
18
+ from adl_drift.lang.java.resolution.model import MethodFacts
19
+ from adl_drift.utils.types import AnalysisResult
20
+
21
+
22
+ def _node_text(node, source: bytes) -> str:
23
+ return source[node.start_byte:node.end_byte].decode("utf-8", errors="replace")
24
+
25
+
26
+ class ContextResolutionMixin:
27
+ def _collect_annotation_type_names(
28
+ self, parses: Dict[Path, ParseResult]
29
+ ) -> Dict[str, Set[str]]:
30
+ """Simple name -> FQNs of every `@interface Foo {}` (`annotation_
31
+ type_declaration`) declared across the analyzed source — used by
32
+ `_annotation_stub_match` to tell a project's own annotation apart
33
+ from an (always-external) Lombok one. Read straight from the CST
34
+ because `JavaSymbolExtractor` does not emit a graph node for an
35
+ annotation type (it is not in `_CLASS_TYPES`), so there is no
36
+ `_by_module_symbol` entry to consult.
37
+
38
+ FQN = declaring file's package (via `self._file_ctx`, already built
39
+ by the time `resolve_calls()` calls this) + every enclosing
40
+ class/interface/enum/record/annotation-type name (outermost first,
41
+ via `_enclosing_type_chain`) + simple name, or just that chain for a
42
+ default-package file — mirrors how Java itself scopes a (possibly
43
+ nested) `@interface` declaration: `class Marks { @interface Getter
44
+ {} }` in package `app` canonically names as `app.Marks.Getter`,
45
+ never `app.Getter` (F-01 fix — the enclosing chain used to be
46
+ dropped, so a nested project annotation collapsed onto the same key
47
+ as an unrelated top-level declaration and lost its "local"
48
+ status). Keying by simple name alone (pre-F-02) let an unrelated
49
+ package's `@interface Getter` blind the Lombok-stub check for every
50
+ *other* package's `@Getter`/`@lombok.Getter` in the whole
51
+ project."""
52
+ names: Dict[str, Set[str]] = defaultdict(set)
53
+ for file_path, parse in parses.items():
54
+ ctx = self._file_ctx.get(str(file_path))
55
+ package = ctx.module if ctx is not None else DEFAULT_PACKAGE
56
+ stack = [parse.tree.root_node]
57
+ while stack:
58
+ node = stack.pop()
59
+ if node.type == "annotation_type_declaration":
60
+ name_node = node.child_by_field_name("name")
61
+ if name_node is not None:
62
+ simple = self._text(name_node, parse.source_bytes)
63
+ segments = self._enclosing_type_chain(node, parse.source_bytes)
64
+ segments.append(simple)
65
+ fqn = (
66
+ ".".join(segments) if package == DEFAULT_PACKAGE
67
+ else f"{package}.{'.'.join(segments)}"
68
+ )
69
+ names[simple].add(fqn)
70
+ stack.extend(node.children)
71
+ return dict(names)
72
+
73
+ @staticmethod
74
+ def _enclosing_type_chain(node, source: bytes) -> List[str]:
75
+ """Names of `node`'s enclosing class/interface/enum/record/
76
+ annotation-type ancestors, outermost first — mirrors
77
+ `JavaSymbolExtractor._scope_chain`'s type-only case (an annotation
78
+ type can only nest inside another type, never inside a method, so
79
+ no method/anonymous-class segment handling is needed here)."""
80
+ chain: List[str] = []
81
+ p = node.parent
82
+ while p is not None:
83
+ if p.type in _ENCLOSING_TYPE_CST_TYPES:
84
+ nn = p.child_by_field_name("name")
85
+ if nn is not None:
86
+ chain.append(_node_text(nn, source))
87
+ p = p.parent
88
+ chain.reverse()
89
+ return chain
90
+
91
+ # ------------------------------------------------------------------ P4.12 index builders
92
+
93
+ def _build_subtype_index(self) -> Dict[str, Set[str]]:
94
+ """Reverse of INHERITS ∪ IMPLEMENTS: every CLASS id -> the set of all
95
+ transitive subtype CLASS ids. Built by walking each class's *forward*
96
+ ancestor chain (`_class_ancestors`, already BFS over both edge kinds)
97
+ and recording the class under each ancestor — so an interface maps to
98
+ every class that implements it directly *or* through an intermediate
99
+ abstract class (`StripeGateway extends AuditedGateway implements ...`),
100
+ which is exactly the implementor set the fan-out enumerates."""
101
+ subtypes: Dict[str, Set[str]] = defaultdict(set)
102
+ for nid, node in self.builder._nodes.items():
103
+ if node.node_type != NodeType.CLASS:
104
+ continue
105
+ for anc in self._class_ancestors(nid):
106
+ subtypes[anc].add(nid)
107
+ return subtypes
108
+
109
+ def _iter_method_cst(self, root, file_path_str: str, func_id_by_position):
110
+ """Yield (method_declaration/constructor_declaration CST node,
111
+ node_id) for every method in the file.
112
+
113
+ Looks the id up by `(file_path, start_line, start_column)` against
114
+ the index built from `results` in `resolve_calls` rather than
115
+ re-deriving the `module:qualname` id from the CST — the id shape
116
+ (scope chain + P4.5 signature_key discriminator) is already computed
117
+ once by `JavaSymbolExtractor`/`CallGraphBuilder._symbol_to_id`;
118
+ duplicating that logic here would be a second, driftable source of
119
+ truth. The column is load-bearing, not decorative: two methods
120
+ routinely share one line (`void a() {} void b() {}`), and a
121
+ line-only key would silently drop one of them from this walk —
122
+ no call-site resolution, no DI indexing, no warning (F-02).
123
+ """
124
+ # Six index-building passes plus the call-site pass each walk every
125
+ # file's whole CST through here; the (node, id) list is identical on
126
+ # every pass of one resolution run (same tree object, same position
127
+ # index), so build it once per file and replay it. `_method_cst_cache`
128
+ # is reset at the top of `_build_resolution_indexes`, so an
129
+ # incremental rebuild with a re-parsed file never sees a stale tree.
130
+ # tree-sitter's Python binding hands out a fresh `Node` wrapper on
131
+ # every `tree.root_node` access (`is` never matches); `Node.id` is
132
+ # the underlying node's identity within its tree, so key on that.
133
+ cache = self._method_cst_cache
134
+ cached = cache.get(file_path_str) if cache is not None else None
135
+ if cached is not None and cached[0] == root.id:
136
+ yield from cached[1]
137
+ return
138
+ found = []
139
+ for node in self._cst_index(root, file_path_str)["__methods__"]:
140
+ nid = func_id_by_position.get(
141
+ (file_path_str, node.start_point[0] + 1, node.start_point[1]),
142
+ )
143
+ if nid:
144
+ found.append((node, nid))
145
+ if cache is not None:
146
+ cache[file_path_str] = (root.id, found)
147
+ yield from found
148
+
149
+ def _cst_index(self, root, file_path_str: str) -> Dict[str, list]:
150
+ """One stack-DFS walk of `root` per file per resolution run, shared by
151
+ every index-building pass that used to walk the whole tree itself.
152
+ `"__methods__"`: every `_METHOD_CST_TYPES` node, in the exact order the
153
+ old per-pass walk yielded them. `"__fields__"`: `field_declaration`
154
+ nodes not nested inside another field's initializer — the old field
155
+ walks stopped descending at a field_declaration, so a field declared
156
+ by an anonymous class inside a field initializer was never visited,
157
+ and still isn't. `"__initializer_nodes__"` keeps every declaration
158
+ shape the initializer resolver needs; unlike `__fields__`, it does
159
+ include nested/anonymous-class fields, matching the old fresh walk.
160
+ Cached in `_cst_index_cache` keyed on `Node.id`."""
161
+ cache = self._cst_index_cache
162
+ cached = cache.get(file_path_str) if cache is not None else None
163
+ if cached is not None and cached[0] == root.id:
164
+ return cached[1]
165
+ methods: list = []
166
+ fields: list = []
167
+ initializer_nodes: list = []
168
+ stack = [(root, False)]
169
+ while stack:
170
+ node, inside_field = stack.pop()
171
+ node_type = node.type
172
+ if node_type in _METHOD_CST_TYPES:
173
+ methods.append(node)
174
+ elif node_type == "field_declaration":
175
+ if not inside_field:
176
+ fields.append(node)
177
+ initializer_nodes.append(node)
178
+ inside_field = True
179
+ elif node_type == "static_initializer":
180
+ initializer_nodes.append(node)
181
+ elif node_type == "block" and node.parent is not None \
182
+ and node.parent.type in _INIT_BLOCK_PARENTS:
183
+ initializer_nodes.append(node)
184
+ stack.extend((child, inside_field) for child in node.children)
185
+ index = {
186
+ "__methods__": methods,
187
+ "__fields__": fields,
188
+ "__initializer_nodes__": initializer_nodes,
189
+ }
190
+ if cache is not None:
191
+ cache[file_path_str] = (root.id, index)
192
+ return index
193
+
194
+ def _field_declarations(self, root, file_path_str: str) -> list:
195
+ """Top-level `field_declaration` nodes of a file (see `_cst_index`)."""
196
+ return self._cst_index(root, file_path_str)["__fields__"]
197
+
198
+ def _initializer_declarations(self, root, file_path_str: str) -> list:
199
+ """All initializer-owning CST nodes of a file (see `_cst_index`)."""
200
+ return self._cst_index(root, file_path_str)["__initializer_nodes__"]
201
+
202
+ def _method_extractor(self):
203
+ """Lazily build (and cache) a `JavaSymbolExtractor` used only by
204
+ `_iter_func_cst` to derive Symbols the same way the real graph nodes
205
+ were built. `JavaSymbolExtractor._extract_method` only calls
206
+ `self.parser.node_text`/`node_position` (both `@staticmethod`s on
207
+ `LanguageParser`), so any real Java `LanguageParser` works here —
208
+ there is no per-file state to keep in sync."""
209
+ if self._method_extractor_cache is None:
210
+ from adl_drift.lang.java.extractor import JavaSymbolExtractor
211
+ from adl_drift.lang.java.parser import create_parser
212
+ self._method_extractor_cache = JavaSymbolExtractor(create_parser())
213
+ return self._method_extractor_cache
214
+
215
+ def _iter_func_cst(self, root, source: bytes, module_name: str):
216
+ """Yield (method/constructor CST node, node_id) for every method in
217
+ the file — P4.6's cross-function taint index (`detectors/taint.py::
218
+ TaintAnalyzer._build_func_index`), mirroring `PythonCallResolver.
219
+ _iter_func_cst`'s contract.
220
+
221
+ Unlike `_iter_method_cst` above (used by `resolve_calls`, which
222
+ looks the id up in a position index built earlier in that same
223
+ pass), this is called standalone by the taint analyzer with only a
224
+ CST root — so it derives each method's `Symbol` itself via
225
+ `JavaSymbolExtractor._extract_method` (the exact function that built
226
+ the real graph nodes) and turns it into an id via
227
+ `CallGraphBuilder._symbol_to_id`. Using the same two functions the
228
+ graph itself used guarantees the taint `function_id` always matches
229
+ the real FUNCTION node id — not a second, independently-derived
230
+ scheme (scope_chain + P4.5 signature_key) that could silently drift
231
+ out of sync with the extractor's own.
232
+ """
233
+ from adl_drift.lang.java.extractor import _METHOD_TYPES
234
+
235
+ extractor = self._method_extractor()
236
+ anon_index = extractor._index_anonymous_classes(root)
237
+ stack = [root]
238
+ while stack:
239
+ node = stack.pop()
240
+ if node.type in _METHOD_TYPES:
241
+ sym = extractor._extract_method(node, source, Path("."), anon_index)
242
+ if sym is not None:
243
+ nid = self.builder._symbol_to_id(sym, module_name)
244
+ yield node, nid
245
+ stack.extend(node.children)
246
+
247
+ def _method_facts(self, body) -> MethodFacts:
248
+ """Return one body walk's reusable call/reference CST facts.
249
+
250
+ The cache is deliberately per-resolution-run and keys on ``Node.id``:
251
+ tree-sitter re-wraps a CST node on access, but the underlying id is
252
+ stable for a parsed tree. Facts are tuples so no consumer can mutate
253
+ the shared view; all semantic resolution remains in its existing
254
+ passes. This preserves the old nested-class/method exclusion exactly.
255
+ """
256
+ cache = self._method_facts_cache
257
+ if cache is not None:
258
+ hit = cache.get(body.id)
259
+ if hit is not None:
260
+ return hit
261
+ call_sites: List[Tuple[object, str]] = []
262
+ method_references: List[object] = []
263
+ assignment_expressions: List[object] = []
264
+ local_scope_nodes: List[object] = []
265
+ lambda_expressions: List[object] = []
266
+ stack = [body]
267
+ while stack:
268
+ node = stack.pop()
269
+ if node.type == "method_invocation":
270
+ call_sites.append((node, "method"))
271
+ elif node.type == "object_creation_expression":
272
+ call_sites.append((node, "object_creation"))
273
+ elif node.type == "explicit_constructor_invocation":
274
+ call_sites.append((node, "explicit_constructor"))
275
+ elif node.type == "method_reference":
276
+ call_sites.append((node, "method_reference"))
277
+ method_references.append(node)
278
+ if node.type == "assignment_expression":
279
+ assignment_expressions.append(node)
280
+ elif node.type in {
281
+ "local_variable_declaration", "enhanced_for_statement",
282
+ "resource", "catch_formal_parameter", "instanceof_expression",
283
+ }:
284
+ local_scope_nodes.append(node)
285
+ elif node.type == "lambda_expression":
286
+ lambda_expressions.append(node)
287
+ if node.type in _NESTED_SCOPE_BOUNDARY:
288
+ continue
289
+ stack.extend(node.children)
290
+ facts = MethodFacts(
291
+ tuple(call_sites), tuple(method_references),
292
+ tuple(assignment_expressions), tuple(local_scope_nodes),
293
+ tuple(lambda_expressions),
294
+ )
295
+ if cache is not None:
296
+ cache[body.id] = facts
297
+ return facts
298
+
299
+ def _collect_call_sites(self, body):
300
+ """Call-site facts for ``body`` from its shared ``MethodFacts`` walk."""
301
+ return self._method_facts(body).call_sites
302
+
303
+ def _collect_method_references(self, body) -> Tuple[object, ...]:
304
+ """Method-reference facts for ``body`` from its shared CST walk."""
305
+ return self._method_facts(body).method_references