Offline static analysis CLI for Python repositories. No API keys. No internet. Just your code.
Repo Inspector parses your Python project with the standard library ast module and surfaces code quality metrics, architectural problems, and technical debt — all without touching the network.
- Cyclomatic complexity — per-function CC scores via Radon, God Object file detection
- Dead code — heuristic detection of unreferenced functions, classes, and files
- Module coupling — fan-in / fan-out scores, highlights highly coupled modules
- Import graph — visualizes the internal dependency tree, detects circular imports
- Duplicate code — AST-normalized structural similarity hashing
- Change impact — BFS on the reversed import graph to find blast radius of a change
- Health score — weighted 0–10 score across six signals (tests, docs, CI, complexity, coupling, dead code)
- TODO scanner — inventories TODO / FIXME / HACK / NOQA / XXX comments
- Respects
.gitignore— never traverses.git/,__pycache__/,.venv/,dist/
pip install repo-inspectorRequirements: Python 3.11 or later, no other system dependencies.
# Full analysis summary
repo-inspector scan /path/to/your/project
# With all individual findings printed
repo-inspector scan /path/to/your/project --full
# Export results to JSON
repo-inspector scan /path/to/your/project --output report.jsonExample output:
────────────────────── Project Analysis Summary ──────────────────────
Root path /path/to/your/project
Total files 84
Python files 71
Lines of code 24,392
Functions 512
Classes 73
Analyzer Critical Warning Info
Complexity Analysis 0 4 0
Import Analysis 1 0 0
Coupling Analysis 0 2 0
Dead Code Analysis 0 0 18
Duplication Analysis 0 3 0
TODO/FIXME Analysis 0 0 11
Health Analysis 0 1 0
Health Score ███████░░░ 7.2/10
| Command | Description |
|---|---|
scan <path> |
Full analysis summary across all analyzers |
complexity <path> |
Cyclomatic complexity per function, large file detection |
deadcode <path> |
Unused functions, classes, and unreferenced files |
coupling <path> |
Fan-in / fan-out module coupling scores |
imports <path> |
Import graph tree and circular dependency detection |
impact <path> <file> |
Modules affected if a given file changes |
health <path> |
Aggregated repository health score (0–10) |
duplication <path> |
Structurally identical function bodies |
todos <path> |
TODO / FIXME / HACK / NOQA / XXX inventory |
repo-inspector scan . --full # Print all individual findings after the summary
repo-inspector scan . --output out.json # Write full results to a JSON file| Metric | Warning | Critical |
|---|---|---|
| Cyclomatic complexity | >= 10 | >= 20 |
| Function size (lines) | >= 50 | >= 80 |
| File size (lines) | >= 500 | >= 1000 |
| Coupling score (fan-in + fan-out) | >= 10 | >= 20 |
The health command produces a weighted score out of 10. Weights are defined in analysis/health.py.
| Signal | Weight | Description |
|---|---|---|
| Test coverage estimate | 0.25 | Ratio of test files to source files |
| Avg complexity score | 0.20 | Inverted average cyclomatic complexity |
| Documentation ratio | 0.20 | Functions with docstrings / total functions |
| CI pipeline present | 0.15 | .github/workflows/, .gitlab-ci.yml, or Jenkinsfile |
| Dead code ratio | 0.10 | Inverted ratio of unreferenced functions |
| Linting config present | 0.10 | .flake8, .pylintrc, pyproject.toml [tool.ruff], etc. |
scan_project()traverses the directory respecting.gitignore, builds aProjectIndex- Each analyzer receives the immutable
ProjectIndexand returns anAnalysisResult cli.pycalls all analyzers and passes results toreport/formatter.pyfor Rich rendering- No analyzer touches the filesystem directly after the index is built
git clone https://github.com/samcab28/Repo-Inspector
cd repo-inspector
python -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install -e ".[dev]"
pytestpytest # all 211 tests
pytest --tb=short -v # verbose with short tracebacks
pytest tests/test_complexity.py # single modulepython -m build
# Produces:
# dist/repo_inspector-0.1.0-py3-none-any.whl
# dist/repo_inspector-0.1.0.tar.gzrepo_inspector/
├── cli.py # Typer CLI — all command definitions
├── scanner/
│ └── project_scanner.py # File traversal, .gitignore handling, ProjectIndex builder
├── analysis/
│ ├── complexity.py # Cyclomatic complexity, function/file size
│ ├── coupling.py # Fan-in / fan-out per module
│ ├── deadcode.py # Unused functions, classes, unreferenced files
│ ├── health.py # Health score aggregator (weights live here)
│ ├── impact.py # Change impact via reverse dependency BFS
│ ├── imports.py # Import graph, circular dependency detection
│ ├── similarity.py # Duplicate function detection via AST hashing
│ └── todos.py # TODO/FIXME/HACK/NOQA/XXX comment scanner
├── utils/
│ ├── ast_parser.py # Dataclasses: ProjectIndex, ModuleInfo, Finding, …
│ └── graph_builder.py # NetworkX DiGraph construction from ProjectIndex
└── report/
└── formatter.py # Rich-based console output (tables, trees, rules)
tests/
├── conftest.py # Shared fixtures: make_project_index, make_module_info
├── test_integration.py # End-to-end subprocess tests for every CLI command
├── test_ast_parser.py
├── test_scanner.py
├── test_complexity.py
├── test_coupling.py
├── test_deadcode.py
├── test_health.py
├── test_impact.py
├── test_imports.py
├── test_similarity.py
└── test_todos.py
These constraints are enforced by convention and tested indirectly:
- Single public function per analyzer: each module in
analysis/exposes exactlyanalyze(index: ProjectIndex) -> AnalysisResult. - Immutable
ProjectIndex: frozen dataclass — no analyzer may mutate it. - No
print()in analyzers: all output goes throughreport/formatter.py. cli.pyis the only cross-layer importer — it imports from bothanalysis/andreport/; analyzers never import fromreport/.- NetworkX for all graph work —
graph_builder.pyreturnsnx.DiGraph; all cycles/BFS use NetworkX algorithms.
Contributions are welcome. Please open an issue before submitting a large change so we can discuss the approach.
# Fork, clone, create a branch
git checkout -b feature/my-analyzer
# Make changes, add tests, confirm the suite passes
pytest --tb=short
# Open a pull request against mainAdding a new analyzer:
- Create
repo_inspector/analysis/my_analyzer.pywithanalyze(index) -> AnalysisResult - Add the corresponding
tests/test_my_analyzer.py - Import and wire up in
cli.py - Add it to the
scancommand'sresultsdict incli.py
MIT — © 2026 Repo Inspector Contributors