50fbc2eec2
* CI: use staticcheck for linting This commit switches the linter for Go code from golint to staticcheck. Golint has been deprecated since last year and staticcheck is a recommended replacement. Signed-off-by: Lucas Servén Marín <lserven@gmail.com> * revendor Signed-off-by: Lucas Servén Marín <lserven@gmail.com> * cmd,pkg: fix lint warnings Signed-off-by: Lucas Servén Marín <lserven@gmail.com>
1331 lines
36 KiB
Go
1331 lines
36 KiB
Go
// Package runner implements a go/analysis runner. It makes heavy use
|
||
// of on-disk caching to reduce overall memory usage and to speed up
|
||
// repeat runs.
|
||
//
|
||
// Public API
|
||
//
|
||
// A Runner maps a list of analyzers and package patterns to a list of
|
||
// results. Results provide access to diagnostics, directives, errors
|
||
// encountered, and information about packages. Results explicitly do
|
||
// not contain ASTs or type information. All position information is
|
||
// returned in the form of token.Position, not token.Pos. All work
|
||
// that requires access to the loaded representation of a package has
|
||
// to occur inside analyzers.
|
||
//
|
||
// Planning and execution
|
||
//
|
||
// Analyzing packages is split into two phases: planning and
|
||
// execution.
|
||
//
|
||
// During planning, a directed acyclic graph of package dependencies
|
||
// is computed. We materialize the full graph so that we can execute
|
||
// the graph from the bottom up, without keeping unnecessary data in
|
||
// memory during a DFS and with simplified parallel execution.
|
||
//
|
||
// During execution, leaf nodes (nodes with no outstanding
|
||
// dependencies) get executed in parallel, bounded by a semaphore
|
||
// sized according to the number of CPUs. Conceptually, this happens
|
||
// in a loop, processing new leaf nodes as they appear, until no more
|
||
// nodes are left. In the actual implementation, nodes know their
|
||
// dependents, and the last dependency of a node to be processed is
|
||
// responsible for scheduling its dependent.
|
||
//
|
||
// The graph is rooted at a synthetic root node. Upon execution of the
|
||
// root node, the algorithm terminates.
|
||
//
|
||
// Analyzing a package repeats the same planning + execution steps,
|
||
// but this time on a graph of analyzers for the package. Parallel
|
||
// execution of individual analyzers is bounded by the same semaphore
|
||
// as executing packages.
|
||
//
|
||
// Parallelism
|
||
//
|
||
// Actions are executed in parallel where the dependency graph allows.
|
||
// Overall parallelism is bounded by a semaphore, sized according to
|
||
// GOMAXPROCS. Each concurrently processed package takes up a
|
||
// token, as does each analyzer – but a package can always execute at
|
||
// least one analyzer, using the package's token.
|
||
//
|
||
// Depending on the overall shape of the graph, there may be GOMAXPROCS
|
||
// packages running a single analyzer each, a single package running
|
||
// GOMAXPROCS analyzers, or anything in between.
|
||
//
|
||
// Total memory consumption grows roughly linearly with the number of
|
||
// CPUs, while total execution time is inversely proportional to the
|
||
// number of CPUs. Overall, parallelism is affected by the shape of
|
||
// the dependency graph. A lot of inter-connected packages will see
|
||
// less parallelism than a lot of independent packages.
|
||
//
|
||
// Caching
|
||
//
|
||
// The runner caches facts, directives and diagnostics in a
|
||
// content-addressable cache that is designed after Go's own cache.
|
||
// Additionally, it makes use of Go's export data.
|
||
//
|
||
// This cache not only speeds up repeat runs, it also reduces peak
|
||
// memory usage. When we've analyzed a package, we cache the results
|
||
// and drop them from memory. When a dependent needs any of this
|
||
// information, or when analysis is complete and we wish to render the
|
||
// results, the data gets loaded from disk again.
|
||
//
|
||
// Data only exists in memory when it is immediately needed, not
|
||
// retained for possible future uses. This trades increased CPU usage
|
||
// for reduced memory usage. A single dependency may be loaded many
|
||
// times over, but it greatly reduces peak memory usage, as an
|
||
// arbitrary amount of time may pass between analyzing a dependency
|
||
// and its dependent, during which other packages will be processed.
|
||
package runner
|
||
|
||
// OPT(dh): we could reduce disk storage usage of cached data by
|
||
// compressing it, either directly at the cache layer, or by feeding
|
||
// compressed data to the cache. Of course doing so may negatively
|
||
// affect CPU usage, and there are lower hanging fruit, such as
|
||
// needing to cache less data in the first place.
|
||
|
||
// OPT(dh): right now, each package is analyzed completely
|
||
// independently. Each package loads all of its dependencies from
|
||
// export data and cached facts. If we have two packages A and B,
|
||
// which both depend on C, and which both get analyzed in parallel,
|
||
// then C will be loaded twice. This wastes CPU time and memory. It
|
||
// would be nice if we could reuse a single C for the analysis of both
|
||
// A and B.
|
||
//
|
||
// We can't reuse the actual types.Package or facts, because each
|
||
// package gets its own token.FileSet. Sharing a global FileSet has
|
||
// several drawbacks, including increased memory usage and running the
|
||
// risk of running out of FileSet address space.
|
||
//
|
||
// We could however avoid loading the same raw export data from disk
|
||
// twice, as well as deserializing gob data twice. One possible
|
||
// solution would be a duplicate-suppressing in-memory cache that
|
||
// caches data for a limited amount of time. When the same package
|
||
// needs to be loaded twice in close succession, we can reuse work,
|
||
// without holding unnecessary data in memory for an extended period
|
||
// of time.
|
||
//
|
||
// We would likely need to do extensive benchmarking to figure out how
|
||
// long to keep data around to find a sweet spot where we reduce CPU
|
||
// load without increasing memory usage.
|
||
//
|
||
// We can probably populate the cache after we've analyzed a package,
|
||
// on the assumption that it will have to be loaded again in the near
|
||
// future.
|
||
|
||
import (
|
||
"bytes"
|
||
"encoding/gob"
|
||
"fmt"
|
||
"go/token"
|
||
"go/types"
|
||
"io"
|
||
"io/ioutil"
|
||
"os"
|
||
"reflect"
|
||
"runtime"
|
||
"sort"
|
||
"strings"
|
||
"sync/atomic"
|
||
"time"
|
||
|
||
"honnef.co/go/tools/analysis/lint"
|
||
"honnef.co/go/tools/analysis/report"
|
||
"honnef.co/go/tools/config"
|
||
"honnef.co/go/tools/go/loader"
|
||
tsync "honnef.co/go/tools/internal/sync"
|
||
"honnef.co/go/tools/lintcmd/cache"
|
||
"honnef.co/go/tools/unused"
|
||
|
||
"golang.org/x/tools/go/analysis"
|
||
"golang.org/x/tools/go/packages"
|
||
"golang.org/x/tools/go/types/objectpath"
|
||
)
|
||
|
||
const sanityCheck = false
|
||
|
||
// Diagnostic is like go/analysis.Diagnostic, but with all token.Pos resolved to token.Position.
|
||
type Diagnostic struct {
|
||
Position token.Position
|
||
End token.Position
|
||
Category string
|
||
Message string
|
||
|
||
SuggestedFixes []SuggestedFix
|
||
Related []RelatedInformation
|
||
}
|
||
|
||
// RelatedInformation provides additional context for a diagnostic.
|
||
type RelatedInformation struct {
|
||
Position token.Position
|
||
End token.Position
|
||
Message string
|
||
}
|
||
|
||
type SuggestedFix struct {
|
||
Message string
|
||
TextEdits []TextEdit
|
||
}
|
||
|
||
type TextEdit struct {
|
||
Position token.Position
|
||
End token.Position
|
||
NewText []byte
|
||
}
|
||
|
||
// A Result describes the result of analyzing a single package.
|
||
//
|
||
// It holds references to cached diagnostics and directives. They can
|
||
// be loaded on demand with the Load method.
|
||
type Result struct {
|
||
Package *loader.PackageSpec
|
||
Config config.Config
|
||
Initial bool
|
||
Skipped bool
|
||
|
||
Failed bool
|
||
Errors []error
|
||
// Action results, path to file
|
||
results string
|
||
// Results relevant to testing, only set when test mode is enabled, path to file
|
||
testData string
|
||
}
|
||
|
||
type SerializedDirective struct {
|
||
Command string
|
||
Arguments []string
|
||
// The position of the comment
|
||
DirectivePosition token.Position
|
||
// The position of the node that the comment is attached to
|
||
NodePosition token.Position
|
||
}
|
||
|
||
func serializeDirective(dir lint.Directive, fset *token.FileSet) SerializedDirective {
|
||
return SerializedDirective{
|
||
Command: dir.Command,
|
||
Arguments: dir.Arguments,
|
||
DirectivePosition: report.DisplayPosition(fset, dir.Directive.Pos()),
|
||
NodePosition: report.DisplayPosition(fset, dir.Node.Pos()),
|
||
}
|
||
}
|
||
|
||
type ResultData struct {
|
||
Directives []SerializedDirective
|
||
Diagnostics []Diagnostic
|
||
Unused unused.SerializedResult
|
||
}
|
||
|
||
func (r Result) Load() (ResultData, error) {
|
||
if r.Failed {
|
||
panic("Load called on failed Result")
|
||
}
|
||
if r.results == "" {
|
||
// this package was only a dependency
|
||
return ResultData{}, nil
|
||
}
|
||
f, err := os.Open(r.results)
|
||
if err != nil {
|
||
return ResultData{}, fmt.Errorf("failed loading result: %w", err)
|
||
}
|
||
defer f.Close()
|
||
var out ResultData
|
||
err = gob.NewDecoder(f).Decode(&out)
|
||
return out, err
|
||
}
|
||
|
||
type Want struct {
|
||
Position token.Position
|
||
Comment string
|
||
}
|
||
|
||
// TestData contains extra information about analysis runs that is only available in test mode.
|
||
type TestData struct {
|
||
// Wants contains a list of '// want' comments extracted from Go files.
|
||
// These comments are used in unit tests.
|
||
Wants []Want
|
||
// Facts contains facts produced by analyzers for a package.
|
||
// Unlike vetx, this list only contains facts specific to this package,
|
||
// not all facts for the transitive closure of dependencies.
|
||
Facts []TestFact
|
||
}
|
||
|
||
// LoadTest returns data relevant to testing.
|
||
// It should only be called if Runner.TestMode was set to true.
|
||
func (r Result) LoadTest() (TestData, error) {
|
||
if r.Failed {
|
||
panic("Load called on failed Result")
|
||
}
|
||
if r.results == "" {
|
||
// this package was only a dependency
|
||
return TestData{}, nil
|
||
}
|
||
f, err := os.Open(r.testData)
|
||
if err != nil {
|
||
return TestData{}, fmt.Errorf("failed loading test data: %w", err)
|
||
}
|
||
defer f.Close()
|
||
var out TestData
|
||
err = gob.NewDecoder(f).Decode(&out)
|
||
return out, err
|
||
}
|
||
|
||
type action interface {
|
||
Deps() []action
|
||
Triggers() []action
|
||
DecrementPending() bool
|
||
MarkFailed()
|
||
IsFailed() bool
|
||
AddError(error)
|
||
}
|
||
|
||
type baseAction struct {
|
||
// Action description
|
||
|
||
deps []action
|
||
triggers []action
|
||
pending uint32
|
||
|
||
// Action results
|
||
|
||
// failed is set to true if the action couldn't be processed. This
|
||
// may either be due to an error specific to this action, in
|
||
// which case the errors field will be populated, or due to a
|
||
// dependency being marked as failed, in which case errors will be
|
||
// empty.
|
||
failed bool
|
||
errors []error
|
||
}
|
||
|
||
func (act *baseAction) Deps() []action { return act.deps }
|
||
func (act *baseAction) Triggers() []action { return act.triggers }
|
||
func (act *baseAction) DecrementPending() bool {
|
||
return atomic.AddUint32(&act.pending, ^uint32(0)) == 0
|
||
}
|
||
func (act *baseAction) MarkFailed() { act.failed = true }
|
||
func (act *baseAction) IsFailed() bool { return act.failed }
|
||
func (act *baseAction) AddError(err error) { act.errors = append(act.errors, err) }
|
||
|
||
// packageAction describes the act of loading a package, fully
|
||
// analyzing it, and storing the results.
|
||
type packageAction struct {
|
||
baseAction
|
||
|
||
// Action description
|
||
Package *loader.PackageSpec
|
||
factsOnly bool
|
||
hash cache.ActionID
|
||
|
||
// Action results
|
||
cfg config.Config
|
||
vetx string
|
||
results string
|
||
testData string
|
||
skipped bool
|
||
}
|
||
|
||
func (act *packageAction) String() string {
|
||
return fmt.Sprintf("packageAction(%s)", act.Package)
|
||
}
|
||
|
||
type objectFact struct {
|
||
fact analysis.Fact
|
||
// TODO(dh): why do we store the objectpath when producing the
|
||
// fact? Is it just for the sanity checking, which compares the
|
||
// stored path with a path recomputed from objectFactKey.Obj?
|
||
path objectpath.Path
|
||
}
|
||
|
||
type objectFactKey struct {
|
||
Obj types.Object
|
||
Type reflect.Type
|
||
}
|
||
|
||
type packageFactKey struct {
|
||
Pkg *types.Package
|
||
Type reflect.Type
|
||
}
|
||
|
||
type gobFact struct {
|
||
PkgPath string
|
||
ObjPath string
|
||
Fact analysis.Fact
|
||
}
|
||
|
||
// TestFact is a serialization of facts that is specific to the test mode.
|
||
type TestFact struct {
|
||
ObjectName string
|
||
Position token.Position
|
||
FactString string
|
||
Analyzer string
|
||
}
|
||
|
||
// analyzerAction describes the act of analyzing a package with a
|
||
// single analyzer.
|
||
type analyzerAction struct {
|
||
baseAction
|
||
|
||
// Action description
|
||
|
||
Analyzer *analysis.Analyzer
|
||
|
||
// Action results
|
||
|
||
// We can store actual results here without worrying about memory
|
||
// consumption because analyzer actions get garbage collected once
|
||
// a package has been fully analyzed.
|
||
Result interface{}
|
||
Diagnostics []Diagnostic
|
||
ObjectFacts map[objectFactKey]objectFact
|
||
PackageFacts map[packageFactKey]analysis.Fact
|
||
Pass *analysis.Pass
|
||
}
|
||
|
||
func (act *analyzerAction) String() string {
|
||
return fmt.Sprintf("analyzerAction(%s)", act.Analyzer)
|
||
}
|
||
|
||
// A Runner executes analyzers on packages.
|
||
type Runner struct {
|
||
Stats Stats
|
||
GoVersion string
|
||
// if GoVersion == "module", and we couldn't determine the
|
||
// module's Go version, use this as the fallback
|
||
FallbackGoVersion string
|
||
// If set to true, Runner will populate results with data relevant to testing analyzers
|
||
TestMode bool
|
||
|
||
// GoVersion might be "module"; actualGoVersion contains the resolved version
|
||
actualGoVersion string
|
||
|
||
// Config that gets merged with per-package configs
|
||
cfg config.Config
|
||
cache *cache.Cache
|
||
semaphore tsync.Semaphore
|
||
}
|
||
|
||
type subrunner struct {
|
||
*Runner
|
||
analyzers []*analysis.Analyzer
|
||
factAnalyzers []*analysis.Analyzer
|
||
analyzerNames string
|
||
cache *cache.Cache
|
||
}
|
||
|
||
// New returns a new Runner.
|
||
func New(cfg config.Config, c *cache.Cache) (*Runner, error) {
|
||
return &Runner{
|
||
cfg: cfg,
|
||
cache: c,
|
||
semaphore: tsync.NewSemaphore(runtime.GOMAXPROCS(0)),
|
||
}, nil
|
||
}
|
||
|
||
func newSubrunner(r *Runner, analyzers []*analysis.Analyzer) *subrunner {
|
||
analyzerNames := make([]string, len(analyzers))
|
||
for i, a := range analyzers {
|
||
analyzerNames[i] = a.Name
|
||
}
|
||
sort.Strings(analyzerNames)
|
||
|
||
var factAnalyzers []*analysis.Analyzer
|
||
for _, a := range analyzers {
|
||
if len(a.FactTypes) > 0 {
|
||
factAnalyzers = append(factAnalyzers, a)
|
||
}
|
||
}
|
||
return &subrunner{
|
||
Runner: r,
|
||
analyzers: analyzers,
|
||
factAnalyzers: factAnalyzers,
|
||
analyzerNames: strings.Join(analyzerNames, ","),
|
||
cache: r.cache,
|
||
}
|
||
}
|
||
|
||
func newPackageActionRoot(pkg *loader.PackageSpec, cache map[*loader.PackageSpec]*packageAction) *packageAction {
|
||
a := newPackageAction(pkg, cache)
|
||
a.factsOnly = false
|
||
return a
|
||
}
|
||
|
||
func newPackageAction(pkg *loader.PackageSpec, cache map[*loader.PackageSpec]*packageAction) *packageAction {
|
||
if a, ok := cache[pkg]; ok {
|
||
return a
|
||
}
|
||
|
||
a := &packageAction{
|
||
Package: pkg,
|
||
factsOnly: true, // will be overwritten by any call to Action
|
||
}
|
||
cache[pkg] = a
|
||
|
||
if len(pkg.Errors) > 0 {
|
||
a.errors = make([]error, len(pkg.Errors))
|
||
for i, err := range pkg.Errors {
|
||
a.errors[i] = err
|
||
}
|
||
a.failed = true
|
||
|
||
// We don't need to process our imports if this package is
|
||
// already broken.
|
||
return a
|
||
}
|
||
|
||
a.deps = make([]action, 0, len(pkg.Imports))
|
||
for _, dep := range pkg.Imports {
|
||
depa := newPackageAction(dep, cache)
|
||
depa.triggers = append(depa.triggers, a)
|
||
a.deps = append(a.deps, depa)
|
||
|
||
if depa.failed {
|
||
a.failed = true
|
||
}
|
||
}
|
||
// sort dependencies because the list of dependencies is part of
|
||
// the cache key
|
||
sort.Slice(a.deps, func(i, j int) bool {
|
||
return a.deps[i].(*packageAction).Package.ID < a.deps[j].(*packageAction).Package.ID
|
||
})
|
||
|
||
a.pending = uint32(len(a.deps))
|
||
|
||
return a
|
||
}
|
||
|
||
func newAnalyzerAction(an *analysis.Analyzer, cache map[*analysis.Analyzer]*analyzerAction) *analyzerAction {
|
||
if a, ok := cache[an]; ok {
|
||
return a
|
||
}
|
||
|
||
a := &analyzerAction{
|
||
Analyzer: an,
|
||
ObjectFacts: map[objectFactKey]objectFact{},
|
||
PackageFacts: map[packageFactKey]analysis.Fact{},
|
||
}
|
||
cache[an] = a
|
||
for _, dep := range an.Requires {
|
||
depa := newAnalyzerAction(dep, cache)
|
||
depa.triggers = append(depa.triggers, a)
|
||
a.deps = append(a.deps, depa)
|
||
}
|
||
a.pending = uint32(len(a.deps))
|
||
return a
|
||
}
|
||
|
||
func getCachedFiles(cache *cache.Cache, ids []cache.ActionID, out []*string) error {
|
||
for i, id := range ids {
|
||
var err error
|
||
*out[i], _, err = cache.GetFile(id)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
}
|
||
return nil
|
||
}
|
||
|
||
func (r *subrunner) do(act action) error {
|
||
a := act.(*packageAction)
|
||
defer func() {
|
||
r.Stats.finishPackage()
|
||
if !a.factsOnly {
|
||
r.Stats.finishInitialPackage()
|
||
}
|
||
}()
|
||
|
||
// compute hash of action
|
||
a.cfg = a.Package.Config.Merge(r.cfg)
|
||
h := r.cache.NewHash("staticcheck " + a.Package.PkgPath)
|
||
|
||
// Note that we do not filter the list of analyzers by the
|
||
// package's configuration. We don't allow configuration to
|
||
// accidentally break dependencies between analyzers, and it's
|
||
// easier to always run all checks and filter the output. This
|
||
// also makes cached data more reusable.
|
||
|
||
// OPT(dh): not all changes in configuration invalidate cached
|
||
// data. specifically, when a.factsOnly == true, we only care
|
||
// about checks that produce facts, and settings that affect those
|
||
// checks.
|
||
|
||
// Config used for constructing the hash; this config doesn't have
|
||
// Checks populated, because we always run all checks.
|
||
//
|
||
// This even works for users who add custom checks, because we include the binary's hash.
|
||
hashCfg := a.cfg
|
||
hashCfg.Checks = nil
|
||
// note that we don't hash staticcheck's version; it is set as the
|
||
// salt by a package main.
|
||
fmt.Fprintf(h, "cfg %#v\n", hashCfg)
|
||
fmt.Fprintf(h, "pkg %x\n", a.Package.Hash)
|
||
fmt.Fprintf(h, "analyzers %s\n", r.analyzerNames)
|
||
fmt.Fprintf(h, "go %s\n", r.actualGoVersion)
|
||
|
||
// OPT(dh): do we actually need to hash vetx? can we not assume
|
||
// that for identical inputs, staticcheck will produce identical
|
||
// vetx?
|
||
for _, dep := range a.deps {
|
||
dep := dep.(*packageAction)
|
||
vetxHash, err := cache.FileHash(dep.vetx)
|
||
if err != nil {
|
||
return fmt.Errorf("failed computing hash: %w", err)
|
||
}
|
||
fmt.Fprintf(h, "vetout %q %x\n", dep.Package.PkgPath, vetxHash)
|
||
}
|
||
a.hash = cache.ActionID(h.Sum())
|
||
|
||
// try to fetch hashed data
|
||
ids := make([]cache.ActionID, 0, 2)
|
||
ids = append(ids, cache.Subkey(a.hash, "vetx"))
|
||
if !a.factsOnly {
|
||
ids = append(ids, cache.Subkey(a.hash, "results"))
|
||
if r.TestMode {
|
||
ids = append(ids, cache.Subkey(a.hash, "testdata"))
|
||
}
|
||
}
|
||
if err := getCachedFiles(r.cache, ids, []*string{&a.vetx, &a.results, &a.testData}); err != nil {
|
||
result, err := r.doUncached(a)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
if a.failed {
|
||
return nil
|
||
}
|
||
|
||
a.skipped = result.skipped
|
||
|
||
// OPT(dh) instead of collecting all object facts and encoding
|
||
// them after analysis finishes, we could encode them as we
|
||
// go. however, that would require some locking.
|
||
//
|
||
// OPT(dh): We could sort gobFacts for more consistent output,
|
||
// but it doesn't matter. The hash of a package includes all
|
||
// of its files, so whether the vetx hash changes or not, a
|
||
// change to a package requires re-analyzing all dependents,
|
||
// even if the vetx data stayed the same. See also the note at
|
||
// the top of loader/hash.go.
|
||
|
||
tf := &bytes.Buffer{}
|
||
enc := gob.NewEncoder(tf)
|
||
for _, gf := range result.facts {
|
||
if err := enc.Encode(gf); err != nil {
|
||
return fmt.Errorf("failed gob encoding data: %w", err)
|
||
}
|
||
}
|
||
|
||
a.vetx, err = r.writeCacheReader(a, "vetx", bytes.NewReader(tf.Bytes()))
|
||
if err != nil {
|
||
return err
|
||
}
|
||
|
||
if a.factsOnly {
|
||
return nil
|
||
}
|
||
|
||
var out ResultData
|
||
out.Directives = make([]SerializedDirective, len(result.dirs))
|
||
for i, dir := range result.dirs {
|
||
out.Directives[i] = serializeDirective(dir, result.lpkg.Fset)
|
||
}
|
||
|
||
out.Diagnostics = result.diags
|
||
out.Unused = result.unused
|
||
a.results, err = r.writeCacheGob(a, "results", out)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
|
||
if r.TestMode {
|
||
out := TestData{
|
||
Wants: result.wants,
|
||
Facts: result.testFacts,
|
||
}
|
||
a.testData, err = r.writeCacheGob(a, "testdata", out)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
}
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// ActiveWorkers returns the number of currently running workers.
|
||
func (r *Runner) ActiveWorkers() int {
|
||
return r.semaphore.Len()
|
||
}
|
||
|
||
// TotalWorkers returns the maximum number of possible workers.
|
||
func (r *Runner) TotalWorkers() int {
|
||
return r.semaphore.Cap()
|
||
}
|
||
|
||
func (r *Runner) writeCacheReader(a *packageAction, kind string, rs io.ReadSeeker) (string, error) {
|
||
h := cache.Subkey(a.hash, kind)
|
||
out, _, err := r.cache.Put(h, rs)
|
||
if err != nil {
|
||
return "", fmt.Errorf("failed caching data: %w", err)
|
||
}
|
||
return r.cache.OutputFile(out), nil
|
||
}
|
||
|
||
func (r *Runner) writeCacheGob(a *packageAction, kind string, data interface{}) (string, error) {
|
||
f, err := ioutil.TempFile("", "staticcheck")
|
||
if err != nil {
|
||
return "", err
|
||
}
|
||
defer f.Close()
|
||
os.Remove(f.Name())
|
||
if err := gob.NewEncoder(f).Encode(data); err != nil {
|
||
return "", fmt.Errorf("failed gob encoding data: %w", err)
|
||
}
|
||
if _, err := f.Seek(0, io.SeekStart); err != nil {
|
||
return "", err
|
||
}
|
||
return r.writeCacheReader(a, kind, f)
|
||
}
|
||
|
||
type packageActionResult struct {
|
||
facts []gobFact
|
||
diags []Diagnostic
|
||
unused unused.SerializedResult
|
||
dirs []lint.Directive
|
||
lpkg *loader.Package
|
||
skipped bool
|
||
|
||
// Only set when using test mode
|
||
testFacts []TestFact
|
||
wants []Want
|
||
}
|
||
|
||
func (r *subrunner) doUncached(a *packageAction) (packageActionResult, error) {
|
||
// OPT(dh): for a -> b; c -> b; if both a and b are being
|
||
// processed concurrently, we shouldn't load b's export data
|
||
// twice.
|
||
|
||
pkg, _, err := loader.Load(a.Package)
|
||
if err != nil {
|
||
return packageActionResult{}, err
|
||
}
|
||
|
||
if len(pkg.Errors) > 0 {
|
||
// this handles errors that occurred during type-checking the
|
||
// package in loader.Load
|
||
for _, err := range pkg.Errors {
|
||
a.errors = append(a.errors, err)
|
||
}
|
||
a.failed = true
|
||
return packageActionResult{}, nil
|
||
}
|
||
|
||
if len(pkg.Syntax) == 0 && pkg.PkgPath != "unsafe" {
|
||
return packageActionResult{lpkg: pkg, skipped: true}, nil
|
||
}
|
||
|
||
// OPT(dh): instead of parsing directives twice (twice because
|
||
// U1000 depends on the facts.Directives analyzer), reuse the
|
||
// existing result
|
||
var dirs []lint.Directive
|
||
if !a.factsOnly {
|
||
dirs = lint.ParseDirectives(pkg.Syntax, pkg.Fset)
|
||
}
|
||
res, err := r.runAnalyzers(a, pkg)
|
||
|
||
var wants []Want
|
||
if r.TestMode {
|
||
// Extract 'want' comments from parsed Go files.
|
||
for _, f := range pkg.Syntax {
|
||
for _, cgroup := range f.Comments {
|
||
for _, c := range cgroup.List {
|
||
|
||
text := strings.TrimPrefix(c.Text, "//")
|
||
if text == c.Text { // not a //-comment.
|
||
text = strings.TrimPrefix(text, "/*")
|
||
text = strings.TrimSuffix(text, "*/")
|
||
}
|
||
|
||
// Hack: treat a comment of the form "//...// want..."
|
||
// or "/*...// want... */
|
||
// as if it starts at 'want'.
|
||
// This allows us to add comments on comments,
|
||
// as required when testing the buildtag analyzer.
|
||
if i := strings.Index(text, "// want"); i >= 0 {
|
||
text = text[i+len("// "):]
|
||
}
|
||
|
||
posn := pkg.Fset.Position(c.Pos())
|
||
wants = append(wants, Want{Position: posn, Comment: text})
|
||
}
|
||
}
|
||
}
|
||
|
||
// TODO(dh): add support for non-Go files
|
||
}
|
||
|
||
return packageActionResult{
|
||
facts: res.facts,
|
||
testFacts: res.testFacts,
|
||
wants: wants,
|
||
diags: res.diagnostics,
|
||
unused: res.unused,
|
||
dirs: dirs,
|
||
lpkg: pkg,
|
||
}, err
|
||
}
|
||
|
||
func pkgPaths(root *types.Package) map[string]*types.Package {
|
||
out := map[string]*types.Package{}
|
||
var dfs func(*types.Package)
|
||
dfs = func(pkg *types.Package) {
|
||
if _, ok := out[pkg.Path()]; ok {
|
||
return
|
||
}
|
||
out[pkg.Path()] = pkg
|
||
for _, imp := range pkg.Imports() {
|
||
dfs(imp)
|
||
}
|
||
}
|
||
dfs(root)
|
||
return out
|
||
}
|
||
|
||
func (r *Runner) loadFacts(root *types.Package, dep *packageAction, objFacts map[objectFactKey]objectFact, pkgFacts map[packageFactKey]analysis.Fact) error {
|
||
// Load facts of all imported packages
|
||
vetx, err := os.Open(dep.vetx)
|
||
if err != nil {
|
||
return fmt.Errorf("failed loading cached facts: %w", err)
|
||
}
|
||
defer vetx.Close()
|
||
|
||
pathToPkg := pkgPaths(root)
|
||
dec := gob.NewDecoder(vetx)
|
||
for {
|
||
var gf gobFact
|
||
err := dec.Decode(&gf)
|
||
if err != nil {
|
||
if err == io.EOF {
|
||
break
|
||
}
|
||
return fmt.Errorf("failed loading cached facts: %w", err)
|
||
}
|
||
|
||
pkg, ok := pathToPkg[gf.PkgPath]
|
||
if !ok {
|
||
continue
|
||
}
|
||
if gf.ObjPath == "" {
|
||
pkgFacts[packageFactKey{
|
||
Pkg: pkg,
|
||
Type: reflect.TypeOf(gf.Fact),
|
||
}] = gf.Fact
|
||
} else {
|
||
obj, err := objectpath.Object(pkg, objectpath.Path(gf.ObjPath))
|
||
if err != nil {
|
||
continue
|
||
}
|
||
objFacts[objectFactKey{
|
||
Obj: obj,
|
||
Type: reflect.TypeOf(gf.Fact),
|
||
}] = objectFact{gf.Fact, objectpath.Path(gf.ObjPath)}
|
||
}
|
||
}
|
||
return nil
|
||
}
|
||
|
||
func genericHandle(a action, root action, queue chan action, sem *tsync.Semaphore, exec func(a action) error) {
|
||
if a == root {
|
||
close(queue)
|
||
if sem != nil {
|
||
sem.Release()
|
||
}
|
||
return
|
||
}
|
||
if !a.IsFailed() {
|
||
// the action may have already been marked as failed during
|
||
// construction of the action graph, for example because of
|
||
// unresolved imports.
|
||
|
||
for _, dep := range a.Deps() {
|
||
if dep.IsFailed() {
|
||
// One of our dependencies failed, so mark this package as
|
||
// failed and bail. We don't need to record an error for
|
||
// this package, the relevant error will have been
|
||
// reported by the first package in the chain that failed.
|
||
a.MarkFailed()
|
||
break
|
||
}
|
||
}
|
||
}
|
||
|
||
if !a.IsFailed() {
|
||
if err := exec(a); err != nil {
|
||
a.MarkFailed()
|
||
a.AddError(err)
|
||
}
|
||
}
|
||
if sem != nil {
|
||
sem.Release()
|
||
}
|
||
|
||
for _, t := range a.Triggers() {
|
||
if t.DecrementPending() {
|
||
queue <- t
|
||
}
|
||
}
|
||
}
|
||
|
||
type analyzerRunner struct {
|
||
pkg *loader.Package
|
||
// object facts of our dependencies; may contain facts of
|
||
// analyzers other than the current one
|
||
depObjFacts map[objectFactKey]objectFact
|
||
// package facts of our dependencies; may contain facts of
|
||
// analyzers other than the current one
|
||
depPkgFacts map[packageFactKey]analysis.Fact
|
||
factsOnly bool
|
||
|
||
stats *Stats
|
||
}
|
||
|
||
func (ar *analyzerRunner) do(act action) error {
|
||
a := act.(*analyzerAction)
|
||
results := map[*analysis.Analyzer]interface{}{}
|
||
// TODO(dh): does this have to be recursive?
|
||
for _, dep := range a.deps {
|
||
dep := dep.(*analyzerAction)
|
||
results[dep.Analyzer] = dep.Result
|
||
}
|
||
// OPT(dh): cache factTypes, it is the same for all packages for a given analyzer
|
||
//
|
||
// OPT(dh): do we need the factTypes map? most analyzers have 0-1
|
||
// fact types. iterating over the slice is probably faster than
|
||
// indexing a map.
|
||
factTypes := map[reflect.Type]struct{}{}
|
||
for _, typ := range a.Analyzer.FactTypes {
|
||
factTypes[reflect.TypeOf(typ)] = struct{}{}
|
||
}
|
||
filterFactType := func(typ reflect.Type) bool {
|
||
_, ok := factTypes[typ]
|
||
return ok
|
||
}
|
||
a.Pass = &analysis.Pass{
|
||
Analyzer: a.Analyzer,
|
||
Fset: ar.pkg.Fset,
|
||
Files: ar.pkg.Syntax,
|
||
OtherFiles: ar.pkg.OtherFiles,
|
||
Pkg: ar.pkg.Types,
|
||
TypesInfo: ar.pkg.TypesInfo,
|
||
TypesSizes: ar.pkg.TypesSizes,
|
||
Report: func(diag analysis.Diagnostic) {
|
||
if !ar.factsOnly {
|
||
if diag.Category == "" {
|
||
diag.Category = a.Analyzer.Name
|
||
}
|
||
d := Diagnostic{
|
||
Position: report.DisplayPosition(ar.pkg.Fset, diag.Pos),
|
||
End: report.DisplayPosition(ar.pkg.Fset, diag.End),
|
||
Category: diag.Category,
|
||
Message: diag.Message,
|
||
}
|
||
for _, sugg := range diag.SuggestedFixes {
|
||
s := SuggestedFix{
|
||
Message: sugg.Message,
|
||
}
|
||
for _, edit := range sugg.TextEdits {
|
||
s.TextEdits = append(s.TextEdits, TextEdit{
|
||
Position: report.DisplayPosition(ar.pkg.Fset, edit.Pos),
|
||
End: report.DisplayPosition(ar.pkg.Fset, edit.End),
|
||
NewText: edit.NewText,
|
||
})
|
||
}
|
||
d.SuggestedFixes = append(d.SuggestedFixes, s)
|
||
}
|
||
for _, rel := range diag.Related {
|
||
d.Related = append(d.Related, RelatedInformation{
|
||
Position: report.DisplayPosition(ar.pkg.Fset, rel.Pos),
|
||
End: report.DisplayPosition(ar.pkg.Fset, rel.End),
|
||
Message: rel.Message,
|
||
})
|
||
}
|
||
a.Diagnostics = append(a.Diagnostics, d)
|
||
}
|
||
},
|
||
ResultOf: results,
|
||
ImportObjectFact: func(obj types.Object, fact analysis.Fact) bool {
|
||
key := objectFactKey{
|
||
Obj: obj,
|
||
Type: reflect.TypeOf(fact),
|
||
}
|
||
if f, ok := ar.depObjFacts[key]; ok {
|
||
reflect.ValueOf(fact).Elem().Set(reflect.ValueOf(f.fact).Elem())
|
||
return true
|
||
} else if f, ok := a.ObjectFacts[key]; ok {
|
||
reflect.ValueOf(fact).Elem().Set(reflect.ValueOf(f.fact).Elem())
|
||
return true
|
||
}
|
||
return false
|
||
},
|
||
ImportPackageFact: func(pkg *types.Package, fact analysis.Fact) bool {
|
||
key := packageFactKey{
|
||
Pkg: pkg,
|
||
Type: reflect.TypeOf(fact),
|
||
}
|
||
if f, ok := ar.depPkgFacts[key]; ok {
|
||
reflect.ValueOf(fact).Elem().Set(reflect.ValueOf(f).Elem())
|
||
return true
|
||
} else if f, ok := a.PackageFacts[key]; ok {
|
||
reflect.ValueOf(fact).Elem().Set(reflect.ValueOf(f).Elem())
|
||
return true
|
||
}
|
||
return false
|
||
},
|
||
ExportObjectFact: func(obj types.Object, fact analysis.Fact) {
|
||
key := objectFactKey{
|
||
Obj: obj,
|
||
Type: reflect.TypeOf(fact),
|
||
}
|
||
path, _ := objectpath.For(obj)
|
||
a.ObjectFacts[key] = objectFact{fact, path}
|
||
},
|
||
ExportPackageFact: func(fact analysis.Fact) {
|
||
key := packageFactKey{
|
||
Pkg: ar.pkg.Types,
|
||
Type: reflect.TypeOf(fact),
|
||
}
|
||
a.PackageFacts[key] = fact
|
||
},
|
||
AllPackageFacts: func() []analysis.PackageFact {
|
||
out := make([]analysis.PackageFact, 0, len(ar.depPkgFacts)+len(a.PackageFacts))
|
||
for key, fact := range ar.depPkgFacts {
|
||
out = append(out, analysis.PackageFact{
|
||
Package: key.Pkg,
|
||
Fact: fact,
|
||
})
|
||
}
|
||
for key, fact := range a.PackageFacts {
|
||
out = append(out, analysis.PackageFact{
|
||
Package: key.Pkg,
|
||
Fact: fact,
|
||
})
|
||
}
|
||
return out
|
||
},
|
||
AllObjectFacts: func() []analysis.ObjectFact {
|
||
out := make([]analysis.ObjectFact, 0, len(ar.depObjFacts)+len(a.ObjectFacts))
|
||
for key, fact := range ar.depObjFacts {
|
||
if filterFactType(key.Type) {
|
||
out = append(out, analysis.ObjectFact{
|
||
Object: key.Obj,
|
||
Fact: fact.fact,
|
||
})
|
||
}
|
||
}
|
||
for key, fact := range a.ObjectFacts {
|
||
if filterFactType(key.Type) {
|
||
out = append(out, analysis.ObjectFact{
|
||
Object: key.Obj,
|
||
Fact: fact.fact,
|
||
})
|
||
}
|
||
}
|
||
return out
|
||
},
|
||
}
|
||
|
||
t := time.Now()
|
||
res, err := a.Analyzer.Run(a.Pass)
|
||
ar.stats.measureAnalyzer(a.Analyzer, ar.pkg.PackageSpec, time.Since(t))
|
||
if err != nil {
|
||
return err
|
||
}
|
||
a.Result = res
|
||
return nil
|
||
}
|
||
|
||
type analysisResult struct {
|
||
facts []gobFact
|
||
diagnostics []Diagnostic
|
||
unused unused.SerializedResult
|
||
|
||
// Only set when using test mode
|
||
testFacts []TestFact
|
||
}
|
||
|
||
func (r *subrunner) runAnalyzers(pkgAct *packageAction, pkg *loader.Package) (analysisResult, error) {
|
||
depObjFacts := map[objectFactKey]objectFact{}
|
||
depPkgFacts := map[packageFactKey]analysis.Fact{}
|
||
|
||
for _, dep := range pkgAct.deps {
|
||
if err := r.loadFacts(pkg.Types, dep.(*packageAction), depObjFacts, depPkgFacts); err != nil {
|
||
return analysisResult{}, err
|
||
}
|
||
}
|
||
|
||
root := &analyzerAction{}
|
||
var analyzers []*analysis.Analyzer
|
||
if pkgAct.factsOnly {
|
||
// When analyzing non-initial packages, we only care about
|
||
// analyzers that produce facts.
|
||
analyzers = r.factAnalyzers
|
||
} else {
|
||
analyzers = r.analyzers
|
||
}
|
||
|
||
all := map[*analysis.Analyzer]*analyzerAction{}
|
||
for _, a := range analyzers {
|
||
a := newAnalyzerAction(a, all)
|
||
root.deps = append(root.deps, a)
|
||
a.triggers = append(a.triggers, root)
|
||
}
|
||
root.pending = uint32(len(root.deps))
|
||
|
||
ar := &analyzerRunner{
|
||
pkg: pkg,
|
||
factsOnly: pkgAct.factsOnly,
|
||
depObjFacts: depObjFacts,
|
||
depPkgFacts: depPkgFacts,
|
||
stats: &r.Stats,
|
||
}
|
||
queue := make(chan action, len(all))
|
||
for _, a := range all {
|
||
if len(a.Deps()) == 0 {
|
||
queue <- a
|
||
}
|
||
}
|
||
|
||
// Don't hang if there are no analyzers to run; for example
|
||
// because we are analyzing a dependency but have no analyzers
|
||
// that produce facts.
|
||
if len(all) == 0 {
|
||
close(queue)
|
||
}
|
||
for item := range queue {
|
||
b := r.semaphore.AcquireMaybe()
|
||
if b {
|
||
go genericHandle(item, root, queue, &r.semaphore, ar.do)
|
||
} else {
|
||
// the semaphore is exhausted; run the analysis under the
|
||
// token we've acquired for analyzing the package.
|
||
genericHandle(item, root, queue, nil, ar.do)
|
||
}
|
||
}
|
||
|
||
var unusedResult unused.SerializedResult
|
||
for _, a := range all {
|
||
if a != root && a.Analyzer.Name == "U1000" && !a.failed {
|
||
// TODO(dh): figure out a clean abstraction, instead of
|
||
// special-casing U1000.
|
||
unusedResult = unused.Serialize(a.Pass, a.Result.(unused.Result), pkg.Fset)
|
||
}
|
||
|
||
for key, fact := range a.ObjectFacts {
|
||
depObjFacts[key] = fact
|
||
}
|
||
for key, fact := range a.PackageFacts {
|
||
depPkgFacts[key] = fact
|
||
}
|
||
}
|
||
|
||
// OPT(dh): cull objects not reachable via the exported closure
|
||
var testFacts []TestFact
|
||
gobFacts := make([]gobFact, 0, len(depObjFacts)+len(depPkgFacts))
|
||
for key, fact := range depObjFacts {
|
||
if fact.path == "" {
|
||
continue
|
||
}
|
||
if sanityCheck {
|
||
p, _ := objectpath.For(key.Obj)
|
||
if p != fact.path {
|
||
panic(fmt.Sprintf("got different object paths for %v. old: %q new: %q", key.Obj, fact.path, p))
|
||
}
|
||
}
|
||
gf := gobFact{
|
||
PkgPath: key.Obj.Pkg().Path(),
|
||
ObjPath: string(fact.path),
|
||
Fact: fact.fact,
|
||
}
|
||
gobFacts = append(gobFacts, gf)
|
||
}
|
||
|
||
for key, fact := range depPkgFacts {
|
||
gf := gobFact{
|
||
PkgPath: key.Pkg.Path(),
|
||
Fact: fact,
|
||
}
|
||
gobFacts = append(gobFacts, gf)
|
||
}
|
||
|
||
if r.TestMode {
|
||
for _, a := range all {
|
||
for key, fact := range a.ObjectFacts {
|
||
tgf := TestFact{
|
||
ObjectName: key.Obj.Name(),
|
||
Position: pkg.Fset.Position(key.Obj.Pos()),
|
||
FactString: fmt.Sprint(fact.fact),
|
||
Analyzer: a.Analyzer.Name,
|
||
}
|
||
testFacts = append(testFacts, tgf)
|
||
}
|
||
|
||
for _, fact := range a.PackageFacts {
|
||
tgf := TestFact{
|
||
ObjectName: "",
|
||
Position: pkg.Fset.Position(pkg.Syntax[0].Pos()),
|
||
FactString: fmt.Sprint(fact),
|
||
Analyzer: a.Analyzer.Name,
|
||
}
|
||
testFacts = append(testFacts, tgf)
|
||
}
|
||
}
|
||
}
|
||
|
||
var diags []Diagnostic
|
||
for _, a := range root.deps {
|
||
a := a.(*analyzerAction)
|
||
diags = append(diags, a.Diagnostics...)
|
||
}
|
||
return analysisResult{
|
||
facts: gobFacts,
|
||
testFacts: testFacts,
|
||
diagnostics: diags,
|
||
unused: unusedResult,
|
||
}, nil
|
||
}
|
||
|
||
func registerGobTypes(analyzers []*analysis.Analyzer) {
|
||
for _, a := range analyzers {
|
||
for _, typ := range a.FactTypes {
|
||
// FIXME(dh): use RegisterName so we can work around collisions
|
||
// in names. For pointer-types, gob incorrectly qualifies
|
||
// type names with the package name, not the import path.
|
||
gob.Register(typ)
|
||
}
|
||
}
|
||
}
|
||
|
||
func allAnalyzers(analyzers []*analysis.Analyzer) []*analysis.Analyzer {
|
||
seen := map[*analysis.Analyzer]struct{}{}
|
||
out := make([]*analysis.Analyzer, 0, len(analyzers))
|
||
var dfs func(*analysis.Analyzer)
|
||
dfs = func(a *analysis.Analyzer) {
|
||
if _, ok := seen[a]; ok {
|
||
return
|
||
}
|
||
seen[a] = struct{}{}
|
||
out = append(out, a)
|
||
for _, dep := range a.Requires {
|
||
dfs(dep)
|
||
}
|
||
}
|
||
for _, a := range analyzers {
|
||
dfs(a)
|
||
}
|
||
return out
|
||
}
|
||
|
||
// Run loads the packages specified by patterns, runs analyzers on
|
||
// them and returns the results. Each result corresponds to a single
|
||
// package. Results will be returned for all packages, including
|
||
// dependencies. Errors specific to packages will be reported in the
|
||
// respective results.
|
||
//
|
||
// If cfg is nil, a default config will be used. Otherwise, cfg will
|
||
// be used, with the exception of the Mode field.
|
||
func (r *Runner) Run(cfg *packages.Config, analyzers []*analysis.Analyzer, patterns []string) ([]Result, error) {
|
||
analyzers = allAnalyzers(analyzers)
|
||
registerGobTypes(analyzers)
|
||
|
||
r.Stats.setState(StateLoadPackageGraph)
|
||
lpkgs, err := loader.Graph(r.cache, cfg, patterns...)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
r.Stats.setInitialPackages(len(lpkgs))
|
||
|
||
if len(lpkgs) == 0 {
|
||
return nil, nil
|
||
}
|
||
|
||
var goVersion string
|
||
if r.GoVersion == "module" {
|
||
for _, lpkg := range lpkgs {
|
||
if m := lpkg.Module; m != nil {
|
||
if goVersion == "" {
|
||
goVersion = m.GoVersion
|
||
} else if goVersion != m.GoVersion {
|
||
// Theoretically, we should only ever see a single Go
|
||
// module. At least that's currently (as of Go 1.15)
|
||
// true when using 'go list'.
|
||
fmt.Fprintln(os.Stderr, "warning: encountered multiple modules and could not deduce targeted Go version")
|
||
goVersion = ""
|
||
break
|
||
}
|
||
}
|
||
}
|
||
} else {
|
||
goVersion = r.GoVersion
|
||
}
|
||
|
||
if goVersion == "" {
|
||
if r.FallbackGoVersion == "" {
|
||
panic("could not determine Go version of module, and fallback version hasn't been set")
|
||
}
|
||
goVersion = r.FallbackGoVersion
|
||
}
|
||
r.actualGoVersion = goVersion
|
||
for _, a := range analyzers {
|
||
flag := a.Flags.Lookup("go")
|
||
if flag == nil {
|
||
continue
|
||
}
|
||
if err := flag.Value.Set(goVersion); err != nil {
|
||
return nil, err
|
||
}
|
||
}
|
||
|
||
r.Stats.setState(StateBuildActionGraph)
|
||
all := map[*loader.PackageSpec]*packageAction{}
|
||
root := &packageAction{}
|
||
for _, lpkg := range lpkgs {
|
||
a := newPackageActionRoot(lpkg, all)
|
||
root.deps = append(root.deps, a)
|
||
a.triggers = append(a.triggers, root)
|
||
}
|
||
root.pending = uint32(len(root.deps))
|
||
|
||
queue := make(chan action)
|
||
r.Stats.setTotalPackages(len(all) - 1)
|
||
|
||
r.Stats.setState(StateProcessing)
|
||
go func() {
|
||
for _, a := range all {
|
||
if len(a.Deps()) == 0 {
|
||
queue <- a
|
||
}
|
||
}
|
||
}()
|
||
|
||
sr := newSubrunner(r, analyzers)
|
||
for item := range queue {
|
||
r.semaphore.Acquire()
|
||
go genericHandle(item, root, queue, &r.semaphore, func(act action) error {
|
||
return sr.do(act)
|
||
})
|
||
}
|
||
|
||
r.Stats.setState(StateFinalizing)
|
||
out := make([]Result, 0, len(all))
|
||
for _, item := range all {
|
||
if item.Package == nil {
|
||
continue
|
||
}
|
||
out = append(out, Result{
|
||
Package: item.Package,
|
||
Config: item.cfg,
|
||
Initial: !item.factsOnly,
|
||
Skipped: item.skipped,
|
||
Failed: item.failed,
|
||
Errors: item.errors,
|
||
results: item.results,
|
||
testData: item.testData,
|
||
})
|
||
}
|
||
return out, nil
|
||
}
|