diff --git a/.golangci.example.yml b/.golangci.example.yml index 0c133bde..bf1e48f4 100644 --- a/.golangci.example.yml +++ b/.golangci.example.yml @@ -193,7 +193,7 @@ issues: exclude: - abcdef - # Excluding configuration per-path and per-linter + # Excluding configuration per-path, per-linter, per-text and per-source exclude-rules: # Exclude some linters from running on tests files. - path: _test\.go @@ -203,28 +203,22 @@ issues: - dupl - gosec - # Ease some gocritic warnings on test files. - - path: _test\.go - text: "(unnamedResult|exitAfterDefer)" - linters: - - gocritic - # Exclude known linters from partially hard-vendored code, # which is impossible to exclude via "nolint" comments. - path: internal/hmac/ text: "weak cryptographic primitive" linters: - gosec - - path: internal/hmac/ - text: "Write\\` is not checked" - linters: - - errcheck - # Ease linting on benchmarking code. - - path: cmd/stun-bench/ - linters: - - gosec - - errcheck + # Exclude some staticcheck messages + - linters: + - staticcheck + text: "SA9003:" + + # Exclude lll issues for long lines with go:generate + - linters: + - lll + source: "^//go:generate " # Independently from option `exclude` we use default exclude patterns, # it can be disabled by this option. To list all diff --git a/README.md b/README.md index f0338856..25a1f9bf 100644 --- a/README.md +++ b/README.md @@ -723,7 +723,7 @@ issues: exclude: - abcdef - # Excluding configuration per-path and per-linter + # Excluding configuration per-path, per-linter, per-text and per-source exclude-rules: # Exclude some linters from running on tests files. - path: _test\.go @@ -733,28 +733,22 @@ issues: - dupl - gosec - # Ease some gocritic warnings on test files. - - path: _test\.go - text: "(unnamedResult|exitAfterDefer)" - linters: - - gocritic - # Exclude known linters from partially hard-vendored code, # which is impossible to exclude via "nolint" comments. - path: internal/hmac/ text: "weak cryptographic primitive" linters: - gosec - - path: internal/hmac/ - text: "Write\\` is not checked" - linters: - - errcheck - # Ease linting on benchmarking code. - - path: cmd/stun-bench/ - linters: - - gosec - - errcheck + # Exclude some staticcheck messages + - linters: + - staticcheck + text: "SA9003:" + + # Exclude lll issues for long lines with go:generate + - linters: + - lll + source: "^//go:generate " # Independently from option `exclude` we use default exclude patterns, # it can be disabled by this option. To list all diff --git a/pkg/commands/executor.go b/pkg/commands/executor.go index 0075668e..481f645d 100644 --- a/pkg/commands/executor.go +++ b/pkg/commands/executor.go @@ -4,6 +4,8 @@ import ( "github.com/spf13/cobra" "github.com/spf13/pflag" + "github.com/golangci/golangci-lint/pkg/fsutils" + "github.com/golangci/golangci-lint/pkg/config" "github.com/golangci/golangci-lint/pkg/goutil" "github.com/golangci/golangci-lint/pkg/lint" @@ -26,6 +28,8 @@ type Executor struct { EnabledLintersSet *lintersdb.EnabledSet contextLoader *lint.ContextLoader goenv *goutil.Env + fileCache *fsutils.FileCache + lineCache *fsutils.LineCache } 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) e.goenv = goutil.NewEnv(e.log.Child("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 } diff --git a/pkg/commands/run.go b/pkg/commands/run.go index e262711c..993c0580 100644 --- a/pkg/commands/run.go +++ b/pkg/commands/run.go @@ -278,13 +278,13 @@ func (e *Executor) runAnalysis(ctx context.Context, args []string) (<-chan resul } 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 { return nil, err } 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 } @@ -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) } + e.fileCache.PrintStats(e.log) + return nil } diff --git a/pkg/config/config.go b/pkg/config/config.go index 0b2dcdeb..9a95744a 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -235,6 +235,7 @@ type ExcludeRule struct { Linters []string Path string Text string + Source string } func validateOptionalRegex(value string) error { @@ -252,6 +253,9 @@ func (e ExcludeRule) Validate() error { if err := validateOptionalRegex(e.Text); err != nil { 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 if len(e.Linters) > 0 { nonBlank++ @@ -262,8 +266,11 @@ func (e ExcludeRule) Validate() error { if e.Text != "" { nonBlank++ } + if e.Source != "" { + nonBlank++ + } 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 } diff --git a/pkg/fsutils/filecache.go b/pkg/fsutils/filecache.go new file mode 100644 index 00000000..6c97bdda --- /dev/null +++ b/pkg/fsutils/filecache.go @@ -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)) +} diff --git a/pkg/fsutils/linecache.go b/pkg/fsutils/linecache.go new file mode 100644 index 00000000..a651a504 --- /dev/null +++ b/pkg/fsutils/linecache.go @@ -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 +} diff --git a/pkg/lint/runner.go b/pkg/lint/runner.go index de9e9146..6652d991 100644 --- a/pkg/lint/runner.go +++ b/pkg/lint/runner.go @@ -9,6 +9,8 @@ import ( "sync" "time" + "github.com/golangci/golangci-lint/pkg/fsutils" + "github.com/golangci/golangci-lint/pkg/config" "github.com/golangci/golangci-lint/pkg/goutil" "github.com/golangci/golangci-lint/pkg/lint/astcache" @@ -25,7 +27,9 @@ type Runner struct { 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 excludePatterns := icfg.ExcludePatterns if icfg.UseDefaultExcludes { @@ -53,6 +57,7 @@ func NewRunner(astCache *astcache.Cache, cfg *config.Config, log logutils.Log, g for _, r := range icfg.ExcludeRules { excludeRules = append(excludeRules, processors.ExcludeRule{ Text: r.Text, + Source: r.Source, Path: r.Path, Linters: r.Linters, }) @@ -68,7 +73,7 @@ func NewRunner(astCache *astcache.Cache, cfg *config.Config, log logutils.Log, g processors.NewAutogeneratedExclude(astCache), processors.NewIdentifierMarker(), // must be befor exclude processors.NewExclude(excludeTotalPattern), - processors.NewExcludeRules(excludeRules), + processors.NewExcludeRules(excludeRules, lineCache, log.Child("exclude_rules")), processors.NewNolint(astCache, log.Child("nolint")), processors.NewUniqByLine(), @@ -76,7 +81,7 @@ func NewRunner(astCache *astcache.Cache, cfg *config.Config, log logutils.Log, g processors.NewMaxPerFileFromLinter(cfg), processors.NewMaxSameIssues(icfg.MaxSameIssues, log.Child("max_same_issues"), 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.NewPathShortener(), }, diff --git a/pkg/result/processors/exclude_rules.go b/pkg/result/processors/exclude_rules.go index e7dc7f39..014efcd0 100644 --- a/pkg/result/processors/exclude_rules.go +++ b/pkg/result/processors/exclude_rules.go @@ -3,11 +3,16 @@ package processors import ( "regexp" + "github.com/golangci/golangci-lint/pkg/logutils" + + "github.com/golangci/golangci-lint/pkg/fsutils" + "github.com/golangci/golangci-lint/pkg/result" ) type excludeRule struct { text *regexp.Regexp + source *regexp.Regexp path *regexp.Regexp linters []string } @@ -16,7 +21,80 @@ func (r *excludeRule) isEmpty() bool { 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() { return false } @@ -26,56 +104,16 @@ func (r excludeRule) Match(i *result.Issue) bool { if r.path != nil && !r.path.MatchString(i.FilePath()) { return false } - if len(r.linters) == 0 { - return true + if len(r.linters) != 0 && !p.matchLinter(i, r) { + return false } - for _, l := range r.linters { - if l == i.FromLinter { - return true - } + + // the most heavyweight checking last + if r.source != nil && !p.matchSource(i, r) { + return false } - return false -} -type ExcludeRule struct { - 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 + return true } func (ExcludeRules) Name() string { return "exclude-rules" } diff --git a/pkg/result/processors/exclude_rules_test.go b/pkg/result/processors/exclude_rules_test.go index 2e8bf356..8fd4a4d3 100644 --- a/pkg/result/processors/exclude_rules_test.go +++ b/pkg/result/processors/exclude_rules_test.go @@ -2,97 +2,108 @@ package processors import ( "go/token" + "path/filepath" "testing" + "github.com/golangci/golangci-lint/pkg/fsutils" + "github.com/stretchr/testify/assert" "github.com/golangci/golangci-lint/pkg/result" ) -func TestExcludeRules(t *testing.T) { - t.Run("Multiple", func(t *testing.T) { - p := NewExcludeRules([]ExcludeRule{ - { - Text: "^exclude$", - Linters: []string{"linter"}, - }, - { - Linters: []string{"testlinter"}, - Path: `_test\.go`, - }, - { - Text: "^testonly$", - Path: `_test\.go`, +func TestExcludeRulesMultiple(t *testing.T) { + lineCache := fsutils.NewLineCache(fsutils.NewFileCache()) + p := NewExcludeRules([]ExcludeRule{ + { + Text: "^exclude$", + Linters: []string{"linter"}, + }, + { + Linters: []string{"testlinter"}, + Path: `_test\.go`, + }, + { + Text: "^testonly$", + 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 - 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, - }, - } - } - 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"}, - } - 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, - }) - } - expectedCases := []issueCase{ - {Path: "e.go", Text: "some", Linter: "linter"}, - {Path: "e_test.go", Text: "another", Linter: "linter"}, - } - 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")) - }) + } + expectedCases := []issueCase{ + {Path: "e.go", Text: "some", Linter: "linter"}, + {Path: "e_test.go", Text: "another", Linter: "linter"}, + } + assert.Equal(t, expectedCases, resultingCases) +} + +func TestExcludeRulesText(t *testing.T) { + p := NewExcludeRules([]ExcludeRule{ + { + Text: "^exclude$", + Linters: []string{ + "linter", + }, + }, + }, nil, nil) + 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) +} +func TestExcludeRulesEmpty(t *testing.T) { + processAssertSame(t, NewExcludeRules(nil, nil, nil), newTextIssue("test")) } diff --git a/pkg/result/processors/fixer.go b/pkg/result/processors/fixer.go index 599f1aa4..2c7888e9 100644 --- a/pkg/result/processors/fixer.go +++ b/pkg/result/processors/fixer.go @@ -3,12 +3,13 @@ package processors import ( "bytes" "fmt" - "io/ioutil" "os" "path/filepath" "sort" "strings" + "github.com/golangci/golangci-lint/pkg/fsutils" + "github.com/golangci/golangci-lint/pkg/logutils" "github.com/pkg/errors" @@ -18,12 +19,13 @@ import ( ) type Fixer struct { - cfg *config.Config - log logutils.Log + cfg *config.Config + log logutils.Log + fileCache *fsutils.FileCache } -func NewFixer(cfg *config.Config, log logutils.Log) *Fixer { - return &Fixer{cfg: cfg, log: log} +func NewFixer(cfg *config.Config, log logutils.Log, fileCache *fsutils.FileCache) *Fixer { + return &Fixer{cfg: cfg, log: log, fileCache: fileCache} } 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 { // 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 - origFileData, err := ioutil.ReadFile(filePath) + origFileData, err := f.fileCache.GetFileBytes(filePath) 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")) diff --git a/pkg/result/processors/source_code.go b/pkg/result/processors/source_code.go index 44710a0d..96616648 100644 --- a/pkg/result/processors/source_code.go +++ b/pkg/result/processors/source_code.go @@ -1,28 +1,22 @@ package processors import ( - "bytes" - "fmt" - "io/ioutil" - + "github.com/golangci/golangci-lint/pkg/fsutils" "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 + lineCache *fsutils.LineCache + log logutils.Log } var _ Processor = SourceCode{} -func NewSourceCode(log logutils.Log) *SourceCode { +func NewSourceCode(lc *fsutils.LineCache, log logutils.Log) *SourceCode { return &SourceCode{ - cache: filesLineCache{}, - log: log, + lineCache: lc, + log: log, } } @@ -32,50 +26,21 @@ func (p SourceCode) Name() string { 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. gosec can do it: it really means first line - line = 1 + for lineNumber := lineRange.From; lineNumber <= lineRange.To; lineNumber++ { + line, err := p.lineCache.GetLine(i.FilePath(), lineNumber) + if err != nil { + p.log.Warnf("Failed to get line %d for file %s: %s", i.FilePath(), lineNumber, err) + return i } - 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) + newI.SourceLines = append(newI.SourceLines, line) } 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() {} diff --git a/pkg/result/processors/testdata/exclude_rules.go b/pkg/result/processors/testdata/exclude_rules.go new file mode 100644 index 00000000..fe90c946 --- /dev/null +++ b/pkg/result/processors/testdata/exclude_rules.go @@ -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