Skip to content

Commit 401d67c

Browse files
improve: fix CI branch target (main->master) and fix CONTRIBUTING.md typos (#46)
* improve: fix CI branch target (main->master) and fix CONTRIBUTING.md typos * chore: add GitHub issue and PR templates * Harden remove against multi-line corruption; fix docs + CI - remove: only blank self-contained single-line findings; skip multi-line constructs with a warning so it never leaves syntactically broken code - README: 'AST analysis' -> regex-based; clarify safe-removal behavior - package.json: correct description (TS/React/Next.js, not Python) - CI: drop pytest -x so all failures surface; add coverage * fix: resolve ruff lint errors (F841, W293, W292) and add stats command by dev-engineer * fix: add tomli fallback for Python 3.10 tomllib compatibility by dev-engineer * fix: correct escape handling and string closure check in _line_self_contained by reviewer-B
1 parent f5870de commit 401d67c

10 files changed

Lines changed: 158 additions & 23 deletions

File tree

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
---
2+
name: Bug Report
3+
about: Report a bug to help us improve
4+
title: "[BUG] "
5+
labels: bug
6+
assignees: ""
7+
---
8+
9+
**Describe the Bug**
10+
A clear and concise description of what the bug is.
11+
12+
**To Reproduce**
13+
Steps to reproduce the behavior:
14+
1. Run command '...'
15+
2. With input '...'
16+
3. See error
17+
18+
**Expected Behavior**
19+
A clear and concise description of what you expected to happen.
20+
21+
**Environment (please complete):**
22+
- OS: [e.g. Ubuntu 22.04, macOS 14, Windows 11]
23+
- Python version: [e.g. 3.10, 3.12]
24+
- deadcode version: [e.g. 0.1.1]
25+
26+
**Additional Context**
27+
Add any other context about the problem here.
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
---
2+
name: Feature Request
3+
about: Suggest an idea for this project
4+
title: "[FEATURE] "
5+
labels: enhancement
6+
assignees: ""
7+
---
8+
9+
**Is your feature request related to a problem?**
10+
A clear and concise description of what the problem is.
11+
12+
**Describe the Solution**
13+
A clear and concise description of what you want to happen.
14+
15+
**Describe Alternatives**
16+
A clear and concise description of any alternative solutions or features you've considered.
17+
18+
**Additional Context**
19+
Add any other context or screenshots about the feature request here.

.github/PULL_REQUEST_TEMPLATE.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
## Description
2+
3+
<!-- Briefly describe the changes in this PR -->
4+
5+
## Type of Change
6+
7+
- [ ] Bug fix
8+
- [ ] New feature
9+
- [ ] Documentation update
10+
- [ ] CI/CD improvement
11+
- [ ] Refactoring
12+
- [ ] Dependency update
13+
14+
## How Has This Been Tested?
15+
16+
- [ ] `pytest tests/ -v` passes
17+
- [ ] `ruff check .` passes
18+
19+
## Checklist
20+
21+
- [ ] My code follows the project's style guidelines
22+
- [ ] I have added tests that prove my fix/feature works
23+
- [ ] All existing tests pass
24+
- [ ] I have updated documentation as needed
25+
26+
## Related Issues
27+
28+
<!-- Link to any related issues: Fixes #123, Closes #456 -->

.github/workflows/ci.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,10 @@ name: CI
22

33
on:
44
push:
5-
branches: [main]
5+
branches: [master]
66
tags: ["v*"]
77
pull_request:
8-
branches: [main]
8+
branches: [master]
99

1010
permissions:
1111
contents: read
@@ -36,7 +36,7 @@ jobs:
3636
run: ruff check .
3737

3838
- name: Run tests
39-
run: python -m pytest tests/ -x -q
39+
run: python -m pytest tests/ -q --cov=deadcode --cov-report=term-missing
4040

4141
publish:
4242
needs: test

CONTRIBUTING.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,15 @@ Thanks for your interest in contributing!
88
2. Create a virtual environment: python -m venv .venv && source .venv/bin/activate
99
3. Install dev dependencies: pip install -e ".[dev]"
1010
4. Run tests: pytest tests/ -v
11-
5. Lint: uff check src/
11+
5. Lint: ruff check src/
12+
6. Run ruff format src/ --check before committing
1213

1314
## Pull Requests
1415

1516
- Fork the repo and create a feature branch
1617
- Add tests for any new functionality
1718
- Ensure all existing tests pass
18-
- Run uff check src/ --fix before committing
19+
- Run ruff check src/ --fix before committing
1920
- Keep PRs focused on a single change
2021

2122
## Reporting Issues
@@ -32,4 +33,4 @@ Thanks for your interest in contributing!
3233

3334
## License
3435

35-
By contributing, you agree your work will be licensed under the same license as this project.
36+
By contributing, you agree your work will be licensed under the same license as this project.

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -76,8 +76,8 @@ deadcode stats
7676
- **Unused export detection** — finds functions, types, classes, interfaces, enums, and consts that are exported but never imported within your project
7777
- **Dead route detection** — detects unreachable page components in Next.js App Router projects
7878
- **Orphaned CSS detection** — finds CSS module classes that are defined but never referenced in TSX/JSX files
79-
- **Safe auto-removal**`--dry-run` preview mode shows exactly what will be deleted before making changes
80-
- **Full-project AST analysis** — regex-based scanning covers export/import patterns, route detection, CSS class usage, and component references across your entire codebase
79+
- **Safe auto-removal**`--dry-run` preview shows exactly what will be deleted first; `remove` only blanks self-contained single-line findings and skips (with a warning) anything spanning multiple lines, so it never leaves half-deleted, broken code
80+
- **Full-project scanning**fast regex-based scanning covers export/import patterns, route detection, CSS class usage, and component references across your entire codebase (no AST/tree-sitter — deliberately dependency-free and quick)
8181
- **Monorepo support** — handles large projects efficiently with ignore patterns
8282
- **CI integration** — JSON output for automated pipelines and gating
8383

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "deadcode-cli",
33
"version": "0.1.1",
4-
"description": "Find unused/dead code in Python projects. Static analysis tool to identify orphaned functions, classes, and imports.",
4+
"description": "Find unused/dead code in TypeScript, React, and Next.js projects. Scans for orphaned exports, dead routes, unreferenced components, and unused CSS module classes.",
55
"author": "Revenue Holdings <engineering@revenueholdings.dev>",
66
"license": "MIT",
77
"repository": {

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ dev = [
3434
"pytest>=7.0.0",
3535
"pytest-cov>=4.0.0",
3636
"ruff>=0.4.0",
37+
"tomli>=1.1.0; python_version < '3.11'",
3738
]
3839

3940
[project.urls]

src/deadcode/cli.py

Lines changed: 65 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,40 @@
2727
FORMAT_CHOICES = click.Choice(["pretty", "compact", "github", "json"])
2828

2929

30+
def _line_self_contained(text: str) -> bool:
31+
"""Return True if a line's brackets/braces/parens are balanced on that line.
32+
33+
Used by ``remove`` to decide whether a single reported line can be safely
34+
blanked. A balanced line is a complete one-liner (``export const X = 1;`` or
35+
``.foo { color: red; }``); a line that opens a brace/bracket/paren it never
36+
closes is the start of a multi-line construct and must not be blanked in
37+
isolation. String and template-literal contents are ignored so brackets
38+
inside quotes don't skew the count.
39+
"""
40+
depth = 0
41+
in_str: str | None = None
42+
escaped = False
43+
for ch in text:
44+
if escaped:
45+
escaped = False
46+
continue
47+
if ch == "\\":
48+
escaped = True
49+
continue
50+
if in_str is not None:
51+
if ch == in_str:
52+
in_str = None
53+
elif ch in ("'", '"', "`"):
54+
in_str = ch
55+
elif ch in "([{":
56+
depth += 1
57+
elif ch in ")]}":
58+
depth -= 1
59+
if depth < 0: # closes something opened on an earlier line
60+
return False
61+
return depth == 0 and in_str is None
62+
63+
3064
@click.group()
3165
@click.option("--project", "-p", default=".", help="Project directory to scan")
3266
@click.option(
@@ -315,24 +349,43 @@ def remove(ctx: click.Context, dry_run: bool, category: str | None) -> None:
315349
console.print(f"[red]Error reading {rel_file}: {e}[/red]")
316350
continue
317351

318-
# Remove lines in reverse order to preserve line numbers
319-
lines_to_remove = sorted(set(f.line for f in file_findings), reverse=True)
352+
# Findings carry only a start line, no span. Blanking a single line of a
353+
# multi-line construct (a multi-line `export { ... }`, a CSS rule, or a
354+
# component body) leaves dangling, syntactically-broken code — worse than
355+
# doing nothing. DeadCode is regex-based with no AST, so guard
356+
# conservatively: only blank a line whose brackets/braces/parens are
357+
# balanced on that line (it's a self-contained one-liner). Anything that
358+
# opens an unclosed block is skipped and reported for manual removal.
359+
candidate_lines = sorted(set(f.line for f in file_findings), reverse=True)
360+
safe_lines = [
361+
n
362+
for n in candidate_lines
363+
if 0 < n <= len(lines) and _line_self_contained(lines[n - 1])
364+
]
365+
skipped_lines = [n for n in candidate_lines if n not in safe_lines]
320366

321367
if dry_run:
322-
for line_num in sorted(lines_to_remove):
323-
content = lines[line_num - 1].rstrip() if line_num <= len(lines) else ""
368+
for line_num in sorted(safe_lines):
369+
content = lines[line_num - 1].strip()
324370
console.print(
325-
f"[yellow]WOULD REMOVE[/yellow] {rel_file}:{line_num}{content.strip()[:80]}"
371+
f"[yellow]WOULD REMOVE[/yellow] {rel_file}:{line_num}{content[:80]}"
326372
)
327-
removed_count += len(lines_to_remove)
373+
removed_count += len(safe_lines)
328374
else:
329-
for line_num in lines_to_remove:
330-
if 0 < line_num <= len(lines):
331-
lines[line_num - 1] = "" # Blank the line (safer than deleting)
332-
filepath.write_text("".join(lines), encoding="utf-8")
333-
removed_count += len(lines_to_remove)
375+
for line_num in safe_lines:
376+
lines[line_num - 1] = "" # Blank the line (safer than deleting)
377+
if safe_lines:
378+
filepath.write_text("".join(lines), encoding="utf-8")
379+
removed_count += len(safe_lines)
380+
console.print(
381+
f"[green]✓[/green] Cleaned {rel_file} ({len(safe_lines)} lines)"
382+
)
383+
384+
for line_num in sorted(skipped_lines):
385+
content = lines[line_num - 1].strip() if 0 < line_num <= len(lines) else ""
334386
console.print(
335-
f"[green]✓[/green] Cleaned {rel_file} ({len(lines_to_remove)} lines)"
387+
f"[yellow]⚠ SKIPPED (multi-line — remove manually)[/yellow] "
388+
f"{rel_file}:{line_num}{content[:80]}"
336389
)
337390

338391
action = "Would remove" if dry_run else "Removed"

tests/test_config_and_fixes.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -583,7 +583,10 @@ def test_ruff_known_first_party(self):
583583
"""ruff known-first-party should be ['deadcode'], not ['*']."""
584584
from pathlib import Path
585585

586-
import tomllib
586+
try:
587+
import tomllib # Python >=3.11
588+
except ModuleNotFoundError:
589+
import tomli as tomllib # Python 3.10 backport
587590

588591
pyproject = Path(__file__).parent.parent / "pyproject.toml"
589592
with open(pyproject, "rb") as f:
@@ -600,7 +603,10 @@ def test_package_data_includes_py_typed(self):
600603
"""pyproject.toml should have package-data config for py.typed."""
601604
from pathlib import Path
602605

603-
import tomllib
606+
try:
607+
import tomllib # Python >=3.11
608+
except ModuleNotFoundError:
609+
import tomli as tomllib # Python 3.10 backport
604610

605611
pyproject = Path(__file__).parent.parent / "pyproject.toml"
606612
with open(pyproject, "rb") as f:

0 commit comments

Comments
 (0)