Support excluding issues by source line regexp

See issues.exclude-rules[i].source.
Also introduced file data and file lines cache.
This commit is contained in:
Denis Isaev 2019-03-17 14:52:04 +03:00 committed by Isaev Denis
parent 7514bf8239
commit 3d2dfac47e
13 changed files with 386 additions and 224 deletions

View File

@ -193,7 +193,7 @@ issues:
exclude: exclude:
- abcdef - abcdef
# Excluding configuration per-path and per-linter # Excluding configuration per-path, per-linter, per-text and per-source
exclude-rules: exclude-rules:
# Exclude some linters from running on tests files. # Exclude some linters from running on tests files.
- path: _test\.go - path: _test\.go
@ -203,28 +203,22 @@ issues:
- dupl - dupl
- gosec - gosec
# Ease some gocritic warnings on test files.
- path: _test\.go
text: "(unnamedResult|exitAfterDefer)"
linters:
- gocritic
# Exclude known linters from partially hard-vendored code, # Exclude known linters from partially hard-vendored code,
# which is impossible to exclude via "nolint" comments. # which is impossible to exclude via "nolint" comments.
- path: internal/hmac/ - path: internal/hmac/
text: "weak cryptographic primitive" text: "weak cryptographic primitive"
linters: linters:
- gosec - gosec
- path: internal/hmac/
text: "Write\\` is not checked"
linters:
- errcheck
# Ease linting on benchmarking code. # Exclude some staticcheck messages
- path: cmd/stun-bench/ - linters:
linters: - staticcheck
- gosec text: "SA9003:"
- errcheck
# Exclude lll issues for long lines with go:generate
- linters:
- lll
source: "^//go:generate "
# Independently from option `exclude` we use default exclude patterns, # Independently from option `exclude` we use default exclude patterns,
# it can be disabled by this option. To list all # it can be disabled by this option. To list all

View File

@ -723,7 +723,7 @@ issues:
exclude: exclude:
- abcdef - abcdef
# Excluding configuration per-path and per-linter # Excluding configuration per-path, per-linter, per-text and per-source
exclude-rules: exclude-rules:
# Exclude some linters from running on tests files. # Exclude some linters from running on tests files.
- path: _test\.go - path: _test\.go
@ -733,28 +733,22 @@ issues:
- dupl - dupl
- gosec - gosec
# Ease some gocritic warnings on test files.
- path: _test\.go
text: "(unnamedResult|exitAfterDefer)"
linters:
- gocritic
# Exclude known linters from partially hard-vendored code, # Exclude known linters from partially hard-vendored code,
# which is impossible to exclude via "nolint" comments. # which is impossible to exclude via "nolint" comments.
- path: internal/hmac/ - path: internal/hmac/
text: "weak cryptographic primitive" text: "weak cryptographic primitive"
linters: linters:
- gosec - gosec
- path: internal/hmac/
text: "Write\\` is not checked"
linters:
- errcheck
# Ease linting on benchmarking code. # Exclude some staticcheck messages
- path: cmd/stun-bench/ - linters:
linters: - staticcheck
- gosec text: "SA9003:"
- errcheck
# Exclude lll issues for long lines with go:generate
- linters:
- lll
source: "^//go:generate "
# Independently from option `exclude` we use default exclude patterns, # Independently from option `exclude` we use default exclude patterns,
# it can be disabled by this option. To list all # it can be disabled by this option. To list all

View File

