
1. Rename in a backward compatible way 2. Remove gosec default exclude list because gosec is already disabled by default. 3. Warn about unmatched linter names in //nolint directives 4. Process linter names in //nolint directives in upper case 5. Disable gosec for golangci-lint in .golangci.yml
348 lines
8.8 KiB
Go
348 lines
8.8 KiB
Go
package lint
|
|
|
|
import (
|
|
"fmt"
|
|
"go/build"
|
|
"go/parser"
|
|
"go/types"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/golangci/golangci-lint/pkg/exitcodes"
|
|
"github.com/golangci/golangci-lint/pkg/fsutils"
|
|
"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 _, lc := range linters {
|
|
if lc.NeedsProgramLoading() {
|
|
if lc.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) {
|
|
ret := make([]string, 0, len(paths))
|
|
for _, p := range paths {
|
|
relPath, err := fsutils.ShortestRelPath(p, "")
|
|
if err != nil {
|
|
return nil, fmt.Errorf("can't get relative path for path %s: %s", p, 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 := fsutils.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),
|
|
TypeChecker: types.Config{
|
|
Sizes: types.SizesFor(build.Default.Compiler, build.Default.GOARCH),
|
|
},
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
if len(pkgProg.Packages()) == 0 {
|
|
return nil, exitcodes.ErrNoGoFiles
|
|
}
|
|
|
|
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
|
|
}
|