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.
Files changed (45) hide show
  1. code_health_suite-0.8.0/.github/workflows/pypi-publish.yml +30 -0
  2. code_health_suite-0.8.0/.gitignore +6 -0
  3. code_health_suite-0.8.0/DEPLOY.md +97 -0
  4. code_health_suite-0.8.0/PKG-INFO +91 -0
  5. code_health_suite-0.8.0/README.md +69 -0
  6. code_health_suite-0.8.0/mcpize.yaml +17 -0
  7. code_health_suite-0.8.0/pyproject.toml +37 -0
  8. code_health_suite-0.8.0/src/code_health_suite/__init__.py +3 -0
  9. code_health_suite-0.8.0/src/code_health_suite/__main__.py +4 -0
  10. code_health_suite-0.8.0/src/code_health_suite/ast_utils.py +178 -0
  11. code_health_suite-0.8.0/src/code_health_suite/engines/__init__.py +1 -0
  12. code_health_suite-0.8.0/src/code_health_suite/engines/bug_detect.py +969 -0
  13. code_health_suite-0.8.0/src/code_health_suite/engines/change_impact.py +840 -0
  14. code_health_suite-0.8.0/src/code_health_suite/engines/clone_detect.py +867 -0
  15. code_health_suite-0.8.0/src/code_health_suite/engines/complexity.py +944 -0
  16. code_health_suite-0.8.0/src/code_health_suite/engines/dead_code.py +1120 -0
  17. code_health_suite-0.8.0/src/code_health_suite/engines/dep_audit.py +880 -0
  18. code_health_suite-0.8.0/src/code_health_suite/engines/docstring_audit.py +522 -0
  19. code_health_suite-0.8.0/src/code_health_suite/engines/env_audit.py +596 -0
  20. code_health_suite-0.8.0/src/code_health_suite/engines/git_audit.py +639 -0
  21. code_health_suite-0.8.0/src/code_health_suite/engines/hotspot.py +670 -0
  22. code_health_suite-0.8.0/src/code_health_suite/engines/import_graph.py +813 -0
  23. code_health_suite-0.8.0/src/code_health_suite/engines/naming_check.py +514 -0
  24. code_health_suite-0.8.0/src/code_health_suite/engines/security_scan.py +934 -0
  25. code_health_suite-0.8.0/src/code_health_suite/engines/test_quality.py +632 -0
  26. code_health_suite-0.8.0/src/code_health_suite/engines/todo_scanner.py +426 -0
  27. code_health_suite-0.8.0/src/code_health_suite/engines/type_audit.py +614 -0
  28. code_health_suite-0.8.0/src/code_health_suite/server.py +1650 -0
  29. code_health_suite-0.8.0/tests/test_bug_detect.py +1070 -0
  30. code_health_suite-0.8.0/tests/test_change_impact.py +2538 -0
  31. code_health_suite-0.8.0/tests/test_clone_detect.py +866 -0
  32. code_health_suite-0.8.0/tests/test_complexity.py +933 -0
  33. code_health_suite-0.8.0/tests/test_dead_code.py +1832 -0
  34. code_health_suite-0.8.0/tests/test_dep_audit.py +1468 -0
  35. code_health_suite-0.8.0/tests/test_docstring_audit.py +824 -0
  36. code_health_suite-0.8.0/tests/test_env_audit.py +926 -0
  37. code_health_suite-0.8.0/tests/test_git_audit.py +1232 -0
  38. code_health_suite-0.8.0/tests/test_hotspot.py +1453 -0
  39. code_health_suite-0.8.0/tests/test_import_graph.py +2540 -0
  40. code_health_suite-0.8.0/tests/test_naming_check.py +505 -0
  41. code_health_suite-0.8.0/tests/test_package.py +221 -0
  42. code_health_suite-0.8.0/tests/test_security_scan.py +1283 -0
  43. code_health_suite-0.8.0/tests/test_test_quality.py +1952 -0
  44. code_health_suite-0.8.0/tests/test_todo_scanner.py +787 -0
  45. 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,6 @@
1
+ __pycache__/
2
+ *.pyc
3
+ *.egg-info/
4
+ dist/
5
+ build/
6
+ .pytest_cache/
@@ -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,3 @@
1
+ """Code Health Suite — 15 analysis engines, 26 MCP tools for Python code quality."""
2
+
3
+ __version__ = "0.7.0"
@@ -0,0 +1,4 @@
1
+ """Allow running as `python -m code_health_suite`."""
2
+ from code_health_suite.server import main
3
+
4
+ main()
@@ -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."""