1818err_console = Console (stderr = True )
1919
2020FORMAT_HELP = "Output format: pretty (default), compact, github, or json"
21- ALL_CATEGORIES = ["unused_export" , "dead_route" , "orphaned_css" , "unreferenced_component" ]
21+ ALL_CATEGORIES = [
22+ "unused_export" ,
23+ "dead_route" ,
24+ "orphaned_css" ,
25+ "unreferenced_component" ,
26+ ]
2227FORMAT_CHOICES = click .Choice (["pretty" , "compact" , "github" , "json" ])
2328
2429
2530@click .group ()
2631@click .option ("--project" , "-p" , default = "." , help = "Project directory to scan" )
27- @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)" )
32+ @click .option (
33+ "--ignore" , "-i" , multiple = True , help = "Additional ignore patterns (gitignore-style)"
34+ )
35+ @click .option (
36+ "--include" ,
37+ multiple = True ,
38+ help = "Include only matching files (gitignore-style whitelist)" ,
39+ )
2940@click .version_option (__version__ , prog_name = "deadcode" )
3041@click .pass_context
31- def cli (ctx : click .Context , project : str , ignore : tuple [str , ...], include : tuple [str , ...]) -> None :
42+ def cli (
43+ ctx : click .Context , project : str , ignore : tuple [str , ...], include : tuple [str , ...]
44+ ) -> None :
3245 """DeadCode — Find and remove dead code in TS/React/Next.js projects.
3346
3447 Scans for unused exports, dead routes, orphaned CSS classes,
@@ -67,11 +80,24 @@ def _get_fail_threshold(ctx: click.Context) -> int:
6780
6881
6982@cli .command ()
70- @click .option ("--json-output" , "-j" , is_flag = True , help = "Alias for --format=json (deprecated)" )
83+ @click .option (
84+ "--json-output" , "-j" , is_flag = True , help = "Alias for --format=json (deprecated)"
85+ )
7186@click .option ("--format" , type = FORMAT_CHOICES , default = "pretty" , help = FORMAT_HELP )
72- @click .option ("--category" , "-c" , type = click .Choice (ALL_CATEGORIES ), default = None , help = "Filter by category" )
73- @click .option ("--fail" , "fail_threshold" , type = int , default = None ,
74- help = "Exit code 1 if findings >= threshold (overrides .deadcode.yml)" )
87+ @click .option (
88+ "--category" ,
89+ "-c" ,
90+ type = click .Choice (ALL_CATEGORIES ),
91+ default = None ,
92+ help = "Filter by category" ,
93+ )
94+ @click .option (
95+ "--fail" ,
96+ "fail_threshold" ,
97+ type = int ,
98+ default = None ,
99+ help = "Exit code 1 if findings >= threshold (overrides .deadcode.yml)" ,
100+ )
75101@click .pass_context
76102def scan (
77103 ctx : click .Context ,
@@ -89,7 +115,9 @@ def scan(
89115 sys .exit (1 )
90116
91117 include_patterns = ctx .obj .get ("include" )
92- scanner = DeadCodeScanner (project , ignore_patterns = ignore , include_patterns = include_patterns )
118+ scanner = DeadCodeScanner (
119+ project , ignore_patterns = ignore , include_patterns = include_patterns
120+ )
93121 result = scanner .scan ()
94122
95123 # Filter by category
@@ -109,8 +137,14 @@ def scan(
109137 output = {
110138 "files_scanned" : result .files_scanned ,
111139 "findings" : [
112- {"file" : f .file , "line" : f .line , "name" : f .name ,
113- "category" : f .category , "detail" : f .detail , "removable" : f .removable }
140+ {
141+ "file" : f .file ,
142+ "line" : f .line ,
143+ "name" : f .name ,
144+ "category" : f .category ,
145+ "detail" : f .detail ,
146+ "removable" : f .removable ,
147+ }
114148 for f in findings
115149 ],
116150 "errors" : result .errors ,
@@ -138,7 +172,9 @@ def scan(
138172 console .print (f"\n ::notice::deadcode: { len (findings )} findings" )
139173 else :
140174 # Summary
141- console .print (f"\n [bold]DeadCode Scan[/bold] — { result .files_scanned } files scanned\n " )
175+ console .print (
176+ f"\n [bold]DeadCode Scan[/bold] — { result .files_scanned } files scanned\n "
177+ )
142178
143179 if not findings :
144180 console .print ("[green]✓ No dead code found![/green]" )
@@ -157,7 +193,9 @@ def scan(
157193
158194 for cat , cat_findings in by_category .items ():
159195 label = category_labels .get (cat , cat )
160- console .print (f"\n [bold yellow]{ label } [/bold yellow] ({ len (cat_findings )} )" )
196+ console .print (
197+ f"\n [bold yellow]{ label } [/bold yellow] ({ len (cat_findings )} )"
198+ )
161199
162200 table = Table (show_header = True )
163201 table .add_column ("File" , style = "cyan" )
@@ -174,26 +212,43 @@ def scan(
174212
175213 # Total
176214 removable = sum (1 for f in findings if f .removable )
177- console .print (f"\n [bold]Total:[/bold] { len (findings )} findings ({ removable } removable)" )
215+ console .print (
216+ f"\n [bold]Total:[/bold] { len (findings )} findings ({ removable } removable)"
217+ )
178218
179219 if result .errors :
180- console .print (f"\n [yellow]{ len (result .errors )} scan errors (use --json-output to see)[/yellow]" )
220+ console .print (
221+ f"\n [yellow]{ len (result .errors )} scan errors (use --json-output to see)[/yellow]"
222+ )
181223
182224 # CI fail threshold
183- effective_threshold = fail_threshold if fail_threshold is not None else _get_fail_threshold (ctx )
225+ effective_threshold = (
226+ fail_threshold if fail_threshold is not None else _get_fail_threshold (ctx )
227+ )
184228 if effective_threshold >= 0 and len (findings ) >= effective_threshold :
185229 if effective_format not in ("json" , "github" ):
186- console .print (f"\n [red]FAIL: { len (findings )} findings >= threshold { effective_threshold } [/red]" )
230+ console .print (
231+ f"\n [red]FAIL: { len (findings )} findings >= threshold { effective_threshold } [/red]"
232+ )
187233 sys .exit (1 )
188234
189235
190236# ── remove ────────────────────────────────────────────────────────────
191237
192238
193239@cli .command ()
194- @click .option ("--dry-run" , is_flag = True , help = "Preview what would be removed without making changes" )
195- @click .option ("--category" , "-c" , type = click .Choice (ALL_CATEGORIES ),
196- default = None , help = "Only remove findings in this category" )
240+ @click .option (
241+ "--dry-run" ,
242+ is_flag = True ,
243+ help = "Preview what would be removed without making changes" ,
244+ )
245+ @click .option (
246+ "--category" ,
247+ "-c" ,
248+ type = click .Choice (ALL_CATEGORIES ),
249+ default = None ,
250+ help = "Only remove findings in this category" ,
251+ )
197252@click .pass_context
198253def remove (ctx : click .Context , dry_run : bool , category : str | None ) -> None :
199254 """Remove dead code (with --dry-run for preview).
@@ -209,13 +264,18 @@ def remove(ctx: click.Context, dry_run: bool, category: str | None) -> None:
209264 sys .exit (1 )
210265
211266 if not dry_run :
212- console .print ("[red]WARNING: This will modify files. Use --dry-run first![/red]" )
267+ console .print (
268+ "[red]WARNING: This will modify files. Use --dry-run first![/red]"
269+ )
213270 console .print ("[dim]Press Ctrl+C to abort. Running in 3 seconds...[/dim]" )
214271 import time
272+
215273 time .sleep (3 )
216274
217275 include_patterns = ctx .obj .get ("include" )
218- scanner = DeadCodeScanner (project , ignore_patterns = ignore , include_patterns = include_patterns )
276+ scanner = DeadCodeScanner (
277+ project , ignore_patterns = ignore , include_patterns = include_patterns
278+ )
219279 result = scanner .scan ()
220280
221281 findings = result .findings
@@ -248,7 +308,9 @@ def remove(ctx: click.Context, dry_run: bool, category: str | None) -> None:
248308 continue
249309
250310 try :
251- lines = filepath .read_text (encoding = "utf-8" , errors = "replace" ).splitlines (keepends = True )
311+ lines = filepath .read_text (encoding = "utf-8" , errors = "replace" ).splitlines (
312+ keepends = True
313+ )
252314 except Exception as e :
253315 console .print (f"[red]Error reading { rel_file } : { e } [/red]" )
254316 continue
@@ -259,15 +321,19 @@ def remove(ctx: click.Context, dry_run: bool, category: str | None) -> None:
259321 if dry_run :
260322 for line_num in sorted (lines_to_remove ):
261323 content = lines [line_num - 1 ].rstrip () if line_num <= len (lines ) else ""
262- console .print (f"[yellow]WOULD REMOVE[/yellow] { rel_file } :{ line_num } — { content .strip ()[:80 ]} " )
324+ console .print (
325+ f"[yellow]WOULD REMOVE[/yellow] { rel_file } :{ line_num } — { content .strip ()[:80 ]} "
326+ )
263327 removed_count += len (lines_to_remove )
264328 else :
265329 for line_num in lines_to_remove :
266330 if 0 < line_num <= len (lines ):
267331 lines [line_num - 1 ] = "" # Blank the line (safer than deleting)
268332 filepath .write_text ("" .join (lines ), encoding = "utf-8" )
269333 removed_count += len (lines_to_remove )
270- console .print (f"[green]✓[/green] Cleaned { rel_file } ({ len (lines_to_remove )} lines)" )
334+ console .print (
335+ f"[green]✓[/green] Cleaned { rel_file } ({ len (lines_to_remove )} lines)"
336+ )
271337
272338 action = "Would remove" if dry_run else "Removed"
273339 console .print (f"\n [bold]{ action } : { removed_count } dead code entries[/bold]" )
@@ -283,14 +349,22 @@ def stats(ctx: click.Context) -> None:
283349 project = ctx .obj ["project" ]
284350 ignore = _merge_config_ignore (ctx )
285351 include_patterns = ctx .obj .get ("include" )
286- scanner = DeadCodeScanner (project , ignore_patterns = ignore , include_patterns = include_patterns )
352+ scanner = DeadCodeScanner (
353+ project , ignore_patterns = ignore , include_patterns = include_patterns
354+ )
287355 result = scanner .scan ()
288356
289357 console .print (f"Files scanned: [bold]{ result .files_scanned } [/bold]" )
290- console .print (f"Unused exports: [bold yellow]{ len (result .unused_exports )} [/bold yellow]" )
358+ console .print (
359+ f"Unused exports: [bold yellow]{ len (result .unused_exports )} [/bold yellow]"
360+ )
291361 console .print (f"Dead routes: [bold red]{ len (result .dead_routes )} [/bold red]" )
292- console .print (f"Orphaned CSS: [bold magenta]{ len (result .orphaned_css )} [/bold magenta]" )
293- console .print (f"Unreferenced components: [bold cyan]{ len (result .unreferenced_components )} [/bold cyan]" )
362+ console .print (
363+ f"Orphaned CSS: [bold magenta]{ len (result .orphaned_css )} [/bold magenta]"
364+ )
365+ console .print (
366+ f"Unreferenced components: [bold cyan]{ len (result .unreferenced_components )} [/bold cyan]"
367+ )
294368 console .print (f"Total findings: [bold]{ len (result .findings )} [/bold]" )
295369
296370 if result .errors :
0 commit comments