gitreins 0.1.2__tar.gz
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.
- gitreins-0.1.2/LICENSE +9 -0
- gitreins-0.1.2/PKG-INFO +123 -0
- gitreins-0.1.2/README.md +96 -0
- gitreins-0.1.2/engine/__init__.py +2 -0
- gitreins-0.1.2/engine/dead_code.py +280 -0
- gitreins-0.1.2/engine/evaluator.py +663 -0
- gitreins-0.1.2/engine/guard_manager.py +347 -0
- gitreins-0.1.2/engine/judge.py +134 -0
- gitreins-0.1.2/engine/llm.py +298 -0
- gitreins-0.1.2/engine/pipeline.py +478 -0
- gitreins-0.1.2/engine/task_manager.py +148 -0
- gitreins-0.1.2/engine/version.py +1 -0
- gitreins-0.1.2/gitreins/cli.py +353 -0
- gitreins-0.1.2/gitreins.egg-info/PKG-INFO +123 -0
- gitreins-0.1.2/gitreins.egg-info/SOURCES.txt +29 -0
- gitreins-0.1.2/gitreins.egg-info/dependency_links.txt +1 -0
- gitreins-0.1.2/gitreins.egg-info/entry_points.txt +2 -0
- gitreins-0.1.2/gitreins.egg-info/requires.txt +3 -0
- gitreins-0.1.2/gitreins.egg-info/top_level.txt +3 -0
- gitreins-0.1.2/gitreins_mcp/__init__.py +1 -0
- gitreins-0.1.2/gitreins_mcp/server.py +417 -0
- gitreins-0.1.2/pyproject.toml +44 -0
- gitreins-0.1.2/setup.cfg +4 -0
- gitreins-0.1.2/tests/test_cli.py +535 -0
- gitreins-0.1.2/tests/test_evaluator.py +525 -0
- gitreins-0.1.2/tests/test_guard_manager.py +404 -0
- gitreins-0.1.2/tests/test_judge.py +266 -0
- gitreins-0.1.2/tests/test_llm.py +450 -0
- gitreins-0.1.2/tests/test_mcp_server.py +913 -0
- gitreins-0.1.2/tests/test_pipeline.py +366 -0
- gitreins-0.1.2/tests/test_task_manager.py +313 -0
gitreins-0.1.2/LICENSE
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Bane
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
6
|
+
|
|
7
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
8
|
+
|
|
9
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
gitreins-0.1.2/PKG-INFO
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: gitreins
|
|
3
|
+
Version: 0.1.2
|
|
4
|
+
Summary: Git-native AI agent co-harness — MCP server, static guards, and agentic evaluator for LLM-assisted coding
|
|
5
|
+
Author: Bane
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/totalwindupflightsystems/gitreins
|
|
8
|
+
Project-URL: Repository, https://github.com/totalwindupflightsystems/gitreins
|
|
9
|
+
Project-URL: Issues, https://github.com/totalwindupflightsystems/gitreins/issues
|
|
10
|
+
Keywords: git,ai,llm,mcp,code-review,agent
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Topic :: Software Development :: Quality Assurance
|
|
19
|
+
Classifier: Topic :: Software Development :: Version Control :: Git
|
|
20
|
+
Requires-Python: >=3.10
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
License-File: LICENSE
|
|
23
|
+
Requires-Dist: mcp>=1.0.0
|
|
24
|
+
Requires-Dist: pyyaml>=6.0
|
|
25
|
+
Requires-Dist: requests>=2.28
|
|
26
|
+
Dynamic: license-file
|
|
27
|
+
|
|
28
|
+
# GitReins (PoC)
|
|
29
|
+
|
|
30
|
+
**Git-Native Agent Co-Harness — Proof of Concept**
|
|
31
|
+
|
|
32
|
+
[](https://github.com/totalwindupflightsystems/gitreins/actions/workflows/ci.yml)
|
|
33
|
+
[](https://www.python.org/)
|
|
34
|
+
[](LICENSE)
|
|
35
|
+
[](https://github.com/totalwindupflightsystems/gitreins/releases)
|
|
36
|
+
[](https://pypi.org/project/gitreins/)
|
|
37
|
+
|
|
38
|
+

|
|
39
|
+
|
|
40
|
+
GitReins lives inside your git repository as a co-harness. It provides MCP tools for task lifecycle management, an agentic evaluator that judges code completeness against task definitions, and git hooks that ensure nothing bypasses the quality gates.
|
|
41
|
+
|
|
42
|
+
> ✅ **Proof of Concept — Implemented (v0.1.0)** — All engine modules, MCP server, CLI, and git hooks are built and working. 322 tests pass.
|
|
43
|
+
|
|
44
|
+
## Install
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
# PyPI (recommended)
|
|
48
|
+
pip install gitreins
|
|
49
|
+
|
|
50
|
+
# GitHub
|
|
51
|
+
pip install git+https://github.com/totalwindupflightsystems/gitreins.git
|
|
52
|
+
|
|
53
|
+
# From source
|
|
54
|
+
git clone https://github.com/totalwindupflightsystems/gitreins.git
|
|
55
|
+
cd gitreins && pip install -e .
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Then activate in any repo:
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
cd /path/to/your-project
|
|
62
|
+
gitreins install
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## How It Works
|
|
66
|
+
|
|
67
|
+
1. **Create tasks** — Define criteria via CLI or MCP tools
|
|
68
|
+
2. **Work with your AI agent** — Pi, Claude, Hermes, or Codex does code generation
|
|
69
|
+
3. **Complete tasks** — Agent calls `task.complete`
|
|
70
|
+
4. **Automatic evaluation** — Tier 1 static guards (secrets, lint, tests) + Tier 2 agentic evaluator
|
|
71
|
+
5. **Commit through harness** — `commit` tool runs guards, blocks if checks fail
|
|
72
|
+
|
|
73
|
+
The evaluator is an agentic loop: it reads files, runs tests, searches patterns, and delivers a structured verdict with per-criterion PASS/FAIL. No single-shot LLM judgment.
|
|
74
|
+
|
|
75
|
+
## Architecture & Docs
|
|
76
|
+
|
|
77
|
+
| Document | Purpose |
|
|
78
|
+
|----------|---------|
|
|
79
|
+
| [Full Architecture](docs/architecture.md) | System design and data flow |
|
|
80
|
+
| [Component Map](docs/component-map.md) | Module inventory with paths and line counts |
|
|
81
|
+
| [Agentic Evaluator Design](docs/evaluator-loop.md) | How the 7-tool agentic loop works |
|
|
82
|
+
| [Sandbox](docs/sandbox.md) | Evaluator scratch space (in-memory, with filesystem plans) |
|
|
83
|
+
| [Implementation Plan](docs/implementation-plan.md) | Phase history |
|
|
84
|
+
|
|
85
|
+
Full reverse-engineered specs are in `specs/` — one per component, with realized-by links to actual code files.
|
|
86
|
+
|
|
87
|
+
## Status
|
|
88
|
+
|
|
89
|
+
**Phase: Fully Implemented (v0.1.0)** — All seven engine modules, MCP server (9 tools), CLI (5 top-level commands: task, guard, judge, commit, mcp-server), git hooks, and install script are built. See [Component Map](docs/component-map.md) for current state.
|
|
90
|
+
|
|
91
|
+
## Quick Start
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
cd gitreins-poc
|
|
95
|
+
./gitreins/install # Activate hooks in <10 seconds
|
|
96
|
+
|
|
97
|
+
# Create a task and evaluate it
|
|
98
|
+
python3 gitreins/cli.py task create demo "Demo task" \
|
|
99
|
+
"File exists" "Has tests" "No secrets"
|
|
100
|
+
|
|
101
|
+
# Start the MCP server for your AI agent
|
|
102
|
+
python3 gitreins_mcp/server.py
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## Demos
|
|
106
|
+
Three demo projects are included showing GitReins in action:
|
|
107
|
+
| Project | Type | What it tests |
|
|
108
|
+
|---------|------|---------------|
|
|
109
|
+
| demo-slugify/ | Single-file | URL slug generator — basic criteria verification |
|
|
110
|
+
| demo-calc/ | Multi-file CLI | Calculator with operations, parser, CLI — 13 pytest tests |
|
|
111
|
+
| demo-string-utils/ | Single-file | String utilities with intentional palindrome bug — FAIL→FIX→PASS cycle |
|
|
112
|
+
|
|
113
|
+
Run any demo:
|
|
114
|
+
|
|
115
|
+
## Tech Stack
|
|
116
|
+
|
|
117
|
+
- **Language:** Python 3.10+
|
|
118
|
+
- **Dependencies:** mcp, pyyaml, requests (3 packages)
|
|
119
|
+
- **MCP Transport:** stdio
|
|
120
|
+
- **Config:** YAML in `.gitreins/` directory
|
|
121
|
+
- **Evaluator Model:** Haiku / GPT-4o-mini (<2s, ~$0.001/check)
|
|
122
|
+
|
|
123
|
+
<!-- axiom:trace work_item=GR-012 spec=specs/01-Architecture.md,specs/09-CLI.md,specs/10-Install-Bootstrap.md,specs/11-Configuration.md plan=.memory-bank/work-items/GR-012/plan.yaml -->
|
gitreins-0.1.2/README.md
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# GitReins (PoC)
|
|
2
|
+
|
|
3
|
+
**Git-Native Agent Co-Harness — Proof of Concept**
|
|
4
|
+
|
|
5
|
+
[](https://github.com/totalwindupflightsystems/gitreins/actions/workflows/ci.yml)
|
|
6
|
+
[](https://www.python.org/)
|
|
7
|
+
[](LICENSE)
|
|
8
|
+
[](https://github.com/totalwindupflightsystems/gitreins/releases)
|
|
9
|
+
[](https://pypi.org/project/gitreins/)
|
|
10
|
+
|
|
11
|
+

|
|
12
|
+
|
|
13
|
+
GitReins lives inside your git repository as a co-harness. It provides MCP tools for task lifecycle management, an agentic evaluator that judges code completeness against task definitions, and git hooks that ensure nothing bypasses the quality gates.
|
|
14
|
+
|
|
15
|
+
> ✅ **Proof of Concept — Implemented (v0.1.0)** — All engine modules, MCP server, CLI, and git hooks are built and working. 322 tests pass.
|
|
16
|
+
|
|
17
|
+
## Install
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
# PyPI (recommended)
|
|
21
|
+
pip install gitreins
|
|
22
|
+
|
|
23
|
+
# GitHub
|
|
24
|
+
pip install git+https://github.com/totalwindupflightsystems/gitreins.git
|
|
25
|
+
|
|
26
|
+
# From source
|
|
27
|
+
git clone https://github.com/totalwindupflightsystems/gitreins.git
|
|
28
|
+
cd gitreins && pip install -e .
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Then activate in any repo:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
cd /path/to/your-project
|
|
35
|
+
gitreins install
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## How It Works
|
|
39
|
+
|
|
40
|
+
1. **Create tasks** — Define criteria via CLI or MCP tools
|
|
41
|
+
2. **Work with your AI agent** — Pi, Claude, Hermes, or Codex does code generation
|
|
42
|
+
3. **Complete tasks** — Agent calls `task.complete`
|
|
43
|
+
4. **Automatic evaluation** — Tier 1 static guards (secrets, lint, tests) + Tier 2 agentic evaluator
|
|
44
|
+
5. **Commit through harness** — `commit` tool runs guards, blocks if checks fail
|
|
45
|
+
|
|
46
|
+
The evaluator is an agentic loop: it reads files, runs tests, searches patterns, and delivers a structured verdict with per-criterion PASS/FAIL. No single-shot LLM judgment.
|
|
47
|
+
|
|
48
|
+
## Architecture & Docs
|
|
49
|
+
|
|
50
|
+
| Document | Purpose |
|
|
51
|
+
|----------|---------|
|
|
52
|
+
| [Full Architecture](docs/architecture.md) | System design and data flow |
|
|
53
|
+
| [Component Map](docs/component-map.md) | Module inventory with paths and line counts |
|
|
54
|
+
| [Agentic Evaluator Design](docs/evaluator-loop.md) | How the 7-tool agentic loop works |
|
|
55
|
+
| [Sandbox](docs/sandbox.md) | Evaluator scratch space (in-memory, with filesystem plans) |
|
|
56
|
+
| [Implementation Plan](docs/implementation-plan.md) | Phase history |
|
|
57
|
+
|
|
58
|
+
Full reverse-engineered specs are in `specs/` — one per component, with realized-by links to actual code files.
|
|
59
|
+
|
|
60
|
+
## Status
|
|
61
|
+
|
|
62
|
+
**Phase: Fully Implemented (v0.1.0)** — All seven engine modules, MCP server (9 tools), CLI (5 top-level commands: task, guard, judge, commit, mcp-server), git hooks, and install script are built. See [Component Map](docs/component-map.md) for current state.
|
|
63
|
+
|
|
64
|
+
## Quick Start
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
cd gitreins-poc
|
|
68
|
+
./gitreins/install # Activate hooks in <10 seconds
|
|
69
|
+
|
|
70
|
+
# Create a task and evaluate it
|
|
71
|
+
python3 gitreins/cli.py task create demo "Demo task" \
|
|
72
|
+
"File exists" "Has tests" "No secrets"
|
|
73
|
+
|
|
74
|
+
# Start the MCP server for your AI agent
|
|
75
|
+
python3 gitreins_mcp/server.py
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## Demos
|
|
79
|
+
Three demo projects are included showing GitReins in action:
|
|
80
|
+
| Project | Type | What it tests |
|
|
81
|
+
|---------|------|---------------|
|
|
82
|
+
| demo-slugify/ | Single-file | URL slug generator — basic criteria verification |
|
|
83
|
+
| demo-calc/ | Multi-file CLI | Calculator with operations, parser, CLI — 13 pytest tests |
|
|
84
|
+
| demo-string-utils/ | Single-file | String utilities with intentional palindrome bug — FAIL→FIX→PASS cycle |
|
|
85
|
+
|
|
86
|
+
Run any demo:
|
|
87
|
+
|
|
88
|
+
## Tech Stack
|
|
89
|
+
|
|
90
|
+
- **Language:** Python 3.10+
|
|
91
|
+
- **Dependencies:** mcp, pyyaml, requests (3 packages)
|
|
92
|
+
- **MCP Transport:** stdio
|
|
93
|
+
- **Config:** YAML in `.gitreins/` directory
|
|
94
|
+
- **Evaluator Model:** Haiku / GPT-4o-mini (<2s, ~$0.001/check)
|
|
95
|
+
|
|
96
|
+
<!-- axiom:trace work_item=GR-012 spec=specs/01-Architecture.md,specs/09-CLI.md,specs/10-Install-Bootstrap.md,specs/11-Configuration.md plan=.memory-bank/work-items/GR-012/plan.yaml -->
|
|
@@ -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
|