From f2efd61d352394e0d85f0e83e8b4e22f0de1ee76 Mon Sep 17 00:00:00 2001 From: Denis Isaev Date: Mon, 18 Jun 2018 00:06:45 +0300 Subject: [PATCH] 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) --- .golangci.example.yml | 94 ++++++++- Gopkg.lock | 2 +- Makefile | 1 - README.md | 165 +++++++++++++++- README.md.tmpl | 16 +- pkg/config/config.go | 3 +- pkg/golinters/govet.go | 194 ++++++++++++++++++- pkg/goutils/goutils.go | 50 +++++ pkg/lint/astcache/astcache.go | 12 +- pkg/lint/lintersdb/lintersdb.go | 1 + pkg/lint/load.go | 42 ++-- pkg/lint/load_test.go | 19 +- pkg/lint/runner.go | 2 +- pkg/packages/package.go | 23 ++- pkg/packages/program.go | 2 +- pkg/packages/resolver.go | 2 + pkg/result/processors/max_same_issues.go | 5 +- scripts/gen_readme/main.go | 6 + test/run_test.go | 18 +- vendor/github.com/golangci/govet/golangci.go | 16 +- vendor/github.com/golangci/govet/main.go | 98 ++++++---- vendor/github.com/golangci/govet/types.go | 109 +++++++---- 22 files changed, 721 insertions(+), 159 deletions(-) create mode 100644 pkg/goutils/goutils.go diff --git a/.golangci.example.yml b/.golangci.example.yml index c0dafcb4b9ed..cbcd534eda1c 100644 --- a/.golangci.example.yml +++ b/.golangci.example.yml @@ -1,40 +1,98 @@ +# 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: - - ".*\\.pb\\.go$" + - ".*\\.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: - threshold: 50 + # 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 @@ -45,8 +103,8 @@ linters-settings: linters: enable: - megacheck - - vet - enable-all: true + - govet + enable-all: false disable: maligned disable-all: false @@ -56,11 +114,35 @@ linters: 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 - 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 \ No newline at end of file diff --git a/Gopkg.lock b/Gopkg.lock index c04e4ce70bff..21944a8d0535 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -122,7 +122,7 @@ "lib/cfg", "lib/whitelist" ] - revision = "1a9ab8120ec96a014df3afab2d6c47f7e12d8928" + revision = "18c83969a303d67eaa9d67747a930f007e3e9c89" [[projects]] branch = "master" diff --git a/Makefile b/Makefile index 687d414ab01f..ac1c08835484 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,4 @@ 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 --fast --no-config -v GL_TEST_RUN=1 golangci-lint run --no-config -v diff --git a/README.md b/README.md index 5bb01b12d61d..ae244fe50574 100644 --- a/README.md +++ b/README.md @@ -84,7 +84,7 @@ GolangCI-Lint can be used with zero configuration. By default the following lint ``` $ golangci-lint 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] 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] @@ -310,10 +310,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. 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 -than the default and more strict settings: +than the default and have more strict settings: ```yaml linters-settings: govet: @@ -409,11 +560,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. -**`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. -The same issue you will encounter if will run `govet` or `golint` without `golangci-lint`. -Try `go install ./...` (or `go install ./cmd/...`: depends on the project) first. +No, you don't need to do it anymore. We will run `go install -i` and `go test -i` +for analyzed packages ourselves. We will run them only +if option `govet.use-installed-packages` is `true`. **`golangci-lint` doesn't work** diff --git a/README.md.tmpl b/README.md.tmpl index 620c3afa9dfa..345045d83878 100644 --- a/README.md.tmpl +++ b/README.md.tmpl @@ -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. 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 -than the default and more strict settings: +than the default and have more strict settings: ```yaml {{.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. -**`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. -The same issue you will encounter if will run `govet` or `golint` without `golangci-lint`. -Try `go install ./...` (or `go install ./cmd/...`: depends on the project) first. +No, you don't need to do it anymore. We will run `go install -i` and `go test -i` +for analyzed packages ourselves. We will run them only +if option `govet.use-installed-packages` is `true`. **`golangci-lint` doesn't work** diff --git a/pkg/config/config.go b/pkg/config/config.go index 5423cd1ea4da..f0c7c3d8b4ce 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -119,7 +119,8 @@ type LintersSettings struct { CheckAssignToBlank bool `mapstructure:"check-blank"` } Govet struct { - CheckShadowing bool `mapstructure:"check-shadowing"` + CheckShadowing bool `mapstructure:"check-shadowing"` + UseInstalledPackages bool `mapstructure:"use-installed-packages"` } Golint struct { MinConfidence float64 `mapstructure:"min-confidence"` diff --git a/pkg/golinters/govet.go b/pkg/golinters/govet.go index 768138d4753e..f5dbe460876a 100644 --- a/pkg/golinters/govet.go +++ b/pkg/golinters/govet.go @@ -2,9 +2,19 @@ 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" ) @@ -19,15 +29,20 @@ func (Govet) Desc() string { } 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 - for _, pkg := range lintCtx.PkgProgram.Packages() { - issues, err := govetAPI.Run(pkg.Files(lintCtx.Cfg.Run.AnalyzeTests), lintCtx.Settings().Govet.CheckShadowing) + var err error + if lintCtx.Settings().Govet.UseInstalledPackages { + govetIssues, err = g.runOnInstalledPackages(ctx, lintCtx) 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 { return nil, nil } @@ -42,3 +57,172 @@ func (g Govet) Run(ctx context.Context, lintCtx *linter.Context) ([]result.Issue } 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 +} diff --git a/pkg/goutils/goutils.go b/pkg/goutils/goutils.go new file mode 100644 index 000000000000..7bd5bb3a9766 --- /dev/null +++ b/pkg/goutils/goutils.go @@ -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 +} diff --git a/pkg/lint/astcache/astcache.go b/pkg/lint/astcache/astcache.go index 15b9d1526fe6..209c73c74e0d 100644 --- a/pkg/lint/astcache/astcache.go +++ b/pkg/lint/astcache/astcache.go @@ -62,10 +62,8 @@ func (c *Cache) prepareValidFiles() { c.s = files } -func LoadFromProgram(prog *loader.Program) (*Cache, error) { - c := &Cache{ - m: map[string]*File{}, - } +func LoadFromProgram(prog *loader.Program, log logutils.Log) (*Cache, error) { + c := NewCache(log) root, err := os.Getwd() if err != nil { @@ -115,10 +113,8 @@ func (c *Cache) parseFile(filePath string, fset *token.FileSet) { } } -func LoadFromFiles(files []string) (*Cache, error) { - c := &Cache{ - m: map[string]*File{}, - } +func LoadFromFiles(files []string, log logutils.Log) (*Cache, error) { + c := NewCache(log) fset := token.NewFileSet() for _, filePath := range files { diff --git a/pkg/lint/lintersdb/lintersdb.go b/pkg/lint/lintersdb/lintersdb.go index 4f35fd66e3e8..dc978936d408 100644 --- a/pkg/lint/lintersdb/lintersdb.go +++ b/pkg/lint/lintersdb/lintersdb.go @@ -57,6 +57,7 @@ func enableLinterConfigs(lcs []linter.Config, isEnabled func(lc *linter.Config) func GetAllSupportedLinterConfigs() []linter.Config { lcs := []linter.Config{ linter.NewConfig(golinters.Govet{}). + WithFullImport(). // TODO: depend on it's configuration here WithPresets(linter.PresetBugs). WithSpeed(4). WithURL("https://golang.org/cmd/vet/"), diff --git a/pkg/lint/load.go b/pkg/lint/load.go index 9f089f5e0bf7..bc899ea783eb 100644 --- a/pkg/lint/load.go +++ b/pkg/lint/load.go @@ -6,11 +6,11 @@ import ( "go/build" "go/parser" "os" - "os/exec" "path/filepath" "strings" "time" + "github.com/golangci/golangci-lint/pkg/goutils" "github.com/golangci/golangci-lint/pkg/logutils" "github.com/golangci/go-tools/ssa" @@ -24,9 +24,14 @@ import ( var loadDebugf = logutils.Debug("load") -func isFullImportNeeded(linters []linter.Config) bool { +func isFullImportNeeded(linters []linter.Config, cfg *config.Config) bool { for _, linter := range linters { if linter.NeedsProgramLoading() { + if linter.Linter.Name() == "govet" && cfg.LintersSettings.Govet.UseInstalledPackages { + // TODO: remove this hack + continue + } + 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) { - if !isFullImportNeeded(linters) { +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, cfg) { return nil, nil, nil } @@ -165,7 +170,7 @@ func loadWholeAppIfNeeded(ctx context.Context, linters []linter.Config, cfg *con Build: bctx, AllowErrors: true, // Try to analyze partially 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 @@ -173,7 +178,7 @@ func loadWholeAppIfNeeded(ctx context.Context, linters []linter.Config, cfg *con if len(dirs) != 0 { loaderArgs = dirs // dirs run } else { - loaderArgs = pkgProg.Files(cfg.AnalyzeTests) // files run + loaderArgs = pkgProg.Files(cfg.Run.AnalyzeTests) // files run } nLoaderArgs, err := normalizePaths(loaderArgs) @@ -181,7 +186,7 @@ func loadWholeAppIfNeeded(ctx context.Context, linters []linter.Config, cfg *con return nil, nil, err } - rest, err := loadcfg.FromArgs(nLoaderArgs, cfg.AnalyzeTests) + 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) } @@ -217,20 +222,6 @@ func buildSSAProgram(ctx context.Context, lprog *loader.Program, log logutils.Lo 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: // a lot of linters crash on such packages. Leave them only for those linters // 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) { // Set GOROOT to have working cross-compilation: cross-compiled binaries // have invalid GOROOT. XXX: can't use runtime.GOROOT(). - goroot, err := discoverGoRoot() + goroot, err := goutils.DiscoverGoRoot() if err != nil { 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 } - prog, loaderConfig, err := loadWholeAppIfNeeded(ctx, linters, &cfg.Run, pkgProg, log) + prog, loaderConfig, err := loadWholeAppIfNeeded(ctx, linters, cfg, pkgProg, log) if err != nil { return nil, err } @@ -301,11 +292,12 @@ func LoadContext(ctx context.Context, linters []linter.Config, cfg *config.Confi ssaProg = buildSSAProgram(ctx, prog, log) } + astLog := log.Child("astcache") var astCache *astcache.Cache if prog != nil { - astCache, err = astcache.LoadFromProgram(prog) + astCache, err = astcache.LoadFromProgram(prog, astLog) } else { - astCache, err = astcache.LoadFromFiles(pkgProg.Files(cfg.Run.AnalyzeTests)) + astCache, err = astcache.LoadFromFiles(pkgProg.Files(cfg.Run.AnalyzeTests), astLog) } if err != nil { return nil, err diff --git a/pkg/lint/load_test.go b/pkg/lint/load_test.go index eaaeb700a802..b0f5d87eff53 100644 --- a/pkg/lint/load_test.go +++ b/pkg/lint/load_test.go @@ -4,6 +4,7 @@ import ( "context" "testing" + "github.com/golangci/golangci-lint/pkg/golinters" "github.com/golangci/golangci-lint/pkg/logutils" "github.com/golangci/golangci-lint/pkg/config" @@ -16,12 +17,13 @@ import ( func TestASTCacheLoading(t *testing.T) { ctx := context.Background() linters := []linter.Config{ - linter.NewConfig(nil).WithFullImport(), + linter.NewConfig(golinters.Errcheck{}).WithFullImport(), } inputPaths := []string{"./...", "./", "./load.go", "load.go"} + log := logutils.NewStderrLog("") for _, inputPath := range inputPaths { - r, err := packages.NewResolver(nil, nil, logutils.NewStderrLog("")) + r, err := packages.NewResolver(nil, nil, log) assert.NoError(t, err) pkgProg, err := r.Resolve(inputPath) @@ -30,15 +32,18 @@ func TestASTCacheLoading(t *testing.T) { assert.NoError(t, err) assert.NotEmpty(t, pkgProg.Files(true)) - prog, _, err := loadWholeAppIfNeeded(ctx, linters, &config.Run{ - AnalyzeTests: true, - }, pkgProg, logutils.NewStderrLog("")) + cfg := &config.Config{ + Run: config.Run{ + AnalyzeTests: true, + }, + } + prog, _, err := loadWholeAppIfNeeded(ctx, linters, cfg, pkgProg, logutils.NewStderrLog("")) assert.NoError(t, err) - astCacheFromProg, err := astcache.LoadFromProgram(prog) + astCacheFromProg, err := astcache.LoadFromProgram(prog, log) assert.NoError(t, err) - astCacheFromFiles, err := astcache.LoadFromFiles(pkgProg.Files(true)) + astCacheFromFiles, err := astcache.LoadFromFiles(pkgProg.Files(true), log) assert.NoError(t, err) filesFromProg := astCacheFromProg.GetAllValidFiles() diff --git a/pkg/lint/runner.go b/pkg/lint/runner.go index 1f7a90f4c20b..ea313fc66c96 100644 --- a/pkg/lint/runner.go +++ b/pkg/lint/runner.go @@ -75,7 +75,7 @@ func (r Runner) runLinterSafe(ctx context.Context, lintCtx *linter.Context, lc l }() 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) if err != nil { return nil, err diff --git a/pkg/packages/package.go b/pkg/packages/package.go index 8dfae0ff4a6c..fa938bd55bed 100644 --- a/pkg/packages/package.go +++ b/pkg/packages/package.go @@ -9,6 +9,7 @@ type Package struct { bp *build.Package isFake bool + dir string // dir != bp.dir only if isFake == true } func (pkg *Package) Files(includeTest bool) []string { @@ -16,8 +17,7 @@ func (pkg *Package) Files(includeTest bool) []string { // TODO: add cgo files if includeTest { - pkgFiles = append(pkgFiles, pkg.bp.TestGoFiles...) - pkgFiles = append(pkgFiles, pkg.bp.XTestGoFiles...) + pkgFiles = append(pkgFiles, pkg.TestFiles()...) } for i, f := range pkgFiles { @@ -26,3 +26,22 @@ func (pkg *Package) Files(includeTest bool) []string { 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 +} diff --git a/pkg/packages/program.go b/pkg/packages/program.go index d268994446d4..638a36e1f797 100644 --- a/pkg/packages/program.go +++ b/pkg/packages/program.go @@ -63,7 +63,7 @@ func (p *Program) Dirs() []string { var ret []string for _, pkg := range p.packages { if !pkg.isFake { - ret = append(ret, pkg.bp.Dir) + ret = append(ret, pkg.Dir()) } } diff --git a/pkg/packages/resolver.go b/pkg/packages/resolver.go index d224e665cee7..f281c59ed285 100644 --- a/pkg/packages/resolver.go +++ b/pkg/packages/resolver.go @@ -138,9 +138,11 @@ func (r Resolver) addFakePackage(filePath string, prog *Program) { // do it. p := 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}, }, isFake: true, + dir: filepath.Dir(filePath), } prog.addPackage(&p) } diff --git a/pkg/result/processors/max_same_issues.go b/pkg/result/processors/max_same_issues.go index b59b7a3227da..fe3c2b22a097 100644 --- a/pkg/result/processors/max_same_issues.go +++ b/pkg/result/processors/max_same_issues.go @@ -57,7 +57,10 @@ type kv struct { func walkStringToIntMapSortedByValue(m map[string]int, walk func(k string, v int)) { var ss []kv 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 { diff --git a/scripts/gen_readme/main.go b/scripts/gen_readme/main.go index eb207ca5f19d..51a3eae9302d 100644 --- a/scripts/gen_readme/main.go +++ b/scripts/gen_readme/main.go @@ -48,6 +48,11 @@ func buildTemplateContext() (map[string]interface{}, error) { 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 { 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{}{ "GolangciYaml": string(golangciYaml), + "GolangciYamlExample": string(golangciYamlExample), "LintersCommandOutputEnabledOnly": string(lintersOutParts[0]), "LintersCommandOutputDisabledOnly": string(lintersOutParts[1]), "EnabledByDefaultLinters": getLintersListMarkdown(true), diff --git a/test/run_test.go b/test/run_test.go index fa9647f2091e..83f4a7d4d476 100644 --- a/test/run_test.go +++ b/test/run_test.go @@ -19,8 +19,11 @@ import ( "github.com/stretchr/testify/assert" ) +var root = filepath.Join("..", "...") var installOnce sync.Once +const noIssuesOut = "Congrats! No issues were found.\n" + func installBinary(t assert.TestingT) { installOnce.Do(func() { cmd := exec.Command("go", "install", filepath.Join("..", "cmd", binName)) @@ -30,11 +33,11 @@ func installBinary(t assert.TestingT) { func checkNoIssuesRun(t *testing.T, out string, exitCode int) { assert.Equal(t, exitcodes.Success, exitCode) - assert.Equal(t, "Congrats! No issues were found.\n", out) + assert.Equal(t, noIssuesOut, out) } func TestCongratsMessageIfNoIssues(t *testing.T) { - out, exitCode := runGolangciLint(t, "../...") + out, exitCode := runGolangciLint(t, root) checkNoIssuesRun(t, out, exitCode) } @@ -60,7 +63,7 @@ func TestRunOnAbsPath(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.Contains(t, out, "deadline exceeded: try increase it by passing --deadline option") assert.NotContains(t, out, "Congrats! No issues were found.") @@ -337,6 +340,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) { out, ec := runGolangciLint(t, "--no-config", "-v", "-p", "style,bugs") assert.Equal(t, exitcodes.Success, ec) diff --git a/vendor/github.com/golangci/govet/golangci.go b/vendor/github.com/golangci/govet/golangci.go index c602876b7bea..a8b4ea457f04 100644 --- a/vendor/github.com/golangci/govet/golangci.go +++ b/vendor/github.com/golangci/govet/golangci.go @@ -1,8 +1,11 @@ package govet import ( + "go/ast" "go/token" "strings" + + "golang.org/x/tools/go/loader" ) type Issue struct { @@ -12,8 +15,9 @@ type Issue struct { 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 + *source = false // import type data for "fmt" from installed packages if checkShadowing { experimental["shadow"] = false @@ -28,12 +32,18 @@ func Run(files []string, checkShadowing bool) ([]Issue, error) { initUnusedFlags() filesRun = true - for _, name := range files { + for _, f := range files { + name := fset.Position(f.Pos()).Filename if !strings.HasSuffix(name, "_test.go") { 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 } diff --git a/vendor/github.com/golangci/govet/main.go b/vendor/github.com/golangci/govet/main.go index c7e2afc1a970..dea3ed719df3 100644 --- a/vendor/github.com/golangci/govet/main.go +++ b/vendor/github.com/golangci/govet/main.go @@ -14,7 +14,6 @@ import ( "go/ast" "go/build" "go/importer" - "go/parser" "go/printer" "go/token" "go/types" @@ -24,12 +23,14 @@ import ( "path/filepath" "strconv" "strings" + + "golang.org/x/tools/go/loader" ) // Important! If you add flags here, make sure to update cmd/go/internal/vet/vetflag.go. 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") tags = flag.String("tags", "", "space-separated list of build tags to apply when parsing") tagList = []string{} // exploded version of tags flag; set in main @@ -271,7 +272,7 @@ func main() { } os.Exit(exitCode) } - if doPackage(flag.Args(), nil) == nil { + if pkg, _ := doPackage(nil, nil, nil, nil); pkg == nil { warnf("no files checked") } os.Exit(exitCode) @@ -344,7 +345,7 @@ func doPackageCfg(cfgFile string) { stdImporter = &vcfg inittypes() mustTypecheck = true - doPackage(vcfg.GoFiles, nil) + doPackage(nil, nil, nil, nil) } // 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.SFiles...) 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. if len(pkg.XTestGoFiles) > 0 { names = pkg.XTestGoFiles prefixDirectory(directory, names) - doPackage(names, basePkg) + doPackage(basePkg, nil, nil, nil) } } @@ -392,58 +393,73 @@ type Package struct { 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. // 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 astFiles []*ast.File - fs := token.NewFileSet() - for _, name := range names { - data, err := ioutil.ReadFile(name) + for _, parsedFile := range astFiles { + name := fs.Position(parsedFile.Pos()).Filename + shortName, err := shortestRelPath(name, "") if err != nil { - // Warn but continue to next package. - warnf("%s: %s", name, err) - return nil + return nil, err } - checkBuildTag(name, data) - var parsedFile *ast.File - if strings.HasSuffix(name, ".go") { - parsedFile, err = parser.ParseFile(fs, name, data, 0) - if err != nil { - warnf("%s: %s", name, err) - return nil - } - astFiles = append(astFiles, parsedFile) + + data, err := ioutil.ReadFile(shortName) + if err != nil { + return nil, fmt.Errorf("can't read %q: %s", shortName, err) } + + checkBuildTag(shortName, data) + files = append(files, &File{ fset: fs, content: data, - name: name, + name: shortName, file: parsedFile, dead: make(map[ast.Node]bool), }) } - if len(astFiles) == 0 { - return nil - } + pkg := new(Package) pkg.path = astFiles[0].Name.Name pkg.files = files // Type check the package. - errs := pkg.check(fs, astFiles) + errs := pkg.check(fs, astFiles, pkgInfo) if errs != nil { - if *verbose || mustTypecheck { - for _, err := range errs { - fmt.Fprintf(os.Stderr, "%v\n", err) - } - 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") - } + errors := []string{} + for _, err := range errs { + errors = append(errors, err.Error()) } + + return nil, fmt.Errorf("can't typecheck package: %s", strings.Join(errors, "|")) } // Check. @@ -464,7 +480,7 @@ func doPackage(names []string, basePkg *Package) *Package { } } asmCheck(pkg) - return pkg + return pkg, nil } 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 // precision can mislead. 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. diff --git a/vendor/github.com/golangci/govet/types.go b/vendor/github.com/golangci/govet/types.go index 4bed51f7967e..14548c8e4e78 100644 --- a/vendor/github.com/golangci/govet/types.go +++ b/vendor/github.com/golangci/govet/types.go @@ -7,11 +7,16 @@ package govet import ( + "fmt" "go/ast" "go/build" "go/importer" "go/token" "go/types" + "os" + "time" + + "golang.org/x/tools/go/loader" ) // stdImporter is the importer we use to import packages. @@ -24,15 +29,22 @@ var ( formatterType *types.Interface // possibly nil ) -func inittypes() { +func inittypes() error { errorType = types.Universe.Lookup("error").Type().Underlying().(*types.Interface) - if typ := importType("fmt", "Stringer"); typ != nil { - stringerType = typ.Underlying().(*types.Interface) + typ, err := importType("fmt", "Stringer") + if err != nil { + return err } - if typ := importType("fmt", "Formatter"); typ != nil { - formatterType = typ.Underlying().(*types.Interface) + stringerType = 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. @@ -48,59 +60,76 @@ func isNamedType(t types.Type, path, name string) bool { // importType returns the type denoted by the qualified identifier // path.name, and adds the respective package to the imports map // 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) if err != nil { // This can happen if the package at path hasn't been compiled yet. - warnf("import failed: %v", err) - return nil + return nil, fmt.Errorf("import of type %s.%s failed: %v", path, name, err) } 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 *source { stdImporter = importer.For("source", nil) } else { 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 - 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, err) + pkg.spans = make(map[types.Object]Span) + if pkgInfo != nil { + pkg.defs = pkgInfo.Defs + pkg.uses = pkgInfo.Uses + pkg.selectors = pkgInfo.Selections + pkg.types = pkgInfo.Types + pkg.typesPkg = pkgInfo.Pkg + } 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) + + 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 } - pkg.typesPkg = typesPkg + // update spans for id, obj := range pkg.defs { pkg.growSpan(id, obj)