Denis Isaev 5514c4393e Fix #17, #87: govet becomes SLOW linter by default
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)
2018-06-18 09:47:15 +03:00

229 lines
6.0 KiB
Go

package golinters
import (
"context"
"fmt"
"go/ast"
"go/token"
"os"
"os/exec"
"strings"
"time"
"github.com/golangci/golangci-lint/pkg/goutils"
"github.com/golangci/golangci-lint/pkg/lint/linter"
"github.com/golangci/golangci-lint/pkg/logutils"
"github.com/golangci/golangci-lint/pkg/result"
"github.com/golangci/golangci-lint/pkg/timeutils"
govetAPI "github.com/golangci/govet"
)
type Govet struct{}
func (Govet) Name() string {
return "govet"
}
func (Govet) Desc() string {
return "Vet examines Go source code and reports suspicious constructs, such as Printf calls whose arguments do not align with the format string"
}
func (g Govet) Run(ctx context.Context, lintCtx *linter.Context) ([]result.Issue, error) {
var govetIssues []govetAPI.Issue
var err error
if lintCtx.Settings().Govet.UseInstalledPackages {
govetIssues, err = g.runOnInstalledPackages(ctx, lintCtx)
if err != nil {
return nil, fmt.Errorf("can't run govet on installed packages: %s", err)
}
} else {
govetIssues, err = g.runOnSourcePackages(ctx, lintCtx)
if err != nil {
return nil, fmt.Errorf("can't run govet on source packages: %s", err)
}
}
if len(govetIssues) == 0 {
return nil, nil
}
res := make([]result.Issue, 0, len(govetIssues))
for _, i := range govetIssues {
res = append(res, result.Issue{
Pos: i.Pos,
Text: i.Message,
FromLinter: g.Name(),
})
}
return res, nil
}
func (g Govet) runOnInstalledPackages(ctx context.Context, lintCtx *linter.Context) ([]govetAPI.Issue, error) {
if err := g.installPackages(ctx, lintCtx); err != nil {
return nil, fmt.Errorf("can't install packages (it's required for govet): %s", err)
}
// TODO: check .S asm files: govet can do it if pass dirs
var govetIssues []govetAPI.Issue
for _, pkg := range lintCtx.PkgProgram.Packages() {
var astFiles []*ast.File
var fset *token.FileSet
for _, fname := range pkg.Files(lintCtx.Cfg.Run.AnalyzeTests) {
af := lintCtx.ASTCache.Get(fname)
if af == nil || af.Err != nil {
return nil, fmt.Errorf("can't get parsed file %q from ast cache: %#v", fname, af)
}
astFiles = append(astFiles, af.F)
fset = af.Fset
}
issues, err := govetAPI.Analyze(astFiles, fset, nil,
lintCtx.Settings().Govet.CheckShadowing)
if err != nil {
return nil, err
}
govetIssues = append(govetIssues, issues...)
}
return govetIssues, nil
}
func (g Govet) installPackages(ctx context.Context, lintCtx *linter.Context) error {
inGoRoot, err := goutils.InGoRoot()
if err != nil {
return fmt.Errorf("can't check whether we are in $GOROOT: %s", err)
}
if inGoRoot {
// Go source packages already should be installed into $GOROOT/pkg with go distribution
lintCtx.Log.Infof("In $GOROOT, don't install packages")
return nil
}
if err := g.installNonTestPackages(ctx, lintCtx); err != nil {
return err
}
if err := g.installTestDependencies(ctx, lintCtx); err != nil {
return err
}
return nil
}
func (g Govet) installTestDependencies(ctx context.Context, lintCtx *linter.Context) error {
log := lintCtx.Log
packages := lintCtx.PkgProgram.Packages()
var testDirs []string
for _, pkg := range packages {
dir := pkg.Dir()
if dir == "" {
log.Warnf("Package %#v has empty dir", pkg)
continue
}
if !strings.HasPrefix(dir, ".") {
// go install can't work without that
dir = "./" + dir
}
if len(pkg.TestFiles()) != 0 {
testDirs = append(testDirs, dir)
}
}
if len(testDirs) == 0 {
log.Infof("No test files in packages %#v", packages)
return nil
}
args := append([]string{"test", "-i"}, testDirs...)
return runGoCommand(ctx, log, args...)
}
func (g Govet) installNonTestPackages(ctx context.Context, lintCtx *linter.Context) error {
log := lintCtx.Log
packages := lintCtx.PkgProgram.Packages()
var importPaths []string
for _, pkg := range packages {
if pkg.IsTestOnly() {
// test-only package will be processed by installTestDependencies
continue
}
dir := pkg.Dir()
if dir == "" {
log.Warnf("Package %#v has empty dir", pkg)
continue
}
if !strings.HasPrefix(dir, ".") {
// go install can't work without that
dir = "./" + dir
}
importPaths = append(importPaths, dir)
}
if len(importPaths) == 0 {
log.Infof("No packages to install, all packages: %#v", packages)
return nil
}
// we need type information of dependencies of analyzed packages
// so we pass -i option to install it
if err := runGoInstall(ctx, log, importPaths, true); err != nil {
// try without -i option: go < 1.10 doesn't support this option
// and install dependencies by default.
return runGoInstall(ctx, log, importPaths, false)
}
return nil
}
func runGoInstall(ctx context.Context, log logutils.Log, importPaths []string, withIOption bool) error {
args := []string{"install"}
if withIOption {
args = append(args, "-i")
}
args = append(args, importPaths...)
return runGoCommand(ctx, log, args...)
}
func runGoCommand(ctx context.Context, log logutils.Log, args ...string) error {
argsStr := strings.Join(args, " ")
defer timeutils.Track(time.Now(), log, "go %s", argsStr)
cmd := exec.CommandContext(ctx, "go", args...)
cmd.Env = append([]string{}, os.Environ()...)
cmd.Env = append(cmd.Env, "GOMAXPROCS=1") // don't consume more than 1 cpu
// use .Output but not .Run to capture StdErr in err
_, err := cmd.Output()
if err != nil {
var stderr string
if ee, ok := err.(*exec.ExitError); ok && ee.Stderr != nil {
stderr = ": " + string(ee.Stderr)
}
return fmt.Errorf("can't run [go %s]: %s%s", argsStr, err, stderr)
}
return nil
}
func (g Govet) runOnSourcePackages(ctx context.Context, lintCtx *linter.Context) ([]govetAPI.Issue, error) {
// TODO: check .S asm files: govet can do it if pass dirs
var govetIssues []govetAPI.Issue
for _, pkg := range lintCtx.Program.InitialPackages() {
issues, err := govetAPI.Analyze(pkg.Files, lintCtx.Program.Fset, pkg,
lintCtx.Settings().Govet.CheckShadowing)
if err != nil {
return nil, err
}
govetIssues = append(govetIssues, issues...)
}
return govetIssues, nil
}