@ -4,6 +4,8 @@ import (
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/spf13/pflag" "github.com/spf13/pflag"
"github.com/golangci/golangci-lint/pkg/fsutils"
"github.com/golangci/golangci-lint/pkg/config" "github.com/golangci/golangci-lint/pkg/config"
"github.com/golangci/golangci-lint/pkg/goutil" "github.com/golangci/golangci-lint/pkg/goutil"
"github.com/golangci/golangci-lint/pkg/lint" "github.com/golangci/golangci-lint/pkg/lint"
@ -26,6 +28,8 @@ type Executor struct {
EnabledLintersSet *lintersdb.EnabledSet EnabledLintersSet *lintersdb.EnabledSet
contextLoader *lint.ContextLoader contextLoader *lint.ContextLoader
goenv *goutil.Env goenv *goutil.Env
fileCache *fsutils.FileCache
lineCache *fsutils.LineCache
} }
func NewExecutor(version, commit, date string) *Executor { func NewExecutor(version, commit, date string) *Executor {
@ -78,6 +82,8 @@ func NewExecutor(version, commit, date string) *Executor {
lintersdb.NewValidator(e.DBManager), e.log.Child("lintersdb"), e.cfg) lintersdb.NewValidator(e.DBManager), e.log.Child("lintersdb"), e.cfg)
e.goenv = goutil.NewEnv(e.log.Child("goenv")) e.goenv = goutil.NewEnv(e.log.Child("goenv"))
e.contextLoader = lint.NewContextLoader(e.cfg, e.log.Child("loader"), e.goenv) e.contextLoader = lint.NewContextLoader(e.cfg, e.log.Child("loader"), e.goenv)
e.fileCache = fsutils.NewFileCache()
e.lineCache = fsutils.NewLineCache(e.fileCache)
return e return e
} }

View File

@ -278,13 +278,13 @@ func (e *Executor) runAnalysis(ctx context.Context, args []string) (<-chan resul
} }
lintCtx.Log = e.log.Child("linters context") lintCtx.Log = e.log.Child("linters context")
runner, err := lint.NewRunner(lintCtx.ASTCache, e.cfg, e.log.Child("runner"), e.goenv) runner, err := lint.NewRunner(lintCtx.ASTCache, e.cfg, e.log.Child("runner"), e.goenv, e.lineCache)
if err != nil { if err != nil {
return nil, err return nil, err
} }
issuesCh := runner.Run(ctx, enabledLinters, lintCtx) issuesCh := runner.Run(ctx, enabledLinters, lintCtx)
fixer := processors.NewFixer(e.cfg, e.log) fixer := processors.NewFixer(e.cfg, e.log, e.fileCache)
return fixer.Process(issuesCh), nil return fixer.Process(issuesCh), nil
} }
@ -350,6 +350,8 @@ func (e *Executor) runAndPrint(ctx context.Context, args []string) error {
return fmt.Errorf("can't print %d issues: %s", len(issues), err) return fmt.Errorf("can't print %d issues: %s", len(issues), err)
} }
e.fileCache.PrintStats(e.log)
return nil return nil
} }

View File

