
1. Allow govet to work in 2 modes: fast and slow. Default is slow. In fast mode golangci-lint runs `go install -i` and `go test -i` for analyzed packages. But it's fast only when: - go >= 1.10 - it's repeated run or $GOPATH/pkg or `go env GOCACHE` is cached between CI builds In slow mode we load program from source code like for another linters and do it only once for all linters. 3. Patch govet code to warn about any troubles with the type information. Default behaviour of govet was to hide such warnings. Fail analysis if there are any troubles with type loading: it will prevent false-positives and false-negatives from govet. 4. Describe almost all options in .golangci.example.yml and include it into README. Describe when to use slow or fast mode of govet. 5. Speed up govet: reuse AST parsing: it's already parsed once by golangci-lint. For "slow" runs (when we run at least one slow linter) speedup by not loading type information second time. 6. Improve logging, debug logging 7. Fix crash in logging of AST cache warnings (#118)
322 lines
8.2 KiB
Go
322 lines
8.2 KiB
Go
package lint
|
|
|
|
import (
|
|
"context"
|
|
"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/go-tools/ssa"
|
|
"github.com/golangci/go-tools/ssa/ssautil"
|
|
"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"
|
|
)
|
|
|
|
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(ctx context.Context, 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(ctx context.Context, 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.
|
|
func separateNotCompilingPackages(lintCtx *linter.Context) {
|
|
prog := lintCtx.Program
|
|
|
|
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)
|
|
} else {
|
|
compilingCreated = append(compilingCreated, info)
|
|
}
|
|
}
|
|
prog.Created = compilingCreated
|
|
}
|
|
|
|
if prog.Imported != nil {
|
|
for k, info := range prog.Imported {
|
|
if len(info.Errors) != 0 {
|
|
lintCtx.NotCompilingPackages = append(lintCtx.NotCompilingPackages, info)
|
|
delete(prog.Imported, k)
|
|
}
|
|
}
|
|
}
|
|
|
|
if len(lintCtx.NotCompilingPackages) != 0 {
|
|
lintCtx.Log.Infof("Not compiling packages: %+v", lintCtx.NotCompilingPackages)
|
|
}
|
|
}
|
|
|
|
//nolint:gocyclo
|
|
func LoadContext(ctx context.Context, 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(ctx, linters, cfg, pkgProg, log)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var ssaProg *ssa.Program
|
|
if prog != nil && isSSAReprNeeded(linters) {
|
|
ssaProg = buildSSAProgram(ctx, 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
|
|
}
|