Denis Isaev 2b587b63d6
Load AST for fast linters in different way.
Use build.Import instead of manual parser.ParseFile and paths traversal. It allows:
1. support build tags for all linters.
2. analyze files only for current GOOS/GOARCH: less false-positives.
3. analyze xtest packages (*_test) by golint: upstream golint and
gometalinter can't do it! And don't break analysis on the first xtest
package like it was before.
4. proper handling of xtest packages for linters like goconst where
package boundary is important: less false-positives is expected.

Also:
1. reuse AST parsing for golint and goconst: minor speedup.
2. allow to specify path (not only name) regexp for --skip-files and
--skip-dirs
3. add more default exclude filters for golint about commits:
`(comment on exported (method|function)|should have( a package)?
    comment|comment should be of the form)`
4. print skipped dir in verbose (-v) mode
5. refactor per-linter tests: declare arguments in comments, run only
one linter and in combination with slow linter
2018-06-10 23:46:24 +03:00

189 lines
4.0 KiB
Go

// Package goconst finds repeated strings that could be replaced by a constant.
//
// There are obvious benefits to using constants instead of repeating strings,
// mostly to ease maintenance. Cannot argue against changing a single constant versus many strings.
// While this could be considered a beginner mistake, across time,
// multiple packages and large codebases, some repetition could have slipped in.
package goconst
import (
"go/ast"
"go/parser"
"go/token"
"log"
"os"
"path/filepath"
"regexp"
"strings"
)
const (
testSuffix = "_test.go"
)
type Parser struct {
// Meant to be passed via New()
path, ignore string
ignoreTests, matchConstant bool
minLength int
supportedTokens []token.Token
// Internals
strs Strings
consts Constants
}
// New creates a new instance of the parser.
// This is your entry point if you'd like to use goconst as an API.
func New(path, ignore string, ignoreTests, matchConstant, numbers bool, minLength int) *Parser {
supportedTokens := []token.Token{token.STRING}
if numbers {
supportedTokens = append(supportedTokens, token.INT, token.FLOAT)
}
return &Parser{
path: path,
ignore: ignore,
ignoreTests: ignoreTests,
matchConstant: matchConstant,
minLength: minLength,
supportedTokens: supportedTokens,
// Initialize the maps
strs: Strings{},
consts: Constants{},
}
}
// ParseTree will search the given path for occurrences that could be moved into constants.
// If "..." is appended, the search will be recursive.
func (p *Parser) ParseTree() (Strings, Constants, error) {
pathLen := len(p.path)
// Parse recursively the given path if the recursive notation is found
if pathLen >= 5 && p.path[pathLen-3:] == "..." {
filepath.Walk(p.path[:pathLen-3], func(path string, f os.FileInfo, err error) error {
if err != nil {
log.Println(err)
// resume walking
return nil
}
if f.IsDir() {
p.parseDir(path)
}
return nil
})
} else {
p.parseDir(p.path)
}
return p.strs, p.consts, nil
}
func (p *Parser) parseDir(dir string) error {
fset := token.NewFileSet()
pkgs, err := parser.ParseDir(fset, dir, func(info os.FileInfo) bool {
valid, name := true, info.Name()
if p.ignoreTests {
if strings.HasSuffix(name, testSuffix) {
valid = false
}
}
if len(p.ignore) != 0 {
match, err := regexp.MatchString(p.ignore, dir+name)
if err != nil {
log.Fatal(err)
return true
}
if match {
valid = false
}
}
return valid
}, 0)
if err != nil {
return err
}
for _, pkg := range pkgs {
for fn, f := range pkg.Files {
ast.Walk(&treeVisitor{
fileSet: fset,
packageName: pkg.Name,
fileName: fn,
p: p,
}, f)
}
}
return nil
}
type Strings map[string][]ExtendedPos
type Constants map[string]ConstType
type ConstType struct {
token.Position
Name, packageName string
}
type ExtendedPos struct {
token.Position
packageName string
}
type Issue struct {
Pos token.Position
OccurencesCount int
Str string
MatchingConst string
}
type Config struct {
MatchWithConstants bool
MinStringLength int
MinOccurrences int
}
func Run(files []*ast.File, fset *token.FileSet, cfg *Config) ([]Issue, error) {
p := New("", "", false, cfg.MatchWithConstants, false, cfg.MinStringLength)
var issues []Issue
for _, f := range files {
ast.Walk(&treeVisitor{
fileSet: fset,
packageName: "",
fileName: "",
p: p,
}, f)
}
for str, item := range p.strs {
// Filter out items whose occurrences don't match the min value
if len(item) < cfg.MinOccurrences {
delete(p.strs, str)
}
}
for str, item := range p.strs {
fi := item[0]
i := Issue{
Pos: fi.Position,
OccurencesCount: len(item),
Str: str,
}
if len(p.consts) != 0 {
if cst, ok := p.consts[str]; ok {
// const should be in the same package and exported
i.MatchingConst = cst.Name
}
}
issues = append(issues, i)
}
return issues, nil
}