code-health-suite 0.8.0__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.
- code_health_suite-0.8.0/.github/workflows/pypi-publish.yml +30 -0
- code_health_suite-0.8.0/.gitignore +6 -0
- code_health_suite-0.8.0/DEPLOY.md +97 -0
- code_health_suite-0.8.0/PKG-INFO +91 -0
- code_health_suite-0.8.0/README.md +69 -0
- code_health_suite-0.8.0/mcpize.yaml +17 -0
- code_health_suite-0.8.0/pyproject.toml +37 -0
- code_health_suite-0.8.0/src/code_health_suite/__init__.py +3 -0
- code_health_suite-0.8.0/src/code_health_suite/__main__.py +4 -0
- code_health_suite-0.8.0/src/code_health_suite/ast_utils.py +178 -0
- code_health_suite-0.8.0/src/code_health_suite/engines/__init__.py +1 -0
- code_health_suite-0.8.0/src/code_health_suite/engines/bug_detect.py +969 -0
- code_health_suite-0.8.0/src/code_health_suite/engines/change_impact.py +840 -0
- code_health_suite-0.8.0/src/code_health_suite/engines/clone_detect.py +867 -0
- code_health_suite-0.8.0/src/code_health_suite/engines/complexity.py +944 -0
- code_health_suite-0.8.0/src/code_health_suite/engines/dead_code.py +1120 -0
- code_health_suite-0.8.0/src/code_health_suite/engines/dep_audit.py +880 -0
- code_health_suite-0.8.0/src/code_health_suite/engines/docstring_audit.py +522 -0
- code_health_suite-0.8.0/src/code_health_suite/engines/env_audit.py +596 -0
- code_health_suite-0.8.0/src/code_health_suite/engines/git_audit.py +639 -0
- code_health_suite-0.8.0/src/code_health_suite/engines/hotspot.py +670 -0
- code_health_suite-0.8.0/src/code_health_suite/engines/import_graph.py +813 -0
- code_health_suite-0.8.0/src/code_health_suite/engines/naming_check.py +514 -0
- code_health_suite-0.8.0/src/code_health_suite/engines/security_scan.py +934 -0
- code_health_suite-0.8.0/src/code_health_suite/engines/test_quality.py +632 -0
- code_health_suite-0.8.0/src/code_health_suite/engines/todo_scanner.py +426 -0
- code_health_suite-0.8.0/src/code_health_suite/engines/type_audit.py +614 -0
- code_health_suite-0.8.0/src/code_health_suite/server.py +1650 -0
- code_health_suite-0.8.0/tests/test_bug_detect.py +1070 -0
- code_health_suite-0.8.0/tests/test_change_impact.py +2538 -0
- code_health_suite-0.8.0/tests/test_clone_detect.py +866 -0
- code_health_suite-0.8.0/tests/test_complexity.py +933 -0
- code_health_suite-0.8.0/tests/test_dead_code.py +1832 -0
- code_health_suite-0.8.0/tests/test_dep_audit.py +1468 -0
- code_health_suite-0.8.0/tests/test_docstring_audit.py +824 -0
- code_health_suite-0.8.0/tests/test_env_audit.py +926 -0
- code_health_suite-0.8.0/tests/test_git_audit.py +1232 -0
- code_health_suite-0.8.0/tests/test_hotspot.py +1453 -0
- code_health_suite-0.8.0/tests/test_import_graph.py +2540 -0
- code_health_suite-0.8.0/tests/test_naming_check.py +505 -0
- code_health_suite-0.8.0/tests/test_package.py +221 -0
- code_health_suite-0.8.0/tests/test_security_scan.py +1283 -0
- code_health_suite-0.8.0/tests/test_test_quality.py +1952 -0
- code_health_suite-0.8.0/tests/test_todo_scanner.py +787 -0
- code_health_suite-0.8.0/tests/test_type_audit.py +1013 -0
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
name: Publish to PyPI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags:
|
|
6
|
+
- 'v*'
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
build-and-publish:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
environment: pypi
|
|
12
|
+
permissions:
|
|
13
|
+
id-token: write # Required for trusted publisher (OIDC)
|
|
14
|
+
|
|
15
|
+
steps:
|
|
16
|
+
- uses: actions/checkout@v4
|
|
17
|
+
|
|
18
|
+
- uses: actions/setup-python@v5
|
|
19
|
+
with:
|
|
20
|
+
python-version: '3.12'
|
|
21
|
+
|
|
22
|
+
- name: Install build tools
|
|
23
|
+
run: pip install build
|
|
24
|
+
|
|
25
|
+
- name: Build package
|
|
26
|
+
run: python -m build
|
|
27
|
+
|
|
28
|
+
- name: Publish to PyPI
|
|
29
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
30
|
+
# No API token needed — uses OIDC trusted publisher
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# Code Health Suite — Deployment Guide
|
|
2
|
+
|
|
3
|
+
## Status (2026-03-15)
|
|
4
|
+
- Server: **VERIFIED WORKING** (22 tools, 13 engines, initialize + tools/list + tools/call all pass)
|
|
5
|
+
- Package: **BUILD READY** (pyproject.toml + hatchling, zero deps)
|
|
6
|
+
- Tests: 4,164+ passing
|
|
7
|
+
- GitHub Actions: **READY** (.github/workflows/pypi-publish.yml — trusted publisher OIDC)
|
|
8
|
+
|
|
9
|
+
## Priority Path: Official MCP Registry (~20 min total)
|
|
10
|
+
|
|
11
|
+
The **Official MCP Registry** (registry.modelcontextprotocol.io) is the highest-value target.
|
|
12
|
+
Linux Foundation-backed industry standard. Preview phase = first-mover advantage window.
|
|
13
|
+
API v0.1 is frozen — publishing now won't break on API changes.
|
|
14
|
+
|
|
15
|
+
### Step 1: Create GitHub Repo (5 min)
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
cd ~/automation/workspace/code-health-suite
|
|
19
|
+
gh repo create code-health-suite --public --source=. --push
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
### Step 2: Configure PyPI Trusted Publisher (5 min)
|
|
23
|
+
|
|
24
|
+
1. Go to https://pypi.org/manage/account/publishing/
|
|
25
|
+
2. Add new pending publisher:
|
|
26
|
+
- PyPI project name: `code-health-suite`
|
|
27
|
+
- Owner: `nge` (your GitHub username)
|
|
28
|
+
- Repository: `code-health-suite`
|
|
29
|
+
- Workflow: `pypi-publish.yml`
|
|
30
|
+
- Environment: `pypi`
|
|
31
|
+
|
|
32
|
+
### Step 3: Publish to PyPI (3 min)
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
cd ~/automation/workspace/code-health-suite
|
|
36
|
+
git tag v0.5.0
|
|
37
|
+
git push --tags
|
|
38
|
+
# GitHub Actions will auto-publish to PyPI via trusted publisher (OIDC, no API token needed)
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Verify at: https://pypi.org/project/code-health-suite/
|
|
42
|
+
|
|
43
|
+
### Step 4: Publish to Official MCP Registry (5 min)
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
pip install mcp-publisher
|
|
47
|
+
mcp-publisher init # generates server.json
|
|
48
|
+
mcp-publisher login github # GitHub OAuth
|
|
49
|
+
mcp-publisher publish # publishes to registry.modelcontextprotocol.io
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Verify at: https://registry.modelcontextprotocol.io
|
|
53
|
+
|
|
54
|
+
### Post-publish: Check MCP Hive deadline
|
|
55
|
+
- MCP Hive founding provider deadline: **May 11, 2026**
|
|
56
|
+
- Apply at mcphive.com after registry listing is live
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
## Bonus: Additional Distribution Channels
|
|
61
|
+
|
|
62
|
+
### MCPize Cloud (~5 min)
|
|
63
|
+
|
|
64
|
+
MCPize deploys local code to their cloud. No GitHub repo needed.
|
|
65
|
+
Revenue split: 85% you / 15% MCPize.
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
cd ~/automation/workspace/code-health-suite
|
|
69
|
+
npx mcpize login
|
|
70
|
+
npx mcpize analyze
|
|
71
|
+
npx mcpize deploy
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### Smithery Registry (~5 min, needs GitHub)
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
npx @anthropic-ai/smithery-cli auth login
|
|
78
|
+
npx @anthropic-ai/smithery-cli mcp publish https://github.com/nge/code-health-suite -n code-health-suite
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### PulseMCP (2 min)
|
|
82
|
+
Visit https://pulsemcp.com → Submit Server → paste GitHub URL.
|
|
83
|
+
|
|
84
|
+
---
|
|
85
|
+
|
|
86
|
+
## Verification Commands
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
# Test server locally
|
|
90
|
+
echo '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"test","version":"0.1"}}}' | python3 -m code_health_suite
|
|
91
|
+
|
|
92
|
+
# Test tool listing
|
|
93
|
+
printf '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"test","version":"0.1"}}}\n{"jsonrpc":"2.0","method":"notifications/initialized"}\n{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}\n' | python3 -m code_health_suite
|
|
94
|
+
|
|
95
|
+
# Run test suite
|
|
96
|
+
cd ~/automation/workspace/code-health-suite && python -m pytest tests/ -q
|
|
97
|
+
```
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: code-health-suite
|
|
3
|
+
Version: 0.8.0
|
|
4
|
+
Summary: 16 analysis engines, 28 MCP tools for Python code quality. Zero external dependencies.
|
|
5
|
+
Project-URL: Homepage, https://github.com/neogeweb3/code-health-suite
|
|
6
|
+
Project-URL: Repository, https://github.com/neogeweb3/code-health-suite
|
|
7
|
+
Author-email: Neo <nge@users.noreply.github.com>
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
Keywords: code-health,complexity,dead-code,mcp,python,security,static-analysis
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Topic :: Software Development :: Quality Assurance
|
|
19
|
+
Classifier: Topic :: Software Development :: Testing
|
|
20
|
+
Requires-Python: >=3.10
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
|
|
23
|
+
# Code Health Suite
|
|
24
|
+
|
|
25
|
+
13 analysis engines, 22 MCP tools for Python code quality. Zero external dependencies.
|
|
26
|
+
|
|
27
|
+
## Quick Start
|
|
28
|
+
|
|
29
|
+
### As MCP Server (Claude Desktop / Claude Code)
|
|
30
|
+
|
|
31
|
+
```json
|
|
32
|
+
{
|
|
33
|
+
"mcpServers": {
|
|
34
|
+
"code-health": {
|
|
35
|
+
"command": "code-health-suite"
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### Install from PyPI
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
pip install code-health-suite
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Or with uvx (no install needed):
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
uvx code-health-suite
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### Install from GitHub
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
pip install git+https://github.com/nge/code-health-suite
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Tools
|
|
60
|
+
|
|
61
|
+
| # | Tool | Engine | What it does |
|
|
62
|
+
|---|------|--------|-------------|
|
|
63
|
+
| 1 | `analyze_complexity` | complexity | Per-function CC, cognitive complexity, nesting, grades |
|
|
64
|
+
| 2 | `get_complexity_score` | complexity | Project health score 0-100 |
|
|
65
|
+
| 3 | `find_dead_code` | dead-code | Unused imports, functions, variables, arguments |
|
|
66
|
+
| 4 | `security_scan` | security | OWASP vulns, CWE-mapped findings |
|
|
67
|
+
| 5 | `get_security_score` | security | Security health score 0-100 |
|
|
68
|
+
| 6 | `analyze_imports` | import-graph | Import dependency graph, circular deps |
|
|
69
|
+
| 7 | `get_import_health` | import-graph | Import architecture score 0-100 |
|
|
70
|
+
| 8 | `find_clones` | clone-detect | Type-1/2/3 code clone detection |
|
|
71
|
+
| 9 | `analyze_test_quality` | test-quality | Test suite metrics, anti-patterns |
|
|
72
|
+
| 10 | `full_health_check` | all engines | Combined report with overall grade |
|
|
73
|
+
| 11 | `find_hotspots` | hotspot | Files with high git churn AND high complexity |
|
|
74
|
+
| 12 | `get_hotspot_score` | hotspot | Project churn-complexity score |
|
|
75
|
+
| 13 | `audit_dependencies` | dep-audit | Outdated/vulnerable dependency check |
|
|
76
|
+
| 14 | `analyze_change_impact` | change-impact | Blast radius of file changes |
|
|
77
|
+
| 15 | `get_coupling_score` | change-impact | Module coupling metrics |
|
|
78
|
+
| 16 | `analyze_types` | type-audit | Type annotation coverage |
|
|
79
|
+
| 17 | `get_type_score` | type-audit | Type coverage score 0-100 |
|
|
80
|
+
| 18 | `audit_env` | env-audit | Environment variable audit |
|
|
81
|
+
| 19 | `audit_git_commits` | git-audit | Commit quality audit (security + complexity) |
|
|
82
|
+
| 20 | `get_git_audit_score` | git-audit | Git commit health score |
|
|
83
|
+
|
|
84
|
+
## Requirements
|
|
85
|
+
|
|
86
|
+
- Python 3.10+
|
|
87
|
+
- Zero external dependencies (stdlib only)
|
|
88
|
+
|
|
89
|
+
## License
|
|
90
|
+
|
|
91
|
+
MIT
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# Code Health Suite
|
|
2
|
+
|
|
3
|
+
13 analysis engines, 22 MCP tools for Python code quality. Zero external dependencies.
|
|
4
|
+
|
|
5
|
+
## Quick Start
|
|
6
|
+
|
|
7
|
+
### As MCP Server (Claude Desktop / Claude Code)
|
|
8
|
+
|
|
9
|
+
```json
|
|
10
|
+
{
|
|
11
|
+
"mcpServers": {
|
|
12
|
+
"code-health": {
|
|
13
|
+
"command": "code-health-suite"
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
### Install from PyPI
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
pip install code-health-suite
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Or with uvx (no install needed):
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
uvx code-health-suite
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
### Install from GitHub
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
pip install git+https://github.com/nge/code-health-suite
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Tools
|
|
38
|
+
|
|
39
|
+
| # | Tool | Engine | What it does |
|
|
40
|
+
|---|------|--------|-------------|
|
|
41
|
+
| 1 | `analyze_complexity` | complexity | Per-function CC, cognitive complexity, nesting, grades |
|
|
42
|
+
| 2 | `get_complexity_score` | complexity | Project health score 0-100 |
|
|
43
|
+
| 3 | `find_dead_code` | dead-code | Unused imports, functions, variables, arguments |
|
|
44
|
+
| 4 | `security_scan` | security | OWASP vulns, CWE-mapped findings |
|
|
45
|
+
| 5 | `get_security_score` | security | Security health score 0-100 |
|
|
46
|
+
| 6 | `analyze_imports` | import-graph | Import dependency graph, circular deps |
|
|
47
|
+
| 7 | `get_import_health` | import-graph | Import architecture score 0-100 |
|
|
48
|
+
| 8 | `find_clones` | clone-detect | Type-1/2/3 code clone detection |
|
|
49
|
+
| 9 | `analyze_test_quality` | test-quality | Test suite metrics, anti-patterns |
|
|
50
|
+
| 10 | `full_health_check` | all engines | Combined report with overall grade |
|
|
51
|
+
| 11 | `find_hotspots` | hotspot | Files with high git churn AND high complexity |
|
|
52
|
+
| 12 | `get_hotspot_score` | hotspot | Project churn-complexity score |
|
|
53
|
+
| 13 | `audit_dependencies` | dep-audit | Outdated/vulnerable dependency check |
|
|
54
|
+
| 14 | `analyze_change_impact` | change-impact | Blast radius of file changes |
|
|
55
|
+
| 15 | `get_coupling_score` | change-impact | Module coupling metrics |
|
|
56
|
+
| 16 | `analyze_types` | type-audit | Type annotation coverage |
|
|
57
|
+
| 17 | `get_type_score` | type-audit | Type coverage score 0-100 |
|
|
58
|
+
| 18 | `audit_env` | env-audit | Environment variable audit |
|
|
59
|
+
| 19 | `audit_git_commits` | git-audit | Commit quality audit (security + complexity) |
|
|
60
|
+
| 20 | `get_git_audit_score` | git-audit | Git commit health score |
|
|
61
|
+
|
|
62
|
+
## Requirements
|
|
63
|
+
|
|
64
|
+
- Python 3.10+
|
|
65
|
+
- Zero external dependencies (stdlib only)
|
|
66
|
+
|
|
67
|
+
## License
|
|
68
|
+
|
|
69
|
+
MIT
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
version: 1
|
|
2
|
+
name: code-health-suite
|
|
3
|
+
description: >-
|
|
4
|
+
13 analysis engines, 22 MCP tools for Python code quality.
|
|
5
|
+
Complexity, dead code, security, imports, clones, test quality,
|
|
6
|
+
hotspots, dependencies, change impact, type coverage, env audit,
|
|
7
|
+
naming conventions, and git commit audit. Zero external dependencies.
|
|
8
|
+
runtime: python
|
|
9
|
+
entry: src/code_health_suite/server.py
|
|
10
|
+
|
|
11
|
+
build:
|
|
12
|
+
install: pip install -e .
|
|
13
|
+
|
|
14
|
+
startCommand:
|
|
15
|
+
type: stdio
|
|
16
|
+
command: python
|
|
17
|
+
args: ["-m", "code_health_suite"]
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "code-health-suite"
|
|
7
|
+
version = "0.8.0"
|
|
8
|
+
description = "16 analysis engines, 28 MCP tools for Python code quality. Zero external dependencies."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
requires-python = ">=3.10"
|
|
12
|
+
authors = [
|
|
13
|
+
{ name = "Neo", email = "nge@users.noreply.github.com" },
|
|
14
|
+
]
|
|
15
|
+
keywords = ["mcp", "code-health", "static-analysis", "complexity", "security", "dead-code", "python"]
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Development Status :: 4 - Beta",
|
|
18
|
+
"Intended Audience :: Developers",
|
|
19
|
+
"License :: OSI Approved :: MIT License",
|
|
20
|
+
"Programming Language :: Python :: 3",
|
|
21
|
+
"Programming Language :: Python :: 3.10",
|
|
22
|
+
"Programming Language :: Python :: 3.11",
|
|
23
|
+
"Programming Language :: Python :: 3.12",
|
|
24
|
+
"Programming Language :: Python :: 3.13",
|
|
25
|
+
"Topic :: Software Development :: Quality Assurance",
|
|
26
|
+
"Topic :: Software Development :: Testing",
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
[project.urls]
|
|
30
|
+
Homepage = "https://github.com/neogeweb3/code-health-suite"
|
|
31
|
+
Repository = "https://github.com/neogeweb3/code-health-suite"
|
|
32
|
+
|
|
33
|
+
[project.scripts]
|
|
34
|
+
code-health-suite = "code_health_suite.server:main"
|
|
35
|
+
|
|
36
|
+
[tool.hatch.build.targets.wheel]
|
|
37
|
+
packages = ["src/code_health_suite"]
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""ast_utils — Safe AST traversal utilities for Python analysis tools.
|
|
3
|
+
|
|
4
|
+
Prevents the ast.walk nested scope leakage bug pattern where ast.walk()
|
|
5
|
+
traverses into nested function/class bodies, causing incorrect scope
|
|
6
|
+
attribution in analysis tools.
|
|
7
|
+
|
|
8
|
+
Known bugs caused by this pattern:
|
|
9
|
+
- BUG-40 (ai-complexity): compute_cyclomatic counted nested function CC
|
|
10
|
+
- BUG-41 (ai-dead-code): find_unused_variables attributed inner vars to outer
|
|
11
|
+
|
|
12
|
+
Usage:
|
|
13
|
+
from ast_utils import walk_scope
|
|
14
|
+
|
|
15
|
+
# Instead of: for child in ast.walk(function_node): ...
|
|
16
|
+
# Use: for child in walk_scope(function_node): ...
|
|
17
|
+
|
|
18
|
+
# The boundary nodes (nested def/class) are yielded,
|
|
19
|
+
# but their children are NOT traversed.
|
|
20
|
+
"""
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import ast
|
|
24
|
+
from typing import Iterator
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
__version__ = "0.1.0"
|
|
28
|
+
|
|
29
|
+
# Node types that create a new scope boundary.
|
|
30
|
+
# When encountered during traversal, we yield the node itself
|
|
31
|
+
# (so callers can see it exists) but do NOT descend into its body.
|
|
32
|
+
SCOPE_BOUNDARIES = (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def walk_scope(node: ast.AST) -> Iterator[ast.AST]:
|
|
36
|
+
"""Walk AST nodes within the current scope, skipping nested scope bodies.
|
|
37
|
+
|
|
38
|
+
Like ``ast.walk()`` but stops at scope boundaries (nested function/class
|
|
39
|
+
definitions). The boundary node itself IS yielded so callers can detect
|
|
40
|
+
its presence, but its children are NOT traversed.
|
|
41
|
+
|
|
42
|
+
This is the correct traversal for any function-level analysis that should
|
|
43
|
+
not "leak" into nested definitions — cyclomatic complexity, variable
|
|
44
|
+
assignment tracking, control-flow analysis, etc.
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
node: The root AST node (typically a FunctionDef or Module).
|
|
48
|
+
|
|
49
|
+
Yields:
|
|
50
|
+
Child AST nodes within the same scope.
|
|
51
|
+
|
|
52
|
+
Example::
|
|
53
|
+
|
|
54
|
+
def outer(): # root node
|
|
55
|
+
x = 1 # ✓ yielded
|
|
56
|
+
if cond: # ✓ yielded
|
|
57
|
+
y = 2 # ✓ yielded
|
|
58
|
+
def inner(): # ✓ yielded (boundary node)
|
|
59
|
+
z = 3 # ✗ NOT yielded (nested scope)
|
|
60
|
+
class Nested: # ✓ yielded (boundary node)
|
|
61
|
+
attr = 4 # ✗ NOT yielded (nested scope)
|
|
62
|
+
"""
|
|
63
|
+
# DFS using a list-based stack. We push all immediate children of
|
|
64
|
+
# non-boundary nodes. Boundary nodes are yielded but not expanded.
|
|
65
|
+
stack = list(ast.iter_child_nodes(node))
|
|
66
|
+
while stack:
|
|
67
|
+
child = stack.pop()
|
|
68
|
+
yield child
|
|
69
|
+
if not isinstance(child, SCOPE_BOUNDARIES):
|
|
70
|
+
stack.extend(ast.iter_child_nodes(child))
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def walk_scope_bfs(node: ast.AST) -> Iterator[ast.AST]:
|
|
74
|
+
"""BFS variant of walk_scope — yields nodes in breadth-first order.
|
|
75
|
+
|
|
76
|
+
Same scope-boundary semantics as ``walk_scope()``, but uses BFS instead
|
|
77
|
+
of DFS. Useful when processing order matters (e.g., you want to see
|
|
78
|
+
all top-level statements before nested ones).
|
|
79
|
+
"""
|
|
80
|
+
from collections import deque
|
|
81
|
+
queue = deque(ast.iter_child_nodes(node))
|
|
82
|
+
while queue:
|
|
83
|
+
child = queue.popleft()
|
|
84
|
+
yield child
|
|
85
|
+
if not isinstance(child, SCOPE_BOUNDARIES):
|
|
86
|
+
queue.extend(ast.iter_child_nodes(child))
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def collect_scope_names(
|
|
90
|
+
node: ast.AST,
|
|
91
|
+
*,
|
|
92
|
+
assignments: bool = True,
|
|
93
|
+
reads: bool = False,
|
|
94
|
+
) -> dict[str, list[int]]:
|
|
95
|
+
"""Collect variable names within the current scope only.
|
|
96
|
+
|
|
97
|
+
Args:
|
|
98
|
+
node: Root AST node to analyze.
|
|
99
|
+
assignments: Include assignment targets (Name in Store context).
|
|
100
|
+
reads: Include name reads (Name in Load context).
|
|
101
|
+
|
|
102
|
+
Returns:
|
|
103
|
+
Dict mapping variable name to list of line numbers.
|
|
104
|
+
"""
|
|
105
|
+
names: dict[str, list[int]] = {}
|
|
106
|
+
|
|
107
|
+
for child in walk_scope(node):
|
|
108
|
+
# Reads: Name in Load context
|
|
109
|
+
if reads and isinstance(child, ast.Name) and isinstance(child.ctx, ast.Load):
|
|
110
|
+
names.setdefault(child.id, []).append(child.lineno)
|
|
111
|
+
|
|
112
|
+
# Assignments: extract from specific assignment node types
|
|
113
|
+
# (NOT from bare Name/Store, which includes annotation-only targets)
|
|
114
|
+
if assignments:
|
|
115
|
+
if isinstance(child, ast.Assign):
|
|
116
|
+
for target in child.targets:
|
|
117
|
+
_collect_assign_targets(target, names)
|
|
118
|
+
elif isinstance(child, ast.AugAssign):
|
|
119
|
+
if isinstance(child.target, ast.Name):
|
|
120
|
+
names.setdefault(child.target.id, []).append(child.lineno)
|
|
121
|
+
elif isinstance(child, ast.AnnAssign) and child.value is not None:
|
|
122
|
+
if isinstance(child.target, ast.Name):
|
|
123
|
+
names.setdefault(child.target.id, []).append(child.lineno)
|
|
124
|
+
elif isinstance(child, ast.NamedExpr):
|
|
125
|
+
# Walrus operator: (x := value)
|
|
126
|
+
names.setdefault(child.target.id, []).append(child.lineno)
|
|
127
|
+
elif isinstance(child, ast.For) or isinstance(child, ast.AsyncFor):
|
|
128
|
+
_collect_assign_targets(child.target, names)
|
|
129
|
+
elif isinstance(child, (ast.With, ast.AsyncWith)):
|
|
130
|
+
for item in child.items:
|
|
131
|
+
if item.optional_vars is not None:
|
|
132
|
+
_collect_assign_targets(item.optional_vars, names)
|
|
133
|
+
elif isinstance(child, ast.ExceptHandler) and child.name:
|
|
134
|
+
names.setdefault(child.name, []).append(child.lineno)
|
|
135
|
+
|
|
136
|
+
return names
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _collect_assign_targets(
|
|
140
|
+
target: ast.AST, names: dict[str, list[int]]
|
|
141
|
+
) -> None:
|
|
142
|
+
"""Recursively collect Name nodes from assignment targets."""
|
|
143
|
+
if isinstance(target, ast.Name):
|
|
144
|
+
names.setdefault(target.id, []).append(target.lineno)
|
|
145
|
+
elif isinstance(target, (ast.Tuple, ast.List)):
|
|
146
|
+
for elt in target.elts:
|
|
147
|
+
_collect_assign_targets(elt, names)
|
|
148
|
+
elif isinstance(target, ast.Starred):
|
|
149
|
+
# Handle starred unpacking: a, *rest, z = items
|
|
150
|
+
_collect_assign_targets(target.value, names)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def count_scope_incrementors(
|
|
154
|
+
node: ast.AST,
|
|
155
|
+
incrementors: tuple,
|
|
156
|
+
) -> int:
|
|
157
|
+
"""Count occurrences of specific node types within current scope.
|
|
158
|
+
|
|
159
|
+
Useful for cyclomatic complexity, where you count decision points
|
|
160
|
+
only within the function's own scope (not nested functions).
|
|
161
|
+
|
|
162
|
+
Args:
|
|
163
|
+
node: Root AST node.
|
|
164
|
+
incrementors: Tuple of AST node types to count.
|
|
165
|
+
|
|
166
|
+
Returns:
|
|
167
|
+
Number of matching nodes found in scope.
|
|
168
|
+
"""
|
|
169
|
+
count = 0
|
|
170
|
+
for child in walk_scope(node):
|
|
171
|
+
if isinstance(child, incrementors):
|
|
172
|
+
count += 1
|
|
173
|
+
return count
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def is_scope_boundary(node: ast.AST) -> bool:
|
|
177
|
+
"""Check if a node creates a new scope boundary."""
|
|
178
|
+
return isinstance(node, SCOPE_BOUNDARIES)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Analysis engines for Code Health Suite."""
|