kilo/vendor/honnef.co/go/tools/lintcmd/sarif.go
Lucas Servén Marín 50fbc2eec2
staticcheck (#313)
* CI: use staticcheck for linting

This commit switches the linter for Go code from golint to staticcheck.
Golint has been deprecated since last year and staticcheck is a
recommended replacement.

Signed-off-by: Lucas Servén Marín <lserven@gmail.com>

* revendor

Signed-off-by: Lucas Servén Marín <lserven@gmail.com>

* cmd,pkg: fix lint warnings

Signed-off-by: Lucas Servén Marín <lserven@gmail.com>
2022-05-19 19:45:43 +02:00

371 lines
10 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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},
})
}