22
33from __future__ import annotations
44
5- import click
65import json
76import sys
87from pathlib import Path
8+
9+ import click
910from rich .console import Console
1011from rich .table import Table
1112
1617console = Console ()
1718err_console = Console (stderr = True )
1819
20+ FORMAT_HELP = "Output format: pretty (default), compact, github, or json"
1921ALL_CATEGORIES = ["unused_export" , "dead_route" , "orphaned_css" , "unreferenced_component" ]
22+ FORMAT_CHOICES = click .Choice (["pretty" , "compact" , "github" , "json" ])
2023
2124
2225@click .group ()
@@ -64,12 +67,13 @@ def _get_fail_threshold(ctx: click.Context) -> int:
6467
6568
6669@cli .command ()
67- @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 )
6872@click .option ("--category" , "-c" , type = click .Choice (ALL_CATEGORIES ), default = None , help = "Filter by category" )
6973@click .option ("--fail" , "fail_threshold" , type = int , default = None ,
7074 help = "Exit code 1 if findings >= threshold (overrides .deadcode.yml)" )
7175@click .pass_context
72- def scan (ctx : click .Context , json_output : bool , category : str | None , fail_threshold : int | None ) -> None :
76+ def scan (ctx : click .Context , json_output : bool , format : str | None , category : str | None , fail_threshold : int | None ) -> None :
7377 """Scan project for dead code."""
7478 project = ctx .obj ["project" ]
7579 ignore = _merge_config_ignore (ctx )
@@ -92,7 +96,10 @@ def scan(ctx: click.Context, json_output: bool, category: str | None, fail_thres
9296 if not category and config and config .categories :
9397 findings = [f for f in findings if f .category in config .categories ]
9498
95- if json_output :
99+ # Determine effective format (legacy --json-output maps to json)
100+ effective_format = "json" if json_output else (format or "pretty" )
101+
102+ if effective_format == "json" :
96103 output = {
97104 "files_scanned" : result .files_scanned ,
98105 "findings" : [
@@ -103,6 +110,26 @@ def scan(ctx: click.Context, json_output: bool, category: str | None, fail_thres
103110 "errors" : result .errors ,
104111 }
105112 console .print (json .dumps (output , indent = 2 , default = str ))
113+ elif effective_format == "compact" :
114+ if not findings :
115+ console .print ("OK — 0 findings" )
116+ else :
117+ for f in findings :
118+ console .print (f"{ f .file } :{ f .line } \u2014 { f .category } : { f .name } " )
119+ console .print (f"\n { len (findings )} findings" )
120+ elif effective_format == "github" :
121+ # GitHub Actions annotation syntax
122+ # ::warning file={name},line={line},endLine={line}::{message}
123+ if not findings :
124+ console .print ("deadcode: 0 findings" )
125+ else :
126+ for f in findings :
127+ level = "error" if f .removable else "warning"
128+ msg = f"{ f .category } : { f .name } "
129+ if f .detail :
130+ msg += f" ({ f .detail [:120 ]} )"
131+ console .print (f"::{ level } file={ f .file } ,line={ f .line } ::{ msg } " )
132+ console .print (f"\n ::notice::deadcode: { len (findings )} findings" )
106133 else :
107134 # Summary
108135 console .print (f"\n [bold]DeadCode Scan[/bold] — { result .files_scanned } files scanned\n " )
@@ -149,7 +176,7 @@ def scan(ctx: click.Context, json_output: bool, category: str | None, fail_thres
149176 # CI fail threshold
150177 effective_threshold = fail_threshold if fail_threshold is not None else _get_fail_threshold (ctx )
151178 if effective_threshold >= 0 and len (findings ) >= effective_threshold :
152- if not json_output :
179+ if effective_format not in ( "json" , "github" ) :
153180 console .print (f"\n [red]FAIL: { len (findings )} findings >= threshold { effective_threshold } [/red]" )
154181 sys .exit (1 )
155182
0 commit comments