
1. Allow govet to work in 2 modes: fast and slow. Default is slow. In fast mode golangci-lint runs `go install -i` and `go test -i` for analyzed packages. But it's fast only when: - go >= 1.10 - it's repeated run or $GOPATH/pkg or `go env GOCACHE` is cached between CI builds In slow mode we load program from source code like for another linters and do it only once for all linters. 3. Patch govet code to warn about any troubles with the type information. Default behaviour of govet was to hide such warnings. Fail analysis if there are any troubles with type loading: it will prevent false-positives and false-negatives from govet. 4. Describe almost all options in .golangci.example.yml and include it into README. Describe when to use slow or fast mode of govet. 5. Speed up govet: reuse AST parsing: it's already parsed once by golangci-lint. For "slow" runs (when we run at least one slow linter) speedup by not loading type information second time. 6. Improve logging, debug logging 7. Fix crash in logging of AST cache warnings (#118)
223 lines
4.8 KiB
Go
223 lines
4.8 KiB
Go
package packages
|
|
|
|
import (
|
|
"fmt"
|
|
"go/build"
|
|
"io/ioutil"
|
|
"os"
|
|
"path/filepath"
|
|
"regexp"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/golangci/golangci-lint/pkg/fsutils"
|
|
"github.com/golangci/golangci-lint/pkg/logutils"
|
|
)
|
|
|
|
type Resolver struct {
|
|
excludeDirs map[string]*regexp.Regexp
|
|
buildTags []string
|
|
|
|
skippedDirs []string
|
|
log logutils.Log
|
|
|
|
wd string // working directory
|
|
}
|
|
|
|
func NewResolver(buildTags, excludeDirs []string, log logutils.Log) (*Resolver, error) {
|
|
excludeDirsMap := map[string]*regexp.Regexp{}
|
|
for _, dir := range excludeDirs {
|
|
re, err := regexp.Compile(dir)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("can't compile regexp %q: %s", dir, err)
|
|
}
|
|
|
|
excludeDirsMap[dir] = re
|
|
}
|
|
|
|
wd, err := os.Getwd()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("can't get working dir: %s", err)
|
|
}
|
|
|
|
return &Resolver{
|
|
excludeDirs: excludeDirsMap,
|
|
buildTags: buildTags,
|
|
log: log,
|
|
wd: wd,
|
|
}, nil
|
|
}
|
|
|
|
func (r Resolver) isIgnoredDir(dir string) bool {
|
|
cleanName := filepath.Clean(dir)
|
|
|
|
dirName := filepath.Base(cleanName)
|
|
|
|
// https://github.com/golang/dep/issues/298
|
|
// https://github.com/tools/godep/issues/140
|
|
if strings.HasPrefix(dirName, ".") && dirName != "." && dirName != ".." {
|
|
return true
|
|
}
|
|
if strings.HasPrefix(dirName, "_") {
|
|
return true
|
|
}
|
|
|
|
for _, dirExludeRe := range r.excludeDirs {
|
|
if dirExludeRe.MatchString(cleanName) {
|
|
return true
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
func (r *Resolver) resolveRecursively(root string, prog *Program) error {
|
|
// import root
|
|
if err := r.resolveDir(root, prog); err != nil {
|
|
return err
|
|
}
|
|
|
|
fis, err := ioutil.ReadDir(root)
|
|
if err != nil {
|
|
return fmt.Errorf("can't resolve dir %s: %s", root, err)
|
|
}
|
|
// TODO: pass cached fis to build.Context
|
|
|
|
for _, fi := range fis {
|
|
if !fi.IsDir() {
|
|
// ignore files: they were already imported by resolveDir(root)
|
|
continue
|
|
}
|
|
|
|
subdir := filepath.Join(root, fi.Name())
|
|
|
|
// Normalize each subdir because working directory can be one of these subdirs:
|
|
// working dir = /app/subdir, resolve root is ../, without this normalization
|
|
// path of subdir will be "../subdir" but it must be ".".
|
|
// Normalize path before checking is ignored dir.
|
|
subdir, err := r.normalizePath(subdir)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if r.isIgnoredDir(subdir) {
|
|
r.skippedDirs = append(r.skippedDirs, subdir)
|
|
continue
|
|
}
|
|
|
|
if err := r.resolveRecursively(subdir, prog); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (r Resolver) resolveDir(dir string, prog *Program) error {
|
|
// TODO: fork build.Import to reuse AST parsing
|
|
bp, err := prog.bctx.ImportDir(dir, build.ImportComment|build.IgnoreVendor)
|
|
if err != nil {
|
|
if _, nogo := err.(*build.NoGoError); nogo {
|
|
// Don't complain if the failure is due to no Go source files.
|
|
return nil
|
|
}
|
|
|
|
return fmt.Errorf("can't resolve dir %s: %s", dir, err)
|
|
}
|
|
|
|
pkg := Package{
|
|
bp: bp,
|
|
}
|
|
prog.addPackage(&pkg)
|
|
return nil
|
|
}
|
|
|
|
func (r Resolver) addFakePackage(filePath string, prog *Program) {
|
|
// Don't take build tags, is it test file or not, etc
|
|
// into account. If user explicitly wants to analyze this file
|
|
// do it.
|
|
p := Package{
|
|
bp: &build.Package{
|
|
// TODO: detect is it test file or not: without that we can't analyze only one test file
|
|
GoFiles: []string{filePath},
|
|
},
|
|
isFake: true,
|
|
dir: filepath.Dir(filePath),
|
|
}
|
|
prog.addPackage(&p)
|
|
}
|
|
|
|
func (r Resolver) Resolve(paths ...string) (prog *Program, err error) {
|
|
startedAt := time.Now()
|
|
defer func() {
|
|
r.log.Infof("Paths resolving took %s: %s", time.Since(startedAt), prog)
|
|
}()
|
|
|
|
if len(paths) == 0 {
|
|
return nil, fmt.Errorf("no paths are set")
|
|
}
|
|
|
|
bctx := build.Default
|
|
bctx.BuildTags = append(bctx.BuildTags, r.buildTags...)
|
|
prog = &Program{
|
|
bctx: bctx,
|
|
}
|
|
|
|
for _, path := range paths {
|
|
if err := r.resolvePath(path, prog); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
if len(r.skippedDirs) != 0 {
|
|
r.log.Infof("Skipped dirs: %s", r.skippedDirs)
|
|
}
|
|
|
|
return prog, nil
|
|
}
|
|
|
|
func (r *Resolver) normalizePath(path string) (string, error) {
|
|
return fsutils.ShortestRelPath(path, r.wd)
|
|
}
|
|
|
|
func (r *Resolver) resolvePath(path string, prog *Program) error {
|
|
needRecursive := strings.HasSuffix(path, "/...")
|
|
if needRecursive {
|
|
path = filepath.Dir(path)
|
|
}
|
|
|
|
evalPath, err := filepath.EvalSymlinks(path)
|
|
if err != nil {
|
|
return fmt.Errorf("can't eval symlinks for path %s: %s", path, err)
|
|
}
|
|
path = evalPath
|
|
|
|
path, err = r.normalizePath(path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if needRecursive {
|
|
if err = r.resolveRecursively(path, prog); err != nil {
|
|
return fmt.Errorf("can't recursively resolve %s: %s", path, err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
fi, err := os.Stat(path)
|
|
if err != nil {
|
|
return fmt.Errorf("can't find path %s: %s", path, err)
|
|
}
|
|
|
|
if fi.IsDir() {
|
|
if err := r.resolveDir(path, prog); err != nil {
|
|
return fmt.Errorf("can't resolve dir %s: %s", path, err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
r.addFakePackage(path, prog)
|
|
return nil
|
|
}
|