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.
- deadpush/__init__.py +1 -0
- deadpush/churn.py +189 -0
- deadpush/cli.py +1584 -0
- deadpush/comments.py +265 -0
- deadpush/complexity.py +254 -0
- deadpush/config.py +284 -0
- deadpush/crawler.py +133 -0
- deadpush/deadness.py +477 -0
- deadpush/debris.py +729 -0
- deadpush/deps.py +323 -0
- deadpush/deps_guard.py +382 -0
- deadpush/entrypoints.py +193 -0
- deadpush/graph.py +401 -0
- deadpush/guard.py +1386 -0
- deadpush/hooks.py +369 -0
- deadpush/importgraph.py +122 -0
- deadpush/imports.py +239 -0
- deadpush/intercept.py +995 -0
- deadpush/languages/__init__.py +143 -0
- deadpush/languages/base.py +70 -0
- deadpush/languages/cpp.py +150 -0
- deadpush/languages/go_.py +177 -0
- deadpush/languages/java.py +185 -0
- deadpush/languages/javascript.py +202 -0
- deadpush/languages/python_.py +278 -0
- deadpush/languages/rust.py +147 -0
- deadpush/languages/typescript.py +192 -0
- deadpush/layers.py +197 -0
- deadpush/mcp_server.py +1061 -0
- deadpush/reachability.py +183 -0
- deadpush/registration.py +280 -0
- deadpush/report.py +113 -0
- deadpush/rules.py +190 -0
- deadpush/sarif.py +123 -0
- deadpush/scorer.py +151 -0
- deadpush/security.py +187 -0
- deadpush/session.py +224 -0
- deadpush/tests.py +333 -0
- deadpush/ui.py +156 -0
- deadpush/verifier.py +168 -0
- deadpush/watch.py +103 -0
- deadpush-0.2.0.dist-info/METADATA +230 -0
- deadpush-0.2.0.dist-info/RECORD +46 -0
- deadpush-0.2.0.dist-info/WHEEL +4 -0
- deadpush-0.2.0.dist-info/entry_points.txt +2 -0
- deadpush-0.2.0.dist-info/licenses/LICENSE +21 -0
|
@@ -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
|