diff --git a/pkg/golinters/goanalysis/issue.go b/pkg/golinters/goanalysis/issue.go index f331a3ab9f1a..34adc7bac4fa 100644 --- a/pkg/golinters/goanalysis/issue.go +++ b/pkg/golinters/goanalysis/issue.go @@ -23,6 +23,7 @@ func NewIssue(i *result.Issue, pass *analysis.Pass) Issue { type EncodingIssue struct { FromLinter string Text string + SuggestedFixes []result.SuggestedFix Pos token.Position LineRange *result.Range Replacement *result.Replacement diff --git a/pkg/golinters/goanalysis/runners.go b/pkg/golinters/goanalysis/runners.go index 7e4cf902e79c..bbb1410d16a9 100644 --- a/pkg/golinters/goanalysis/runners.go +++ b/pkg/golinters/goanalysis/runners.go @@ -98,10 +98,11 @@ func buildIssues(diags []Diagnostic, linterNameBuilder func(diag *Diagnostic) st } issues = append(issues, result.Issue{ - FromLinter: linterName, - Text: text, - Pos: diag.Position, - Pkg: diag.Pkg, + FromLinter: linterName, + Text: text, + SuggestedFixes: result.BuildSuggestedFixes(diag.SuggestedFixes), + Pos: diag.Position, + Pkg: diag.Pkg, }) if len(diag.Related) > 0 { @@ -150,6 +151,7 @@ func saveIssuesToCache(allPkgs []*packages.Package, pkgsFromCache map[*packages. encodedIssues = append(encodedIssues, EncodingIssue{ FromLinter: i.FromLinter, Text: i.Text, + SuggestedFixes: i.SuggestedFixes, Pos: i.Pos, LineRange: i.LineRange, Replacement: i.Replacement, @@ -221,6 +223,7 @@ func loadIssuesFromCache(pkgs []*packages.Package, lintCtx *linter.Context, issues = append(issues, result.Issue{ FromLinter: i.FromLinter, Text: i.Text, + SuggestedFixes: i.SuggestedFixes, Pos: i.Pos, LineRange: i.LineRange, Replacement: i.Replacement, diff --git a/pkg/printers/codeclimate.go b/pkg/printers/codeclimate.go index 35a22ce99a72..afeca12334a2 100644 --- a/pkg/printers/codeclimate.go +++ b/pkg/printers/codeclimate.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "fmt" + "strings" "github.com/golangci/golangci-lint/pkg/logutils" "github.com/golangci/golangci-lint/pkg/result" @@ -13,6 +14,7 @@ import ( // It is just enough to support GitLab CI Code Quality - https://docs.gitlab.com/ee/user/project/merge_requests/code_quality.html type CodeClimateIssue struct { Description string `json:"description"` + Content string `json:"content,omitempty"` Severity string `json:"severity,omitempty"` Fingerprint string `json:"fingerprint"` Location struct { @@ -40,6 +42,11 @@ func (p CodeClimate) Print(ctx context.Context, issues []result.Issue) error { codeClimateIssue.Location.Lines.Begin = issue.Pos.Line codeClimateIssue.Fingerprint = issue.Fingerprint() + content := p.buildContentString(&issues[i]) + if content != "" { + codeClimateIssue.Content = content + } + if issue.Severity != "" { codeClimateIssue.Severity = issue.Severity } @@ -55,3 +62,23 @@ func (p CodeClimate) Print(ctx context.Context, issues []result.Issue) error { fmt.Fprint(logutils.StdOut, string(outputJSON)) return nil } + +func (p CodeClimate) buildContentString(issue *result.Issue) string { + if len(issue.SuggestedFixes) == 0 { + return "" + } + + var text string + for _, fix := range issue.SuggestedFixes { + text += fmt.Sprintf("%s\n", strings.TrimSpace(fix.Message)) + var suggestedEdits []string + for _, textEdit := range fix.TextEdits { + suggestedEdits = append(suggestedEdits, strings.TrimSpace(textEdit.NewText)) + } + if len(suggestedEdits) > 0 { + text += "```\n" + strings.Join(suggestedEdits, "\n") + "\n" + "```\n" + } + } + + return text +} diff --git a/pkg/printers/html.go b/pkg/printers/html.go index 65ab753bd512..e4f18eee2c59 100644 --- a/pkg/printers/html.go +++ b/pkg/printers/html.go @@ -59,6 +59,25 @@ const templateContent = ` } } + class SuggestedEdit extends React.Component { + render() { + if (this.props.data.SuggestedFix && this.props.data.SuggestedFix.length) { + return ( +
+
+ Suggested Edits +
+
+ +
+
+ ) + } + + return null + } + } + class Issue extends React.Component { render() { return ( @@ -77,6 +96,7 @@ const templateContent = `
+ ); } @@ -117,10 +137,11 @@ const templateContent = ` ` type htmlIssue struct { - Title string - Pos string - Linter string - Code string + Title string + Pos string + SuggestedFix string + Linter string + Code string } type HTML struct{} @@ -139,10 +160,11 @@ func (h HTML) Print(_ context.Context, issues []result.Issue) error { } htmlIssues = append(htmlIssues, htmlIssue{ - Title: strings.TrimSpace(issues[i].Text), - Pos: pos, - Linter: issues[i].FromLinter, - Code: strings.Join(issues[i].SourceLines, "\n"), + Title: strings.TrimSpace(issues[i].Text), + Pos: pos, + SuggestedFix: h.getSuggestedFix(&issues[i]), + Linter: issues[i].FromLinter, + Code: strings.Join(issues[i].SourceLines, "\n"), }) } @@ -153,3 +175,19 @@ func (h HTML) Print(_ context.Context, issues []result.Issue) error { return t.Execute(logutils.StdOut, struct{ Issues []htmlIssue }{Issues: htmlIssues}) } + +func (h HTML) getSuggestedFix(i *result.Issue) string { + var text string + if len(i.SuggestedFixes) > 0 { + for _, fix := range i.SuggestedFixes { + text += fmt.Sprintf("%s\n", strings.TrimSpace(fix.Message)) + var suggestedEdits []string + for _, textEdit := range fix.TextEdits { + suggestedEdits = append(suggestedEdits, strings.TrimSpace(textEdit.NewText)) + } + text += strings.Join(suggestedEdits, "\n") + "\n" + } + } + + return text +} diff --git a/pkg/printers/junitxml.go b/pkg/printers/junitxml.go index 9277cd66f2fe..44aa50d6a92b 100644 --- a/pkg/printers/junitxml.go +++ b/pkg/printers/junitxml.go @@ -3,6 +3,7 @@ package printers import ( "context" "encoding/xml" + "fmt" "strings" "github.com/golangci/golangci-lint/pkg/logutils" @@ -41,7 +42,7 @@ func NewJunitXML() *JunitXML { return &JunitXML{} } -func (JunitXML) Print(ctx context.Context, issues []result.Issue) error { +func (j JunitXML) Print(ctx context.Context, issues []result.Issue) error { suites := make(map[string]testSuiteXML) // use a map to group by file for ind := range issues { @@ -52,12 +53,14 @@ func (JunitXML) Print(ctx context.Context, issues []result.Issue) error { testSuite.Tests++ testSuite.Failures++ + content := strings.Join(i.SourceLines, "\n") + content += j.getSuggestedFix(&issues[ind]) tc := testCaseXML{ Name: i.FromLinter, ClassName: i.Pos.String(), Failure: failureXML{ Message: i.Text, - Content: strings.Join(i.SourceLines, "\n"), + Content: content, }, } @@ -77,3 +80,23 @@ func (JunitXML) Print(ctx context.Context, issues []result.Issue) error { } return nil } + +func (j JunitXML) getSuggestedFix(i *result.Issue) string { + var text string + if len(i.SuggestedFixes) > 0 { + for _, fix := range i.SuggestedFixes { + text += fmt.Sprintf("%s\n", strings.TrimSpace(fix.Message)) + var suggestedEdits []string + for _, textEdit := range fix.TextEdits { + suggestedEdits = append(suggestedEdits, strings.TrimSpace(textEdit.NewText)) + } + text += strings.Join(suggestedEdits, "\n") + "\n" + } + } + + if text != "" { + return fmt.Sprintf("\n\n%s", text) + } + + return "" +} diff --git a/pkg/printers/tab.go b/pkg/printers/tab.go index d3cdce673dd8..d789d69cf32d 100644 --- a/pkg/printers/tab.go +++ b/pkg/printers/tab.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "io" + "strings" "text/tabwriter" "github.com/fatih/color" @@ -34,6 +35,7 @@ func (p *Tab) Print(ctx context.Context, issues []result.Issue) error { for i := range issues { p.printIssue(&issues[i], w) + p.printSuggestedEdits(&issues[i], w) } if err := w.Flush(); err != nil { @@ -56,3 +58,19 @@ func (p Tab) printIssue(i *result.Issue, w io.Writer) { fmt.Fprintf(w, "%s\t%s\n", pos, text) } + +func (p Tab) printSuggestedEdits(i *result.Issue, w io.Writer) { + var text string + if len(i.SuggestedFixes) > 0 { + for _, fix := range i.SuggestedFixes { + text += p.SprintfColored(color.FgRed, "%s\n", strings.TrimSpace(fix.Message)) + var suggestedEdits []string + for _, textEdit := range fix.TextEdits { + suggestedEdits = append(suggestedEdits, strings.TrimSpace(textEdit.NewText)) + } + text += strings.Join(suggestedEdits, "\n") + "\n" + } + } + + fmt.Fprintln(w, text) +} diff --git a/pkg/printers/text.go b/pkg/printers/text.go index 1814528884c4..cf1a7e111bb2 100644 --- a/pkg/printers/text.go +++ b/pkg/printers/text.go @@ -47,6 +47,7 @@ func (p *Text) Print(ctx context.Context, issues []result.Issue) error { p.printSourceCode(&issues[i]) p.printUnderLinePointer(&issues[i]) + p.printSuggestedEdits(&issues[i]) } return nil @@ -89,3 +90,19 @@ func (p Text) printUnderLinePointer(i *result.Issue) { fmt.Fprintf(logutils.StdOut, "%s%s\n", string(prefixRunes), p.SprintfColored(color.FgYellow, "^")) } + +func (p Text) printSuggestedEdits(i *result.Issue) { + var text string + if len(i.SuggestedFixes) > 0 { + for _, fix := range i.SuggestedFixes { + text += p.SprintfColored(color.FgRed, "%s\n", strings.TrimSpace(fix.Message)) + var suggestedEdits []string + for _, textEdit := range fix.TextEdits { + suggestedEdits = append(suggestedEdits, strings.TrimSpace(textEdit.NewText)) + } + text += strings.Join(suggestedEdits, "\n") + "\n" + } + } + + fmt.Fprintln(logutils.StdOut, text) +} diff --git a/pkg/result/issue.go b/pkg/result/issue.go index 707a2b17cd95..9e6332ad24a1 100644 --- a/pkg/result/issue.go +++ b/pkg/result/issue.go @@ -5,6 +5,7 @@ import ( "fmt" "go/token" + "golang.org/x/tools/go/analysis" "golang.org/x/tools/go/packages" ) @@ -24,9 +25,47 @@ type InlineFix struct { NewString string } +type SuggestedFix struct { + // A description for this suggested fix to be shown to a user deciding + // whether to accept it. + Message string + TextEdits []TextEdit +} + +type TextEdit struct { + Pos token.Pos + End token.Pos + NewText string +} + +func BuildSuggestedFixes(fixes []analysis.SuggestedFix) []SuggestedFix { + if len(fixes) == 0 { + return nil + } + + suggestedFixes := make([]SuggestedFix, 0, len(fixes)) + for _, fix := range fixes { + textEdits := make([]TextEdit, 0, len(fix.TextEdits)) + for _, edit := range fix.TextEdits { + textEdits = append(textEdits, TextEdit{ + Pos: edit.Pos, + End: edit.End, + NewText: string(edit.NewText), + }) + } + suggestedFixes = append(suggestedFixes, SuggestedFix{ + Message: fix.Message, + TextEdits: textEdits, + }) + } + + return suggestedFixes +} + type Issue struct { - FromLinter string - Text string + FromLinter string + Text string + SuggestedFixes []SuggestedFix Severity string