Skip to content

Commit 61997cb

Browse files
author
DevForge Engineer
committed
merge: origin/main into main (take revenueholdings-license import with graceful fallback)
2 parents 30be8c3 + 4a906cc commit 61997cb

5 files changed

Lines changed: 57 additions & 53 deletions

File tree

.github/CODEOWNERS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
* @Coding-Dev-Tools

AGENTS.md

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# deadcode
2+
3+
## Purpose
4+
Detect and remove unused exports, dead routes, orphaned CSS, and unreferenced components in TypeScript/React/Next.js projects.
5+
6+
## Build & Test Commands
7+
- Install: `pip install -e .` or `pip install deadcode-cli`
8+
- Test: `pytest tests/` (or `python -m pytest tests/ -v --tb=short`)
9+
- Lint: `ruff check .`
10+
- Build: `pip install build twine && python -m build && twine check dist/*`
11+
- CLI check: `deadcode --help`
12+
13+
## Architecture
14+
Key directories:
15+
- `src/deadcode/` — Main package (CLI, analyzers for TS/JS/CSS/React/Next.js)
16+
- `tests/` — Test suite
17+
- `.github/workflows/` — CI/CD (ci.yml, pages.yml, publish.yml)
18+
- `dist/` — Built distributions
19+
20+
## Conventions
21+
- Language: Python 3.10+ (with Node.js for TS analysis)
22+
- Test framework: pytest
23+
- CI: GitHub Actions (ci.yml, pages.yml, publish.yml)
24+
- Linting: ruff
25+
- Build system: setuptools
26+
- Package layout: src/ layout
27+
- Dependencies: click, rich, tree-sitter, tree-sitter-typescript, esprima
28+
- CLI entry point: deadcode.cli:cli
29+
- Default branch: main

package.json

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,5 +38,8 @@
3838
"engines": {
3939
"node": ">=16.0.0"
4040
},
41-
"preferGlobal": true
42-
}
41+
"preferGlobal": true,
42+
"publishConfig": {
43+
"access": "public"
44+
}
45+
}

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ dev = [
3535
"pytest-cov>=4.0.0",
3636
"ruff>=0.4.0",
3737
]
38+
license = ["revenueholdings-license>=0.1.0"]
3839

3940
[project.urls]
4041
Homepage = "https://github.com/Coding-Dev-Tools/deadcode"

src/deadcode/cli.py

Lines changed: 21 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -2,33 +2,37 @@
22

33
from __future__ import annotations
44

5+
import click
56
import json
67
import sys
78
from pathlib import Path
8-
9-
import click
109
from rich.console import Console
1110
from rich.table import Table
1211

12+
try:
13+
from revenueholdings_license import require_license
14+
except ImportError:
15+
import warnings
16+
warnings.warn("revenueholdings-license not installed; license checks skipped", stacklevel=2)
17+
def require_license(product: str) -> None: # type: ignore[misc]
18+
pass
19+
1320
from . import __version__
1421
from .config import DeadCodeConfig
1522
from .scanner import DeadCodeScanner, Finding
1623

1724
console = Console()
1825
err_console = Console(stderr=True)
1926

20-
FORMAT_HELP = "Output format: pretty (default), compact, github, or json"
2127
ALL_CATEGORIES = ["unused_export", "dead_route", "orphaned_css", "unreferenced_component"]
22-
FORMAT_CHOICES = click.Choice(["pretty", "compact", "github", "json"])
2328

2429

