From 3d2dfac47e0717cfba60c0eaef08f2f90d217730 Mon Sep 17 00:00:00 2001
From: Denis Isaev <denis@golangci.com>
Date: Sun, 17 Mar 2019 14:52:04 +0300
Subject: [PATCH] Support excluding issues by source line regexp

See issues.exclude-rules[i].source.
Also introduced file data and file lines cache.
---
 .golangci.example.yml                         |  26 +--
 README.md                                     |  26 +--
 pkg/commands/executor.go                      |   6 +
 pkg/commands/run.go                           |   6 +-
 pkg/config/config.go                          |   9 +-
 pkg/fsutils/filecache.go                      |  64 +++++++
 pkg/fsutils/linecache.go                      |  69 +++++++
 pkg/lint/runner.go                            |  11 +-
 pkg/result/processors/exclude_rules.go        | 134 ++++++++-----
 pkg/result/processors/exclude_rules_test.go   | 179 ++++++++++--------
 pkg/result/processors/fixer.go                |  16 +-
 pkg/result/processors/source_code.go          |  59 ++----
 .../processors/testdata/exclude_rules.go      |   5 +
 13 files changed, 386 insertions(+), 224 deletions(-)
 create mode 100644 pkg/fsutils/filecache.go
 create mode 100644 pkg/fsutils/linecache.go
 create mode 100644 pkg/result/processors/testdata/exclude_rules.go

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