package lint import ( "fmt" "go/build" "go/parser" "os" "path/filepath" "strings" "time" "github.com/golangci/golangci-lint/pkg/goutils" "github.com/golangci/golangci-lint/pkg/logutils" "github.com/golangci/golangci-lint/pkg/config" "github.com/golangci/golangci-lint/pkg/lint/astcache" "github.com/golangci/golangci-lint/pkg/lint/linter" "github.com/golangci/golangci-lint/pkg/packages" "golang.org/x/tools/go/loader" "golang.org/x/tools/go/ssa" "golang.org/x/tools/go/ssa/ssautil" ) var loadDebugf = logutils.Debug("load") func isFullImportNeeded(linters []linter.Config, cfg *config.Config) bool { for _, linter := range linters { if linter.NeedsProgramLoading() { if linter.Linter.Name() == "govet" && cfg.LintersSettings.Govet.UseInstalledPackages { // TODO: remove this hack continue } return true } } return false } func isSSAReprNeeded(linters []linter.Config) bool { for _, linter := range linters { if linter.NeedsSSARepresentation() { return true } } return false } func normalizePaths(paths []string) ([]string, error) { root, err := os.Getwd() if err != nil { return nil, fmt.Errorf("can't get working dir: %s", err) } ret := make([]string, 0, len(paths)) for _, p := range paths { if filepath.IsAbs(p) { relPath, err := filepath.Rel(root, p) if err != nil { return nil, fmt.Errorf("can't get relative path for path %s and root %s: %s", p, root, err) } p = relPath } ret = append(ret, "./"+p) } return ret, nil } func getCurrentProjectImportPath() (string, error) { gopath := os.Getenv("GOPATH") if gopath == "" { return "", fmt.Errorf("no GOPATH env variable") } wd, err := os.Getwd() if err != nil { return "", fmt.Errorf("can't get workind directory: %s", err) } if !strings.HasPrefix(wd, gopath) { return "", fmt.Errorf("currently no in gopath: %q isn't a prefix of %q", gopath, wd) } path := strings.TrimPrefix(wd, gopath) path = strings.TrimPrefix(path, string(os.PathSeparator)) // if GOPATH contains separator at the end src := "src" + string(os.PathSeparator) if !strings.HasPrefix(path, src) { return "", fmt.Errorf("currently no in gopath/src: %q isn't a prefix of %q", src, path) } path = strings.TrimPrefix(path, src) path = strings.Replace(path, string(os.PathSeparator), "/", -1) return path, nil } func isLocalProjectAnalysis(args []string) bool { for _, arg := range args { if strings.HasPrefix(arg, "..") || filepath.IsAbs(arg) { return false } } return true } func getTypeCheckFuncBodies(cfg *config.Run, linters []linter.Config, pkgProg *packages.Program, log logutils.Log) func(string) bool { if !isLocalProjectAnalysis(cfg.Args) { loadDebugf("analysis in nonlocal, don't optimize loading by not typechecking func bodies") return nil } if isSSAReprNeeded(linters) { loadDebugf("ssa repr is needed, don't optimize loading by not typechecking func bodies") return nil } if len(pkgProg.Dirs()) == 0 { // files run, in this mode packages are fake: can't check their path properly return nil } projPath, err := getCurrentProjectImportPath() if err != nil { log.Infof("Can't get cur project path: %s", err) return nil } return func(path string) bool { if strings.HasPrefix(path, ".") { loadDebugf("%s: dot import: typecheck func bodies", path) return true } isLocalPath := strings.HasPrefix(path, projPath) if isLocalPath { localPath := strings.TrimPrefix(path, projPath) localPath = strings.TrimPrefix(localPath, "/") if strings.HasPrefix(localPath, "vendor/") { loadDebugf("%s: local vendor import: DO NOT typecheck func bodies", path) return false } loadDebugf("%s: local import: typecheck func bodies", path) return true } loadDebugf("%s: not local import: DO NOT typecheck func bodies", path) return false } } func loadWholeAppIfNeeded(linters []linter.Config, cfg *config.Config, pkgProg *packages.Program, log logutils.Log) (*loader.Program, *loader.Config, error) { if !isFullImportNeeded(linters, cfg) { return nil, nil, nil } startedAt := time.Now() defer func() { log.Infof("Program loading took %s", time.Since(startedAt)) }() bctx := pkgProg.BuildContext() loadcfg := &loader.Config{ Build: bctx, AllowErrors: true, // Try to analyze partially ParserMode: parser.ParseComments, // AST will be reused by linters TypeCheckFuncBodies: getTypeCheckFuncBodies(&cfg.Run, linters, pkgProg, log), } var loaderArgs []string dirs := pkgProg.Dirs() if len(dirs) != 0 { loaderArgs = dirs // dirs run } else { loaderArgs = pkgProg.Files(cfg.Run.AnalyzeTests) // files run } nLoaderArgs, err := normalizePaths(loaderArgs) if err != nil { return nil, nil, err } rest, err := loadcfg.FromArgs(nLoaderArgs, cfg.Run.AnalyzeTests) if err != nil { return nil, nil, fmt.Errorf("can't parepare load config with paths: %s", err) } if len(rest) > 0 { return nil, nil, fmt.Errorf("unhandled loading paths: %v", rest) } prog, err := loadcfg.Load() if err != nil { return nil, nil, fmt.Errorf("can't load program from paths %v: %s", loaderArgs, err) } if len(prog.InitialPackages()) == 1 { pkg := prog.InitialPackages()[0] var files []string for _, f := range pkg.Files { files = append(files, prog.Fset.Position(f.Pos()).Filename) } log.Infof("pkg %s files: %s", pkg, files) } return prog, loadcfg, nil } func buildSSAProgram(lprog *loader.Program, log logutils.Log) *ssa.Program { startedAt := time.Now() defer func() { log.Infof("SSA repr building took %s", time.Since(startedAt)) }() ssaProg := ssautil.CreateProgram(lprog, ssa.GlobalDebug) ssaProg.Build() return ssaProg } // separateNotCompilingPackages moves not compiling packages into separate slices: // a lot of linters crash on such packages. Leave them only for those linters // which can work with them. //nolint:gocyclo func separateNotCompilingPackages(lintCtx *linter.Context) { prog := lintCtx.Program notCompilingPackagesSet := map[*loader.PackageInfo]bool{} if prog.Created != nil { compilingCreated := make([]*loader.PackageInfo, 0, len(prog.Created)) for _, info := range prog.Created { if len(info.Errors) != 0 { lintCtx.NotCompilingPackages = append(lintCtx.NotCompilingPackages, info) notCompilingPackagesSet[info] = true } else { compilingCreated = append(compilingCreated, info) } } prog.Created = compilingCreated } if prog.Imported != nil { for k, info := range prog.Imported { if len(info.Errors) == 0 { continue } lintCtx.NotCompilingPackages = append(lintCtx.NotCompilingPackages, info) notCompilingPackagesSet[info] = true delete(prog.Imported, k) } } if prog.AllPackages != nil { for k, info := range prog.AllPackages { if len(info.Errors) == 0 { continue } if !notCompilingPackagesSet[info] { lintCtx.NotCompilingPackages = append(lintCtx.NotCompilingPackages, info) notCompilingPackagesSet[info] = true } delete(prog.AllPackages, k) } } if len(lintCtx.NotCompilingPackages) != 0 { lintCtx.Log.Infof("Not compiling packages: %+v", lintCtx.NotCompilingPackages) } } //nolint:gocyclo func LoadContext(linters []linter.Config, cfg *config.Config, log logutils.Log) (*linter.Context, error) { // Set GOROOT to have working cross-compilation: cross-compiled binaries // have invalid GOROOT. XXX: can't use runtime.GOROOT(). goroot, err := goutils.DiscoverGoRoot() if err != nil { return nil, fmt.Errorf("can't discover GOROOT: %s", err) } os.Setenv("GOROOT", goroot) build.Default.GOROOT = goroot args := cfg.Run.Args if len(args) == 0 { args = []string{"./..."} } skipDirs := append([]string{}, packages.StdExcludeDirRegexps...) skipDirs = append(skipDirs, cfg.Run.SkipDirs...) r, err := packages.NewResolver(cfg.Run.BuildTags, skipDirs, log.Child("path_resolver")) if err != nil { return nil, err } pkgProg, err := r.Resolve(args...) if err != nil { return nil, err } prog, loaderConfig, err := loadWholeAppIfNeeded(linters, cfg, pkgProg, log) if err != nil { return nil, err } var ssaProg *ssa.Program if prog != nil && isSSAReprNeeded(linters) { ssaProg = buildSSAProgram(prog, log) } astLog := log.Child("astcache") var astCache *astcache.Cache if prog != nil { astCache, err = astcache.LoadFromProgram(prog, astLog) } else { astCache, err = astcache.LoadFromFiles(pkgProg.Files(cfg.Run.AnalyzeTests), astLog) } if err != nil { return nil, err } ret := &linter.Context{ PkgProgram: pkgProg, Cfg: cfg, Program: prog, SSAProgram: ssaProg, LoaderConfig: loaderConfig, ASTCache: astCache, Log: log, } if prog != nil { separateNotCompilingPackages(ret) } return ret, nil }