pretty printing issues

This commit is contained in:
golangci 2018-05-08 11:54:30 +03:00
parent 073ad51ed9
commit 5dc876c260
29 changed files with 226 additions and 112 deletions

View File

@ -2,22 +2,19 @@ package commands
import ( import (
"context" "context"
"encoding/json"
"fmt" "fmt"
"go/build" "go/build"
"log" "log"
"os"
"strings" "strings"
"syscall"
"time" "time"
"github.com/fatih/color"
"github.com/golangci/go-tools/ssa" "github.com/golangci/go-tools/ssa"
"github.com/golangci/go-tools/ssa/ssautil" "github.com/golangci/go-tools/ssa/ssautil"
"github.com/golangci/golangci-lint/pkg" "github.com/golangci/golangci-lint/pkg"
"github.com/golangci/golangci-lint/pkg/config" "github.com/golangci/golangci-lint/pkg/config"
"github.com/golangci/golangci-lint/pkg/fsutils" "github.com/golangci/golangci-lint/pkg/fsutils"
"github.com/golangci/golangci-lint/pkg/golinters" "github.com/golangci/golangci-lint/pkg/golinters"
"github.com/golangci/golangci-lint/pkg/printers"
"github.com/golangci/golangci-lint/pkg/result" "github.com/golangci/golangci-lint/pkg/result"
"github.com/golangci/golangci-lint/pkg/result/processors" "github.com/golangci/golangci-lint/pkg/result/processors"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
@ -39,6 +36,8 @@ func (e *Executor) initRun() {
runCmd.Flags().StringVar(&rc.OutFormat, "out-format", runCmd.Flags().StringVar(&rc.OutFormat, "out-format",
config.OutFormatColoredLineNumber, config.OutFormatColoredLineNumber,
fmt.Sprintf("Format of output: %s", strings.Join(config.OutFormats, "|"))) fmt.Sprintf("Format of output: %s", strings.Join(config.OutFormats, "|")))
runCmd.Flags().BoolVar(&rc.PrintIssuedLine, "print-issued-lines", true, "Print lines of code with issue")
runCmd.Flags().IntVar(&rc.ExitCodeIfIssuesFound, "issues-exit-code", runCmd.Flags().IntVar(&rc.ExitCodeIfIssuesFound, "issues-exit-code",
1, "Exit code when issues were found") 1, "Exit code when issues were found")
runCmd.Flags().StringSliceVar(&rc.BuildTags, "build-tags", []string{}, "Build tags (not all linters support them)") runCmd.Flags().StringSliceVar(&rc.BuildTags, "build-tags", []string{}, "Build tags (not all linters support them)")
@ -227,9 +226,15 @@ func (e *Executor) executeRun(cmd *cobra.Command, args []string) {
return err return err
} }
gotAnyIssues, err := outputIssues(e.cfg.Run.OutFormat, issues) var p printers.Printer
if e.cfg.Run.OutFormat == config.OutFormatJSON {
p = printers.NewJSON()
} else {
p = printers.NewText(e.cfg.Run.PrintIssuedLine, e.cfg.Run.OutFormat == config.OutFormatColoredLineNumber)
}
gotAnyIssues, err := p.Print(issues)
if err != nil { if err != nil {
return fmt.Errorf("can't output %d issues: %s", len(issues), err) return fmt.Errorf("can't print %d issues: %s", len(issues), err)
} }
if gotAnyIssues { if gotAnyIssues {
@ -247,43 +252,3 @@ func (e *Executor) executeRun(cmd *cobra.Command, args []string) {
} }
} }
} }
func outputIssues(format string, issues chan result.Issue) (bool, error) {
stdout := os.NewFile(uintptr(syscall.Stdout), "/dev/stdout") // was set to /dev/null
if format == config.OutFormatLineNumber || format == config.OutFormatColoredLineNumber {
gotAnyIssue := false
for i := range issues {
gotAnyIssue = true
text := i.Text
if format == config.OutFormatColoredLineNumber {
text = color.RedString(text)
}
fmt.Fprintf(stdout, "%s:%d: %s\n", i.File, i.LineNumber, text)
}
if !gotAnyIssue {
outStr := "Congrats! No issues were found."
if format == config.OutFormatColoredLineNumber {
outStr = color.GreenString(outStr)
}
fmt.Fprintln(stdout, outStr)
}
return gotAnyIssue, nil
}
if format == config.OutFormatJSON {
var allIssues []result.Issue
for i := range issues {
allIssues = append(allIssues, i)
}
outputJSON, err := json.Marshal(allIssues)
if err != nil {
return false, err
}
fmt.Fprint(stdout, string(outputJSON))
return len(allIssues) != 0, nil
}
return false, fmt.Errorf("unknown output format %q", format)
}

