github.com/thrasher-corp/golangci-lint@v1.17.3/pkg/lint/load.go (about)

     1  package lint
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"go/build"
     7  	"go/types"
     8  	"os"
     9  	"path/filepath"
    10  	"regexp"
    11  	"strings"
    12  	"time"
    13  
    14  	"github.com/golangci/golangci-lint/pkg/fsutils"
    15  
    16  	"github.com/pkg/errors"
    17  	"golang.org/x/tools/go/loader"
    18  	"golang.org/x/tools/go/packages"
    19  	"golang.org/x/tools/go/ssa"
    20  	"golang.org/x/tools/go/ssa/ssautil"
    21  
    22  	"github.com/golangci/golangci-lint/pkg/config"
    23  	"github.com/golangci/golangci-lint/pkg/exitcodes"
    24  	"github.com/golangci/golangci-lint/pkg/goutil"
    25  	"github.com/golangci/golangci-lint/pkg/lint/astcache"
    26  	"github.com/golangci/golangci-lint/pkg/lint/linter"
    27  	"github.com/golangci/golangci-lint/pkg/logutils"
    28  )
    29  
    30  type ContextLoader struct {
    31  	cfg         *config.Config
    32  	log         logutils.Log
    33  	debugf      logutils.DebugFunc
    34  	goenv       *goutil.Env
    35  	pkgTestIDRe *regexp.Regexp
    36  	lineCache   *fsutils.LineCache
    37  	fileCache   *fsutils.FileCache
    38  }
    39  
    40  func NewContextLoader(cfg *config.Config, log logutils.Log, goenv *goutil.Env,
    41  	lineCache *fsutils.LineCache, fileCache *fsutils.FileCache) *ContextLoader {
    42  
    43  	return &ContextLoader{
    44  		cfg:         cfg,
    45  		log:         log,
    46  		debugf:      logutils.Debug("loader"),
    47  		goenv:       goenv,
    48  		pkgTestIDRe: regexp.MustCompile(`^(.*) \[(.*)\.test\]`),
    49  		lineCache:   lineCache,
    50  		fileCache:   fileCache,
    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("GOROOT")
    58  	if goroot == "" {
    59  		return
    60  	}
    61  
    62  	os.Setenv("GOROOT", goroot)
    63  	build.Default.GOROOT = goroot
    64  	build.Default.BuildTags = cl.cfg.Run.BuildTags
    65  }
    66  
    67  func (cl ContextLoader) makeFakeLoaderPackageInfo(pkg *packages.Package) *loader.PackageInfo {
    68  	var errs []error
    69  	for _, err := range pkg.Errors {
    70  		errs = append(errs, err)
    71  	}
    72  
    73  	typeInfo := &types.Info{}
    74  	if pkg.TypesInfo != nil {
    75  		typeInfo = pkg.TypesInfo
    76  	}
    77  
    78  	return &loader.PackageInfo{
    79  		Pkg:                   pkg.Types,
    80  		Importable:            true, // not used
    81  		TransitivelyErrorFree: !pkg.IllTyped,
    82  
    83  		// use compiled (preprocessed) go files AST;
    84  		// AST linters use not preprocessed go files AST
    85  		Files:  pkg.Syntax,
    86  		Errors: errs,
    87  		Info:   *typeInfo,
    88  	}
    89  }
    90  
    91  func shouldSkipPkg(pkg *packages.Package) bool {
    92  	// it's an implicit testmain package
    93  	return pkg.Name == "main" && strings.HasSuffix(pkg.PkgPath, ".test")
    94  }
    95  
    96  func (cl ContextLoader) makeFakeLoaderProgram(pkgs []*packages.Package) *loader.Program {
    97  	var createdPkgs []*loader.PackageInfo
    98  	for _, pkg := range pkgs {
    99  		if pkg.IllTyped {
   100  			// some linters crash on packages with errors,
   101  			// skip them and warn about them in another place
   102  			continue
   103  		}
   104  
   105  		pkgInfo := cl.makeFakeLoaderPackageInfo(pkg)
   106  		createdPkgs = append(createdPkgs, pkgInfo)
   107  	}
   108  
   109  	allPkgs := map[*types.Package]*loader.PackageInfo{}
   110  	for _, pkg := range createdPkgs {
   111  		pkg := pkg
   112  		allPkgs[pkg.Pkg] = pkg
   113  	}
   114  	for _, pkg := range pkgs {
   115  		if pkg.IllTyped {
   116  			// some linters crash on packages with errors,
   117  			// skip them and warn about them in another place
   118  			continue
   119  		}
   120  
   121  		for _, impPkg := range pkg.Imports {
   122  			// don't use astcache for imported packages: we don't find issues in cgo imported deps
   123  			pkgInfo := cl.makeFakeLoaderPackageInfo(impPkg)
   124  			allPkgs[pkgInfo.Pkg] = pkgInfo
   125  		}
   126  	}
   127  
   128  	return &loader.Program{
   129  		Fset:        pkgs[0].Fset,
   130  		Imported:    nil,         // not used without .Created in any linter
   131  		Created:     createdPkgs, // all initial packages
   132  		AllPackages: allPkgs,     // all initial packages and their depndencies
   133  	}
   134  }
   135  
   136  func (cl ContextLoader) buildSSAProgram(pkgs []*packages.Package) *ssa.Program {
   137  	startedAt := time.Now()
   138  	var pkgsBuiltDuration time.Duration
   139  	defer func() {
   140  		cl.log.Infof("SSA repr building timing: packages building %s, total %s",
   141  			pkgsBuiltDuration, time.Since(startedAt))
   142  	}()
   143  
   144  	ssaProg, _ := ssautil.Packages(pkgs, ssa.GlobalDebug)
   145  	pkgsBuiltDuration = time.Since(startedAt)
   146  	ssaProg.Build()
   147  	return ssaProg
   148  }
   149  
   150  func (cl ContextLoader) findLoadMode(linters []*linter.Config) packages.LoadMode {
   151  	maxLoadMode := packages.LoadFiles
   152  	for _, lc := range linters {
   153  		curLoadMode := packages.LoadFiles
   154  		if lc.NeedsTypeInfo {
   155  			curLoadMode = packages.LoadSyntax
   156  		}
   157  		if lc.NeedsSSARepr {
   158  			curLoadMode = packages.LoadAllSyntax
   159  		}
   160  		if curLoadMode > maxLoadMode {
   161  			maxLoadMode = curLoadMode
   162  		}
   163  	}
   164  
   165  	return maxLoadMode
   166  }
   167  
   168  func stringifyLoadMode(mode packages.LoadMode) string {
   169  	switch mode {
   170  	case packages.LoadFiles:
   171  		return "load files"
   172  	case packages.LoadImports:
   173  		return "load imports"
   174  	case packages.LoadTypes:
   175  		return "load types"
   176  	case packages.LoadSyntax:
   177  		return "load types and syntax"
   178  	}
   179  	// it may be an alias, and may be not
   180  	if mode == packages.LoadAllSyntax {
   181  		return "load deps types and syntax"
   182  	}
   183  	return "unknown"
   184  }
   185  
   186  func (cl ContextLoader) buildArgs() []string {
   187  	args := cl.cfg.Run.Args
   188  	if len(args) == 0 {
   189  		return []string{"./..."}
   190  	}
   191  
   192  	var retArgs []string
   193  	for _, arg := range args {
   194  		if strings.HasPrefix(arg, ".") || filepath.IsAbs(arg) {
   195  			retArgs = append(retArgs, arg)
   196  		} else {
   197  			// go/packages doesn't work well if we don't have prefix ./ for local packages
   198  			retArgs = append(retArgs, fmt.Sprintf(".%c%s", filepath.Separator, arg))
   199  		}
   200  	}
   201  
   202  	return retArgs
   203  }
   204  
   205  func (cl ContextLoader) makeBuildFlags() ([]string, error) {
   206  	var buildFlags []string
   207  
   208  	if len(cl.cfg.Run.BuildTags) != 0 {
   209  		// go help build
   210  		buildFlags = append(buildFlags, "-tags", strings.Join(cl.cfg.Run.BuildTags, " "))
   211  	}
   212  
   213  	mod := cl.cfg.Run.ModulesDownloadMode
   214  	if mod != "" {
   215  		// go help modules
   216  		allowedMods := []string{"release", "readonly", "vendor"}
   217  		var ok bool
   218  		for _, am := range allowedMods {
   219  			if am == mod {
   220  				ok = true
   221  				break
   222  			}
   223  		}
   224  		if !ok {
   225  			return nil, fmt.Errorf("invalid modules download path %s, only (%s) allowed", mod, strings.Join(allowedMods, "|"))
   226  		}
   227  
   228  		buildFlags = append(buildFlags, fmt.Sprintf("-mod=%s", cl.cfg.Run.ModulesDownloadMode))
   229  	}
   230  
   231  	return buildFlags, nil
   232  }
   233  
   234  func (cl ContextLoader) loadPackages(ctx context.Context, loadMode packages.LoadMode) ([]*packages.Package, error) {
   235  	defer func(startedAt time.Time) {
   236  		cl.log.Infof("Go packages loading at mode %s took %s", stringifyLoadMode(loadMode), time.Since(startedAt))
   237  	}(time.Now())
   238  
   239  	cl.prepareBuildContext()
   240  
   241  	buildFlags, err := cl.makeBuildFlags()
   242  	if err != nil {
   243  		return nil, errors.Wrap(err, "failed to make build flags for go list")
   244  	}
   245  
   246  	conf := &packages.Config{
   247  		Mode:       loadMode,
   248  		Tests:      cl.cfg.Run.AnalyzeTests,
   249  		Context:    ctx,
   250  		BuildFlags: buildFlags,
   251  		//TODO: use fset, parsefile, overlay
   252  	}
   253  
   254  	args := cl.buildArgs()
   255  	cl.debugf("Built loader args are %s", args)
   256  	pkgs, err := packages.Load(conf, args...)
   257  	if err != nil {
   258  		return nil, errors.Wrap(err, "failed to load program with go/packages")
   259  	}
   260  	cl.debugf("loaded %d pkgs", len(pkgs))
   261  	for i, pkg := range pkgs {
   262  		var syntaxFiles []string
   263  		for _, sf := range pkg.Syntax {
   264  			syntaxFiles = append(syntaxFiles, pkg.Fset.Position(sf.Pos()).Filename)
   265  		}
   266  		cl.debugf("Loaded pkg #%d: ID=%s GoFiles=%s CompiledGoFiles=%s Syntax=%s",
   267  			i, pkg.ID, pkg.GoFiles, pkg.CompiledGoFiles, syntaxFiles)
   268  	}
   269  
   270  	for _, pkg := range pkgs {
   271  		for _, err := range pkg.Errors {
   272  			if strings.Contains(err.Msg, "no Go files") {
   273  				return nil, errors.Wrapf(exitcodes.ErrNoGoFiles, "package %s", pkg.PkgPath)
   274  			}
   275  			if strings.Contains(err.Msg, "cannot find package") {
   276  				// when analyzing not existing directory
   277  				return nil, errors.Wrap(exitcodes.ErrFailure, err.Msg)
   278  			}
   279  		}
   280  	}
   281  
   282  	return cl.filterPackages(pkgs), nil
   283  }
   284  
   285  func (cl ContextLoader) tryParseTestPackage(pkg *packages.Package) (name, testName string, isTest bool) {
   286  	matches := cl.pkgTestIDRe.FindStringSubmatch(pkg.ID)
   287  	if matches == nil {
   288  		return "", "", false
   289  	}
   290  
   291  	return matches[1], matches[2], true
   292  }
   293  
   294  func (cl ContextLoader) filterPackages(pkgs []*packages.Package) []*packages.Package {
   295  	packagesWithTests := map[string]bool{}
   296  	for _, pkg := range pkgs {
   297  		name, _, isTest := cl.tryParseTestPackage(pkg)
   298  		if !isTest {
   299  			continue
   300  		}
   301  		packagesWithTests[name] = true
   302  	}
   303  
   304  	cl.debugf("package with tests: %#v", packagesWithTests)
   305  
   306  	var retPkgs []*packages.Package
   307  	for _, pkg := range pkgs {
   308  		if shouldSkipPkg(pkg) {
   309  			cl.debugf("skip pkg ID=%s", pkg.ID)
   310  			continue
   311  		}
   312  
   313  		_, _, isTest := cl.tryParseTestPackage(pkg)
   314  		if !isTest && packagesWithTests[pkg.PkgPath] {
   315  			// If tests loading is enabled,
   316  			// for package with files a.go and a_test.go go/packages loads two packages:
   317  			// 1. ID=".../a" GoFiles=[a.go]
   318  			// 2. ID=".../a [.../a.test]" GoFiles=[a.go a_test.go]
   319  			// We need only the second package, otherwise we can get warnings about unused variables/fields/functions
   320  			// in a.go if they are used only in a_test.go.
   321  			cl.debugf("skip pkg ID=%s because we load it with test package", pkg.ID)
   322  			continue
   323  		}
   324  
   325  		retPkgs = append(retPkgs, pkg)
   326  	}
   327  
   328  	return retPkgs
   329  }
   330  
   331  //nolint:gocyclo
   332  func (cl ContextLoader) Load(ctx context.Context, linters []*linter.Config) (*linter.Context, error) {
   333  	loadMode := cl.findLoadMode(linters)
   334  	pkgs, err := cl.loadPackages(ctx, loadMode)
   335  	if err != nil {
   336  		return nil, err
   337  	}
   338  
   339  	if len(pkgs) == 0 {
   340  		return nil, exitcodes.ErrNoGoFiles
   341  	}
   342  
   343  	var prog *loader.Program
   344  	if loadMode >= packages.LoadSyntax {
   345  		prog = cl.makeFakeLoaderProgram(pkgs)
   346  	}
   347  
   348  	var ssaProg *ssa.Program
   349  	if loadMode == packages.LoadAllSyntax {
   350  		ssaProg = cl.buildSSAProgram(pkgs)
   351  	}
   352  
   353  	astLog := cl.log.Child("astcache")
   354  	astCache, err := astcache.LoadFromPackages(pkgs, astLog)
   355  	if err != nil {
   356  		return nil, err
   357  	}
   358  
   359  	ret := &linter.Context{
   360  		Packages:   pkgs,
   361  		Program:    prog,
   362  		SSAProgram: ssaProg,
   363  		LoaderConfig: &loader.Config{
   364  			Cwd:   "",  // used by depguard and fallbacked to os.Getcwd
   365  			Build: nil, // used by depguard and megacheck and fallbacked to build.Default
   366  		},
   367  		Cfg:       cl.cfg,
   368  		ASTCache:  astCache,
   369  		Log:       cl.log,
   370  		FileCache: cl.fileCache,
   371  		LineCache: cl.lineCache,
   372  	}
   373  
   374  	separateNotCompilingPackages(ret)
   375  	return ret, nil
   376  }
   377  
   378  // separateNotCompilingPackages moves not compiling packages into separate slice:
   379  // a lot of linters crash on such packages
   380  func separateNotCompilingPackages(lintCtx *linter.Context) {
   381  	goodPkgs := make([]*packages.Package, 0, len(lintCtx.Packages))
   382  	for _, pkg := range lintCtx.Packages {
   383  		if pkg.IllTyped {
   384  			lintCtx.NotCompilingPackages = append(lintCtx.NotCompilingPackages, pkg)
   385  		} else {
   386  			goodPkgs = append(goodPkgs, pkg)
   387  		}
   388  	}
   389  
   390  	lintCtx.Packages = goodPkgs
   391  	if len(lintCtx.NotCompilingPackages) != 0 {
   392  		lintCtx.Log.Infof("Packages that do not compile: %+v", lintCtx.NotCompilingPackages)
   393  	}
   394  }