Repository: reviewdog/reviewdog
Pull request #1807 (2024-06-30 17:15:33)

Support -reporter=sarif
- [x] Updated Unreleased section in [CHANGELOG](https://github.com/reviewdog/reviewdog/blob/master/CHANGELOG.md) or it's not notable changes.



github-advanced-security[bot]: This pull request sets up GitHub code scanning for this repository. Once the scans have completed and the checks have passed, the analysis results for this pull request branch will appear on [this overview](/reviewdog/reviewdog/security/code-scanning?query=pr%3A1807+is%3Aopen). Once you merge this pull request, the 'Security' tab will show more code scanning analysis results (for example, for the default branch). Depending on your configuration and choice of analysis tool, future pull requests will be annotated with code scanning analysis results. For more information about GitHub code scanning, check out [the documentation](https://docs.github.com/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/about-code-scanning). 

Patch:
diff --git a/.github/workflows/reviewdog.yml b/.github/workflows/reviewdog.yml
index a20265cc..0c549f90 100644
--- a/.github/workflows/reviewdog.yml
+++ b/.github/workflows/reviewdog.yml
@@ -233,6 +233,34 @@ jobs:
           cat ./_testdata/custom_rdjson.json | \
             reviewdog -name="custom-rdjson" -f=rdjson -reporter=github-pr-annotations
 
+  reviewdog-sarif:
+    permissions:
+      contents: read
+    name: reviewdog (sarif)
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v4
+      - uses: actions/setup-go@v5
+        with:
+          go-version-file: "go.mod"
+      - name: Setup reviewdog
+        # uses: reviewdog/action-setup@v1
+        run: |
+          go install ./cmd/reviewdog
+      - name: Custom rdjson test
+        run: |
+          mkdir ../results
+          cat ./_testdata/custom_rdjson.json |
+            reviewdog -name="custom-rdjson" -f=rdjson -reporter=sarif |
+            tee ../results/custom-rdjson.sarif
+      - name: Install linters
+        run: go install golang.org/x/lint/golint@latest
+      - name: Run golint
+        run: |
+          golint ./... | reviewdog -f=golint -reporter=sarif |
+            tee ../results/golint.sarif
+      - uses: github/codeql-action/upload-sarif@v3
+
   typos:
     name: runner / typos
     runs-on: ubuntu-latest
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 010a9046..e0adc1f8 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -13,7 +13,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
   outdated comments in github-pr-review reporter. Note that it won't delete
   comments if there is a reply considering there can be a meaningful
   discussion.
-- [#1806](https://github.com/reviewdog/reviewdog/pull/1806) Add -reporter=rdjson/rdjsonl which outputs rdjson/rdjsonl format to stdout. It also changes the default behavior of -diff and -filter-mode for local reporters. If -filter-mode is not provided (-filter-mode=default) and -diff flag is not provided, reviewdog automatically set -filter-mode=nofilter.
+- [#1806](https://github.com/reviewdog/reviewdog/pull/1806) Add
+  -reporter=rdjson/rdjsonl which outputs rdjson/rdjsonl format to stdout. It
+  also changes the default behavior of -diff and -filter-mode for local
+  reporters. If -filter-mode is not provided (-filter-mode=default) and -diff
+  flag is not provided, reviewdog automatically set -filter-mode=nofilter.
+- [#1807](https://github.com/reviewdog/reviewdog/pull/1807) Add -reporter=sarif
+  which outputs SARIF format to stdout. You can upload the output SARIF to
+  GitHub and see code scanning alerts.
+
 
 ### :bug: Fixes
 - ...
diff --git a/cmd/reviewdog/main.go b/cmd/reviewdog/main.go
index c3509f11..d5c2a5a8 100644
--- a/cmd/reviewdog/main.go
+++ b/cmd/reviewdog/main.go
@@ -100,6 +100,9 @@ const (
 	"rdjsonl"
 		Report results to stdout in rdjsonl format.
 
+	"sarif"
+		Report results to stdout in SARIF format.
+
 	"github-check"
 		Report results to GitHub Check. It works both for Pull Requests and commits.
 		For Pull Request, you can see report results in GitHub PullRequest Check
@@ -452,6 +455,13 @@ func run(r io.Reader, w io.Writer, opt *option) error {
 		}
 		ds = d
 		cs = reviewdog.NewRDJSONLCommentWriter(w)
+	case "sarif":
+		d, err := localDiffService(opt)
+		if err != nil {
+			return err
+		}
+		ds = d
+		cs = reviewdog.NewSARIFCommentWriter(w, toolName(opt))
 	}
 
 	if isProject {
diff --git a/comment_iowriter.go b/comment_iowriter.go
index 9e043068..e69330cf 100644
--- a/comment_iowriter.go
+++ b/comment_iowriter.go
@@ -2,9 +2,11 @@ package reviewdog
 
 import (
 	"context"
+	"encoding/json"
 	"fmt"
 	"io"
 
+	"github.com/haya14busa/go-sarif/sarif"
 	"github.com/reviewdog/reviewdog/proto/rdf"
 	"google.golang.org/protobuf/encoding/protojson"
 )
@@ -142,3 +144,138 @@ func (cw *RDJSONCommentWriter) Flush(_ context.Context) error {
 	}
 	return nil
 }
+
+var _ CommentService = &SARIFCommentWriter{}
+
+// SARIFCommentWriter
+type SARIFCommentWriter struct {
+	w        io.Writer
+	comments []*Comment
+	toolName string
+}
+
+func NewSARIFCommentWriter(w io.Writer, toolName string) *SARIFCommentWriter {
+	return &SARIFCommentWriter{w: w, toolName: toolName}
+}
+
+func (cw *SARIFCommentWriter) Post(_ context.Context, c *Comment) error {
+	cw.comments = append(cw.comments, c)
+	return nil
+}
+
+func (cw *SARIFCommentWriter) Flush(_ context.Context) error {
+	run := sarif.Run{
+		Tool: sarif.Tool{
+			Driver: sarif.ToolComponent{
+				Name:  cw.toolName,
+				Rules: make([]sarif.ReportingDescriptor, 0),
+			},
+		},
+	}
+	seenRules := make(map[string]bool)
+	for _, c := range cw.comments {
+		result := sarif.Result{
+			Message: sarif.Message{
+				Text: sarif.String(c.Result.Diagnostic.Message),
+			},
+		}
+		if code := c.Result.Diagnostic.GetCode(); code.GetValue() != "" {
+			result.RuleID = sarif.String(code.GetValue())
+			if seen := seenRules[code.GetValue()]; !seen {
+				seenRules[code.GetValue()] = true
+				rd := sarif.ReportingDescriptor{
+					ID: code.GetValue(),
+				}
+				if code.GetUrl() != "" {
+					rd.HelpURI = sarif.String(code.GetUrl())
+				}
+				run.Tool.Driver.Rules = append(run.Tool.Driver.Rules, rd)
+			}
+		}
+		level := severity2level(c.Result.Diagnostic.GetSeverity())
+		if level != sarif.None {
+			result.Level = &level
+		}
+		artifactLoc := sarif.ArtifactLocation{
+			URI: sarif.String(c.Result.Diagnostic.GetLocation().GetPath()),
+		}
+		result.Locations = []sarif.Location{{
+			PhysicalLocation: &sarif.PhysicalLocation{
+				ArtifactLocation: &artifactLoc,
+				Region:           range2region(c.Result.Diagnostic.GetLocation().GetRange()),
+			},
+		}}
+		if len(c.Result.Diagnostic.GetSuggestions()) > 0 {
+			result.Fixes = make([]sarif.Fix, 0)
+			for _, suggestion := range c.Result.Diagnostic.GetSuggestions() {
+				result.Fixes = append(result.Fixes, sarif.Fix{
+					ArtifactChanges: []sarif.ArtifactChange{
+						{
+							ArtifactLocation: artifactLoc,
+							Replacements: []sarif.Replacement{{
+								DeletedRegion: *range2region(suggestion.GetRange()),
+								InsertedContent: &sarif.ArtifactContent{
+									Text: sarif.String(suggestion.GetText()),
+								},
+							}},
+						},
+					},
+				})
+			}
+		}
+		if len(c.Result.Diagnostic.GetRelatedLocations()) > 0 {
+			result.RelatedLocations = make([]sarif.Location, 0)
+			for _, relLoc := range c.Result.Diagnostic.GetRelatedLocations() {
+				result.RelatedLocations = append(result.RelatedLocations, sarif.Location{
+					PhysicalLocation: &sarif.PhysicalLocation{
+						ArtifactLocation: &sarif.ArtifactLocation{
+							URI: sarif.String(relLoc.GetLocation().GetPath()),
+						},
+						Region: range2region(relLoc.GetLocation().GetRange()),
+					},
+					Message: &sarif.Message{
+						Text: sarif.String(relLoc.Message),
+					},
+				})
+			}
+		}
+		run.Results = append(run.Results, result)
+	}
+	slf := sarif.NewSarif()
+	slf.Runs = []sarif.Run{run}
+	encoder := json.NewEncoder(cw.w)
+	encoder.SetIndent("", "  ")
+	return encoder.Encode(slf)
+}
+
+func range2region(rng *rdf.Range) *sarif.Region {
+	region := &sarif.Region{}
+	start := rng.GetStart()
+	end := rng.GetEnd()
+	if start.GetLine() > 0 {
+		region.StartLine = sarif.Int64(int64(start.GetLine()))
+	}
+	if start.GetColumn() > 0 {
+		// Column is not usually unicodeCodePoints, but let's just keep it
+		// as is...
+		region.StartColumn = sarif.Int64(int64(start.GetColumn()))
+	}
+	if end.GetLine() > 0 {
+		region.EndLine = sarif.Int64(int64(end.GetLine()))
+	}
+	if end.GetColumn() > 0 {
+		region.EndColumn = sarif.Int64(int64(end.GetColumn()))
+	}
+	return region
+}
+
+func severity2level(s rdf.Severity) sarif.Level {
+	switch s {
+	case rdf.Severity_ERROR:
+		return sarif.Error
+	case rdf.Severity_WARNING:
+		return sarif.Warning
+	default:
+		return sarif.None
+	}
+}


Test patch:
diff --git a/comment_iowriter_test.go b/comment_iowriter_test.go
index 1ab06fde..8aeaf502 100644
--- a/comment_iowriter_test.go
+++ b/comment_iowriter_test.go
@@ -259,3 +259,126 @@ func TestRDJSONCommentWriter_Post(t *testing.T) {
 		t.Errorf("got\n%v\nwant:\n%v", got, want)
 	}
 }
+
+func TestSARIFCommentWriter_Post(t *testing.T) {
+	comments := []*Comment{
+		{
+			Result: &filter.FilteredDiagnostic{
+				Diagnostic: &rdf.Diagnostic{
+					Location: &rdf.Location{Path: "/path/to/file"},
+					Message:  "message",
+				},
+			},
+			ToolName: "tool name",
+		},
+		{
+			Result: &filter.FilteredDiagnostic{
+				Diagnostic: &rdf.Diagnostic{
+					Location: &rdf.Location{
+						Path: "/path/to/file",
+						Range: &rdf.Range{Start: &rdf.Position{
+							Column: 14,
+						}},
+					},
+					Message: "message",
+				},
+			},
+		},
+		{
+			Result: &filter.FilteredDiagnostic{
+				Diagnostic: &rdf.Diagnostic{
+					Location: &rdf.Location{
+						Path: "/path/to/file",
+						Range: &rdf.Range{Start: &rdf.Position{
+							Column: 14,
+						}},
+					},
+					Message: "message",
+					Source: &rdf.Source{
+						Name: "tool name in Diagnostic",
+						Url:  "tool url",
+					},
+				},
+			},
+		},
+	}
+	buf := new(bytes.Buffer)
+	cw := NewSARIFCommentWriter(buf, "tool name [constructor]")
+	for _, c := range comments {
+		if err := cw.Post(context.Background(), c); err != nil {
+			t.Error(err)
+		}
+	}
+	if err := cw.Flush(context.Background()); err != nil {
+		t.Error(err)
+	}
+	want := `
+{
+  "$schema": "https://docs.oasis-open.org/sarif/sarif/v2.1.0/errata01/os/schemas/sarif-schema-2.1.0.json",
+  "runs": [
+    {
+      "results": [
+        {
+          "locations": [
+            {
+              "physicalLocation": {
+                "artifactLocation": {
+                  "uri": "/path/to/file"
+                },
+                "region": {}
+              }
+            }
+          ],
+          "message": {
+            "text": "message"
+          }
+        },
+        {
+          "locations": [
+            {
+              "physicalLocation": {
+                "artifactLocation": {
+                  "uri": "/path/to/file"
+                },
+                "region": {
+                  "startColumn": 14
+                }
+              }
+            }
+          ],
+          "message": {
+            "text": "message"
+          }
+        },
+        {
+          "locations": [
+            {
+              "physicalLocation": {
+                "artifactLocation": {
+                  "uri": "/path/to/file"
+                },
+                "region": {
+                  "startColumn": 14
+                }
+              }
+            }
+          ],
+          "message": {
+            "text": "message"
+          }
+        }
+      ],
+      "tool": {
+        "driver": {
+          "name": "tool name [constructor]"
+        }
+      }
+    }
+  ],
+  "version": "2.1.0"
+}`
+	got := strings.TrimSpace(buf.String())
+	if got != strings.TrimSpace(want) {
+		t.Errorf("got\n%v\nwant:\n%v", got, want)
+	}
+}