2530
@click.group()
2631
@click.option("--project", "-p", default=".", help="Project directory to scan")
2732
@click.option("--ignore", "-i", multiple=True, help="Additional ignore patterns (gitignore-style)")
28-
@click.option("--include", multiple=True, help="Include only matching files (gitignore-style whitelist)")
2933
@click.version_option(__version__, prog_name="deadcode")
3034
@click.pass_context
31-
def cli(ctx: click.Context, project: str, ignore: tuple[str, ...], include: tuple[str, ...]) -> None:
35+
def cli(ctx: click.Context, project: str, ignore: tuple[str, ...]) -> None:
3236
"""DeadCode — Find and remove dead code in TS/React/Next.js projects.
3337
3438
Scans for unused exports, dead routes, orphaned CSS classes,
@@ -37,7 +41,6 @@ def cli(ctx: click.Context, project: str, ignore: tuple[str, ...], include: tupl
3741
ctx.ensure_object(dict)
3842
ctx.obj["project"] = project
3943
ctx.obj["ignore"] = list(ignore) if ignore else None
40-
ctx.obj["include"] = list(include) if include else None
4144
# Load .deadcode.yml config
4245
ctx.obj["config"] = DeadCodeConfig.load(project)
4346

@@ -67,29 +70,23 @@ def _get_fail_threshold(ctx: click.Context) -> int:
6770

6871

6972
@cli.command()
70-
@click.option("--json-output", "-j", is_flag=True, help="Alias for --format=json (deprecated)")
71-
@click.option("--format", type=FORMAT_CHOICES, default="pretty", help=FORMAT_HELP)
73+
@click.option("--json-output", "-j", is_flag=True, help="Output as JSON")
7274
@click.option("--category", "-c", type=click.Choice(ALL_CATEGORIES), default=None, help="Filter by category")
7375
@click.option("--fail", "fail_threshold", type=int, default=None,
7476
help="Exit code 1 if findings >= threshold (overrides .deadcode.yml)")
7577
@click.pass_context
76-
def scan(
77-
ctx: click.Context,
78-
json_output: bool,
79-
format: str | None,
80-
category: str | None,
81-
fail_threshold: int | None,
82-
) -> None:
78+
def scan(ctx: click.Context, json_output: bool, category: str | None, fail_threshold: int | None) -> None:
8379
"""Scan project for dead code."""
80+
if require_license:
81+
require_license("deadcode")
8482
project = ctx.obj["project"]
8583
ignore = _merge_config_ignore(ctx)
8684

8785
if not Path(project).exists():
8886
err_console.print(f"[red]Project directory '{project}' not found.[/red]")
8987
sys.exit(1)
9088

91-
include_patterns = ctx.obj.get("include")
92-
scanner = DeadCodeScanner(project, ignore_patterns=ignore, include_patterns=include_patterns)
89+
scanner = DeadCodeScanner(project, ignore_patterns=ignore)
9390
result = scanner.scan()
9491

9592
# Filter by category
@@ -102,10 +99,7 @@ def scan(
10299
if not category and config and config.categories:
103100
findings = [f for f in findings if f.category in config.categories]
104101

105-
# Determine effective format (legacy --json-output maps to json)
106-
effective_format = "json" if json_output else (format or "pretty")
107-
108-
if effective_format == "json":
102+
if json_output:
109103
output = {
110104
"files_scanned": result.files_scanned,
111105
"findings": [
@@ -116,26 +110,6 @@ def scan(
116110
"errors": result.errors,
117111
}
118112
console.print(json.dumps(output, indent=2, default=str))
119-
elif effective_format == "compact":
120-
if not findings:
121-
console.print("OK — 0 findings")
122-
else:
123-
for f in findings:
124-
console.print(f"{f.file}:{f.line} \u2014 {f.category}: {f.name}")
125-
console.print(f"\n{len(findings)} findings")
126-
elif effective_format == "github":
127-
# GitHub Actions annotation syntax
128-
# ::warning file={name},line={line},endLine={line}::{message}
129-
if not findings:
130-
console.print("deadcode: 0 findings")
131-
else:
132-
for f in findings:
133-
level = "error" if f.removable else "warning"
134-
msg = f"{f.category}: {f.name}"
135-
if f.detail:
136-
msg += f" ({f.detail[:120]})"
137-
console.print(f"::{level} file={f.file},line={f.line}::{msg}")
138-
console.print(f"\n::notice::deadcode: {len(findings)} findings")
139113
else:
140114
# Summary
141115
console.print(f"\n[bold]DeadCode Scan[/bold] — {result.files_scanned} files scanned\n")
@@ -182,7 +156,7 @@ def scan(
182156
# CI fail threshold
183157
effective_threshold = fail_threshold if fail_threshold is not None else _get_fail_threshold(ctx)
184158
if effective_threshold >= 0 and len(findings) >= effective_threshold:
185-
if effective_format not in ("json", "github"):
159+
if not json_output:
186160
console.print(f"\n[red]FAIL: {len(findings)} findings >= threshold {effective_threshold}[/red]")
187161
sys.exit(1)
188162

@@ -204,18 +178,13 @@ def remove(ctx: click.Context, dry_run: bool, category: str | None) -> None:
204178
project = ctx.obj["project"]
205179
ignore = _merge_config_ignore(ctx)
206180

207-
if not Path(project).exists():
208-
err_console.print(f"[red]Project directory '{project}' not found.[/red]")
209-
sys.exit(1)
210-
211181
if not dry_run:
212182
console.print("[red]WARNING: This will modify files. Use --dry-run first![/red]")
213183
console.print("[dim]Press Ctrl+C to abort. Running in 3 seconds...[/dim]")
214184
import time
215185
time.sleep(3)
216186

217-
include_patterns = ctx.obj.get("include")
218-
scanner = DeadCodeScanner(project, ignore_patterns=ignore, include_patterns=include_patterns)
187+
scanner = DeadCodeScanner(project, ignore_patterns=ignore)
219188
result = scanner.scan()
220189

221190
findings = result.findings
@@ -280,10 +249,11 @@ def remove(ctx: click.Context, dry_run: bool, category: str | None) -> None:
280249
@click.pass_context
281250
def stats(ctx: click.Context) -> None:
282251
"""Show quick stats about the project's dead code."""
252+
if require_license:
253+
require_license("deadcode")
283254
project = ctx.obj["project"]
284255
ignore = _merge_config_ignore(ctx)
285-
include_patterns = ctx.obj.get("include")
286-
scanner = DeadCodeScanner(project, ignore_patterns=ignore, include_patterns=include_patterns)
256+
scanner = DeadCodeScanner(project, ignore_patterns=ignore)
287257
result = scanner.scan()
288258

289259
console.print(f"Files scanned: [bold]{result.files_scanned}[/bold]")

0 commit comments

Comments
 (0)