@@ -108,7 +108,13 @@ export async function runOxlint(targets: string[], fix?: boolean): Promise<Viola
108108 } ) ;
109109}
110110
111- export async function runKnip ( ) : Promise < Violation [ ] > {
111+ /**
112+ * Knip dead-code issue kinds (unused exports/types/files) fire constantly
113+ * mid-implementation — an export is "unused" until its consumer exists.
114+ * They only carry signal as a commit-time gate, so they require `--strict`.
115+ * Dependency hygiene issues are stable and always reported.
116+ */
117+ export async function runKnip ( options ?: { strict ?: boolean } ) : Promise < Violation [ ] > {
112118 const args = [ '--no-progress' , '--reporter' , 'json' ] ;
113119 const result = await x ( local ( 'knip' ) , args , { throwOnError : false } ) ;
114120 if ( ! result . stdout . trim ( ) ) return [ ] ;
@@ -139,6 +145,7 @@ export async function runKnip(): Promise<Violation[]> {
139145 column : dep . col ,
140146 } ) ;
141147 }
148+ if ( ! options ?. strict ) continue ;
142149 for ( const exp of issue . exports ?? [ ] ) {
143150 violations . push ( {
144151 tool : 'knip' ,
@@ -178,11 +185,12 @@ export async function runKnip(): Promise<Violation[]> {
178185}
179186
180187async function runTypeScript ( targets : string [ ] ) : Promise < Violation [ ] > {
181- const args =
182- targets . length > 0
183- ? [ '--noEmit' , '--pretty' , 'false' , ...targets ]
184- : [ '--noEmit' , '--pretty' , 'false' ] ;
185- const result = await x ( local ( 'tsgo' ) , args , { throwOnError : false } ) ;
188+ // Always run in project mode: passing files on the command line makes
189+ // tsgo skip the tsconfig (TS5112). Explicit targets filter the report
190+ // after the fact instead.
191+ const result = await x ( local ( 'tsgo' ) , [ '--noEmit' , '--pretty' , 'false' ] , {
192+ throwOnError : false ,
193+ } ) ;
186194 const output = result . stdout + result . stderr ;
187195 if ( ! output . trim ( ) ) return [ ] ;
188196
@@ -200,41 +208,38 @@ async function runTypeScript(targets: string[]): Promise<Violation[]> {
200208 column : Number ( match [ 3 ] ) ,
201209 } ) ;
202210 }
203- return violations ;
211+ if ( targets . length === 0 ) return violations ;
212+ const prefixes = targets . map ( ( t ) => t . replace ( / ^ \. \/ / , '' ) ) ;
213+ return violations . filter ( ( v ) => v . file && prefixes . some ( ( p ) => v . file ! . startsWith ( p ) ) ) ;
204214}
205215
206216// -- Output --
207217
208- export function printViolations ( violations : Violation [ ] ) {
209- const grouped = new Map < string , Violation [ ] > ( ) ;
210- for ( const v of violations ) {
211- const key = v . file ?? '(project)' ;
212- if ( ! grouped . has ( key ) ) grouped . set ( key , [ ] ) ;
213- grouped . get ( key ) ! . push ( v ) ;
214- }
215-
216- const colors = {
217- error : '\x1b[31m' ,
218- warning : '\x1b[33m' ,
219- suggestion : '\x1b[34m' ,
220- dim : '\x1b[2m' ,
221- reset : '\x1b[0m' ,
222- } ;
218+ const colors = {
219+ error : '\x1b[31m' ,
220+ warning : '\x1b[33m' ,
221+ suggestion : '\x1b[34m' ,
222+ dim : '\x1b[2m' ,
223+ reset : '\x1b[0m' ,
224+ } ;
223225
224- for ( const [ file , items ] of grouped ) {
225- console . log ( `\n${ file } ` ) ;
226- for ( const v of items ) {
227- const loc = v . line != null ? ` ${ v . line } :${ v . column ?? 0 } ` : ' -' ;
228- const color = colors [ v . level ] ;
229- const tag = `${ v . tool } /${ v . code } ` ;
230- console . log (
231- `${ colors . dim } ${ loc . padEnd ( 10 ) } ${ colors . reset } ${ color } ${ v . level . padEnd ( 12 ) } ${ colors . reset } ${ v . message } ${ colors . dim } ${ tag } ${ colors . reset } ` ,
232- ) ;
233- }
234- }
226+ function printViolation ( v : Violation ) {
227+ const loc = v . line != null ? ` ${ v . line } :${ v . column ?? 0 } ` : ' -' ;
228+ const color = colors [ v . level ] ;
229+ const tag = `${ v . tool } /${ v . code } ` ;
230+ console . log (
231+ `${ colors . dim } ${ loc . padEnd ( 10 ) } ${ colors . reset } ${ color } ${ v . level . padEnd ( 12 ) } ${ colors . reset } ${ v . message } ${ colors . dim } ${ tag } ${ colors . reset } ` ,
232+ ) ;
233+ }
235234
235+ function countByLevel ( violations : Violation [ ] ) {
236236 const counts = { error : 0 , warning : 0 , suggestion : 0 } ;
237237 for ( const v of violations ) counts [ v . level ] ++ ;
238+ return counts ;
239+ }
240+
241+ function printSummary ( violations : Violation [ ] ) {
242+ const counts = countByLevel ( violations ) ;
238243 const parts = [ ] ;
239244 if ( counts . error )
240245 parts . push ( `${ colors . error } ${ counts . error } error${ counts . error > 1 ? 's' : '' } ${ colors . reset } ` ) ;
@@ -246,17 +251,69 @@ export function printViolations(violations: Violation[]) {
246251 parts . push (
247252 `${ colors . suggestion } ${ counts . suggestion } suggestion${ counts . suggestion > 1 ? 's' : '' } ${ colors . reset } ` ,
248253 ) ;
249- if ( parts . length > 0 ) {
250- console . log ( `\n${ parts . join ( ', ' ) } ` ) ;
251- } else {
252- console . log ( '\nNo issues found.' ) ;
254+ console . log ( parts . length > 0 ? `\n${ parts . join ( ', ' ) } ` : '\nNo issues found.' ) ;
255+ }
256+
257+ /**
258+ * Print violations grouped by file. Errors are always shown in full.
259+ * Warnings collapse to a per-rule count unless `warnings` is set — they
260+ * don't affect the exit code, so a wall of them buries actual failures.
261+ */
262+ export function printViolations ( violations : Violation [ ] , options ?: { warnings ?: boolean } ) {
263+ const showWarnings = options ?. warnings ?? false ;
264+ const visible = showWarnings ? violations : violations . filter ( ( v ) => v . level === 'error' ) ;
265+
266+ const grouped = new Map < string , Violation [ ] > ( ) ;
267+ for ( const v of visible ) {
268+ const key = v . file ?? '(project)' ;
269+ if ( ! grouped . has ( key ) ) grouped . set ( key , [ ] ) ;
270+ grouped . get ( key ) ! . push ( v ) ;
271+ }
272+
273+ for ( const [ file , items ] of grouped ) {
274+ console . log ( `\n${ file } ` ) ;
275+ for ( const v of items ) printViolation ( v ) ;
276+ }
277+
278+ if ( ! showWarnings ) {
279+ const hidden = violations . filter ( ( v ) => v . level !== 'error' ) ;
280+ if ( hidden . length > 0 ) {
281+ const byRule = new Map < string , number > ( ) ;
282+ for ( const v of hidden ) {
283+ const tag = `${ v . tool } /${ v . code } ` ;
284+ byRule . set ( tag , ( byRule . get ( tag ) ?? 0 ) + 1 ) ;
285+ }
286+ console . log (
287+ `\n${ colors . dim } ${ hidden . length } warning${ hidden . length > 1 ? 's' : '' } hidden (run with --warnings to show):${ colors . reset } ` ,
288+ ) ;
289+ for ( const [ tag , count ] of [ ...byRule ] . sort ( ( a , b ) => b [ 1 ] - a [ 1 ] ) ) {
290+ console . log ( `${ colors . dim } ${ count } × ${ tag } ${ colors . reset } ` ) ;
291+ }
292+ }
253293 }
294+
295+ printSummary ( violations ) ;
296+ }
297+
298+ /** Machine-readable report for agents and CI. */
299+ export function printJson ( violations : Violation [ ] ) {
300+ console . log ( JSON . stringify ( { summary : countByLevel ( violations ) , violations } , null , 2 ) ) ;
254301}
255302
256303// -- Main --
257304
258- async function collectViolations ( targets : string [ ] ) : Promise < Violation [ ] > {
259- const results = await Promise . allSettled ( [ runOxlint ( targets ) , runKnip ( ) , runTypeScript ( targets ) ] ) ;
305+ async function collectViolations (
306+ targets : string [ ] ,
307+ options ?: { strict ?: boolean } ,
308+ ) : Promise < Violation [ ] > {
309+ // oxlint honors targets (default: project-wide); tsgo runs in project
310+ // mode unless the user explicitly narrowed the target set.
311+ const explicit = targets . length > 0 ;
312+ const results = await Promise . allSettled ( [
313+ runOxlint ( explicit ? targets : [ '.' ] ) ,
314+ runKnip ( { strict : options ?. strict } ) ,
315+ runTypeScript ( explicit ? targets : [ ] ) ,
316+ ] ) ;
260317
261318 const violations : Violation [ ] = [ ] ;
262319 for ( const result of results ) {
@@ -271,26 +328,31 @@ async function collectViolations(targets: string[]): Promise<Violation[]> {
271328
272329export async function lint ( ctx : CommandContext ) {
273330 const args = parse ( ctx . args , {
274- boolean : [ 'fix' ] ,
331+ boolean : [ 'fix' , 'strict' , 'warnings' ] ,
332+ string : [ 'format' ] ,
275333 } ) ;
276- const targets = args . _ . length > 0 ? args . _ . map ( String ) : [ './src' ] ;
334+ const targets = args . _ . map ( String ) ;
335+ const json = args . format === 'json' ;
336+ const print = ( violations : Violation [ ] ) =>
337+ json ? printJson ( violations ) : printViolations ( violations , { warnings : args . warnings } ) ;
277338
278339 if ( args . fix ) {
279- await runOxlint ( targets , true ) ;
340+ await runOxlint ( targets . length > 0 ? targets : [ '.' ] , true ) ;
280341
281342 // Report remaining
282- const remaining = await collectViolations ( targets ) ;
343+ const remaining = await collectViolations ( targets , { strict : args . strict } ) ;
283344 if ( remaining . length > 0 ) {
284- printViolations ( remaining ) ;
285- process . exit ( 1 ) ;
345+ print ( remaining ) ;
346+ if ( remaining . some ( ( v ) => v . level === 'error' ) ) process . exit ( 1 ) ;
347+ return ;
286348 }
287- console . log ( 'No issues found.' ) ;
349+ if ( ! json ) console . log ( 'No issues found.' ) ;
288350 return ;
289351 }
290352
291353 // Default: report only
292- const violations = await collectViolations ( targets ) ;
293- printViolations ( violations ) ;
354+ const violations = await collectViolations ( targets , { strict : args . strict } ) ;
355+ print ( violations ) ;
294356 if ( violations . some ( ( v ) => v . level === 'error' ) ) {
295357 process . exit ( 1 ) ;
296358 }
0 commit comments