Skip to content

Commit c46eabc

Browse files
author
DevForge Engineer
committed
cowork-bot: resolve merge conflict in tests/test_scanner.py (keep both TestMultiLineExportList and master test suites)
2 parents d3317e5 + 0ba9ecc commit c46eabc

12 files changed

Lines changed: 852 additions & 27 deletions

File tree

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# Automated Code Review — caller workflow
2+
#
3+
# Drop this file into any Coding-Dev-Tools repo at
4+
# .github/workflows/auto-code-review.yml to enable
5+
# automated PR code review (lint, format, secret detection,
6+
# TODO/FIXME check, large file check, and PR comment summary).
7+
#
8+
# The reusable workflow is defined in the org .github repo:
9+
# Coding-Dev-Tools/.github/.github/workflows/auto-code-review.yml@main
10+
11+
name: Auto Code Review
12+
13+
on:
14+
pull_request:
15+
branches: [main, master]
16+
types: [opened, synchronize, reopened]
17+
push:
18+
branches: [main, master]
19+
workflow_dispatch:
20+
21+
permissions:
22+
contents: read
23+
pull-requests: write
24+
security-events: write
25+
26+
jobs:
27+
code-review:
28+
uses: Coding-Dev-Tools/.github/.github/workflows/auto-code-review.yml@main

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,3 +71,7 @@ Thumbs.db
7171
research/
7272
fixtures/generated/
7373
.ruff_cache/
74+
75+
# Operational state (not for commit)
76+
LEARNING/
77+
_cowork_ops/

README.md

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -81,12 +81,23 @@ deadcode stats
8181
- **Monorepo support** — handles large projects efficiently with ignore patterns
8282
- **CI integration** — JSON output for automated pipelines and gating
8383

84-
## Ignore Patterns
84+
## Ignore / Include Patterns
8585

8686
```bash
87+
# Ignore specific paths
8788
deadcode scan -i "generated/" -i "**/*.generated.ts"
89+
90+
# Include only matching paths (whitelist)
91+
deadcode scan --include "src/" # Only scan src/
92+
deadcode scan --include "src/" --include "lib/" # Multiple dirs
93+
deadcode remove --dry-run --include "packages/" # Works with remove
94+
deadcode stats --include "app/" # Works with stats
8895
```
8996

97+
`--include` accepts gitignore-style patterns and is repeatable (specify multiple targets).
98+
When both `--include` and `-i`/`--ignore` are used, `--include` is applied first, then
99+
`--ignore` excludes any matches within the included paths.
100+
90101
Default ignores: `node_modules/`, `.git/`, `.next/`, `dist/`, `build/`, `public/`, `static/`
91102

92103
## Pricing
@@ -122,7 +133,7 @@ DeadCode is one of 11 tools in the Revenue Holdings suite. One license covers al
122133
---
123134

124135
<p align="center">
125-
<sub>Part of <a href="https://coding-dev-tools.github.io/revenueholdings.dev/">Revenue Holdings</a> — CLI tools built by autonomous AI.</sub>
136+
<sub>Part of <a href="https://coding-dev-tools.github.io/devforge/">Revenue Holdings</a> — CLI tools built by autonomous AI.</sub>
126137
</p>
127138

128139
## CI/CD Integration
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
date: 2026-06-10
2+
project: deadcode-cli
3+
runner: pytest on Python 3.12
4+
findings:
5+
- 2026-06-10T__:fast smoke `py -3.12 -m pytest --no-header -q -q --maxfail=1` passed.
6+
- full suite: pending

pyproject.toml

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,9 +46,15 @@ Changelog = "https://github.com/Coding-Dev-Tools/deadcode/releases"
4646
[project.scripts]
4747
deadcode = "deadcode.cli:cli"
4848

49+
[tool.setuptools]
50+
include-package-data = true
51+
4952
[tool.setuptools.packages.find]
5053
where = ["src"]
5154

55+
[tool.setuptools.package-data]
56+
deadcode = ["py.typed"]
57+
5258
[tool.pytest.ini_options]
5359
testpaths = ["tests"]
5460
python_files = ["test_*.py"]
@@ -62,4 +68,4 @@ select = ["E", "F", "W", "I", "UP", "B", "SIM"]
6268
ignore = ["E501"]
6369

6470
[tool.ruff.lint.isort]
65-
known-first-party = ["*"]
71+
known-first-party = ["deadcode"]

src/deadcode/__main__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
"""Allow running deadcode as: python -m deadcode"""
2-
from deadcode.cli import cli
2+
from .cli import cli
33

44
if __name__ == "__main__":
55
cli()

src/deadcode/cli.py

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

33
from __future__ import annotations
44

5-
import click
65
import json
76
import sys
87
from pathlib import Path
8+
9+
import click
910
from rich.console import Console
1011
from rich.table import Table
1112

12-
try:
13-
from revenueholdings_license import require_license
14-
except ImportError:
15-
require_license = None
16-
1713
from . import __version__
1814
from .config import DeadCodeConfig
1915
from .scanner import DeadCodeScanner, Finding
2016

2117
console = Console()
2218
err_console = Console(stderr=True)
2319

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

2624

