@@ -568,6 +568,63 @@ func TestReportFixResult_DryRunJSONOutput(t *testing.T) {
568568 assert .Equal (t , "f.md" , records [0 ]["path" ])
569569}
570570
571+ func TestReportFixResult_DryRunSARIFOutput (t * testing.T ) {
572+ opts := fixCLIOpts {dryRun : true , format : "sarif" }
573+ result := & fixpkg.Result {
574+ FilesChecked : 1 ,
575+ WouldFix : 1 ,
576+ WouldFixFiles : []fixpkg.WouldFixFile {
577+ {Path : "f.md" , Count : 1 , Rules : []fixpkg.RuleFixCount {{RuleID : "MDS001" , Count : 1 }}},
578+ },
579+ Diagnostics : []lint.Diagnostic {
580+ {
581+ File : "f.md" , Line : 5 , RuleID : "MDS002" , RuleName : "no-fix-rule" ,
582+ Severity : lint .Warning , Message : "unfixable" ,
583+ },
584+ },
585+ }
586+ var code int
587+ var stdout string
588+ stderr := captureStderr (func () {
589+ stdout = captureStdout (func () {
590+ code = reportFixResult (opts , result , & vlog.Logger {})
591+ })
592+ })
593+ assert .Equal (t , 1 , code , "unfixable diagnostics → exit 1" )
594+ assert .Empty (t , stdout , "SARIF must go to stderr, not stdout" )
595+
596+ var doc map [string ]any
597+ require .NoError (t , json .Unmarshal ([]byte (strings .TrimSpace (stderr )), & doc ),
598+ "dry-run SARIF output must be valid JSON" )
599+ assert .Equal (t , "2.1.0" , doc ["version" ])
600+ runs := doc ["runs" ].([]any )
601+ results := runs [0 ].(map [string ]any )["results" ].([]any )
602+ assert .Len (t , results , 1 , "only unfixable diagnostics appear in dry-run SARIF" )
603+ }
604+
605+ func TestReportFixResult_DryRunSARIFQuietSuppressesOutput (t * testing.T ) {
606+ opts := fixCLIOpts {dryRun : true , format : "sarif" , quiet : true }
607+ result := & fixpkg.Result {
608+ WouldFix : 1 ,
609+ WouldFixFiles : []fixpkg.WouldFixFile {
610+ {Path : "f.md" , Count : 1 },
611+ },
612+ Diagnostics : []lint.Diagnostic {
613+ {File : "f.md" , Line : 1 , RuleID : "MDS001" , RuleName : "r" , Severity : lint .Error , Message : "m" },
614+ },
615+ }
616+ var code int
617+ var stdout string
618+ stderr := captureStderr (func () {
619+ stdout = captureStdout (func () {
620+ code = reportFixResult (opts , result , & vlog.Logger {})
621+ })
622+ })
623+ assert .Equal (t , 1 , code )
624+ assert .Empty (t , stdout )
625+ assert .NotContains (t , stderr , "{" , "--quiet must suppress dry-run SARIF on stderr too" )
626+ }
627+
571628func TestReportFixResult_DryRunJSONQuietSuppressesOutput (t * testing.T ) {
572629 opts := fixCLIOpts {dryRun : true , format : "json" , quiet : true }
573630 result := & fixpkg.Result {
@@ -588,6 +645,51 @@ func TestReportFixResult_DryRunJSONQuietSuppressesOutput(t *testing.T) {
588645 assert .NotContains (t , stderr , "{" , "--quiet must suppress dry-run JSON on stderr too" )
589646}
590647
648+ func TestReportCheckResult_SARIFEmittedWhenNoDiagnostics (t * testing.T ) {
649+ // SARIF must always be emitted so github/codeql-action/upload-sarif
650+ // receives a valid document (not an empty file) on a clean codebase.
651+ opts := checkCLIOpts {format : "sarif" }
652+ result := & engine.Result {FilesChecked : 3 }
653+ var code int
654+ var stdout string
655+ stderr := captureStderr (func () {
656+ stdout = captureStdout (func () {
657+ code = reportCheckResult (result , opts , & vlog.Logger {})
658+ })
659+ })
660+ assert .Equal (t , 0 , code )
661+ assert .Empty (t , stdout , "SARIF must go to stderr" )
662+
663+ var doc map [string ]any
664+ require .NoError (t , json .Unmarshal ([]byte (strings .TrimSpace (stderr )), & doc ),
665+ "must emit valid SARIF even with zero diagnostics" )
666+ assert .Equal (t , "2.1.0" , doc ["version" ])
667+ runs := doc ["runs" ].([]any )
668+ results := runs [0 ].(map [string ]any )["results" ].([]any )
669+ assert .Empty (t , results , "zero diagnostics → empty results array" )
670+ }
671+
672+ func TestReportFixResult_SARIFEmittedWhenNoDiagnostics (t * testing.T ) {
673+ // Same invariant as check: fix -f sarif must produce a valid SARIF
674+ // document even when fixing resolved all issues (Diagnostics empty).
675+ opts := fixCLIOpts {format : "sarif" }
676+ result := & fixpkg.Result {FilesChecked : 2 , Modified : []string {"f.md" }}
677+ var code int
678+ var stdout string
679+ stderr := captureStderr (func () {
680+ stdout = captureStdout (func () {
681+ code = reportFixResult (opts , result , & vlog.Logger {})
682+ })
683+ })
684+ assert .Equal (t , 0 , code )
685+ assert .Empty (t , stdout )
686+
687+ var doc map [string ]any
688+ require .NoError (t , json .Unmarshal ([]byte (strings .TrimSpace (stderr )), & doc ),
689+ "must emit valid SARIF even after all issues are fixed" )
690+ assert .Equal (t , "2.1.0" , doc ["version" ])
691+ }
692+
591693func TestReportFixResult_DiagnosticsReturnsCode1 (t * testing.T ) {
592694 opts := fixCLIOpts {format : "text" }
593695 result := & fixpkg.Result {
@@ -2080,6 +2182,16 @@ func TestReportFixResultTo_DryRunJSONWriteErrorFlushes(t *testing.T) {
20802182 assert .Equal (t , 2 , code )
20812183}
20822184
2185+ func TestReportFixResultTo_DryRunSARIFWriteErrorReturns2 (t * testing.T ) {
2186+ // Drive lines 369-370 in fix.go: enough diagnostics to overflow the
2187+ // 64 KiB buffer during SARIF JSON encoding so formatDiagnosticsTo
2188+ // returns non-zero and the early-return branch is taken.
2189+ opts := fixCLIOpts {dryRun : true , format : "sarif" }
2190+ result := & fixpkg.Result {FilesChecked : 1 , Diagnostics : manyDiagnostics (2000 )}
2191+ code := reportFixResultTo (opts , result , & vlog.Logger {}, & alwaysErrorWriter {})
2192+ assert .Equal (t , 2 , code )
2193+ }
2194+
20832195func TestReportCheckResultTo_LargeDiagWriteErrorReturns2 (t * testing.T ) {
20842196 // Enough diagnostics to overflow the 64 KiB stderr buffer, so the
20852197 // formatter itself observes the write failure mid-stream and the
0 commit comments