316 lines
		
	
	
		
			8.6 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			316 lines
		
	
	
		
			8.6 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| package lint
 | |
| 
 | |
| import (
 | |
| 	"context"
 | |
| 	"fmt"
 | |
| 	"go/build"
 | |
| 	"go/token"
 | |
| 	"os"
 | |
| 	"path/filepath"
 | |
| 	"regexp"
 | |
| 	"strings"
 | |
| 	"time"
 | |
| 
 | |
| 	"github.com/pkg/errors"
 | |
| 	"golang.org/x/tools/go/packages"
 | |
| 
 | |
| 	"github.com/golangci/golangci-lint/internal/pkgcache"
 | |
| 	"github.com/golangci/golangci-lint/pkg/config"
 | |
| 	"github.com/golangci/golangci-lint/pkg/exitcodes"
 | |
| 	"github.com/golangci/golangci-lint/pkg/fsutils"
 | |
| 	"github.com/golangci/golangci-lint/pkg/golinters/goanalysis/load"
 | |
| 	"github.com/golangci/golangci-lint/pkg/goutil"
 | |
| 	"github.com/golangci/golangci-lint/pkg/lint/linter"
 | |
| 	"github.com/golangci/golangci-lint/pkg/logutils"
 | |
| )
 | |
| 
 | |
| type ContextLoader struct {
 | |
| 	cfg         *config.Config
 | |
| 	log         logutils.Log
 | |
| 	debugf      logutils.DebugFunc
 | |
| 	goenv       *goutil.Env
 | |
| 	pkgTestIDRe *regexp.Regexp
 | |
| 	lineCache   *fsutils.LineCache
 | |
| 	fileCache   *fsutils.FileCache
 | |
| 	pkgCache    *pkgcache.Cache
 | |
| 	loadGuard   *load.Guard
 | |
| }
 | |
| 
 | |
