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:
- 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

View File

@ -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

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

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"
"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(),
},

View File

@ -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" }

View File

@ -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"))
}

View File

@ -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"))

View File

@ -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() {}

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