316 lines
		
	
	
		
			8.5 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			316 lines
		
	
	
		
			8.5 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("loader"),
 | 
						|
		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("GOROOT", 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
 | 
						|
}
 |