| func NewContextLoader(cfg *config.Config, log logutils.Log, goenv *goutil.Env,
 | |
| 	lineCache *fsutils.LineCache, fileCache *fsutils.FileCache, pkgCache *pkgcache.Cache, loadGuard *load.Guard) *ContextLoader {
 | |
| 	return &ContextLoader{
 | |
| 		cfg:         cfg,
 | |
| 		log:         log,
 | |
| 		debugf:      logutils.Debug(logutils.DebugKeyLoader),
 | |
| 		goenv:       goenv,
 | |
| 		pkgTestIDRe: regexp.MustCompile(`^(.*) \[(.*)\.test\]`),
 | |
| 		lineCache:   lineCache,
 | |
| 		fileCache:   fileCache,
 | |
| 		pkgCache:    pkgCache,
 | |
| 		loadGuard:   loadGuard,
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func (cl *ContextLoader) prepareBuildContext() {
 | |
| 	// Set GOROOT to have working cross-compilation: cross-compiled binaries
 | |
| 	// have invalid GOROOT. XXX: can't use runtime.GOROOT().
 | |
| 	goroot := cl.goenv.Get(goutil.EnvGoRoot)
 | |
| 	if goroot == "" {
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	os.Setenv(string(goutil.EnvGoRoot), goroot)
 | |
| 	build.Default.GOROOT = goroot
 | |
| 	build.Default.BuildTags = cl.cfg.Run.BuildTags
 | |
| }
 | |
| 
 | |
| func (cl *ContextLoader) findLoadMode(linters []*linter.Config) packages.LoadMode {
 | |
| 	loadMode := packages.LoadMode(0)
 | |
| 	for _, lc := range linters {
 | |
| 		loadMode |= lc.LoadMode
 | |
| 	}
 | |
| 
 | |
| 	return loadMode
 | |
| }
 | |
| 
 | |
| func (cl *ContextLoader) buildArgs() []string {
 | |
| 	args := cl.cfg.Run.Args
 | |
| 	if len(args) == 0 {
 | |
| 		return []string{"./..."}
 | |
| 	}
 | |
| 
 | |
| 	var retArgs []string
 | |
| 	for _, arg := range args {
 | |
| 		if strings.HasPrefix(arg, ".") || filepath.IsAbs(arg) {
 | |
| 			retArgs = append(retArgs, arg)
 | |
| 		} else {
 | |
| 			// go/packages doesn't work well if we don't have the prefix ./ for local packages
 | |
| 			retArgs = append(retArgs, fmt.Sprintf(".%c%s", filepath.Separator, arg))
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	return retArgs
 | |
| }
 | |
| 
 | |
| func (cl *ContextLoader) makeBuildFlags() ([]string, error) {
 | |
| 	var buildFlags []string
 | |
| 
 | |
| 	if len(cl.cfg.Run.BuildTags) != 0 {
 | |
| 		// go help build
 | |
| 		buildFlags = append(buildFlags, "-tags", strings.Join(cl.cfg.Run.BuildTags, " "))
 | |
| 		cl.log.Infof("Using build tags: %v", cl.cfg.Run.BuildTags)
 | |
| 	}
 | |
| 
 | |
| 	mod := cl.cfg.Run.ModulesDownloadMode
 | |
| 	if mod != "" {
 | |
| 		// go help modules
 | |
| 		allowedMods := []string{"mod", "readonly", "vendor"}
 | |
| 		var ok bool
 | |
| 		for _, am := range allowedMods {
 | |
| 			if am == mod {
 | |
| 				ok = true
 | |
| 				break
 | |
| 			}
 | |
| 		}
 | |
| 		if !ok {
 | |
| 			return nil, fmt.Errorf("invalid modules download path %s, only (%s) allowed", mod, strings.Join(allowedMods, "|"))
 | |
| 		}
 | |
| 
 | |
| 		buildFlags = append(buildFlags, fmt.Sprintf("-mod=%s", cl.cfg.Run.ModulesDownloadMode))
 | |
| 	}
 | |
| 
 | |
| 	return buildFlags, nil
 | |
| }
 | |
| 
 | |
| func stringifyLoadMode(mode packages.LoadMode) string {
 | |
| 	m := map[packages.LoadMode]string{
 | |
| 		packages.NeedCompiledGoFiles: "compiled_files",
 | |
| 		packages.NeedDeps:            "deps",
 | |
| 		packages.NeedExportFile:      "exports_file",
 | |
| 		packages.NeedFiles:           "files",
 | |
| 		packages.NeedImports:         "imports",
 | |
| 		packages.NeedName:            "name",
 | |
| 		packages.NeedSyntax:          "syntax",
 | |
| 		packages.NeedTypes:           "types",
 | |
| 		packages.NeedTypesInfo:       "types_info",
 | |
| 		packages.NeedTypesSizes:      "types_sizes",
 | |
| 	}
 | |
| 
 | |
| 	var flags []string
 | |
| 	for flag, flagStr := range m {
 | |
| 		if mode&flag != 0 {
 | |
| 			flags = append(flags, flagStr)
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	return fmt.Sprintf("%d (%s)", mode, strings.Join(flags, "|"))
 | |
| }
 | |
| 
 | |
| func (cl *ContextLoader) debugPrintLoadedPackages(pkgs []*packages.Package) {
 | |
| 	cl.debugf("loaded %d pkgs", len(pkgs))
 | |
| 	for i, pkg := range pkgs {
 | |
| 		var syntaxFiles []string
 | |
| 		for _, sf := range pkg.Syntax {
 | |
| 			syntaxFiles = append(syntaxFiles, pkg.Fset.Position(sf.Pos()).Filename)
 | |
| 		}
 | |
| 		cl.debugf("Loaded pkg #%d: ID=%s GoFiles=%s CompiledGoFiles=%s Syntax=%s",
 | |
| 			i, pkg.ID, pkg.GoFiles, pkg.CompiledGoFiles, syntaxFiles)
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func (cl *ContextLoader) parseLoadedPackagesErrors(pkgs []*packages.Package) error {
 | |
| 	for _, pkg := range pkgs {
 | |
| 		for _, err := range pkg.Errors {
 | |
| 			if strings.Contains(err.Msg, "no Go files") {
 | |
| 				return errors.Wrapf(exitcodes.ErrNoGoFiles, "package %s", pkg.PkgPath)
 | |
| 			}
 | |
| 			if strings.Contains(err.Msg, "cannot find package") {
 | |
| 				// when analyzing not existing directory
 | |
| 				return errors.Wrap(exitcodes.ErrFailure, err.Msg)
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| func (cl *ContextLoader) loadPackages(ctx context.Context, loadMode packages.LoadMode) ([]*packages.Package, error) {
 | |
| 	defer func(startedAt time.Time) {
 | |
| 		cl.log.Infof("Go packages loading at mode %s took %s", stringifyLoadMode(loadMode), time.Since(startedAt))
 | |
| 	}(time.Now())
 | |
| 
 | |
| 	cl.prepareBuildContext()
 | |
| 
 | |
| 	buildFlags, err := cl.makeBuildFlags()
 | |
| 	if err != nil {
 | |
| 		return nil, errors.Wrap(err, "failed to make build flags for go list")
 | |
| 	}
 | |
| 
 | |
| 	conf := &packages.Config{
 | |
| 		Mode:       loadMode,
 | |
| 		Tests:      cl.cfg.Run.AnalyzeTests,
 | |
| 		Context:    ctx,
 | |
| 		BuildFlags: buildFlags,
 | |
| 		Logf:       cl.debugf,
 | |
| 		// TODO: use fset, parsefile, overlay
 | |
| 	}
 | |
| 
 | |
| 	args := cl.buildArgs()
 | |
| 	cl.debugf("Built loader args are %s", args)
 | |
| 	pkgs, err := packages.Load(conf, args...)
 | |
| 	if err != nil {
 | |
| 		return nil, errors.Wrap(err, "failed to load with go/packages")
 | |
| 	}
 | |
| 
 | |
| 	// Currently, go/packages doesn't guarantee that error will be returned
 | |
| 	// if context was canceled. See
 | |
| 	// https://github.com/golang/tools/commit/c5cec6710e927457c3c29d6c156415e8539a5111#r39261855
 | |
| 	if ctx.Err() != nil {
 | |
| 		return nil, errors.Wrap(ctx.Err(), "timed out to load packages")
 | |
| 	}
 | |
| 
 | |
| 	if loadMode&packages.NeedSyntax == 0 {
 | |
| 		// Needed e.g. for go/analysis loading.
 | |
| 		fset := token.NewFileSet()
 | |
| 		packages.Visit(pkgs, nil, func(pkg *packages.Package) {
 | |
| 			pkg.Fset = fset
 | |
| 			cl.loadGuard.AddMutexForPkg(pkg)
 | |
| 		})
 | |
| 	}
 | |
| 
 | |
| 	cl.debugPrintLoadedPackages(pkgs)
 | |
| 
 | |
| 	if err := cl.parseLoadedPackagesErrors(pkgs); err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 
 | |
| 	return cl.filterTestMainPackages(pkgs), nil
 | |
| }
 | |
| 
 | |
| func (cl *ContextLoader) tryParseTestPackage(pkg *packages.Package) (name string, isTest bool) {
 | |
| 	matches := cl.pkgTestIDRe.FindStringSubmatch(pkg.ID)
 | |
| 	if matches == nil {
 | |
| 		return "", false
 | |
| 	}
 | |
| 
 | |
| 	return matches[1], true
 | |
| }
 | |
| 
 | |
| func (cl *ContextLoader) filterTestMainPackages(pkgs []*packages.Package) []*packages.Package {
 | |
| 	var retPkgs []*packages.Package
 | |
| 	for _, pkg := range pkgs {
 | |
| 		if pkg.Name == "main" && strings.HasSuffix(pkg.PkgPath, ".test") {
 | |
| 			// it's an implicit testmain package
 | |
| 			cl.debugf("skip pkg ID=%s", pkg.ID)
 | |
| 			continue
 | |
| 		}
 | |
| 
 | |
| 		retPkgs = append(retPkgs, pkg)
 | |
| 	}
 | |
| 
 | |
| 	return retPkgs
 | |
| }
 | |
| 
 | |
| func (cl *ContextLoader) filterDuplicatePackages(pkgs []*packages.Package) []*packages.Package {
 | |
| 	packagesWithTests := map[string]bool{}
 | |
| 	for _, pkg := range pkgs {
 | |
| 		name, isTest := cl.tryParseTestPackage(pkg)
 | |
| 		if !isTest {
 | |
| 			continue
 | |
| 		}
 | |
| 		packagesWithTests[name] = true
 | |
| 	}
 | |
| 
 | |
| 	cl.debugf("package with tests: %#v", packagesWithTests)
 | |
| 
 | |
| 	var retPkgs []*packages.Package
 | |
| 	for _, pkg := range pkgs {
 | |
| 		_, isTest := cl.tryParseTestPackage(pkg)
 | |
| 		if !isTest && packagesWithTests[pkg.PkgPath] {
 | |
| 			// If tests loading is enabled,
 | |
| 			// for package with files a.go and a_test.go go/packages loads two packages:
 | |
| 			// 1. ID=".../a" GoFiles=[a.go]
 | |
| 			// 2. ID=".../a [.../a.test]" GoFiles=[a.go a_test.go]
 | |
| 			// We need only the second package, otherwise we can get warnings about unused variables/fields/functions
 | |
| 			// in a.go if they are used only in a_test.go.
 | |
| 			cl.debugf("skip pkg ID=%s because we load it with test package", pkg.ID)
 | |
| 			continue
 | |
| 		}
 | |
| 
 | |
| 		retPkgs = append(retPkgs, pkg)
 | |
| 	}
 | |
| 
 | |
| 	return retPkgs
 | |
| }
 | |
| 
 | |
| func (cl *ContextLoader) Load(ctx context.Context, linters []*linter.Config) (*linter.Context, error) {
 | |
| 	loadMode := cl.findLoadMode(linters)
 | |
| 	pkgs, err := cl.loadPackages(ctx, loadMode)
 | |
| 	if err != nil {
 | |
| 		return nil, errors.Wrap(err, "failed to load packages")
 | |
| 	}
 | |
| 
 | |
| 	deduplicatedPkgs := cl.filterDuplicatePackages(pkgs)
 | |
| 
 | |
| 	if len(deduplicatedPkgs) == 0 {
 | |
| 		return nil, exitcodes.ErrNoGoFiles
 | |
| 	}
 | |
| 
 | |
| 	ret := &linter.Context{
 | |
| 		Packages: deduplicatedPkgs,
 | |
| 
 | |
| 		// At least `unused` linters works properly only on original (not deduplicated) packages,
 | |
| 		// see https://github.com/golangci/golangci-lint/pull/585.
 | |
| 		OriginalPackages: pkgs,
 | |
| 
 | |
| 		Cfg:       cl.cfg,
 | |
| 		Log:       cl.log,
 | |
| 		FileCache: cl.fileCache,
 | |
| 		LineCache: cl.lineCache,
 | |
| 		PkgCache:  cl.pkgCache,
 | |
| 		LoadGuard: cl.loadGuard,
 | |
| 	}
 | |
| 
 | |
| 	return ret, nil
 | |
| }
 | 