2725
@click.group()
2826
@click.option("--project", "-p", default=".", help="Project directory to scan")
2927
@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)")
3029
@click.version_option(__version__, prog_name="deadcode")
3130
@click.pass_context
32-
def cli(ctx: click.Context, project: str, ignore: tuple[str, ...]) -> None:
31+
def cli(ctx: click.Context, project: str, ignore: tuple[str, ...], include: tuple[str, ...]) -> None:
3332
"""DeadCode — Find and remove dead code in TS/React/Next.js projects.
3433
3534
Scans for unused exports, dead routes, orphaned CSS classes,
@@ -38,6 +37,7 @@ def cli(ctx: click.Context, project: str, ignore: tuple[str, ...]) -> None:
3837
ctx.ensure_object(dict)
3938
ctx.obj["project"] = project
4039
ctx.obj["ignore"] = list(ignore) if ignore else None
40+
ctx.obj["include"] = list(include) if include else None
4141
# Load .deadcode.yml config
4242
ctx.obj["config"] = DeadCodeConfig.load(project)
4343

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

6868

6969
@cli.command()
70-
@click.option("--json-output", "-j", is_flag=True, help="Output as JSON")
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)
7172
@click.option("--category", "-c", type=click.Choice(ALL_CATEGORIES), default=None, help="Filter by category")
7273
@click.option("--fail", "fail_threshold", type=int, default=None,
7374
help="Exit code 1 if findings >= threshold (overrides .deadcode.yml)")
7475
@click.pass_context
75-
def scan(ctx: click.Context, json_output: bool, category: str | None, fail_threshold: int | None) -> None:
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:
7683
"""Scan project for dead code."""
77-
if require_license:
78-
require_license("deadcode")
7984
project = ctx.obj["project"]
8085
ignore = _merge_config_ignore(ctx)
8186

8287
if not Path(project).exists():
8388
err_console.print(f"[red]Project directory '{project}' not found.[/red]")
8489
sys.exit(1)
8590

86-
scanner = DeadCodeScanner(project, ignore_patterns=ignore)
91+
include_patterns = ctx.obj.get("include")
92+
scanner = DeadCodeScanner(project, ignore_patterns=ignore, include_patterns=include_patterns)
8793
result = scanner.scan()
8894

8995
# Filter by category
@@ -96,7 +102,10 @@ def scan(ctx: click.Context, json_output: bool, category: str | None, fail_thres
96102
if not category and config and config.categories:
97103
findings = [f for f in findings if f.category in config.categories]
98104

99-
if json_output:
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":
100109
output = {
101110
"files_scanned": result.files_scanned,
102111
"findings": [
@@ -107,6 +116,26 @@ def scan(ctx: click.Context, json_output: bool, category: str | None, fail_thres
107116
"errors": result.errors,
108117
}
109118
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")
110139
else:
111140
# Summary
112141
console.print(f"\n[bold]DeadCode Scan[/bold] — {result.files_scanned} files scanned\n")
@@ -153,7 +182,7 @@ def scan(ctx: click.Context, json_output: bool, category: str | None, fail_thres
153182
# CI fail threshold
154183
effective_threshold = fail_threshold if fail_threshold is not None else _get_fail_threshold(ctx)
155184
if effective_threshold >= 0 and len(findings) >= effective_threshold:
156-
if not json_output:
185+
if effective_format not in ("json", "github"):
157186
console.print(f"\n[red]FAIL: {len(findings)} findings >= threshold {effective_threshold}[/red]")
158187
sys.exit(1)
159188

@@ -175,13 +204,18 @@ def remove(ctx: click.Context, dry_run: bool, category: str | None) -> None:
175204
project = ctx.obj["project"]
176205
ignore = _merge_config_ignore(ctx)
177206

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

184-
scanner = DeadCodeScanner(project, ignore_patterns=ignore)
217+
include_patterns = ctx.obj.get("include")
218+
scanner = DeadCodeScanner(project, ignore_patterns=ignore, include_patterns=include_patterns)
185219
result = scanner.scan()
186220

187221
findings = result.findings
@@ -246,11 +280,10 @@ def remove(ctx: click.Context, dry_run: bool, category: str | None) -> None:
246280
@click.pass_context
247281
def stats(ctx: click.Context) -> None:
248282
"""Show quick stats about the project's dead code."""
249-
if require_license:
250-
require_license("deadcode")
251283
project = ctx.obj["project"]
252284
ignore = _merge_config_ignore(ctx)
253-
scanner = DeadCodeScanner(project, ignore_patterns=ignore)
285+
include_patterns = ctx.obj.get("include")
286+
scanner = DeadCodeScanner(project, ignore_patterns=ignore, include_patterns=include_patterns)
254287
result = scanner.scan()
255288

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

src/deadcode/py.typed

Whitespace-only changes.

src/deadcode/scanner.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,12 @@
1010
from __future__ import annotations
1111

1212
import os
13-
import pathspec
1413
import re
1514
from dataclasses import dataclass, field
1615
from pathlib import Path
1716

17+
import pathspec
18+
1819
# ── Data structures ───────────────────────────────────────────────────
1920

2021

0 commit comments

Comments
 (0)