22
33from __future__ import annotations
44
5+ import click
56import json
67import sys
78from pathlib import Path
8-
9- import click
109from rich .console import Console
1110from 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+
1320from . import __version__
1421from .config import DeadCodeConfig
1522from .scanner import DeadCodeScanner , Finding
1623
1724console = Console ()
1825err_console = Console (stderr = True )
1926
20- FORMAT_HELP = "Output format: pretty (default), compact, github, or json"
2127ALL_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
281250def 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