136 lines
2.9 KiB
Go
136 lines
2.9 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
|
|
}
|