adl-toolkit 0.0.0-stage → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/INSTALL_TOOLKIT.md +163 -0
- package/README.md +269 -2
- package/adl_drift/__init__.py +6 -0
- package/adl_drift/cli/__init__.py +1 -0
- package/adl_drift/cli/commands.py +993 -0
- package/adl_drift/cli/main.py +744 -0
- package/adl_drift/cortex/__init__.py +1 -0
- package/adl_drift/cortex/embeddings.py +96 -0
- package/adl_drift/cortex/memory.py +1749 -0
- package/adl_drift/cortex/patterns.py +89 -0
- package/adl_drift/cortex/telemetry.py +192 -0
- package/adl_drift/detectors/__init__.py +1 -0
- package/adl_drift/detectors/async_patterns.py +115 -0
- package/adl_drift/detectors/base.py +121 -0
- package/adl_drift/detectors/config.py +122 -0
- package/adl_drift/detectors/data_access.py +128 -0
- package/adl_drift/detectors/decorators.py +122 -0
- package/adl_drift/detectors/dynamic_patterns.py +260 -0
- package/adl_drift/detectors/errors.py +134 -0
- package/adl_drift/detectors/imports.py +132 -0
- package/adl_drift/detectors/logging_patterns.py +108 -0
- package/adl_drift/detectors/routes.py +257 -0
- package/adl_drift/detectors/security.py +236 -0
- package/adl_drift/detectors/taint.py +406 -0
- package/adl_drift/detectors/tests.py +95 -0
- package/adl_drift/detectors/type_conventions.py +81 -0
- package/adl_drift/graph/__init__.py +1 -0
- package/adl_drift/graph/builder.py +2670 -0
- package/adl_drift/graph/cfg.py +461 -0
- package/adl_drift/graph/cluster.py +40 -0
- package/adl_drift/graph/edges.py +86 -0
- package/adl_drift/graph/node.py +90 -0
- package/adl_drift/harness/__init__.py +10 -0
- package/adl_drift/harness/setup.py +832 -0
- package/adl_drift/harness/skills/drift-antigravity.md +19 -0
- package/adl_drift/harness/skills/drift-generic.md +19 -0
- package/adl_drift/harness/skills/drift.md +25 -0
- package/adl_drift/harness/watch.py +84 -0
- package/adl_drift/lang/__init__.py +8 -0
- package/adl_drift/lang/base.py +464 -0
- package/adl_drift/lang/java/__init__.py +6 -0
- package/adl_drift/lang/java/backend.py +97 -0
- package/adl_drift/lang/java/cfg.py +1424 -0
- package/adl_drift/lang/java/config.py +707 -0
- package/adl_drift/lang/java/detectors/__init__.py +29 -0
- package/adl_drift/lang/java/detectors/additional.py +445 -0
- package/adl_drift/lang/java/detectors/base.py +154 -0
- package/adl_drift/lang/java/detectors/data_access.py +148 -0
- package/adl_drift/lang/java/detectors/imports.py +53 -0
- package/adl_drift/lang/java/detectors/routes.py +159 -0
- package/adl_drift/lang/java/detectors/security.py +386 -0
- package/adl_drift/lang/java/detectors/tests.py +74 -0
- package/adl_drift/lang/java/extractor.py +582 -0
- package/adl_drift/lang/java/ids.py +99 -0
- package/adl_drift/lang/java/parser.py +49 -0
- package/adl_drift/lang/java/resolution/__init__.py +1 -0
- package/adl_drift/lang/java/resolution/constants.py +1619 -0
- package/adl_drift/lang/java/resolution/constructors.py +278 -0
- package/adl_drift/lang/java/resolution/context.py +305 -0
- package/adl_drift/lang/java/resolution/dispatch.py +540 -0
- package/adl_drift/lang/java/resolution/functional.py +1318 -0
- package/adl_drift/lang/java/resolution/generated.py +95 -0
- package/adl_drift/lang/java/resolution/model.py +153 -0
- package/adl_drift/lang/java/resolution/orchestration.py +866 -0
- package/adl_drift/lang/java/resolution/overloads.py +1275 -0
- package/adl_drift/lang/java/resolution/scopes.py +1111 -0
- package/adl_drift/lang/java/resolution/types.py +2947 -0
- package/adl_drift/lang/java/resolver.py +1401 -0
- package/adl_drift/lang/java/scopes.py +69 -0
- package/adl_drift/lang/java/taint.py +500 -0
- package/adl_drift/lang/java/type_provenance.py +342 -0
- package/adl_drift/lang/python/__init__.py +8 -0
- package/adl_drift/lang/python/backend.py +94 -0
- package/adl_drift/lang/python/cfg.py +537 -0
- package/adl_drift/lang/python/extractor.py +454 -0
- package/adl_drift/lang/python/parser.py +41 -0
- package/adl_drift/lang/python/resolver.py +1789 -0
- package/adl_drift/lang/python/taint.py +365 -0
- package/adl_drift/lang/registry.py +117 -0
- package/adl_drift/parser/__init__.py +1 -0
- package/adl_drift/parser/cache.py +38 -0
- package/adl_drift/parser/core.py +33 -0
- package/adl_drift/parser/extractor.py +17 -0
- package/adl_drift/parser/literal_eval.py +326 -0
- package/adl_drift/scanner/__init__.py +1 -0
- package/adl_drift/scanner/discovery.py +338 -0
- package/adl_drift/scanner/incremental.py +261 -0
- package/adl_drift/scanner/parallel.py +224 -0
- package/adl_drift/server/__init__.py +1 -0
- package/adl_drift/server/handlers.py +2849 -0
- package/adl_drift/server/mcp_server.py +127 -0
- package/adl_drift/server/mcp_stdio.py +335 -0
- package/adl_drift/server/protocol.py +71 -0
- package/adl_drift/server/static/index.html +696 -0
- package/adl_drift/server/web.py +350 -0
- package/adl_drift/utils/__init__.py +1 -0
- package/adl_drift/utils/config.py +77 -0
- package/adl_drift/utils/fswalk.py +52 -0
- package/adl_drift/utils/types.py +345 -0
- package/bin/adl-drift.mjs +35 -0
- package/bin/adl-toolkit.mjs +488 -0
- package/package.json +29 -4
- package/pyproject.toml +49 -0
- 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)
|