mirror of
https://github.com/hanzoai/visor.git
synced 2026-09-24 17:04:14 +00:00
This is a bug in my previous change. std can be split across multiple root prefixes. FilterStdPackages expects to see the complete merged set. PiperOrigin-RevId: 885193601
932 lines
27 KiB
Go
932 lines
27 KiB
Go
// Copyright 2019 The gVisor Authors.
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
// Package check implements binary analysis similar to bazel's nogo, or the
|
|
// unitchecker package. It exists in order to provide additional facilities for
|
|
// analysis, namely plumbing through the output from dumping the generated
|
|
// binary (to analyze actual produced code).
|
|
package check
|
|
|
|
import (
|
|
"fmt"
|
|
"go/ast"
|
|
"go/build"
|
|
"go/parser"
|
|
"go/token"
|
|
"go/types"
|
|
"io"
|
|
"os"
|
|
"path"
|
|
"path/filepath"
|
|
"reflect"
|
|
"regexp"
|
|
"runtime/debug"
|
|
"slices"
|
|
"strings"
|
|
"sync"
|
|
|
|
"golang.org/x/tools/go/analysis"
|
|
"golang.org/x/tools/go/gcexportdata"
|
|
"gvisor.dev/gvisor/runsc/flag"
|
|
"gvisor.dev/gvisor/tools/nogo/facts"
|
|
"gvisor.dev/gvisor/tools/nogo/flags"
|
|
)
|
|
|
|
var (
|
|
// showTimes indicates we should show analyzer times.
|
|
showTimes = flag.Bool("show_times", false, "show all analyzer times")
|
|
)
|
|
|
|
var (
|
|
tagsOnce sync.Once
|
|
buildTags []string
|
|
releaseTagsVal []string
|
|
releaseTagsErr error
|
|
)
|
|
|
|
// Hack! factFacts only provides facts loaded from directly imported packages
|
|
// for efficiency (see importer.cache). In general, if you need a fact from a
|
|
// package that isn't otherwise imported, the expectation is that you will add
|
|
// a dummy import/use of the desired package to ensure it is a dependency.
|
|
//
|
|
// Unfortunately, some packages need facts from internal packages. Since
|
|
// internal packages cannot be imported we explicitly import in this tool to
|
|
// ensure the facts are available to ImportPackageFact.
|
|
var internalPackages = []string{
|
|
// Required by pkg/sync for internal/abi.MapType.
|
|
"internal/abi",
|
|
}
|
|
|
|
// shouldInclude indicates whether the file should be included.
|
|
func shouldInclude(path string) (bool, error) {
|
|
tagsOnce.Do(func() {
|
|
if len(flags.BuildTags) > 0 {
|
|
buildTags = strings.Split(flags.BuildTags, ",")
|
|
}
|
|
releaseTagsVal, releaseTagsErr = releaseTags()
|
|
})
|
|
if releaseTagsErr != nil {
|
|
return false, releaseTagsErr
|
|
}
|
|
// Users should set GOEXPERIMENT, GOARM64, GOAMD64, etc in the
|
|
// environment as necessary to ensure build.Default picks the correct
|
|
// values.
|
|
//
|
|
// TODO(mpratt): remove the GOOS and GOARCH flags? Those can also come
|
|
// from the environment.
|
|
ctx := build.Default
|
|
ctx.GOOS = flags.GOOS
|
|
ctx.GOARCH = flags.GOARCH
|
|
ctx.BuildTags = buildTags
|
|
ctx.ReleaseTags = releaseTagsVal
|
|
// Force disable cgo, as we don't have the Bazel infrastructure to
|
|
// analyze cgo dependencies.
|
|
ctx.CgoEnabled = false
|
|
// Remove the boringcrypto build tag, as it uses cgo. CgoEnabled =
|
|
// false should be sufficient to exclude the cgo files, but it doesn't
|
|
// seem to be.
|
|
toolTags := slices.Clone(ctx.ToolTags)
|
|
toolTags = slices.DeleteFunc(toolTags, func(t string) bool { return t == "goexperiment.boringcrypto" })
|
|
ctx.ToolTags = toolTags
|
|
return ctx.MatchFile(filepath.Dir(path), filepath.Base(path))
|
|
}
|
|
|
|
// sortSrcs sorts a set of src files into Go files and non-Go files.
|
|
func sortSrcs(srcs []string) (goFiles []string, nonGoFiles []string) {
|
|
for _, filename := range srcs {
|
|
if strings.HasSuffix(filename, ".go") {
|
|
goFiles = append(goFiles, filename)
|
|
} else {
|
|
nonGoFiles = append(nonGoFiles, filename)
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|
|
// importerEntry is a single entry in the importer.
|
|
type importerEntry struct {
|
|
ready sync.WaitGroup
|
|
pkg *types.Package
|
|
findings FindingSet
|
|
err error
|
|
factsMu sync.Mutex
|
|
facts *facts.Package
|
|
}
|
|
|
|
// importer is an almost-implementation of go/types.Importer.
|
|
//
|
|
// This wraps a configuration, which provides the map of package names to
|
|
// files, and the facts. Note that this importer implementation will always
|
|
// pass when a given package is not available.
|
|
type importer struct {
|
|
fset *token.FileSet
|
|
|
|
// sources is a map from package path to the source file paths in that
|
|
// package.
|
|
//
|
|
// N.B. key is the package path (including GOROOT vendor/ prefix if any),
|
|
// not the import path (what appears in the import declaration).
|
|
sources map[string][]string
|
|
|
|
// roots is the set of potential GOROOT prefixes on the file paths in sources.
|
|
// GOROOT is effectively a merged version of these roots.
|
|
roots []string
|
|
|
|
analyzers map[*analysis.Analyzer]analyzer
|
|
|
|
// mu protects cache & bundles (see below).
|
|
mu sync.Mutex
|
|
cache map[string]*importerEntry // key is package path (see sources above).
|
|
|
|
// bundles is protected by mu, but once set is immutable.
|
|
bundles []*facts.Bundle
|
|
|
|
// importsMu protects imports.
|
|
importsMu sync.Mutex
|
|
imports map[string]*types.Package // key is package path (see sources above).
|
|
}
|
|
|
|
// loadBundles loads all bundle files.
|
|
//
|
|
// This should only be called from loadFacts, below. After calling this
|
|
// function, i.bundles may be read freely without holding a lock.
|
|
func (i *importer) loadBundles() error {
|
|
i.mu.Lock()
|
|
defer i.mu.Unlock()
|
|
|
|
// Are bundles already available?
|
|
if i.bundles != nil {
|
|
return nil
|
|
}
|
|
|
|
// Scan all bundle files.
|
|
for _, filename := range flags.Bundles {
|
|
// Open the given filename as a bundle.
|
|
loadedFacts, err := facts.BundleFrom(filename)
|
|
if err != nil {
|
|
return fmt.Errorf("error loading bundled facts: %w", err)
|
|
}
|
|
|
|
// Add to the set of available bundles.
|
|
i.bundles = append(i.bundles, loadedFacts)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// loadFacts returns all package facts for the given name.
|
|
//
|
|
// This should be called only from importPackage, as this may deserialize a
|
|
// facts file (which is an expensive operation). Callers should generally rely
|
|
// on fastFacts to access facts for packages that have already been imported.
|
|
func (i *importer) loadFacts(pkg *types.Package) (*facts.Package, error) {
|
|
// Attempt to load from the fact map.
|
|
filename, ok := flags.FactMap[pkg.Path()]
|
|
if ok {
|
|
r, openErr := os.Open(filename)
|
|
if openErr != nil {
|
|
return nil, fmt.Errorf("error loading facts from %q: %w", filename, openErr)
|
|
}
|
|
defer r.Close()
|
|
loadedFacts := facts.NewPackage()
|
|
if readErr := loadedFacts.ReadFrom(pkg, r); readErr != nil {
|
|
return nil, fmt.Errorf("error loading facts: %w", readErr)
|
|
}
|
|
return loadedFacts, nil
|
|
}
|
|
|
|
// Attempt to load any bundles.
|
|
if err := i.loadBundles(); err != nil {
|
|
return nil, fmt.Errorf("error loading bundles: %w", err)
|
|
}
|
|
|
|
// Try to import from the bundle.
|
|
for _, bundleFacts := range i.bundles {
|
|
localFacts, err := bundleFacts.Package(pkg)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error loading from a bundle: %w", err)
|
|
}
|
|
if localFacts != nil {
|
|
return localFacts, nil
|
|
}
|
|
}
|
|
|
|
// Nothing available for this package?
|
|
return nil, nil
|
|
}
|
|
|
|
// fastFacts returns facts for the given package.
|
|
//
|
|
// This relies exclusively on loaded packages, as the parameter is
|
|
// *types.Package and therefore the package data must already be available.
|
|
func (i *importer) fastFacts(pkg *types.Package) *facts.Package {
|
|
i.mu.Lock()
|
|
e, ok := i.cache[pkg.Path()]
|
|
i.mu.Unlock()
|
|
if !ok {
|
|
return nil
|
|
}
|
|
|
|
e.factsMu.Lock()
|
|
defer e.factsMu.Unlock()
|
|
|
|
// Do we have them already?
|
|
if e.facts != nil {
|
|
return e.facts
|
|
}
|
|
|
|
// Load the facts.
|
|
facts, err := i.loadFacts(pkg)
|
|
if err != nil {
|
|
// There are no facts available, but no good way to propagate
|
|
// this minor error. It may be intentional that no analysis was
|
|
// performed on some part of the standard library, for example.
|
|
return nil
|
|
}
|
|
e.facts = facts // Cache the result.
|
|
return facts
|
|
}
|
|
|
|
// findArchive finds the archive for the given package.
|
|
func (i *importer) findArchive(path string) (rc io.ReadCloser, err error) {
|
|
realPath, ok := flags.ArchiveMap[path]
|
|
if !ok {
|
|
return i.findBinary(path)
|
|
}
|
|
return os.Open(realPath)
|
|
}
|
|
|
|
// findBinary finds the binary for the given package.
|
|
func (i *importer) findBinary(path string) (rc io.ReadCloser, err error) {
|
|
realPath, ok := flags.ImportMap[path]
|
|
if !ok {
|
|
// Not found in the import path. Attempt to find the package
|
|
// via the standard library.
|
|
rc, err = findStdPkg(path)
|
|
} else {
|
|
// Open the file.
|
|
rc, err = os.Open(realPath)
|
|
}
|
|
return rc, err
|
|
}
|
|
|
|
// importPackage almost-implements types.ImporterFrom.ImportFrom.
|
|
//
|
|
// This must be called by other methods directly.
|
|
func (i *importer) importPackage(importPath, dir string) (*types.Package, error) {
|
|
if importPath == "unsafe" {
|
|
// Special case: go/types has pre-defined type information for
|
|
// unsafe. We ensure that this package is correct, in case any
|
|
// analyzers are specifically looking for this.
|
|
return types.Unsafe, nil
|
|
}
|
|
|
|
pkgPath := importPath
|
|
if dir != "" && len(i.roots) > 0 {
|
|
// Vendor directories get first chance to satisfy import.
|
|
//
|
|
// N.B. The goal here is to support vendored packages in the standard
|
|
// library (i.roots non-empty), which has a single top-level
|
|
// vendor directory. We may not handle more complex vendor
|
|
// situations correctly.
|
|
// Check if a package path is available without actually loading it.
|
|
pkgExists := func(pkgPath string) bool {
|
|
// 1. In cache?
|
|
i.mu.Lock()
|
|
_, ok := i.cache[pkgPath]
|
|
i.mu.Unlock()
|
|
if ok {
|
|
return true
|
|
}
|
|
|
|
// 2. Source available?
|
|
if _, ok := i.sources[pkgPath]; ok {
|
|
return true
|
|
}
|
|
|
|
// 3. Binary available?
|
|
rc, err := i.findBinary(pkgPath)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
// TODO(mpratt): Add a more efficient option that doesn't
|
|
// actually open the binary if this gets too expensive.
|
|
rc.Close()
|
|
return true
|
|
}
|
|
|
|
// Determine which root we are in.
|
|
var root string
|
|
var dirPath string
|
|
for _, cand := range i.roots {
|
|
var ok bool
|
|
dirPath, ok = strings.CutPrefix(dir, cand)
|
|
if ok {
|
|
root = cand
|
|
break
|
|
}
|
|
}
|
|
if root == "" {
|
|
return nil, fmt.Errorf("importPackage dir %q outside of known roots %s", dir, i.roots)
|
|
}
|
|
|
|
for {
|
|
candPkgPath := path.Join(dirPath, "vendor", importPath)
|
|
if pkgExists(candPkgPath) {
|
|
// Package is vendored. Continue import below
|
|
// using the full vendor path.
|
|
pkgPath = candPkgPath
|
|
break
|
|
}
|
|
|
|
if dirPath == "" {
|
|
// Already at root. Nothing else to search.
|
|
// Package is not vendored.
|
|
break
|
|
}
|
|
|
|
// Walk up one directory.
|
|
i := strings.LastIndex(dirPath, "/")
|
|
if i >= 0 {
|
|
dirPath = dirPath[:i]
|
|
} else {
|
|
dirPath = ""
|
|
}
|
|
}
|
|
}
|
|
|
|
// Pull the internal entry.
|
|
i.mu.Lock()
|
|
entry, ok := i.cache[pkgPath]
|
|
if ok && entry.pkg != nil {
|
|
i.mu.Unlock()
|
|
entry.ready.Wait()
|
|
return entry.pkg, entry.err
|
|
}
|
|
|
|
// Start preparing this entry.
|
|
entry = new(importerEntry)
|
|
entry.ready.Add(1)
|
|
defer entry.ready.Done()
|
|
i.cache[pkgPath] = entry
|
|
i.mu.Unlock()
|
|
|
|
// If we have the srcs for this package, then we can actually do an
|
|
// analysis from first principles to validate the package and derive
|
|
// the types. We strictly prefer this to the gcexportdata.
|
|
if srcs, ok := i.sources[pkgPath]; ok && len(srcs) > 0 {
|
|
entry.pkg, entry.findings, entry.facts, entry.err = i.checkPackage(pkgPath, srcs)
|
|
if entry.err != nil {
|
|
return nil, entry.err
|
|
}
|
|
i.importsMu.Lock()
|
|
defer i.importsMu.Unlock()
|
|
i.imports[pkgPath] = entry.pkg
|
|
return entry.pkg, entry.err
|
|
}
|
|
|
|
// Load all exported data. Unfortunately, we will have to hold the lock
|
|
// during this time. The imported may access imports directly.
|
|
rc, err := i.findBinary(pkgPath)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rc.Close()
|
|
r, err := gcexportdata.NewReader(rc)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
i.importsMu.Lock()
|
|
defer i.importsMu.Unlock()
|
|
entry.pkg, entry.err = gcexportdata.Read(r, i.fset, i.imports, pkgPath)
|
|
return entry.pkg, entry.err
|
|
}
|
|
|
|
// Import implements types.Importer.Import.
|
|
func (i *importer) Import(path string) (*types.Package, error) {
|
|
return i.ImportFrom(path, "", 0)
|
|
}
|
|
|
|
// ImportFrom implements types.ImporterFrom.ImportFrom.
|
|
func (i *importer) ImportFrom(path, dir string, mode types.ImportMode) (*types.Package, error) {
|
|
return i.importPackage(path, dir)
|
|
}
|
|
|
|
// errorImporter tracks the last error.
|
|
type errorImporter struct {
|
|
*importer
|
|
lastErr error
|
|
}
|
|
|
|
// Import implements types.Importer.Import.
|
|
func (i *errorImporter) Import(path string) (*types.Package, error) {
|
|
return i.ImportFrom(path, "", 0)
|
|
}
|
|
|
|
// ImportFrom implements types.ImporterFrom.ImportFrom.
|
|
func (i *errorImporter) ImportFrom(path, dir string, mode types.ImportMode) (*types.Package, error) {
|
|
pkg, err := i.importer.importPackage(path, dir)
|
|
if err != nil {
|
|
i.lastErr = err
|
|
}
|
|
return pkg, err
|
|
}
|
|
|
|
// checkPackage is the backing implementation for CheckPackage and others.
|
|
//
|
|
// The implementation was adapted from [1], which was in turn adapted from [2].
|
|
// This returns a list of matching analysis issues, or an error if the analysis
|
|
// could not be completed.
|
|
//
|
|
// Note that a partial result may be returned if an error occurred on at least
|
|
// one analyzer. This may be expected if e.g. a binary is not provided but a
|
|
// binaryAnalyzer is used.
|
|
//
|
|
// [1] bazelbuid/rules_go/tools/builders/nogo_main.go
|
|
// [2] golang.org/x/tools/go/checker/internal/checker
|
|
func (i *importer) checkPackage(path string, srcs []string) (*types.Package, FindingSet, *facts.Package, error) {
|
|
// Load all source files.
|
|
goFiles, nonGoFiles := sortSrcs(srcs)
|
|
syntax := make([]*ast.File, 0, len(goFiles))
|
|
for _, file := range goFiles {
|
|
include, err := shouldInclude(file)
|
|
if err != nil {
|
|
return nil, nil, nil, fmt.Errorf("error evaluating file %q: %w", file, err)
|
|
}
|
|
if !include {
|
|
continue
|
|
}
|
|
s, err := parser.ParseFile(i.fset, file, nil, parser.ParseComments)
|
|
if err != nil {
|
|
return nil, nil, nil, fmt.Errorf("error parsing file %q: %w", file, err)
|
|
}
|
|
syntax = append(syntax, s)
|
|
}
|
|
otherFiles := make([]string, 0, len(nonGoFiles))
|
|
for _, file := range nonGoFiles {
|
|
include, err := shouldInclude(file)
|
|
if err != nil {
|
|
return nil, nil, nil, fmt.Errorf("error evaluating non-Go file %q: %w", file, err)
|
|
}
|
|
if !include {
|
|
continue
|
|
}
|
|
otherFiles = append(otherFiles, file)
|
|
}
|
|
|
|
// Check type information.
|
|
ei := &errorImporter{
|
|
importer: i,
|
|
}
|
|
typesSizes := types.SizesFor("gc", flags.GOARCH)
|
|
typeConfig := types.Config{
|
|
Importer: ei,
|
|
Error: func(error) {},
|
|
GoVersion: flags.GOVERSION,
|
|
}
|
|
typesInfo := &types.Info{
|
|
Types: make(map[ast.Expr]types.TypeAndValue),
|
|
Instances: make(map[*ast.Ident]types.Instance),
|
|
Uses: make(map[*ast.Ident]types.Object),
|
|
Defs: make(map[*ast.Ident]types.Object),
|
|
Implicits: make(map[ast.Node]types.Object),
|
|
Scopes: make(map[ast.Node]*types.Scope),
|
|
Selections: make(map[*ast.SelectorExpr]*types.Selection),
|
|
FileVersions: make(map[*ast.File]string),
|
|
}
|
|
astPackage, err := typeConfig.Check(path, i.fset, syntax, typesInfo)
|
|
if err != nil {
|
|
return nil, nil, nil, fmt.Errorf("error checking types: %w", err)
|
|
}
|
|
|
|
// Note that facts should be reconcilable between types as of go/tools
|
|
// commit ee04797aa0b6be5ce3d5f7ac0f91e34716b3acdf. We previously used
|
|
// to do a sanity check to ensure that binary import data was
|
|
// compatible with ast-derived data, but this is no longer necessary.
|
|
// If packages are available locally, we can refer to those directly.
|
|
astFacts := facts.NewPackage()
|
|
|
|
// Recursively visit all analyzers.
|
|
var (
|
|
resultsMu sync.RWMutex // protects results & errs, findings.
|
|
factsMu sync.RWMutex // protects facts.
|
|
ready = make(map[*analysis.Analyzer]*sync.WaitGroup)
|
|
results = make(map[*analysis.Analyzer]any)
|
|
errs = make(map[*analysis.Analyzer]error)
|
|
findings = make(FindingSet, 0)
|
|
)
|
|
for a := range i.analyzers {
|
|
wg := new(sync.WaitGroup)
|
|
wg.Add(1) // For analysis.
|
|
ready[a] = wg
|
|
}
|
|
limit := make(chan struct{}, 1)
|
|
for a, wg := range ready {
|
|
go func(a *analysis.Analyzer, wg *sync.WaitGroup) {
|
|
defer wg.Done()
|
|
|
|
// Wait for all requirements.
|
|
for _, orig := range a.Requires {
|
|
ready[orig].Wait()
|
|
|
|
// Should we bail early?
|
|
resultsMu.RLock()
|
|
if err := errs[orig]; err != nil {
|
|
resultsMu.RUnlock()
|
|
resultsMu.Lock()
|
|
defer resultsMu.Unlock()
|
|
errs[a] = err
|
|
return
|
|
}
|
|
resultsMu.RUnlock()
|
|
}
|
|
|
|
limit <- struct{}{}
|
|
defer func() { <-limit }()
|
|
|
|
// Collect local fact types.
|
|
localFactTypes := make(map[reflect.Type]bool)
|
|
for _, ft := range a.FactTypes {
|
|
localFactTypes[reflect.TypeOf(ft)] = true
|
|
}
|
|
|
|
// Run the analysis.
|
|
var localFindings FindingSet
|
|
p := &analysis.Pass{
|
|
Analyzer: a,
|
|
Fset: i.fset,
|
|
Files: syntax,
|
|
OtherFiles: otherFiles,
|
|
Pkg: astPackage,
|
|
TypesInfo: typesInfo,
|
|
ResultOf: results, // All results.
|
|
Report: func(d analysis.Diagnostic) {
|
|
localFindings = append(localFindings, Finding{
|
|
Category: a.Name,
|
|
Position: i.fset.Position(d.Pos),
|
|
Message: d.Message,
|
|
GOOS: flags.GOOS,
|
|
GOARCH: flags.GOARCH,
|
|
})
|
|
},
|
|
ImportPackageFact: func(pkg *types.Package, ptr analysis.Fact) bool {
|
|
if pkg != astPackage {
|
|
if f := i.fastFacts(pkg); f != nil {
|
|
return f.ImportFact(nil, ptr)
|
|
}
|
|
return false
|
|
}
|
|
factsMu.RLock()
|
|
defer factsMu.RUnlock()
|
|
return astFacts.ImportFact(nil, ptr)
|
|
},
|
|
ExportPackageFact: func(fact analysis.Fact) {
|
|
factsMu.Lock()
|
|
defer factsMu.Unlock()
|
|
astFacts.ExportFact(nil, fact)
|
|
},
|
|
ImportObjectFact: func(obj types.Object, ptr analysis.Fact) bool {
|
|
if pkg := obj.Pkg(); pkg != nil && pkg != astPackage {
|
|
if f := i.fastFacts(pkg); f != nil {
|
|
return f.ImportFact(obj, ptr)
|
|
}
|
|
return false
|
|
}
|
|
factsMu.RLock()
|
|
defer factsMu.RUnlock()
|
|
return astFacts.ImportFact(obj, ptr)
|
|
},
|
|
ExportObjectFact: func(obj types.Object, fact analysis.Fact) {
|
|
if obj == nil {
|
|
// Tried to export nil object?
|
|
return
|
|
}
|
|
if obj.Pkg() != astPackage {
|
|
// This is not allowed: the
|
|
// built-in facts library will
|
|
// also panic in this case.
|
|
return
|
|
}
|
|
factsMu.Lock()
|
|
defer factsMu.Unlock()
|
|
astFacts.ExportFact(obj, fact)
|
|
},
|
|
AllPackageFacts: func() (rv []analysis.PackageFact) {
|
|
factsMu.RLock()
|
|
defer factsMu.RUnlock()
|
|
// Pull all dependencies.
|
|
for _, importedPkg := range astPackage.Imports() {
|
|
otherFacts := i.fastFacts(importedPkg)
|
|
if otherFacts == nil {
|
|
continue
|
|
}
|
|
for typ := range localFactTypes {
|
|
v := reflect.New(typ.Elem())
|
|
if otherFacts.ImportFact(nil, v.Interface().(analysis.Fact)) {
|
|
rv = append(rv, analysis.PackageFact{
|
|
Package: importedPkg,
|
|
Fact: v.Interface().(analysis.Fact),
|
|
})
|
|
}
|
|
}
|
|
}
|
|
// Pull all local facts.
|
|
for typ := range localFactTypes {
|
|
v := reflect.New(typ.Elem())
|
|
if astFacts.ImportFact(nil, v.Interface().(analysis.Fact)) {
|
|
rv = append(rv, analysis.PackageFact{
|
|
Package: astPackage,
|
|
Fact: v.Interface().(analysis.Fact),
|
|
})
|
|
}
|
|
}
|
|
return
|
|
},
|
|
AllObjectFacts: func() (rv []analysis.ObjectFact) {
|
|
factsMu.RLock()
|
|
defer factsMu.RUnlock()
|
|
// Pull all local facts.
|
|
for obj := range astFacts.Objects {
|
|
for typ := range localFactTypes {
|
|
v := reflect.New(typ.Elem())
|
|
if astFacts.ImportFact(obj, v.Interface().(analysis.Fact)) {
|
|
rv = append(rv, analysis.ObjectFact{
|
|
Object: obj,
|
|
Fact: v.Interface().(analysis.Fact),
|
|
})
|
|
}
|
|
}
|
|
}
|
|
return
|
|
},
|
|
TypesSizes: typesSizes,
|
|
}
|
|
|
|
// Ensure any analyzer panics are captured. This may
|
|
// happen for packages that are not supported by
|
|
// specific analyzers. The only panic that can happen
|
|
// is while resultsMu is held as a read-only lock.
|
|
var (
|
|
result any
|
|
err error
|
|
)
|
|
defer func() {
|
|
if r := recover(); r != nil {
|
|
// In order to make the multiple
|
|
// analyzers running concurrently
|
|
// debuggable, capture panic exceptions
|
|
// and propagate as an analyzer error.
|
|
err = fmt.Errorf("panic recovered: %s (%s)", r, debug.Stack())
|
|
resultsMu.RUnlock() // +checklocksignore
|
|
}
|
|
resultsMu.Lock()
|
|
findings = append(findings, localFindings...)
|
|
results[a] = result
|
|
errs[a] = err
|
|
resultsMu.Unlock()
|
|
}()
|
|
found := findAnalyzer(i.analyzers, a)
|
|
resultsMu.RLock()
|
|
if ba, ok := found.(binaryAnalyzer); ok {
|
|
// Load the binary and analyze.
|
|
rc, loadErr := i.findArchive(path)
|
|
if loadErr != nil {
|
|
err = loadErr
|
|
} else {
|
|
result, err = ba.Run(p, rc)
|
|
rc.Close()
|
|
}
|
|
} else {
|
|
result, err = a.Run(p)
|
|
}
|
|
resultsMu.RUnlock()
|
|
}(a, wg)
|
|
}
|
|
for _, wg := range ready {
|
|
// Wait for completion.
|
|
wg.Wait()
|
|
}
|
|
for a := range ready {
|
|
// Check the error. If we generate an error here, we report
|
|
// this as a finding that can be suppressed. Some analyzers
|
|
// will fail on some packages.
|
|
if errs[a] != nil {
|
|
filename := ""
|
|
if len(srcs) > 0 {
|
|
filename = srcs[0]
|
|
}
|
|
findings = append(findings, Finding{
|
|
Category: a.Name,
|
|
Position: token.Position{Filename: filename},
|
|
Message: errs[a].Error(),
|
|
GOOS: flags.GOOS,
|
|
GOARCH: flags.GOARCH,
|
|
})
|
|
continue
|
|
}
|
|
|
|
// Check the result. Per above, we check that the type is what
|
|
// we expected and that an error did not occur during analysis.
|
|
if got, want := reflect.TypeOf(results[a]), a.ResultType; got != want {
|
|
return astPackage, findings, astFacts, fmt.Errorf("error: analyzer %s returned %v (expected type %v)", a.Name, results[a], want)
|
|
}
|
|
}
|
|
|
|
// Return all findings.
|
|
return astPackage, findings, astFacts, nil
|
|
}
|
|
|
|
func (i *importer) hardImportPackages(pkgs []string) error {
|
|
for _, pkg := range internalPackages {
|
|
_, err := i.Import(pkg)
|
|
if err != nil {
|
|
return fmt.Errorf("error importing %s: %w", pkg, err)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Package runs all analyzer on a single package.
|
|
func Package(path string, srcs []string) (FindingSet, facts.Serializer, error) {
|
|
i := &importer{
|
|
fset: token.NewFileSet(),
|
|
cache: make(map[string]*importerEntry),
|
|
imports: make(map[string]*types.Package),
|
|
analyzers: allAnalyzers,
|
|
}
|
|
|
|
// See comment on internalPackages.
|
|
if err := i.hardImportPackages(internalPackages); err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
_, findings, facts, err := i.checkPackage(path, srcs)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
return findings, facts, nil
|
|
}
|
|
|
|
// allFactsAndFindings returns all factsAndFindings from an importer.
|
|
func (i *importer) allFactsAndFindings() (FindingSet, *facts.Bundle) {
|
|
var (
|
|
findings = make(FindingSet, 0)
|
|
allFacts = facts.NewBundle()
|
|
)
|
|
for path, entry := range i.cache {
|
|
findings = append(findings, entry.findings...)
|
|
if entry.facts != nil {
|
|
allFacts.Add(path, entry.facts)
|
|
}
|
|
}
|
|
return findings, allFacts
|
|
}
|
|
|
|
// TemplateFuncs runs all analyzers necessary for template generation, and
|
|
// returns a funcmap for template rendering.
|
|
//
|
|
// Look at facts.ResolveFuncs for a list of supported functions.
|
|
func TemplateFuncs(path string, srcs []string) (map[string]any, any, error) {
|
|
i := &importer{
|
|
fset: token.NewFileSet(),
|
|
cache: make(map[string]*importerEntry),
|
|
imports: make(map[string]*types.Package),
|
|
analyzers: renderAnalyzers,
|
|
}
|
|
|
|
// See comment on internalPackages.
|
|
if err := i.hardImportPackages(internalPackages); err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
pkg, _, localFacts, err := i.checkPackage(path, srcs)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
_, allFacts := i.allFactsAndFindings()
|
|
funcMap, err := facts.ResolveFuncs(pkg, localFacts, allFacts)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
return funcMap, pkg, nil
|
|
}
|
|
|
|
// FindRoots finds a package roots.
|
|
func FindRoots(srcs []string, srcRootRegex string) ([]string, error) {
|
|
if srcRootRegex == "" {
|
|
return nil, nil
|
|
}
|
|
|
|
// Calculate the root source directories. This is always a directory
|
|
// named 'src'. It's possible that there are more than one of these
|
|
// directories.
|
|
//
|
|
// Note that there may be extra files outside of the root source
|
|
// directory; we simply ignore those.
|
|
re, err := regexp.Compile(srcRootRegex)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("srcRootRegex is not valid: %w", err)
|
|
}
|
|
var srcRootPrefixes []string
|
|
for _, filename := range srcs {
|
|
if s := re.FindString(filename); s != "" && !slices.Contains(srcRootPrefixes, s) {
|
|
srcRootPrefixes = append(srcRootPrefixes, s)
|
|
}
|
|
}
|
|
if len(srcRootPrefixes) == 0 {
|
|
// For whatever reason, we didn't identify a good common prefix to use here.
|
|
return nil, fmt.Errorf("unable to identify src prefix for %v with regex %s", srcs, srcRootRegex)
|
|
}
|
|
return srcRootPrefixes, nil
|
|
}
|
|
|
|
// SplitStdPackages splits a std GOROOT package structure into packages.
|
|
func SplitStdPackages(srcs []string, srcRootPrefix string) (map[string][]string, error) {
|
|
sources := make(map[string][]string)
|
|
for _, filename := range srcs {
|
|
if !strings.HasPrefix(filename, srcRootPrefix) {
|
|
continue // Superflouous file.
|
|
}
|
|
d := path.Dir(filename)
|
|
if len(srcRootPrefix) >= len(d) {
|
|
continue // Not a file.
|
|
}
|
|
pkg := d[len(srcRootPrefix):]
|
|
for len(pkg) > 0 && pkg[0] == '/' {
|
|
pkg = pkg[1:]
|
|
}
|
|
if len(pkg) == 0 {
|
|
continue // Also not a file.
|
|
}
|
|
|
|
// Ignore any files with /testdata/ in the path.
|
|
if strings.Contains(filename, "/testdata/") {
|
|
continue
|
|
}
|
|
|
|
// Ignore all test files since they *may* be in a different
|
|
// package than the rest of the sources.
|
|
if strings.HasSuffix(filename, "_test.go") {
|
|
continue
|
|
}
|
|
|
|
// Skip the "builtin" package, which is only for docs and not a
|
|
// real package. Attempting type checking goes crazy.
|
|
if pkg == "builtin" {
|
|
continue
|
|
}
|
|
|
|
if strings.Contains(filename, "cmd/compile/internal/typecheck/_builtin/runtime.go") {
|
|
// Place the special runtime package (functions emitted by the
|
|
// compiler itself) into the runtime packages.
|
|
pkg = "runtime"
|
|
} else if strings.HasPrefix(pkg, "cmd") {
|
|
// Ignore packages in cmd, these are packages that are needed to build Go
|
|
// programs (like the Go tool or the compiler), but they are not part of
|
|
// the standard library and can't be in the set of transitive dependencies.
|
|
continue
|
|
}
|
|
|
|
// Add to the package.
|
|
sources[pkg] = append(sources[pkg], filename)
|
|
}
|
|
|
|
return sources, nil
|
|
}
|
|
|
|
// Bundle checks a bundle of files (typically the standard library).
|
|
//
|
|
// roots is the set of potential GOROOT prefixes on the file paths in sources.
|
|
// GOROOT is effectively a merged version of these roots.
|
|
func Bundle(sources map[string][]string, roots []string) (FindingSet, facts.Serializer, error) {
|
|
// Process all packages.
|
|
i := &importer{
|
|
fset: token.NewFileSet(),
|
|
sources: sources,
|
|
roots: roots,
|
|
cache: make(map[string]*importerEntry),
|
|
imports: make(map[string]*types.Package),
|
|
analyzers: allAnalyzers,
|
|
}
|
|
for pkg := range sources {
|
|
// Was there an error processing this package?
|
|
if _, err := i.importPackage(pkg, ""); err != nil {
|
|
return nil, nil, err
|
|
}
|
|
}
|
|
|
|
findings, facts := i.allFactsAndFindings()
|
|
return findings, facts, nil
|
|
}
|