golangci-lint/pkg/commands/executor.go
2023-03-02 00:34:22 +01:00

254 lines
7.1 KiB
Go

package commands
import (
"bytes"
"context"
"crypto/sha256"
"fmt"
"io"
"os"
"path/filepath"
"strings"
"time"
"github.com/fatih/color"
"github.com/gofrs/flock"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
"gopkg.in/yaml.v3"
"github.com/golangci/golangci-lint/internal/cache"
"github.com/golangci/golangci-lint/internal/pkgcache"
"github.com/golangci/golangci-lint/pkg/config"
"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"
"github.com/golangci/golangci-lint/pkg/lint/lintersdb"
"github.com/golangci/golangci-lint/pkg/logutils"
"github.com/golangci/golangci-lint/pkg/report"
"github.com/golangci/golangci-lint/pkg/timeutils"
)
type BuildInfo struct {
GoVersion string `json:"goVersion"`
Version string `json:"version"`
Commit string `json:"commit"`
Date string `json:"date"`
}
type Executor struct {
rootCmd *cobra.Command
runCmd *cobra.Command
lintersCmd *cobra.Command
exitCode int
buildInfo BuildInfo
cfg *config.Config // cfg is the unmarshaled data from the golangci config file.
log logutils.Log
reportData report.Data
DBManager *lintersdb.Manager
EnabledLintersSet *lintersdb.EnabledSet
contextLoader *lint.ContextLoader
goenv *goutil.Env
fileCache *fsutils.FileCache
lineCache *fsutils.LineCache
pkgCache *pkgcache.Cache
debugf logutils.DebugFunc
sw *timeutils.Stopwatch
loadGuard *load.Guard
flock *flock.Flock
}
// NewExecutor creates and initializes a new command executor.
func NewExecutor(buildInfo BuildInfo) *Executor {
startedAt := time.Now()
e := &Executor{
cfg: config.NewDefault(),
buildInfo: buildInfo,
DBManager: lintersdb.NewManager(nil, nil),
debugf: logutils.Debug(logutils.DebugKeyExec),
}
e.debugf("Starting execution...")
e.log = report.NewLogWrapper(logutils.NewStderrLog(logutils.DebugKeyEmpty), &e.reportData)
// to setup log level early we need to parse config from command line extra time to
// find `-v` option
commandLineCfg, err := e.getConfigForCommandLine()
if err != nil && err != pflag.ErrHelp {
e.log.Fatalf("Can't get config for command line: %s", err)
}
if commandLineCfg != nil {
logutils.SetupVerboseLog(e.log, commandLineCfg.Run.IsVerbose)
switch commandLineCfg.Output.Color {
case "always":
color.NoColor = false
case "never":
color.NoColor = true
case "auto":
// nothing
default:
e.log.Fatalf("invalid value %q for --color; must be 'always', 'auto', or 'never'", commandLineCfg.Output.Color)
}
}
// init of commands must be done before config file reading because
// init sets config with the default values of flags
e.initRoot()
e.initRun()
e.initHelp()
e.initLinters()
e.initConfig()
e.initVersion()
e.initCache()
// init e.cfg by values from config: flags parse will see these values
// like the default ones. It will overwrite them only if the same option
// is found in command-line: it's ok, command-line has higher priority.
r := config.NewFileReader(e.cfg, commandLineCfg, e.log.Child(logutils.DebugKeyConfigReader))
if err = r.Read(); err != nil {
e.log.Fatalf("Can't read config: %s", err)
}
if (commandLineCfg == nil || commandLineCfg.Run.Go == "") && e.cfg != nil && e.cfg.Run.Go == "" {
e.cfg.Run.Go = config.DetectGoVersion()
}
// recreate after getting config
e.DBManager = lintersdb.NewManager(e.cfg, e.log).WithCustomLinters()
// Slice options must be explicitly set for proper merging of config and command-line options.
fixSlicesFlags(e.runCmd.Flags())
fixSlicesFlags(e.lintersCmd.Flags())
e.EnabledLintersSet = lintersdb.NewEnabledSet(e.DBManager,
lintersdb.NewValidator(e.DBManager), e.log.Child(logutils.DebugKeyLintersDB), e.cfg)
e.goenv = goutil.NewEnv(e.log.Child(logutils.DebugKeyGoEnv))
e.fileCache = fsutils.NewFileCache()
e.lineCache = fsutils.NewLineCache(e.fileCache)
e.sw = timeutils.NewStopwatch("pkgcache", e.log.Child(logutils.DebugKeyStopwatch))
e.pkgCache, err = pkgcache.NewCache(e.sw, e.log.Child(logutils.DebugKeyPkgCache))
if err != nil {
e.log.Fatalf("Failed to build packages cache: %s", err)
}
e.loadGuard = load.NewGuard()
e.contextLoader = lint.NewContextLoader(e.cfg, e.log.Child(logutils.DebugKeyLoader), e.goenv,
e.lineCache, e.fileCache, e.pkgCache, e.loadGuard)
if err = e.initHashSalt(buildInfo.Version); err != nil {
e.log.Fatalf("Failed to init hash salt: %s", err)
}
e.debugf("Initialized executor in %s", time.Since(startedAt))
return e
}
func (e *Executor) Execute() error {
return e.rootCmd.Execute()
}
func (e *Executor) initHashSalt(version string) error {
binSalt, err := computeBinarySalt(version)
if err != nil {
return fmt.Errorf("failed to calculate binary salt: %w", err)
}
configSalt, err := computeConfigSalt(e.cfg)
if err != nil {
return fmt.Errorf("failed to calculate config salt: %w", err)
}
b := bytes.NewBuffer(binSalt)
b.Write(configSalt)
cache.SetSalt(b.Bytes())
return nil
}
func computeBinarySalt(version string) ([]byte, error) {
if version != "" && version != "(devel)" {
return []byte(version), nil
}
if logutils.HaveDebugTag(logutils.DebugKeyBinSalt) {
return []byte("debug"), nil
}
p, err := os.Executable()
if err != nil {
return nil, err
}
f, err := os.Open(p)
if err != nil {
return nil, err
}
defer f.Close()
h := sha256.New()
if _, err := io.Copy(h, f); err != nil {
return nil, err
}
return h.Sum(nil), nil
}
func computeConfigSalt(cfg *config.Config) ([]byte, error) {
// We don't hash all config fields to reduce meaningless cache
// invalidations. At least, it has a huge impact on tests speed.
lintersSettingsBytes, err := yaml.Marshal(cfg.LintersSettings)
if err != nil {
return nil, fmt.Errorf("failed to json marshal config linter settings: %w", err)
}
configData := bytes.NewBufferString("linters-settings=")
configData.Write(lintersSettingsBytes)
configData.WriteString("\nbuild-tags=%s" + strings.Join(cfg.Run.BuildTags, ","))
h := sha256.New()
if _, err := h.Write(configData.Bytes()); err != nil {
return nil, err
}
return h.Sum(nil), nil
}
func (e *Executor) acquireFileLock() bool {
if e.cfg.Run.AllowParallelRunners {
e.debugf("Parallel runners are allowed, no locking")
return true
}
lockFile := filepath.Join(os.TempDir(), "golangci-lint.lock")
e.debugf("Locking on file %s...", lockFile)
f := flock.New(lockFile)
const retryDelay = time.Second
ctx := context.Background()
if !e.cfg.Run.AllowSerialRunners {
const totalTimeout = 5 * time.Second
var cancel context.CancelFunc
ctx, cancel = context.WithTimeout(ctx, totalTimeout)
defer cancel()
}
if ok, _ := f.TryLockContext(ctx, retryDelay); !ok {
return false
}
e.flock = f
return true
}
func (e *Executor) releaseFileLock() {
if e.cfg.Run.AllowParallelRunners {
return
}
if err := e.flock.Unlock(); err != nil {
e.debugf("Failed to unlock on file: %s", err)
}
if err := os.Remove(e.flock.Path()); err != nil {
e.debugf("Failed to remove lock file: %s", err)
}
}