
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
189 lines
4.0 KiB
Go
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
|
|
}
|