
Before that fix golangci-lint couldn't run analysis if in any dir there was go file with corrupted `package` or `import` statement. After that fix it will only warn about such files and continue analysis. But it will fail analysis after finding 10 packages with such errors to not being too noisy in case of internal error.
231 lines
5.0 KiB
Go
231 lines
5.0 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
|
|
importErrorsOccured int // count of errors because too bad files in packages
|
|
}
|
|
|
|
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 read 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
|
|
}
|
|
|
|
err = fmt.Errorf("can't import dir %q: %s", dir, err)
|
|
r.importErrorsOccured++
|
|
if r.importErrorsOccured >= 10 {
|
|
return err
|
|
}
|
|
|
|
r.log.Warnf("Can't analyze dir %q: %s", dir, err)
|
|
return nil
|
|
}
|
|
|
|
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
|
|
}
|