231 lines
		
	
	
		
			5.1 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			231 lines
		
	
	
		
			5.1 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 := fsutils.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
 | 
						|
}
 |