@ -235,6 +235,7 @@ type ExcludeRule struct {
Linters []string Linters []string
Path string Path string
Text string Text string
Source string
} }
func validateOptionalRegex(value string) error { func validateOptionalRegex(value string) error {
@ -252,6 +253,9 @@ func (e ExcludeRule) Validate() error {
if err := validateOptionalRegex(e.Text); err != nil { if err := validateOptionalRegex(e.Text); err != nil {
return fmt.Errorf("invalid text regex: %v", err) return fmt.Errorf("invalid text regex: %v", err)
} }
if err := validateOptionalRegex(e.Source); err != nil {
return fmt.Errorf("invalid source regex: %v", err)
}
nonBlank := 0 nonBlank := 0
if len(e.Linters) > 0 { if len(e.Linters) > 0 {
nonBlank++ nonBlank++
@ -262,8 +266,11 @@ func (e ExcludeRule) Validate() error {
if e.Text != "" { if e.Text != "" {
nonBlank++ nonBlank++
} }
if e.Source != "" {
nonBlank++
}
if nonBlank < 2 { if nonBlank < 2 {
return errors.New("at least 2 of (text, path, linters) should be set") return errors.New("at least 2 of (text, source, path, linters) should be set")
} }
return nil return nil
} }

64
pkg/fsutils/filecache.go Normal file
View File

@ -0,0 +1,64 @@
package fsutils
import (
"fmt"
"io/ioutil"
"github.com/golangci/golangci-lint/pkg/logutils"
"github.com/pkg/errors"
)
type FileCache struct {
files map[string][]byte
}
func NewFileCache() *FileCache {
return &FileCache{
files: map[string][]byte{},
}
}
func (fc *FileCache) GetFileBytes(filePath string) ([]byte, error) {
cachedBytes := fc.files[filePath]
if cachedBytes != nil {
return cachedBytes, nil
}
fileBytes, err := ioutil.ReadFile(filePath)
if err != nil {
return nil, errors.Wrapf(err, "can't read file %s", filePath)
}
fc.files[filePath] = fileBytes
return fileBytes, nil
}
func prettifyBytesCount(n int) string {
const (
Multiplexer = 1024
KiB = 1 * Multiplexer
MiB = KiB * Multiplexer
GiB = MiB * Multiplexer
)
if n >= GiB {
return fmt.Sprintf("%.1fGiB", float64(n)/GiB)
}
if n >= MiB {
return fmt.Sprintf("%.1fMiB", float64(n)/MiB)
}
if n >= KiB {
return fmt.Sprintf("%.1fKiB", float64(n)/KiB)
}
return fmt.Sprintf("%dB", n)
}
func (fc *FileCache) PrintStats(log logutils.Log) {
var size int
for _, fileBytes := range fc.files {
size += len(fileBytes)
}
log.Infof("File cache stats: %d entries of total size %s", len(fc.files), prettifyBytesCount(size))
}

69
pkg/fsutils/linecache.go Normal file
View File

@ -0,0 +1,69 @@
package fsutils
import (
"bytes"
"fmt"
"github.com/pkg/errors"
)
type fileLinesCache [][]byte
type LineCache struct {
files map[string]fileLinesCache
fileCache *FileCache
}
func NewLineCache(fc *FileCache) *LineCache {
return &LineCache{
files: map[string]fileLinesCache{},
fileCache: fc,
}
}
// GetLine returns a index1-th (1-based index) line from the file on filePath
func (lc *LineCache) GetLine(filePath string, index1 int) (string, error) {
if index1 == 0 { // some linters, e.g. gosec can do it: it really means first line
index1 = 1
}
rawLine, err := lc.getRawLine(filePath, index1-1)
if err != nil {
return "", err
}
return string(bytes.Trim(rawLine, "\r")), nil
}
func (lc *LineCache) getRawLine(filePath string, index0 int) ([]byte, error) {
fc, err := lc.getFileCache(filePath)
if err != nil {
return nil, errors.Wrapf(err, "failed to get file %s lines cache", filePath)
}
if index0 < 0 {
return nil, fmt.Errorf("invalid file line index0 < 0: %d", index0)
}
if index0 >= len(fc) {
return nil, fmt.Errorf("invalid file line index0 (%d) >= len(fc) (%d)", index0, len(fc))
}
return fc[index0], nil
}
func (lc *LineCache) getFileCache(filePath string) (fileLinesCache, error) {
fc := lc.files[filePath]
if fc != nil {
return fc, nil
}
fileBytes, err := lc.fileCache.GetFileBytes(filePath)
if err != nil {
return nil, errors.Wrapf(err, "can't get file %s bytes from cache", filePath)
}
fc = bytes.Split(fileBytes, []byte("\n"))
lc.files[filePath] = fc
return fc, nil
}

View File

@ -9,6 +9,8 @@ import (
"sync" "sync"
"time" "time"
"github.com/golangci/golangci-lint/pkg/fsutils"
"github.com/golangci/golangci-lint/pkg/config" "github.com/golangci/golangci-lint/pkg/config"
"github.com/golangci/golangci-lint/pkg/goutil" "github.com/golangci/golangci-lint/pkg/goutil"
"github.com/golangci/golangci-lint/pkg/lint/astcache" "github.com/golangci/golangci-lint/pkg/lint/astcache"
@ -25,7 +27,9 @@ type Runner struct {
Log logutils.Log Log logutils.Log
} }
func NewRunner(astCache *astcache.Cache, cfg *config.Config, log logutils.Log, goenv *goutil.Env) (*Runner, error) { func NewRunner(astCache *astcache.Cache, cfg *config.Config, log logutils.Log, goenv *goutil.Env,
lineCache *fsutils.LineCache) (*Runner, error) {
icfg := cfg.Issues icfg := cfg.Issues
excludePatterns := icfg.ExcludePatterns excludePatterns := icfg.ExcludePatterns
if icfg.UseDefaultExcludes { if icfg.UseDefaultExcludes {
@ -53,6 +57,7 @@ func NewRunner(astCache *astcache.Cache, cfg *config.Config, log logutils.Log, g
for _, r := range icfg.ExcludeRules { for _, r := range icfg.ExcludeRules {
excludeRules = append(excludeRules, processors.ExcludeRule{ excludeRules = append(excludeRules, processors.ExcludeRule{
Text: r.Text, Text: r.Text,
Source: r.Source,
Path: r.Path, Path: r.Path,
Linters: r.Linters, Linters: r.Linters,
}) })
@ -68,7 +73,7 @@ func NewRunner(astCache *astcache.Cache, cfg *config.Config, log logutils.Log, g
processors.NewAutogeneratedExclude(astCache), processors.NewAutogeneratedExclude(astCache),
processors.NewIdentifierMarker(), // must be befor exclude processors.NewIdentifierMarker(), // must be befor exclude
processors.NewExclude(excludeTotalPattern), processors.NewExclude(excludeTotalPattern),
processors.NewExcludeRules(excludeRules), processors.NewExcludeRules(excludeRules, lineCache, log.Child("exclude_rules")),
processors.NewNolint(astCache, log.Child("nolint")), processors.NewNolint(astCache, log.Child("nolint")),
processors.NewUniqByLine(), processors.NewUniqByLine(),
@ -76,7 +81,7 @@ func NewRunner(astCache *astcache.Cache, cfg *config.Config, log logutils.Log, g
processors.NewMaxPerFileFromLinter(cfg), processors.NewMaxPerFileFromLinter(cfg),
processors.NewMaxSameIssues(icfg.MaxSameIssues, log.Child("max_same_issues"), cfg), processors.NewMaxSameIssues(icfg.MaxSameIssues, log.Child("max_same_issues"), cfg),
processors.NewMaxFromLinter(icfg.MaxIssuesPerLinter, log.Child("max_from_linter"), cfg), processors.NewMaxFromLinter(icfg.MaxIssuesPerLinter, log.Child("max_from_linter"), cfg),
processors.NewSourceCode(log.Child("source_code")), processors.NewSourceCode(lineCache, log.Child("source_code")),
processors.NewReplacementBuilder(log.Child("replacement_builder")), // must be after source code processors.NewReplacementBuilder(log.Child("replacement_builder")), // must be after source code
processors.NewPathShortener(), processors.NewPathShortener(),
}, },

View File

@ -3,11 +3,16 @@ package processors
import ( import (
"regexp" "regexp"
"github.com/golangci/golangci-lint/pkg/logutils"
"github.com/golangci/golangci-lint/pkg/fsutils"
"github.com/golangci/golangci-lint/pkg/result" "github.com/golangci/golangci-lint/pkg/result"
) )
type excludeRule struct { type excludeRule struct {
text *regexp.Regexp text *regexp.Regexp
source *regexp.Regexp
path *regexp.Regexp path *regexp.Regexp
linters []string linters []string
} }
@ -16,7 +21,80 @@ func (r *excludeRule) isEmpty() bool {
return r.text == nil && r.path == nil && len(r.linters) == 0 return r.text == nil && r.path == nil && len(r.linters) == 0
} }
func (r excludeRule) Match(i *result.Issue) bool { type ExcludeRule struct {
Text string
Source string
Path string
Linters []string
}
type ExcludeRules struct {
rules []excludeRule
lineCache *fsutils.LineCache
log logutils.Log
}
func NewExcludeRules(rules []ExcludeRule, lineCache *fsutils.LineCache, log logutils.Log) *ExcludeRules {
r := &ExcludeRules{
lineCache: lineCache,
log: log,
}
for _, rule := range rules {
parsedRule := excludeRule{
linters: rule.Linters,
}
if rule.Text != "" {
parsedRule.text = regexp.MustCompile("(?i)" + rule.Text)
}
if rule.Source != "" {
parsedRule.source = regexp.MustCompile("(?i)" + rule.Source)
}
if rule.Path != "" {
parsedRule.path = regexp.MustCompile(rule.Path)
}
r.rules = append(r.rules, parsedRule)
}
return r
}
func (p ExcludeRules) Process(issues []result.Issue) ([]result.Issue, error) {
if len(p.rules) == 0 {
return issues, nil
}
return filterIssues(issues, func(i *result.Issue) bool {
for _, rule := range p.rules {
rule := rule
if p.match(i, &rule) {
return false
}
}
return true
}), nil
}
func (p ExcludeRules) matchLinter(i *result.Issue, r *excludeRule) bool {
for _, linter := range r.linters {
if linter == i.FromLinter {
return true
}
}
return false
}
func (p ExcludeRules) matchSource(i *result.Issue, r *excludeRule) bool { //nolint:interfacer
sourceLine, err := p.lineCache.GetLine(i.FilePath(), i.Line())
if err != nil {
p.log.Warnf("Failed to get line %s:%d from line cache: %s", i.FilePath(), i.Line(), err)
return false // can't properly match
}
return r.source.MatchString(sourceLine)
}
func (p ExcludeRules) match(i *result.Issue, r *excludeRule) bool {
if r.isEmpty() { if r.isEmpty() {
return false return false
} }
@ -26,56 +104,16 @@ func (r excludeRule) Match(i *result.Issue) bool {
if r.path != nil && !r.path.MatchString(i.FilePath()) { if r.path != nil && !r.path.MatchString(i.FilePath()) {
return false return false
} }
if len(r.linters) == 0 { if len(r.linters) != 0 && !p.matchLinter(i, r) {
return true return false
} }
for _, l := range r.linters {
if l == i.FromLinter { // the most heavyweight checking last
return true if r.source != nil && !p.matchSource(i, r) {
} return false
} }
return false
}
type ExcludeRule struct { return true
Text string
Path string
Linters []string
}
func NewExcludeRules(rules []ExcludeRule) *ExcludeRules {
r := new(ExcludeRules)
for _, rule := range rules {
parsedRule := excludeRule{
linters: rule.Linters,
}
if rule.Text != "" {
parsedRule.text = regexp.MustCompile("(?i)" + rule.Text)
}
if rule.Path != "" {
parsedRule.path = regexp.MustCompile(rule.Path)
}
r.rules = append(r.rules, parsedRule)
}
return r
}
type ExcludeRules struct {
rules []excludeRule
}
func (r ExcludeRules) Process(issues []result.Issue) ([]result.Issue, error) {
if len(r.rules) == 0 {
return issues, nil
}
return filterIssues(issues, func(i *result.Issue) bool {
for _, rule := range r.rules {
if rule.Match(i) {
return false
}
}
return true
}), nil
} }
func (ExcludeRules) Name() string { return "exclude-rules" } func (ExcludeRules) Name() string { return "exclude-rules" }

View File

@ -2,97 +2,108 @@ package processors
import ( import (
"go/token" "go/token"
"path/filepath"
"testing" "testing"
"github.com/golangci/golangci-lint/pkg/fsutils"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/golangci/golangci-lint/pkg/result" "github.com/golangci/golangci-lint/pkg/result"
) )
func TestExcludeRules(t *testing.T) { func TestExcludeRulesMultiple(t *testing.T) {
t.Run("Multiple", func(t *testing.T) { lineCache := fsutils.NewLineCache(fsutils.NewFileCache())
p := NewExcludeRules([]ExcludeRule{ p := NewExcludeRules([]ExcludeRule{
{ {
Text: "^exclude$", Text: "^exclude$",
Linters: []string{"linter"}, Linters: []string{"linter"},
}, },
{ {
Linters: []string{"testlinter"}, Linters: []string{"testlinter"},
Path: `_test\.go`, Path: `_test\.go`,
}, },
{ {
Text: "^testonly$", Text: "^testonly$",
Path: `_test\.go`, Path: `_test\.go`,
},
{
Source: "^//go:generate ",
Linters: []string{"lll"},
},
}, lineCache, nil)
type issueCase struct {
Path string
Line int
Text string
Linter string
}
var newIssueCase = func(c issueCase) result.Issue {
return result.Issue{
Text: c.Text,
FromLinter: c.Linter,
Pos: token.Position{
Filename: c.Path,
Line: c.Line,
}, },
}
}
cases := []issueCase{
{Path: "e.go", Text: "exclude", Linter: "linter"},
{Path: "e.go", Text: "some", Linter: "linter"},
{Path: "e_test.go", Text: "normal", Linter: "testlinter"},
{Path: "e_test.go", Text: "another", Linter: "linter"},
{Path: "e_test.go", Text: "testonly", Linter: "linter"},
{Path: filepath.Join("testdata", "exclude_rules.go"), Line: 3, Linter: "lll"},
}
var issues []result.Issue
for _, c := range cases {
issues = append(issues, newIssueCase(c))
}
processedIssues := process(t, p, issues...)
var resultingCases []issueCase
for _, i := range processedIssues {
resultingCases = append(resultingCases, issueCase{
Path: i.FilePath(),
Linter: i.FromLinter,
Text: i.Text,
Line: i.Line(),
}) })
type issueCase struct { }
Path string expectedCases := []issueCase{
Text string {Path: "e.go", Text: "some", Linter: "linter"},
Linter string {Path: "e_test.go", Text: "another", Linter: "linter"},
} }
var newIssueCase = func(c issueCase) result.Issue { assert.Equal(t, expectedCases, resultingCases)
return result.Issue{ }
Text: c.Text,
FromLinter: c.Linter, func TestExcludeRulesText(t *testing.T) {
Pos: token.Position{ p := NewExcludeRules([]ExcludeRule{
Filename: c.Path, {
}, Text: "^exclude$",
} Linters: []string{
} "linter",
cases := []issueCase{ },
{Path: "e.go", Text: "exclude", Linter: "linter"}, },
{Path: "e.go", Text: "some", Linter: "linter"}, }, nil, nil)
{Path: "e_test.go", Text: "normal", Linter: "testlinter"}, texts := []string{"excLude", "1", "", "exclud", "notexclude"}
{Path: "e_test.go", Text: "another", Linter: "linter"}, var issues []result.Issue
{Path: "e_test.go", Text: "testonly", Linter: "linter"}, for _, t := range texts {
} issues = append(issues, result.Issue{
var issues []result.Issue Text: t,
for _, c := range cases { FromLinter: "linter",
issues = append(issues, newIssueCase(c)) })
} }
processedIssues := process(t, p, issues...)
var resultingCases []issueCase processedIssues := process(t, p, issues...)
for _, i := range processedIssues { assert.Len(t, processedIssues, len(issues)-1)
resultingCases = append(resultingCases, issueCase{
Path: i.FilePath(), var processedTexts []string
Linter: i.FromLinter, for _, i := range processedIssues {
Text: i.Text, processedTexts = append(processedTexts, i.Text)
}) }
} assert.Equal(t, texts[1:], processedTexts)
expectedCases := []issueCase{ }
{Path: "e.go", Text: "some", Linter: "linter"}, func TestExcludeRulesEmpty(t *testing.T) {
{Path: "e_test.go", Text: "another", Linter: "linter"}, processAssertSame(t, NewExcludeRules(nil, nil, nil), newTextIssue("test"))
}
assert.Equal(t, expectedCases, resultingCases)
})
t.Run("Text", func(t *testing.T) {
p := NewExcludeRules([]ExcludeRule{
{
Text: "^exclude$",
Linters: []string{
"linter",
},
},
})
texts := []string{"excLude", "1", "", "exclud", "notexclude"}
var issues []result.Issue
for _, t := range texts {
issues = append(issues, result.Issue{
Text: t,
FromLinter: "linter",
})
}
processedIssues := process(t, p, issues...)
assert.Len(t, processedIssues, len(issues)-1)
var processedTexts []string
for _, i := range processedIssues {
processedTexts = append(processedTexts, i.Text)
}
assert.Equal(t, texts[1:], processedTexts)
})
t.Run("Empty", func(t *testing.T) {
processAssertSame(t, NewExcludeRules(nil), newTextIssue("test"))
})
} }

View File

@ -3,12 +3,13 @@ package processors
import ( import (
"bytes" "bytes"
"fmt" "fmt"
"io/ioutil"
"os" "os"
"path/filepath" "path/filepath"
"sort" "sort"
"strings" "strings"
"github.com/golangci/golangci-lint/pkg/fsutils"
"github.com/golangci/golangci-lint/pkg/logutils" "github.com/golangci/golangci-lint/pkg/logutils"
"github.com/pkg/errors" "github.com/pkg/errors"
@ -18,12 +19,13 @@ import (
) )
type Fixer struct { type Fixer struct {
cfg *config.Config cfg *config.Config
log logutils.Log log logutils.Log
fileCache *fsutils.FileCache
} }
func NewFixer(cfg *config.Config, log logutils.Log) *Fixer { func NewFixer(cfg *config.Config, log logutils.Log, fileCache *fsutils.FileCache) *Fixer {
return &Fixer{cfg: cfg, log: log} return &Fixer{cfg: cfg, log: log, fileCache: fileCache}
} }
func (f Fixer) Process(issues <-chan result.Issue) <-chan result.Issue { func (f Fixer) Process(issues <-chan result.Issue) <-chan result.Issue {
@ -63,9 +65,9 @@ func (f Fixer) Process(issues <-chan result.Issue) <-chan result.Issue {
func (f Fixer) fixIssuesInFile(filePath string, issues []result.Issue) error { func (f Fixer) fixIssuesInFile(filePath string, issues []result.Issue) error {
// TODO: don't read the whole file into memory: read line by line; // TODO: don't read the whole file into memory: read line by line;
// can't just use bufio.scanner: it has a line length limit // can't just use bufio.scanner: it has a line length limit
origFileData, err := ioutil.ReadFile(filePath) origFileData, err := f.fileCache.GetFileBytes(filePath)
if err != nil { if err != nil {
return errors.Wrapf(err, "failed to read %s", filePath) return errors.Wrapf(err, "failed to get file bytes for %s", filePath)
} }
origFileLines := bytes.Split(origFileData, []byte("\n")) origFileLines := bytes.Split(origFileData, []byte("\n"))

View File

@ -1,28 +1,22 @@
package processors package processors
import ( import (
"bytes" "github.com/golangci/golangci-lint/pkg/fsutils"
"fmt"
"io/ioutil"
"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 filesLineCache map[string]linesCache
type SourceCode struct { type SourceCode struct {
cache filesLineCache lineCache *fsutils.LineCache
log logutils.Log log logutils.Log
} }
var _ Processor = SourceCode{} var _ Processor = SourceCode{}
func NewSourceCode(log logutils.Log) *SourceCode { func NewSourceCode(lc *fsutils.LineCache, log logutils.Log) *SourceCode {
return &SourceCode{ return &SourceCode{
cache: filesLineCache{}, lineCache: lc,
log: log, log: log,
} }
} }
@ -32,50 +26,21 @@ func (p SourceCode) Name() string {
func (p SourceCode) Process(issues []result.Issue) ([]result.Issue, error) { func (p SourceCode) 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 {
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 newI := *i
lineRange := i.GetLineRange() lineRange := i.GetLineRange()
var lineStr string for lineNumber := lineRange.From; lineNumber <= lineRange.To; lineNumber++ {
for line := lineRange.From; line <= lineRange.To; line++ { line, err := p.lineCache.GetLine(i.FilePath(), lineNumber)
if line == 0 { // some linters, e.g. gosec can do it: it really means first line if err != nil {
line = 1 p.log.Warnf("Failed to get line %d for file %s: %s", i.FilePath(), lineNumber, err)
return i
} }
zeroIndexedLine := line - 1 newI.SourceLines = append(newI.SourceLines, line)
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 return &newI
}), nil }), 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() {} func (p SourceCode) Finish() {}

View File

@ -0,0 +1,5 @@
package testdata
//go:generate --long line --with a --lot of --arguments --that we --would like --to exclude --from lll --issues --by exclude-rules
// long line that we don't want to exclude from lll issues. Use the similar pattern: go:generate. This line should be reported by lll