mirror of
https://github.com/woodpecker-ci/woodpecker.git
synced 2024-12-13 12:06:29 +00:00
c28f7cb29f
Initial part of #435
315 lines
8.5 KiB
Go
315 lines
8.5 KiB
Go
package lint
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"go/build"
|
|
"go/token"
|
|
"os"
|
|
"path/filepath"
|
|
"regexp"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/pkg/errors"
|
|
"golang.org/x/tools/go/packages"
|
|
|
|
"github.com/golangci/golangci-lint/internal/pkgcache"
|
|
"github.com/golangci/golangci-lint/pkg/config"
|
|
"github.com/golangci/golangci-lint/pkg/exitcodes"
|
|
"github.com/golangci/golangci-lint/pkg/fsutils"
|
|
"github.com/golangci/golangci-lint/pkg/golinters/goanalysis/load"
|
|
"github.com/golangci/golangci-lint/pkg/goutil"
|
|
"github.com/golangci/golangci-lint/pkg/lint/linter"
|
|
"github.com/golangci/golangci-lint/pkg/logutils"
|
|
)
|
|
|
|
type ContextLoader struct {
|
|
cfg *config.Config
|
|
log logutils.Log
|
|
debugf logutils.DebugFunc
|
|
goenv *goutil.Env
|
|
pkgTestIDRe *regexp.Regexp
|
|
lineCache *fsutils.LineCache
|
|
fileCache *fsutils.FileCache
|
|
pkgCache *pkgcache.Cache
|
|
loadGuard *load.Guard
|
|
}
|
|
|
|
func NewContextLoader(cfg *config.Config, log logutils.Log, goenv *goutil.Env,
|
|
lineCache *fsutils.LineCache, fileCache *fsutils.FileCache, pkgCache *pkgcache.Cache, loadGuard *load.Guard) *ContextLoader {
|
|
return &ContextLoader{
|
|
cfg: cfg,
|
|
log: log,
|
|
debugf: logutils.Debug("loader"),
|
|
goenv: goenv,
|
|
pkgTestIDRe: regexp.MustCompile(`^(.*) \[(.*)\.test\]`),
|
|
lineCache: lineCache,
|
|
fileCache: fileCache,
|
|
pkgCache: pkgCache,
|
|
loadGuard: loadGuard,
|
|
}
|
|
}
|
|
|
|
func (cl *ContextLoader) prepareBuildContext() {
|
|
// Set GOROOT to have working cross-compilation: cross-compiled binaries
|
|
// have invalid GOROOT. XXX: can't use runtime.GOROOT().
|
|
goroot := cl.goenv.Get(goutil.EnvGoRoot)
|
|
if goroot == "" {
|
|
return
|
|
}
|
|
|
|
os.Setenv("GOROOT", goroot)
|
|
build.Default.GOROOT = goroot
|
|
build.Default.BuildTags = cl.cfg.Run.BuildTags
|
|
}
|
|
|
|
func (cl *ContextLoader) findLoadMode(linters []*linter.Config) packages.LoadMode {
|
|
loadMode := packages.LoadMode(0)
|
|
for _, lc := range linters {
|
|
loadMode |= lc.LoadMode
|
|
}
|
|
|
|
return loadMode
|
|
}
|
|
|
|
func (cl *ContextLoader) buildArgs() []string {
|
|
args := cl.cfg.Run.Args
|
|
if len(args) == 0 {
|
|
return []string{"./..."}
|
|
}
|
|
|
|
var retArgs []string
|
|
for _, arg := range args {
|
|
if strings.HasPrefix(arg, ".") || filepath.IsAbs(arg) {
|
|
retArgs = append(retArgs, arg)
|
|
} else {
|
|
// go/packages doesn't work well if we don't have prefix ./ for local packages
|
|
retArgs = append(retArgs, fmt.Sprintf(".%c%s", filepath.Separator, arg))
|
|
}
|
|
}
|
|
|
|
return retArgs
|
|
}
|
|
|
|
func (cl *ContextLoader) makeBuildFlags() ([]string, error) {
|
|
var buildFlags []string
|
|
|
|
if len(cl.cfg.Run.BuildTags) != 0 {
|
|
// go help build
|
|
buildFlags = append(buildFlags, "-tags", strings.Join(cl.cfg.Run.BuildTags, " "))
|
|
cl.log.Infof("Using build tags: %v", cl.cfg.Run.BuildTags)
|
|
}
|
|
|
|
mod := cl.cfg.Run.ModulesDownloadMode
|
|
if mod != "" {
|
|
// go help modules
|
|
allowedMods := []string{"mod", "readonly", "vendor"}
|
|
var ok bool
|
|
for _, am := range allowedMods {
|
|
if am == mod {
|
|
ok = true
|
|
break
|
|
}
|
|
}
|
|
if !ok {
|
|
return nil, fmt.Errorf("invalid modules download path %s, only (%s) allowed", mod, strings.Join(allowedMods, "|"))
|
|
}
|
|
|
|
buildFlags = append(buildFlags, fmt.Sprintf("-mod=%s", cl.cfg.Run.ModulesDownloadMode))
|
|
}
|
|
|
|
return buildFlags, nil
|
|
}
|
|
|
|
func stringifyLoadMode(mode packages.LoadMode) string {
|
|
m := map[packages.LoadMode]string{
|
|
packages.NeedCompiledGoFiles: "compiled_files",
|
|
packages.NeedDeps: "deps",
|
|
packages.NeedExportsFile: "exports_file",
|
|
packages.NeedFiles: "files",
|
|
packages.NeedImports: "imports",
|
|
packages.NeedName: "name",
|
|
packages.NeedSyntax: "syntax",
|
|
packages.NeedTypes: "types",
|
|
packages.NeedTypesInfo: "types_info",
|
|
packages.NeedTypesSizes: "types_sizes",
|
|
}
|
|
|
|
var flags []string
|
|
for flag, flagStr := range m {
|
|
if mode&flag != 0 {
|
|
flags = append(flags, flagStr)
|
|
}
|
|
}
|
|
|
|
return fmt.Sprintf("%d (%s)", mode, strings.Join(flags, "|"))
|
|
}
|
|
|
|
func (cl *ContextLoader) debugPrintLoadedPackages(pkgs []*packages.Package) {
|
|
cl.debugf("loaded %d pkgs", len(pkgs))
|
|
for i, pkg := range pkgs {
|
|
var syntaxFiles []string
|
|
for _, sf := range pkg.Syntax {
|
|
syntaxFiles = append(syntaxFiles, pkg.Fset.Position(sf.Pos()).Filename)
|
|
}
|
|
cl.debugf("Loaded pkg #%d: ID=%s GoFiles=%s CompiledGoFiles=%s Syntax=%s",
|
|
i, pkg.ID, pkg.GoFiles, pkg.CompiledGoFiles, syntaxFiles)
|
|
}
|
|
}
|
|
|
|
func (cl *ContextLoader) parseLoadedPackagesErrors(pkgs []*packages.Package) error {
|
|
for _, pkg := range pkgs {
|
|
for _, err := range pkg.Errors {
|
|
if strings.Contains(err.Msg, "no Go files") {
|
|
return errors.Wrapf(exitcodes.ErrNoGoFiles, "package %s", pkg.PkgPath)
|
|
}
|
|
if strings.Contains(err.Msg, "cannot find package") {
|
|
// when analyzing not existing directory
|
|
return errors.Wrap(exitcodes.ErrFailure, err.Msg)
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (cl *ContextLoader) loadPackages(ctx context.Context, loadMode packages.LoadMode) ([]*packages.Package, error) {
|
|
defer func(startedAt time.Time) {
|
|
cl.log.Infof("Go packages loading at mode %s took %s", stringifyLoadMode(loadMode), time.Since(startedAt))
|
|
}(time.Now())
|
|
|
|
cl.prepareBuildContext()
|
|
|
|
buildFlags, err := cl.makeBuildFlags()
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "failed to make build flags for go list")
|
|
}
|
|
|
|
conf := &packages.Config{
|
|
Mode: loadMode,
|
|
Tests: cl.cfg.Run.AnalyzeTests,
|
|
Context: ctx,
|
|
BuildFlags: buildFlags,
|
|
Logf: cl.debugf,
|
|
//TODO: use fset, parsefile, overlay
|
|
}
|
|
|
|
args := cl.buildArgs()
|
|
cl.debugf("Built loader args are %s", args)
|
|
pkgs, err := packages.Load(conf, args...)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "failed to load with go/packages")
|
|
}
|
|
|
|
// Currently, go/packages doesn't guarantee that error will be returned
|
|
// if context was canceled. See
|
|
// https://github.com/golang/tools/commit/c5cec6710e927457c3c29d6c156415e8539a5111#r39261855
|
|
if ctx.Err() != nil {
|
|
return nil, errors.Wrap(ctx.Err(), "timed out to load packages")
|
|
}
|
|
|
|
if loadMode&packages.NeedSyntax == 0 {
|
|
// Needed e.g. for go/analysis loading.
|
|
fset := token.NewFileSet()
|
|
packages.Visit(pkgs, nil, func(pkg *packages.Package) {
|
|
pkg.Fset = fset
|
|
cl.loadGuard.AddMutexForPkg(pkg)
|
|
})
|
|
}
|
|
|
|
cl.debugPrintLoadedPackages(pkgs)
|
|
|
|
if err := cl.parseLoadedPackagesErrors(pkgs); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return cl.filterTestMainPackages(pkgs), nil
|
|
}
|
|
|
|
func (cl *ContextLoader) tryParseTestPackage(pkg *packages.Package) (name string, isTest bool) {
|
|
matches := cl.pkgTestIDRe.FindStringSubmatch(pkg.ID)
|
|
if matches == nil {
|
|
return "", false
|
|
}
|
|
|
|
return matches[1], true
|
|
}
|
|
|
|
func (cl *ContextLoader) filterTestMainPackages(pkgs []*packages.Package) []*packages.Package {
|
|
var retPkgs []*packages.Package
|
|
for _, pkg := range pkgs {
|
|
if pkg.Name == "main" && strings.HasSuffix(pkg.PkgPath, ".test") {
|
|
// it's an implicit testmain package
|
|
cl.debugf("skip pkg ID=%s", pkg.ID)
|
|
continue
|
|
}
|
|
|
|
retPkgs = append(retPkgs, pkg)
|
|
}
|
|
|
|
return retPkgs
|
|
}
|
|
|
|
func (cl *ContextLoader) filterDuplicatePackages(pkgs []*packages.Package) []*packages.Package {
|
|
packagesWithTests := map[string]bool{}
|
|
for _, pkg := range pkgs {
|
|
name, isTest := cl.tryParseTestPackage(pkg)
|
|
if !isTest {
|
|
continue
|
|
}
|
|
packagesWithTests[name] = true
|
|
}
|
|
|
|
cl.debugf("package with tests: %#v", packagesWithTests)
|
|
|
|
var retPkgs []*packages.Package
|
|
for _, pkg := range pkgs {
|
|
_, isTest := cl.tryParseTestPackage(pkg)
|
|
if !isTest && packagesWithTests[pkg.PkgPath] {
|
|
// If tests loading is enabled,
|
|
// for package with files a.go and a_test.go go/packages loads two packages:
|
|
// 1. ID=".../a" GoFiles=[a.go]
|
|
// 2. ID=".../a [.../a.test]" GoFiles=[a.go a_test.go]
|
|
// We need only the second package, otherwise we can get warnings about unused variables/fields/functions
|
|
// in a.go if they are used only in a_test.go.
|
|
cl.debugf("skip pkg ID=%s because we load it with test package", pkg.ID)
|
|
continue
|
|
}
|
|
|
|
retPkgs = append(retPkgs, pkg)
|
|
}
|
|
|
|
return retPkgs
|
|
}
|
|
|
|
func (cl *ContextLoader) Load(ctx context.Context, linters []*linter.Config) (*linter.Context, error) {
|
|
loadMode := cl.findLoadMode(linters)
|
|
pkgs, err := cl.loadPackages(ctx, loadMode)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "failed to load packages")
|
|
}
|
|
|
|
deduplicatedPkgs := cl.filterDuplicatePackages(pkgs)
|
|
|
|
if len(deduplicatedPkgs) == 0 {
|
|
return nil, exitcodes.ErrNoGoFiles
|
|
}
|
|
|
|
ret := &linter.Context{
|
|
Packages: deduplicatedPkgs,
|
|
|
|
// At least `unused` linters works properly only on original (not deduplicated) packages,
|
|
// see https://github.com/golangci/golangci-lint/pull/585.
|
|
OriginalPackages: pkgs,
|
|
|
|
Cfg: cl.cfg,
|
|
Log: cl.log,
|
|
FileCache: cl.fileCache,
|
|
LineCache: cl.lineCache,
|
|
PkgCache: cl.pkgCache,
|
|
LoadGuard: cl.loadGuard,
|
|
}
|
|
|
|
return ret, nil
|
|
}
|