github.com/nozzle/golangci-lint@v1.49.0-nz3/pkg/lint/load.go (about)

     1  package lint
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"go/build"
     7  	"go/token"
     8  	"os"
     9  	"path/filepath"
    10  	"regexp"
    11  	"strings"
    12  	"time"
    13  
    14  	"github.com/pkg/errors"
    15  	"golang.org/x/tools/go/packages"
    16  
    17  	"github.com/golangci/golangci-lint/internal/pkgcache"
    18  	"github.com/golangci/golangci-lint/pkg/config"
    19  	"github.com/golangci/golangci-lint/pkg/exitcodes"
    20  	"github.com/golangci/golangci-lint/pkg/fsutils"
    21  	"github.com/golangci/golangci-lint/pkg/golinters/goanalysis/load"
    22  	"github.com/golangci/golangci-lint/pkg/goutil"
    23  	"github.com/golangci/golangci-lint/pkg/lint/linter"
    24  	"github.com/golangci/golangci-lint/pkg/logutils"
    25  )
    26  
    27  type ContextLoader struct {
    28  	cfg         *config.Config
    29  	log         logutils.Log
    30  	debugf      logutils.DebugFunc
    31  	goenv       *goutil.Env
    32  	pkgTestIDRe *regexp.Regexp
    33  	lineCache   *fsutils.LineCache
    34  	fileCache   *fsutils.FileCache
    35  	pkgCache    *pkgcache.Cache
    36  	loadGuard   *load.Guard
    37  }
    38  
    39  func NewContextLoader(cfg *config.Config, log logutils.Log, goenv *goutil.Env,
    40  	lineCache *fsutils.LineCache, fileCache *fsutils.FileCache, pkgCache *pkgcache.Cache, loadGuard *load.Guard) *ContextLoader {
    41  	return &ContextLoader{
    42  		cfg:         cfg,
    43  		log:         log,
    44  		debugf:      logutils.Debug(logutils.DebugKeyLoader),
    45  		goenv:       goenv,
    46  		pkgTestIDRe: regexp.MustCompile(`^(.*) \[(.*)\.test\]`),
    47  		lineCache:   lineCache,
    48  		fileCache:   fileCache,
    49  		pkgCache:    pkgCache,
    50  		loadGuard:   loadGuard,
    51  	}
    52  }
    53  
    54  func (cl *ContextLoader) prepareBuildContext() {
    55  	// Set GOROOT to have working cross-compilation: cross-compiled binaries
    56  	// have invalid GOROOT. XXX: can't use runtime.GOROOT().
    57  	goroot := cl.goenv.Get(goutil.EnvGoRoot)
    58  	if goroot == "" {
    59  		return
    60  	}
    61  
    62  	os.Setenv(string(goutil.EnvGoRoot), goroot)
    63  	build.Default.GOROOT = goroot
    64  	build.Default.BuildTags = cl.cfg.Run.BuildTags
    65  }
    66  
    67  func (cl *ContextLoader) findLoadMode(linters []*linter.Config) packages.LoadMode {
    68  	loadMode := packages.LoadMode(0)
    69  	for _, lc := range linters {
    70  		loadMode |= lc.LoadMode
    71  	}
    72  
    73  	return loadMode
    74  }
    75  
    76  func (cl *ContextLoader) buildArgs() []string {
    77  	args := cl.cfg.Run.Args
    78  	if len(args) == 0 {
    79  		return []string{"./..."}
    80  	}
    81  
    82  	var retArgs []string
    83  	for _, arg := range args {
    84  		if strings.HasPrefix(arg, ".") || filepath.IsAbs(arg) {
    85  			retArgs = append(retArgs, arg)
    86  		} else {
    87  			// go/packages doesn't work well if we don't have the prefix ./ for local packages
    88  			retArgs = append(retArgs, fmt.Sprintf(".%c%s", filepath.Separator, arg))
    89  		}
    90  	}
    91  
    92  	return retArgs
    93  }
    94  
    95  func (cl *ContextLoader) makeBuildFlags() ([]string, error) {
    96  	var buildFlags []string
    97  
    98  	if len(cl.cfg.Run.BuildTags) != 0 {
    99  		// go help build
   100  		buildFlags = append(buildFlags, "-tags", strings.Join(cl.cfg.Run.BuildTags, " "))
   101  		cl.log.Infof("Using build tags: %v", cl.cfg.Run.BuildTags)
   102  	}
   103  
   104  	mod := cl.cfg.Run.ModulesDownloadMode
   105  	if mod != "" {
   106  		// go help modules
   107  		allowedMods := []string{"mod", "readonly", "vendor"}
   108  		var ok bool
   109  		for _, am := range allowedMods {
   110  			if am == mod {
   111  				ok = true
   112  				break
   113  			}
   114  		}
   115  		if !ok {
   116  			return nil, fmt.Errorf("invalid modules download path %s, only (%s) allowed", mod, strings.Join(allowedMods, "|"))
   117  		}
   118  
   119  		buildFlags = append(buildFlags, fmt.Sprintf("-mod=%s", cl.cfg.Run.ModulesDownloadMode))
   120  	}
   121  
   122  	return buildFlags, nil
   123  }
   124  
   125  func stringifyLoadMode(mode packages.LoadMode) string {
   126  	m := map[packages.LoadMode]string{
   127  		packages.NeedCompiledGoFiles: "compiled_files",
   128  		packages.NeedDeps:            "deps",
   129  		packages.NeedExportFile:      "exports_file",
   130  		packages.NeedFiles:           "files",
   131  		packages.NeedImports:         "imports",
   132  		packages.NeedName:            "name",
   133  		packages.NeedSyntax:          "syntax",
   134  		packages.NeedTypes:           "types",
   135  		packages.NeedTypesInfo:       "types_info",
   136  		packages.NeedTypesSizes:      "types_sizes",
   137  	}
   138  
   139  	var flags []string
   140  	for flag, flagStr := range m {
   141  		if mode&flag != 0 {
   142  			flags = append(flags, flagStr)
   143  		}
   144  	}
   145  
   146  	return fmt.Sprintf("%d (%s)", mode, strings.Join(flags, "|"))
   147  }
   148  
   149  func (cl *ContextLoader) debugPrintLoadedPackages(pkgs []*packages.Package) {
   150  	cl.debugf("loaded %d pkgs", len(pkgs))
   151  	for i, pkg := range pkgs {
   152  		var syntaxFiles []string
   153  		for _, sf := range pkg.Syntax {
   154  			syntaxFiles = append(syntaxFiles, pkg.Fset.Position(sf.Pos()).Filename)
   155  		}
   156  		cl.debugf("Loaded pkg #%d: ID=%s GoFiles=%s CompiledGoFiles=%s Syntax=%s",
   157  			i, pkg.ID, pkg.GoFiles, pkg.CompiledGoFiles, syntaxFiles)
   158  	}
   159  }
   160  
   161  func (cl *ContextLoader) parseLoadedPackagesErrors(pkgs []*packages.Package) error {
   162  	for _, pkg := range pkgs {
   163  		for _, err := range pkg.Errors {
   164  			if strings.Contains(err.Msg, "no Go files") {
   165  				return errors.Wrapf(exitcodes.ErrNoGoFiles, "package %s", pkg.PkgPath)
   166  			}
   167  			if strings.Contains(err.Msg, "cannot find package") {
   168  				// when analyzing not existing directory
   169  				return errors.Wrap(exitcodes.ErrFailure, err.Msg)
   170  			}
   171  		}
   172  	}
   173  
   174  	return nil
   175  }
   176  
   177  func (cl *ContextLoader) loadPackages(ctx context.Context, loadMode packages.LoadMode) ([]*packages.Package, error) {
   178  	defer func(startedAt time.Time) {
   179  		cl.log.Infof("Go packages loading at mode %s took %s", stringifyLoadMode(loadMode), time.Since(startedAt))
   180  	}(time.Now())
   181  
   182  	cl.prepareBuildContext()
   183  
   184  	buildFlags, err := cl.makeBuildFlags()
   185  	if err != nil {
   186  		return nil, errors.Wrap(err, "failed to make build flags for go list")
   187  	}
   188  
   189  	conf := &packages.Config{
   190  		Mode:       loadMode,
   191  		Tests:      cl.cfg.Run.AnalyzeTests,
   192  		Context:    ctx,
   193  		BuildFlags: buildFlags,
   194  		Logf:       cl.debugf,
   195  		// TODO: use fset, parsefile, overlay
   196  	}
   197  
   198  	args := cl.buildArgs()
   199  	cl.debugf("Built loader args are %s", args)
   200  	pkgs, err := packages.Load(conf, args...)
   201  	if err != nil {
   202  		return nil, errors.Wrap(err, "failed to load with go/packages")
   203  	}
   204  
   205  	// Currently, go/packages doesn't guarantee that error will be returned
   206  	// if context was canceled. See
   207  	// https://github.com/golang/tools/commit/c5cec6710e927457c3c29d6c156415e8539a5111#r39261855
   208  	if ctx.Err() != nil {
   209  		return nil, errors.Wrap(ctx.Err(), "timed out to load packages")
   210  	}
   211  
   212  	if loadMode&packages.NeedSyntax == 0 {
   213  		// Needed e.g. for go/analysis loading.
   214  		fset := token.NewFileSet()
   215  		packages.Visit(pkgs, nil, func(pkg *packages.Package) {
   216  			pkg.Fset = fset
   217  			cl.loadGuard.AddMutexForPkg(pkg)
   218  		})
   219  	}
   220  
   221  	cl.debugPrintLoadedPackages(pkgs)
   222  
   223  	if err := cl.parseLoadedPackagesErrors(pkgs); err != nil {
   224  		return nil, err
   225  	}
   226  
   227  	return cl.filterTestMainPackages(pkgs), nil
   228  }
   229  
   230  func (cl *ContextLoader) tryParseTestPackage(pkg *packages.Package) (name string, isTest bool) {
   231  	matches := cl.pkgTestIDRe.FindStringSubmatch(pkg.ID)
   232  	if matches == nil {
   233  		return "", false
   234  	}
   235  
   236  	return matches[1], true
   237  }
   238  
   239  func (cl *ContextLoader) filterTestMainPackages(pkgs []*packages.Package) []*packages.Package {
   240  	var retPkgs []*packages.Package
   241  	for _, pkg := range pkgs {
   242  		if pkg.Name == "main" && strings.HasSuffix(pkg.PkgPath, ".test") {
   243  			// it's an implicit testmain package
   244  			cl.debugf("skip pkg ID=%s", pkg.ID)
   245  			continue
   246  		}
   247  
   248  		retPkgs = append(retPkgs, pkg)
   249  	}
   250  
   251  	return retPkgs
   252  }
   253  
   254  func (cl *ContextLoader) filterDuplicatePackages(pkgs []*packages.Package) []*packages.Package {
   255  	packagesWithTests := map[string]bool{}
   256  	for _, pkg := range pkgs {
   257  		name, isTest := cl.tryParseTestPackage(pkg)
   258  		if !isTest {
   259  			continue
   260  		}
   261  		packagesWithTests[name] = true
   262  	}
   263  
   264  	cl.debugf("package with tests: %#v", packagesWithTests)
   265  
   266  	var retPkgs []*packages.Package
   267  	for _, pkg := range pkgs {
   268  		_, isTest := cl.tryParseTestPackage(pkg)
   269  		if !isTest && packagesWithTests[pkg.PkgPath] {
   270  			// If tests loading is enabled,
   271  			// for package with files a.go and a_test.go go/packages loads two packages:
   272  			// 1. ID=".../a" GoFiles=[a.go]
   273  			// 2. ID=".../a [.../a.test]" GoFiles=[a.go a_test.go]
   274  			// We need only the second package, otherwise we can get warnings about unused variables/fields/functions
   275  			// in a.go if they are used only in a_test.go.
   276  			cl.debugf("skip pkg ID=%s because we load it with test package", pkg.ID)
   277  			continue
   278  		}
   279  
   280  		retPkgs = append(retPkgs, pkg)
   281  	}
   282  
   283  	return retPkgs
   284  }
   285  
   286  func (cl *ContextLoader) Load(ctx context.Context, linters []*linter.Config) (*linter.Context, error) {
   287  	loadMode := cl.findLoadMode(linters)
   288  	pkgs, err := cl.loadPackages(ctx, loadMode)
   289  	if err != nil {
   290  		return nil, errors.Wrap(err, "failed to load packages")
   291  	}
   292  
   293  	deduplicatedPkgs := cl.filterDuplicatePackages(pkgs)
   294  
   295  	if len(deduplicatedPkgs) == 0 {
   296  		return nil, exitcodes.ErrNoGoFiles
   297  	}
   298  
   299  	ret := &linter.Context{
   300  		Packages: deduplicatedPkgs,
   301  
   302  		// At least `unused` linters works properly only on original (not deduplicated) packages,
   303  		// see https://github.com/golangci/golangci-lint/pull/585.
   304  		OriginalPackages: pkgs,
   305  
   306  		Cfg:       cl.cfg,
   307  		Log:       cl.log,
   308  		FileCache: cl.fileCache,
   309  		LineCache: cl.lineCache,
   310  		PkgCache:  cl.pkgCache,
   311  		LoadGuard: cl.loadGuard,
   312  	}
   313  
   314  	return ret, nil
   315  }