github.com/bgpat/reviewdog@v0.0.0-20230909064023-077e44ca1f66/parser/sarif.go (about)

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