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,342 @@
1
+ """Java import/declared-type provenance (P4.6 F-04, PROBLEMS.md).
2
+
3
+ Confirms a bare type/annotation *simple* name (`RequestParam`, `Statement`)
4
+ actually resolves to a specific FQN (`org.springframework...RequestParam`,
5
+ `java.sql.Statement`) via the compilation unit's own `import` declarations
6
+ and local type/variable declarations, rather than trusting simple-name text
7
+ alone — a same-named method/annotation on an unrelated, project-owned class
8
+ must not be mistaken for the real JDK/framework API.
9
+
10
+ This is P4.3's `lang/java/detectors/base.py::JavaDetector` machinery
11
+ (`_imports`/`_declared_types`/`_type_has_provenance`/`_annotation_is`, used
12
+ by `security.py`/`data_access.py`/`routes.py`/`additional.py`/`tests.py`)
13
+ extracted to free functions so P4.6's Java taint layer (`lang/java/cfg.py`/
14
+ `lang/java/taint.py`) can reuse the exact same, already-tested logic instead
15
+ of re-deriving its own — `JavaDetector`'s methods below are now thin
16
+ delegating wrappers, unchanged in signature or behavior for their many
17
+ existing call sites.
18
+
19
+ Unlike `JavaDetector`'s binary `_type_has_provenance`/`_annotation_is`
20
+ (good enough for a single-file pattern detector, which simply doesn't flag
21
+ when unsure), the taint layer needs a *three-way* verdict so it can keep
22
+ recall on an unresolved case instead of dropping it silently:
23
+ - `"confirmed"` — exact FQN literal, or simple name + a matching import
24
+ with nothing else in scope claiming that simple name.
25
+ - `"rejected"` — a *different* type/annotation is definitely in scope
26
+ under this simple name (a local declaration, or a conflicting import) —
27
+ this is provably not `qualified_name`.
28
+ - `"ambiguous"` — the simple name matches and nothing contradicts it, but
29
+ no import positively confirms it either (e.g. no import statement at
30
+ all). Still plausibly right — just not proven — so callers that care
31
+ about recall should keep treating it as a match, at reduced confidence.
32
+ """
33
+
34
+ from __future__ import annotations
35
+
36
+ import re
37
+ from dataclasses import dataclass, field
38
+ from typing import Dict, List, Literal, Optional, Set, Tuple
39
+
40
+ from tree_sitter import Node
41
+
42
+ from adl_drift.lang.java.scopes import (
43
+ TYPE_DECLARATIONS,
44
+ TypeScopes,
45
+ declaration_scope,
46
+ declared_type_scopes,
47
+ type_declared_at,
48
+ )
49
+
50
+ DeclaredTypes = Dict[str, List[Tuple[int, int, str]]]
51
+ # `(scope_start, scope_end, direct_supertypes)` per declaration, keyed by
52
+ # simple name — scope-aware for the same reason `DeclaredTypes` is: two local
53
+ # classes can share a simple name across sibling methods (PROBLEMS.md P4.6
54
+ # F-02), and a flat `name -> supertypes` table would let one leak into the
55
+ # other's `assignability_provenance` lookup.
56
+ LocalSupertypes = Dict[str, List[Tuple[int, int, List[str]]]]
57
+ Provenance = Literal["confirmed", "ambiguous", "rejected"]
58
+
59
+ _DECLARATION_TYPES = ("field_declaration", "local_variable_declaration")
60
+ _SCOPE_TYPES = (*TYPE_DECLARATIONS, "method_declaration", "constructor_declaration", "block", "lambda_expression")
61
+
62
+
63
+ def _walk(node: Node):
64
+ yield node
65
+ for child in node.children:
66
+ yield from _walk(child)
67
+
68
+
69
+ def _text(node: Optional[Node], source: bytes) -> str:
70
+ if node is None:
71
+ return ""
72
+ return source[node.start_byte:node.end_byte].decode("utf-8", errors="replace").strip()
73
+
74
+
75
+ def imports_of(root: Node, source: bytes) -> Set[str]:
76
+ """`{"pkg.Simple", "pkg.*", ...}` for every `import`/`import static` in
77
+ the compilation unit (regex over each `import_declaration`'s own text —
78
+ cheap, and every import statement has a fixed, simple grammar)."""
79
+ imports: Set[str] = set()
80
+ for node in root.named_children:
81
+ if node.type != "import_declaration":
82
+ continue
83
+ match = re.match(r"import\s+(?:static\s+)?([\w.]+)(\.\*)?\s*;", _text(node, source))
84
+ if match:
85
+ imports.add(match.group(1) + (match.group(2) or ""))
86
+ return imports
87
+
88
+
89
+ def has_import(imports: Set[str], qualified_name: str) -> bool:
90
+ package, _, _ = qualified_name.rpartition(".")
91
+ return qualified_name in imports or f"{package}.*" in imports
92
+
93
+
94
+ def simple_type(type_name: str) -> str:
95
+ return re.sub(r"<.*>", "", type_name).rsplit(".", 1)[-1].replace("[]", "")
96
+
97
+
98
+ def type_provenance(
99
+ raw_type: str, imports: Set[str], local_types: TypeScopes, qualified_name: str, at_byte: int,
100
+ ) -> Provenance:
101
+ raw_type = re.sub(r"<.*>", "", raw_type).replace("[]", "").strip()
102
+ simple = qualified_name.rsplit(".", 1)[-1]
103
+ if raw_type == qualified_name:
104
+ return "confirmed"
105
+ if raw_type != simple:
106
+ return "rejected"
107
+ if type_declared_at(local_types, simple, at_byte):
108
+ return "rejected"
109
+ conflicting = any(
110
+ imported.endswith(f".{simple}") and imported != qualified_name
111
+ for imported in imports
112
+ )
113
+ if conflicting:
114
+ return "rejected"
115
+ if qualified_name.startswith("java.lang.") or has_import(imports, qualified_name):
116
+ return "confirmed"
117
+ return "ambiguous"
118
+
119
+
120
+ def type_has_provenance(
121
+ raw_type: str, imports: Set[str], local_types: TypeScopes, qualified_name: str, at_byte: int,
122
+ ) -> bool:
123
+ return type_provenance(raw_type, imports, local_types, qualified_name, at_byte) == "confirmed"
124
+
125
+
126
+ def annotation_provenance(
127
+ name: str, at_byte: int, imports: Set[str], local_types: TypeScopes, qualified_name: str,
128
+ ) -> Provenance:
129
+ simple = qualified_name.rsplit(".", 1)[-1]
130
+ if name == qualified_name:
131
+ return "confirmed"
132
+ if name != simple:
133
+ return "rejected"
134
+ if type_declared_at(local_types, simple, at_byte):
135
+ return "rejected"
136
+ conflicting = any(
137
+ imported.endswith(f".{simple}") and imported != qualified_name
138
+ for imported in imports
139
+ )
140
+ if conflicting:
141
+ return "rejected"
142
+ if has_import(imports, qualified_name):
143
+ return "confirmed"
144
+ return "ambiguous"
145
+
146
+
147
+ def annotation_is(
148
+ name: str, at_byte: int, imports: Set[str], local_types: TypeScopes, qualified_name: str,
149
+ ) -> bool:
150
+ return annotation_provenance(name, at_byte, imports, local_types, qualified_name) == "confirmed"
151
+
152
+
153
+ def _declaration_scope(node: Node) -> Node:
154
+ parent = node.parent
155
+ while parent is not None and parent.type not in _SCOPE_TYPES:
156
+ parent = parent.parent
157
+ return parent or node
158
+
159
+
160
+ def _typed_declaration_scope(node: Node) -> Optional[Node]:
161
+ if node.type == "enhanced_for_statement":
162
+ return node.child_by_field_name("body")
163
+ if node.type == "catch_formal_parameter":
164
+ return node.parent.child_by_field_name("body") if node.parent else None
165
+ statement = node.parent.parent if node.parent is not None else None
166
+ return statement.child_by_field_name("body") if statement is not None else None
167
+
168
+
169
+ def declared_types_of(root: Node, source: bytes) -> DeclaredTypes:
170
+ """`{var_name: [(scope_start, scope_end, type_text), ...]}` for every
171
+ local variable, field, formal parameter, enhanced-`for`/`catch`/
172
+ try-with-resources binding in the compilation unit."""
173
+ types: DeclaredTypes = {}
174
+ for node in _walk(root):
175
+ if node.type in _DECLARATION_TYPES:
176
+ type_name = _text(node.child_by_field_name("type"), source)
177
+ scope = _declaration_scope(node)
178
+ start = scope.start_byte if node.type == "field_declaration" else node.start_byte
179
+ for child in node.children:
180
+ if child.type == "variable_declarator":
181
+ name = _text(child.child_by_field_name("name"), source)
182
+ if name:
183
+ types.setdefault(name, []).append((start, scope.end_byte, type_name))
184
+ elif node.type == "formal_parameter":
185
+ name = _text(node.child_by_field_name("name"), source)
186
+ if name:
187
+ scope = _declaration_scope(node)
188
+ types.setdefault(name, []).append((
189
+ scope.start_byte, scope.end_byte,
190
+ _text(node.child_by_field_name("type"), source),
191
+ ))
192
+ elif node.type in {"enhanced_for_statement", "catch_formal_parameter", "resource"}:
193
+ name = _text(node.child_by_field_name("name"), source)
194
+ type_node = node.child_by_field_name("type")
195
+ if type_node is None and node.type == "catch_formal_parameter":
196
+ type_node = next(
197
+ (child for child in node.named_children if child.type == "catch_type"), None,
198
+ )
199
+ scope = _typed_declaration_scope(node)
200
+ if name and type_node is not None and scope is not None:
201
+ start = node.start_byte if node.type == "resource" else scope.start_byte
202
+ types.setdefault(name, []).append((start, scope.end_byte, _text(type_node, source)))
203
+ return types
204
+
205
+
206
+ def declared_type_of(receiver: str, types: DeclaredTypes, at_byte: int) -> str:
207
+ matches = [
208
+ (end - start, -start, type_name)
209
+ for start, end, type_name in types.get(receiver, [])
210
+ if start <= at_byte < end
211
+ ]
212
+ return min(matches)[2] if matches else ""
213
+
214
+
215
+ def _type_names_in(node: Optional[Node], source: bytes) -> List[str]:
216
+ if node is None:
217
+ return []
218
+ out: List[str] = []
219
+ for child in _walk(node):
220
+ if child.type in ("type_identifier", "scoped_type_identifier"):
221
+ text = _text(child, source)
222
+ if text:
223
+ out.append(text)
224
+ return out
225
+
226
+
227
+ def local_supertypes_of(root: Node, source: bytes) -> LocalSupertypes:
228
+ """Direct `extends`/`implements` type names for local Java declarations,
229
+ scoped identically to `declared_type_scopes` (`declaration_scope`) so a
230
+ lookup at a given `at_byte` only ever sees the declaration actually in
231
+ scope there.
232
+
233
+ Records an entry for *every* type declaration, even one with no
234
+ `extends`/`implements` (an empty `direct` list) — such a declaration
235
+ still needs a place in the table so `_supertypes_at` can pick it as the
236
+ innermost match and correctly shadow an unrelated outer/enclosing
237
+ declaration of the same simple name (a supertype-less local class named
238
+ the same as a top-level class that *does* implement something) — F-05,
239
+ PROBLEMS.md P4.6. Omitting the empty-`direct` case left nothing in the
240
+ table to shadow with, so the outer declaration's supertypes leaked
241
+ through untouched."""
242
+ supertypes: LocalSupertypes = {}
243
+ for node in _walk(root):
244
+ if node.type not in TYPE_DECLARATIONS:
245
+ continue
246
+ name = _text(node.child_by_field_name("name"), source)
247
+ if not name:
248
+ continue
249
+ direct: List[str] = []
250
+ direct.extend(_type_names_in(node.child_by_field_name("superclass"), source))
251
+ direct.extend(_type_names_in(node.child_by_field_name("interfaces"), source))
252
+ for child in node.children:
253
+ if child.type == "extends_interfaces":
254
+ direct.extend(_type_names_in(child, source))
255
+ start, end = declaration_scope(node, root)
256
+ supertypes.setdefault(name, []).append((start, end, direct))
257
+ return supertypes
258
+
259
+
260
+ def _supertypes_at(local_supertypes: LocalSupertypes, name: str, at_byte: int) -> List[str]:
261
+ """Only the *innermost* enclosing declaration's supertypes — mirroring
262
+ `declared_type_of`'s smallest-range selection. A flat union across every
263
+ scope containing `at_byte` is wrong whenever one declaration's scope
264
+ nests inside another's: a top-level class is (correctly) visible for the
265
+ entire compilation unit (`declaration_scope`'s documented `root` case),
266
+ so a same-named local class declared anywhere inside a method would
267
+ still satisfy that top-level scope's range check too. Without narrowing
268
+ to the innermost match, the unrelated top-level class's `extends`/
269
+ `implements` leaked into the local class's own `assignability_provenance`
270
+ lookup (F-05, PROBLEMS.md P4.6) — the same false-positive shape F-02
271
+ fixed for sibling-method local classes, surviving here for outer/local
272
+ shadowing."""
273
+ matches = [
274
+ (end - start, -start, direct)
275
+ for start, end, direct in local_supertypes.get(name, ())
276
+ if start <= at_byte < end
277
+ ]
278
+ if not matches:
279
+ return []
280
+ matches.sort(key=lambda m: (m[0], m[1]))
281
+ return matches[0][2]
282
+
283
+
284
+ def assignability_provenance(
285
+ raw_type: str,
286
+ imports: Set[str],
287
+ local_types: TypeScopes,
288
+ local_supertypes: LocalSupertypes,
289
+ qualified_name: str,
290
+ at_byte: int,
291
+ ) -> Provenance:
292
+ """Like `type_provenance`, but follows local `extends`/`implements`.
293
+
294
+ This stays compilation-unit local: external library hierarchies are not
295
+ guessed here, because callers need to choose where a low-confidence
296
+ external subtype fallback is acceptable.
297
+ """
298
+ direct = type_provenance(raw_type, imports, local_types, qualified_name, at_byte)
299
+ if direct == "confirmed":
300
+ return "confirmed"
301
+
302
+ start = simple_type(raw_type)
303
+ if not type_declared_at(local_types, start, at_byte):
304
+ return direct
305
+
306
+ seen: Set[str] = set()
307
+ stack = list(_supertypes_at(local_supertypes, start, at_byte))
308
+ while stack:
309
+ current = stack.pop()
310
+ current_simple = simple_type(current)
311
+ if current_simple in seen:
312
+ continue
313
+ seen.add(current_simple)
314
+ verdict = type_provenance(current, imports, local_types, qualified_name, at_byte)
315
+ if verdict == "confirmed":
316
+ return "confirmed"
317
+ if verdict == "ambiguous":
318
+ return "ambiguous"
319
+ stack.extend(_supertypes_at(local_supertypes, current_simple, at_byte))
320
+ return direct
321
+
322
+
323
+ @dataclass
324
+ class JavaTypeContext:
325
+ """Per-compilation-unit provenance data — computed once per file
326
+ (`build_type_context`) and reused for every function/call-site/
327
+ annotation lookup within it, rather than re-walking the whole file's
328
+ CST on every single check."""
329
+
330
+ imports: Set[str] = field(default_factory=set)
331
+ local_types: TypeScopes = field(default_factory=dict)
332
+ declared_types: DeclaredTypes = field(default_factory=dict)
333
+ local_supertypes: LocalSupertypes = field(default_factory=dict)
334
+
335
+
336
+ def build_type_context(root: Node, source: bytes) -> JavaTypeContext:
337
+ return JavaTypeContext(
338
+ imports=imports_of(root, source),
339
+ local_types=declared_type_scopes(root, source),
340
+ declared_types=declared_types_of(root, source),
341
+ local_supertypes=local_supertypes_of(root, source),
342
+ )
@@ -0,0 +1,8 @@
1
+ """Landing spot for the Python `LanguageBackend`.
2
+
3
+ `backend.py` is a thin shim over the existing top-level Python modules
4
+ (`parser/`, `detectors/`, `utils/config.py`) so the `LanguageBackend`
5
+ contract has one real, working implementation. It is NOT the P4.1-P4.7
6
+ migration: those modules still live at their current locations, unmoved and
7
+ unmodified. Refactoring them to live under this package is P4.1-P4.7.
8
+ """
@@ -0,0 +1,94 @@
1
+ """Thin `LanguageBackend` shim over the existing Python pipeline.
2
+
3
+ `parser()` is wired to the real migrated parser (`lang/python/parser.py`,
4
+ P4.1). `resolver_config()`/`cfg_config()` are wired to the real P4.4/P4.6
5
+ Python policy (`lang/python/resolver.py::default_python_resolver_config()`,
6
+ `lang/python/cfg.py::default_python_cfg_config()`) — the same objects
7
+ `graph/builder.py`'s pre-P4.4/P4.6 lazy fallback already defaults to, so
8
+ callers get byte-identical behavior whether or not they go through this
9
+ method. Everything else still wraps the current top-level modules
10
+ (`parser.extractor.SymbolExtractor`, `detectors.*`, `utils.config.
11
+ ProjectConfig`) as-is, unmodified.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ from pathlib import Path
17
+ from typing import List
18
+
19
+ from adl_drift.detectors.async_patterns import AsyncPatternDetector
20
+ from adl_drift.detectors.base import BaseDetector
21
+ from adl_drift.detectors.config import ConfigDetector
22
+ from adl_drift.detectors.data_access import DataAccessDetector
23
+ from adl_drift.detectors.decorators import DecoratorDetector
24
+ from adl_drift.detectors.dynamic_patterns import DynamicPatternDetector
25
+ from adl_drift.detectors.errors import ErrorPatternDetector
26
+ from adl_drift.detectors.imports import ImportDetector
27
+ from adl_drift.detectors.logging_patterns import LoggingDetector
28
+ from adl_drift.detectors.routes import DjangoURLDetector, RouteDetector
29
+ from adl_drift.detectors.security import SecurityDetector
30
+ from adl_drift.detectors.tests import TestDetector
31
+ from adl_drift.detectors.type_conventions import TypeConventionDetector
32
+ from adl_drift.lang.base import CFGConfig, LanguageParser, ResolverConfig
33
+ from adl_drift.lang.python.cfg import default_python_cfg_config
34
+ from adl_drift.lang.python.parser import create_parser
35
+ from adl_drift.lang.python.resolver import default_python_resolver_config
36
+ from adl_drift.parser.extractor import SymbolExtractor
37
+ from adl_drift.utils.config import ProjectConfig as PythonProjectConfig
38
+
39
+
40
+ class PythonBackend:
41
+ """`LanguageBackend` for Python, satisfied structurally (no ABC needed)."""
42
+
43
+ name = "python"
44
+ file_extensions = (".py",)
45
+ project_markers = ("pyproject.toml", "setup.py", "setup.cfg")
46
+
47
+ def parser(self) -> LanguageParser:
48
+ return create_parser()
49
+
50
+ def extractor(self) -> SymbolExtractor:
51
+ return SymbolExtractor(self.parser())
52
+
53
+ def detectors(self) -> List[BaseDetector]:
54
+ parser = self.parser()
55
+ return [
56
+ RouteDetector(parser),
57
+ DjangoURLDetector(parser),
58
+ ErrorPatternDetector(parser),
59
+ DataAccessDetector(parser),
60
+ DecoratorDetector(parser),
61
+ AsyncPatternDetector(parser),
62
+ ImportDetector(parser),
63
+ SecurityDetector(parser),
64
+ TestDetector(parser),
65
+ ConfigDetector(parser),
66
+ LoggingDetector(parser),
67
+ TypeConventionDetector(parser),
68
+ DynamicPatternDetector(parser),
69
+ ]
70
+
71
+ def resolver_config(self) -> ResolverConfig:
72
+ return default_python_resolver_config()
73
+
74
+ def cfg_config(self) -> CFGConfig:
75
+ return default_python_cfg_config()
76
+
77
+ def project_config(self, root: Path) -> PythonProjectConfig:
78
+ return PythonProjectConfig(root)
79
+
80
+ def module_name(self, file: Path, root: Path) -> str:
81
+ """Mirrors `CallGraphBuilder._path_to_module` without depending on
82
+ it, so this shim stays decoupled from the (language-coupled, P4.4/
83
+ P4.5) graph builder internals.
84
+ """
85
+ file = Path(file)
86
+ root = Path(root)
87
+ try:
88
+ rel = file.relative_to(root)
89
+ except ValueError:
90
+ rel = file
91
+ parts = rel.with_suffix("").parts
92
+ if parts and parts[-1] == "__init__":
93
+ parts = parts[:-1]
94
+ return ".".join(parts)