371 lines
10 KiB
Go
371 lines
10 KiB
Go
|
package lintcmd
|
|||
|
|
|||
|
// Notes on GitHub-specific restrictions:
|
|||
|
//
|
|||
|
// Result.Message needs to either have ID or Text set. Markdown
|
|||
|
// gets ignored. Text isn't treated verbatim however: Markdown
|
|||
|
// formatting gets stripped, except for links.
|
|||
|
//
|
|||
|
// GitHub does not display RelatedLocations. The only way to make
|
|||
|
// use of them is to link to them (via their ID) in the
|
|||
|
// Result.Message. And even then, it will only show the referred
|
|||
|
// line of code, not the message. We can duplicate the messages in
|
|||
|
// the Result.Message, but we can't even indent them, because
|
|||
|
// leading whitespace gets stripped.
|
|||
|
//
|
|||
|
// GitHub does use the Markdown version of rule help, but it
|
|||
|
// renders it the way it renders comments on issues – that is, it
|
|||
|
// turns line breaks into hard line breaks, even though it
|
|||
|
// shouldn't.
|
|||
|
//
|
|||
|
// GitHub doesn't make use of the tool's URI or version, nor of
|
|||
|
// the help URIs of rules.
|
|||
|
//
|
|||
|
// There does not seem to be a way of using SARIF for "normal" CI,
|
|||
|
// without results showing up as code scanning alerts. Also, a
|
|||
|
// SARIF file containing only warnings, no errors, will not fail
|
|||
|
// CI by default, but this is configurable.
|
|||
|
// GitHub does display some parts of SARIF results in PRs, but
|
|||
|
// most of the useful parts of SARIF, such as help text of rules,
|
|||
|
// is only accessible via the code scanning alerts, which are only
|
|||
|
// accessible by users with write permissions.
|
|||
|
//
|
|||
|
// Result.Suppressions is being ignored.
|
|||
|
//
|
|||
|
//
|
|||
|
// Notes on other tools
|
|||
|
//
|
|||
|
// VS Code Sarif viewer
|
|||
|
//
|
|||
|
// The Sarif viewer in VS Code displays the full message in the
|
|||
|
// tabular view, removing newlines. That makes our multi-line
|
|||
|
// messages (which we use as a workaround for missing related
|
|||
|
// information) very ugly.
|
|||
|
//
|
|||
|
// Much like GitHub, the Sarif viewer does not make related
|
|||
|
// information visible unless we explicitly refer to it in the
|
|||
|
// message.
|
|||
|
//
|
|||
|
// Suggested fixes are not exposed in any way.
|
|||
|
//
|
|||
|
// It only shows the shortDescription or fullDescription of a
|
|||
|
// rule, not its help. We can't put the help in fullDescription,
|
|||
|
// because the fullDescription isn't meant to be that long. For
|
|||
|
// example, GitHub displays it in a single line, under the
|
|||
|
// shortDescription.
|
|||
|
//
|
|||
|
// VS Code can filter based on Result.Suppressions, but it doesn't
|
|||
|
// display our suppression message. Also, by default, suppressed
|
|||
|
// results get shown, and the column indicating that a result is
|
|||
|
// suppressed is hidden, which makes for a confusing experience.
|
|||
|
//
|
|||
|
// When a rule has only an ID, no name, VS Code displays a
|
|||
|
// prominent dash in place of the name. When the name and ID are
|
|||
|
// identical, it prints both. However, we can't make them
|
|||
|
// identical, as SARIF requires that either the ID and name are
|
|||
|
// different, or that the name is omitted.
|
|||
|
|
|||
|
// FIXME(dh): we're currently reporting column information using UTF-8
|
|||
|
// byte offsets, not using Unicode code points or UTF-16, which are
|
|||
|
// the only two ways allowed by SARIF.
|
|||
|
|
|||
|
// TODO(dh) set properties.tags – we can use different tags for the
|
|||
|
// staticcheck, simple, stylecheck and unused checks, so users can
|
|||
|
// filter their results
|
|||
|
|
|||
|
import (
|
|||
|
"encoding/json"
|
|||
|
"fmt"
|
|||
|
"net/url"
|
|||
|
"os"
|
|||
|
"path/filepath"
|
|||
|
"regexp"
|
|||
|
"strings"
|
|||
|
|
|||
|
"honnef.co/go/tools/analysis/lint"
|
|||
|
"honnef.co/go/tools/sarif"
|
|||
|
)
|
|||
|
|
|||
|
type sarifFormatter struct {
|
|||
|
driverName string
|
|||
|
driverVersion string
|
|||
|
driverWebsite string
|
|||
|
}
|
|||
|
|
|||
|
func sarifLevel(severity lint.Severity) string {
|
|||
|
switch severity {
|
|||
|
case lint.SeverityNone:
|
|||
|
// no configured severity, default to warning
|
|||
|
return "warning"
|
|||
|
case lint.SeverityError:
|
|||
|
return "error"
|
|||
|
case lint.SeverityDeprecated:
|
|||
|
return "warning"
|
|||
|
case lint.SeverityWarning:
|
|||
|
return "warning"
|
|||
|
case lint.SeverityInfo:
|
|||
|
return "note"
|
|||
|
case lint.SeverityHint:
|
|||
|
return "note"
|
|||
|
default:
|
|||
|
// unreachable
|
|||
|
return "none"
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
func encodePath(path string) string {
|
|||
|
return (&url.URL{Path: path}).EscapedPath()
|
|||
|
}
|
|||
|
|
|||
|
func sarifURI(path string) string {
|
|||
|
u := url.URL{
|
|||
|
Scheme: "file",
|
|||
|
Path: path,
|
|||
|
}
|
|||
|
return u.String()
|
|||
|
}
|
|||
|
|
|||
|
func sarifArtifactLocation(name string) sarif.ArtifactLocation {
|
|||
|
// Ideally we use relative paths so that GitHub can resolve them
|
|||
|
name = shortPath(name)
|
|||
|
if filepath.IsAbs(name) {
|
|||
|
return sarif.ArtifactLocation{
|
|||
|
URI: sarifURI(name),
|
|||
|
}
|
|||
|
} else {
|
|||
|
return sarif.ArtifactLocation{
|
|||
|
URI: encodePath(name),
|
|||
|
URIBaseID: "%SRCROOT%", // This is specific to GitHub,
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
func sarifFormatText(s string) string {
|
|||
|
// GitHub doesn't ignore line breaks, even though it should, so we remove them.
|
|||
|
|
|||
|
var out strings.Builder
|
|||
|
lines := strings.Split(s, "\n")
|
|||
|
for i, line := range lines[:len(lines)-1] {
|
|||
|
out.WriteString(line)
|
|||
|
if line == "" {
|
|||
|
out.WriteString("\n")
|
|||
|
} else {
|
|||
|
nextLine := lines[i+1]
|
|||
|
if nextLine == "" || strings.HasPrefix(line, "> ") || strings.HasPrefix(line, " ") {
|
|||
|
out.WriteString("\n")
|
|||
|
} else {
|
|||
|
out.WriteString(" ")
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
out.WriteString(lines[len(lines)-1])
|
|||
|
return convertCodeBlocks(out.String())
|
|||
|
}
|
|||
|
|
|||
|
func moreCodeFollows(lines []string) bool {
|
|||
|
for _, line := range lines {
|
|||
|
if line == "" {
|
|||
|
continue
|
|||
|
}
|
|||
|
if strings.HasPrefix(line, " ") {
|
|||
|
return true
|
|||
|
} else {
|
|||
|
return false
|
|||
|
}
|
|||
|
}
|
|||
|
return false
|
|||
|
}
|
|||
|
|
|||
|
var alpha = regexp.MustCompile(`^[a-zA-Z ]+$`)
|
|||
|
|
|||
|
func convertCodeBlocks(text string) string {
|
|||
|
var buf strings.Builder
|
|||
|
lines := strings.Split(text, "\n")
|
|||
|
|
|||
|
inCode := false
|
|||
|
empties := 0
|
|||
|
for i, line := range lines {
|
|||
|
if inCode {
|
|||
|
if !moreCodeFollows(lines[i:]) {
|
|||
|
if inCode {
|
|||
|
fmt.Fprintln(&buf, "```")
|
|||
|
inCode = false
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
prevEmpties := empties
|
|||
|
if line == "" && !inCode {
|
|||
|
empties++
|
|||
|
} else {
|
|||
|
empties = 0
|
|||
|
}
|
|||
|
|
|||
|
if line == "" {
|
|||
|
fmt.Fprintln(&buf)
|
|||
|
continue
|
|||
|
}
|
|||
|
|
|||
|
if strings.HasPrefix(line, " ") {
|
|||
|
line = line[4:]
|
|||
|
if !inCode {
|
|||
|
fmt.Fprintln(&buf, "```go")
|
|||
|
inCode = true
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
onlyAlpha := alpha.MatchString(line)
|
|||
|
out := line
|
|||
|
if !inCode && prevEmpties >= 2 && onlyAlpha {
|
|||
|
fmt.Fprintf(&buf, "## %s\n", out)
|
|||
|
} else {
|
|||
|
fmt.Fprint(&buf, out)
|
|||
|
fmt.Fprintln(&buf)
|
|||
|
}
|
|||
|
}
|
|||
|
if inCode {
|
|||
|
fmt.Fprintln(&buf, "```")
|
|||
|
}
|
|||
|
|
|||
|
return buf.String()
|
|||
|
}
|
|||
|
|
|||
|
func (o *sarifFormatter) Format(checks []*lint.Analyzer, diagnostics []diagnostic) {
|
|||
|
// TODO(dh): some diagnostics shouldn't be reported as results. For example, when the user specifies a package on the command line that doesn't exist.
|
|||
|
|
|||
|
cwd, _ := os.Getwd()
|
|||
|
run := sarif.Run{
|
|||
|
Tool: sarif.Tool{
|
|||
|
Driver: sarif.ToolComponent{
|
|||
|
Name: o.driverName,
|
|||
|
Version: o.driverVersion,
|
|||
|
InformationURI: o.driverWebsite,
|
|||
|
},
|
|||
|
},
|
|||
|
Invocations: []sarif.Invocation{{
|
|||
|
Arguments: os.Args[1:],
|
|||
|
WorkingDirectory: sarif.ArtifactLocation{
|
|||
|
URI: sarifURI(cwd),
|
|||
|
},
|
|||
|
ExecutionSuccessful: true,
|
|||
|
}},
|
|||
|
}
|
|||
|
for _, c := range checks {
|
|||
|
run.Tool.Driver.Rules = append(run.Tool.Driver.Rules,
|
|||
|
sarif.ReportingDescriptor{
|
|||
|
// We don't set Name, as Name and ID mustn't be identical.
|
|||
|
ID: c.Analyzer.Name,
|
|||
|
ShortDescription: sarif.Message{
|
|||
|
Text: c.Doc.Title,
|
|||
|
Markdown: c.Doc.TitleMarkdown,
|
|||
|
},
|
|||
|
HelpURI: "https://staticcheck.io/docs/checks#" + c.Analyzer.Name,
|
|||
|
// We use our markdown as the plain text version, too. We
|
|||
|
// use very little markdown, primarily quotations,
|
|||
|
// indented code blocks and backticks. All of these are
|
|||
|
// fine as plain text, too.
|
|||
|
Help: sarif.Message{
|
|||
|
Text: sarifFormatText(c.Doc.Format(false)),
|
|||
|
Markdown: sarifFormatText(c.Doc.FormatMarkdown(false)),
|
|||
|
},
|
|||
|
DefaultConfiguration: sarif.ReportingConfiguration{
|
|||
|
// TODO(dh): we could figure out which checks were disabled globally
|
|||
|
Enabled: true,
|
|||
|
Level: sarifLevel(c.Doc.Severity),
|
|||
|
},
|
|||
|
})
|
|||
|
}
|
|||
|
|
|||
|
for _, p := range diagnostics {
|
|||
|
r := sarif.Result{
|
|||
|
RuleID: p.Category,
|
|||
|
Kind: sarif.Fail,
|
|||
|
Message: sarif.Message{
|
|||
|
Text: p.Message,
|
|||
|
},
|
|||
|
}
|
|||
|
r.Locations = []sarif.Location{{
|
|||
|
PhysicalLocation: sarif.PhysicalLocation{
|
|||
|
ArtifactLocation: sarifArtifactLocation(p.Position.Filename),
|
|||
|
Region: sarif.Region{
|
|||
|
StartLine: p.Position.Line,
|
|||
|
StartColumn: p.Position.Column,
|
|||
|
EndLine: p.End.Line,
|
|||
|
EndColumn: p.End.Column,
|
|||
|
},
|
|||
|
},
|
|||
|
}}
|
|||
|
for _, fix := range p.SuggestedFixes {
|
|||
|
sfix := sarif.Fix{
|
|||
|
Description: sarif.Message{
|
|||
|
Text: fix.Message,
|
|||
|
},
|
|||
|
}
|
|||
|
// file name -> replacements
|
|||
|
changes := map[string][]sarif.Replacement{}
|
|||
|
for _, edit := range fix.TextEdits {
|
|||
|
changes[edit.Position.Filename] = append(changes[edit.Position.Filename], sarif.Replacement{
|
|||
|
DeletedRegion: sarif.Region{
|
|||
|
StartLine: edit.Position.Line,
|
|||
|
StartColumn: edit.Position.Column,
|
|||
|
EndLine: edit.End.Line,
|
|||
|
EndColumn: edit.End.Column,
|
|||
|
},
|
|||
|
InsertedContent: sarif.ArtifactContent{
|
|||
|
Text: string(edit.NewText),
|
|||
|
},
|
|||
|
})
|
|||
|
}
|
|||
|
for path, replacements := range changes {
|
|||
|
sfix.ArtifactChanges = append(sfix.ArtifactChanges, sarif.ArtifactChange{
|
|||
|
ArtifactLocation: sarifArtifactLocation(path),
|
|||
|
Replacements: replacements,
|
|||
|
})
|
|||
|
}
|
|||
|
r.Fixes = append(r.Fixes, sfix)
|
|||
|
}
|
|||
|
for i, related := range p.Related {
|
|||
|
r.Message.Text += fmt.Sprintf("\n\t[%s](%d)", related.Message, i+1)
|
|||
|
|
|||
|
r.RelatedLocations = append(r.RelatedLocations,
|
|||
|
sarif.Location{
|
|||
|
ID: i + 1,
|
|||
|
Message: &sarif.Message{
|
|||
|
Text: related.Message,
|
|||
|
},
|
|||
|
PhysicalLocation: sarif.PhysicalLocation{
|
|||
|
ArtifactLocation: sarifArtifactLocation(related.Position.Filename),
|
|||
|
Region: sarif.Region{
|
|||
|
StartLine: related.Position.Line,
|
|||
|
StartColumn: related.Position.Column,
|
|||
|
EndLine: related.End.Line,
|
|||
|
EndColumn: related.End.Column,
|
|||
|
},
|
|||
|
},
|
|||
|
})
|
|||
|
}
|
|||
|
|
|||
|
if p.Severity == severityIgnored {
|
|||
|
// Note that GitHub does not support suppressions, which is why Staticcheck still requires the -show-ignored flag to be set for us to emit ignored diagnostics.
|
|||
|
|
|||
|
r.Suppressions = []sarif.Suppression{{
|
|||
|
Kind: "inSource",
|
|||
|
// TODO(dh): populate the Justification field
|
|||
|
}}
|
|||
|
} else {
|
|||
|
// We want an empty slice, not nil. SARIF differentiates
|
|||
|
// between the two. An empty slice means that the diagnostic
|
|||
|
// wasn't suppressed, while nil means that we don't have the
|
|||
|
// information available.
|
|||
|
r.Suppressions = []sarif.Suppression{}
|
|||
|
}
|
|||
|
run.Results = append(run.Results, r)
|
|||
|
}
|
|||
|
|
|||
|
json.NewEncoder(os.Stdout).Encode(sarif.Log{
|
|||
|
Version: sarif.Version,
|
|||
|
Schema: sarif.Schema,
|
|||
|
Runs: []sarif.Run{run},
|
|||
|
})
|
|||
|
}
|