View File

@ -28,6 +28,8 @@ type Run struct {
BuildTags []string BuildTags []string
OutFormat string OutFormat string
PrintIssuedLine bool
ExitCodeIfIssuesFound int ExitCodeIfIssuesFound int
Errcheck struct { Errcheck struct {

View File

@ -23,8 +23,7 @@ func (d Deadcode) Run(ctx context.Context, lintCtx *Context) ([]result.Issue, er
var res []result.Issue var res []result.Issue
for _, i := range issues { for _, i := range issues {
res = append(res, result.Issue{ res = append(res, result.Issue{
File: i.Pos.Filename, Pos: i.Pos,
LineNumber: i.Pos.Line,
Text: fmt.Sprintf("%s is unused", formatCode(i.UnusedIdentName, lintCtx.RunCfg())), Text: fmt.Sprintf("%s is unused", formatCode(i.UnusedIdentName, lintCtx.RunCfg())),
FromLinter: d.Name(), FromLinter: d.Name(),
}) })

View File

@ -3,6 +3,7 @@ package golinters
import ( import (
"context" "context"
"fmt" "fmt"
"go/token"
"github.com/golangci/golangci-lint/pkg/result" "github.com/golangci/golangci-lint/pkg/result"
duplAPI "github.com/mibk/dupl" duplAPI "github.com/mibk/dupl"
@ -27,8 +28,14 @@ func (d Dupl) Run(ctx context.Context, lintCtx *Context) ([]result.Issue, error)
i.From.LineStart(), i.From.LineEnd(), i.From.LineStart(), i.From.LineEnd(),
formatCode(dupl, lintCtx.RunCfg())) formatCode(dupl, lintCtx.RunCfg()))
res = append(res, result.Issue{ res = append(res, result.Issue{
File: i.From.Filename(), Pos: token.Position{
LineNumber: i.From.LineStart(), Filename: i.From.Filename(),
Line: i.From.LineStart(),
},
LineRange: result.Range{
From: i.From.LineStart(),
To: i.From.LineEnd(),
},
Text: text, Text: text,
FromLinter: d.Name(), FromLinter: d.Name(),
}) })

View File

@ -37,8 +37,7 @@ func (e Errcheck) Run(ctx context.Context, lintCtx *Context) ([]result.Issue, er
res = append(res, result.Issue{ res = append(res, result.Issue{
FromLinter: e.Name(), FromLinter: e.Name(),
Text: text, Text: text,
LineNumber: i.Pos.Line, Pos: i.Pos,
File: i.Pos.Filename,
}) })
} }

View File

@ -3,6 +3,7 @@ package golinters
import ( import (
"context" "context"
"fmt" "fmt"
"go/token"
"io/ioutil" "io/ioutil"
"log" "log"
"strconv" "strconv"
@ -33,8 +34,10 @@ func (lint Gas) Run(ctx context.Context, lintCtx *Context) ([]result.Issue, erro
text := fmt.Sprintf("%s: %s", i.RuleID, i.What) // TODO: use severity and confidence text := fmt.Sprintf("%s: %s", i.RuleID, i.What) // TODO: use severity and confidence
line, _ := strconv.Atoi(i.Line) line, _ := strconv.Atoi(i.Line)
res = append(res, result.Issue{ res = append(res, result.Issue{
File: i.File, Pos: token.Position{
LineNumber: line, Filename: i.File,
Line: line,
},
Text: text, Text: text,
FromLinter: lint.Name(), FromLinter: lint.Name(),
}) })

View File

@ -33,8 +33,7 @@ func (lint Goconst) Run(ctx context.Context, lintCtx *Context) ([]result.Issue,
textEnd = fmt.Sprintf(", but such constant %s already exists", formatCode(i.MatchingConst, lintCtx.RunCfg())) textEnd = fmt.Sprintf(", but such constant %s already exists", formatCode(i.MatchingConst, lintCtx.RunCfg()))
} }
res = append(res, result.Issue{ res = append(res, result.Issue{
File: i.Pos.Filename, Pos: i.Pos,
LineNumber: i.Pos.Line,
Text: textBegin + textEnd, Text: textBegin + textEnd,
FromLinter: lint.Name(), FromLinter: lint.Name(),
}) })

View File

@ -24,8 +24,7 @@ func (g Gocyclo) Run(ctx context.Context, lintCtx *Context) ([]result.Issue, err
} }
res = append(res, result.Issue{ res = append(res, result.Issue{
File: s.Pos.Filename, Pos: s.Pos,
LineNumber: s.Pos.Line,
Text: fmt.Sprintf("cyclomatic complexity %d of func %s is high (> %d)", Text: fmt.Sprintf("cyclomatic complexity %d of func %s is high (> %d)",
s.Complexity, formatCode(s.FuncName, lintCtx.RunCfg()), lintCtx.RunCfg().Gocyclo.MinComplexity), s.Complexity, formatCode(s.FuncName, lintCtx.RunCfg()), lintCtx.RunCfg().Gocyclo.MinComplexity),
FromLinter: g.Name(), FromLinter: g.Name(),

View File

@ -4,6 +4,7 @@ import (
"bytes" "bytes"
"context" "context"
"fmt" "fmt"
"go/token"
gofmtAPI "github.com/golangci/gofmt/gofmt" gofmtAPI "github.com/golangci/gofmt/gofmt"
goimportsAPI "github.com/golangci/gofmt/goimports" goimportsAPI "github.com/golangci/gofmt/goimports"
@ -79,8 +80,10 @@ func (g Gofmt) extractIssuesFromPatch(patch string) ([]result.Issue, error) {
} }
i := result.Issue{ i := result.Issue{
FromLinter: g.Name(), FromLinter: g.Name(),
File: d.NewName, Pos: token.Position{
LineNumber: deletedLine, Filename: d.NewName,
Line: deletedLine,
},
Text: text, Text: text,
} }
issues = append(issues, i) issues = append(issues, i)

View File

@ -89,8 +89,7 @@ func lintFiles(minConfidence float64, filenames ...string) ([]result.Issue, erro
for _, p := range ps { for _, p := range ps {
if p.Confidence >= minConfidence { if p.Confidence >= minConfidence {
issues = append(issues, result.Issue{ issues = append(issues, result.Issue{
File: p.Position.Filename, Pos: p.Position,
LineNumber: p.Position.Line,
Text: p.Text, Text: p.Text,
}) })
// TODO: use p.Link and p.Category // TODO: use p.Link and p.Category

View File

@ -22,8 +22,7 @@ func (g Govet) Run(ctx context.Context, lintCtx *Context) ([]result.Issue, error
var res []result.Issue var res []result.Issue
for _, i := range issues { for _, i := range issues {
res = append(res, result.Issue{ res = append(res, result.Issue{
File: i.Pos.Filename, Pos: i.Pos,
LineNumber: i.Pos.Line,
Text: i.Message, Text: i.Message,
FromLinter: g.Name(), FromLinter: g.Name(),
}) })

View File

@ -20,8 +20,7 @@ func (lint Ineffassign) Run(ctx context.Context, lintCtx *Context) ([]result.Iss
var res []result.Issue var res []result.Issue
for _, i := range issues { for _, i := range issues {
res = append(res, result.Issue{ res = append(res, result.Issue{
File: i.Pos.Filename, Pos: i.Pos,
LineNumber: i.Pos.Line,
Text: fmt.Sprintf("ineffectual assignment to %s", formatCode(i.IdentName, lintCtx.RunCfg())), Text: fmt.Sprintf("ineffectual assignment to %s", formatCode(i.IdentName, lintCtx.RunCfg())),
FromLinter: lint.Name(), FromLinter: lint.Name(),
}) })

View File

@ -28,8 +28,7 @@ func (lint Interfacer) Run(ctx context.Context, lintCtx *Context) ([]result.Issu
for _, i := range issues { for _, i := range issues {
pos := lintCtx.SSAProgram.Fset.Position(i.Pos()) pos := lintCtx.SSAProgram.Fset.Position(i.Pos())
res = append(res, result.Issue{ res = append(res, result.Issue{
File: pos.Filename, Pos: pos,
LineNumber: pos.Line,
Text: i.Message(), Text: i.Message(),
FromLinter: lint.Name(), FromLinter: lint.Name(),
}) })

View File

@ -24,8 +24,7 @@ func (m Maligned) Run(ctx context.Context, lintCtx *Context) ([]result.Issue, er
text += fmt.Sprintf(":\n%s", formatCodeBlock(i.NewStructDef, lintCtx.RunCfg())) text += fmt.Sprintf(":\n%s", formatCodeBlock(i.NewStructDef, lintCtx.RunCfg()))
} }
res = append(res, result.Issue{ res = append(res, result.Issue{
File: i.Pos.Filename, Pos: i.Pos,
LineNumber: i.Pos.Line,
Text: text, Text: text,
FromLinter: m.Name(), FromLinter: m.Name(),
}) })

View File

@ -20,8 +20,7 @@ func (m Megacheck) Run(ctx context.Context, lintCtx *Context) ([]result.Issue, e
var res []result.Issue var res []result.Issue
for _, i := range issues { for _, i := range issues {
res = append(res, result.Issue{ res = append(res, result.Issue{
File: i.Position.Filename, Pos: i.Position,
LineNumber: i.Position.Line,
Text: i.Text, Text: i.Text,
FromLinter: m.Name(), FromLinter: m.Name(),
}) })

View File

@ -20,8 +20,7 @@ func (s Structcheck) Run(ctx context.Context, lintCtx *Context) ([]result.Issue,
var res []result.Issue var res []result.Issue
for _, i := range issues { for _, i := range issues {
res = append(res, result.Issue{ res = append(res, result.Issue{
File: i.Pos.Filename, Pos: i.Pos,
LineNumber: i.Pos.Line,
Text: fmt.Sprintf("%s is unused", formatCode(i.FieldName, lintCtx.RunCfg())), Text: fmt.Sprintf("%s is unused", formatCode(i.FieldName, lintCtx.RunCfg())),
FromLinter: s.Name(), FromLinter: s.Name(),
}) })

View File

@ -18,8 +18,7 @@ func (lint Unconvert) Run(ctx context.Context, lintCtx *Context) ([]result.Issue
var res []result.Issue var res []result.Issue
for _, pos := range positions { for _, pos := range positions {
res = append(res, result.Issue{ res = append(res, result.Issue{
File: pos.Filename, Pos: pos,
LineNumber: pos.Line,
Text: "unnecessary conversion", Text: "unnecessary conversion",
FromLinter: lint.Name(), FromLinter: lint.Name(),
}) })

View File

@ -20,8 +20,7 @@ func (v Varcheck) Run(ctx context.Context, lintCtx *Context) ([]result.Issue, er
var res []result.Issue var res []result.Issue
for _, i := range issues { for _, i := range issues {
res = append(res, result.Issue{ res = append(res, result.Issue{
File: i.Pos.Filename, Pos: i.Pos,
LineNumber: i.Pos.Line,
Text: fmt.Sprintf("%s is unused", formatCode(i.VarName, lintCtx.RunCfg())), Text: fmt.Sprintf("%s is unused", formatCode(i.VarName, lintCtx.RunCfg())),
FromLinter: v.Name(), FromLinter: v.Name(),
}) })

27
pkg/printers/json.go Normal file
View File

@ -0,0 +1,27 @@
package printers
import (
"encoding/json"
"fmt"
"github.com/golangci/golangci-lint/pkg/result"
)
type JSON struct{}
func NewJSON() *JSON {
return &JSON{}
}
func (JSON) Print(issues chan result.Issue) (bool, error) {
var allIssues []result.Issue
for i := range issues {
allIssues = append(allIssues, i)
}
outputJSON, err := json.Marshal(allIssues)
if err != nil {
return false, err
}
fmt.Fprint(getOutWriter(), string(outputJSON))
return len(allIssues) != 0, nil
}

7
pkg/printers/printer.go Normal file
View File

@ -0,0 +1,7 @@
package printers
import "github.com/golangci/golangci-lint/pkg/result"
type Printer interface {
Print(issues chan result.Issue) (bool, error)
}

89
pkg/printers/text.go Normal file
View File

@ -0,0 +1,89 @@
package printers
import (
"bytes"
"fmt"
"io/ioutil"
"time"
"github.com/fatih/color"
"github.com/golangci/golangci-lint/pkg/result"
"github.com/sirupsen/logrus"
)
type Text struct {
printIssuedLine bool
useColors bool
}
func NewText(printIssuedLine bool, useColors bool) *Text {
return &Text{
printIssuedLine: printIssuedLine,
useColors: useColors,
}
}
type linesCache [][]byte
type filesCache map[string]linesCache
func (p Text) SprintfColored(ca color.Attribute, format string, args ...interface{}) string {
if !p.useColors {
return fmt.Sprintf(format, args...)
}
c := color.New(ca)
return c.Sprintf(format, args...)
}
func (p Text) Print(issues chan result.Issue) (bool, error) {
var issuedLineExtractingDuration time.Duration
defer func() {
logrus.Infof("Extracting issued lines took %s", issuedLineExtractingDuration)
}()
gotAnyIssue := false
cache := filesCache{}
out := getOutWriter()
for i := range issues {
gotAnyIssue = true
text := p.SprintfColored(color.FgRed, "%s", i.Text)
pos := p.SprintfColored(color.Bold, "%s:%d", i.FilePath(), i.Line())
fmt.Fprintf(out, "%s: %s\n", pos, text)
if !p.printIssuedLine {
continue
}
fc := cache[i.FilePath()]
if fc == nil {
startedAt := time.Now()
// TODO: make more optimal algorithm: don't load all files into memory
fileBytes, err := ioutil.ReadFile(i.FilePath())
if err != nil {
return false, fmt.Errorf("can't read file %s for printing issued line: %s", i.FilePath(), err)
}
lines := bytes.Split(fileBytes, []byte("\n")) // TODO: what about \r\n?
fc = lines
cache[i.FilePath()] = fc
issuedLineExtractingDuration += time.Since(startedAt)
}
lineRange := i.GetLineRange()
for line := lineRange.From; line <= lineRange.To; line++ {
zeroIndexedLine := line - 1
if zeroIndexedLine >= len(fc) {
logrus.Warnf("No line %d in file %s", line, i.FilePath())
break
}
fmt.Fprintln(out, string(bytes.Trim(fc[zeroIndexedLine], "\r")))
}
}
if !gotAnyIssue {
outStr := p.SprintfColored(color.FgGreen, "Congrats! No issues were found.")
fmt.Fprintln(out, outStr)
}
return gotAnyIssue, nil
}

11
pkg/printers/utils.go Normal file
View File

@ -0,0 +1,11 @@
package printers
import (
"io"
"os"
"syscall"
)
func getOutWriter() io.Writer {
return os.NewFile(uintptr(syscall.Stdout), "/dev/stdout") // was set to /dev/null
}

View File

@ -1,27 +1,35 @@
package result package result
import "go/token"
type Range struct {
From, To int
}
type Issue struct { type Issue struct {
FromLinter string FromLinter string
Text string Text string
File string
LineNumber int Pos token.Position
LineRange Range
HunkPos int HunkPos int
} }
func (i Issue) FilePath() string { func (i Issue) FilePath() string {
return i.File return i.Pos.Filename
} }
func (i Issue) Line() int { func (i Issue) Line() int {
return i.LineNumber return i.Pos.Line
} }
func NewIssue(fromLinter, text, file string, lineNumber, hunkPos int) Issue { func (i Issue) GetLineRange() Range {
return Issue{ if i.LineRange.From == 0 {
FromLinter: fromLinter, return Range{
Text: text, From: i.Line(),
File: file, To: i.Line(),
LineNumber: lineNumber,
HunkPos: hunkPos,
} }
} }
return i.LineRange
}

View File

@ -36,16 +36,16 @@ func (p *MaxPerFileFromLinter) Process(issues []result.Issue) ([]result.Issue, e
return true return true
} }
lm := p.flc[i.File] lm := p.flc[i.FilePath()]
if lm == nil { if lm == nil {
p.flc[i.File] = linterToCountMap{} p.flc[i.FilePath()] = linterToCountMap{}
} }
count := p.flc[i.File][i.FromLinter] count := p.flc[i.FilePath()][i.FromLinter]
if count >= limit { if count >= limit {
return false return false
} }
p.flc[i.File][i.FromLinter]++ p.flc[i.FilePath()][i.FromLinter]++
return true return true
}), nil }), nil
} }

View File

@ -39,19 +39,19 @@ func (p *Nolint) Process(issues []result.Issue) ([]result.Issue, error) {
} }
func (p *Nolint) shouldPassIssue(i *result.Issue) (bool, error) { func (p *Nolint) shouldPassIssue(i *result.Issue) (bool, error) {
comments := p.cache[i.File] comments := p.cache[i.FilePath()]
if comments == nil { if comments == nil {
file, err := parser.ParseFile(p.fset, i.File, nil, parser.ParseComments) file, err := parser.ParseFile(p.fset, i.FilePath(), nil, parser.ParseComments)
if err != nil { if err != nil {
return true, err return true, err
} }
comments = extractFileComments(p.fset, file.Comments...) comments = extractFileComments(p.fset, file.Comments...)
p.cache[i.File] = comments p.cache[i.FilePath()] = comments
} }
for _, comment := range comments { for _, comment := range comments {
if comment.line != i.LineNumber { if comment.line != i.Line() {
continue continue
} }

View File

@ -10,8 +10,10 @@ import (
func newNolintFileIssue(line int, fromLinter string) result.Issue { func newNolintFileIssue(line int, fromLinter string) result.Issue {
return result.Issue{ return result.Issue{
File: filepath.Join("testdata", "nolint.go"), Pos: token.Position{
LineNumber: line, Filename: filepath.Join("testdata", "nolint.go"),
Line: line,
},
FromLinter: fromLinter, FromLinter: fromLinter,
} }
} }

View File

@ -30,17 +30,17 @@ func (p PathPrettifier) Name() string {
func (p PathPrettifier) Process(issues []result.Issue) ([]result.Issue, error) { func (p PathPrettifier) Process(issues []result.Issue) ([]result.Issue, error) {
return transformIssues(issues, func(i *result.Issue) *result.Issue { return transformIssues(issues, func(i *result.Issue) *result.Issue {
if !filepath.IsAbs(i.File) { if !filepath.IsAbs(i.FilePath()) {
return i return i
} }
rel, err := filepath.Rel(p.root, i.File) rel, err := filepath.Rel(p.root, i.FilePath())
if err != nil { if err != nil {
return i return i
} }
newI := i newI := i
newI.File = rel newI.Pos.Filename = rel
return newI return newI
}), nil }), nil
} }

View File

@ -25,19 +25,19 @@ func (p UniqByLine) Name() string {
func (p *UniqByLine) Process(issues []result.Issue) ([]result.Issue, error) { func (p *UniqByLine) Process(issues []result.Issue) ([]result.Issue, error) {
return filterIssues(issues, func(i *result.Issue) bool { return filterIssues(issues, func(i *result.Issue) bool {
lc := p.flc[i.File] lc := p.flc[i.FilePath()]
if lc == nil { if lc == nil {
lc = lineToCount{} lc = lineToCount{}
p.flc[i.File] = lc p.flc[i.FilePath()] = lc
} }
const limit = 1 const limit = 1
count := lc[i.LineNumber] count := lc[i.Line()]
if count == limit { if count == limit {
return false return false
} }
lc[i.LineNumber]++ lc[i.Line()]++
return true return true
}), nil }), nil
} }

View File

@ -1,6 +1,7 @@
package processors package processors
import ( import (
"go/token"
"testing" "testing"
"github.com/golangci/golangci-lint/pkg/result" "github.com/golangci/golangci-lint/pkg/result"
@ -8,8 +9,10 @@ import (
func newFLIssue(file string, line int) result.Issue { func newFLIssue(file string, line int) result.Issue {
return result.Issue{ return result.Issue{
File: file, Pos: token.Position{
LineNumber: line, Filename: file,
Line: line,
},
} }
} }