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,132 @@
|
|
|
1
|
+
"""Import pattern detectors.
|
|
2
|
+
|
|
3
|
+
Analyzes import structure, unused imports, circular dependencies.
|
|
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 ImportDetector(BaseDetector):
|
|
18
|
+
"""Detects and analyzes import patterns."""
|
|
19
|
+
|
|
20
|
+
STDLIB_MODULES = {
|
|
21
|
+
"os", "sys", "json", "re", "collections", "itertools", "functools",
|
|
22
|
+
"pathlib", "typing", "dataclasses", "enum", "abc", "inspect",
|
|
23
|
+
"threading", "multiprocessing", "asyncio", "unittest", "logging",
|
|
24
|
+
"hashlib", "random", "datetime", "time", "math", "statistics",
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
def __init__(self, parser: PythonParser):
|
|
28
|
+
super().__init__(parser)
|
|
29
|
+
|
|
30
|
+
@property
|
|
31
|
+
def category(self) -> PatternCategory:
|
|
32
|
+
return PatternCategory.IMPORTS
|
|
33
|
+
|
|
34
|
+
def detect(self, tree: Tree, source_bytes: bytes, file_path: Path) -> List[Pattern]:
|
|
35
|
+
patterns = []
|
|
36
|
+
|
|
37
|
+
for node in self._walk(tree.root_node):
|
|
38
|
+
if node.type == "import_statement":
|
|
39
|
+
patterns.extend(self._analyze_import(node, source_bytes, file_path))
|
|
40
|
+
elif node.type == "import_from_statement":
|
|
41
|
+
patterns.extend(self._analyze_from_import(node, source_bytes, file_path))
|
|
42
|
+
|
|
43
|
+
return patterns
|
|
44
|
+
|
|
45
|
+
def _analyze_import(self, node: Node, source: bytes, path: Path) -> List[Pattern]:
|
|
46
|
+
patterns = []
|
|
47
|
+
|
|
48
|
+
for child in node.children:
|
|
49
|
+
if child.type == "dotted_name":
|
|
50
|
+
name = self.parser.node_text(child, source)
|
|
51
|
+
top_module = name.split(".")[0]
|
|
52
|
+
category = self._classify_module(top_module)
|
|
53
|
+
|
|
54
|
+
patterns.append(self._make_pattern(
|
|
55
|
+
name=f"import:{category}",
|
|
56
|
+
description=f"Import {category}: {name}",
|
|
57
|
+
node=node,
|
|
58
|
+
file_path=path,
|
|
59
|
+
confidence=0.95,
|
|
60
|
+
metadata={
|
|
61
|
+
"import_type": category,
|
|
62
|
+
"module": name,
|
|
63
|
+
"top_module": top_module,
|
|
64
|
+
}
|
|
65
|
+
))
|
|
66
|
+
|
|
67
|
+
return patterns
|
|
68
|
+
|
|
69
|
+
def _analyze_from_import(self, node: Node, source: bytes, path: Path) -> List[Pattern]:
|
|
70
|
+
patterns = []
|
|
71
|
+
|
|
72
|
+
module_node = node.child_by_field_name("module_name")
|
|
73
|
+
module = self.parser.node_text(module_node, source) if module_node else ""
|
|
74
|
+
|
|
75
|
+
node_text = self.parser.node_text(node, source)
|
|
76
|
+
if "import *" in node_text:
|
|
77
|
+
patterns.append(self._make_pattern(
|
|
78
|
+
name="star_import",
|
|
79
|
+
description=f"Star import from {module} (pollutes namespace)",
|
|
80
|
+
node=node,
|
|
81
|
+
file_path=path,
|
|
82
|
+
confidence=0.9,
|
|
83
|
+
metadata={
|
|
84
|
+
"import_type": "star",
|
|
85
|
+
"module": module,
|
|
86
|
+
"severity": "warning",
|
|
87
|
+
}
|
|
88
|
+
))
|
|
89
|
+
|
|
90
|
+
if module.startswith("."):
|
|
91
|
+
patterns.append(self._make_pattern(
|
|
92
|
+
name="relative_import",
|
|
93
|
+
description=f"Relative import: {module}",
|
|
94
|
+
node=node,
|
|
95
|
+
file_path=path,
|
|
96
|
+
confidence=0.95,
|
|
97
|
+
metadata={
|
|
98
|
+
"import_type": "relative",
|
|
99
|
+
"module": module,
|
|
100
|
+
}
|
|
101
|
+
))
|
|
102
|
+
|
|
103
|
+
top_module = module.lstrip(".").split(".")[0] if module else ""
|
|
104
|
+
category = self._classify_module(top_module)
|
|
105
|
+
|
|
106
|
+
patterns.append(self._make_pattern(
|
|
107
|
+
name=f"from_import:{category}",
|
|
108
|
+
description=f"From import {category}: {module}",
|
|
109
|
+
node=node,
|
|
110
|
+
file_path=path,
|
|
111
|
+
confidence=0.9,
|
|
112
|
+
metadata={
|
|
113
|
+
"import_type": category,
|
|
114
|
+
"module": module,
|
|
115
|
+
"top_module": top_module,
|
|
116
|
+
}
|
|
117
|
+
))
|
|
118
|
+
|
|
119
|
+
return patterns
|
|
120
|
+
|
|
121
|
+
def _classify_module(self, name: str) -> str:
|
|
122
|
+
if name in self.STDLIB_MODULES:
|
|
123
|
+
return "stdlib"
|
|
124
|
+
elif name in {"__future__"}:
|
|
125
|
+
return "future"
|
|
126
|
+
else:
|
|
127
|
+
return "third_party"
|
|
128
|
+
|
|
129
|
+
def _walk(self, node):
|
|
130
|
+
yield node
|
|
131
|
+
for child in node.children:
|
|
132
|
+
yield from self._walk(child)
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"""Logging detector — print(), logger calls, eager-format anti-pattern."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import List, Optional
|
|
7
|
+
|
|
8
|
+
from tree_sitter import Node, Tree
|
|
9
|
+
|
|
10
|
+
from adl_drift.detectors.base import BaseDetector
|
|
11
|
+
from adl_drift.utils.types import Pattern, PatternCategory
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
LOG_LEVELS = {"debug", "info", "warning", "warn", "error", "critical", "exception"}
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class LoggingDetector(BaseDetector):
|
|
18
|
+
"""Flags print, logger calls, and f-string args to logger methods."""
|
|
19
|
+
|
|
20
|
+
@property
|
|
21
|
+
def category(self) -> PatternCategory:
|
|
22
|
+
return PatternCategory.LOGGING
|
|
23
|
+
|
|
24
|
+
def detect(self, tree: Tree, source_bytes: bytes, file_path: Path) -> List[Pattern]:
|
|
25
|
+
patterns: List[Pattern] = []
|
|
26
|
+
has_logger_init = False
|
|
27
|
+
|
|
28
|
+
for node in self._walk(tree.root_node):
|
|
29
|
+
if node.type != "call":
|
|
30
|
+
continue
|
|
31
|
+
func = node.child_by_field_name("function")
|
|
32
|
+
if func is None:
|
|
33
|
+
continue
|
|
34
|
+
func_text = self.parser.node_text(func, source_bytes).strip()
|
|
35
|
+
|
|
36
|
+
if func_text == "print":
|
|
37
|
+
patterns.append(self._make_pattern(
|
|
38
|
+
name="print_call",
|
|
39
|
+
description="print() — prefer logger",
|
|
40
|
+
node=node, file_path=file_path, confidence=0.5,
|
|
41
|
+
metadata={"severity": "LOW"},
|
|
42
|
+
))
|
|
43
|
+
continue
|
|
44
|
+
|
|
45
|
+
if func_text == "logging.getLogger":
|
|
46
|
+
has_logger_init = True
|
|
47
|
+
continue
|
|
48
|
+
|
|
49
|
+
level = self._level_from_attr(func, source_bytes)
|
|
50
|
+
if level is None:
|
|
51
|
+
continue
|
|
52
|
+
eager = self._has_fstring_arg(node, source_bytes)
|
|
53
|
+
name = "logger_fstring" if eager else "logger_call"
|
|
54
|
+
desc = (
|
|
55
|
+
f"logger.{level}(f\"...\") — eager-format anti-pattern"
|
|
56
|
+
if eager else f"logger.{level}() call"
|
|
57
|
+
)
|
|
58
|
+
patterns.append(self._make_pattern(
|
|
59
|
+
name=name,
|
|
60
|
+
description=desc,
|
|
61
|
+
node=node, file_path=file_path,
|
|
62
|
+
confidence=0.7 if eager else 0.6,
|
|
63
|
+
metadata={"level": level, "eager_format": eager},
|
|
64
|
+
))
|
|
65
|
+
|
|
66
|
+
if has_logger_init:
|
|
67
|
+
patterns.append(self._make_pattern(
|
|
68
|
+
name="logger_init",
|
|
69
|
+
description="Module initializes a logger via logging.getLogger",
|
|
70
|
+
node=tree.root_node, file_path=file_path, confidence=0.95,
|
|
71
|
+
metadata={"role": "logger_module"},
|
|
72
|
+
))
|
|
73
|
+
return patterns
|
|
74
|
+
|
|
75
|
+
def _level_from_attr(self, func_node: Node, source: bytes) -> Optional[str]:
|
|
76
|
+
if func_node.type != "attribute":
|
|
77
|
+
return None
|
|
78
|
+
attr = func_node.child_by_field_name("attribute")
|
|
79
|
+
if attr is None:
|
|
80
|
+
return None
|
|
81
|
+
name = self.parser.node_text(attr, source)
|
|
82
|
+
if name in LOG_LEVELS:
|
|
83
|
+
obj = func_node.child_by_field_name("object")
|
|
84
|
+
if obj is not None:
|
|
85
|
+
obj_text = self.parser.node_text(obj, source)
|
|
86
|
+
if obj_text in {"logger", "log", "logging", "LOG", "LOGGER"} or \
|
|
87
|
+
obj_text.endswith("_logger") or obj_text.endswith(".logger"):
|
|
88
|
+
return name
|
|
89
|
+
return None
|
|
90
|
+
|
|
91
|
+
def _has_fstring_arg(self, call_node: Node, source: bytes) -> bool:
|
|
92
|
+
args = call_node.child_by_field_name("arguments")
|
|
93
|
+
if args is None:
|
|
94
|
+
return False
|
|
95
|
+
for c in args.children:
|
|
96
|
+
if c.type == "string":
|
|
97
|
+
# tree-sitter-python uses interpolation child for f-strings
|
|
98
|
+
for sub in c.children:
|
|
99
|
+
if sub.type == "interpolation":
|
|
100
|
+
return True
|
|
101
|
+
if self.parser.node_text(c, source).lstrip().startswith(("f'", 'f"', "rf", "fr")):
|
|
102
|
+
return True
|
|
103
|
+
return False
|
|
104
|
+
|
|
105
|
+
def _walk(self, node: Node):
|
|
106
|
+
yield node
|
|
107
|
+
for child in node.children:
|
|
108
|
+
yield from self._walk(child)
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
"""HTTP route detectors for Flask, FastAPI, Django.
|
|
2
|
+
|
|
3
|
+
Finds URL endpoints and their handler functions.
|
|
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, Symbol
|
|
14
|
+
from adl_drift.parser.core import PythonParser
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class RouteDetector(BaseDetector):
|
|
18
|
+
"""Detects HTTP route definitions across frameworks.
|
|
19
|
+
|
|
20
|
+
Handles:
|
|
21
|
+
- Flask: @app.route("/path")
|
|
22
|
+
- FastAPI: @app.get("/path"), @router.post("/path")
|
|
23
|
+
- Django: path("/path", views.handler) in urls.py
|
|
24
|
+
- Starlette: @app.route("/path")
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
# Known HTTP method names used by frameworks
|
|
28
|
+
HTTP_METHODS = {
|
|
29
|
+
"route", "get", "post", "put", "patch", "delete",
|
|
30
|
+
"head", "options", "trace", "connect",
|
|
31
|
+
"api_route", "include_router",
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
def __init__(self, parser: PythonParser):
|
|
35
|
+
super().__init__(parser)
|
|
36
|
+
|
|
37
|
+
@property
|
|
38
|
+
def category(self) -> PatternCategory:
|
|
39
|
+
return PatternCategory.ROUTES
|
|
40
|
+
|
|
41
|
+
def detect(self, tree: Tree, source_bytes: bytes, file_path: Path) -> List[Pattern]:
|
|
42
|
+
patterns = []
|
|
43
|
+
|
|
44
|
+
for node in self._walk(tree.root_node):
|
|
45
|
+
if node.type != "function_definition":
|
|
46
|
+
continue
|
|
47
|
+
|
|
48
|
+
decorators = []
|
|
49
|
+
seen = set()
|
|
50
|
+
# tree-sitter-python wraps decorators in decorated_definition; they are siblings
|
|
51
|
+
for dec_node in (node.prev_sibling, *node.children):
|
|
52
|
+
if dec_node is None:
|
|
53
|
+
continue
|
|
54
|
+
if dec_node.type == "decorator":
|
|
55
|
+
dec_info = self._parse_decorator(dec_node, source_bytes)
|
|
56
|
+
if dec_info and dec_info.get("is_route"):
|
|
57
|
+
key = dec_info.get("raw", "")
|
|
58
|
+
if key not in seen:
|
|
59
|
+
seen.add(key)
|
|
60
|
+
decorators.append(dec_info)
|
|
61
|
+
# Also walk up to parent decorated_definition to catch all decorators
|
|
62
|
+
if node.parent and node.parent.type == "decorated_definition":
|
|
63
|
+
for sib in node.parent.children:
|
|
64
|
+
if sib.type == "decorator" and sib is not dec_node:
|
|
65
|
+
dec_info = self._parse_decorator(sib, source_bytes)
|
|
66
|
+
if dec_info and dec_info.get("is_route"):
|
|
67
|
+
key = dec_info.get("raw", "")
|
|
68
|
+
if key not in seen:
|
|
69
|
+
seen.add(key)
|
|
70
|
+
decorators.append(dec_info)
|
|
71
|
+
|
|
72
|
+
if not decorators:
|
|
73
|
+
continue
|
|
74
|
+
|
|
75
|
+
func_name = self._get_func_name(node, source_bytes)
|
|
76
|
+
|
|
77
|
+
for dec in decorators:
|
|
78
|
+
pattern = self._make_pattern(
|
|
79
|
+
name=f"route:{dec.get('method', 'unknown').upper()}",
|
|
80
|
+
description=f"HTTP {dec.get('method', 'unknown').upper()} {dec.get('path', '/')} -> {func_name}",
|
|
81
|
+
node=node,
|
|
82
|
+
file_path=file_path,
|
|
83
|
+
confidence=0.95,
|
|
84
|
+
metadata={
|
|
85
|
+
"framework": dec.get("framework", "unknown"),
|
|
86
|
+
"http_method": dec.get("method", "get"),
|
|
87
|
+
"route_path": dec.get("path", "/"),
|
|
88
|
+
"handler": func_name,
|
|
89
|
+
"decorator": dec.get("raw", ""),
|
|
90
|
+
}
|
|
91
|
+
)
|
|
92
|
+
patterns.append(pattern)
|
|
93
|
+
|
|
94
|
+
return patterns
|
|
95
|
+
|
|
96
|
+
def _parse_decorator(self, node: Node, source: bytes) -> Optional[Dict[str, Any]]:
|
|
97
|
+
"""Parse a decorator node to see if it's a route definition."""
|
|
98
|
+
text = self.parser.node_text(node, source).strip()
|
|
99
|
+
|
|
100
|
+
call_node = None
|
|
101
|
+
for child in node.children:
|
|
102
|
+
if child.type == "call":
|
|
103
|
+
call_node = child
|
|
104
|
+
break
|
|
105
|
+
|
|
106
|
+
if not call_node:
|
|
107
|
+
return None
|
|
108
|
+
|
|
109
|
+
func_node = call_node.child_by_field_name("function")
|
|
110
|
+
if not func_node:
|
|
111
|
+
return None
|
|
112
|
+
|
|
113
|
+
func_text = self.parser.node_text(func_node, source)
|
|
114
|
+
|
|
115
|
+
method = None
|
|
116
|
+
framework = None
|
|
117
|
+
|
|
118
|
+
if func_node.type == "attribute":
|
|
119
|
+
obj_node = func_node.child_by_field_name("object")
|
|
120
|
+
attr_node = func_node.child_by_field_name("attribute")
|
|
121
|
+
|
|
122
|
+
if obj_node and attr_node:
|
|
123
|
+
obj_name = self.parser.node_text(obj_node, source)
|
|
124
|
+
attr_name = self.parser.node_text(attr_node, source)
|
|
125
|
+
|
|
126
|
+
if attr_name.lower() in self.HTTP_METHODS:
|
|
127
|
+
method = attr_name.lower()
|
|
128
|
+
framework = self._detect_framework(obj_name, source)
|
|
129
|
+
elif func_node.type == "identifier":
|
|
130
|
+
name = self.parser.node_text(func_node, source)
|
|
131
|
+
if name.lower() in self.HTTP_METHODS:
|
|
132
|
+
method = name.lower()
|
|
133
|
+
framework = "unknown"
|
|
134
|
+
|
|
135
|
+
if not method:
|
|
136
|
+
return None
|
|
137
|
+
|
|
138
|
+
args_node = call_node.child_by_field_name("arguments")
|
|
139
|
+
path = "/"
|
|
140
|
+
if args_node:
|
|
141
|
+
for arg in args_node.children:
|
|
142
|
+
if arg.type == "string":
|
|
143
|
+
path = self.parser.node_text(arg, source).strip("'\"")
|
|
144
|
+
break
|
|
145
|
+
|
|
146
|
+
# If the decorator is `@app.route(...)` (generic), look for a
|
|
147
|
+
# `methods=[...]` keyword arg and use the first listed method.
|
|
148
|
+
if method == "route" and args_node:
|
|
149
|
+
for arg in args_node.children:
|
|
150
|
+
if arg.type != "keyword_argument":
|
|
151
|
+
continue
|
|
152
|
+
name_node = arg.child_by_field_name("name")
|
|
153
|
+
value_node = arg.child_by_field_name("value")
|
|
154
|
+
if not (name_node and value_node):
|
|
155
|
+
continue
|
|
156
|
+
if self.parser.node_text(name_node, source) != "methods":
|
|
157
|
+
continue
|
|
158
|
+
for v in value_node.children:
|
|
159
|
+
if v.type == "string":
|
|
160
|
+
verb = self.parser.node_text(v, source).strip("'\"").lower()
|
|
161
|
+
if verb in self.HTTP_METHODS:
|
|
162
|
+
method = verb
|
|
163
|
+
break
|
|
164
|
+
break
|
|
165
|
+
|
|
166
|
+
return {
|
|
167
|
+
"is_route": True,
|
|
168
|
+
"method": method,
|
|
169
|
+
"path": path,
|
|
170
|
+
"framework": framework,
|
|
171
|
+
"raw": text,
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
def _detect_framework(self, obj_name: str, source: bytes) -> str:
|
|
175
|
+
"""Guess framework from app variable name."""
|
|
176
|
+
if obj_name in {"app", "application"}:
|
|
177
|
+
return "flask_or_fastapi"
|
|
178
|
+
elif obj_name == "router":
|
|
179
|
+
return "fastapi"
|
|
180
|
+
elif obj_name == "blueprint":
|
|
181
|
+
return "flask"
|
|
182
|
+
return "unknown"
|
|
183
|
+
|
|
184
|
+
def _get_func_name(self, node: Node, source: bytes) -> str:
|
|
185
|
+
"""Extract function name from definition node."""
|
|
186
|
+
name_node = node.child_by_field_name("name")
|
|
187
|
+
if name_node:
|
|
188
|
+
return self.parser.node_text(name_node, source)
|
|
189
|
+
return "<anonymous>"
|
|
190
|
+
|
|
191
|
+
def _walk(self, node: Node):
|
|
192
|
+
yield node
|
|
193
|
+
for child in node.children:
|
|
194
|
+
yield from self._walk(child)
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
class DjangoURLDetector(BaseDetector):
|
|
198
|
+
"""Detects Django URL patterns in urls.py files."""
|
|
199
|
+
|
|
200
|
+
def __init__(self, parser: PythonParser):
|
|
201
|
+
super().__init__(parser)
|
|
202
|
+
|
|
203
|
+
@property
|
|
204
|
+
def category(self) -> PatternCategory:
|
|
205
|
+
return PatternCategory.ROUTES
|
|
206
|
+
|
|
207
|
+
def detect(self, tree: Tree, source_bytes: bytes, file_path: Path) -> List[Pattern]:
|
|
208
|
+
patterns = []
|
|
209
|
+
if file_path.name != "urls.py":
|
|
210
|
+
return patterns
|
|
211
|
+
|
|
212
|
+
for node in self._walk(tree.root_node):
|
|
213
|
+
if node.type != "call":
|
|
214
|
+
continue
|
|
215
|
+
|
|
216
|
+
func_node = node.child_by_field_name("function")
|
|
217
|
+
if not func_node:
|
|
218
|
+
continue
|
|
219
|
+
|
|
220
|
+
func_name = self.parser.node_text(func_node, source_bytes)
|
|
221
|
+
if func_name not in {"path", "re_path", "url"}:
|
|
222
|
+
continue
|
|
223
|
+
|
|
224
|
+
args_node = node.child_by_field_name("arguments")
|
|
225
|
+
if not args_node or len(args_node.children) < 2:
|
|
226
|
+
continue
|
|
227
|
+
|
|
228
|
+
route = "/"
|
|
229
|
+
view = "unknown"
|
|
230
|
+
|
|
231
|
+
for arg in args_node.children:
|
|
232
|
+
if arg.type == "string":
|
|
233
|
+
route = self.parser.node_text(arg, source_bytes).strip("'""")
|
|
234
|
+
elif arg.type in ("identifier", "attribute"):
|
|
235
|
+
view = self.parser.node_text(arg, source_bytes)
|
|
236
|
+
|
|
237
|
+
pattern = self._make_pattern(
|
|
238
|
+
name=f"django_url:{func_name}",
|
|
239
|
+
description=f"Django URL: {route} -> {view}",
|
|
240
|
+
node=node,
|
|
241
|
+
file_path=file_path,
|
|
242
|
+
confidence=0.9,
|
|
243
|
+
metadata={
|
|
244
|
+
"framework": "django",
|
|
245
|
+
"url_type": func_name,
|
|
246
|
+
"route": route,
|
|
247
|
+
"view": view,
|
|
248
|
+
}
|
|
249
|
+
)
|
|
250
|
+
patterns.append(pattern)
|
|
251
|
+
|
|
252
|
+
return patterns
|
|
253
|
+
|
|
254
|
+
def _walk(self, node):
|
|
255
|
+
yield node
|
|
256
|
+
for child in node.children:
|
|
257
|
+
yield from self._walk(child)
|