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)
This commit is contained in:
Denis Isaev 2018-06-18 00:06:45 +03:00 committed by Isaev Denis
parent f239b80ce1
commit 5514c4393e
22 changed files with 720 additions and 158 deletions

View File

@ -1,40 +1,98 @@
# This file contains all available configuration options
# with their default values.
# options for analysis running
run: run:
# default concurrency is a available CPU number
concurrency: 4 concurrency: 4
# timeout for analysis, e.g. 30s, 5m, default is 1m
deadline: 1m deadline: 1m
# exit code when at least one issue was found, default is 1
issues-exit-code: 1 issues-exit-code: 1
# include test files or not, default is true
tests: true tests: true
# list of build tags, all linters use it. Default is empty list.
build-tags: build-tags:
- mytag - mytag
# which dirs to skip: they won't be analyzed;
# can use regexp here: generated.*, regexp is applied on full path;
# default value is empty list, but next dirs are always skipped independently
# from this option's value:
# vendor$, third_party$, testdata$, examples$, Godeps$, builtin$
skip-dirs: skip-dirs:
- src/external_libs - src/external_libs
- autogenerated_by_my_lib - autogenerated_by_my_lib
# which files to skip: they will be analyzed, but issues from them
# won't be reported. Default value is empty list, but there is
# no need to include all autogenerated files, we confidently recognize
# autogenerated files. If it's not please let us know.
skip-files: skip-files:
- ".*\\.pb\\.go$" - ".*\\.my\\.go$"
- lib/bad.go - lib/bad.go
# output configuration options
output: output:
# colored-line-number|line-number|json|tab|checkstyle, default is "colored-line-number"
format: colored-line-number format: colored-line-number
# print lines of code with issue, default is true
print-issued-lines: true print-issued-lines: true
# print linter name in the end of issue text, default is true
print-linter-name: true print-linter-name: true
# all available settings of specific linters
linters-settings: linters-settings:
errcheck: errcheck:
# report about not checking of errors in type assetions: `a := b.(MyStruct)`;
# default is false: such cases aren't reported by default.
check-type-assertions: false check-type-assertions: false
# report about assignment of errors to blank identifier: `num, _ := strconv.Atoi(numStr)`;
# default is false: such cases aren't reported by default.
check-blank: false check-blank: false
govet: govet:
# report about shadowed variables
check-shadowing: true check-shadowing: true
# Obtain type information from installed (to $GOPATH/pkg) package files:
# golangci-lint will execute `go install -i` and `go test -i` for analyzed packages
# before analyzing them.
# By default this option is disabled and govet gets type information by loader from source code.
# Loading from source code is slow, but it's done only once for all linters.
# Go-installing of packages first time is much slower than loading them from source code,
# therefore this option is disabled by default.
# But repeated installation is fast in go >= 1.10 because of build caching.
# Enable this option only if all conditions are met:
# 1. you use only "fast" linters (--fast e.g.): no program loading occurs
# 2. you use go >= 1.10
# 3. you do repeated runs (false for CI) or cache $GOPATH/pkg or `go env GOCACHE` dir in CI.
use-installed-packages: false
golint: golint:
# minimal confidence for issues, default is 0.8
min-confidence: 0.8 min-confidence: 0.8
gofmt: gofmt:
# simplify code: gofmt with `-s` option, true by default
simplify: true simplify: true
gocyclo: gocyclo:
# minimal code complexity to report, 30 by default (but we recommend 10-20)
min-complexity: 10 min-complexity: 10
maligned: maligned:
# print struct with more effective memory layout or not, false by default
suggest-new: true suggest-new: true
dupl: dupl:
threshold: 50 # tokens count to trigger issue, 150 by default
threshold: 100
goconst: goconst:
# minimal length of string constant, 3 by default
min-len: 3 min-len: 3
# minimal occurrences count to trigger, 3 by default
min-occurrences: 3 min-occurrences: 3
depguard: depguard:
list-type: blacklist list-type: blacklist
@ -45,8 +103,8 @@ linters-settings:
linters: linters:
enable: enable:
- megacheck - megacheck
- vet - govet
enable-all: true enable-all: false
disable: disable:
maligned maligned
disable-all: false disable-all: false
@ -56,11 +114,35 @@ linters:
fast: false fast: false
issues: issues:
# List of regexps of issue texts to exclude, empty list by default.
# But independently from this option we use default exclude patterns,
# it can be disabled by `exclude-use-default: false`. To list all
# excluded by default patterns execute `golangci-lint run --help`
exclude: exclude:
- abcdef - abcdef
# Independently from option `exclude` we use default exclude patterns,
# it can be disabled by this option. To list all
# excluded by default patterns execute `golangci-lint run --help`.
# Default value for this option is false.
exclude-use-default: true exclude-use-default: true
# Maximum issues count per one linter. Set to 0 to disable. Default is 50.
max-per-linter: 0 max-per-linter: 0
# Maximum count of issues with the same text. Set to 0 to disable. Default is 3.
max-same: 0 max-same: 0
# Show only new issues: if there are unstaged changes or untracked files,
# only those changes are analyzed, else only changes in HEAD~ are analyzed.
# It's a super-useful option for integration of golangci-lint into existing
# large codebase. It's not practical to fix all existing issues at the moment
# of integration: much better don't allow issues in new code.
# Default is false.
new: false new: false
new-from-rev: ""
new-from-patch: "" # Show only new issues created after git revision `REV`
new-from-rev: REV
# Show only new issues created in git patch with set file path.
new-from-patch: path/to/patch/file

2
Gopkg.lock generated
View File

@ -122,7 +122,7 @@
"lib/cfg", "lib/cfg",
"lib/whitelist" "lib/whitelist"
] ]
revision = "1a9ab8120ec96a014df3afab2d6c47f7e12d8928" revision = "18c83969a303d67eaa9d67747a930f007e3e9c89"
[[projects]] [[projects]]
branch = "master" branch = "master"

View File

@ -1,5 +1,4 @@
test: test:
go install ./cmd/... # needed for govet and golint if go < 1.10
GL_TEST_RUN=1 golangci-lint run -v GL_TEST_RUN=1 golangci-lint run -v
GL_TEST_RUN=1 golangci-lint run --fast --no-config -v GL_TEST_RUN=1 golangci-lint run --fast --no-config -v
GL_TEST_RUN=1 golangci-lint run --no-config -v GL_TEST_RUN=1 golangci-lint run --no-config -v

165
README.md
View File

