gitreins 0.1.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.
- engine/__init__.py +2 -0
- engine/dead_code.py +280 -0
- engine/evaluator.py +663 -0
- engine/guard_manager.py +352 -0
- engine/judge.py +134 -0
- engine/llm.py +298 -0
- engine/pipeline.py +428 -0
- engine/task_manager.py +148 -0
- engine/version.py +1 -0
- gitreins/cli.py +221 -0
- gitreins-0.1.0.dist-info/METADATA +101 -0
- gitreins-0.1.0.dist-info/RECORD +18 -0
- gitreins-0.1.0.dist-info/WHEEL +5 -0
- gitreins-0.1.0.dist-info/entry_points.txt +2 -0
- gitreins-0.1.0.dist-info/licenses/LICENSE +9 -0
- gitreins-0.1.0.dist-info/top_level.txt +3 -0
- gitreins_mcp/__init__.py +1 -0
- gitreins_mcp/server.py +417 -0
engine/__init__.py
ADDED
engine/dead_code.py
ADDED
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
"""Dead Code Detector — AST-based static analysis for unreachable and unused code.
|
|
2
|
+
|
|
3
|
+
Catches:
|
|
4
|
+
1. Unreachable code — statements after return/raise/break/continue in same block
|
|
5
|
+
2. Unused functions — defined but never called anywhere in the project
|
|
6
|
+
3. Unused imports — modules imported but never referenced
|
|
7
|
+
4. Empty functions — defined with pass/... only, no body
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import ast
|
|
11
|
+
import os
|
|
12
|
+
from dataclasses import dataclass, field
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass
|
|
16
|
+
class DeadCodeFinding:
|
|
17
|
+
file: str
|
|
18
|
+
line: int
|
|
19
|
+
category: str # unreachable | unused_function | unused_import | empty_function
|
|
20
|
+
message: str
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass
|
|
24
|
+
class DeadCodeReport:
|
|
25
|
+
findings: list[DeadCodeFinding] = field(default_factory=list)
|
|
26
|
+
|
|
27
|
+
@property
|
|
28
|
+
def passed(self) -> bool:
|
|
29
|
+
return len(self.findings) == 0
|
|
30
|
+
|
|
31
|
+
@property
|
|
32
|
+
def summary(self) -> str:
|
|
33
|
+
if not self.findings:
|
|
34
|
+
return "No dead code found"
|
|
35
|
+
lines = []
|
|
36
|
+
by_cat: dict[str, list[DeadCodeFinding]] = {}
|
|
37
|
+
for f in self.findings:
|
|
38
|
+
by_cat.setdefault(f.category, []).append(f)
|
|
39
|
+
for cat, finds in sorted(by_cat.items()):
|
|
40
|
+
lines.append(f"\n {cat.upper()} ({len(finds)}):")
|
|
41
|
+
for f in finds[:10]:
|
|
42
|
+
lines.append(f" {f.file}:{f.line} — {f.message}")
|
|
43
|
+
if len(finds) > 10:
|
|
44
|
+
lines.append(f" ... and {len(finds) - 10} more")
|
|
45
|
+
return "\n".join(lines)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class DeadCodeDetector:
|
|
49
|
+
"""AST-based dead code analysis for Python projects."""
|
|
50
|
+
|
|
51
|
+
WHITELIST_FUNCTIONS = {
|
|
52
|
+
# Standard dunder methods
|
|
53
|
+
"__init__", "__repr__", "__str__", "__eq__", "__hash__", "__lt__",
|
|
54
|
+
"__le__", "__gt__", "__ge__", "__add__", "__sub__", "__mul__",
|
|
55
|
+
"__call__", "__getitem__", "__setitem__", "__delitem__",
|
|
56
|
+
"__enter__", "__exit__", "__iter__", "__next__", "__len__",
|
|
57
|
+
"__contains__", "__getattr__", "__setattr__", "__delattr__",
|
|
58
|
+
"__post_init__", "__new__",
|
|
59
|
+
# Test functions
|
|
60
|
+
"setUp", "tearDown", "setUpClass", "tearDownClass",
|
|
61
|
+
# Common framework hooks
|
|
62
|
+
"main", "run", "handle", "process", "execute", "dispatch",
|
|
63
|
+
}
|
|
64
|
+
# Decorators that mean a function IS called (just not via Call AST node)
|
|
65
|
+
CALLED_VIA_DECORATOR = {"property", "cached_property", "staticmethod", "classmethod"}
|
|
66
|
+
# Decorator qualifiers that mark fixture/test functions called by frameworks
|
|
67
|
+
FRAMEWORK_DECORATORS = {"pytest.fixture", "fixture"}
|
|
68
|
+
|
|
69
|
+
def __init__(self, workdir: str = "."):
|
|
70
|
+
self.workdir = os.path.abspath(workdir)
|
|
71
|
+
self._func_defs: dict[str, list[tuple[str, int]]] = {} # func_name -> [(file, line)]
|
|
72
|
+
self._func_calls: set[str] = set()
|
|
73
|
+
self._imports: dict[str, set[str]] = {} # file -> {import names}
|
|
74
|
+
|
|
75
|
+
def scan(self, files: list[str] | None = None) -> DeadCodeReport:
|
|
76
|
+
"""Scan project for dead code. If files is None, scans all Python files."""
|
|
77
|
+
report = DeadCodeReport()
|
|
78
|
+
|
|
79
|
+
if files is None:
|
|
80
|
+
files = self._find_python_files()
|
|
81
|
+
|
|
82
|
+
# Phase 1: Build symbol table (definitions, imports)
|
|
83
|
+
for fpath in files:
|
|
84
|
+
self._collect_symbols(fpath)
|
|
85
|
+
|
|
86
|
+
# Phase 2: Collect all function calls across the project
|
|
87
|
+
for fpath in files:
|
|
88
|
+
self._collect_calls(fpath)
|
|
89
|
+
|
|
90
|
+
# Phase 3: Detect dead code per file
|
|
91
|
+
for fpath in files:
|
|
92
|
+
findings = self._analyze_file(fpath)
|
|
93
|
+
report.findings.extend(findings)
|
|
94
|
+
|
|
95
|
+
return report
|
|
96
|
+
|
|
97
|
+
def _find_python_files(self) -> list[str]:
|
|
98
|
+
"""Find all Python files in the project (excluding venv, node_modules, etc.)."""
|
|
99
|
+
py_files = []
|
|
100
|
+
skip_dirs = {".git", "__pycache__", ".venv", "venv", "node_modules",
|
|
101
|
+
".tox", ".eggs", "build", "dist", ".pytest_cache",
|
|
102
|
+
".gitreins", "temporal-vector"}
|
|
103
|
+
for root, dirs, filenames in os.walk(self.workdir):
|
|
104
|
+
dirs[:] = [d for d in dirs if d not in skip_dirs]
|
|
105
|
+
for fname in filenames:
|
|
106
|
+
if fname.endswith(".py"):
|
|
107
|
+
py_files.append(os.path.join(root, fname))
|
|
108
|
+
return py_files
|
|
109
|
+
|
|
110
|
+
def _relpath(self, abspath: str) -> str:
|
|
111
|
+
try:
|
|
112
|
+
return os.path.relpath(abspath, self.workdir)
|
|
113
|
+
except ValueError:
|
|
114
|
+
return abspath
|
|
115
|
+
|
|
116
|
+
def _collect_symbols(self, fpath: str) -> None:
|
|
117
|
+
"""Collect function definitions and imports from a file."""
|
|
118
|
+
try:
|
|
119
|
+
with open(fpath, "r") as f:
|
|
120
|
+
source = f.read()
|
|
121
|
+
tree = ast.parse(source, filename=fpath)
|
|
122
|
+
except (SyntaxError, UnicodeDecodeError, FileNotFoundError, PermissionError):
|
|
123
|
+
return
|
|
124
|
+
|
|
125
|
+
rel = self._relpath(fpath)
|
|
126
|
+
|
|
127
|
+
for node in ast.walk(tree):
|
|
128
|
+
# Function definitions
|
|
129
|
+
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
130
|
+
name = node.name
|
|
131
|
+
if name not in self._func_defs:
|
|
132
|
+
self._func_defs[name] = []
|
|
133
|
+
self._func_defs[name].append((rel, node.lineno))
|
|
134
|
+
# Functions decorated with @property, @staticmethod etc. are
|
|
135
|
+
# accessed via attribute access, not Call AST — treat as "called"
|
|
136
|
+
for decorator in node.decorator_list:
|
|
137
|
+
dec_name = None
|
|
138
|
+
if isinstance(decorator, ast.Name):
|
|
139
|
+
dec_name = decorator.id
|
|
140
|
+
elif isinstance(decorator, ast.Attribute):
|
|
141
|
+
dec_name = decorator.attr
|
|
142
|
+
# Check for pytest.fixture style
|
|
143
|
+
if isinstance(decorator.value, ast.Name):
|
|
144
|
+
qualified = f"{decorator.value.id}.{dec_name}"
|
|
145
|
+
if qualified in self.FRAMEWORK_DECORATORS:
|
|
146
|
+
self._func_calls.add(name)
|
|
147
|
+
break
|
|
148
|
+
if dec_name and dec_name in self.CALLED_VIA_DECORATOR:
|
|
149
|
+
self._func_calls.add(name)
|
|
150
|
+
break
|
|
151
|
+
if dec_name in self.FRAMEWORK_DECORATORS:
|
|
152
|
+
self._func_calls.add(name)
|
|
153
|
+
break
|
|
154
|
+
|
|
155
|
+
# Imports
|
|
156
|
+
if isinstance(node, ast.Import):
|
|
157
|
+
for alias in node.names:
|
|
158
|
+
name = alias.asname or alias.name
|
|
159
|
+
self._imports.setdefault(rel, set()).add(name)
|
|
160
|
+
elif isinstance(node, ast.ImportFrom):
|
|
161
|
+
for alias in node.names:
|
|
162
|
+
name = alias.asname or alias.name
|
|
163
|
+
if name != "*":
|
|
164
|
+
self._imports.setdefault(rel, set()).add(name)
|
|
165
|
+
|
|
166
|
+
def _collect_calls(self, fpath: str) -> None:
|
|
167
|
+
"""Collect all function calls across the project."""
|
|
168
|
+
try:
|
|
169
|
+
with open(fpath, "r") as f:
|
|
170
|
+
source = f.read()
|
|
171
|
+
tree = ast.parse(source, filename=fpath)
|
|
172
|
+
except (SyntaxError, UnicodeDecodeError, FileNotFoundError, PermissionError):
|
|
173
|
+
return
|
|
174
|
+
|
|
175
|
+
for node in ast.walk(tree):
|
|
176
|
+
if isinstance(node, ast.Call):
|
|
177
|
+
if isinstance(node.func, ast.Name):
|
|
178
|
+
self._func_calls.add(node.func.id)
|
|
179
|
+
elif isinstance(node.func, ast.Attribute):
|
|
180
|
+
self._func_calls.add(node.func.attr)
|
|
181
|
+
|
|
182
|
+
def _analyze_file(self, fpath: str) -> list[DeadCodeFinding]:
|
|
183
|
+
"""Analyze a single file for dead code."""
|
|
184
|
+
findings: list[DeadCodeFinding] = []
|
|
185
|
+
try:
|
|
186
|
+
with open(fpath, "r") as f:
|
|
187
|
+
source = f.read()
|
|
188
|
+
tree = ast.parse(source, filename=fpath)
|
|
189
|
+
lines = source.split("\n")
|
|
190
|
+
except (SyntaxError, UnicodeDecodeError, FileNotFoundError, PermissionError):
|
|
191
|
+
return findings
|
|
192
|
+
|
|
193
|
+
rel = self._relpath(fpath)
|
|
194
|
+
|
|
195
|
+
# --- UNREACHABLE CODE (function bodies only) ---
|
|
196
|
+
for node in ast.walk(tree):
|
|
197
|
+
if not isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
198
|
+
continue
|
|
199
|
+
body = node.body
|
|
200
|
+
for i, child in enumerate(body):
|
|
201
|
+
if isinstance(child, (ast.Return, ast.Raise, ast.Break, ast.Continue)):
|
|
202
|
+
if i + 1 < len(body):
|
|
203
|
+
next_sib = body[i + 1]
|
|
204
|
+
if isinstance(next_sib, ast.Expr) and isinstance(next_sib.value, ast.Constant):
|
|
205
|
+
continue # Skip docstrings
|
|
206
|
+
findings.append(DeadCodeFinding(
|
|
207
|
+
file=rel, line=next_sib.lineno,
|
|
208
|
+
category="unreachable",
|
|
209
|
+
message=f"Code after {type(child).__name__.lower()} on line {child.lineno} is unreachable",
|
|
210
|
+
))
|
|
211
|
+
|
|
212
|
+
# --- EMPTY FUNCTIONS ---
|
|
213
|
+
for node in ast.walk(tree):
|
|
214
|
+
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
215
|
+
body = node.body
|
|
216
|
+
# Strip docstrings
|
|
217
|
+
if body and isinstance(body[0], ast.Expr) and isinstance(body[0].value, ast.Constant):
|
|
218
|
+
body = body[1:]
|
|
219
|
+
if len(body) == 0 or (len(body) == 1 and isinstance(body[0], ast.Pass)):
|
|
220
|
+
findings.append(DeadCodeFinding(
|
|
221
|
+
file=rel, line=node.lineno,
|
|
222
|
+
category="empty_function",
|
|
223
|
+
message=f"Function '{node.name}' has no implementation (empty body)",
|
|
224
|
+
))
|
|
225
|
+
|
|
226
|
+
# --- UNUSED IMPORTS ---
|
|
227
|
+
if rel in self._imports:
|
|
228
|
+
used_names = set()
|
|
229
|
+
for node in ast.walk(tree):
|
|
230
|
+
if isinstance(node, ast.Name):
|
|
231
|
+
used_names.add(node.id)
|
|
232
|
+
elif isinstance(node, ast.Attribute):
|
|
233
|
+
if isinstance(node.value, ast.Name):
|
|
234
|
+
used_names.add(node.value.id)
|
|
235
|
+
|
|
236
|
+
for imp_name in self._imports[rel]:
|
|
237
|
+
# Split dotted imports to check root
|
|
238
|
+
root = imp_name.split(".")[0]
|
|
239
|
+
if root not in used_names and imp_name not in used_names:
|
|
240
|
+
# Find the import line
|
|
241
|
+
for node in ast.walk(tree):
|
|
242
|
+
if isinstance(node, ast.Import):
|
|
243
|
+
for alias in node.names:
|
|
244
|
+
name = alias.asname or alias.name
|
|
245
|
+
if name == imp_name:
|
|
246
|
+
findings.append(DeadCodeFinding(
|
|
247
|
+
file=rel, line=node.lineno,
|
|
248
|
+
category="unused_import",
|
|
249
|
+
message=f"Import '{imp_name}' is never used",
|
|
250
|
+
))
|
|
251
|
+
elif isinstance(node, ast.ImportFrom):
|
|
252
|
+
for alias in node.names:
|
|
253
|
+
name = alias.asname or alias.name
|
|
254
|
+
if name == imp_name:
|
|
255
|
+
findings.append(DeadCodeFinding(
|
|
256
|
+
file=rel, line=node.lineno,
|
|
257
|
+
category="unused_import",
|
|
258
|
+
message=f"Import '{imp_name}' is never used",
|
|
259
|
+
))
|
|
260
|
+
|
|
261
|
+
return findings
|
|
262
|
+
|
|
263
|
+
def find_unused_functions(self) -> list[DeadCodeFinding]:
|
|
264
|
+
"""Post-scan: identify functions defined but never called project-wide."""
|
|
265
|
+
findings: list[DeadCodeFinding] = []
|
|
266
|
+
for func_name, defs in self._func_defs.items():
|
|
267
|
+
if func_name.startswith("_"):
|
|
268
|
+
continue # Private functions are often unused by design
|
|
269
|
+
if func_name.startswith("test_"):
|
|
270
|
+
continue # Test functions are called by pytest, not other code
|
|
271
|
+
if func_name in self.WHITELIST_FUNCTIONS:
|
|
272
|
+
continue
|
|
273
|
+
if func_name not in self._func_calls:
|
|
274
|
+
for file, line in defs:
|
|
275
|
+
findings.append(DeadCodeFinding(
|
|
276
|
+
file=file, line=line,
|
|
277
|
+
category="unused_function",
|
|
278
|
+
message=f"Function '{func_name}' is defined but never called in the project",
|
|
279
|
+
))
|
|
280
|
+
return findings
|