435c0d9fc8
This PR switches the Nomad repository from using govendor to Go modules for managing dependencies. Aspects of the Nomad workflow remain pretty much the same. The usual Makefile targets should continue to work as they always did. The API submodule simply defers to the parent Nomad version on the repository, keeping the semantics of API versioning that currently exists.
1115 lines
31 KiB
Go
1115 lines
31 KiB
Go
package lint
|
||
|
||
/*
|
||
Package loading
|
||
|
||
Conceptually, package loading in the runner can be imagined as a
|
||
graph-shaped work list. We iteratively pop off leaf nodes (packages
|
||
that have no unloaded dependencies) and load data from export data,
|
||
our cache, or source.
|
||
|
||
Specifically, non-initial packages are loaded from export data and the
|
||
fact cache if possible, otherwise from source. Initial packages are
|
||
loaded from export data, the fact cache and the (problems, ignores,
|
||
config) cache if possible, otherwise from source.
|
||
|
||
The appeal of this approach is that it is both simple to implement and
|
||
easily parallelizable. Each leaf node can be processed independently,
|
||
and new leaf nodes appear as their dependencies are being processed.
|
||
|
||
The downside of this approach, however, is that we're doing more work
|
||
than necessary. Imagine an initial package A, which has the following
|
||
dependency chain: A->B->C->D – in the current implementation, we will
|
||
load all 4 packages. However, if package A can be loaded fully from
|
||
cached information, then none of its dependencies are necessary, and
|
||
we could avoid loading them.
|
||
|
||
|
||
Parallelism
|
||
|
||
Runner implements parallel processing of packages by spawning one
|
||
goroutine per package in the dependency graph, without any semaphores.
|
||
Each goroutine initially waits on the completion of all of its
|
||
dependencies, thus establishing correct order of processing. Once all
|
||
dependencies finish processing, the goroutine will load the package
|
||
from export data or source – this loading is guarded by a semaphore,
|
||
sized according to the number of CPU cores. This way, we only have as
|
||
many packages occupying memory and CPU resources as there are actual
|
||
cores to process them.
|
||
|
||
This combination of unbounded goroutines but bounded package loading
|
||
means that if we have many parallel, independent subgraphs, they will
|
||
all execute in parallel, while not wasting resources for long linear
|
||
chains or trying to process more subgraphs in parallel than the system
|
||
can handle.
|
||
|
||
|
||
Caching
|
||
|
||
We make use of several caches. These caches are Go's export data, our
|
||
facts cache, and our (problems, ignores, config) cache.
|
||
|
||
Initial packages will either be loaded from a combination of all three
|
||
caches, or from source. Non-initial packages will either be loaded
|
||
from a combination of export data and facts cache, or from source.
|
||
|
||
The facts cache is separate from the (problems, ignores, config) cache
|
||
because when we process non-initial packages, we generate facts, but
|
||
we discard problems and ignores.
|
||
|
||
The facts cache is keyed by (package, analyzer), whereas the
|
||
(problems, ignores, config) cache is keyed by (package, list of
|
||
analyzes). The difference between the two exists because there are
|
||
only a handful of analyses that produce facts, but hundreds of
|
||
analyses that don't. Creating one cache entry per fact-generating
|
||
analysis is feasible, creating one cache entry per normal analysis has
|
||
significant performance and storage overheads.
|
||
|
||
The downside of keying by the list of analyzes is, naturally, that a
|
||
change in list of analyzes changes the cache key. `staticcheck -checks
|
||
A` and `staticcheck -checks A,B` will therefore need their own cache
|
||
entries and not reuse each other's work. This problem does not affect
|
||
the facts cache.
|
||
|
||
*/
|
||
|
||
import (
|
||
"bytes"
|
||
"encoding/gob"
|
||
"encoding/hex"
|
||
"fmt"
|
||
"go/ast"
|
||
"go/token"
|
||
"go/types"
|
||
"reflect"
|
||
"regexp"
|
||
"runtime"
|
||
"sort"
|
||
"strconv"
|
||
"strings"
|
||
"sync"
|
||
"sync/atomic"
|
||
"time"
|
||
|
||
"golang.org/x/tools/go/analysis"
|
||
"golang.org/x/tools/go/packages"
|
||
"golang.org/x/tools/go/types/objectpath"
|
||
"honnef.co/go/tools/config"
|
||
"honnef.co/go/tools/facts"
|
||
"honnef.co/go/tools/internal/cache"
|
||
"honnef.co/go/tools/loader"
|
||
)
|
||
|
||
func init() {
|
||
gob.Register(&FileIgnore{})
|
||
gob.Register(&LineIgnore{})
|
||
}
|
||
|
||
// If enabled, abuse of the go/analysis API will lead to panics
|
||
const sanityCheck = true
|
||
|
||
// OPT(dh): for a dependency tree A->B->C->D, if we have cached data
|
||
// for B, there should be no need to load C and D individually. Go's
|
||
// export data for B contains all the data we need on types, and our
|
||
// fact cache could store the union of B, C and D in B.
|
||
//
|
||
// This may change unused's behavior, however, as it may observe fewer
|
||
// interfaces from transitive dependencies.
|
||
|
||
// OPT(dh): every single package will have the same value for
|
||
// canClearTypes. We could move the Package.decUse method to runner to
|
||
// eliminate this field. This is probably not worth it, though. There
|
||
// are only thousands of packages, so the field only takes up
|
||
// kilobytes of memory.
|
||
|
||
// OPT(dh): do we really need the Package.gen field? it's based
|
||
// trivially on pkg.results and merely caches the result of a type
|
||
// assertion. How often do we actually use the field?
|
||
|
||
type Package struct {
|
||
// dependents is initially set to 1 plus the number of packages
|
||
// that directly import this package. It is atomically decreased
|
||
// by 1 every time a dependent has been processed or when the
|
||
// package itself has been processed. Once the value reaches zero,
|
||
// the package is no longer needed.
|
||
dependents uint64
|
||
|
||
*packages.Package
|
||
Imports []*Package
|
||
initial bool
|
||
// fromSource is set to true for packages that have been loaded
|
||
// from source. This is the case for initial packages, packages
|
||
// with missing export data, and packages with no cached facts.
|
||
fromSource bool
|
||
// hash stores the package hash, as computed by packageHash
|
||
hash string
|
||
actionID cache.ActionID
|
||
done chan struct{}
|
||
|
||
resultsMu sync.Mutex
|
||
// results maps analyzer IDs to analyzer results. it is
|
||
// implemented as a deduplicating concurrent cache.
|
||
results []*result
|
||
|
||
cfg *config.Config
|
||
// gen maps file names to the code generator that created them
|
||
gen map[string]facts.Generator
|
||
problems []Problem
|
||
ignores []Ignore
|
||
errs []error
|
||
|
||
// these slices are indexed by analysis
|
||
facts []map[types.Object][]analysis.Fact
|
||
pkgFacts [][]analysis.Fact
|
||
|
||
// canClearTypes is set to true if we can discard type
|
||
// information after the package and its dependents have been
|
||
// processed. This is the case when no cumulative checkers are
|
||
// being run.
|
||
canClearTypes bool
|
||
}
|
||
|
||
type cachedPackage struct {
|
||
Problems []Problem
|
||
Ignores []Ignore
|
||
Config *config.Config
|
||
}
|
||
|
||
func (pkg *Package) decUse() {
|
||
ret := atomic.AddUint64(&pkg.dependents, ^uint64(0))
|
||
if ret == 0 {
|
||
// nobody depends on this package anymore
|
||
if pkg.canClearTypes {
|
||
pkg.Types = nil
|
||
}
|
||
pkg.facts = nil
|
||
pkg.pkgFacts = nil
|
||
|
||
for _, imp := range pkg.Imports {
|
||
imp.decUse()
|
||
}
|
||
}
|
||
}
|
||
|
||
type result struct {
|
||
v interface{}
|
||
err error
|
||
ready chan struct{}
|
||
}
|
||
|
||
type Runner struct {
|
||
cache *cache.Cache
|
||
goVersion int
|
||
stats *Stats
|
||
repeatAnalyzers uint
|
||
|
||
analyzerIDs analyzerIDs
|
||
problemsCacheKey string
|
||
|
||
// limits parallelism of loading packages
|
||
loadSem chan struct{}
|
||
}
|
||
|
||
type analyzerIDs struct {
|
||
m map[*analysis.Analyzer]int
|
||
}
|
||
|
||
func (ids analyzerIDs) get(a *analysis.Analyzer) int {
|
||
id, ok := ids.m[a]
|
||
if !ok {
|
||
panic(fmt.Sprintf("no analyzer ID for %s", a.Name))
|
||
}
|
||
return id
|
||
}
|
||
|
||
type Fact struct {
|
||
Path string
|
||
Fact analysis.Fact
|
||
}
|
||
|
||
type analysisAction struct {
|
||
analyzer *analysis.Analyzer
|
||
analyzerID int
|
||
pkg *Package
|
||
newPackageFacts []analysis.Fact
|
||
problems []Problem
|
||
|
||
pkgFacts map[*types.Package][]analysis.Fact
|
||
}
|
||
|
||
func (ac *analysisAction) String() string {
|
||
return fmt.Sprintf("%s @ %s", ac.analyzer, ac.pkg)
|
||
}
|
||
|
||
func (ac *analysisAction) allObjectFacts() []analysis.ObjectFact {
|
||
out := make([]analysis.ObjectFact, 0, len(ac.pkg.facts[ac.analyzerID]))
|
||
for obj, facts := range ac.pkg.facts[ac.analyzerID] {
|
||
for _, fact := range facts {
|
||
out = append(out, analysis.ObjectFact{
|
||
Object: obj,
|
||
Fact: fact,
|
||
})
|
||
}
|
||
}
|
||
return out
|
||
}
|
||
|
||
func (ac *analysisAction) allPackageFacts() []analysis.PackageFact {
|
||
out := make([]analysis.PackageFact, 0, len(ac.pkgFacts))
|
||
for pkg, facts := range ac.pkgFacts {
|
||
for _, fact := range facts {
|
||
out = append(out, analysis.PackageFact{
|
||
Package: pkg,
|
||
Fact: fact,
|
||
})
|
||
}
|
||
}
|
||
return out
|
||
}
|
||
|
||
func (ac *analysisAction) importObjectFact(obj types.Object, fact analysis.Fact) bool {
|
||
if sanityCheck && len(ac.analyzer.FactTypes) == 0 {
|
||
panic("analysis doesn't export any facts")
|
||
}
|
||
for _, f := range ac.pkg.facts[ac.analyzerID][obj] {
|
||
if reflect.TypeOf(f) == reflect.TypeOf(fact) {
|
||
reflect.ValueOf(fact).Elem().Set(reflect.ValueOf(f).Elem())
|
||
return true
|
||
}
|
||
}
|
||
return false
|
||
}
|
||
|
||
func (ac *analysisAction) importPackageFact(pkg *types.Package, fact analysis.Fact) bool {
|
||
if sanityCheck && len(ac.analyzer.FactTypes) == 0 {
|
||
panic("analysis doesn't export any facts")
|
||
}
|
||
for _, f := range ac.pkgFacts[pkg] {
|
||
if reflect.TypeOf(f) == reflect.TypeOf(fact) {
|
||
reflect.ValueOf(fact).Elem().Set(reflect.ValueOf(f).Elem())
|
||
return true
|
||
}
|
||
}
|
||
return false
|
||
}
|
||
|
||
func (ac *analysisAction) exportObjectFact(obj types.Object, fact analysis.Fact) {
|
||
if sanityCheck && len(ac.analyzer.FactTypes) == 0 {
|
||
panic("analysis doesn't export any facts")
|
||
}
|
||
ac.pkg.facts[ac.analyzerID][obj] = append(ac.pkg.facts[ac.analyzerID][obj], fact)
|
||
}
|
||
|
||
func (ac *analysisAction) exportPackageFact(fact analysis.Fact) {
|
||
if sanityCheck && len(ac.analyzer.FactTypes) == 0 {
|
||
panic("analysis doesn't export any facts")
|
||
}
|
||
ac.pkgFacts[ac.pkg.Types] = append(ac.pkgFacts[ac.pkg.Types], fact)
|
||
ac.newPackageFacts = append(ac.newPackageFacts, fact)
|
||
}
|
||
|
||
func (ac *analysisAction) report(pass *analysis.Pass, d analysis.Diagnostic) {
|
||
p := Problem{
|
||
Pos: DisplayPosition(pass.Fset, d.Pos),
|
||
End: DisplayPosition(pass.Fset, d.End),
|
||
Message: d.Message,
|
||
Check: pass.Analyzer.Name,
|
||
}
|
||
for _, r := range d.Related {
|
||
p.Related = append(p.Related, Related{
|
||
Pos: DisplayPosition(pass.Fset, r.Pos),
|
||
End: DisplayPosition(pass.Fset, r.End),
|
||
Message: r.Message,
|
||
})
|
||
}
|
||
ac.problems = append(ac.problems, p)
|
||
}
|
||
|
||
func (r *Runner) runAnalysis(ac *analysisAction) (ret interface{}, err error) {
|
||
ac.pkg.resultsMu.Lock()
|
||
res := ac.pkg.results[r.analyzerIDs.get(ac.analyzer)]
|
||
if res != nil {
|
||
ac.pkg.resultsMu.Unlock()
|
||
<-res.ready
|
||
return res.v, res.err
|
||
} else {
|
||
res = &result{
|
||
ready: make(chan struct{}),
|
||
}
|
||
ac.pkg.results[r.analyzerIDs.get(ac.analyzer)] = res
|
||
ac.pkg.resultsMu.Unlock()
|
||
|
||
defer func() {
|
||
res.v = ret
|
||
res.err = err
|
||
close(res.ready)
|
||
}()
|
||
|
||
pass := new(analysis.Pass)
|
||
*pass = analysis.Pass{
|
||
Analyzer: ac.analyzer,
|
||
Fset: ac.pkg.Fset,
|
||
Files: ac.pkg.Syntax,
|
||
// type information may be nil or may be populated. if it is
|
||
// nil, it will get populated later.
|
||
Pkg: ac.pkg.Types,
|
||
TypesInfo: ac.pkg.TypesInfo,
|
||
TypesSizes: ac.pkg.TypesSizes,
|
||
ResultOf: map[*analysis.Analyzer]interface{}{},
|
||
ImportObjectFact: ac.importObjectFact,
|
||
ImportPackageFact: ac.importPackageFact,
|
||
ExportObjectFact: ac.exportObjectFact,
|
||
ExportPackageFact: ac.exportPackageFact,
|
||
Report: func(d analysis.Diagnostic) {
|
||
ac.report(pass, d)
|
||
},
|
||
AllObjectFacts: ac.allObjectFacts,
|
||
AllPackageFacts: ac.allPackageFacts,
|
||
}
|
||
|
||
if !ac.pkg.initial {
|
||
// Don't report problems in dependencies
|
||
pass.Report = func(analysis.Diagnostic) {}
|
||
}
|
||
return r.runAnalysisUser(pass, ac)
|
||
}
|
||
}
|
||
|
||
func (r *Runner) loadCachedPackage(pkg *Package, analyzers []*analysis.Analyzer) (cachedPackage, bool) {
|
||
// OPT(dh): we can cache this computation, it'll be the same for all packages
|
||
id := cache.Subkey(pkg.actionID, "data "+r.problemsCacheKey)
|
||
|
||
b, _, err := r.cache.GetBytes(id)
|
||
if err != nil {
|
||
return cachedPackage{}, false
|
||
}
|
||
var cpkg cachedPackage
|
||
if err := gob.NewDecoder(bytes.NewReader(b)).Decode(&cpkg); err != nil {
|
||
return cachedPackage{}, false
|
||
}
|
||
return cpkg, true
|
||
}
|
||
|
||
func (r *Runner) loadCachedFacts(a *analysis.Analyzer, pkg *Package) ([]Fact, bool) {
|
||
if len(a.FactTypes) == 0 {
|
||
return nil, true
|
||
}
|
||
|
||
var facts []Fact
|
||
// Look in the cache for facts
|
||
aID := passActionID(pkg, a)
|
||
aID = cache.Subkey(aID, "facts")
|
||
b, _, err := r.cache.GetBytes(aID)
|
||
if err != nil {
|
||
// No cached facts, analyse this package like a user-provided one, but ignore diagnostics
|
||
return nil, false
|
||
}
|
||
|
||
if err := gob.NewDecoder(bytes.NewReader(b)).Decode(&facts); err != nil {
|
||
// Cached facts are broken, analyse this package like a user-provided one, but ignore diagnostics
|
||
return nil, false
|
||
}
|
||
return facts, true
|
||
}
|
||
|
||
type dependencyError struct {
|
||
dep string
|
||
err error
|
||
}
|
||
|
||
func (err dependencyError) nested() dependencyError {
|
||
if o, ok := err.err.(dependencyError); ok {
|
||
return o.nested()
|
||
}
|
||
return err
|
||
}
|
||
|
||
func (err dependencyError) Error() string {
|
||
if o, ok := err.err.(dependencyError); ok {
|
||
return o.Error()
|
||
}
|
||
return fmt.Sprintf("error running dependency %s: %s", err.dep, err.err)
|
||
}
|
||
|
||
func (r *Runner) makeAnalysisAction(a *analysis.Analyzer, pkg *Package) *analysisAction {
|
||
aid := r.analyzerIDs.get(a)
|
||
ac := &analysisAction{
|
||
analyzer: a,
|
||
analyzerID: aid,
|
||
pkg: pkg,
|
||
}
|
||
|
||
if len(a.FactTypes) == 0 {
|
||
return ac
|
||
}
|
||
|
||
// Merge all package facts of dependencies
|
||
ac.pkgFacts = map[*types.Package][]analysis.Fact{}
|
||
seen := map[*Package]struct{}{}
|
||
var dfs func(*Package)
|
||
dfs = func(pkg *Package) {
|
||
if _, ok := seen[pkg]; ok {
|
||
return
|
||
}
|
||
seen[pkg] = struct{}{}
|
||
s := pkg.pkgFacts[aid]
|
||
ac.pkgFacts[pkg.Types] = s[0:len(s):len(s)]
|
||
for _, imp := range pkg.Imports {
|
||
dfs(imp)
|
||
}
|
||
}
|
||
dfs(pkg)
|
||
|
||
return ac
|
||
}
|
||
|
||
// analyzes that we always want to run, even if they're not being run
|
||
// explicitly or as dependencies. these are necessary for the inner
|
||
// workings of the runner.
|
||
var injectedAnalyses = []*analysis.Analyzer{facts.Generated, config.Analyzer}
|
||
|
||
func (r *Runner) runAnalysisUser(pass *analysis.Pass, ac *analysisAction) (interface{}, error) {
|
||
if !ac.pkg.fromSource {
|
||
panic(fmt.Sprintf("internal error: %s was not loaded from source", ac.pkg))
|
||
}
|
||
|
||
// User-provided package, analyse it
|
||
// First analyze it with dependencies
|
||
for _, req := range ac.analyzer.Requires {
|
||
acReq := r.makeAnalysisAction(req, ac.pkg)
|
||
ret, err := r.runAnalysis(acReq)
|
||
if err != nil {
|
||
// We couldn't run a dependency, no point in going on
|
||
return nil, dependencyError{req.Name, err}
|
||
}
|
||
|
||
pass.ResultOf[req] = ret
|
||
}
|
||
|
||
// Then with this analyzer
|
||
var ret interface{}
|
||
for i := uint(0); i < r.repeatAnalyzers+1; i++ {
|
||
var err error
|
||
t := time.Now()
|
||
ret, err = ac.analyzer.Run(pass)
|
||
r.stats.MeasureAnalyzer(ac.analyzer, ac.pkg, time.Since(t))
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
}
|
||
|
||
if len(ac.analyzer.FactTypes) > 0 {
|
||
// Merge new facts into the package and persist them.
|
||
var facts []Fact
|
||
for _, fact := range ac.newPackageFacts {
|
||
id := r.analyzerIDs.get(ac.analyzer)
|
||
ac.pkg.pkgFacts[id] = append(ac.pkg.pkgFacts[id], fact)
|
||
facts = append(facts, Fact{"", fact})
|
||
}
|
||
for obj, afacts := range ac.pkg.facts[ac.analyzerID] {
|
||
if obj.Pkg() != ac.pkg.Package.Types {
|
||
continue
|
||
}
|
||
path, err := objectpath.For(obj)
|
||
if err != nil {
|
||
continue
|
||
}
|
||
for _, fact := range afacts {
|
||
facts = append(facts, Fact{string(path), fact})
|
||
}
|
||
}
|
||
|
||
if err := r.cacheData(facts, ac.pkg, ac.analyzer, "facts"); err != nil {
|
||
return nil, err
|
||
}
|
||
}
|
||
|
||
return ret, nil
|
||
}
|
||
|
||
func (r *Runner) cacheData(v interface{}, pkg *Package, a *analysis.Analyzer, subkey string) error {
|
||
buf := &bytes.Buffer{}
|
||
if err := gob.NewEncoder(buf).Encode(v); err != nil {
|
||
return err
|
||
}
|
||
aID := passActionID(pkg, a)
|
||
aID = cache.Subkey(aID, subkey)
|
||
if err := r.cache.PutBytes(aID, buf.Bytes()); err != nil {
|
||
return err
|
||
}
|
||
return nil
|
||
}
|
||
|
||
func NewRunner(stats *Stats) (*Runner, error) {
|
||
cache, err := cache.Default()
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
return &Runner{
|
||
cache: cache,
|
||
stats: stats,
|
||
}, nil
|
||
}
|
||
|
||
// Run loads packages corresponding to patterns and analyses them with
|
||
// analyzers. It returns the loaded packages, which contain reported
|
||
// diagnostics as well as extracted ignore directives.
|
||
//
|
||
// Note that diagnostics have not been filtered at this point yet, to
|
||
// accommodate cumulative analyzes that require additional steps to
|
||
// produce diagnostics.
|
||
func (r *Runner) Run(cfg *packages.Config, patterns []string, analyzers []*analysis.Analyzer, hasCumulative bool) ([]*Package, error) {
|
||
checkerNames := make([]string, len(analyzers))
|
||
for i, a := range analyzers {
|
||
checkerNames[i] = a.Name
|
||
}
|
||
sort.Strings(checkerNames)
|
||
r.problemsCacheKey = strings.Join(checkerNames, " ")
|
||
|
||
var allAnalyzers []*analysis.Analyzer
|
||
r.analyzerIDs = analyzerIDs{m: map[*analysis.Analyzer]int{}}
|
||
id := 0
|
||
seen := map[*analysis.Analyzer]struct{}{}
|
||
var dfs func(a *analysis.Analyzer)
|
||
dfs = func(a *analysis.Analyzer) {
|
||
if _, ok := seen[a]; ok {
|
||
return
|
||
}
|
||
seen[a] = struct{}{}
|
||
allAnalyzers = append(allAnalyzers, a)
|
||
r.analyzerIDs.m[a] = id
|
||
id++
|
||
for _, f := range a.FactTypes {
|
||
gob.Register(f)
|
||
}
|
||
for _, req := range a.Requires {
|
||
dfs(req)
|
||
}
|
||
}
|
||
for _, a := range analyzers {
|
||
if v := a.Flags.Lookup("go"); v != nil {
|
||
v.Value.Set(fmt.Sprintf("1.%d", r.goVersion))
|
||
}
|
||
dfs(a)
|
||
}
|
||
for _, a := range injectedAnalyses {
|
||
dfs(a)
|
||
}
|
||
// Run all analyzers on all packages (subject to further
|
||
// restrictions enforced later). This guarantees that if analyzer
|
||
// A1 depends on A2, and A2 has facts, that A2 will run on the
|
||
// dependencies of user-provided packages, even though A1 won't.
|
||
analyzers = allAnalyzers
|
||
|
||
var dcfg packages.Config
|
||
if cfg != nil {
|
||
dcfg = *cfg
|
||
}
|
||
|
||
atomic.StoreUint32(&r.stats.State, StateGraph)
|
||
initialPkgs, err := loader.Graph(dcfg, patterns...)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
defer r.cache.Trim()
|
||
|
||
var allPkgs []*Package
|
||
m := map[*packages.Package]*Package{}
|
||
packages.Visit(initialPkgs, nil, func(l *packages.Package) {
|
||
m[l] = &Package{
|
||
Package: l,
|
||
results: make([]*result, len(r.analyzerIDs.m)),
|
||
facts: make([]map[types.Object][]analysis.Fact, len(r.analyzerIDs.m)),
|
||
pkgFacts: make([][]analysis.Fact, len(r.analyzerIDs.m)),
|
||
done: make(chan struct{}),
|
||
// every package needs itself
|
||
dependents: 1,
|
||
canClearTypes: !hasCumulative,
|
||
}
|
||
allPkgs = append(allPkgs, m[l])
|
||
for i := range m[l].facts {
|
||
m[l].facts[i] = map[types.Object][]analysis.Fact{}
|
||
}
|
||
for _, err := range l.Errors {
|
||
m[l].errs = append(m[l].errs, err)
|
||
}
|
||
for _, v := range l.Imports {
|
||
m[v].dependents++
|
||
m[l].Imports = append(m[l].Imports, m[v])
|
||
}
|
||
|
||
m[l].hash, err = r.packageHash(m[l])
|
||
m[l].actionID = packageActionID(m[l])
|
||
if err != nil {
|
||
m[l].errs = append(m[l].errs, err)
|
||
}
|
||
})
|
||
|
||
pkgs := make([]*Package, len(initialPkgs))
|
||
for i, l := range initialPkgs {
|
||
pkgs[i] = m[l]
|
||
pkgs[i].initial = true
|
||
}
|
||
|
||
atomic.StoreUint32(&r.stats.InitialPackages, uint32(len(initialPkgs)))
|
||
atomic.StoreUint32(&r.stats.TotalPackages, uint32(len(allPkgs)))
|
||
atomic.StoreUint32(&r.stats.State, StateProcessing)
|
||
|
||
var wg sync.WaitGroup
|
||
wg.Add(len(allPkgs))
|
||
r.loadSem = make(chan struct{}, runtime.GOMAXPROCS(-1))
|
||
atomic.StoreUint32(&r.stats.TotalWorkers, uint32(cap(r.loadSem)))
|
||
for _, pkg := range allPkgs {
|
||
pkg := pkg
|
||
go func() {
|
||
r.processPkg(pkg, analyzers)
|
||
|
||
if pkg.initial {
|
||
atomic.AddUint32(&r.stats.ProcessedInitialPackages, 1)
|
||
}
|
||
atomic.AddUint32(&r.stats.Problems, uint32(len(pkg.problems)))
|
||
wg.Done()
|
||
}()
|
||
}
|
||
wg.Wait()
|
||
|
||
return pkgs, nil
|
||
}
|
||
|
||
var posRe = regexp.MustCompile(`^(.+?):(\d+)(?::(\d+)?)?`)
|
||
|
||
func parsePos(pos string) (token.Position, int, error) {
|
||
if pos == "-" || pos == "" {
|
||
return token.Position{}, 0, nil
|
||
}
|
||
parts := posRe.FindStringSubmatch(pos)
|
||
if parts == nil {
|
||
return token.Position{}, 0, fmt.Errorf("malformed position %q", pos)
|
||
}
|
||
file := parts[1]
|
||
line, _ := strconv.Atoi(parts[2])
|
||
col, _ := strconv.Atoi(parts[3])
|
||
return token.Position{
|
||
Filename: file,
|
||
Line: line,
|
||
Column: col,
|
||
}, len(parts[0]), nil
|
||
}
|
||
|
||
// loadPkg loads a Go package. It may be loaded from a combination of
|
||
// caches, or from source.
|
||
func (r *Runner) loadPkg(pkg *Package, analyzers []*analysis.Analyzer) error {
|
||
if pkg.Types != nil {
|
||
panic(fmt.Sprintf("internal error: %s has already been loaded", pkg.Package))
|
||
}
|
||
|
||
if pkg.initial {
|
||
// Try to load cached package
|
||
cpkg, ok := r.loadCachedPackage(pkg, analyzers)
|
||
if ok {
|
||
pkg.problems = cpkg.Problems
|
||
pkg.ignores = cpkg.Ignores
|
||
pkg.cfg = cpkg.Config
|
||
} else {
|
||
pkg.fromSource = true
|
||
return loader.LoadFromSource(pkg.Package)
|
||
}
|
||
}
|
||
|
||
// At this point we're either working with a non-initial package,
|
||
// or we managed to load cached problems for the package. We still
|
||
// need export data and facts.
|
||
|
||
// OPT(dh): we don't need type information for this package if no
|
||
// other package depends on it. this may be the case for initial
|
||
// packages.
|
||
|
||
// Load package from export data
|
||
if err := loader.LoadFromExport(pkg.Package); err != nil {
|
||
// We asked Go to give us up to date export data, yet
|
||
// we can't load it. There must be something wrong.
|
||
//
|
||
// Attempt loading from source. This should fail (because
|
||
// otherwise there would be export data); we just want to
|
||
// get the compile errors. If loading from source succeeds
|
||
// we discard the result, anyway. Otherwise we'll fail
|
||
// when trying to reload from export data later.
|
||
//
|
||
// FIXME(dh): we no longer reload from export data, so
|
||
// theoretically we should be able to continue
|
||
pkg.fromSource = true
|
||
if err := loader.LoadFromSource(pkg.Package); err != nil {
|
||
return err
|
||
}
|
||
// Make sure this package can't be imported successfully
|
||
pkg.Package.Errors = append(pkg.Package.Errors, packages.Error{
|
||
Pos: "-",
|
||
Msg: fmt.Sprintf("could not load export data: %s", err),
|
||
Kind: packages.ParseError,
|
||
})
|
||
return fmt.Errorf("could not load export data: %s", err)
|
||
}
|
||
|
||
failed := false
|
||
seen := make([]bool, len(r.analyzerIDs.m))
|
||
var dfs func(*analysis.Analyzer)
|
||
dfs = func(a *analysis.Analyzer) {
|
||
if seen[r.analyzerIDs.get(a)] {
|
||
return
|
||
}
|
||
seen[r.analyzerIDs.get(a)] = true
|
||
|
||
if len(a.FactTypes) > 0 {
|
||
facts, ok := r.loadCachedFacts(a, pkg)
|
||
if !ok {
|
||
failed = true
|
||
return
|
||
}
|
||
|
||
for _, f := range facts {
|
||
if f.Path == "" {
|
||
// This is a package fact
|
||
pkg.pkgFacts[r.analyzerIDs.get(a)] = append(pkg.pkgFacts[r.analyzerIDs.get(a)], f.Fact)
|
||
continue
|
||
}
|
||
obj, err := objectpath.Object(pkg.Types, objectpath.Path(f.Path))
|
||
if err != nil {
|
||
// Be lenient about these errors. For example, when
|
||
// analysing io/ioutil from source, we may get a fact
|
||
// for methods on the devNull type, and objectpath
|
||
// will happily create a path for them. However, when
|
||
// we later load io/ioutil from export data, the path
|
||
// no longer resolves.
|
||
//
|
||
// If an exported type embeds the unexported type,
|
||
// then (part of) the unexported type will become part
|
||
// of the type information and our path will resolve
|
||
// again.
|
||
continue
|
||
}
|
||
pkg.facts[r.analyzerIDs.get(a)][obj] = append(pkg.facts[r.analyzerIDs.get(a)][obj], f.Fact)
|
||
}
|
||
}
|
||
|
||
for _, req := range a.Requires {
|
||
dfs(req)
|
||
}
|
||
}
|
||
for _, a := range analyzers {
|
||
dfs(a)
|
||
}
|
||
|
||
if !failed {
|
||
return nil
|
||
}
|
||
|
||
// We failed to load some cached facts
|
||
pkg.fromSource = true
|
||
// XXX we added facts to the maps, we need to get rid of those
|
||
return loader.LoadFromSource(pkg.Package)
|
||
}
|
||
|
||
type analysisError struct {
|
||
analyzer *analysis.Analyzer
|
||
pkg *Package
|
||
err error
|
||
}
|
||
|
||
func (err analysisError) Error() string {
|
||
return fmt.Sprintf("error running analyzer %s on %s: %s", err.analyzer, err.pkg, err.err)
|
||
}
|
||
|
||
// processPkg processes a package. This involves loading the package,
|
||
// either from export data or from source. For packages loaded from
|
||
// source, the provides analyzers will be run on the package.
|
||
func (r *Runner) processPkg(pkg *Package, analyzers []*analysis.Analyzer) {
|
||
defer func() {
|
||
// Clear information we no longer need. Make sure to do this
|
||
// when returning from processPkg so that we clear
|
||
// dependencies, not just initial packages.
|
||
pkg.TypesInfo = nil
|
||
pkg.Syntax = nil
|
||
pkg.results = nil
|
||
|
||
atomic.AddUint32(&r.stats.ProcessedPackages, 1)
|
||
pkg.decUse()
|
||
close(pkg.done)
|
||
}()
|
||
|
||
// Ensure all packages have the generated map and config. This is
|
||
// required by internals of the runner. Analyses that themselves
|
||
// make use of either have an explicit dependency so that other
|
||
// runners work correctly, too.
|
||
analyzers = append(analyzers[0:len(analyzers):len(analyzers)], injectedAnalyses...)
|
||
|
||
if len(pkg.errs) != 0 {
|
||
return
|
||
}
|
||
|
||
for _, imp := range pkg.Imports {
|
||
<-imp.done
|
||
if len(imp.errs) > 0 {
|
||
if imp.initial {
|
||
// Don't print the error of the dependency since it's
|
||
// an initial package and we're already printing the
|
||
// error.
|
||
pkg.errs = append(pkg.errs, fmt.Errorf("could not analyze dependency %s of %s", imp, pkg))
|
||
} else {
|
||
var s string
|
||
for _, err := range imp.errs {
|
||
s += "\n\t" + err.Error()
|
||
}
|
||
pkg.errs = append(pkg.errs, fmt.Errorf("could not analyze dependency %s of %s: %s", imp, pkg, s))
|
||
}
|
||
return
|
||
}
|
||
}
|
||
if pkg.PkgPath == "unsafe" {
|
||
pkg.Types = types.Unsafe
|
||
return
|
||
}
|
||
|
||
r.loadSem <- struct{}{}
|
||
atomic.AddUint32(&r.stats.ActiveWorkers, 1)
|
||
defer func() {
|
||
<-r.loadSem
|
||
atomic.AddUint32(&r.stats.ActiveWorkers, ^uint32(0))
|
||
}()
|
||
if err := r.loadPkg(pkg, analyzers); err != nil {
|
||
pkg.errs = append(pkg.errs, err)
|
||
return
|
||
}
|
||
|
||
// A package's object facts is the union of all of its dependencies.
|
||
for _, imp := range pkg.Imports {
|
||
for ai, m := range imp.facts {
|
||
for obj, facts := range m {
|
||
pkg.facts[ai][obj] = facts[0:len(facts):len(facts)]
|
||
}
|
||
}
|
||
}
|
||
|
||
if !pkg.fromSource {
|
||
// Nothing left to do for the package.
|
||
return
|
||
}
|
||
|
||
// Run analyses on initial packages and those missing facts
|
||
var wg sync.WaitGroup
|
||
wg.Add(len(analyzers))
|
||
errs := make([]error, len(analyzers))
|
||
var acs []*analysisAction
|
||
for i, a := range analyzers {
|
||
i := i
|
||
a := a
|
||
ac := r.makeAnalysisAction(a, pkg)
|
||
acs = append(acs, ac)
|
||
go func() {
|
||
defer wg.Done()
|
||
// Only initial packages and packages with missing
|
||
// facts will have been loaded from source.
|
||
if pkg.initial || len(a.FactTypes) > 0 {
|
||
if _, err := r.runAnalysis(ac); err != nil {
|
||
errs[i] = analysisError{a, pkg, err}
|
||
return
|
||
}
|
||
}
|
||
}()
|
||
}
|
||
wg.Wait()
|
||
|
||
depErrors := map[dependencyError]int{}
|
||
for _, err := range errs {
|
||
if err == nil {
|
||
continue
|
||
}
|
||
switch err := err.(type) {
|
||
case analysisError:
|
||
switch err := err.err.(type) {
|
||
case dependencyError:
|
||
depErrors[err.nested()]++
|
||
default:
|
||
pkg.errs = append(pkg.errs, err)
|
||
}
|
||
default:
|
||
pkg.errs = append(pkg.errs, err)
|
||
}
|
||
}
|
||
for err, count := range depErrors {
|
||
pkg.errs = append(pkg.errs,
|
||
fmt.Errorf("could not run %s@%s, preventing %d analyzers from running: %s", err.dep, pkg, count, err.err))
|
||
}
|
||
|
||
// We can't process ignores at this point because `unused` needs
|
||
// to see more than one package to make its decision.
|
||
//
|
||
// OPT(dh): can't we guard this block of code by pkg.initial?
|
||
ignores, problems := parseDirectives(pkg.Package)
|
||
pkg.ignores = append(pkg.ignores, ignores...)
|
||
pkg.problems = append(pkg.problems, problems...)
|
||
for _, ac := range acs {
|
||
pkg.problems = append(pkg.problems, ac.problems...)
|
||
}
|
||
|
||
if pkg.initial {
|
||
// Only initial packages have these analyzers run, and only
|
||
// initial packages need these.
|
||
if pkg.results[r.analyzerIDs.get(config.Analyzer)].v != nil {
|
||
pkg.cfg = pkg.results[r.analyzerIDs.get(config.Analyzer)].v.(*config.Config)
|
||
}
|
||
pkg.gen = pkg.results[r.analyzerIDs.get(facts.Generated)].v.(map[string]facts.Generator)
|
||
}
|
||
|
||
// In a previous version of the code, we would throw away all type
|
||
// information and reload it from export data. That was
|
||
// nonsensical. The *types.Package doesn't keep any information
|
||
// live that export data wouldn't also. We only need to discard
|
||
// the AST and the TypesInfo maps; that happens after we return
|
||
// from processPkg.
|
||
}
|
||
|
||
func parseDirective(s string) (cmd string, args []string) {
|
||
if !strings.HasPrefix(s, "//lint:") {
|
||
return "", nil
|
||
}
|
||
s = strings.TrimPrefix(s, "//lint:")
|
||
fields := strings.Split(s, " ")
|
||
return fields[0], fields[1:]
|
||
}
|
||
|
||
// parseDirectives extracts all linter directives from the source
|
||
// files of the package. Malformed directives are returned as problems.
|
||
func parseDirectives(pkg *packages.Package) ([]Ignore, []Problem) {
|
||
var ignores []Ignore
|
||
var problems []Problem
|
||
|
||
for _, f := range pkg.Syntax {
|
||
found := false
|
||
commentLoop:
|
||
for _, cg := range f.Comments {
|
||
for _, c := range cg.List {
|
||
if strings.Contains(c.Text, "//lint:") {
|
||
found = true
|
||
break commentLoop
|
||
}
|
||
}
|
||
}
|
||
if !found {
|
||
continue
|
||
}
|
||
cm := ast.NewCommentMap(pkg.Fset, f, f.Comments)
|
||
for node, cgs := range cm {
|
||
for _, cg := range cgs {
|
||
for _, c := range cg.List {
|
||
if !strings.HasPrefix(c.Text, "//lint:") {
|
||
continue
|
||
}
|
||
cmd, args := parseDirective(c.Text)
|
||
switch cmd {
|
||
case "ignore", "file-ignore":
|
||
if len(args) < 2 {
|
||
p := Problem{
|
||
Pos: DisplayPosition(pkg.Fset, c.Pos()),
|
||
Message: "malformed linter directive; missing the required reason field?",
|
||
Severity: Error,
|
||
Check: "compile",
|
||
}
|
||
problems = append(problems, p)
|
||
continue
|
||
}
|
||
default:
|
||
// unknown directive, ignore
|
||
continue
|
||
}
|
||
checks := strings.Split(args[0], ",")
|
||
pos := DisplayPosition(pkg.Fset, node.Pos())
|
||
var ig Ignore
|
||
switch cmd {
|
||
case "ignore":
|
||
ig = &LineIgnore{
|
||
File: pos.Filename,
|
||
Line: pos.Line,
|
||
Checks: checks,
|
||
Pos: DisplayPosition(pkg.Fset, c.Pos()),
|
||
}
|
||
case "file-ignore":
|
||
ig = &FileIgnore{
|
||
File: pos.Filename,
|
||
Checks: checks,
|
||
}
|
||
}
|
||
ignores = append(ignores, ig)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
return ignores, problems
|
||
}
|
||
|
||
// packageHash computes a package's hash. The hash is based on all Go
|
||
// files that make up the package, as well as the hashes of imported
|
||
// packages.
|
||
func (r *Runner) packageHash(pkg *Package) (string, error) {
|
||
key := cache.NewHash("package hash")
|
||
fmt.Fprintf(key, "pkgpath %s\n", pkg.PkgPath)
|
||
fmt.Fprintf(key, "go %d\n", r.goVersion)
|
||
for _, f := range pkg.CompiledGoFiles {
|
||
h, err := cache.FileHash(f)
|
||
if err != nil {
|
||
return "", err
|
||
}
|
||
fmt.Fprintf(key, "file %s %x\n", f, h)
|
||
}
|
||
|
||
// Actually load the configuration to calculate its hash. This
|
||
// will take into consideration inheritance of configuration
|
||
// files, as well as the default configuration.
|
||
//
|
||
// OPT(dh): doing this means we'll load the config twice: once for
|
||
// computing the hash, and once when analyzing the package from
|
||
// source.
|
||
cdir := config.Dir(pkg.GoFiles)
|
||
if cdir == "" {
|
||
fmt.Fprintf(key, "file %s %x\n", config.ConfigName, [cache.HashSize]byte{})
|
||
} else {
|
||
cfg, err := config.Load(cdir)
|
||
if err != nil {
|
||
return "", err
|
||
}
|
||
h := cache.NewHash(config.ConfigName)
|
||
if _, err := h.Write([]byte(cfg.String())); err != nil {
|
||
return "", err
|
||
}
|
||
fmt.Fprintf(key, "file %s %x\n", config.ConfigName, h.Sum())
|
||
}
|
||
|
||
imps := make([]*Package, len(pkg.Imports))
|
||
copy(imps, pkg.Imports)
|
||
sort.Slice(imps, func(i, j int) bool {
|
||
return imps[i].PkgPath < imps[j].PkgPath
|
||
})
|
||
for _, dep := range imps {
|
||
if dep.PkgPath == "unsafe" {
|
||
continue
|
||
}
|
||
|
||
fmt.Fprintf(key, "import %s %s\n", dep.PkgPath, dep.hash)
|
||
}
|
||
h := key.Sum()
|
||
return hex.EncodeToString(h[:]), nil
|
||
}
|
||
|
||
func packageActionID(pkg *Package) cache.ActionID {
|
||
key := cache.NewHash("package ID")
|
||
fmt.Fprintf(key, "pkgpath %s\n", pkg.PkgPath)
|
||
fmt.Fprintf(key, "pkghash %s\n", pkg.hash)
|
||
return key.Sum()
|
||
}
|
||
|
||
// passActionID computes an ActionID for an analysis pass.
|
||
func passActionID(pkg *Package, analyzer *analysis.Analyzer) cache.ActionID {
|
||
return cache.Subkey(pkg.actionID, fmt.Sprintf("analyzer %s", analyzer.Name))
|
||
}
|