move source code lines extraction to processor and store source lines in output json
This commit is contained in:
parent
f81283f38d
commit
e58c27e463
@ -273,6 +273,26 @@ func (e *Executor) setOutputToDevNull() (savedStdout, savedStderr *os.File) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (e *Executor) setExitCodeIfIssuesFound(issues <-chan result.Issue) <-chan result.Issue {
|
||||||
|
resCh := make(chan result.Issue, 1024)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
issuesFound := false
|
||||||
|
for i := range issues {
|
||||||
|
issuesFound = true
|
||||||
|
resCh <- i
|
||||||
|
}
|
||||||
|
|
||||||
|
if issuesFound {
|
||||||
|
e.exitCode = e.cfg.Run.ExitCodeIfIssuesFound
|
||||||
|
}
|
||||||
|
|
||||||
|
close(resCh)
|
||||||
|
}()
|
||||||
|
|
||||||
|
return resCh
|
||||||
|
}
|
||||||
|
|
||||||
func (e *Executor) runAndPrint(ctx context.Context, args []string) error {
|
func (e *Executor) runAndPrint(ctx context.Context, args []string) error {
|
||||||
if !logutils.HaveDebugTag("linters_output") {
|
if !logutils.HaveDebugTag("linters_output") {
|
||||||
// Don't allow linters and loader to print anything
|
// Don't allow linters and loader to print anything
|
||||||
@ -293,14 +313,10 @@ func (e *Executor) runAndPrint(ctx context.Context, args []string) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
gotAnyIssues, err := p.Print(ctx, issues)
|
issues = e.setExitCodeIfIssuesFound(issues)
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("can't print %d issues: %s", len(issues), err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if gotAnyIssues {
|
if err = p.Print(ctx, issues); err != nil {
|
||||||
e.exitCode = e.cfg.Run.ExitCodeIfIssuesFound
|
return fmt.Errorf("can't print %d issues: %s", len(issues), err)
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
@ -55,6 +55,7 @@ func NewRunner(astCache *astcache.Cache, cfg *config.Config, log logutils.Log) (
|
|||||||
processors.NewMaxPerFileFromLinter(),
|
processors.NewMaxPerFileFromLinter(),
|
||||||
processors.NewMaxSameIssues(icfg.MaxSameIssues, log.Child("max_same_issues")),
|
processors.NewMaxSameIssues(icfg.MaxSameIssues, log.Child("max_same_issues")),
|
||||||
processors.NewMaxFromLinter(icfg.MaxIssuesPerLinter, log.Child("max_from_linter")),
|
processors.NewMaxFromLinter(icfg.MaxIssuesPerLinter, log.Child("max_from_linter")),
|
||||||
|
processors.NewSourceCode(log.Child("source_code")),
|
||||||
},
|
},
|
||||||
Log: log,
|
Log: log,
|
||||||
}, nil
|
}, nil
|
||||||
|
@ -36,7 +36,7 @@ func NewCheckstyle() *Checkstyle {
|
|||||||
return &Checkstyle{}
|
return &Checkstyle{}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (Checkstyle) Print(ctx context.Context, issues <-chan result.Issue) (bool, error) {
|
func (Checkstyle) Print(ctx context.Context, issues <-chan result.Issue) error {
|
||||||
out := checkstyleOutput{
|
out := checkstyleOutput{
|
||||||
Version: "5.0",
|
Version: "5.0",
|
||||||
}
|
}
|
||||||
@ -71,9 +71,9 @@ func (Checkstyle) Print(ctx context.Context, issues <-chan result.Issue) (bool,
|
|||||||
|
|
||||||
data, err := xml.Marshal(&out)
|
data, err := xml.Marshal(&out)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Fprintf(logutils.StdOut, "%s%s\n", xml.Header, data)
|
fmt.Fprintf(logutils.StdOut, "%s%s\n", xml.Header, data)
|
||||||
return len(files) > 0, nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -25,7 +25,7 @@ type JSONResult struct {
|
|||||||
Report *report.Data
|
Report *report.Data
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p JSON) Print(ctx context.Context, issues <-chan result.Issue) (bool, error) {
|
func (p JSON) Print(ctx context.Context, issues <-chan result.Issue) error {
|
||||||
allIssues := []result.Issue{}
|
allIssues := []result.Issue{}
|
||||||
for i := range issues {
|
for i := range issues {
|
||||||
allIssues = append(allIssues, i)
|
allIssues = append(allIssues, i)
|
||||||
@ -38,9 +38,9 @@ func (p JSON) Print(ctx context.Context, issues <-chan result.Issue) (bool, erro
|
|||||||
|
|
||||||
outputJSON, err := json.Marshal(res)
|
outputJSON, err := json.Marshal(res)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Fprint(logutils.StdOut, string(outputJSON))
|
fmt.Fprint(logutils.StdOut, string(outputJSON))
|
||||||
return len(allIssues) != 0, nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -7,5 +7,5 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type Printer interface {
|
type Printer interface {
|
||||||
Print(ctx context.Context, issues <-chan result.Issue) (bool, error)
|
Print(ctx context.Context, issues <-chan result.Issue) error
|
||||||
}
|
}
|
||||||
|
@ -28,24 +28,18 @@ func (p Tab) SprintfColored(ca color.Attribute, format string, args ...interface
|
|||||||
return c.Sprintf(format, args...)
|
return c.Sprintf(format, args...)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Tab) Print(ctx context.Context, issues <-chan result.Issue) (bool, error) {
|
func (p *Tab) Print(ctx context.Context, issues <-chan result.Issue) error {
|
||||||
w := tabwriter.NewWriter(logutils.StdOut, 0, 0, 2, ' ', 0)
|
w := tabwriter.NewWriter(logutils.StdOut, 0, 0, 2, ' ', 0)
|
||||||
|
|
||||||
issuesN := 0
|
|
||||||
for i := range issues {
|
for i := range issues {
|
||||||
issuesN++
|
|
||||||
p.printIssue(&i, w)
|
p.printIssue(&i, w)
|
||||||
}
|
}
|
||||||
|
|
||||||
if issuesN != 0 {
|
|
||||||
p.log.Infof("Found %d issues", issuesN)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := w.Flush(); err != nil {
|
if err := w.Flush(); err != nil {
|
||||||
p.log.Warnf("Can't flush tab writer: %s", err)
|
p.log.Warnf("Can't flush tab writer: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return issuesN != 0, nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p Tab) printIssue(i *result.Issue, w io.Writer) {
|
func (p Tab) printIssue(i *result.Issue, w io.Writer) {
|
||||||
|
@ -1,27 +1,20 @@
|
|||||||
package printers
|
package printers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/fatih/color"
|
"github.com/fatih/color"
|
||||||
"github.com/golangci/golangci-lint/pkg/logutils"
|
"github.com/golangci/golangci-lint/pkg/logutils"
|
||||||
"github.com/golangci/golangci-lint/pkg/result"
|
"github.com/golangci/golangci-lint/pkg/result"
|
||||||
)
|
)
|
||||||
|
|
||||||
type linesCache [][]byte
|
|
||||||
type filesCache map[string]linesCache
|
|
||||||
|
|
||||||
type Text struct {
|
type Text struct {
|
||||||
printIssuedLine bool
|
printIssuedLine bool
|
||||||
useColors bool
|
useColors bool
|
||||||
printLinterName bool
|
printLinterName bool
|
||||||
|
|
||||||
cache filesCache
|
log logutils.Log
|
||||||
log logutils.Log
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewText(printIssuedLine, useColors, printLinterName bool, log logutils.Log) *Text {
|
func NewText(printIssuedLine, useColors, printLinterName bool, log logutils.Log) *Text {
|
||||||
@ -29,7 +22,6 @@ func NewText(printIssuedLine, useColors, printLinterName bool, log logutils.Log)
|
|||||||
printIssuedLine: printIssuedLine,
|
printIssuedLine: printIssuedLine,
|
||||||
useColors: useColors,
|
useColors: useColors,
|
||||||
printLinterName: printLinterName,
|
printLinterName: printLinterName,
|
||||||
cache: filesCache{},
|
|
||||||
log: log,
|
log: log,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -43,56 +35,19 @@ func (p Text) SprintfColored(ca color.Attribute, format string, args ...interfac
|
|||||||
return c.Sprintf(format, args...)
|
return c.Sprintf(format, args...)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Text) getFileLinesForIssue(i *result.Issue) (linesCache, error) {
|
func (p *Text) Print(ctx context.Context, issues <-chan result.Issue) error {
|
||||||
fc := p.cache[i.FilePath()]
|
|
||||||
if fc != nil {
|
|
||||||
return fc, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: make more optimal algorithm: don't load all files into memory
|
|
||||||
fileBytes, err := ioutil.ReadFile(i.FilePath())
|
|
||||||
if err != nil {
|
|
||||||
return nil, 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
|
|
||||||
p.cache[i.FilePath()] = fc
|
|
||||||
return fc, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *Text) Print(ctx context.Context, issues <-chan result.Issue) (bool, error) {
|
|
||||||
var issuedLineExtractingDuration time.Duration
|
|
||||||
defer func() {
|
|
||||||
p.log.Infof("Extracting issued lines took %s", issuedLineExtractingDuration)
|
|
||||||
}()
|
|
||||||
|
|
||||||
issuesN := 0
|
|
||||||
for i := range issues {
|
for i := range issues {
|
||||||
issuesN++
|
|
||||||
p.printIssue(&i)
|
p.printIssue(&i)
|
||||||
|
|
||||||
if !p.printIssuedLine {
|
if !p.printIssuedLine {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
startedAt := time.Now()
|
p.printSourceCode(&i)
|
||||||
lines, err := p.getFileLinesForIssue(&i)
|
p.printUnderLinePointer(&i)
|
||||||
if err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
issuedLineExtractingDuration += time.Since(startedAt)
|
|
||||||
|
|
||||||
p.printIssuedLines(&i, lines)
|
|
||||||
if i.Line()-1 < len(lines) {
|
|
||||||
p.printUnderLinePointer(&i, string(lines[i.Line()-1]))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if issuesN != 0 {
|
return nil
|
||||||
p.log.Infof("Found %d issues", issuesN)
|
|
||||||
}
|
|
||||||
|
|
||||||
return issuesN != 0, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p Text) printIssue(i *result.Issue) {
|
func (p Text) printIssue(i *result.Issue) {
|
||||||
@ -107,32 +62,19 @@ func (p Text) printIssue(i *result.Issue) {
|
|||||||
fmt.Fprintf(logutils.StdOut, "%s: %s\n", pos, text)
|
fmt.Fprintf(logutils.StdOut, "%s: %s\n", pos, text)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p Text) printIssuedLines(i *result.Issue, lines linesCache) {
|
func (p Text) printSourceCode(i *result.Issue) {
|
||||||
lineRange := i.GetLineRange()
|
for _, line := range i.SourceLines {
|
||||||
var lineStr string
|
fmt.Fprintln(logutils.StdOut, line)
|
||||||
for line := lineRange.From; line <= lineRange.To; line++ {
|
|
||||||
if line == 0 { // some linters, e.g. gas can do it: it really means first line
|
|
||||||
line = 1
|
|
||||||
}
|
|
||||||
|
|
||||||
zeroIndexedLine := line - 1
|
|
||||||
if zeroIndexedLine >= len(lines) {
|
|
||||||
p.log.Warnf("No line %d in file %s", line, i.FilePath())
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
lineStr = string(bytes.Trim(lines[zeroIndexedLine], "\r"))
|
|
||||||
fmt.Fprintln(logutils.StdOut, lineStr)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p Text) printUnderLinePointer(i *result.Issue, line string) {
|
func (p Text) printUnderLinePointer(i *result.Issue) {
|
||||||
lineRange := i.GetLineRange()
|
if len(i.SourceLines) != 1 || i.Pos.Column == 0 {
|
||||||
if lineRange.From != lineRange.To || i.Pos.Column == 0 {
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
col0 := i.Pos.Column - 1
|
col0 := i.Pos.Column - 1
|
||||||
|
line := i.SourceLines[0]
|
||||||
prefixRunes := make([]rune, 0, len(line))
|
prefixRunes := make([]rune, 0, len(line))
|
||||||
for j := 0; j < len(line) && j < col0; j++ {
|
for j := 0; j < len(line) && j < col0; j++ {
|
||||||
if line[j] == '\t' {
|
if line[j] == '\t' {
|
||||||
|
@ -13,6 +13,8 @@ type Issue struct {
|
|||||||
Pos token.Position
|
Pos token.Position
|
||||||
LineRange *Range `json:",omitempty"`
|
LineRange *Range `json:",omitempty"`
|
||||||
HunkPos int `json:",omitempty"`
|
HunkPos int `json:",omitempty"`
|
||||||
|
|
||||||
|
SourceLines []string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (i Issue) FilePath() string {
|
func (i Issue) FilePath() string {
|
||||||
|
81
pkg/result/processors/source_code.go
Normal file
81
pkg/result/processors/source_code.go
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
package processors
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
|
||||||
|
"github.com/golangci/golangci-lint/pkg/logutils"
|
||||||
|
"github.com/golangci/golangci-lint/pkg/result"
|
||||||
|
)
|
||||||
|
|
||||||
|
type linesCache [][]byte
|
||||||
|
type filesLineCache map[string]linesCache
|
||||||
|
|
||||||
|
type SourceCode struct {
|
||||||
|
cache filesLineCache
|
||||||
|
log logutils.Log
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ Processor = SourceCode{}
|
||||||
|
|
||||||
|
func NewSourceCode(log logutils.Log) *SourceCode {
|
||||||
|
return &SourceCode{
|
||||||
|
cache: filesLineCache{},
|
||||||
|
log: log,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p SourceCode) Name() string {
|
||||||
|
return "source_code"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p SourceCode) Process(issues []result.Issue) ([]result.Issue, error) {
|
||||||
|
return transformIssues(issues, func(i *result.Issue) *result.Issue {
|
||||||
|
lines, err := p.getFileLinesForIssue(i)
|
||||||
|
if err != nil {
|
||||||
|
p.log.Warnf("Failed to get lines for file %s: %s", i.FilePath(), err)
|
||||||
|
return i
|
||||||
|
}
|
||||||
|
|
||||||
|
newI := *i
|
||||||
|
|
||||||
|
lineRange := i.GetLineRange()
|
||||||
|
var lineStr string
|
||||||
|
for line := lineRange.From; line <= lineRange.To; line++ {
|
||||||
|
if line == 0 { // some linters, e.g. gas can do it: it really means first line
|
||||||
|
line = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
zeroIndexedLine := line - 1
|
||||||
|
if zeroIndexedLine >= len(lines) {
|
||||||
|
p.log.Warnf("No line %d in file %s", line, i.FilePath())
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
lineStr = string(bytes.Trim(lines[zeroIndexedLine], "\r"))
|
||||||
|
newI.SourceLines = append(newI.SourceLines, lineStr)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &newI
|
||||||
|
}), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *SourceCode) getFileLinesForIssue(i *result.Issue) (linesCache, error) {
|
||||||
|
fc := p.cache[i.FilePath()]
|
||||||
|
if fc != nil {
|
||||||
|
return fc, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: make more optimal algorithm: don't load all files into memory
|
||||||
|
fileBytes, err := ioutil.ReadFile(i.FilePath())
|
||||||
|
if err != nil {
|
||||||
|
return nil, 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
|
||||||
|
p.cache[i.FilePath()] = fc
|
||||||
|
return fc, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p SourceCode) Finish() {}
|
Loading…
x
Reference in New Issue
Block a user