Repository: reviewdog/reviewdog
Pull request #1554 (2023-09-02 11:17:04)

Add SARIF format input support.
This PR makes reviewdog can read [SARIF 2.1.0](https://docs.oasis-open.org/sarif/sarif/v2.1.0/sarif-v2.1.0.html) format.

* [x] Add SarifParser (parser/sarif.go)
* [x] Add Tests (parser/sarif_test.go)
* [x] Add `-f=sarif` option
* [x] Add GitHhub Actions textlint sarif workflow

---

- related: #1175
- [x] Updated Unreleased section in [CHANGELOG](https://github.com/reviewdog/reviewdog/blob/master/CHANGELOG.md) or it's not notable changes.



Patch:
diff --git a/.github/workflows/reviewdog.yml b/.github/workflows/reviewdog.yml
index 90912c24..4b71ba54 100644
--- a/.github/workflows/reviewdog.yml
+++ b/.github/workflows/reviewdog.yml
@@ -258,3 +258,17 @@ jobs:
         run: |
           npx textlint -f checkstyle README.md | \
             reviewdog -f=checkstyle -name="textlint" -reporter=github-check -level=info
+
+  sarif:
+    name: runner / textlint sarif
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v3
+      - uses: reviewdog/action-setup@v1
+      - run: npm install
+      - name: textlint sarif
+        env:
+          REVIEWDOG_GITHUB_API_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+        run: |
+          npx textlint -f @microsoft/eslint-formatter-sarif README.md | \
+            reviewdog -f=sarif -name="textlint" -reporter=github-check -level=info
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 3da47e32..012f9e2e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -9,7 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 ### :sparkles: Release Note <!-- optional -->
 
 ### :rocket: Enhancements
-- ...
+- [#1554](https://github.com/reviewdog/reviewdog/pull/1554) Add SARIF format input support.
 
 ### :bug: Fixes
 - ...
diff --git a/README.md b/README.md
index b8af9d27..4b3accb5 100644
--- a/README.md
+++ b/README.md
@@ -74,6 +74,7 @@ by diff.
   * [Reviewdog Diagnostic Format (RDFormat)](#reviewdog-diagnostic-format-rdformat)
   * [Diff](#diff)
   * [checkstyle format](#checkstyle-format)
+  * [SARIF format](#sarif-format)
 - [Code Suggestions](#code-suggestions)
 - [reviewdog config file](#reviewdog-config-file)
 - [Reporters](#reporters)
@@ -287,6 +288,16 @@ Also, if you want to pass other Json/XML/etc... format to reviewdog, you can wri
 $ <linter> | <convert-to-checkstyle> | reviewdog -f=checkstyle -name="<linter>" -reporter=github-pr-check
 ```
 
+### SARIF format
+
+reviewdog supports [SARIF 2.1.0 JSON format](https://sarifweb.azurewebsites.net/).
+You can use reviewdog with -f=sarif option.
+
+```shell
+# Local
+$ eslint -f @microsoft/eslint-formatter-sarif . | reviewdog -f=sarif -diff="git diff"
+````
+
 ## Code Suggestions
 
 ![eslint reviewdog suggestion demo](https://user-images.githubusercontent.com/3797062/97085944-87233a80-165b-11eb-94a8-0a47d5e24905.png)
diff --git a/cmd/reviewdog/main.go b/cmd/reviewdog/main.go
index 52fab54e..354321cf 100644
--- a/cmd/reviewdog/main.go
+++ b/cmd/reviewdog/main.go
@@ -399,6 +399,7 @@ func runList(w io.Writer) error {
 	fmt.Fprintf(tabw, "%s\t%s\t- %s\n", "rdjsonl", "Reviewdog Diagnostic JSONL Format (JSONL of Diagnostic message)", "https://github.com/reviewdog/reviewdog")
 	fmt.Fprintf(tabw, "%s\t%s\t- %s\n", "diff", "Unified Diff Format", "https://en.wikipedia.org/wiki/Diff#Unified_format")
 	fmt.Fprintf(tabw, "%s\t%s\t- %s\n", "checkstyle", "checkstyle XML format", "http://checkstyle.sourceforge.net/")
+	fmt.Fprintf(tabw, "%s\t%s\t- %s\n", "sarif", "SARIF JSON format", "https://sarifweb.azurewebsites.net/")
 	for _, f := range sortedFmts(fmts.DefinedFmts()) {
 		fmt.Fprintf(tabw, "%s\t%s\t- %s\n", f.Name, f.Description, f.URL)
 	}
diff --git a/go.mod b/go.mod
index b5349ac0..aaaa7210 100644
--- a/go.mod
+++ b/go.mod
@@ -28,7 +28,7 @@ require (
 	github.com/vvakame/sdlog v1.2.0
 	github.com/xanzy/go-gitlab v0.90.0
 	go.opencensus.io v0.24.0
-	golang.org/x/build v0.0.0-20230901231555-066ec55e3caf
+	golang.org/x/build v0.0.0-20230829194117-88aa4609365e
 	golang.org/x/oauth2 v0.11.0
 	golang.org/x/sync v0.3.0
 	google.golang.org/protobuf v1.31.0
diff --git a/go.sum b/go.sum
index 83f43237..677d3430 100644
--- a/go.sum
+++ b/go.sum
@@ -287,8 +287,8 @@ go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E=
 go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
 go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
 go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=
-golang.org/x/build v0.0.0-20230901231555-066ec55e3caf h1:+7+4XRn7zTEWRyxL6JohyIXLPFAITcxqZS58W438dFo=
-golang.org/x/build v0.0.0-20230901231555-066ec55e3caf/go.mod h1:LblaorLo4w94wk4xZvKeLSaFLku6WbfOih5CoMhhvVc=
+golang.org/x/build v0.0.0-20230829194117-88aa4609365e h1:rHH9VSxgM4Ghsc37YRsBtyIsCQfO/5JI72FwEAc2gb8=
+golang.org/x/build v0.0.0-20230829194117-88aa4609365e/go.mod h1:LblaorLo4w94wk4xZvKeLSaFLku6WbfOih5CoMhhvVc=
 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
 golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
diff --git a/parser/parser.go b/parser/parser.go
index 61418717..e4133e4a 100644
--- a/parser/parser.go
+++ b/parser/parser.go
@@ -41,6 +41,8 @@ func New(opt *Option) (Parser, error) {
 		return NewRDJSONParser(), nil
 	case "diff":
 		return NewDiffParser(opt.DiffStrip), nil
+	case "sarif":
+		return NewSarifParser(), nil
 	}
 
 	// use defined errorformat
diff --git a/parser/sarif.go b/parser/sarif.go
new file mode 100644
index 00000000..f00f14de
--- /dev/null
+++ b/parser/sarif.go
@@ -0,0 +1,364 @@
+package parser
+
+import (
+	"encoding/json"
+	"fmt"
+	"io"
+	"net/url"
+	"os"
+	"path/filepath"
+
+	"github.com/reviewdog/reviewdog/proto/rdf"
+	"github.com/reviewdog/reviewdog/service/serviceutil"
+)
+
+var _ Parser = &SarifParser{}
+
+// SarifParser is sarif parser.
+type SarifParser struct{}
+
+// NewSarifParser returns a new SarifParser.
+func NewSarifParser() Parser {
+	return &SarifParser{}
+}
+
+func (p *SarifParser) Parse(r io.Reader) ([]*rdf.Diagnostic, error) {
+	slf := new(SarifJson)
+	if err := json.NewDecoder(r).Decode(slf); err != nil {
+		return nil, err
+	}
+	var ds []*rdf.Diagnostic
+	basedir, err := os.Getwd()
+	if err != nil {
+		return nil, err
+	}
+	if root, err := serviceutil.GetGitRoot(); err == nil {
+		basedir = root
+	}
+	for _, run := range slf.Runs {
+		tool := run.Tool
+		driver := tool.Driver
+		name := driver.Name
+		informationURI := driver.InformationURI
+		baseURIs := run.OriginalURIBaseIds
+		rules := map[string]SarifRule{}
+		for _, rule := range driver.Rules {
+			rules[rule.ID] = rule
+		}
+		for _, result := range run.Results {
+			message := result.Message.GetText()
+			ruleID := result.RuleID
+			rule := rules[ruleID]
+			level := result.Level
+			if level == "" {
+				level = rule.DefaultConfiguration.Level
+			}
+			suggestionsMap := map[string][]*rdf.Suggestion{}
+			for _, fix := range result.Fixes {
+				for _, artifactChange := range fix.ArtifactChanges {
+					suggestions := []*rdf.Suggestion{}
+					path, err := artifactChange.ArtifactLocation.GetPath(baseURIs, basedir)
+					if err != nil {
+						// invalid path
+						return nil, err
+					}
+					for _, replacement := range artifactChange.Replacements {
+						deletedRegion := replacement.DeletedRegion
+						rng := deletedRegion.GetRdfRange()
+						if rng == nil {
+							// No line information in fix
+							continue
+						}
+						s := &rdf.Suggestion{
+							Range: rng,
+							Text:  replacement.InsertedContent.Text,
+						}
+						suggestions = append(suggestions, s)
+					}
+					suggestionsMap[path] = suggestions
+				}
+			}
+			for _, location := range result.Locations {
+				physicalLocation := location.PhysicalLocation
+				artifactLocation := physicalLocation.ArtifactLocation
+				path, err := artifactLocation.GetPath(baseURIs, basedir)
+				if err != nil {
+					// invalid path
+					return nil, err
+				}
+				region := physicalLocation.Region
+				rng := region.GetRdfRange()
+				if rng == nil {
+					// No line information in result
+					continue
+				}
+				var code *rdf.Code
+				if ruleID != "" {
+					code = &rdf.Code{
+						Value: ruleID,
+						Url:   rule.HelpURI,
+					}
+				}
+				d := &rdf.Diagnostic{
+					Message: message,
+					Location: &rdf.Location{
+						Path:  path,
+						Range: rng,
+					},
+					Severity: severity(level),
+					Source: &rdf.Source{
+						Name: name,
+						Url:  informationURI,
+					},
+					Code:        code,
+					Suggestions: suggestionsMap[path],
+					OriginalOutput: fmt.Sprintf(
+						"%v:%d:%d: %v: %v (%v)",
+						path, rng.Start.Line, getActualStartColumn(rng), level, message, ruleID,
+					),
+				}
+				ds = append(ds, d)
+			}
+		}
+	}
+	return ds, nil
+}
+
+// SARIF JSON Format
+//
+// References:
+//   - https://sarifweb.azurewebsites.net/
+type SarifJson struct {
+	Runs []struct {
+		OriginalURIBaseIds map[string]SarifOriginalURI `json:"originalUriBaseIds"`
+		Results            []struct {
+			Level     string `json:"level"`
+			Locations []struct {
+				PhysicalLocation struct {
+					ArtifactLocation SarifArtifactLocation `json:"artifactLocation"`
+					Region           SarifRegion           `json:"region"`
+				} `json:"physicalLocation"`
+			} `json:"locations"`
+			Message SarifText `json:"message"`
+			RuleID  string    `json:"ruleId"`
+			Fixes   []struct {
+				Description     SarifText `json:"description"`
+				ArtifactChanges []struct {
+					ArtifactLocation SarifArtifactLocation `json:"artifactLocation"`
+					Replacements     []struct {
+						DeletedRegion   SarifRegion `json:"deletedRegion"`
+						InsertedContent struct {
+							Text string `json:"text"`
+						} `json:"insertedContent"`
+					} `json:"replacements"`
+				} `json:"artifactChanges"`
+			} `json:"fixes"`
+		} `json:"results"`
+		Tool struct {
+			Driver struct {
+				FullName       string      `json:"fullName"`
+				InformationURI string      `json:"informationUri"`
+				Name           string      `json:"name"`
+				Rules          []SarifRule `json:"rules"`
+			} `json:"driver"`
+		} `json:"tool"`
+	} `json:"runs"`
+}
+
+type SarifOriginalURI struct {
+	URI string `json:"uri"`
+}
+
+type SarifArtifactLocation struct {
+	URI       string `json:"uri"`
+	URIBaseID string `json:"uriBaseId"`
+	Index     int    `json:"index"`
+}
+
+func (l *SarifArtifactLocation) GetPath(
+	baseURIs map[string]SarifOriginalURI,
+	basedir string,
+) (string, error) {
+	uri := l.URI
+	baseURI := baseURIs[l.URIBaseID].URI
+	if baseURI != "" {
+		if u, err := url.JoinPath(baseURI, uri); err == nil {
+			uri = u
+		}
+	}
+	parse, err := url.Parse(uri)
+	if err != nil {
+		return "", err
+	}
+	path := parse.Path
+	if relpath, err := filepath.Rel(basedir, path); err == nil {
+		path = relpath
+	}
+	return path, nil
+}
+
+type SarifText struct {
+	Text     string  `json:"text"`
+	Markdown *string `json:"markdown"`
+}
+
+func (t *SarifText) GetText() string {
+	text := t.Text
+	if t.Markdown != nil {
+		text = *t.Markdown
+	}
+	return text
+}
+
+type SarifRegion struct {
+	StartLine   *int `json:"startLine"`
+	StartColumn *int `json:"startColumn"`
+	EndLine     *int `json:"endLine"`
+	EndColumn   *int `json:"endColumn"`
+}
+
+// convert SARIF Region to RDF Range
+//
+// * Supported SARIF: Line + Column Text region
+// * Not supported SARIF: Offset + Length Text region ("charOffset", "charLength"), Binary region
+//
+// example text:
+//
+//	abc\n
+//	def\n
+//
+// region: "abc"
+// SARIF: { "startLine": 1 }
+//
+//	= { "startLine": 1, "startColumn": 1, "endLine": 1, "endColumn": null }
+//
+// -> RDF: { "start": { "line": 1 } }
+//
+// region: "bc"
+// SARIF: { "startLine": 1, "startColumn": 2 }
+//
+//	= { "startLine": 1, "startColumn": 2, "endLine": 1, "endColumn": null }
+//
+// -> RDF: { "start": { "line": 1, "column": 2 }, "end": { "line": 1 } }
+//
+// region: "a"
+// SARIF: { "startLine": 1, "endColumn": 2 }
+//
+//	= { "startLine": 1, "startColumn": 1, "endLine": 1, "endColumn": 2 }
+//
+// -> RDF: { "start": { "line": 1 }, "end": { "column": 2 } }
+//
+//	= { "start": { "line": 1 }, "end": { "line": 1, "column": 2 } }
+//
+// region: "b"
+// SARIF: { "startLine": 1, "startColumn": 2, "endColumn": 3 }
+//
+//	= { "startLine": 1, "startColumn": 2, "endLine": 1, "endColumn": 3 }
+//
+// -> RDF: { "start": { "line": 1, "column": 2 }, "end": { "column": 3 } }
+//
+//	= { "start": { "line": 1, "column": 2 }, "end": { "line": 1, column": 3 } }
+//
+// region: "abc\ndef"
+// SARIF: { "startLine": 1, "endLine": 2 }
+//
+//	= { "startLine": 1, "startColumn": 1, "endLine": 2, "endColumn": null }
+//
+// -> RDF: { "start": { "line": 1 }, "end": { "line": 2 } }
+//
+// region: "abc\n"
+// SARIF: { "startLine": 1, "endLine": 2, "endColumn": 1 }
+//
+//	= { "startLine": 1, "startColumn": 1, "endLine": 2, "endColumn": 1 }
+//
+// -> RDF: { "start": { "line": 1 }, "end": { "line": 2, "column": 1 } }
+//
+// zero width region: "{■}abc"
+// SARIF: { "startLine": 1, "endColumn": 1 }
+//
+//	= { "startLine": 1, "startColumn": 1, "endLine": 1, "endColumn": 1 }
+//
+// -> RDF: { "start": { "line": 1, "column": 1 } }
+//
+//	= { "start": { "line": 1, "column": 1 }, "end": { "line": 1, "column": 1 } }
+func (r *SarifRegion) GetRdfRange() *rdf.Range {
+	if r.StartLine == nil {
+		// No line information
+		return nil
+	}
+	startLine := *r.StartLine
+	startColumn := 1 // default value of startColumn in SARIF is 1
+	if r.StartColumn != nil {
+		startColumn = *r.StartColumn
+	}
+	endLine := startLine // default value of endLine in SARIF is startLine
+	if r.EndLine != nil {
+		endLine = *r.EndLine
+	}
+	endColumn := 0 // default value of endColumn in SARIF is null (that means EOL)
+	if r.EndColumn != nil {
+		endColumn = *r.EndColumn
+	}
+	var end *rdf.Position
+	if startLine == endLine && startColumn == endColumn {
+		// zero width region
+		end = &rdf.Position{
+			Line:   int32(endLine),
+			Column: int32(endColumn),
+		}
+	} else {
+		// not zero width region
+		if startColumn == 1 {
+			// startColumn = 1 is default value, then omit it from result
+			startColumn = 0
+		}
+		if startLine != endLine {
+			// when multi line region, End property must be provided
+			end = &rdf.Position{
+				Line:   int32(endLine),
+				Column: int32(endColumn),
+			}
+		} else {
+			// when single line region
+			if startColumn == 0 && endColumn == 0 {
+				// if single whole line region, no End properties are needed
+			} else {
+				// otherwise, End property is needed
+				end = &rdf.Position{
+					Line:   int32(endLine),
+					Column: int32(endColumn),
+				}
+			}
+		}
+	}
+	rng := &rdf.Range{
+		Start: &rdf.Position{
+			Line:   int32(startLine),
+			Column: int32(startColumn),
+		},
+		End: end,
+	}
+	return rng
+}
+
+type SarifRule struct {
+	ID                   string    `json:"id"`
+	Name                 string    `json:"name"`
+	ShortDescription     SarifText `json:"shortDescription"`
+	FullDescription      SarifText `json:"fullDescription"`
+	Help                 SarifText `json:"help"`
+	HelpURI              string    `json:"helpUri"`
+	DefaultConfiguration struct {
+		Level string `json:"level"`
+		Rank  int    `json:"rank"`
+	} `json:"defaultConfiguration"`
+}
+
+func getActualStartColumn(r *rdf.Range) int32 {
+	startColumn := r.Start.Column
+	if startColumn == 0 {
+		// startColumn's default value means column 1
+		startColumn = 1
+	}
+	return startColumn
+}
diff --git a/service/serviceutil/serviceutil.go b/service/serviceutil/serviceutil.go
index 49a53406..320b664d 100644
--- a/service/serviceutil/serviceutil.go
+++ b/service/serviceutil/serviceutil.go
@@ -33,6 +33,14 @@ func GitRelWorkdir() (string, error) {
 	return path, nil
 }
 
+func GetGitRoot() (string, error) {
+	cwd, err := os.Getwd()
+	if err != nil {
+		return "", err
+	}
+	return findGitRoot(cwd)
+}
+
 func findGitRoot(path string) (string, error) {
 	gitPath, err := findDotGitPath(path)
 	if err != nil {


Test patch:
diff --git a/parser/parser_test.go b/parser/parser_test.go
index 960ccb90..76e5f2c1 100644
--- a/parser/parser_test.go
+++ b/parser/parser_test.go
@@ -35,6 +35,12 @@ func TestNewParser(t *testing.T) {
 			},
 			typ: &ErrorformatParser{},
 		},
+		{
+			in: &Option{
+				FormatName: "sarif",
+			},
+			typ: &SarifParser{},
+		},
 		{ // empty
 			in:      &Option{},
 			wantErr: true,
diff --git a/parser/sarif_test.go b/parser/sarif_test.go
new file mode 100644
index 00000000..9b4ac416
--- /dev/null
+++ b/parser/sarif_test.go
@@ -0,0 +1,276 @@
+package parser
+
+import (
+	"bytes"
+	"encoding/json"
+	"fmt"
+	"reflect"
+	"strings"
+	"testing"
+
+	"github.com/reviewdog/reviewdog/service/serviceutil"
+	"google.golang.org/protobuf/encoding/protojson"
+)
+
+func TestExampleSarifParser(t *testing.T) {
+	p := NewSarifParser()
+	for i, fixture := range fixtures {
+		diagnostics, err := p.Parse(strings.NewReader(fixture[0]))
+		if err != nil {
+			panic(err)
+		}
+		for _, d := range diagnostics {
+			rdjson, _ := protojson.MarshalOptions{Indent: "  "}.Marshal(d)
+			var actualJson interface{}
+			var expectJson interface{}
+			json.Unmarshal([]byte(rdjson), &actualJson)
+			json.Unmarshal([]byte(fixture[1]), &expectJson)
+			if !reflect.DeepEqual(actualJson, expectJson) {
+				var out bytes.Buffer
+				json.Indent(&out, rdjson, "", "\t")
+				actual := out.String()
+				expect := fixture[1]
+				t.Errorf("actual(%v):\n%v\n---\nexpect(%v):\n%v", i, actual, i, expect)
+			}
+		}
+	}
+}
+
+func basedir() string {
+	root, err := serviceutil.GetGitRoot()
+	if err != nil {
+		panic(err)
+	}
+	return root
+}
+
+var fixtures = [][]string{{
+	fmt.Sprintf(`{
+	"$schema": "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json",
+	"version": "2.1.0",
+	"runs": [
+		{
+			"originalUriBaseIds": {
+				"SRCROOT": {
+					"uri": "file://%s"
+				}
+			},
+			"results": [
+				{
+					"level": "warning",
+					"locations": [
+						{
+							"physicalLocation": {
+								"artifactLocation": {
+									"uri": "src/MyClass.kt",
+									"uriBaseId": "SRCROOT"
+								},
+								"region": {
+									"startLine": 10,
+									"startColumn": 5,
+									"endLine": 12,
+									"endColumn": 15
+								}
+							}
+						}
+					],
+					"message": {
+						"text": "result message"
+					},
+					"ruleId": "rule_id"
+				}
+			],
+			"tool": {
+				"driver": {
+					"informationUri": "https://example.com",
+					"name": "driver_name",
+					"rules": [
+						{
+							"helpUri": "https://example.com",
+							"id": "rule_id",
+							"name": "My Rule",
+							"shortDescription": {
+								"text": "Rule description"
+							}
+						}
+					]
+				}
+			}
+		}
+	]
+}`, basedir()), `{
+	"message": "result message",
+	"location": {
+		"path": "src/MyClass.kt",
+		"range": {
+			"start": {
+				"line": 10,
+				"column": 5
+			},
+			"end": {
+				"line": 12,
+				"column": 15
+			}
+		}
+	},
+	"severity": "WARNING",
+	"source": {
+		"name": "driver_name",
+		"url": "https://example.com"
+	},
+	"code": {
+		"value": "rule_id",
+		"url": "https://example.com"
+	},
+	"originalOutput": "src/MyClass.kt:10:5: warning: result message (rule_id)"
+}`},
+	{`{
+	"runs": [
+		{
+			"originalUriBaseIds": {
+				"SRCROOT": {
+					"description": "uri deleted root"
+				}
+			},
+			"results": [
+				{
+					"locations": [
+						{
+							"physicalLocation": {
+								"artifactLocation": {
+									"uri": "src/MyClass.kt",
+									"uriBaseId": "SRCROOT"
+								},
+								"region": {
+									"startLine": 10
+								}
+							}
+						}
+					],
+					"message": {
+						"text": "message"
+					},
+					"ruleId": "rule_id"
+				}
+			],
+			"tool": {
+				"driver": {
+					"name": "driver_name",
+					"rules": [
+						{
+							"id": "rule_id",
+							"defaultConfiguration": {
+								"level": "error"
+							}
+						}
+					]
+				}
+			}
+		}
+	]
+}`, `{
+	"message": "message",
+	"location": {
+		"path": "src/MyClass.kt",
+		"range": {
+			"start": {
+				"line": 10
+			}
+		}
+	},
+	"severity": "ERROR",
+	"source": {
+		"name": "driver_name"
+	},
+	"code": {
+		"value": "rule_id"
+	},
+	"originalOutput": "src/MyClass.kt:10:1: error: message (rule_id)"
+}`},
+	{`{
+	"runs": [
+		{
+			"results": [
+				{
+					"locations": [
+						{
+							"physicalLocation": {
+								"artifactLocation": {
+									"uri": "src/MyClass.kt"
+								},
+								"region": {
+									"startLine": 10
+								}
+							}
+						}
+					],
+					"fixes": [
+						{
+							"artifactChanges": [
+								{
+									"artifactLocation": {
+										"uri": "src/MyClass.kt"
+									},
+									"replacements": [
+										{
+											"deletedRegion": {
+												"startLine": 10,
+												"startColumn": 1,
+												"endColumn": 1
+											},
+											"insertedContent": {
+												"text": "// "
+											}
+										}
+									]
+								}
+							]
+						}
+					],
+					"message": {
+						"markdown": "message"
+					},
+					"ruleId": "rule_id"
+				}
+			],
+			"tool": {
+				"driver": {
+					"name": "driver_name"
+				}
+			}
+		}
+	]
+}`, `{
+	"message": "message",
+	"location": {
+		"path": "src/MyClass.kt",
+		"range": {
+			"start": {
+				"line": 10
+			}
+		}
+	},
+	"source": {
+		"name": "driver_name"
+	},
+	"code": {
+		"value": "rule_id"
+	},
+	"suggestions": [
+		{
+			"range": {
+				"start": {
+					"line": 10,
+					"column": 1
+				},
+				"end": {
+					"line": 10,
+					"column": 1
+				}
+			},
+			"text": "// "
+		}
+	],
+	"originalOutput": "src/MyClass.kt:10:1: : message (rule_id)"
+}`},
+}