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