github.com/reviewdog/reviewdog@v0.17.5-0.20240516205324-0cd103a83d58/parser/sarif.go (about)

     1  package parser
     2  
     3  import (
     4  	"encoding/json"
     5  	"io"
     6  	"net/url"
     7  	"os"
     8  	"path/filepath"
     9  
    10  	"github.com/reviewdog/reviewdog/proto/rdf"
    11  	"github.com/reviewdog/reviewdog/service/serviceutil"
    12  )
    13  
    14  var _ Parser = &SarifParser{}
    15  
    16  // SarifParser is sarif parser.
    17  type SarifParser struct{}
    18  
    19  // NewSarifParser returns a new SarifParser.
    20  func NewSarifParser() Parser {
    21  	return &SarifParser{}
    22  }
    23  
    24  func (p *SarifParser) Parse(r io.Reader) ([]*rdf.Diagnostic, error) {
    25  	slf := new(SarifJson)
    26  	if err := json.NewDecoder(r).Decode(slf); err != nil {
    27  		return nil, err
    28  	}
    29  	var ds []*rdf.Diagnostic
    30  	basedir, err := os.Getwd()
    31  	if err != nil {
    32  		return nil, err
    33  	}
    34  	if root, err := serviceutil.GetGitRoot(); err == nil {
    35  		basedir = root
    36  	}
    37  	for _, run := range slf.Runs {
    38  		tool := run.Tool
    39  		driver := tool.Driver
    40  		name := driver.Name
    41  		informationURI := driver.InformationURI
    42  		baseURIs := run.OriginalURIBaseIds
    43  		rules := map[string]SarifRule{}
    44  		for _, rule := range driver.Rules {
    45  			rules[rule.ID] = rule
    46  		}
    47  		for _, result := range run.Results {
    48  			original, err := json.Marshal(result)
    49  			if err != nil {
    50  				return nil, err
    51  			}
    52  			message := result.Message.GetText()
    53  			ruleID := result.RuleID
    54  			rule := rules[ruleID]
    55  			level := result.Level
    56  			if level == "" {
    57  				level = rule.DefaultConfiguration.Level
    58  			}
    59  			suggestionsMap := map[string][]*rdf.Suggestion{}
    60  			for _, fix := range result.Fixes {
    61  				for _, artifactChange := range fix.ArtifactChanges {
    62  					suggestions := []*rdf.Suggestion{}
    63  					path, err := artifactChange.ArtifactLocation.GetPath(baseURIs, basedir)
    64  					if err != nil {
    65  						// invalid path
    66  						return nil, err
    67  					}
    68  					for _, replacement := range artifactChange.Replacements {
    69  						deletedRegion := replacement.DeletedRegion
    70  						rng := deletedRegion.GetRdfRange()
    71  						if rng == nil {
    72  							// No line information in fix
    73  							continue
    74  						}
    75  						s := &rdf.Suggestion{
    76  							Range: rng,
    77  							Text:  replacement.InsertedContent.Text,
    78  						}
    79  						suggestions = append(suggestions, s)
    80  					}
    81  					suggestionsMap[path] = suggestions
    82  				}
    83  			}
    84  			for _, location := range result.Locations {
    85  				physicalLocation := location.PhysicalLocation
    86  				artifactLocation := physicalLocation.ArtifactLocation
    87  				path, err := artifactLocation.GetPath(baseURIs, basedir)
    88  				if err != nil {
    89  					// invalid path
    90  					return nil, err
    91  				}
    92  				region := physicalLocation.Region
    93  				rng := region.GetRdfRange()
    94  				var code *rdf.Code
    95  				if ruleID != "" {
    96  					code = &rdf.Code{
    97  						Value: ruleID,
    98  						Url:   rule.HelpURI,
    99  					}
   100  				}
   101  				d := &rdf.Diagnostic{
   102  					Message: message,
   103  					Location: &rdf.Location{
   104  						Path:  path,
   105  						Range: rng,
   106  					},
   107  					Severity: severity(level),
   108  					Source: &rdf.Source{
   109  						Name: name,
   110  						Url:  informationURI,
   111  					},
   112  					Code:           code,
   113  					Suggestions:    suggestionsMap[path],
   114  					OriginalOutput: string(original),
   115  				}
   116  				ds = append(ds, d)
   117  			}
   118  		}
   119  	}
   120  	return ds, nil
   121  }
   122  
   123  // SARIF JSON Format
   124  //
   125  // References:
   126  //   - https://sarifweb.azurewebsites.net/
   127  type SarifJson struct {
   128  	Runs []struct {
   129  		OriginalURIBaseIds map[string]SarifOriginalURI `json:"originalUriBaseIds"`
   130  		Results            []struct {
   131  			Level     string `json:"level,omitempty"`
   132  			Locations []struct {
   133  				PhysicalLocation struct {
   134  					ArtifactLocation SarifArtifactLocation `json:"artifactLocation,omitempty"`
   135  					Region           SarifRegion           `json:"region,omitempty"`
   136  				} `json:"physicalLocation,omitempty"`
   137  			} `json:"locations"`
   138  			Message SarifText `json:"message"`
   139  			RuleID  string    `json:"ruleId,omitempty"`
   140  			Fixes   []struct {
   141  				Description     SarifText `json:"description"`
   142  				ArtifactChanges []struct {
   143  					ArtifactLocation SarifArtifactLocation `json:"artifactLocation,omitempty"`
   144  					Replacements     []struct {
   145  						DeletedRegion   SarifRegion `json:"deletedRegion"`
   146  						InsertedContent struct {
   147  							Text string `json:"text"`
   148  						} `json:"insertedContent,omitempty"`
   149  					} `json:"replacements"`
   150  				} `json:"artifactChanges"`
   151  			} `json:"fixes,omitempty"`
   152  		} `json:"results"`
   153  		Tool struct {
   154  			Driver struct {
   155  				FullName       string      `json:"fullName"`
   156  				InformationURI string      `json:"informationUri"`
   157  				Name           string      `json:"name"`
   158  				Rules          []SarifRule `json:"rules"`
   159  			} `json:"driver"`
   160  		} `json:"tool"`
   161  	} `json:"runs"`
   162  }
   163  
   164  type SarifOriginalURI struct {
   165  	URI string `json:"uri"`
   166  }
   167  
   168  type SarifArtifactLocation struct {
   169  	URI       string `json:"uri,omitempty"`
   170  	URIBaseID string `json:"uriBaseId"`
   171  	Index     int    `json:"index,omitempty"`
   172  }
   173  
   174  func (l *SarifArtifactLocation) GetPath(
   175  	baseURIs map[string]SarifOriginalURI,
   176  	basedir string,
   177  ) (string, error) {
   178  	uri := l.URI
   179  	baseURI := baseURIs[l.URIBaseID].URI
   180  	if baseURI != "" {
   181  		if u, err := url.JoinPath(baseURI, uri); err == nil {
   182  			uri = u
   183  		}
   184  	}
   185  	parse, err := url.Parse(uri)
   186  	if err != nil {
   187  		return "", err
   188  	}
   189  	path := parse.Path
   190  	if relpath, err := filepath.Rel(basedir, path); err == nil {
   191  		path = relpath
   192  	}
   193  	return path, nil
   194  }
   195  
   196  type SarifText struct {
   197  	Text     string  `json:"text,omitempty"`
   198  	Markdown *string `json:"markdown,omitempty"`
   199  }
   200  
   201  func (t *SarifText) GetText() string {
   202  	text := t.Text
   203  	if t.Markdown != nil {
   204  		text = *t.Markdown
   205  	}
   206  	return text
   207  }
   208  
   209  type SarifRegion struct {
   210  	StartLine   *int `json:"startLine"`
   211  	StartColumn *int `json:"startColumn,omitempty"`
   212  	EndLine     *int `json:"endLine,omitempty"`
   213  	EndColumn   *int `json:"endColumn,omitempty"`
   214  }
   215  
   216  // convert SARIF Region to RDF Range
   217  //
   218  // * Supported SARIF: Line + Column Text region
   219  // * Not supported SARIF: Offset + Length Text region ("charOffset", "charLength"), Binary region
   220  //
   221  // example text:
   222  //
   223  //	abc\n
   224  //	def\n
   225  //
   226  // region: "abc"
   227  // SARIF: { "startLine": 1 }
   228  //
   229  //	= { "startLine": 1, "startColumn": 1, "endLine": 1, "endColumn": null }
   230  //
   231  // -> RDF: { "start": { "line": 1 } }
   232  //
   233  // region: "bc"
   234  // SARIF: { "startLine": 1, "startColumn": 2 }
   235  //
   236  //	= { "startLine": 1, "startColumn": 2, "endLine": 1, "endColumn": null }
   237  //
   238  // -> RDF: { "start": { "line": 1, "column": 2 }, "end": { "line": 1 } }
   239  //
   240  // region: "a"
   241  // SARIF: { "startLine": 1, "endColumn": 2 }
   242  //
   243  //	= { "startLine": 1, "startColumn": 1, "endLine": 1, "endColumn": 2 }
   244  //
   245  // -> RDF: { "start": { "line": 1 }, "end": { "column": 2 } }
   246  //
   247  //	= { "start": { "line": 1 }, "end": { "line": 1, "column": 2 } }
   248  //
   249  // region: "b"
   250  // SARIF: { "startLine": 1, "startColumn": 2, "endColumn": 3 }
   251  //
   252  //	= { "startLine": 1, "startColumn": 2, "endLine": 1, "endColumn": 3 }
   253  //
   254  // -> RDF: { "start": { "line": 1, "column": 2 }, "end": { "column": 3 } }
   255  //
   256  //	= { "start": { "line": 1, "column": 2 }, "end": { "line": 1, column": 3 } }
   257  //
   258  // region: "abc\ndef"
   259  // SARIF: { "startLine": 1, "endLine": 2 }
   260  //
   261  //	= { "startLine": 1, "startColumn": 1, "endLine": 2, "endColumn": null }
   262  //
   263  // -> RDF: { "start": { "line": 1 }, "end": { "line": 2 } }
   264  //
   265  // region: "abc\n"
   266  // SARIF: { "startLine": 1, "endLine": 2, "endColumn": 1 }
   267  //
   268  //	= { "startLine": 1, "startColumn": 1, "endLine": 2, "endColumn": 1 }
   269  //
   270  // -> RDF: { "start": { "line": 1 }, "end": { "line": 2, "column": 1 } }
   271  //
   272  // zero width region: "{■}abc"
   273  // SARIF: { "startLine": 1, "endColumn": 1 }
   274  //
   275  //	= { "startLine": 1, "startColumn": 1, "endLine": 1, "endColumn": 1 }
   276  //
   277  // -> RDF: { "start": { "line": 1, "column": 1 } }
   278  //
   279  //	= { "start": { "line": 1, "column": 1 }, "end": { "line": 1, "column": 1 } }
   280  func (r *SarifRegion) GetRdfRange() *rdf.Range {
   281  	if r.StartLine == nil {
   282  		// No line information
   283  		return nil
   284  	}
   285  	startLine := *r.StartLine
   286  	startColumn := 1 // default value of startColumn in SARIF is 1
   287  	if r.StartColumn != nil {
   288  		startColumn = *r.StartColumn
   289  	}
   290  	endLine := startLine // default value of endLine in SARIF is startLine
   291  	if r.EndLine != nil {
   292  		endLine = *r.EndLine
   293  	}
   294  	endColumn := 0 // default value of endColumn in SARIF is null (that means EOL)
   295  	if r.EndColumn != nil {
   296  		endColumn = *r.EndColumn
   297  	}
   298  	var end *rdf.Position
   299  	if startLine == endLine && startColumn == endColumn {
   300  		// zero width region
   301  		end = &rdf.Position{
   302  			Line:   int32(endLine),
   303  			Column: int32(endColumn),
   304  		}
   305  	} else {
   306  		// not zero width region
   307  		if startColumn == 1 {
   308  			// startColumn = 1 is default value, then omit it from result
   309  			startColumn = 0
   310  		}
   311  		if startLine != endLine {
   312  			// when multi line region, End property must be provided
   313  			end = &rdf.Position{
   314  				Line:   int32(endLine),
   315  				Column: int32(endColumn),
   316  			}
   317  		} else {
   318  			// when single line region
   319  			if startColumn == 0 && endColumn == 0 {
   320  				// if single whole line region, no End properties are needed
   321  			} else {
   322  				// otherwise, End property is needed
   323  				end = &rdf.Position{
   324  					Line:   int32(endLine),
   325  					Column: int32(endColumn),
   326  				}
   327  			}
   328  		}
   329  	}
   330  	rng := &rdf.Range{
   331  		Start: &rdf.Position{
   332  			Line:   int32(startLine),
   333  			Column: int32(startColumn),
   334  		},
   335  		End: end,
   336  	}
   337  	return rng
   338  }
   339  
   340  type SarifRule struct {
   341  	ID                   string    `json:"id"`
   342  	Name                 string    `json:"name"`
   343  	ShortDescription     SarifText `json:"shortDescription"`
   344  	FullDescription      SarifText `json:"fullDescription"`
   345  	Help                 SarifText `json:"help"`
   346  	HelpURI              string    `json:"helpUri"`
   347  	DefaultConfiguration struct {
   348  		Level string `json:"level"`
   349  		Rank  int    `json:"rank"`
   350  	} `json:"defaultConfiguration"`
   351  }