www.github.com/golangci/golangci-lint.git@v1.10.1/pkg/lint/load.go (about)

     1  package lint
     2  
     3  import (
     4  	"fmt"
     5  	"go/build"
     6  	"go/parser"
     7  	"go/types"
     8  	"os"
     9  	"path/filepath"
    10  	"strings"
    11  	"time"
    12  
    13  	"github.com/golangci/golangci-lint/pkg/exitcodes"
    14  	"github.com/golangci/golangci-lint/pkg/fsutils"
    15  	"github.com/golangci/golangci-lint/pkg/goutils"
    16  	"github.com/golangci/golangci-lint/pkg/logutils"
    17  
    18  	"github.com/golangci/golangci-lint/pkg/config"
    19  	"github.com/golangci/golangci-lint/pkg/lint/astcache"
    20  	"github.com/golangci/golangci-lint/pkg/lint/linter"
    21  	"github.com/golangci/golangci-lint/pkg/packages"
    22  	"golang.org/x/tools/go/loader"
    23  	"golang.org/x/tools/go/ssa"
    24  	"golang.org/x/tools/go/ssa/ssautil"
    25  )
    26  
    27  var loadDebugf = logutils.Debug("load")
    28  
    29  func isFullImportNeeded(linters []linter.Config, cfg *config.Config) bool {
    30  	for _, linter := range linters {
    31  		if linter.NeedsProgramLoading() {
    32  			if linter.Linter.Name() == "govet" && cfg.LintersSettings.Govet.UseInstalledPackages {
    33  				// TODO: remove this hack
    34  				continue
    35  			}
    36  
    37  			return true
    38  		}
    39  	}
    40  
    41  	return false
    42  }
    43  
    44  func isSSAReprNeeded(linters []linter.Config) bool {
    45  	for _, linter := range linters {
    46  		if linter.NeedsSSARepresentation() {
    47  			return true
    48  		}
    49  	}
    50  
    51  	return false
    52  }
    53  
    54  func normalizePaths(paths []string) ([]string, error) {
    55  	ret := make([]string, 0, len(paths))
    56  	for _, p := range paths {
    57  		relPath, err := fsutils.ShortestRelPath(p, "")
    58  		if err != nil {
    59  			return nil, fmt.Errorf("can't get relative path for path %s: %s", p, err)
    60  		}
    61  		p = relPath
    62  
    63  		ret = append(ret, "./"+p)
    64  	}
    65  
    66  	return ret, nil
    67  }
    68  
    69  func getCurrentProjectImportPath() (string, error) {
    70  	gopath := os.Getenv("GOPATH")
    71  	if gopath == "" {
    72  		return "", fmt.Errorf("no GOPATH env variable")
    73  	}
    74  
    75  	wd, err := fsutils.Getwd()
    76  	if err != nil {
    77  		return "", fmt.Errorf("can't get workind directory: %s", err)
    78  	}
    79  
    80  	if !strings.HasPrefix(wd, gopath) {
    81  		return "", fmt.Errorf("currently no in gopath: %q isn't a prefix of %q", gopath, wd)
    82  	}
    83  
    84  	path := strings.TrimPrefix(wd, gopath)
    85  	path = strings.TrimPrefix(path, string(os.PathSeparator)) // if GOPATH contains separator at the end
    86  	src := "src" + string(os.PathSeparator)
    87  	if !strings.HasPrefix(path, src) {
    88  		return "", fmt.Errorf("currently no in gopath/src: %q isn't a prefix of %q", src, path)
    89  	}
    90  
    91  	path = strings.TrimPrefix(path, src)
    92  	path = strings.Replace(path, string(os.PathSeparator), "/", -1)
    93  	return path, nil
    94  }
    95  
    96  func isLocalProjectAnalysis(args []string) bool {
    97  	for _, arg := range args {
    98  		if strings.HasPrefix(arg, "..") || filepath.IsAbs(arg) {
    99  			return false
   100  		}
   101  	}
   102  
   103  	return true
   104  }
   105  
   106  func getTypeCheckFuncBodies(cfg *config.Run, linters []linter.Config,
   107  	pkgProg *packages.Program, log logutils.Log) func(string) bool {
   108  
   109  	if !isLocalProjectAnalysis(cfg.Args) {
   110  		loadDebugf("analysis in nonlocal, don't optimize loading by not typechecking func bodies")
   111  		return nil
   112  	}
   113  
   114  	if isSSAReprNeeded(linters) {
   115  		loadDebugf("ssa repr is needed, don't optimize loading by not typechecking func bodies")
   116  		return nil
   117  	}
   118  
   119  	if len(pkgProg.Dirs()) == 0 {
   120  		// files run, in this mode packages are fake: can't check their path properly
   121  		return nil
   122  	}
   123  
   124  	projPath, err := getCurrentProjectImportPath()
   125  	if err != nil {
   126  		log.Infof("Can't get cur project path: %s", err)
   127  		return nil
   128  	}
   129  
   130  	return func(path string) bool {
   131  		if strings.HasPrefix(path, ".") {
   132  			loadDebugf("%s: dot import: typecheck func bodies", path)
   133  			return true
   134  		}
   135  
   136  		isLocalPath := strings.HasPrefix(path, projPath)
   137  		if isLocalPath {
   138  			localPath := strings.TrimPrefix(path, projPath)
   139  			localPath = strings.TrimPrefix(localPath, "/")
   140  			if strings.HasPrefix(localPath, "vendor/") {
   141  				loadDebugf("%s: local vendor import: DO NOT typecheck func bodies", path)
   142  				return false
   143  			}
   144  
   145  			loadDebugf("%s: local import: typecheck func bodies", path)
   146  			return true
   147  		}
   148  
   149  		loadDebugf("%s: not local import: DO NOT typecheck func bodies", path)
   150  		return false
   151  	}
   152  }
   153  
   154  func loadWholeAppIfNeeded(linters []linter.Config, cfg *config.Config,
   155  	pkgProg *packages.Program, log logutils.Log) (*loader.Program, *loader.Config, error) {
   156  
   157  	if !isFullImportNeeded(linters, cfg) {
   158  		return nil, nil, nil
   159  	}
   160  
   161  	startedAt := time.Now()
   162  	defer func() {
   163  		log.Infof("Program loading took %s", time.Since(startedAt))
   164  	}()
   165  
   166  	bctx := pkgProg.BuildContext()
   167  	loadcfg := &loader.Config{
   168  		Build:               bctx,
   169  		AllowErrors:         true,                 // Try to analyze partially
   170  		ParserMode:          parser.ParseComments, // AST will be reused by linters
   171  		TypeCheckFuncBodies: getTypeCheckFuncBodies(&cfg.Run, linters, pkgProg, log),
   172  		TypeChecker: types.Config{
   173  			Sizes: types.SizesFor(build.Default.Compiler, build.Default.GOARCH),
   174  		},
   175  	}
   176  
   177  	var loaderArgs []string
   178  	dirs := pkgProg.Dirs()
   179  	if len(dirs) != 0 {
   180  		loaderArgs = dirs // dirs run
   181  	} else {
   182  		loaderArgs = pkgProg.Files(cfg.Run.AnalyzeTests) // files run
   183  	}
   184  
   185  	nLoaderArgs, err := normalizePaths(loaderArgs)
   186  	if err != nil {
   187  		return nil, nil, err
   188  	}
   189  
   190  	rest, err := loadcfg.FromArgs(nLoaderArgs, cfg.Run.AnalyzeTests)
   191  	if err != nil {
   192  		return nil, nil, fmt.Errorf("can't parepare load config with paths: %s", err)
   193  	}
   194  	if len(rest) > 0 {
   195  		return nil, nil, fmt.Errorf("unhandled loading paths: %v", rest)
   196  	}
   197  
   198  	prog, err := loadcfg.Load()
   199  	if err != nil {
   200  		return nil, nil, fmt.Errorf("can't load program from paths %v: %s", loaderArgs, err)
   201  	}
   202  
   203  	if len(prog.InitialPackages()) == 1 {
   204  		pkg := prog.InitialPackages()[0]
   205  		var files []string
   206  		for _, f := range pkg.Files {
   207  			files = append(files, prog.Fset.Position(f.Pos()).Filename)
   208  		}
   209  		log.Infof("pkg %s files: %s", pkg, files)
   210  	}
   211  
   212  	return prog, loadcfg, nil
   213  }
   214  
   215  func buildSSAProgram(lprog *loader.Program, log logutils.Log) *ssa.Program {
   216  	startedAt := time.Now()
   217  	defer func() {
   218  		log.Infof("SSA repr building took %s", time.Since(startedAt))
   219  	}()
   220  
   221  	ssaProg := ssautil.CreateProgram(lprog, ssa.GlobalDebug)
   222  	ssaProg.Build()
   223  	return ssaProg
   224  }
   225  
   226  // separateNotCompilingPackages moves not compiling packages into separate slices:
   227  // a lot of linters crash on such packages. Leave them only for those linters
   228  // which can work with them.
   229  //nolint:gocyclo
   230  func separateNotCompilingPackages(lintCtx *linter.Context) {
   231  	prog := lintCtx.Program
   232  
   233  	notCompilingPackagesSet := map[*loader.PackageInfo]bool{}
   234  
   235  	if prog.Created != nil {
   236  		compilingCreated := make([]*loader.PackageInfo, 0, len(prog.Created))
   237  		for _, info := range prog.Created {
   238  			if len(info.Errors) != 0 {
   239  				lintCtx.NotCompilingPackages = append(lintCtx.NotCompilingPackages, info)
   240  				notCompilingPackagesSet[info] = true
   241  			} else {
   242  				compilingCreated = append(compilingCreated, info)
   243  			}
   244  		}
   245  		prog.Created = compilingCreated
   246  	}
   247  
   248  	if prog.Imported != nil {
   249  		for k, info := range prog.Imported {
   250  			if len(info.Errors) == 0 {
   251  				continue
   252  			}
   253  
   254  			lintCtx.NotCompilingPackages = append(lintCtx.NotCompilingPackages, info)
   255  			notCompilingPackagesSet[info] = true
   256  			delete(prog.Imported, k)
   257  		}
   258  	}
   259  
   260  	if prog.AllPackages != nil {
   261  		for k, info := range prog.AllPackages {
   262  			if len(info.Errors) == 0 {
   263  				continue
   264  			}
   265  
   266  			if !notCompilingPackagesSet[info] {
   267  				lintCtx.NotCompilingPackages = append(lintCtx.NotCompilingPackages, info)
   268  				notCompilingPackagesSet[info] = true
   269  			}
   270  			delete(prog.AllPackages, k)
   271  		}
   272  	}
   273  
   274  	if len(lintCtx.NotCompilingPackages) != 0 {
   275  		lintCtx.Log.Infof("Not compiling packages: %+v", lintCtx.NotCompilingPackages)
   276  	}
   277  }
   278  
   279  //nolint:gocyclo
   280  func LoadContext(linters []linter.Config, cfg *config.Config, log logutils.Log) (*linter.Context, error) {
   281  	// Set GOROOT to have working cross-compilation: cross-compiled binaries
   282  	// have invalid GOROOT. XXX: can't use runtime.GOROOT().
   283  	goroot, err := goutils.DiscoverGoRoot()
   284  	if err != nil {
   285  		return nil, fmt.Errorf("can't discover GOROOT: %s", err)
   286  	}
   287  	os.Setenv("GOROOT", goroot)
   288  	build.Default.GOROOT = goroot
   289  
   290  	args := cfg.Run.Args
   291  	if len(args) == 0 {
   292  		args = []string{"./..."}
   293  	}
   294  
   295  	skipDirs := append([]string{}, packages.StdExcludeDirRegexps...)
   296  	skipDirs = append(skipDirs, cfg.Run.SkipDirs...)
   297  	r, err := packages.NewResolver(cfg.Run.BuildTags, skipDirs, log.Child("path_resolver"))
   298  	if err != nil {
   299  		return nil, err
   300  	}
   301  
   302  	pkgProg, err := r.Resolve(args...)
   303  	if err != nil {
   304  		return nil, err
   305  	}
   306  
   307  	if len(pkgProg.Packages()) == 0 {
   308  		return nil, exitcodes.ErrNoGoFiles
   309  	}
   310  
   311  	prog, loaderConfig, err := loadWholeAppIfNeeded(linters, cfg, pkgProg, log)
   312  	if err != nil {
   313  		return nil, err
   314  	}
   315  
   316  	var ssaProg *ssa.Program
   317  	if prog != nil && isSSAReprNeeded(linters) {
   318  		ssaProg = buildSSAProgram(prog, log)
   319  	}
   320  
   321  	astLog := log.Child("astcache")
   322  	var astCache *astcache.Cache
   323  	if prog != nil {
   324  		astCache, err = astcache.LoadFromProgram(prog, astLog)
   325  	} else {
   326  		astCache, err = astcache.LoadFromFiles(pkgProg.Files(cfg.Run.AnalyzeTests), astLog)
   327  	}
   328  	if err != nil {
   329  		return nil, err
   330  	}
   331  
   332  	ret := &linter.Context{
   333  		PkgProgram:   pkgProg,
   334  		Cfg:          cfg,
   335  		Program:      prog,
   336  		SSAProgram:   ssaProg,
   337  		LoaderConfig: loaderConfig,
   338  		ASTCache:     astCache,
   339  		Log:          log,
   340  	}
   341  
   342  	if prog != nil {
   343  		separateNotCompilingPackages(ret)
   344  	}
   345  
   346  	return ret, nil
   347  }