148 lines
3.4 KiB
Go

package printers
import (
"bytes"
"fmt"
"io/ioutil"
"strings"
"time"
"github.com/fatih/color"
"github.com/golangci/golangci-lint/pkg/result"
"github.com/sirupsen/logrus"
)
type linesCache [][]byte
type filesCache map[string]linesCache
type Text struct {
printIssuedLine bool
useColors bool
printLinterName bool
cache filesCache
}
func NewText(printIssuedLine, useColors, printLinterName bool) *Text {
return &Text{
printIssuedLine: printIssuedLine,
useColors: useColors,
printLinterName: printLinterName,
cache: filesCache{},
}
}
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) 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 *Text) Print(issues chan result.Issue) (bool, error) {
var issuedLineExtractingDuration time.Duration
defer func() {
logrus.Infof("Extracting issued lines took %s", issuedLineExtractingDuration)
}()
issuesN := 0
for i := range issues {
issuesN++
p.printIssue(&i)
if !p.printIssuedLine {
continue
}
startedAt := time.Now()
lines, err := p.getFileLinesForIssue(&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 {
outStr := p.SprintfColored(color.FgGreen, "Congrats! No issues were found.")
fmt.Fprintln(getOutWriter(), outStr)
} else {
logrus.Infof("Found %d issues", issuesN)
}
return issuesN != 0, nil
}
func (p Text) printIssue(i *result.Issue) {
text := p.SprintfColored(color.FgRed, "%s", i.Text)
if p.printLinterName {
text += fmt.Sprintf(" (%s)", i.FromLinter)
}
pos := p.SprintfColored(color.Bold, "%s:%d", i.FilePath(), i.Line())
fmt.Fprintf(getOutWriter(), "%s: %s\n", pos, text)
}
func (p Text) printIssuedLines(i *result.Issue, lines linesCache) {
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) {
logrus.Warnf("No line %d in file %s", line, i.FilePath())
break
}
lineStr = string(bytes.Trim(lines[zeroIndexedLine], "\r"))
fmt.Fprintln(getOutWriter(), lineStr)
}
}
func (p Text) printUnderLinePointer(i *result.Issue, line string) {
lineRange := i.GetLineRange()
if lineRange.From != lineRange.To || i.Pos.Column == 0 {
return
}
var j int
for ; j < len(line) && line[j] == '\t'; j++ {
}
tabsCount := j
spacesCount := i.Pos.Column - 1 - tabsCount
prefix := ""
if tabsCount != 0 {
prefix += strings.Repeat("\t", tabsCount)
}
if spacesCount != 0 {
prefix += strings.Repeat(" ", spacesCount)
}
fmt.Fprintf(getOutWriter(), "%s%s\n", prefix, p.SprintfColored(color.FgYellow, "^"))
}