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,128 @@
|
|
|
1
|
+
"""Data access / ORM pattern detectors.
|
|
2
|
+
|
|
3
|
+
Finds SQLAlchemy, Django ORM, Peewee, Tortoise queries.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from typing import List, Dict, Any, Optional
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from tree_sitter import Node, Tree
|
|
11
|
+
|
|
12
|
+
from adl_drift.detectors.base import BaseDetector
|
|
13
|
+
from adl_drift.utils.types import Pattern, PatternCategory
|
|
14
|
+
from adl_drift.parser.core import PythonParser
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class DataAccessDetector(BaseDetector):
|
|
18
|
+
"""Detects database access patterns."""
|
|
19
|
+
|
|
20
|
+
ORM_PATTERNS = {
|
|
21
|
+
"sqlalchemy": [
|
|
22
|
+
"session.query", "Query(", ".filter(", ".all()", ".first()",
|
|
23
|
+
".join(", ".add(", ".commit()", ".rollback()",
|
|
24
|
+
],
|
|
25
|
+
"django": [
|
|
26
|
+
".objects.filter", ".objects.get", ".objects.all", ".objects.create",
|
|
27
|
+
".save(", ".delete(", ".update(", ".aggregate(",
|
|
28
|
+
],
|
|
29
|
+
"peewee": [
|
|
30
|
+
".select()", ".where(", ".join(", ".get()", ".create(",
|
|
31
|
+
],
|
|
32
|
+
"tortoise": [
|
|
33
|
+
".filter(", ".get(", ".all()", ".create(", ".update(",
|
|
34
|
+
],
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
def __init__(self, parser: PythonParser):
|
|
38
|
+
super().__init__(parser)
|
|
39
|
+
|
|
40
|
+
@property
|
|
41
|
+
def category(self) -> PatternCategory:
|
|
42
|
+
return PatternCategory.DATA_ACCESS
|
|
43
|
+
|
|
44
|
+
def detect(self, tree: Tree, source_bytes: bytes, file_path: Path) -> List[Pattern]:
|
|
45
|
+
patterns = []
|
|
46
|
+
|
|
47
|
+
for node in self._walk(tree.root_node):
|
|
48
|
+
if node.type == "call":
|
|
49
|
+
pattern = self._analyze_call(node, source_bytes, file_path)
|
|
50
|
+
if pattern:
|
|
51
|
+
patterns.append(pattern)
|
|
52
|
+
elif node.type == "attribute":
|
|
53
|
+
pattern = self._analyze_attribute_chain(node, source_bytes, file_path)
|
|
54
|
+
if pattern:
|
|
55
|
+
patterns.append(pattern)
|
|
56
|
+
|
|
57
|
+
return patterns
|
|
58
|
+
|
|
59
|
+
def _analyze_call(self, node: Node, source: bytes, path: Path) -> Optional[Pattern]:
|
|
60
|
+
text = self.parser.node_text(node, source)
|
|
61
|
+
|
|
62
|
+
for orm, markers in self.ORM_PATTERNS.items():
|
|
63
|
+
for marker in markers:
|
|
64
|
+
if marker in text:
|
|
65
|
+
return self._make_pattern(
|
|
66
|
+
name=f"{orm}_query",
|
|
67
|
+
description=f"{orm.capitalize()} database query: {text[:60]}",
|
|
68
|
+
node=node,
|
|
69
|
+
file_path=path,
|
|
70
|
+
confidence=0.8,
|
|
71
|
+
metadata={
|
|
72
|
+
"orm": orm,
|
|
73
|
+
"operation": marker.strip(".()"),
|
|
74
|
+
"query_text": text[:200],
|
|
75
|
+
}
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
func_node = node.child_by_field_name("function")
|
|
79
|
+
if func_node:
|
|
80
|
+
func_text = self.parser.node_text(func_node, source)
|
|
81
|
+
if func_text in {"execute", "executemany", "run"}:
|
|
82
|
+
args = node.child_by_field_name("arguments")
|
|
83
|
+
if args:
|
|
84
|
+
for arg in args.children:
|
|
85
|
+
if arg.type == "string":
|
|
86
|
+
arg_text = self.parser.node_text(arg, source).lower()
|
|
87
|
+
if any(kw in arg_text for kw in ["select", "insert", "update", "delete"]):
|
|
88
|
+
return self._make_pattern(
|
|
89
|
+
name="raw_sql",
|
|
90
|
+
description=f"Raw SQL execution: {func_text}",
|
|
91
|
+
node=node,
|
|
92
|
+
file_path=path,
|
|
93
|
+
confidence=0.9,
|
|
94
|
+
metadata={
|
|
95
|
+
"type": "raw_sql",
|
|
96
|
+
"sql_preview": arg_text[:100],
|
|
97
|
+
}
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
return None
|
|
101
|
+
|
|
102
|
+
def _analyze_attribute_chain(self, node: Node, source: bytes, path: Path) -> Optional[Pattern]:
|
|
103
|
+
text = self.parser.node_text(node, source)
|
|
104
|
+
|
|
105
|
+
if ".objects." in text:
|
|
106
|
+
parts = text.split(".objects.")
|
|
107
|
+
if len(parts) >= 2:
|
|
108
|
+
model = parts[0].split(".")[-1]
|
|
109
|
+
operation = parts[1].split("(")[0]
|
|
110
|
+
return self._make_pattern(
|
|
111
|
+
name="django_orm_chain",
|
|
112
|
+
description=f"Django ORM: {model}.objects.{operation}",
|
|
113
|
+
node=node,
|
|
114
|
+
file_path=path,
|
|
115
|
+
confidence=0.85,
|
|
116
|
+
metadata={
|
|
117
|
+
"orm": "django",
|
|
118
|
+
"model": model,
|
|
119
|
+
"operation": operation,
|
|
120
|
+
}
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
return None
|
|
124
|
+
|
|
125
|
+
def _walk(self, node):
|
|
126
|
+
yield node
|
|
127
|
+
for child in node.children:
|
|
128
|
+
yield from self._walk(child)
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
"""Decorator pattern detectors.
|
|
2
|
+
|
|
3
|
+
Finds custom decorators, caching decorators, auth decorators, etc.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from typing import List, Dict, Any, Set
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from tree_sitter import Node, Tree
|
|
11
|
+
|
|
12
|
+
from adl_drift.detectors.base import BaseDetector
|
|
13
|
+
from adl_drift.utils.types import Pattern, PatternCategory
|
|
14
|
+
from adl_drift.parser.core import PythonParser
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class DecoratorDetector(BaseDetector):
|
|
18
|
+
"""Detects decorator usage patterns."""
|
|
19
|
+
|
|
20
|
+
BUILT_IN = {"staticmethod", "classmethod", "property", "abstractmethod", "dataclass"}
|
|
21
|
+
CACHE_DECORATORS = {"lru_cache", "cache", "cached_property", "memoize"}
|
|
22
|
+
AUTH_DECORATORS = {"login_required", "permission_required", "requires_auth", "authenticated"}
|
|
23
|
+
|
|
24
|
+
def __init__(self, parser: PythonParser):
|
|
25
|
+
super().__init__(parser)
|
|
26
|
+
|
|
27
|
+
@property
|
|
28
|
+
def category(self) -> PatternCategory:
|
|
29
|
+
return PatternCategory.DECORATORS
|
|
30
|
+
|
|
31
|
+
def detect(self, tree: Tree, source_bytes: bytes, file_path: Path) -> List[Pattern]:
|
|
32
|
+
patterns = []
|
|
33
|
+
|
|
34
|
+
for node in self._walk(tree.root_node):
|
|
35
|
+
if node.type == "decorator":
|
|
36
|
+
pattern = self._analyze_decorator(node, source_bytes, file_path)
|
|
37
|
+
if pattern:
|
|
38
|
+
patterns.append(pattern)
|
|
39
|
+
elif node.type == "function_definition":
|
|
40
|
+
if self._is_decorator_factory(node, source_bytes):
|
|
41
|
+
patterns.append(self._make_pattern(
|
|
42
|
+
name="custom_decorator",
|
|
43
|
+
description="Custom decorator factory function",
|
|
44
|
+
node=node,
|
|
45
|
+
file_path=file_path,
|
|
46
|
+
confidence=0.8,
|
|
47
|
+
metadata={"type": "decorator_factory"}
|
|
48
|
+
))
|
|
49
|
+
|
|
50
|
+
return patterns
|
|
51
|
+
|
|
52
|
+
def _analyze_decorator(self, node: Node, source: bytes, path: Path) -> Optional[Pattern]:
|
|
53
|
+
text = self.parser.node_text(node, source).strip()
|
|
54
|
+
name = self._extract_name(node, source)
|
|
55
|
+
|
|
56
|
+
if name in self.BUILT_IN:
|
|
57
|
+
return self._make_pattern(
|
|
58
|
+
name=f"builtin_decorator:{name}",
|
|
59
|
+
description=f"Built-in decorator: @{name}",
|
|
60
|
+
node=node,
|
|
61
|
+
file_path=path,
|
|
62
|
+
confidence=0.99,
|
|
63
|
+
metadata={"category": "builtin", "decorator": name}
|
|
64
|
+
)
|
|
65
|
+
elif name in self.CACHE_DECORATORS:
|
|
66
|
+
return self._make_pattern(
|
|
67
|
+
name=f"cache_decorator:{name}",
|
|
68
|
+
description=f"Caching decorator: @{name}",
|
|
69
|
+
node=node,
|
|
70
|
+
file_path=path,
|
|
71
|
+
confidence=0.95,
|
|
72
|
+
metadata={"category": "caching", "decorator": name}
|
|
73
|
+
)
|
|
74
|
+
elif name in self.AUTH_DECORATORS:
|
|
75
|
+
return self._make_pattern(
|
|
76
|
+
name=f"auth_decorator:{name}",
|
|
77
|
+
description=f"Authentication decorator: @{name}",
|
|
78
|
+
node=node,
|
|
79
|
+
file_path=path,
|
|
80
|
+
confidence=0.9,
|
|
81
|
+
metadata={"category": "auth", "decorator": name}
|
|
82
|
+
)
|
|
83
|
+
else:
|
|
84
|
+
return self._make_pattern(
|
|
85
|
+
name=f"custom_decorator:{name}",
|
|
86
|
+
description=f"Custom decorator: @{name}",
|
|
87
|
+
node=node,
|
|
88
|
+
file_path=path,
|
|
89
|
+
confidence=0.7,
|
|
90
|
+
metadata={"category": "custom", "decorator": name}
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
def _extract_name(self, node: Node, source: bytes) -> str:
|
|
94
|
+
for child in node.children:
|
|
95
|
+
if child.type == "identifier":
|
|
96
|
+
return self.parser.node_text(child, source)
|
|
97
|
+
elif child.type == "attribute":
|
|
98
|
+
attr = child.child_by_field_name("attribute")
|
|
99
|
+
if attr:
|
|
100
|
+
return self.parser.node_text(attr, source)
|
|
101
|
+
elif child.type == "call":
|
|
102
|
+
func = child.child_by_field_name("function")
|
|
103
|
+
if func:
|
|
104
|
+
if func.type == "identifier":
|
|
105
|
+
return self.parser.node_text(func, source)
|
|
106
|
+
elif func.type == "attribute":
|
|
107
|
+
attr = func.child_by_field_name("attribute")
|
|
108
|
+
if attr:
|
|
109
|
+
return self.parser.node_text(attr, source)
|
|
110
|
+
return "unknown"
|
|
111
|
+
|
|
112
|
+
def _is_decorator_factory(self, node: Node, source: bytes) -> bool:
|
|
113
|
+
body = node.child_by_field_name("body")
|
|
114
|
+
if not body:
|
|
115
|
+
return False
|
|
116
|
+
body_text = self.parser.node_text(body, source)
|
|
117
|
+
return "def " in body_text and "return" in body_text
|
|
118
|
+
|
|
119
|
+
def _walk(self, node):
|
|
120
|
+
yield node
|
|
121
|
+
for child in node.children:
|
|
122
|
+
yield from self._walk(child)
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
"""Dynamic dispatch / reflection pattern detector (P2.12).
|
|
2
|
+
|
|
3
|
+
Flags 8 categories of dynamic code that defeat static analysis:
|
|
4
|
+
getattr/setattr/delattr with non-literal name args,
|
|
5
|
+
__getattr__/__getattribute__ definitions (open attribute universe),
|
|
6
|
+
exec/eval (cross-tagged with SECURITY),
|
|
7
|
+
importlib.import_module / __import__ calls,
|
|
8
|
+
metaclass= class kwargs,
|
|
9
|
+
__init_subclass__ definitions (subclass registries),
|
|
10
|
+
subscript-call dispatch (d[k]()),
|
|
11
|
+
decorator bodies that return a new locally-defined class (decorator rebind).
|
|
12
|
+
|
|
13
|
+
Each Pattern.metadata carries:
|
|
14
|
+
resolvable: "candidates" (literals available), "partial", or "none"
|
|
15
|
+
reason: short human-readable explanation for the agent
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
from typing import List, Optional
|
|
22
|
+
|
|
23
|
+
from tree_sitter import Node, Tree
|
|
24
|
+
|
|
25
|
+
from adl_drift.detectors.base import BaseDetector
|
|
26
|
+
from adl_drift.utils.types import Pattern, PatternCategory, SourcePosition
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class DynamicPatternDetector(BaseDetector):
|
|
30
|
+
"""Detects patterns where static resolution is unreliable or impossible."""
|
|
31
|
+
|
|
32
|
+
@property
|
|
33
|
+
def category(self) -> PatternCategory:
|
|
34
|
+
return PatternCategory.DYNAMIC
|
|
35
|
+
|
|
36
|
+
def detect(self, tree: Tree, source_bytes: bytes, file_path: Path) -> List[Pattern]:
|
|
37
|
+
patterns: List[Pattern] = []
|
|
38
|
+
for node in self._walk(tree.root_node):
|
|
39
|
+
t = node.type
|
|
40
|
+
if t == "call":
|
|
41
|
+
patterns.extend(self._check_call(node, source_bytes, file_path))
|
|
42
|
+
elif t == "subscript":
|
|
43
|
+
patterns.extend(self._check_subscript_call(node, source_bytes, file_path))
|
|
44
|
+
elif t == "class_definition":
|
|
45
|
+
patterns.extend(self._check_class_def(node, source_bytes, file_path))
|
|
46
|
+
elif t == "function_definition":
|
|
47
|
+
patterns.extend(self._check_func_def(node, source_bytes, file_path))
|
|
48
|
+
return patterns
|
|
49
|
+
|
|
50
|
+
# ------------------------------------------------------------------ calls
|
|
51
|
+
|
|
52
|
+
def _check_call(self, node: Node, source: bytes, file_path: Path) -> List[Pattern]:
|
|
53
|
+
out: List[Pattern] = []
|
|
54
|
+
func = node.child_by_field_name("function")
|
|
55
|
+
if func is None:
|
|
56
|
+
return out
|
|
57
|
+
func_text = self.parser.node_text(func, source).strip()
|
|
58
|
+
|
|
59
|
+
# getattr / setattr / delattr with non-literal name argument
|
|
60
|
+
if func_text in ("getattr", "setattr", "delattr"):
|
|
61
|
+
args = node.child_by_field_name("arguments")
|
|
62
|
+
name_arg = self._nth_positional_arg(args, 1) # second positional
|
|
63
|
+
if name_arg is not None and not self._is_literal_string(name_arg):
|
|
64
|
+
resolvable = "candidates" if self._is_prefixed_fstring(name_arg, source) else "none"
|
|
65
|
+
out.append(self._make_pattern(
|
|
66
|
+
name=f"dynamic_{func_text}",
|
|
67
|
+
description=(
|
|
68
|
+
f"{func_text}() with non-literal name — "
|
|
69
|
+
"attribute resolution bypasses static analysis"
|
|
70
|
+
),
|
|
71
|
+
node=node, file_path=file_path, confidence=0.9,
|
|
72
|
+
metadata={
|
|
73
|
+
"function": func_text,
|
|
74
|
+
"resolvable": resolvable,
|
|
75
|
+
"reason": "non-literal attribute name; candidates may be recoverable from f-string prefix",
|
|
76
|
+
},
|
|
77
|
+
))
|
|
78
|
+
|
|
79
|
+
# importlib.import_module / __import__
|
|
80
|
+
elif func_text in ("importlib.import_module", "__import__"):
|
|
81
|
+
args = node.child_by_field_name("arguments")
|
|
82
|
+
mod_arg = self._nth_positional_arg(args, 0)
|
|
83
|
+
resolvable = "candidates" if (
|
|
84
|
+
mod_arg is not None and self._is_literal_string(mod_arg)
|
|
85
|
+
) else ("candidates" if (
|
|
86
|
+
mod_arg is not None and self._is_prefixed_fstring(mod_arg, source)
|
|
87
|
+
) else "none")
|
|
88
|
+
out.append(self._make_pattern(
|
|
89
|
+
name="dynamic_import",
|
|
90
|
+
description=(
|
|
91
|
+
f"{func_text}() — dynamically imported module "
|
|
92
|
+
"invisible to import graph"
|
|
93
|
+
),
|
|
94
|
+
node=node, file_path=file_path, confidence=0.85,
|
|
95
|
+
metadata={
|
|
96
|
+
"function": func_text,
|
|
97
|
+
"resolvable": resolvable,
|
|
98
|
+
"reason": "dynamic import; module may be inferrable from literal prefix",
|
|
99
|
+
},
|
|
100
|
+
))
|
|
101
|
+
|
|
102
|
+
# exec / eval (cross-tag: also caught by SecurityDetector with HIGH severity)
|
|
103
|
+
elif func_text in ("exec", "eval"):
|
|
104
|
+
args = node.child_by_field_name("arguments")
|
|
105
|
+
code_arg = self._nth_positional_arg(args, 0)
|
|
106
|
+
resolvable = "candidates" if (
|
|
107
|
+
code_arg is not None and self._is_literal_string(code_arg)
|
|
108
|
+
) else "none"
|
|
109
|
+
out.append(self._make_pattern(
|
|
110
|
+
name="dynamic_exec_eval",
|
|
111
|
+
description=(
|
|
112
|
+
f"{func_text}() — emitted symbols invisible to graph; "
|
|
113
|
+
"static analysis confidence reduced"
|
|
114
|
+
),
|
|
115
|
+
node=node, file_path=file_path, confidence=0.8,
|
|
116
|
+
metadata={
|
|
117
|
+
"function": func_text,
|
|
118
|
+
"resolvable": resolvable,
|
|
119
|
+
"reason": "synthesized symbols not in graph; recoverable when arg is a literal template",
|
|
120
|
+
},
|
|
121
|
+
))
|
|
122
|
+
|
|
123
|
+
return out
|
|
124
|
+
|
|
125
|
+
# ---------------------------------------------------- subscript-call dispatch
|
|
126
|
+
|
|
127
|
+
def _check_subscript_call(self, node: Node, source: bytes, file_path: Path) -> List[Pattern]:
|
|
128
|
+
"""Detect d[k]() — parent of subscript node is a call."""
|
|
129
|
+
parent = node.parent
|
|
130
|
+
if parent is None or parent.type != "call":
|
|
131
|
+
return []
|
|
132
|
+
func_field = parent.child_by_field_name("function")
|
|
133
|
+
if func_field is None or func_field != node:
|
|
134
|
+
return []
|
|
135
|
+
return [self._make_pattern(
|
|
136
|
+
name="subscript_dispatch",
|
|
137
|
+
description=(
|
|
138
|
+
"subscript-call dispatch d[k]() — target determined at runtime; "
|
|
139
|
+
"CALLS edges missing for all candidates"
|
|
140
|
+
),
|
|
141
|
+
node=parent, file_path=file_path, confidence=0.8,
|
|
142
|
+
metadata={
|
|
143
|
+
"resolvable": "candidates",
|
|
144
|
+
"reason": "literal dict values may be recoverable as candidate targets",
|
|
145
|
+
},
|
|
146
|
+
)]
|
|
147
|
+
|
|
148
|
+
# ------------------------------------------------------------ class-level
|
|
149
|
+
|
|
150
|
+
def _check_class_def(self, node: Node, source: bytes, file_path: Path) -> List[Pattern]:
|
|
151
|
+
out: List[Pattern] = []
|
|
152
|
+
|
|
153
|
+
# metaclass= keyword argument
|
|
154
|
+
args_node = node.child_by_field_name("superclasses")
|
|
155
|
+
if args_node is not None:
|
|
156
|
+
for child in args_node.children:
|
|
157
|
+
if child.type == "keyword_argument":
|
|
158
|
+
kname = child.child_by_field_name("name")
|
|
159
|
+
if kname is not None and self.parser.node_text(kname, source).strip() == "metaclass":
|
|
160
|
+
out.append(self._make_pattern(
|
|
161
|
+
name="metaclass_class",
|
|
162
|
+
description=(
|
|
163
|
+
"class uses metaclass= — injected attributes/methods "
|
|
164
|
+
"invisible to static graph"
|
|
165
|
+
),
|
|
166
|
+
node=node, file_path=file_path, confidence=0.9,
|
|
167
|
+
metadata={
|
|
168
|
+
"resolvable": "partial",
|
|
169
|
+
"reason": "metaclass __new__ may inject literal-keyed namespace entries",
|
|
170
|
+
},
|
|
171
|
+
))
|
|
172
|
+
break
|
|
173
|
+
|
|
174
|
+
return out
|
|
175
|
+
|
|
176
|
+
# ----------------------------------------------------------- function-level
|
|
177
|
+
|
|
178
|
+
def _check_func_def(self, node: Node, source: bytes, file_path: Path) -> List[Pattern]:
|
|
179
|
+
out: List[Pattern] = []
|
|
180
|
+
name_node = node.child_by_field_name("name")
|
|
181
|
+
if name_node is None:
|
|
182
|
+
return out
|
|
183
|
+
name = self.parser.node_text(name_node, source).strip()
|
|
184
|
+
|
|
185
|
+
# __getattr__ / __getattribute__ definitions → open attribute universe
|
|
186
|
+
if name in ("__getattr__", "__getattribute__"):
|
|
187
|
+
out.append(self._make_pattern(
|
|
188
|
+
name="open_attr_universe",
|
|
189
|
+
description=(
|
|
190
|
+
f"{name}() defined — attribute reads on instances are "
|
|
191
|
+
"unresolvable; impact routed through this method"
|
|
192
|
+
),
|
|
193
|
+
node=node, file_path=file_path, confidence=0.9,
|
|
194
|
+
metadata={
|
|
195
|
+
"function": name,
|
|
196
|
+
"resolvable": "none",
|
|
197
|
+
"reason": "open attribute universe; unresolved reads should emit DYNAMIC_ATTR edges",
|
|
198
|
+
},
|
|
199
|
+
))
|
|
200
|
+
|
|
201
|
+
# __init_subclass__ → subclass registry pattern
|
|
202
|
+
elif name == "__init_subclass__":
|
|
203
|
+
out.append(self._make_pattern(
|
|
204
|
+
name="subclass_registry",
|
|
205
|
+
description=(
|
|
206
|
+
"__init_subclass__ defined — subclasses auto-registered; "
|
|
207
|
+
"registry lookup may be recoverable via INHERITS walk"
|
|
208
|
+
),
|
|
209
|
+
node=node, file_path=file_path, confidence=0.85,
|
|
210
|
+
metadata={
|
|
211
|
+
"function": name,
|
|
212
|
+
"resolvable": "candidates",
|
|
213
|
+
"reason": "INHERITS edges to subclasses exist; kwarg literals identify registry keys",
|
|
214
|
+
},
|
|
215
|
+
))
|
|
216
|
+
|
|
217
|
+
return out
|
|
218
|
+
|
|
219
|
+
# ----------------------------------------------------------------- helpers
|
|
220
|
+
|
|
221
|
+
@staticmethod
|
|
222
|
+
def _nth_positional_arg(args_node: Optional[Node], n: int) -> Optional[Node]:
|
|
223
|
+
"""Return the n-th positional argument node, skipping *args/**kwargs."""
|
|
224
|
+
if args_node is None:
|
|
225
|
+
return None
|
|
226
|
+
pos = 0
|
|
227
|
+
for child in args_node.children:
|
|
228
|
+
if child.type in (",", "(", ")", "*", "**"):
|
|
229
|
+
continue
|
|
230
|
+
if child.type in ("keyword_argument", "dictionary_splat_argument",
|
|
231
|
+
"list_splat_argument"):
|
|
232
|
+
continue
|
|
233
|
+
if pos == n:
|
|
234
|
+
return child
|
|
235
|
+
pos += 1
|
|
236
|
+
return None
|
|
237
|
+
|
|
238
|
+
@staticmethod
|
|
239
|
+
def _is_literal_string(node: Node) -> bool:
|
|
240
|
+
return node.type in ("string", "concatenated_string")
|
|
241
|
+
|
|
242
|
+
@staticmethod
|
|
243
|
+
def _is_prefixed_fstring(node: Node, source: bytes) -> bool:
|
|
244
|
+
"""True when the node is an f-string with at least one literal prefix."""
|
|
245
|
+
if node.type not in ("string", "concatenated_string"):
|
|
246
|
+
return False
|
|
247
|
+
try:
|
|
248
|
+
text = source[node.start_byte:node.end_byte].decode("utf-8", errors="replace")
|
|
249
|
+
except Exception:
|
|
250
|
+
return False
|
|
251
|
+
return text.startswith(('f"', "f'", 'f"""', "f'''"))
|
|
252
|
+
|
|
253
|
+
def _walk(self, node: Node):
|
|
254
|
+
yield node
|
|
255
|
+
for child in node.children:
|
|
256
|
+
yield from self._walk(child)
|
|
257
|
+
|
|
258
|
+
@staticmethod
|
|
259
|
+
def _line_position(line: int) -> SourcePosition:
|
|
260
|
+
return SourcePosition(line=line, column=0, end_line=line, end_column=0)
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
"""Error handling pattern detectors.
|
|
2
|
+
|
|
3
|
+
Finds try/except blocks, custom exceptions, error propagation patterns.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from typing import List, Dict, Any
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from tree_sitter import Node, Tree
|
|
11
|
+
|
|
12
|
+
from adl_drift.detectors.base import BaseDetector
|
|
13
|
+
from adl_drift.utils.types import Pattern, PatternCategory
|
|
14
|
+
from adl_drift.parser.core import PythonParser
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class ErrorPatternDetector(BaseDetector):
|
|
18
|
+
"""Detects error handling patterns."""
|
|
19
|
+
|
|
20
|
+
def __init__(self, parser: PythonParser):
|
|
21
|
+
super().__init__(parser)
|
|
22
|
+
|
|
23
|
+
@property
|
|
24
|
+
def category(self) -> PatternCategory:
|
|
25
|
+
return PatternCategory.ERRORS
|
|
26
|
+
|
|
27
|
+
def detect(self, tree: Tree, source_bytes: bytes, file_path: Path) -> List[Pattern]:
|
|
28
|
+
patterns = []
|
|
29
|
+
|
|
30
|
+
for node in self._walk(tree.root_node):
|
|
31
|
+
if node.type == "try_statement":
|
|
32
|
+
patterns.extend(self._analyze_try_block(node, source_bytes, file_path))
|
|
33
|
+
elif node.type == "raise_statement":
|
|
34
|
+
patterns.extend(self._analyze_raise(node, source_bytes, file_path))
|
|
35
|
+
elif node.type == "class_definition":
|
|
36
|
+
patterns.extend(self._analyze_exception_class(node, source_bytes, file_path))
|
|
37
|
+
|
|
38
|
+
return patterns
|
|
39
|
+
|
|
40
|
+
def _analyze_try_block(self, node: Node, source: bytes, path: Path) -> List[Pattern]:
|
|
41
|
+
patterns = []
|
|
42
|
+
|
|
43
|
+
for child in node.children:
|
|
44
|
+
if child.type != "except_clause":
|
|
45
|
+
continue
|
|
46
|
+
|
|
47
|
+
except_text = self.parser.node_text(child, source)
|
|
48
|
+
|
|
49
|
+
if "except:" in except_text and "except Exception" not in except_text:
|
|
50
|
+
patterns.append(self._make_pattern(
|
|
51
|
+
name="bare_except",
|
|
52
|
+
description="Bare except clause catches KeyboardInterrupt, SystemExit",
|
|
53
|
+
node=child,
|
|
54
|
+
file_path=path,
|
|
55
|
+
confidence=0.85,
|
|
56
|
+
metadata={"severity": "warning", "fix": "Use 'except Exception:'"}
|
|
57
|
+
))
|
|
58
|
+
elif "except Exception:" in except_text:
|
|
59
|
+
patterns.append(self._make_pattern(
|
|
60
|
+
name="broad_exception",
|
|
61
|
+
description="Catching all exceptions may hide bugs",
|
|
62
|
+
node=child,
|
|
63
|
+
file_path=path,
|
|
64
|
+
confidence=0.7,
|
|
65
|
+
metadata={"severity": "info"}
|
|
66
|
+
))
|
|
67
|
+
|
|
68
|
+
except_body = child.child_by_field_name("body")
|
|
69
|
+
if except_body:
|
|
70
|
+
body_text = self.parser.node_text(except_body, source)
|
|
71
|
+
if "pass" in body_text and len(body_text.strip()) < 10:
|
|
72
|
+
patterns.append(self._make_pattern(
|
|
73
|
+
name="empty_except",
|
|
74
|
+
description="Except block does nothing (pass)",
|
|
75
|
+
node=child,
|
|
76
|
+
file_path=path,
|
|
77
|
+
confidence=0.9,
|
|
78
|
+
metadata={"severity": "error"}
|
|
79
|
+
))
|
|
80
|
+
|
|
81
|
+
return patterns
|
|
82
|
+
|
|
83
|
+
def _analyze_raise(self, node: Node, source: bytes, path: Path) -> List[Pattern]:
|
|
84
|
+
patterns = []
|
|
85
|
+
text = self.parser.node_text(node, source)
|
|
86
|
+
|
|
87
|
+
if "from" not in text:
|
|
88
|
+
parent = node.parent
|
|
89
|
+
while parent:
|
|
90
|
+
if parent.type == "except_clause":
|
|
91
|
+
patterns.append(self._make_pattern(
|
|
92
|
+
name="raise_without_from",
|
|
93
|
+
description="Re-raising without 'from' loses original traceback context",
|
|
94
|
+
node=node,
|
|
95
|
+
file_path=path,
|
|
96
|
+
confidence=0.6,
|
|
97
|
+
metadata={"severity": "info", "fix": "Use 'raise NewError from old_e'"}
|
|
98
|
+
))
|
|
99
|
+
break
|
|
100
|
+
parent = parent.parent
|
|
101
|
+
|
|
102
|
+
return patterns
|
|
103
|
+
|
|
104
|
+
def _analyze_exception_class(self, node: Node, source: bytes, path: Path) -> List[Pattern]:
|
|
105
|
+
patterns = []
|
|
106
|
+
|
|
107
|
+
bases = node.child_by_field_name("superclasses")
|
|
108
|
+
if not bases:
|
|
109
|
+
return patterns
|
|
110
|
+
|
|
111
|
+
bases_text = self.parser.node_text(bases, source)
|
|
112
|
+
if "Exception" in bases_text or "BaseException" in bases_text:
|
|
113
|
+
name_node = node.child_by_field_name("name")
|
|
114
|
+
name = self.parser.node_text(name_node, source) if name_node else "<unknown>"
|
|
115
|
+
|
|
116
|
+
patterns.append(self._make_pattern(
|
|
117
|
+
name="custom_exception",
|
|
118
|
+
description=f"Custom exception class: {name}",
|
|
119
|
+
node=node,
|
|
120
|
+
file_path=path,
|
|
121
|
+
confidence=0.95,
|
|
122
|
+
metadata={
|
|
123
|
+
"exception_name": name,
|
|
124
|
+
"bases": bases_text,
|
|
125
|
+
"severity": "info",
|
|
126
|
+
}
|
|
127
|
+
))
|
|
128
|
+
|
|
129
|
+
return patterns
|
|
130
|
+
|
|
131
|
+
def _walk(self, node):
|
|
132
|
+
yield node
|
|
133
|
+
for child in node.children:
|
|
134
|
+
yield from self._walk(child)
|