@ -84,7 +84,7 @@ GolangCI-Lint can be used with zero configuration. By default the following lint
``` ```
$ golangci-lint linters $ golangci-lint linters
Enabled by default linters: Enabled by default linters:
govet: Vet examines Go source code and reports suspicious constructs, such as Printf calls whose arguments do not align with the format string [fast: true] govet: Vet examines Go source code and reports suspicious constructs, such as Printf calls whose arguments do not align with the format string [fast: false]
errcheck: Errcheck is a program for checking for unchecked errors in go programs. These unchecked errors can be critical bugs in some cases [fast: false] errcheck: Errcheck is a program for checking for unchecked errors in go programs. These unchecked errors can be critical bugs in some cases [fast: false]
staticcheck: Staticcheck is a go vet on steroids, applying a ton of static analysis checks [fast: false] staticcheck: Staticcheck is a go vet on steroids, applying a ton of static analysis checks [fast: false]
unused: Checks Go code for unused constants, variables, functions and types [fast: false] unused: Checks Go code for unused constants, variables, functions and types [fast: false]
@ -311,10 +311,161 @@ To see which config file is being used and where it was sourced from run golangc
Config options inside the file are identical to command-line options. Config options inside the file are identical to command-line options.
You can configure specific linters' options only within the config file (not the command-line). You can configure specific linters' options only within the config file (not the command-line).
There is a [`.golangci.yml`](https://github.com/golangci/golangci-lint/blob/master/.golangci.example.yml) example config file with all supported options. There is a [`.golangci.yml`](https://github.com/golangci/golangci-lint/blob/master/.golangci.example.yml) example
config file with all supported options, their description and default value:
```yaml
# This file contains all available configuration options
# with their default values.
# options for analysis running
run:
# default concurrency is a available CPU number
concurrency: 4
# timeout for analysis, e.g. 30s, 5m, default is 1m
deadline: 1m
# exit code when at least one issue was found, default is 1
issues-exit-code: 1
# include test files or not, default is true
tests: true
# list of build tags, all linters use it. Default is empty list.
build-tags:
- mytag
# which dirs to skip: they won't be analyzed;
# can use regexp here: generated.*, regexp is applied on full path;
# default value is empty list, but next dirs are always skipped independently
# from this option's value:
# vendor$, third_party$, testdata$, examples$, Godeps$, builtin$
skip-dirs:
- src/external_libs
- autogenerated_by_my_lib
# which files to skip: they will be analyzed, but issues from them
# won't be reported. Default value is empty list, but there is
# no need to include all autogenerated files, we confidently recognize
# autogenerated files. If it's not please let us know.
skip-files:
- ".*\\.my\\.go$"
- lib/bad.go
# output configuration options
output:
# colored-line-number|line-number|json|tab|checkstyle, default is "colored-line-number"
format: colored-line-number
# print lines of code with issue, default is true
print-issued-lines: true
# print linter name in the end of issue text, default is true
print-linter-name: true
# all available settings of specific linters
linters-settings:
errcheck:
# report about not checking of errors in type assetions: `a := b.(MyStruct)`;
# default is false: such cases aren't reported by default.
check-type-assertions: false
# report about assignment of errors to blank identifier: `num, _ := strconv.Atoi(numStr)`;
# default is false: such cases aren't reported by default.
check-blank: false
govet:
# report about shadowed variables
check-shadowing: true
# Obtain type information from installed (to $GOPATH/pkg) package files:
# golangci-lint will execute `go install -i` and `go test -i` for analyzed packages
# before analyzing them.
# By default this option is disabled and govet gets type information by loader from source code.
# Loading from source code is slow, but it's done only once for all linters.
# Go-installing of packages first time is much slower than loading them from source code,
# therefore this option is disabled by default.
# But repeated installation is fast in go >= 1.10 because of build caching.
# Enable this option only if all conditions are met:
# 1. you use only "fast" linters (--fast e.g.): no program loading occurs
# 2. you use go >= 1.10
# 3. you do repeated runs (false for CI) or cache $GOPATH/pkg or `go env GOCACHE` dir in CI.
use-installed-packages: false
golint:
# minimal confidence for issues, default is 0.8
min-confidence: 0.8
gofmt:
# simplify code: gofmt with `-s` option, true by default
simplify: true
gocyclo:
# minimal code complexity to report, 30 by default (but we recommend 10-20)
min-complexity: 10
maligned:
# print struct with more effective memory layout or not, false by default
suggest-new: true
dupl:
# tokens count to trigger issue, 150 by default
threshold: 100
goconst:
# minimal length of string constant, 3 by default
min-len: 3
# minimal occurrences count to trigger, 3 by default
min-occurrences: 3
depguard:
list-type: blacklist
include-go-root: false
packages:
- github.com/davecgh/go-spew/spew
linters:
enable:
- megacheck
- govet
enable-all: false
disable:
maligned
disable-all: false
presets:
- bugs
- unused
fast: false
issues:
# List of regexps of issue texts to exclude, empty list by default.
# But independently from this option we use default exclude patterns,
# it can be disabled by `exclude-use-default: false`. To list all
# excluded by default patterns execute `golangci-lint run --help`
exclude:
- abcdef
# Independently from option `exclude` we use default exclude patterns,
# it can be disabled by this option. To list all
# excluded by default patterns execute `golangci-lint run --help`.
# Default value for this option is false.
exclude-use-default: true
# Maximum issues count per one linter. Set to 0 to disable. Default is 50.
max-per-linter: 0
# Maximum count of issues with the same text. Set to 0 to disable. Default is 3.
max-same: 0
# Show only new issues: if there are unstaged changes or untracked files,
# only those changes are analyzed, else only changes in HEAD~ are analyzed.
# It's a super-useful option for integration of golangci-lint into existing
# large codebase. It's not practical to fix all existing issues at the moment
# of integration: much better don't allow issues in new code.
# Default is false.
new: false
# Show only new issues created after git revision `REV`
new-from-rev: REV
# Show only new issues created in git patch with set file path.
new-from-patch: path/to/patch/file
```
It's a [.golangci.yml](https://github.com/golangci/golangci-lint/blob/master/.golangci.yml) config file of this repo: we enable more linters It's a [.golangci.yml](https://github.com/golangci/golangci-lint/blob/master/.golangci.yml) config file of this repo: we enable more linters
than the default and more strict settings: than the default and have more strict settings:
```yaml ```yaml
linters-settings: linters-settings:
govet: govet:
@ -410,11 +561,11 @@ go install ./vendor/github.com/golangci/golangci-lint/cmd/golangci-lint/
``` ```
Vendoring `golangci-lint` saves a network request, potentially making your CI system a little more reliable. Vendoring `golangci-lint` saves a network request, potentially making your CI system a little more reliable.
**`govet` or `golint` reports false-positives or false-negatives** **Does I need to run `go install`?**
These linters obtain type information from installed package files. No, you don't need to do it anymore. We will run `go install -i` and `go test -i`
The same issue you will encounter if will run `govet` or `golint` without `golangci-lint`. for analyzed packages ourselves. We will run them only
Try `go install ./...` (or `go install ./cmd/...`: depends on the project) first. if option `govet.use-installed-packages` is `true`.
**`golangci-lint` doesn't work** **`golangci-lint` doesn't work**

View File

@ -200,10 +200,14 @@ To see which config file is being used and where it was sourced from run golangc
Config options inside the file are identical to command-line options. Config options inside the file are identical to command-line options.
You can configure specific linters' options only within the config file (not the command-line). You can configure specific linters' options only within the config file (not the command-line).
There is a [`.golangci.yml`](https://github.com/golangci/golangci-lint/blob/master/.golangci.example.yml) example config file with all supported options. There is a [`.golangci.yml`](https://github.com/golangci/golangci-lint/blob/master/.golangci.example.yml) example
config file with all supported options, their description and default value:
```yaml
{{.GolangciYamlExample}}
```
It's a [.golangci.yml](https://github.com/golangci/golangci-lint/blob/master/.golangci.yml) config file of this repo: we enable more linters It's a [.golangci.yml](https://github.com/golangci/golangci-lint/blob/master/.golangci.yml) config file of this repo: we enable more linters
than the default and more strict settings: than the default and have more strict settings:
```yaml ```yaml
{{.GolangciYaml}} {{.GolangciYaml}}
``` ```
@ -275,11 +279,11 @@ go install ./vendor/github.com/golangci/golangci-lint/cmd/golangci-lint/
``` ```
Vendoring `golangci-lint` saves a network request, potentially making your CI system a little more reliable. Vendoring `golangci-lint` saves a network request, potentially making your CI system a little more reliable.
**`govet` or `golint` reports false-positives or false-negatives** **Does I need to run `go install`?**
These linters obtain type information from installed package files. No, you don't need to do it anymore. We will run `go install -i` and `go test -i`
The same issue you will encounter if will run `govet` or `golint` without `golangci-lint`. for analyzed packages ourselves. We will run them only
Try `go install ./...` (or `go install ./cmd/...`: depends on the project) first. if option `govet.use-installed-packages` is `true`.
**`golangci-lint` doesn't work** **`golangci-lint` doesn't work**

View File

@ -120,7 +120,8 @@ type LintersSettings struct {
CheckAssignToBlank bool `mapstructure:"check-blank"` CheckAssignToBlank bool `mapstructure:"check-blank"`
} }
Govet struct { Govet struct {
CheckShadowing bool `mapstructure:"check-shadowing"` CheckShadowing bool `mapstructure:"check-shadowing"`
UseInstalledPackages bool `mapstructure:"use-installed-packages"`
} }
Golint struct { Golint struct {
MinConfidence float64 `mapstructure:"min-confidence"` MinConfidence float64 `mapstructure:"min-confidence"`

View File

@ -2,9 +2,19 @@ package golinters
import ( import (
"context" "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/lint/linter"
"github.com/golangci/golangci-lint/pkg/logutils"
"github.com/golangci/golangci-lint/pkg/result" "github.com/golangci/golangci-lint/pkg/result"
"github.com/golangci/golangci-lint/pkg/timeutils"
govetAPI "github.com/golangci/govet" govetAPI "github.com/golangci/govet"
) )
@ -19,15 +29,20 @@ func (Govet) Desc() string {
} }
func (g Govet) Run(ctx context.Context, lintCtx *linter.Context) ([]result.Issue, error) { func (g Govet) Run(ctx context.Context, lintCtx *linter.Context) ([]result.Issue, error) {
// TODO: check .S asm files: govet can do it if pass dirs
var govetIssues []govetAPI.Issue var govetIssues []govetAPI.Issue
for _, pkg := range lintCtx.PkgProgram.Packages() { var err error
issues, err := govetAPI.Run(pkg.Files(lintCtx.Cfg.Run.AnalyzeTests), lintCtx.Settings().Govet.CheckShadowing) if lintCtx.Settings().Govet.UseInstalledPackages {
govetIssues, err = g.runOnInstalledPackages(ctx, lintCtx)
if err != nil { if err != nil {
return nil, err 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)
} }
govetIssues = append(govetIssues, issues...)
} }
if len(govetIssues) == 0 { if len(govetIssues) == 0 {
return nil, nil return nil, nil
} }
@ -42,3 +57,172 @@ func (g Govet) Run(ctx context.Context, lintCtx *linter.Context) ([]result.Issue
} }
return res, nil 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
}

50
pkg/goutils/goutils.go Normal file
View File

@ -0,0 +1,50 @@
package goutils
import (
"fmt"
"os"
"os/exec"
"strings"
"sync"
)
var discoverGoRootOnce sync.Once
var discoveredGoRoot string
var discoveredGoRootError error
func DiscoverGoRoot() (string, error) {
discoverGoRootOnce.Do(func() {
discoveredGoRoot, discoveredGoRootError = discoverGoRootImpl()
})
return discoveredGoRoot, discoveredGoRootError
}
func discoverGoRootImpl() (string, error) {
goroot := os.Getenv("GOROOT")
if goroot != "" {
return goroot, nil
}
output, err := exec.Command("go", "env", "GOROOT").Output()
if err != nil {
return "", fmt.Errorf("can't execute go env GOROOT: %s", err)
}
return strings.TrimSpace(string(output)), nil
}
func InGoRoot() (bool, error) {
goroot, err := DiscoverGoRoot()
if err != nil {
return false, err
}
wd, err := os.Getwd()
if err != nil {
return false, err
}
// TODO: strip, then add slashes
return strings.HasPrefix(wd, goroot), nil
}

View File

@ -62,10 +62,8 @@ func (c *Cache) prepareValidFiles() {
c.s = files c.s = files
} }
func LoadFromProgram(prog *loader.Program) (*Cache, error) { func LoadFromProgram(prog *loader.Program, log logutils.Log) (*Cache, error) {
c := &Cache{ c := NewCache(log)
m: map[string]*File{},
}
root, err := os.Getwd() root, err := os.Getwd()
if err != nil { if err != nil {
@ -115,10 +113,8 @@ func (c *Cache) parseFile(filePath string, fset *token.FileSet) {
} }
} }
func LoadFromFiles(files []string) (*Cache, error) { func LoadFromFiles(files []string, log logutils.Log) (*Cache, error) {
c := &Cache{ c := NewCache(log)
m: map[string]*File{},
}
fset := token.NewFileSet() fset := token.NewFileSet()
for _, filePath := range files { for _, filePath := range files {

View File

@ -57,6 +57,7 @@ func enableLinterConfigs(lcs []linter.Config, isEnabled func(lc *linter.Config)
func GetAllSupportedLinterConfigs() []linter.Config { func GetAllSupportedLinterConfigs() []linter.Config {
lcs := []linter.Config{ lcs := []linter.Config{
linter.NewConfig(golinters.Govet{}). linter.NewConfig(golinters.Govet{}).
WithFullImport(). // TODO: depend on it's configuration here
WithPresets(linter.PresetBugs). WithPresets(linter.PresetBugs).
WithSpeed(4). WithSpeed(4).
WithURL("https://golang.org/cmd/vet/"), WithURL("https://golang.org/cmd/vet/"),

View File

@ -6,11 +6,11 @@ import (
"go/build" "go/build"
"go/parser" "go/parser"
"os" "os"
"os/exec"
"path/filepath" "path/filepath"
"strings" "strings"
"time" "time"
"github.com/golangci/golangci-lint/pkg/goutils"
"github.com/golangci/golangci-lint/pkg/logutils" "github.com/golangci/golangci-lint/pkg/logutils"
"github.com/golangci/go-tools/ssa" "github.com/golangci/go-tools/ssa"
@ -24,9 +24,14 @@ import (
var loadDebugf = logutils.Debug("load") var loadDebugf = logutils.Debug("load")
func isFullImportNeeded(linters []linter.Config) bool { func isFullImportNeeded(linters []linter.Config, cfg *config.Config) bool {
for _, linter := range linters { for _, linter := range linters {
if linter.NeedsProgramLoading() { if linter.NeedsProgramLoading() {
if linter.Linter.Name() == "govet" && cfg.LintersSettings.Govet.UseInstalledPackages {
// TODO: remove this hack
continue
}
return true return true
} }
} }
@ -150,8 +155,8 @@ func getTypeCheckFuncBodies(cfg *config.Run, linters []linter.Config, pkgProg *p
} }
} }
func loadWholeAppIfNeeded(ctx context.Context, linters []linter.Config, cfg *config.Run, pkgProg *packages.Program, log logutils.Log) (*loader.Program, *loader.Config, error) { 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) { if !isFullImportNeeded(linters, cfg) {
return nil, nil, nil return nil, nil, nil
} }
@ -165,7 +170,7 @@ func loadWholeAppIfNeeded(ctx context.Context, linters []linter.Config, cfg *con
Build: bctx, Build: bctx,
AllowErrors: true, // Try to analyze partially AllowErrors: true, // Try to analyze partially
ParserMode: parser.ParseComments, // AST will be reused by linters ParserMode: parser.ParseComments, // AST will be reused by linters
TypeCheckFuncBodies: getTypeCheckFuncBodies(cfg, linters, pkgProg, log), TypeCheckFuncBodies: getTypeCheckFuncBodies(&cfg.Run, linters, pkgProg, log),
} }
var loaderArgs []string var loaderArgs []string
@ -173,7 +178,7 @@ func loadWholeAppIfNeeded(ctx context.Context, linters []linter.Config, cfg *con
if len(dirs) != 0 { if len(dirs) != 0 {
loaderArgs = dirs // dirs run loaderArgs = dirs // dirs run
} else { } else {
loaderArgs = pkgProg.Files(cfg.AnalyzeTests) // files run loaderArgs = pkgProg.Files(cfg.Run.AnalyzeTests) // files run
} }
nLoaderArgs, err := normalizePaths(loaderArgs) nLoaderArgs, err := normalizePaths(loaderArgs)
@ -181,7 +186,7 @@ func loadWholeAppIfNeeded(ctx context.Context, linters []linter.Config, cfg *con
return nil, nil, err return nil, nil, err
} }
rest, err := loadcfg.FromArgs(nLoaderArgs, cfg.AnalyzeTests) rest, err := loadcfg.FromArgs(nLoaderArgs, cfg.Run.AnalyzeTests)
if err != nil { if err != nil {
return nil, nil, fmt.Errorf("can't parepare load config with paths: %s", err) return nil, nil, fmt.Errorf("can't parepare load config with paths: %s", err)
} }
@ -217,20 +222,6 @@ func buildSSAProgram(ctx context.Context, lprog *loader.Program, log logutils.Lo
return ssaProg return ssaProg
} }
func discoverGoRoot() (string, error) {
goroot := os.Getenv("GOROOT")
if goroot != "" {
return goroot, nil
}
output, err := exec.Command("go", "env", "GOROOT").Output()
if err != nil {
return "", fmt.Errorf("can't execute go env GOROOT: %s", err)
}
return strings.TrimSpace(string(output)), nil
}
// separateNotCompilingPackages moves not compiling packages into separate slices: // separateNotCompilingPackages moves not compiling packages into separate slices:
// a lot of linters crash on such packages. Leave them only for those linters // a lot of linters crash on such packages. Leave them only for those linters
// which can work with them. // which can work with them.
@ -267,7 +258,7 @@ func separateNotCompilingPackages(lintCtx *linter.Context) {
func LoadContext(ctx context.Context, linters []linter.Config, cfg *config.Config, log logutils.Log) (*linter.Context, error) { 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 // Set GOROOT to have working cross-compilation: cross-compiled binaries
// have invalid GOROOT. XXX: can't use runtime.GOROOT(). // have invalid GOROOT. XXX: can't use runtime.GOROOT().
goroot, err := discoverGoRoot() goroot, err := goutils.DiscoverGoRoot()
if err != nil { if err != nil {
return nil, fmt.Errorf("can't discover GOROOT: %s", err) return nil, fmt.Errorf("can't discover GOROOT: %s", err)
} }
@ -291,7 +282,7 @@ func LoadContext(ctx context.Context, linters []linter.Config, cfg *config.Confi
return nil, err return nil, err
} }
prog, loaderConfig, err := loadWholeAppIfNeeded(ctx, linters, &cfg.Run, pkgProg, log) prog, loaderConfig, err := loadWholeAppIfNeeded(ctx, linters, cfg, pkgProg, log)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -301,11 +292,12 @@ func LoadContext(ctx context.Context, linters []linter.Config, cfg *config.Confi
ssaProg = buildSSAProgram(ctx, prog, log) ssaProg = buildSSAProgram(ctx, prog, log)
} }
astLog := log.Child("astcache")
var astCache *astcache.Cache var astCache *astcache.Cache
if prog != nil { if prog != nil {
astCache, err = astcache.LoadFromProgram(prog) astCache, err = astcache.LoadFromProgram(prog, astLog)
} else { } else {
astCache, err = astcache.LoadFromFiles(pkgProg.Files(cfg.Run.AnalyzeTests)) astCache, err = astcache.LoadFromFiles(pkgProg.Files(cfg.Run.AnalyzeTests), astLog)
} }
if err != nil { if err != nil {
return nil, err return nil, err

View File

@ -4,6 +4,7 @@ import (
"context" "context"
"testing" "testing"
"github.com/golangci/golangci-lint/pkg/golinters"
"github.com/golangci/golangci-lint/pkg/logutils" "github.com/golangci/golangci-lint/pkg/logutils"
"github.com/golangci/golangci-lint/pkg/config" "github.com/golangci/golangci-lint/pkg/config"
@ -16,12 +17,13 @@ import (
func TestASTCacheLoading(t *testing.T) { func TestASTCacheLoading(t *testing.T) {
ctx := context.Background() ctx := context.Background()
linters := []linter.Config{ linters := []linter.Config{
linter.NewConfig(nil).WithFullImport(), linter.NewConfig(golinters.Errcheck{}).WithFullImport(),
} }
inputPaths := []string{"./...", "./", "./load.go", "load.go"} inputPaths := []string{"./...", "./", "./load.go", "load.go"}
log := logutils.NewStderrLog("")
for _, inputPath := range inputPaths { for _, inputPath := range inputPaths {
r, err := packages.NewResolver(nil, nil, logutils.NewStderrLog("")) r, err := packages.NewResolver(nil, nil, log)
assert.NoError(t, err) assert.NoError(t, err)
pkgProg, err := r.Resolve(inputPath) pkgProg, err := r.Resolve(inputPath)
@ -30,15 +32,18 @@ func TestASTCacheLoading(t *testing.T) {
assert.NoError(t, err) assert.NoError(t, err)
assert.NotEmpty(t, pkgProg.Files(true)) assert.NotEmpty(t, pkgProg.Files(true))
prog, _, err := loadWholeAppIfNeeded(ctx, linters, &config.Run{ cfg := &config.Config{
AnalyzeTests: true, Run: config.Run{
}, pkgProg, logutils.NewStderrLog("")) AnalyzeTests: true,
},
}
prog, _, err := loadWholeAppIfNeeded(ctx, linters, cfg, pkgProg, logutils.NewStderrLog(""))
assert.NoError(t, err) assert.NoError(t, err)
astCacheFromProg, err := astcache.LoadFromProgram(prog) astCacheFromProg, err := astcache.LoadFromProgram(prog, log)
assert.NoError(t, err) assert.NoError(t, err)
astCacheFromFiles, err := astcache.LoadFromFiles(pkgProg.Files(true)) astCacheFromFiles, err := astcache.LoadFromFiles(pkgProg.Files(true), log)
assert.NoError(t, err) assert.NoError(t, err)
filesFromProg := astCacheFromProg.GetAllValidFiles() filesFromProg := astCacheFromProg.GetAllValidFiles()

View File

@ -75,7 +75,7 @@ func (r Runner) runLinterSafe(ctx context.Context, lintCtx *linter.Context, lc l
}() }()
specificLintCtx := *lintCtx specificLintCtx := *lintCtx
specificLintCtx.Log = lintCtx.Log.Child(lc.Linter.Name()) specificLintCtx.Log = r.Log.Child(lc.Linter.Name())
issues, err := lc.Linter.Run(ctx, &specificLintCtx) issues, err := lc.Linter.Run(ctx, &specificLintCtx)
if err != nil { if err != nil {
return nil, err return nil, err

View File

@ -9,6 +9,7 @@ type Package struct {
bp *build.Package bp *build.Package
isFake bool isFake bool
dir string // dir != bp.dir only if isFake == true
} }
func (pkg *Package) Files(includeTest bool) []string { func (pkg *Package) Files(includeTest bool) []string {
@ -16,8 +17,7 @@ func (pkg *Package) Files(includeTest bool) []string {
// TODO: add cgo files // TODO: add cgo files
if includeTest { if includeTest {
pkgFiles = append(pkgFiles, pkg.bp.TestGoFiles...) pkgFiles = append(pkgFiles, pkg.TestFiles()...)
pkgFiles = append(pkgFiles, pkg.bp.XTestGoFiles...)
} }
for i, f := range pkgFiles { for i, f := range pkgFiles {
@ -26,3 +26,22 @@ func (pkg *Package) Files(includeTest bool) []string {
return pkgFiles return pkgFiles
} }
func (pkg *Package) Dir() string {
if pkg.dir != "" { // for fake packages
return pkg.dir
}
return pkg.bp.Dir
}
func (pkg *Package) IsTestOnly() bool {
return len(pkg.bp.GoFiles) == 0
}
func (pkg *Package) TestFiles() []string {
var pkgFiles []string
pkgFiles = append(pkgFiles, pkg.bp.TestGoFiles...)
pkgFiles = append(pkgFiles, pkg.bp.XTestGoFiles...)
return pkgFiles
}

View File

@ -63,7 +63,7 @@ func (p *Program) Dirs() []string {
var ret []string var ret []string
for _, pkg := range p.packages { for _, pkg := range p.packages {
if !pkg.isFake { if !pkg.isFake {
ret = append(ret, pkg.bp.Dir) ret = append(ret, pkg.Dir())
} }
} }

View File

@ -138,9 +138,11 @@ func (r Resolver) addFakePackage(filePath string, prog *Program) {
// do it. // do it.
p := Package{ p := Package{
bp: &build.Package{ bp: &build.Package{
// TODO: detect is it test file or not: without that we can't analyze only one test file
GoFiles: []string{filePath}, GoFiles: []string{filePath},
}, },
isFake: true, isFake: true,
dir: filepath.Dir(filePath),
} }
prog.addPackage(&p) prog.addPackage(&p)
} }

View File

@ -57,7 +57,10 @@ type kv struct {
func walkStringToIntMapSortedByValue(m map[string]int, walk func(k string, v int)) { func walkStringToIntMapSortedByValue(m map[string]int, walk func(k string, v int)) {
var ss []kv var ss []kv
for k, v := range m { for k, v := range m {
ss = append(ss, kv{k, v}) ss = append(ss, kv{
Key: k,
Value: v,
})
} }
sort.Slice(ss, func(i, j int) bool { sort.Slice(ss, func(i, j int) bool {

View File

@ -48,6 +48,11 @@ func buildTemplateContext() (map[string]interface{}, error) {
return nil, fmt.Errorf("can't read .golangci.yml: %s", err) return nil, fmt.Errorf("can't read .golangci.yml: %s", err)
} }
golangciYamlExample, err := ioutil.ReadFile(".golangci.example.yml")
if err != nil {
return nil, fmt.Errorf("can't read .golangci.example.yml: %s", err)
}
if err = exec.Command("go", "install", "./cmd/...").Run(); err != nil { if err = exec.Command("go", "install", "./cmd/...").Run(); err != nil {
return nil, fmt.Errorf("can't run go install: %s", err) return nil, fmt.Errorf("can't run go install: %s", err)
} }
@ -72,6 +77,7 @@ func buildTemplateContext() (map[string]interface{}, error) {
return map[string]interface{}{ return map[string]interface{}{
"GolangciYaml": string(golangciYaml), "GolangciYaml": string(golangciYaml),
"GolangciYamlExample": string(golangciYamlExample),
"LintersCommandOutputEnabledOnly": string(lintersOutParts[0]), "LintersCommandOutputEnabledOnly": string(lintersOutParts[0]),
"LintersCommandOutputDisabledOnly": string(lintersOutParts[1]), "LintersCommandOutputDisabledOnly": string(lintersOutParts[1]),
"EnabledByDefaultLinters": getLintersListMarkdown(true), "EnabledByDefaultLinters": getLintersListMarkdown(true),

View File

@ -19,8 +19,11 @@ import (
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
var root = filepath.Join("..", "...")
var installOnce sync.Once var installOnce sync.Once
const noIssuesOut = "Congrats! No issues were found.\n"
func installBinary(t assert.TestingT) { func installBinary(t assert.TestingT) {
installOnce.Do(func() { installOnce.Do(func() {
cmd := exec.Command("go", "install", filepath.Join("..", "cmd", binName)) cmd := exec.Command("go", "install", filepath.Join("..", "cmd", binName))
@ -30,7 +33,7 @@ func installBinary(t assert.TestingT) {
func checkNoIssuesRun(t *testing.T, out string, exitCode int) { func checkNoIssuesRun(t *testing.T, out string, exitCode int) {
assert.Equal(t, exitcodes.Success, exitCode) assert.Equal(t, exitcodes.Success, exitCode)
assert.Equal(t, "Congrats! No issues were found.\n", out) assert.Equal(t, noIssuesOut, out)
} }
func TestCongratsMessageGoneIfSilent(t *testing.T) { func TestCongratsMessageGoneIfSilent(t *testing.T) {
@ -40,7 +43,7 @@ func TestCongratsMessageGoneIfSilent(t *testing.T) {
} }
func TestCongratsMessageIfNoIssues(t *testing.T) { func TestCongratsMessageIfNoIssues(t *testing.T) {
out, exitCode := runGolangciLint(t, "../...") out, exitCode := runGolangciLint(t, root)
checkNoIssuesRun(t, out, exitCode) checkNoIssuesRun(t, out, exitCode)
} }
@ -66,7 +69,7 @@ func TestRunOnAbsPath(t *testing.T) {
} }
func TestDeadline(t *testing.T) { func TestDeadline(t *testing.T) {
out, exitCode := runGolangciLint(t, "--deadline=1ms", filepath.Join("..", "...")) out, exitCode := runGolangciLint(t, "--deadline=1ms", root)
assert.Equal(t, exitcodes.Timeout, exitCode) assert.Equal(t, exitcodes.Timeout, exitCode)
assert.Contains(t, out, "deadline exceeded: try increase it by passing --deadline option") assert.Contains(t, out, "deadline exceeded: try increase it by passing --deadline option")
assert.NotContains(t, out, "Congrats! No issues were found.") assert.NotContains(t, out, "Congrats! No issues were found.")
@ -343,6 +346,15 @@ func TestEnabledLinters(t *testing.T) {
} }
} }
func TestGovetInFastMode(t *testing.T) {
cfg := `
linters-settings:
use-installed-packages: true
`
out := runGolangciLintWithYamlConfig(t, cfg, "--fast", "-Egovet", root)
assert.Equal(t, noIssuesOut, out)
}
func TestEnabledPresetsAreNotDuplicated(t *testing.T) { func TestEnabledPresetsAreNotDuplicated(t *testing.T) {
out, ec := runGolangciLint(t, "--no-config", "-v", "-p", "style,bugs") out, ec := runGolangciLint(t, "--no-config", "-v", "-p", "style,bugs")
assert.Equal(t, exitcodes.Success, ec) assert.Equal(t, exitcodes.Success, ec)

View File

@ -1,8 +1,11 @@
package govet package govet
import ( import (
"go/ast"
"go/token" "go/token"
"strings" "strings"
"golang.org/x/tools/go/loader"
) )
type Issue struct { type Issue struct {
@ -12,8 +15,9 @@ type Issue struct {
var foundIssues []Issue var foundIssues []Issue
func Run(files []string, checkShadowing bool) ([]Issue, error) { func Analyze(files []*ast.File, fset *token.FileSet, pkgInfo *loader.PackageInfo, checkShadowing bool) ([]Issue, error) {
foundIssues = nil foundIssues = nil
*source = false // import type data for "fmt" from installed packages
if checkShadowing { if checkShadowing {
experimental["shadow"] = false experimental["shadow"] = false
@ -28,12 +32,18 @@ func Run(files []string, checkShadowing bool) ([]Issue, error) {
initUnusedFlags() initUnusedFlags()
filesRun = true filesRun = true
for _, name := range files { for _, f := range files {
name := fset.Position(f.Pos()).Filename
if !strings.HasSuffix(name, "_test.go") { if !strings.HasSuffix(name, "_test.go") {
includesNonTest = true includesNonTest = true
} }
} }
if doPackage(files, nil) == nil { pkg, err := doPackage(nil, pkgInfo, fset, files)
if err != nil {
return nil, err
}
if pkg == nil {
return nil, nil return nil, nil
} }

View File

@ -14,7 +14,6 @@ import (
"go/ast" "go/ast"
"go/build" "go/build"
"go/importer" "go/importer"
"go/parser"
"go/printer" "go/printer"
"go/token" "go/token"
"go/types" "go/types"
@ -24,12 +23,14 @@ import (
"path/filepath" "path/filepath"
"strconv" "strconv"
"strings" "strings"
"golang.org/x/tools/go/loader"
) )
// Important! If you add flags here, make sure to update cmd/go/internal/vet/vetflag.go. // Important! If you add flags here, make sure to update cmd/go/internal/vet/vetflag.go.
var ( var (
verbose = flag.Bool("v", false, "verbose") verbose = flag.Bool("v", true, "verbose")
source = flag.Bool("source", false, "import from source instead of compiled object files") source = flag.Bool("source", false, "import from source instead of compiled object files")
tags = flag.String("tags", "", "space-separated list of build tags to apply when parsing") tags = flag.String("tags", "", "space-separated list of build tags to apply when parsing")
tagList = []string{} // exploded version of tags flag; set in main tagList = []string{} // exploded version of tags flag; set in main
@ -271,7 +272,7 @@ func main() {
} }
os.Exit(exitCode) os.Exit(exitCode)
} }
if doPackage(flag.Args(), nil) == nil { if pkg, _ := doPackage(nil, nil, nil, nil); pkg == nil {
warnf("no files checked") warnf("no files checked")
} }
os.Exit(exitCode) os.Exit(exitCode)
@ -344,7 +345,7 @@ func doPackageCfg(cfgFile string) {
stdImporter = &vcfg stdImporter = &vcfg
inittypes() inittypes()
mustTypecheck = true mustTypecheck = true
doPackage(vcfg.GoFiles, nil) doPackage(nil, nil, nil, nil)
} }
// doPackageDir analyzes the single package found in the directory, if there is one, // doPackageDir analyzes the single package found in the directory, if there is one,
@ -372,12 +373,12 @@ func doPackageDir(directory string) {
names = append(names, pkg.TestGoFiles...) // These are also in the "foo" package. names = append(names, pkg.TestGoFiles...) // These are also in the "foo" package.
names = append(names, pkg.SFiles...) names = append(names, pkg.SFiles...)
prefixDirectory(directory, names) prefixDirectory(directory, names)
basePkg := doPackage(names, nil) basePkg, _ := doPackage(nil, nil, nil, nil)
// Is there also a "foo_test" package? If so, do that one as well. // Is there also a "foo_test" package? If so, do that one as well.
if len(pkg.XTestGoFiles) > 0 { if len(pkg.XTestGoFiles) > 0 {
names = pkg.XTestGoFiles names = pkg.XTestGoFiles
prefixDirectory(directory, names) prefixDirectory(directory, names)
doPackage(names, basePkg) doPackage(basePkg, nil, nil, nil)
} }
} }
@ -392,58 +393,73 @@ type Package struct {
typesPkg *types.Package typesPkg *types.Package
} }
func shortestRelPath(path string, wd string) (string, error) {
if wd == "" { // get it if user don't have cached working dir
var err error
wd, err = os.Getwd()
if err != nil {
return "", fmt.Errorf("can't get working directory: %s", err)
}
}
// make path absolute and then relative to be able to fix this case:
// we'are in /test dir, we want to normalize ../test, and have file file.go in this dir;
// it must have normalized path file.go, not ../test/file.go,
var absPath string
if filepath.IsAbs(path) {
absPath = path
} else {
absPath = filepath.Join(wd, path)
}
relPath, err := filepath.Rel(wd, absPath)
if err != nil {
return "", fmt.Errorf("can't get relative path for path %s and root %s: %s",
absPath, wd, err)
}
return relPath, nil
}
// doPackage analyzes the single package constructed from the named files. // doPackage analyzes the single package constructed from the named files.
// It returns the parsed Package or nil if none of the files have been checked. // It returns the parsed Package or nil if none of the files have been checked.
func doPackage(names []string, basePkg *Package) *Package { func doPackage(basePkg *Package, pkgInfo *loader.PackageInfo, fs *token.FileSet, astFiles []*ast.File) (*Package, error) {
var files []*File var files []*File
var astFiles []*ast.File for _, parsedFile := range astFiles {
fs := token.NewFileSet() name := fs.Position(parsedFile.Pos()).Filename
for _, name := range names { shortName, err := shortestRelPath(name, "")
data, err := ioutil.ReadFile(name)
if err != nil { if err != nil {
// Warn but continue to next package. return nil, err
warnf("%s: %s", name, err)
return nil
} }
checkBuildTag(name, data)
var parsedFile *ast.File data, err := ioutil.ReadFile(shortName)
if strings.HasSuffix(name, ".go") { if err != nil {
parsedFile, err = parser.ParseFile(fs, name, data, 0) return nil, fmt.Errorf("can't read %q: %s", shortName, err)
if err != nil {
warnf("%s: %s", name, err)
return nil
}
astFiles = append(astFiles, parsedFile)
} }
checkBuildTag(shortName, data)
files = append(files, &File{ files = append(files, &File{
fset: fs, fset: fs,
content: data, content: data,
name: name, name: shortName,
file: parsedFile, file: parsedFile,
dead: make(map[ast.Node]bool), dead: make(map[ast.Node]bool),
}) })
} }
if len(astFiles) == 0 {
return nil
}
pkg := new(Package) pkg := new(Package)
pkg.path = astFiles[0].Name.Name pkg.path = astFiles[0].Name.Name
pkg.files = files pkg.files = files
// Type check the package. // Type check the package.
errs := pkg.check(fs, astFiles) errs := pkg.check(fs, astFiles, pkgInfo)
if errs != nil { if errs != nil {
if *verbose || mustTypecheck { errors := []string{}
for _, err := range errs { for _, err := range errs {
fmt.Fprintf(os.Stderr, "%v\n", err) errors = append(errors, err.Error())
}
if mustTypecheck {
// This message could be silenced, and we could just exit,
// but it might be helpful at least at first to make clear that the
// above errors are coming from vet and not the compiler
// (they often look like compiler errors, such as "declared but not used").
errorf("typecheck failures")
}
} }
return nil, fmt.Errorf("can't typecheck package: %s", strings.Join(errors, "|"))
} }
// Check. // Check.
@ -464,7 +480,7 @@ func doPackage(names []string, basePkg *Package) *Package {
} }
} }
asmCheck(pkg) asmCheck(pkg)
return pkg return pkg, nil
} }
func visit(path string, f os.FileInfo, err error) error { func visit(path string, f os.FileInfo, err error) error {
@ -552,7 +568,7 @@ func (f *File) loc(pos token.Pos) string {
// expression instead of the inner part with the actual error, the // expression instead of the inner part with the actual error, the
// precision can mislead. // precision can mislead.
posn := f.fset.Position(pos) posn := f.fset.Position(pos)
return fmt.Sprintf("%s:%d", posn.Filename, posn.Line) return fmt.Sprintf("%s:%d", f.name, posn.Line)
} }
// locPrefix returns a formatted representation of the position for use as a line prefix. // locPrefix returns a formatted representation of the position for use as a line prefix.

View File

@ -7,11 +7,16 @@
package govet package govet
import ( import (
"fmt"
"go/ast" "go/ast"
"go/build" "go/build"
"go/importer" "go/importer"
"go/token" "go/token"
"go/types" "go/types"
"os"
"time"
"golang.org/x/tools/go/loader"
) )
// stdImporter is the importer we use to import packages. // stdImporter is the importer we use to import packages.
@ -24,15 +29,22 @@ var (
formatterType *types.Interface // possibly nil formatterType *types.Interface // possibly nil
) )
func inittypes() { func inittypes() error {
errorType = types.Universe.Lookup("error").Type().Underlying().(*types.Interface) errorType = types.Universe.Lookup("error").Type().Underlying().(*types.Interface)
if typ := importType("fmt", "Stringer"); typ != nil { typ, err := importType("fmt", "Stringer")
stringerType = typ.Underlying().(*types.Interface) if err != nil {
return err
} }
if typ := importType("fmt", "Formatter"); typ != nil { stringerType = typ.Underlying().(*types.Interface)
formatterType = typ.Underlying().(*types.Interface)
typ, err = importType("fmt", "Formatter")
if err != nil {
return err
} }
formatterType = typ.Underlying().(*types.Interface)
return nil
} }
// isNamedType reports whether t is the named type path.name. // isNamedType reports whether t is the named type path.name.
@ -48,59 +60,76 @@ func isNamedType(t types.Type, path, name string) bool {
// importType returns the type denoted by the qualified identifier // importType returns the type denoted by the qualified identifier
// path.name, and adds the respective package to the imports map // path.name, and adds the respective package to the imports map
// as a side effect. In case of an error, importType returns nil. // as a side effect. In case of an error, importType returns nil.
func importType(path, name string) types.Type { func importType(path, name string) (types.Type, error) {
startedAt := time.Now()
defer func() {
fmt.Fprintf(os.Stderr, "vet: import of type %s.%s took %s\n", path, name, time.Since(startedAt))
}()
pkg, err := stdImporter.Import(path) pkg, err := stdImporter.Import(path)
if err != nil { if err != nil {
// This can happen if the package at path hasn't been compiled yet. // This can happen if the package at path hasn't been compiled yet.
warnf("import failed: %v", err) return nil, fmt.Errorf("import of type %s.%s failed: %v", path, name, err)
return nil
} }
if obj, ok := pkg.Scope().Lookup(name).(*types.TypeName); ok { if obj, ok := pkg.Scope().Lookup(name).(*types.TypeName); ok {
return obj.Type() return obj.Type(), nil
} }
warnf("invalid type name %q", name)
return nil return nil, fmt.Errorf("can't import type %s.%s: invalid type name %q", path, name, name)
} }
func (pkg *Package) check(fs *token.FileSet, astFiles []*ast.File) []error { func (pkg *Package) check(fs *token.FileSet, astFiles []*ast.File, pkgInfo *loader.PackageInfo) []error {
if stdImporter == nil { if stdImporter == nil {
if *source { if *source {
stdImporter = importer.For("source", nil) stdImporter = importer.For("source", nil)
} else { } else {
stdImporter = importer.Default() stdImporter = importer.Default()
} }
inittypes() if err := inittypes(); err != nil {
return []error{fmt.Errorf("can't init std types: %s", err)}
}
} }
pkg.defs = make(map[*ast.Ident]types.Object)
pkg.uses = make(map[*ast.Ident]types.Object)
pkg.selectors = make(map[*ast.SelectorExpr]*types.Selection)
pkg.spans = make(map[types.Object]Span)
pkg.types = make(map[ast.Expr]types.TypeAndValue)
var allErrors []error var allErrors []error
config := types.Config{ pkg.spans = make(map[types.Object]Span)
// We use the same importer for all imports to ensure that if pkgInfo != nil {
// everybody sees identical packages for the given paths. pkg.defs = pkgInfo.Defs
Importer: stdImporter, pkg.uses = pkgInfo.Uses
// By providing a Config with our own error function, it will continue pkg.selectors = pkgInfo.Selections
// past the first error. We collect them all for printing later. pkg.types = pkgInfo.Types
Error: func(e error) { pkg.typesPkg = pkgInfo.Pkg
allErrors = append(allErrors, e) } else {
}, pkg.defs = make(map[*ast.Ident]types.Object)
pkg.uses = make(map[*ast.Ident]types.Object)
pkg.selectors = make(map[*ast.SelectorExpr]*types.Selection)
pkg.types = make(map[ast.Expr]types.TypeAndValue)
Sizes: archSizes, config := types.Config{
// We use the same importer for all imports to ensure that
// everybody sees identical packages for the given paths.
Importer: stdImporter,
// By providing a Config with our own error function, it will continue
// past the first error. We collect them all for printing later.
Error: func(e error) {
allErrors = append(allErrors, e)
},
Sizes: archSizes,
}
info := &types.Info{
Selections: pkg.selectors,
Types: pkg.types,
Defs: pkg.defs,
Uses: pkg.uses,
}
typesPkg, err := config.Check(pkg.path, fs, astFiles, info)
if len(allErrors) == 0 && err != nil {
allErrors = append(allErrors, fmt.Errorf("type checker failed: %s", err))
}
pkg.typesPkg = typesPkg
} }
info := &types.Info{
Selections: pkg.selectors,
Types: pkg.types,
Defs: pkg.defs,
Uses: pkg.uses,
}
typesPkg, err := config.Check(pkg.path, fs, astFiles, info)
if len(allErrors) == 0 && err != nil {
allErrors = append(allErrors, err)
}
pkg.typesPkg = typesPkg
// update spans // update spans
for id, obj := range pkg.defs { for id, obj := range pkg.defs {
pkg.growSpan(id, obj) pkg.growSpan(id, obj)