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,29 @@
1
+ """Java-specific registry detectors (P4.3)."""
2
+
3
+ from adl_drift.lang.java.detectors.additional import (
4
+ JavaAnnotationDetector,
5
+ JavaAsyncDetector,
6
+ JavaConfigDetector,
7
+ JavaDynamicDetector,
8
+ JavaLoggingDetector,
9
+ JavaTypeDetector,
10
+ )
11
+ from adl_drift.lang.java.detectors.data_access import JavaDataAccessDetector
12
+ from adl_drift.lang.java.detectors.imports import JavaImportDetector
13
+ from adl_drift.lang.java.detectors.routes import JavaRouteDetector
14
+ from adl_drift.lang.java.detectors.security import JavaSecurityDetector
15
+ from adl_drift.lang.java.detectors.tests import JavaTestDetector
16
+
17
+ __all__ = [
18
+ "JavaAnnotationDetector",
19
+ "JavaAsyncDetector",
20
+ "JavaConfigDetector",
21
+ "JavaDataAccessDetector",
22
+ "JavaDynamicDetector",
23
+ "JavaImportDetector",
24
+ "JavaLoggingDetector",
25
+ "JavaRouteDetector",
26
+ "JavaSecurityDetector",
27
+ "JavaTestDetector",
28
+ "JavaTypeDetector",
29
+ ]
@@ -0,0 +1,445 @@
1
+ """Small Java detectors for the lower-volume shared categories."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+ import re
7
+ from typing import Dict, List, Optional, Set
8
+
9
+ from tree_sitter import Node, Tree
10
+
11
+ from adl_drift.lang.java.detectors.base import DeclaredTypes, JavaDetector
12
+ from adl_drift.lang.java.scopes import TypeScopes
13
+ from adl_drift.utils.types import Pattern, PatternCategory
14
+
15
+
16
+ class JavaAnnotationDetector(JavaDetector):
17
+ @property
18
+ def category(self) -> PatternCategory:
19
+ return PatternCategory.DECORATORS
20
+
21
+ def detect(self, tree: Tree, source_bytes: bytes, file_path: Path) -> List[Pattern]:
22
+ return [
23
+ self._make_pattern(
24
+ name="java_annotation",
25
+ description=f"Java annotation: @{self._annotation_name(node, source_bytes)}",
26
+ node=node,
27
+ file_path=file_path,
28
+ confidence=0.99,
29
+ metadata={
30
+ "annotation": self._annotation_name(node, source_bytes),
31
+ "reason": "Java annotation is explicit in source",
32
+ },
33
+ )
34
+ for node in self._walk(tree.root_node)
35
+ if node.type in ("annotation", "marker_annotation")
36
+ ]
37
+
38
+
39
+ class JavaConfigDetector(JavaDetector):
40
+ SPRING_PACKAGE = "org.springframework.beans.factory.annotation.Value"
41
+ PROPERTIES_PACKAGE = "org.springframework.boot.context.properties.ConfigurationProperties"
42
+ RESOURCE_FILE = re.compile(
43
+ r"^(?:application|bootstrap)(?:-[A-Za-z0-9_.-]+)?\.(?:properties|ya?ml)$",
44
+ re.IGNORECASE,
45
+ )
46
+ SPRING_RESOURCE_CONSTRUCTORS = {
47
+ "org.springframework.core.io.ClassPathResource",
48
+ "org.springframework.core.io.FileSystemResource",
49
+ }
50
+
51
+ @property
52
+ def category(self) -> PatternCategory:
53
+ return PatternCategory.CONFIG
54
+
55
+ def detect(self, tree: Tree, source_bytes: bytes, file_path: Path) -> List[Pattern]:
56
+ root = tree.root_node
57
+ imports = self._imports(root, source_bytes)
58
+ types = self._declared_types(root, source_bytes)
59
+ local_types = self._declared_type_names(root, source_bytes)
60
+ patterns: List[Pattern] = []
61
+ for node in self._walk(root):
62
+ if node.type == "method_invocation":
63
+ method = self._text(node.child_by_field_name("name"), source_bytes)
64
+ receiver = self._text(node.child_by_field_name("object"), source_bytes)
65
+ if method in {"getenv", "getProperty"} and self._type_has_provenance(
66
+ receiver, imports, local_types, "java.lang.System", node
67
+ ):
68
+ patterns.append(self._finding(
69
+ "environment_read", node, file_path, 0.99,
70
+ f"System.{method} reads external configuration", {"method": method},
71
+ ))
72
+ resource = self._resource_invocation(
73
+ node, source_bytes, types, imports, local_types
74
+ )
75
+ if resource:
76
+ patterns.append(self._finding(
77
+ "config_resource_read", node, file_path, 0.96,
78
+ "Java resource API reads an application configuration file", resource,
79
+ ))
80
+ elif node.type == "object_creation_expression":
81
+ resource = self._resource_constructor(node, source_bytes, imports, local_types)
82
+ if resource:
83
+ patterns.append(self._finding(
84
+ "config_resource_read", node, file_path, 0.97,
85
+ "Spring resource object targets an application configuration file", resource,
86
+ ))
87
+ elif node.type in ("field_declaration", "class_declaration", "record_declaration"):
88
+ for annotation in self._annotations(node):
89
+ if self._annotation_is(annotation, source_bytes, imports, self.SPRING_PACKAGE):
90
+ raw_keys = self._annotation_strings(annotation, source_bytes)
91
+ patterns.append(self._finding(
92
+ "spring_value", node, file_path, 0.99,
93
+ "Field uses imported Spring @Value binding",
94
+ {
95
+ "keys": [self._normalize_key(key) for key in raw_keys],
96
+ "raw_keys": raw_keys,
97
+ },
98
+ ))
99
+ elif self._annotation_is(annotation, source_bytes, imports, self.PROPERTIES_PACKAGE):
100
+ patterns.append(self._finding(
101
+ "configuration_properties", node, file_path, 0.99,
102
+ "Type uses imported Spring @ConfigurationProperties binding",
103
+ {"prefix": (self._annotation_strings(annotation, source_bytes) or [""])[0]},
104
+ ))
105
+ return patterns
106
+
107
+ def _resource_invocation(
108
+ self, node: Node, source: bytes, types: DeclaredTypes,
109
+ imports: Set[str], local_types: TypeScopes,
110
+ ) -> Optional[Dict[str, object]]:
111
+ resource_path = self._resource_path(node, source)
112
+ if not resource_path:
113
+ return None
114
+ method = self._text(node.child_by_field_name("name"), source)
115
+ receiver_node = node.child_by_field_name("object")
116
+ receiver = self._text(receiver_node, source)
117
+ declared = self._declared_type(receiver, types, node)
118
+ receiver_args = (
119
+ receiver_node.child_by_field_name("arguments")
120
+ if receiver_node is not None and receiver_node.type == "method_invocation"
121
+ else None
122
+ )
123
+ get_class_call = receiver_node is not None \
124
+ and receiver_node.type == "method_invocation" \
125
+ and self._text(receiver_node.child_by_field_name("name"), source) == "getClass" \
126
+ and receiver_args is not None and not receiver_args.named_children
127
+ class_resource = method in {"getResource", "getResourceAsStream"} and (
128
+ get_class_call or (receiver_node is not None and receiver_node.type == "class_literal")
129
+ )
130
+ classloader_resource = method in {
131
+ "getResource", "getResourceAsStream", "getSystemResource", "getSystemResourceAsStream",
132
+ } and (
133
+ self._type_has_provenance(
134
+ declared, imports, local_types, "java.lang.ClassLoader", node
135
+ ) or self._type_has_provenance(
136
+ receiver, imports, local_types, "java.lang.ClassLoader", node
137
+ )
138
+ )
139
+ spring_resource = method == "getResource" and self._type_has_provenance(
140
+ declared, imports, local_types, "org.springframework.core.io.ResourceLoader", node
141
+ )
142
+ if not (class_resource or classloader_resource or spring_resource):
143
+ return None
144
+ return {"resource": resource_path, "api": method}
145
+
146
+ def _resource_constructor(
147
+ self, node: Node, source: bytes, imports: Set[str], local_types: TypeScopes,
148
+ ) -> Optional[Dict[str, object]]:
149
+ resource_path = self._resource_path(node, source)
150
+ if not resource_path:
151
+ return None
152
+ raw_type = self._text(node.child_by_field_name("type"), source)
153
+ qualified = next(
154
+ (candidate for candidate in self.SPRING_RESOURCE_CONSTRUCTORS
155
+ if candidate.rsplit(".", 1)[-1] == self._simple_type(raw_type)),
156
+ "",
157
+ )
158
+ if not qualified or not self._type_has_provenance(
159
+ raw_type, imports, local_types, qualified, node
160
+ ):
161
+ return None
162
+ return {"resource": resource_path, "api": self._simple_type(raw_type)}
163
+
164
+ def _resource_path(self, node: Node, source: bytes) -> str:
165
+ argument = self._first_argument(node)
166
+ if argument is None or argument.type != "string_literal":
167
+ return ""
168
+ raw = self._text(argument, source)
169
+ value = raw[1:-1] if len(raw) >= 2 else ""
170
+ filename = value.rsplit("/", 1)[-1]
171
+ return value if self.RESOURCE_FILE.fullmatch(filename) else ""
172
+
173
+ @staticmethod
174
+ def _normalize_key(value: str) -> str:
175
+ key = value
176
+ if key.startswith("${") and key.endswith("}"):
177
+ key = key[2:-1].split(":", 1)[0]
178
+ key = re.sub(r"([a-z0-9])([A-Z])", r"\1-\2", key)
179
+ return key.replace("_", "-").lower()
180
+
181
+ def _finding(
182
+ self, name: str, node: Node, path: Path, confidence: float,
183
+ reason: str, extra: Dict[str, object],
184
+ ) -> Pattern:
185
+ return self._make_pattern(
186
+ name=name, description=reason, node=node, file_path=path,
187
+ confidence=confidence, metadata={"reason": reason, **extra},
188
+ )
189
+
190
+
191
+ class JavaLoggingDetector(JavaDetector):
192
+ @property
193
+ def category(self) -> PatternCategory:
194
+ return PatternCategory.LOGGING
195
+
196
+ def detect(self, tree: Tree, source_bytes: bytes, file_path: Path) -> List[Pattern]:
197
+ imports = self._imports(tree.root_node, source_bytes)
198
+ types = self._declared_types(tree.root_node, source_bytes)
199
+ local_types = self._declared_type_names(tree.root_node, source_bytes)
200
+ patterns: List[Pattern] = []
201
+ for node in self._walk(tree.root_node):
202
+ if node.type != "method_invocation":
203
+ continue
204
+ method = self._text(node.child_by_field_name("name"), source_bytes)
205
+ receiver = self._text(node.child_by_field_name("object"), source_bytes)
206
+ system_type, separator, stream = receiver.rpartition(".")
207
+ if separator and stream in {"out", "err"} and method.startswith("print") \
208
+ and self._type_has_provenance(
209
+ system_type, imports, local_types, "java.lang.System", node
210
+ ):
211
+ patterns.append(self._finding(
212
+ "system_output", node, file_path, 0.99,
213
+ "Direct System.out/System.err output", {"stream": receiver},
214
+ ))
215
+ elif any(self._type_has_provenance(
216
+ self._declared_type(receiver, types, node), imports, local_types,
217
+ qualified, node,
218
+ ) for qualified in (
219
+ "org.slf4j.Logger", "org.apache.logging.log4j.Logger",
220
+ "org.apache.log4j.Logger", "java.util.logging.Logger",
221
+ )) \
222
+ and method in {
223
+ "trace", "debug", "info", "warn", "error", "fatal", "log"
224
+ }:
225
+ first = self._first_argument(node)
226
+ eager = first is not None and first.type == "binary_expression"
227
+ patterns.append(self._finding(
228
+ "logger_call", node, file_path, 0.95,
229
+ "Call targets a declared logger",
230
+ {"level": method, "eager_concatenation": eager},
231
+ ))
232
+ return patterns
233
+
234
+ def _finding(
235
+ self, name: str, node: Node, path: Path, confidence: float,
236
+ reason: str, extra: Dict[str, object],
237
+ ) -> Pattern:
238
+ return self._make_pattern(
239
+ name=name, description=reason, node=node, file_path=path,
240
+ confidence=confidence, metadata={"reason": reason, **extra},
241
+ )
242
+
243
+
244
+ class JavaTypeDetector(JavaDetector):
245
+ RAW_TYPES = {"Collection", "List", "Set", "Map", "Optional", "Class"}
246
+ NULLABILITY_ANNOTATIONS = {
247
+ "org.jetbrains.annotations.Nullable": "nullable",
248
+ "org.jetbrains.annotations.NotNull": "non_null",
249
+ "jakarta.annotation.Nullable": "nullable",
250
+ "javax.annotation.Nullable": "nullable",
251
+ "jakarta.validation.constraints.NotNull": "non_null",
252
+ "javax.validation.constraints.NotNull": "non_null",
253
+ "org.springframework.lang.Nullable": "nullable",
254
+ "org.springframework.lang.NonNull": "non_null",
255
+ "org.jspecify.annotations.Nullable": "nullable",
256
+ "org.jspecify.annotations.NonNull": "non_null",
257
+ "org.jspecify.annotations.NullMarked": "non_null_by_default",
258
+ "org.jspecify.annotations.NullUnmarked": "unspecified_by_default",
259
+ }
260
+ NULLABILITY_TARGETS = {
261
+ "field_declaration": "field",
262
+ "local_variable_declaration": "local",
263
+ "formal_parameter": "parameter",
264
+ "method_declaration": "return",
265
+ "class_declaration": "type",
266
+ "interface_declaration": "type",
267
+ "record_declaration": "type",
268
+ }
269
+
270
+ @property
271
+ def category(self) -> PatternCategory:
272
+ return PatternCategory.TYPES
273
+
274
+ def detect(self, tree: Tree, source_bytes: bytes, file_path: Path) -> List[Pattern]:
275
+ root = tree.root_node
276
+ imports = self._imports(root, source_bytes)
277
+ local_types = self._declared_type_names(root, source_bytes)
278
+ patterns: List[Pattern] = []
279
+ for node in self._walk(root):
280
+ if node.type in (
281
+ "field_declaration", "local_variable_declaration", "formal_parameter",
282
+ "method_declaration",
283
+ ):
284
+ type_node = node.child_by_field_name("type")
285
+ type_name = self._text(type_node, source_bytes)
286
+ simple = self._simple_type(type_name)
287
+ qualified = f"java.lang.{simple}" if simple == "Class" else f"java.util.{simple}"
288
+ if "<" not in type_name and simple in self.RAW_TYPES and self._type_has_provenance(
289
+ type_name, imports, local_types, qualified, node
290
+ ):
291
+ patterns.append(self._make_pattern(
292
+ name="raw_generic_type",
293
+ description=f"Raw generic type: {type_name}",
294
+ node=type_node or node,
295
+ file_path=file_path,
296
+ confidence=0.98,
297
+ metadata={
298
+ "type": type_name,
299
+ "reason": "Imported generic JDK type is used without type arguments",
300
+ },
301
+ ))
302
+ target_kind = self.NULLABILITY_TARGETS.get(node.type)
303
+ if not target_kind:
304
+ continue
305
+ for annotation in self._annotations(node):
306
+ for qualified, nullability in self.NULLABILITY_ANNOTATIONS.items():
307
+ if not self._annotation_is(annotation, source_bytes, imports, qualified):
308
+ continue
309
+ patterns.append(self._make_pattern(
310
+ name="nullability_annotation",
311
+ description=f"Java nullability contract: {nullability}",
312
+ node=annotation,
313
+ file_path=file_path,
314
+ confidence=0.99,
315
+ metadata={
316
+ "nullability": nullability,
317
+ "target_kind": target_kind,
318
+ "annotation_fqn": qualified,
319
+ "reason": "Imported or fully-qualified nullability annotation",
320
+ },
321
+ ))
322
+ break
323
+ return patterns
324
+
325
+
326
+ class JavaAsyncDetector(JavaDetector):
327
+ SPRING_ASYNC = "org.springframework.scheduling.annotation.Async"
328
+
329
+ @property
330
+ def category(self) -> PatternCategory:
331
+ return PatternCategory.ASYNC
332
+
333
+ def detect(self, tree: Tree, source_bytes: bytes, file_path: Path) -> List[Pattern]:
334
+ root = tree.root_node
335
+ imports = self._imports(root, source_bytes)
336
+ types = self._declared_types(root, source_bytes)
337
+ local_types = self._declared_type_names(root, source_bytes)
338
+ patterns: List[Pattern] = []
339
+ for node in self._walk(root):
340
+ if node.type == "method_declaration" and any(
341
+ self._annotation_is(a, source_bytes, imports, self.SPRING_ASYNC)
342
+ for a in self._annotations(node)
343
+ ):
344
+ patterns.append(self._finding(
345
+ "spring_async", node, file_path, 0.99, "Method uses imported Spring @Async",
346
+ ))
347
+ elif node.type == "object_creation_expression" and self._type_has_provenance(
348
+ self._text(node.child_by_field_name("type"), source_bytes),
349
+ imports, local_types, "java.lang.Thread", node,
350
+ ):
351
+ patterns.append(self._finding(
352
+ "raw_thread", node, file_path, 0.95, "Code constructs a raw java.lang.Thread",
353
+ ))
354
+ elif node.type == "method_invocation":
355
+ method = self._text(node.child_by_field_name("name"), source_bytes)
356
+ receiver = self._text(node.child_by_field_name("object"), source_bytes)
357
+ raw_receiver_type = self._declared_type(receiver, types, node)
358
+ executor_type = self._simple_type(raw_receiver_type)
359
+ if method in {"submit", "execute", "invokeAll"} and executor_type in {
360
+ "Executor", "ExecutorService",
361
+ } and self._type_has_provenance(
362
+ raw_receiver_type, imports, local_types,
363
+ f"java.util.concurrent.{executor_type}", node,
364
+ ):
365
+ patterns.append(self._finding(
366
+ "executor_task", node, file_path, 0.95,
367
+ "Declared Executor/ExecutorService schedules work",
368
+ ))
369
+ elif method in {
370
+ "runAsync", "supplyAsync", "completedFuture", "allOf", "anyOf",
371
+ } and self._type_has_provenance(
372
+ receiver, imports, local_types,
373
+ "java.util.concurrent.CompletableFuture", node,
374
+ ):
375
+ patterns.append(self._finding(
376
+ "completable_future", node, file_path, 0.98,
377
+ "CompletableFuture schedules or composes asynchronous work",
378
+ ))
379
+ elif method in {
380
+ "thenApplyAsync", "thenAcceptAsync", "thenRunAsync", "handleAsync",
381
+ "whenCompleteAsync",
382
+ } and self._type_has_provenance(
383
+ self._declared_type(receiver, types, node), imports, local_types,
384
+ "java.util.concurrent.CompletableFuture", node,
385
+ ):
386
+ patterns.append(self._finding(
387
+ "completable_future", node, file_path, 0.96,
388
+ "Declared CompletableFuture composes asynchronous work",
389
+ ))
390
+ return patterns
391
+
392
+ def _finding(
393
+ self, name: str, node: Node, path: Path, confidence: float, reason: str
394
+ ) -> Pattern:
395
+ return self._make_pattern(
396
+ name=name, description=reason, node=node, file_path=path,
397
+ confidence=confidence, metadata={"reason": reason},
398
+ )
399
+
400
+
401
+ class JavaDynamicDetector(JavaDetector):
402
+ @property
403
+ def category(self) -> PatternCategory:
404
+ return PatternCategory.DYNAMIC
405
+
406
+ def detect(self, tree: Tree, source_bytes: bytes, file_path: Path) -> List[Pattern]:
407
+ imports = self._imports(tree.root_node, source_bytes)
408
+ types = self._declared_types(tree.root_node, source_bytes)
409
+ local_types = self._declared_type_names(tree.root_node, source_bytes)
410
+ patterns: List[Pattern] = []
411
+ for node in self._walk(tree.root_node):
412
+ if node.type != "method_invocation":
413
+ continue
414
+ method = self._text(node.child_by_field_name("name"), source_bytes)
415
+ receiver = self._text(node.child_by_field_name("object"), source_bytes)
416
+ name: Optional[str] = None
417
+ reason = ""
418
+ confidence = 0.9
419
+ if method == "forName" and self._type_has_provenance(
420
+ receiver, imports, local_types, "java.lang.Class", node
421
+ ):
422
+ name, reason, confidence = "class_for_name", "Class.forName resolves a type at runtime", 0.98
423
+ elif method in {"getMethod", "getDeclaredMethod", "getField", "getDeclaredField"} \
424
+ and (receiver.endswith("getClass()")
425
+ or self._type_has_provenance(
426
+ self._declared_type(receiver, types, node), imports,
427
+ local_types, "java.lang.Class", node,
428
+ )):
429
+ name, reason = "reflective_lookup", "Member name is resolved through Java reflection"
430
+ elif method == "invoke" and self._has_import(imports, "java.lang.reflect.Method") \
431
+ and self._type_has_provenance(
432
+ self._declared_type(receiver, types, node), imports, local_types,
433
+ "java.lang.reflect.Method", node,
434
+ ):
435
+ name, reason, confidence = "reflective_invoke", "Declared java.lang.reflect.Method is invoked", 0.96
436
+ elif method == "newProxyInstance" and self._type_has_provenance(
437
+ receiver, imports, local_types, "java.lang.reflect.Proxy", node):
438
+ name, reason, confidence = "dynamic_proxy", "java.lang.reflect.Proxy creates runtime dispatch", 0.98
439
+ if name:
440
+ patterns.append(self._make_pattern(
441
+ name=name, description=reason, node=node, file_path=file_path,
442
+ confidence=confidence,
443
+ metadata={"resolvable": "partial", "reason": reason},
444
+ ))
445
+ return patterns
@@ -0,0 +1,154 @@
1
+ """Small CST helpers shared by Java detectors.
2
+
3
+ Import/declared-type provenance matching (`_imports`/`_declared_types`/
4
+ `_type_has_provenance`/`_annotation_is`) delegates to `lang/java/
5
+ type_provenance.py`, which P4.6's Java taint layer also uses directly
6
+ against a per-file `JavaTypeContext` — this class's methods are thin
7
+ wrappers kept for its many existing detector call sites, not a second
8
+ implementation.
9
+
10
+ `_imports`/`_declared_types`/`_declared_type_names` are memoized per file
11
+ (P8 Phase 2): up to 11 detector instances each call these at the top of
12
+ their own `detect()`, and `_annotation_is` (below) calls
13
+ `_declared_type_names` again on every single annotation check — profiling
14
+ showed this re-walking the same file's CST 20-30x instead of once. The
15
+ cache is a single-slot `threading.local()`, keyed by `root.id` — tree-sitter
16
+ Node objects are rewrapped on every attribute access (`tree.root_node is
17
+ tree.root_node` is `False`), so Python `is`/`id()` can't identify "same
18
+ underlying node" across the separate `tree.root_node` fetches each detector
19
+ makes; `root.id` is tree-sitter's own identifier for the underlying node and
20
+ *is* stable across those rewraps. `root.id` is itself address-derived and
21
+ in principle reusable once its tree is freed, but the slot also holds a
22
+ strong reference to the `root` it last cached (`_file_cache.root`) — as
23
+ long as that reference is live, its tree can't be freed, so its id can't be
24
+ handed to an unrelated new tree; the id can only repeat once the slot has
25
+ already moved on to caching something else. The cache naturally resets when
26
+ `DetectorRegistry.run_all()` moves to the next file, and stays correctly
27
+ isolated per worker under every `scanner/parallel.py` execution tier: a
28
+ fresh interpreter (and thread-local) per `ProcessPoolExecutor` worker, a
29
+ genuinely separate slot per `ThreadPoolExecutor` thread, and a single slot
30
+ reused file-by-file when sequential."""
31
+
32
+ from __future__ import annotations
33
+
34
+ import ast
35
+ import threading
36
+ from typing import Iterator, List, Optional, Set
37
+
38
+ from tree_sitter import Node
39
+
40
+ from adl_drift.detectors.base import BaseDetector
41
+ from adl_drift.lang.java import type_provenance as _tp
42
+ from adl_drift.lang.java.scopes import TYPE_DECLARATIONS, TypeScopes, declared_type_scopes
43
+
44
+ ANNOTATION_TYPES = ("annotation", "marker_annotation")
45
+ DECLARATION_TYPES = _tp._DECLARATION_TYPES
46
+ CLASS_TYPES = tuple(TYPE_DECLARATIONS)
47
+ SCOPE_TYPES = _tp._SCOPE_TYPES
48
+ DeclaredTypes = _tp.DeclaredTypes
49
+
50
+ _file_cache = threading.local()
51
+
52
+
53
+ def _cache_for(root: Node) -> threading.local:
54
+ if getattr(_file_cache, "root_id", None) != root.id:
55
+ _file_cache.root_id = root.id
56
+ _file_cache.root = root
57
+ _file_cache.imports = None
58
+ _file_cache.declared_types = None
59
+ _file_cache.local_types = None
60
+ return _file_cache
61
+
62
+
63
+ class JavaDetector(BaseDetector):
64
+ def _walk(self, node: Node) -> Iterator[Node]:
65
+ yield node
66
+ for child in node.children:
67
+ yield from self._walk(child)
68
+
69
+ def _text(self, node: Optional[Node], source: bytes) -> str:
70
+ return self.parser.node_text(node, source).strip() if node is not None else ""
71
+
72
+ def _annotations(self, node: Node) -> List[Node]:
73
+ modifiers = next((c for c in node.children if c.type == "modifiers"), None)
74
+ return [c for c in modifiers.children if c.type in ANNOTATION_TYPES] if modifiers else []
75
+
76
+ def _annotation_name(self, annotation: Node, source: bytes) -> str:
77
+ return self._text(annotation.child_by_field_name("name"), source)
78
+
79
+ def _annotation_strings(self, annotation: Node, source: bytes) -> List[str]:
80
+ values: List[str] = []
81
+ for node in self._walk(annotation):
82
+ if node.type != "string_literal":
83
+ continue
84
+ raw = self._text(node, source)
85
+ try:
86
+ values.append(ast.literal_eval(raw))
87
+ except (SyntaxError, ValueError):
88
+ values.append(raw.strip('"'))
89
+ return values
90
+
91
+ def _imports(self, root: Node, source: bytes) -> Set[str]:
92
+ cache = _cache_for(root)
93
+ if cache.imports is None:
94
+ cache.imports = _tp.imports_of(root, source)
95
+ return cache.imports
96
+
97
+ @staticmethod
98
+ def _has_import(imports: Set[str], qualified_name: str) -> bool:
99
+ return _tp.has_import(imports, qualified_name)
100
+
101
+ def _annotation_is(
102
+ self, annotation: Node, source: bytes, imports: Set[str], qualified_name: str
103
+ ) -> bool:
104
+ name = self._annotation_name(annotation, source)
105
+ root = annotation
106
+ while root.parent is not None:
107
+ root = root.parent
108
+ local_types = self._declared_type_names(root, source)
109
+ return _tp.annotation_is(name, annotation.start_byte, imports, local_types, qualified_name)
110
+
111
+ def _declared_types(self, root: Node, source: bytes) -> DeclaredTypes:
112
+ cache = _cache_for(root)
113
+ if cache.declared_types is None:
114
+ cache.declared_types = _tp.declared_types_of(root, source)
115
+ return cache.declared_types
116
+
117
+ @staticmethod
118
+ def _typed_declaration_scope(node: Node) -> Optional[Node]:
119
+ return _tp._typed_declaration_scope(node)
120
+
121
+ @staticmethod
122
+ def _declaration_scope(node: Node) -> Node:
123
+ return _tp._declaration_scope(node)
124
+
125
+ def _declared_type(self, receiver: str, types: DeclaredTypes, at: Node) -> str:
126
+ return _tp.declared_type_of(receiver, types, at.start_byte)
127
+
128
+ def _declared_type_names(self, root: Node, source: bytes) -> TypeScopes:
129
+ cache = _cache_for(root)
130
+ if cache.local_types is None:
131
+ cache.local_types = declared_type_scopes(root, source)
132
+ return cache.local_types
133
+
134
+ @staticmethod
135
+ def _simple_type(type_name: str) -> str:
136
+ return _tp.simple_type(type_name)
137
+
138
+ def _type_has_provenance(
139
+ self, raw_type: str, imports: Set[str], local_types: TypeScopes,
140
+ qualified_name: str, at: Node,
141
+ ) -> bool:
142
+ return _tp.type_has_provenance(raw_type, imports, local_types, qualified_name, at.start_byte)
143
+
144
+ def _first_argument(self, invocation: Node) -> Optional[Node]:
145
+ args = invocation.child_by_field_name("arguments")
146
+ if args is None:
147
+ return None
148
+ return next((c for c in args.named_children if not c.is_extra), None)
149
+
150
+ def _package(self, root: Node, source: bytes) -> str:
151
+ for node in root.named_children:
152
+ if node.type == "package_declaration":
153
+ return self._text(node.named_children[-1], source)
154
+ return "<default>"