deadpush 0.2.0__py3-none-any.whl

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.
@@ -0,0 +1,143 @@
1
+ """
2
+ deadpush language plugin registry.
3
+
4
+ This central module makes adding + integrating new languages seamless:
5
+
6
+ - All plugins are registered here (lazily imported to keep startup fast and
7
+ avoid requiring every tree-sitter-foo even if lang disabled).
8
+ - get_enabled_plugins(config) returns only the active ones for the run.
9
+ - Helpers for extensions, names, etc. are used by crawler, watch, ui etc.
10
+ - To add a new language:
11
+ 1. pip add the tree-sitter-xxx dep + update pyproject.toml
12
+ 2. Implement xxx.py following the LanguagePlugin protocol (see base.py)
13
+ 3. Add to LANGUAGE_REGISTRY below
14
+ 4. (optional) update default list in config.py
15
+
16
+ Plugins are structural (no inheritance required) but should implement the
17
+ methods in base.LanguagePlugin .
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ from typing import TYPE_CHECKING, Callable
23
+
24
+ from .base import Import, CallSite, LanguagePlugin
25
+
26
+ if TYPE_CHECKING:
27
+ from ..config import Config
28
+
29
+ # ----------------------------------------------------------------------------
30
+ # Registry: name -> (import_path, class_name)
31
+ # Keep sorted for determinism. Lazy to support partial installs.
32
+ # ----------------------------------------------------------------------------
33
+ LANGUAGE_REGISTRY: dict[str, tuple[str, str]] = {
34
+ "python": (".python_", "PythonPlugin"),
35
+ "typescript": (".typescript", "TypeScriptPlugin"),
36
+ "javascript": (".javascript", "JavaScriptPlugin"),
37
+ "go": (".go_", "GoPlugin"),
38
+ "rust": (".rust", "RustPlugin"),
39
+ "cpp": (".cpp", "CppPlugin"),
40
+ "java": (".java", "JavaPlugin"),
41
+ }
42
+
43
+ # Common aliases users / config may use
44
+ LANGUAGE_ALIASES: dict[str, str] = {
45
+ "py": "python",
46
+ "ts": "typescript",
47
+ "tsx": "typescript",
48
+ "js": "javascript",
49
+ "jsx": "javascript",
50
+ "c++": "cpp",
51
+ "cxx": "cpp",
52
+ "cc": "cpp",
53
+ "c": "cpp", # treat C as cpp plugin for simplicity (overlapping extensions)
54
+ }
55
+
56
+
57
+ def _load_plugin_class(module_rel: str, class_name: str):
58
+ """Import and return the plugin class (lazy)."""
59
+ from importlib import import_module
60
+ mod = import_module(module_rel, package=__name__)
61
+ return getattr(mod, class_name)
62
+
63
+
64
+ def get_plugin(name: str) -> LanguagePlugin | None:
65
+ """Return an instantiated plugin for a canonical language name, or None."""
66
+ canonical = LANGUAGE_ALIASES.get(name.lower(), name.lower())
67
+ if canonical not in LANGUAGE_REGISTRY:
68
+ return None
69
+ mod_rel, cls_name = LANGUAGE_REGISTRY[canonical]
70
+ try:
71
+ cls = _load_plugin_class(mod_rel, cls_name)
72
+ return cls()
73
+ except Exception as e:
74
+ # Fail gracefully so one bad language parser doesn't kill the run
75
+ import warnings
76
+ warnings.warn(f"Failed to load language plugin {name!r}: {e}")
77
+ return None
78
+
79
+
80
+ def get_enabled_plugins(config: "Config") -> dict[str, LanguagePlugin]:
81
+ """Return {lang_name: plugin_instance, ...} for languages enabled in config."""
82
+ plugins: dict[str, LanguagePlugin] = {}
83
+ for name in config.languages:
84
+ canonical = LANGUAGE_ALIASES.get(name.lower(), name.lower())
85
+ if canonical in plugins:
86
+ continue
87
+ plug = get_plugin(canonical)
88
+ if plug:
89
+ plugins[canonical] = plug
90
+ # Also respect aliases that map into enabled set
91
+ for alias, canon in LANGUAGE_ALIASES.items():
92
+ if config.is_language_enabled(alias) and canon not in plugins:
93
+ plug = get_plugin(canon)
94
+ if plug:
95
+ plugins[canon] = plug
96
+ return plugins
97
+
98
+
99
+ def get_all_plugins() -> dict[str, LanguagePlugin]:
100
+ """Return all known plugins (best effort; some may fail to import if deps missing)."""
101
+ plugins = {}
102
+ for name in list(LANGUAGE_REGISTRY.keys()):
103
+ p = get_plugin(name)
104
+ if p:
105
+ plugins[name] = p
106
+ return plugins
107
+
108
+
109
+ def get_all_extensions() -> set[str]:
110
+ """Union of all file extensions known to any plugin (for filtering etc)."""
111
+ exts: set[str] = set()
112
+ for name in LANGUAGE_REGISTRY:
113
+ try:
114
+ p = get_plugin(name)
115
+ if p and hasattr(p, "extensions"):
116
+ exts.update(p.extensions)
117
+ except Exception:
118
+ pass
119
+ return exts
120
+
121
+
122
+ def get_language_for_file(path: str | Path, plugins: dict[str, LanguagePlugin] | None = None) -> tuple[str, LanguagePlugin] | None:
123
+ """Given a path, return (lang_name, plugin) that claims its suffix, or None."""
124
+ suffix = Path(path).suffix.lower()
125
+ plugs = plugins or get_all_plugins()
126
+ for lang, plug in plugs.items():
127
+ if hasattr(plug, "extensions") and suffix in plug.extensions:
128
+ return lang, plug
129
+ return None
130
+
131
+
132
+ # Re-export for convenience
133
+ __all__ = [
134
+ "Import",
135
+ "CallSite",
136
+ "LanguagePlugin",
137
+ "LANGUAGE_REGISTRY",
138
+ "get_plugin",
139
+ "get_enabled_plugins",
140
+ "get_all_plugins",
141
+ "get_all_extensions",
142
+ "get_language_for_file",
143
+ ]
@@ -0,0 +1,70 @@
1
+ """
2
+ Base types and protocol for deadpush language plugins.
3
+
4
+ Defines the common Import representation and LanguagePlugin structural interface
5
+ used by all language backends (rust, cpp, python, etc.).
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from dataclasses import dataclass
11
+ from typing import Any, Protocol, runtime_checkable
12
+
13
+
14
+ @dataclass(frozen=True, slots=True)
15
+ class Import:
16
+ """Normalized representation of an import/use statement.
17
+
18
+ - module: the module/package being imported (e.g. "std::io" or "crate")
19
+ - names: imported names or ["*"] for glob
20
+ - level: 0 for absolute, >0 indicates relative import depth (Python style)
21
+ """
22
+ module: str
23
+ names: list[str]
24
+ level: int = 0
25
+
26
+
27
+ @dataclass(frozen=True, slots=True)
28
+ class CallSite:
29
+ """Structured representation of a function/method call site.
30
+
31
+ This enables much better call-graph construction than raw text matching.
32
+ """
33
+ caller_id: str # symbol id of the containing function/method
34
+ callee: str # best-effort name of the callee (e.g. "findOne", "db.findOne")
35
+ line: int
36
+ column: int = 0
37
+ is_method: bool = False
38
+ receiver: str | None = None # e.g. "db", "this", "Model" for method calls
39
+ raw_callee_text: str = "" # original text for fallback
40
+
41
+
42
+ @runtime_checkable
43
+ class LanguagePlugin(Protocol):
44
+ """Structural protocol for language plugins.
45
+
46
+ Plugins are not required to inherit; they only need to provide the attributes
47
+ and methods with matching signatures. This enables static checking and
48
+ optional runtime isinstance checks.
49
+ """
50
+
51
+ extensions: list[str]
52
+ language: Any # tree_sitter.Language
53
+
54
+ def get_parser(self) -> Any: ...
55
+
56
+ def parse(self, source: bytes, path: str) -> Any: ...
57
+
58
+ def extract_symbols(self, tree: Any, path: str) -> list[Any]: ...
59
+
60
+ def extract_call_sites(self, tree: Any, path: str) -> list[CallSite]: ...
61
+
62
+ def extract_imports(self, tree: Any, path: str) -> list[Import]: ...
63
+
64
+ def detect_entry_points(
65
+ self, tree: Any, path: str, config_dynamic_patterns: list[str]
66
+ ) -> list[str]: ...
67
+
68
+ def classify_dynamic_risk(self, tree: Any, path: str) -> float: ...
69
+
70
+ def supports_suppression_comment(self, line_text: str) -> bool: ...
@@ -0,0 +1,150 @@
1
+ """
2
+ Production-grade C++ language plugin for deadpush.
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ from pathlib import Path
8
+
9
+ from tree_sitter import Language, Node, Tree
10
+ import tree_sitter_cpp as tscpp
11
+
12
+ from ..graph import Symbol, make_symbol_id
13
+ from .base import Import, CallSite, LanguagePlugin
14
+
15
+ CPP_LANGUAGE = Language(tscpp.language())
16
+
17
+
18
+ class CppPlugin:
19
+ extensions = [".cpp", ".cc", ".cxx", ".hpp", ".hh", ".h"]
20
+ language = CPP_LANGUAGE
21
+
22
+ def get_parser(self):
23
+ from tree_sitter import Parser
24
+ return Parser(self.language)
25
+
26
+ def parse(self, source: bytes, path: str) -> Tree:
27
+ return self.get_parser().parse(source)
28
+
29
+ def extract_symbols(self, tree: Tree, path: str) -> list[Symbol]:
30
+ symbols: list[Symbol] = []
31
+ root = tree.root_node
32
+
33
+ def walk(node: Node):
34
+ if node.type in ("function_definition", "declaration"):
35
+ declarator = node.child_by_field_name("declarator")
36
+ if declarator:
37
+ name_node = declarator.child_by_field_name("declarator") or declarator
38
+ if hasattr(name_node, 'type') and name_node.type == "identifier":
39
+ name = name_node.text.decode("utf-8")
40
+ symbols.append(Symbol(
41
+ id=make_symbol_id(path, name),
42
+ name=name,
43
+ kind="function",
44
+ path=path,
45
+ line=node.start_point[0] + 1,
46
+ is_entry_point=(name == "main"),
47
+ ))
48
+ elif node.type == "class_specifier":
49
+ name_node = node.child_by_field_name("name")
50
+ if name_node:
51
+ symbols.append(Symbol(
52
+ id=make_symbol_id(path, name_node.text.decode("utf-8")),
53
+ name=name_node.text.decode("utf-8"),
54
+ kind="class",
55
+ path=path,
56
+ line=node.start_point[0] + 1,
57
+ ))
58
+
59
+ for child in node.children:
60
+ walk(child)
61
+
62
+ walk(root)
63
+
64
+ symbols.append(Symbol(
65
+ id=make_symbol_id(path, Path(path).name),
66
+ name=Path(path).name,
67
+ kind="file",
68
+ path=path,
69
+ line=1
70
+ ))
71
+ return symbols
72
+
73
+ def extract_call_sites(self, tree: Tree, path: str) -> list[CallSite]:
74
+ """Exhaustive C/C++ call extraction (best-effort due to complex grammar).
75
+
76
+ Captures function calls, method calls ( . and -> ), qualified names.
77
+ Note: templates, overloads, and full resolution are limited without
78
+ additional semantic info.
79
+ """
80
+ calls: list[CallSite] = []
81
+ root = tree.root_node
82
+
83
+ def get_text(n: Node | None) -> str:
84
+ if not n:
85
+ return ""
86
+ return n.text.decode("utf-8", errors="ignore").strip()
87
+
88
+ def walk(node: Node, current_func_id: str | None = None):
89
+ if node.type in ("function_definition", "declaration"):
90
+ # rough current func from declarator
91
+ declarator = node.child_by_field_name("declarator")
92
+ if declarator:
93
+ name_node = declarator.child_by_field_name("declarator") or declarator
94
+ if get_text(name_node):
95
+ current_func_id = make_symbol_id(path, get_text(name_node).split("(")[0].strip())
96
+
97
+ if node.type == "call_expression":
98
+ func_node = node.child_by_field_name("function")
99
+ if func_node and current_func_id:
100
+ raw = get_text(func_node)
101
+ callee_name = raw
102
+ receiver = None
103
+ is_method = False
104
+
105
+ if func_node.type == "field_expression":
106
+ is_method = True
107
+ field = func_node.child_by_field_name("field")
108
+ value = func_node.child_by_field_name("argument")
109
+ callee_name = get_text(field) if field else raw
110
+ receiver = get_text(value) if value else None
111
+ elif func_node.type in ("identifier", "qualified_identifier", "scoped_identifier"):
112
+ parts = raw.split("::")
113
+ callee_name = parts[-1]
114
+
115
+ callee_name = callee_name.split("(")[0].strip()
116
+
117
+ call = CallSite(
118
+ caller_id=current_func_id,
119
+ callee=callee_name,
120
+ line=node.start_point[0] + 1,
121
+ is_method=is_method,
122
+ receiver=receiver,
123
+ raw_callee_text=raw
124
+ )
125
+ calls.append(call)
126
+
127
+ for child in node.children:
128
+ walk(child, current_func_id)
129
+
130
+ walk(root)
131
+ return calls
132
+
133
+ def extract_imports(self, tree: Tree, path: str) -> list[Import]:
134
+ return []
135
+
136
+ def detect_entry_points(self, tree: Tree, path: str, config_dynamic_patterns: list[str]) -> list[str]:
137
+ source = tree.root_node.text.decode("utf-8", errors="ignore")
138
+ return ["main"] if "int main(" in source[:600] or "void main(" in source[:600] else []
139
+
140
+ def classify_dynamic_risk(self, tree: Tree, path: str) -> float:
141
+ text = tree.root_node.text.decode("utf-8", errors="ignore").lower()
142
+ risk = 0.0
143
+ if "virtual" in text or "override" in text:
144
+ risk += 0.2
145
+ if "#define" in text:
146
+ risk += 0.15
147
+ return min(risk, 1.0)
148
+
149
+ def supports_suppression_comment(self, line_text: str) -> bool:
150
+ return "// deadpush: ignore" in line_text or "/* deadpush: ignore */" in line_text
@@ -0,0 +1,177 @@
1
+ """
2
+ Production-grade Go language plugin for deadpush.
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ from pathlib import Path
8
+
9
+ from tree_sitter import Language, Node, Tree
10
+ import tree_sitter_go as tsgo
11
+
12
+ from ..graph import Symbol, make_symbol_id
13
+ from .base import Import, CallSite, LanguagePlugin
14
+
15
+ GO_LANGUAGE = Language(tsgo.language())
16
+
17
+
18
+ class GoPlugin:
19
+ extensions = [".go"]
20
+ language = GO_LANGUAGE
21
+
22
+ def get_parser(self):
23
+ from tree_sitter import Parser
24
+ return Parser(self.language)
25
+
26
+ def parse(self, source: bytes, path: str) -> Tree:
27
+ return self.get_parser().parse(source)
28
+
29
+ def extract_symbols(self, tree: Tree, path: str) -> list[Symbol]:
30
+ symbols: list[Symbol] = []
31
+ root = tree.root_node
32
+
33
+ def walk(node: Node):
34
+ if node.type == "function_declaration":
35
+ name_node = node.child_by_field_name("name")
36
+ if name_node:
37
+ name = name_node.text.decode("utf-8")
38
+ symbols.append(Symbol(
39
+ id=make_symbol_id(path, name),
40
+ name=name,
41
+ kind="function",
42
+ path=path,
43
+ line=node.start_point[0] + 1,
44
+ is_entry_point=(name == "main"),
45
+ ))
46
+ elif node.type == "method_declaration":
47
+ # receiver + name
48
+ name_node = node.child_by_field_name("name")
49
+ recv = node.child_by_field_name("receiver")
50
+ if name_node:
51
+ name = name_node.text.decode("utf-8")
52
+ # include receiver type in name for uniqueness e.g. (*Foo).Bar
53
+ recv_text = ""
54
+ if recv:
55
+ for c in recv.children:
56
+ if c.type in ("type_identifier", "pointer_type"):
57
+ recv_text = c.text.decode("utf-8", "ignore").strip("*() ")
58
+ break
59
+ full = f"{recv_text}.{name}" if recv_text else name
60
+ symbols.append(Symbol(
61
+ id=make_symbol_id(path, full),
62
+ name=full,
63
+ kind="method",
64
+ path=path,
65
+ line=node.start_point[0] + 1,
66
+ ))
67
+ elif node.type == "type_spec":
68
+ # struct or interface
69
+ name_node = node.child_by_field_name("name")
70
+ if name_node:
71
+ symbols.append(Symbol(
72
+ id=make_symbol_id(path, name_node.text.decode("utf-8")),
73
+ name=name_node.text.decode("utf-8"),
74
+ kind="class",
75
+ path=path,
76
+ line=node.start_point[0] + 1,
77
+ ))
78
+
79
+ for child in node.children:
80
+ walk(child)
81
+
82
+ walk(root)
83
+
84
+ symbols.append(Symbol(
85
+ id=make_symbol_id(path, Path(path).name),
86
+ name=Path(path).name,
87
+ kind="file",
88
+ path=path,
89
+ line=1
90
+ ))
91
+ return symbols
92
+
93
+ def extract_call_sites(self, tree: Tree, path: str) -> list[CallSite]:
94
+ """Exhaustive Go call extraction.
95
+
96
+ Handles function calls, method calls on receivers (value/pointer),
97
+ qualified identifiers (pkg.Func), etc.
98
+ """
99
+ calls: list[CallSite] = []
100
+ root = tree.root_node
101
+
102
+ def get_text(n: Node | None) -> str:
103
+ if not n:
104
+ return ""
105
+ return n.text.decode("utf-8", errors="ignore").strip()
106
+
107
+ def walk(node: Node, current_func_id: str | None = None):
108
+ if node.type in ("function_declaration", "method_declaration"):
109
+ name_node = node.child_by_field_name("name")
110
+ func_name = get_text(name_node) if name_node else None
111
+ if func_name:
112
+ current_func_id = make_symbol_id(path, func_name)
113
+
114
+ if node.type == "call_expression":
115
+ func_node = node.child_by_field_name("function")
116
+ if func_node and current_func_id:
117
+ raw = get_text(func_node)
118
+ callee_name = raw
119
+ receiver = None
120
+ is_method = False
121
+
122
+ if func_node.type == "selector_expression":
123
+ is_method = True
124
+ sel = func_node.child_by_field_name("field")
125
+ x = func_node.child_by_field_name("operand")
126
+ callee_name = get_text(sel) if sel else raw
127
+ receiver = get_text(x) if x else None
128
+ elif func_node.type == "identifier":
129
+ callee_name = raw
130
+
131
+ callee_name = callee_name.split("(")[0].strip()
132
+
133
+ call = CallSite(
134
+ caller_id=current_func_id,
135
+ callee=callee_name,
136
+ line=node.start_point[0] + 1,
137
+ is_method=is_method,
138
+ receiver=receiver,
139
+ raw_callee_text=raw
140
+ )
141
+ calls.append(call)
142
+
143
+ for child in node.children:
144
+ walk(child, current_func_id)
145
+
146
+ walk(root)
147
+ return calls
148
+
149
+ def extract_imports(self, tree: Tree, path: str) -> list[Import]:
150
+ imports: list[Import] = []
151
+ root = tree.root_node
152
+ for node in root.children:
153
+ if node.type == "import_declaration":
154
+ for spec in node.children:
155
+ if spec.type == "import_spec":
156
+ path_node = spec.child_by_field_name("path") or spec
157
+ mod = path_node.text.decode("utf-8").strip('"')
158
+ imports.append(Import(module=mod, names=["*"], level=0))
159
+ return imports or [Import(module="main", names=["*"], level=0)]
160
+
161
+ def detect_entry_points(self, tree: Tree, path: str, config_dynamic_patterns: list[str]) -> list[str]:
162
+ src = tree.root_node.text.decode("utf-8", errors="ignore")
163
+ if 'package main' in src and 'func main()' in src:
164
+ return ["main"]
165
+ return ["main"] if "func main(" in src[:400] else []
166
+
167
+ def classify_dynamic_risk(self, tree: Tree, path: str) -> float:
168
+ text = tree.root_node.text.decode("utf-8", errors="ignore").lower()
169
+ risk = 0.0
170
+ if "unsafe" in text or "reflect." in text:
171
+ risk += 0.35
172
+ if "go " in text and "func" in text: # goroutines
173
+ risk += 0.15
174
+ return min(risk, 1.0)
175
+
176
+ def supports_suppression_comment(self, line_text: str) -> bool:
177
+ return "// deadpush: ignore" in line_text