diff options
Diffstat (limited to 'tools/nogo')
-rw-r--r-- | tools/nogo/BUILD | 74 | ||||
-rw-r--r-- | tools/nogo/README.md | 31 | ||||
-rw-r--r-- | tools/nogo/analyzers.go | 131 | ||||
-rw-r--r-- | tools/nogo/build.go | 30 | ||||
-rw-r--r-- | tools/nogo/check/BUILD | 11 | ||||
-rw-r--r-- | tools/nogo/check/main.go | 114 | ||||
-rw-r--r-- | tools/nogo/config.go | 261 | ||||
-rw-r--r-- | tools/nogo/defs.bzl | 428 | ||||
-rw-r--r-- | tools/nogo/filter/BUILD | 14 | ||||
-rw-r--r-- | tools/nogo/filter/main.go | 131 | ||||
-rw-r--r-- | tools/nogo/findings.go | 63 | ||||
-rw-r--r-- | tools/nogo/nogo.go | 473 |
12 files changed, 0 insertions, 1761 deletions
diff --git a/tools/nogo/BUILD b/tools/nogo/BUILD deleted file mode 100644 index 12b8b597c..000000000 --- a/tools/nogo/BUILD +++ /dev/null @@ -1,74 +0,0 @@ -load("//tools:defs.bzl", "bzl_library", "go_library", "select_goarch", "select_goos") -load("//tools/nogo:defs.bzl", "nogo_objdump_tool", "nogo_stdlib", "nogo_target") - -package(licenses = ["notice"]) - -nogo_target( - name = "target", - goarch = select_goarch(), - goos = select_goos(), - visibility = ["//visibility:public"], -) - -nogo_objdump_tool( - name = "objdump_tool", - visibility = ["//visibility:public"], -) - -nogo_stdlib( - name = "stdlib", - visibility = ["//visibility:public"], -) - -go_library( - name = "nogo", - srcs = [ - "analyzers.go", - "build.go", - "config.go", - "findings.go", - "nogo.go", - ], - nogo = False, - visibility = ["//:sandbox"], - deps = [ - "//tools/checkescape", - "//tools/checkunsafe", - "@co_honnef_go_tools//staticcheck:go_default_library", - "@co_honnef_go_tools//stylecheck:go_default_library", - "@org_golang_x_tools//go/analysis:go_tool_library", - "@org_golang_x_tools//go/analysis/internal/facts:go_tool_library", - "@org_golang_x_tools//go/analysis/passes/asmdecl:go_tool_library", - "@org_golang_x_tools//go/analysis/passes/assign:go_tool_library", - "@org_golang_x_tools//go/analysis/passes/atomic:go_tool_library", - "@org_golang_x_tools//go/analysis/passes/bools:go_tool_library", - "@org_golang_x_tools//go/analysis/passes/buildtag:go_tool_library", - "@org_golang_x_tools//go/analysis/passes/cgocall:go_tool_library", - "@org_golang_x_tools//go/analysis/passes/composite:go_tool_library", - "@org_golang_x_tools//go/analysis/passes/copylock:go_tool_library", - "@org_golang_x_tools//go/analysis/passes/errorsas:go_tool_library", - "@org_golang_x_tools//go/analysis/passes/httpresponse:go_tool_library", - "@org_golang_x_tools//go/analysis/passes/loopclosure:go_tool_library", - "@org_golang_x_tools//go/analysis/passes/lostcancel:go_tool_library", - "@org_golang_x_tools//go/analysis/passes/nilfunc:go_tool_library", - "@org_golang_x_tools//go/analysis/passes/nilness:go_tool_library", - "@org_golang_x_tools//go/analysis/passes/printf:go_tool_library", - "@org_golang_x_tools//go/analysis/passes/shadow:go_tool_library", - "@org_golang_x_tools//go/analysis/passes/shift:go_tool_library", - "@org_golang_x_tools//go/analysis/passes/stdmethods:go_tool_library", - "@org_golang_x_tools//go/analysis/passes/stringintconv:go_tool_library", - "@org_golang_x_tools//go/analysis/passes/structtag:go_tool_library", - "@org_golang_x_tools//go/analysis/passes/tests:go_tool_library", - "@org_golang_x_tools//go/analysis/passes/unmarshal:go_tool_library", - "@org_golang_x_tools//go/analysis/passes/unreachable:go_tool_library", - "@org_golang_x_tools//go/analysis/passes/unsafeptr:go_tool_library", - "@org_golang_x_tools//go/analysis/passes/unusedresult:go_tool_library", - "@org_golang_x_tools//go/gcexportdata:go_tool_library", - ], -) - -bzl_library( - name = "defs_bzl", - srcs = ["defs.bzl"], - visibility = ["//visibility:private"], -) diff --git a/tools/nogo/README.md b/tools/nogo/README.md deleted file mode 100644 index 6e4db18de..000000000 --- a/tools/nogo/README.md +++ /dev/null @@ -1,31 +0,0 @@ -# Extended "nogo" analysis - -This package provides a build aspect that perform nogo analysis. This will be -automatically injected to all relevant libraries when using the default -`go_binary` and `go_library` rules. - -It exists for several reasons. - -* The default `nogo` provided by bazel is insufficient with respect to the - possibility of binary analysis. This package allows us to analyze the - generated binary in addition to using the standard analyzers. - -* The configuration provided in this package is much richer than the standard - `nogo` JSON blob. Specifically, it allows us to exclude specific structures - from the composite rules (such as the Ranges that are common with the set - types). - -* The bazel version of `nogo` is run directly against the `go_library` and - `go_binary` targets, meaning that any change to the configuration requires a - rebuild from scratch (for some reason included all C++ source files in the - process). Using an aspect is more efficient in this regard. - -* The checks supported by this package are exported as tests, which makes it - easier to reason about and plumb into the build system. - -* For uninteresting reasons, it is impossible to integrate the default `nogo` - analyzer provided by bazel with internal Google tooling. To provide a - consistent experience, this package allows those systems to be unified. - -To use this package, import `nogo_test` from `defs.bzl` and add a single -dependency which is a `go_binary` or `go_library` rule. diff --git a/tools/nogo/analyzers.go b/tools/nogo/analyzers.go deleted file mode 100644 index b919bc2f8..000000000 --- a/tools/nogo/analyzers.go +++ /dev/null @@ -1,131 +0,0 @@ -// 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 nogo - -import ( - "encoding/gob" - - "golang.org/x/tools/go/analysis" - "golang.org/x/tools/go/analysis/passes/asmdecl" - "golang.org/x/tools/go/analysis/passes/assign" - "golang.org/x/tools/go/analysis/passes/atomic" - "golang.org/x/tools/go/analysis/passes/bools" - "golang.org/x/tools/go/analysis/passes/buildtag" - "golang.org/x/tools/go/analysis/passes/cgocall" - "golang.org/x/tools/go/analysis/passes/composite" - "golang.org/x/tools/go/analysis/passes/copylock" - "golang.org/x/tools/go/analysis/passes/errorsas" - "golang.org/x/tools/go/analysis/passes/httpresponse" - "golang.org/x/tools/go/analysis/passes/loopclosure" - "golang.org/x/tools/go/analysis/passes/lostcancel" - "golang.org/x/tools/go/analysis/passes/nilfunc" - "golang.org/x/tools/go/analysis/passes/nilness" - "golang.org/x/tools/go/analysis/passes/printf" - "golang.org/x/tools/go/analysis/passes/shadow" - "golang.org/x/tools/go/analysis/passes/shift" - "golang.org/x/tools/go/analysis/passes/stdmethods" - "golang.org/x/tools/go/analysis/passes/stringintconv" - "golang.org/x/tools/go/analysis/passes/structtag" - "golang.org/x/tools/go/analysis/passes/tests" - "golang.org/x/tools/go/analysis/passes/unmarshal" - "golang.org/x/tools/go/analysis/passes/unreachable" - "golang.org/x/tools/go/analysis/passes/unsafeptr" - "golang.org/x/tools/go/analysis/passes/unusedresult" - "honnef.co/go/tools/staticcheck" - "honnef.co/go/tools/stylecheck" - - "gvisor.dev/gvisor/tools/checkescape" - "gvisor.dev/gvisor/tools/checkunsafe" -) - -// AllAnalyzers is a list of all available analyzers. -var AllAnalyzers = []*analysis.Analyzer{ - asmdecl.Analyzer, - assign.Analyzer, - atomic.Analyzer, - bools.Analyzer, - buildtag.Analyzer, - cgocall.Analyzer, - composite.Analyzer, - copylock.Analyzer, - errorsas.Analyzer, - httpresponse.Analyzer, - loopclosure.Analyzer, - lostcancel.Analyzer, - nilfunc.Analyzer, - nilness.Analyzer, - printf.Analyzer, - shift.Analyzer, - stdmethods.Analyzer, - stringintconv.Analyzer, - shadow.Analyzer, - structtag.Analyzer, - tests.Analyzer, - unmarshal.Analyzer, - unreachable.Analyzer, - unsafeptr.Analyzer, - unusedresult.Analyzer, - checkescape.Analyzer, - checkunsafe.Analyzer, -} - -// EscapeAnalyzers is a list of escape-related analyzers. -var EscapeAnalyzers = []*analysis.Analyzer{ - checkescape.EscapeAnalyzer, -} - -func register(all []*analysis.Analyzer) { - // Register all fact types. - // - // N.B. This needs to be done recursively, because there may be - // analyzers in the Requires list that do not appear explicitly above. - registered := make(map[*analysis.Analyzer]struct{}) - var registerOne func(*analysis.Analyzer) - registerOne = func(a *analysis.Analyzer) { - if _, ok := registered[a]; ok { - return - } - - // Register dependencies. - for _, da := range a.Requires { - registerOne(da) - } - - // Register local facts. - for _, f := range a.FactTypes { - gob.Register(f) - } - - registered[a] = struct{}{} // Done. - } - for _, a := range all { - registerOne(a) - } -} - -func init() { - // Add all staticcheck analyzers. - for _, a := range staticcheck.Analyzers { - AllAnalyzers = append(AllAnalyzers, a) - } - // Add all stylecheck analyzers. - for _, a := range stylecheck.Analyzers { - AllAnalyzers = append(AllAnalyzers, a) - } - - // Register lists. - register(AllAnalyzers) - register(EscapeAnalyzers) -} diff --git a/tools/nogo/build.go b/tools/nogo/build.go deleted file mode 100644 index d173cff1f..000000000 --- a/tools/nogo/build.go +++ /dev/null @@ -1,30 +0,0 @@ -// 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 nogo - -import ( - "fmt" - "io" - "os" -) - -// findStdPkg needs to find the bundled standard library packages. -func findStdPkg(GOOS, GOARCH, path string) (io.ReadCloser, error) { - if path == "C" { - // Cgo builds cannot be analyzed. Skip. - return nil, ErrSkip - } - return os.Open(fmt.Sprintf("external/go_sdk/pkg/%s_%s/%s.a", GOOS, GOARCH, path)) -} diff --git a/tools/nogo/check/BUILD b/tools/nogo/check/BUILD deleted file mode 100644 index e18483a18..000000000 --- a/tools/nogo/check/BUILD +++ /dev/null @@ -1,11 +0,0 @@ -load("//tools:defs.bzl", "go_binary") - -package(licenses = ["notice"]) - -go_binary( - name = "check", - srcs = ["main.go"], - nogo = False, - visibility = ["//visibility:public"], - deps = ["//tools/nogo"], -) diff --git a/tools/nogo/check/main.go b/tools/nogo/check/main.go deleted file mode 100644 index 69bdfe502..000000000 --- a/tools/nogo/check/main.go +++ /dev/null @@ -1,114 +0,0 @@ -// 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. - -// Binary check is the nogo entrypoint. -package main - -import ( - "encoding/json" - "flag" - "fmt" - "io/ioutil" - "log" - "os" - - "gvisor.dev/gvisor/tools/nogo" -) - -var ( - packageFile = flag.String("package", "", "package configuration file (in JSON format)") - stdlibFile = flag.String("stdlib", "", "stdlib configuration file (in JSON format)") - findingsOutput = flag.String("findings", "", "output file (or stdout, if not specified)") - factsOutput = flag.String("facts", "", "output file for facts (optional)") - escapesOutput = flag.String("escapes", "", "output file for escapes (optional)") -) - -func loadConfig(file string, config interface{}) interface{} { - // Load the configuration. - f, err := os.Open(file) - if err != nil { - log.Fatalf("unable to open configuration %q: %v", file, err) - } - defer f.Close() - dec := json.NewDecoder(f) - dec.DisallowUnknownFields() - if err := dec.Decode(config); err != nil { - log.Fatalf("unable to decode configuration: %v", err) - } - return config -} - -func main() { - // Parse all flags. - flag.Parse() - - var ( - findings []nogo.Finding - factData []byte - err error - ) - - // Check & load the configuration. - if *packageFile != "" && *stdlibFile != "" { - log.Fatalf("unable to perform stdlib and package analysis; provide only one!") - } - - // Run the configuration. - if *stdlibFile != "" { - // Perform basic analysis. - c := loadConfig(*stdlibFile, new(nogo.StdlibConfig)).(*nogo.StdlibConfig) - findings, factData, err = nogo.CheckStdlib(c, nogo.AllAnalyzers) - - } else if *packageFile != "" { - // Perform basic analysis. - c := loadConfig(*packageFile, new(nogo.PackageConfig)).(*nogo.PackageConfig) - findings, factData, err = nogo.CheckPackage(c, nogo.AllAnalyzers, nil) - - // Do we need to do escape analysis? - if *escapesOutput != "" { - escapes, _, err := nogo.CheckPackage(c, nogo.EscapeAnalyzers, nil) - if err != nil { - log.Fatalf("error performing escape analysis: %v", err) - } - if err := nogo.WriteFindingsToFile(escapes, *escapesOutput); err != nil { - log.Fatalf("error writing escapes to %q: %v", *escapesOutput, err) - } - } - } else { - log.Fatalf("please provide at least one of package or stdlib!") - } - - // Check that analysis was successful. - if err != nil { - log.Fatalf("error performing analysis: %v", err) - } - - // Save facts. - if *factsOutput != "" { - if err := ioutil.WriteFile(*factsOutput, factData, 0644); err != nil { - log.Fatalf("error saving findings to %q: %v", *factsOutput, err) - } - } - - // Write all findings. - if *findingsOutput != "" { - if err := nogo.WriteFindingsToFile(findings, *findingsOutput); err != nil { - log.Fatalf("error writing findings to %q: %v", *findingsOutput, err) - } - } else { - for _, finding := range findings { - fmt.Fprintf(os.Stdout, "%s\n", finding.String()) - } - } -} diff --git a/tools/nogo/config.go b/tools/nogo/config.go deleted file mode 100644 index 2fea5b3e1..000000000 --- a/tools/nogo/config.go +++ /dev/null @@ -1,261 +0,0 @@ -// 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 nogo - -import ( - "fmt" - "regexp" -) - -// GroupName is a named group. -type GroupName string - -// AnalyzerName is a named analyzer. -type AnalyzerName string - -// Group represents a named collection of files. -type Group struct { - // Name is the short name for the group. - Name GroupName `yaml:"name"` - - // Regex matches all full paths in the group. - Regex string `yaml:"regex"` - regex *regexp.Regexp `yaml:"-"` - - // Default determines the default group behavior. - // - // If Default is true, all Analyzers are enabled for this - // group. Otherwise, Analyzers must be individually enabled - // by specifying a (possible empty) ItemConfig for the group - // in the AnalyzerConfig. - Default bool `yaml:"default"` -} - -func (g *Group) compile() error { - r, err := regexp.Compile(g.Regex) - if err != nil { - return err - } - g.regex = r - return nil -} - -// ItemConfig is an (Analyzer,Group) configuration. -type ItemConfig struct { - // Exclude are analyzer exclusions. - // - // Exclude is a list of regular expressions. If the corresponding - // Analyzer emits a Finding for which Finding.Position.String() - // matches a regular expression in Exclude, the finding will not - // be reported. - Exclude []string `yaml:"exclude,omitempty"` - exclude []*regexp.Regexp `yaml:"-"` - - // Suppress are analyzer suppressions. - // - // Suppress is a list of regular expressions. If the corresponding - // Analyzer emits a Finding for which Finding.Message matches a regular - // expression in Suppress, the finding will not be reported. - Suppress []string `yaml:"suppress,omitempty"` - suppress []*regexp.Regexp `yaml:"-"` -} - -func compileRegexps(ss []string, rs *[]*regexp.Regexp) error { - *rs = make([]*regexp.Regexp, 0, len(ss)) - for _, s := range ss { - r, err := regexp.Compile(s) - if err != nil { - return err - } - *rs = append(*rs, r) - } - return nil -} - -func (i *ItemConfig) compile() error { - if i == nil { - // This may be nil if nothing is included in the - // item configuration. That's fine, there's nothing - // to compile and nothing to exclude & suppress. - return nil - } - if err := compileRegexps(i.Exclude, &i.exclude); err != nil { - return fmt.Errorf("in exclude: %w", err) - } - if err := compileRegexps(i.Suppress, &i.suppress); err != nil { - return fmt.Errorf("in suppress: %w", err) - } - return nil -} - -func (i *ItemConfig) merge(other *ItemConfig) { - i.Exclude = append(i.Exclude, other.Exclude...) - i.Suppress = append(i.Suppress, other.Suppress...) -} - -func (i *ItemConfig) shouldReport(fullPos, msg string) bool { - if i == nil { - // See above. - return true - } - for _, r := range i.exclude { - if r.MatchString(fullPos) { - return false - } - } - for _, r := range i.suppress { - if r.MatchString(msg) { - return false - } - } - return true -} - -// AnalyzerConfig is the configuration for a single analyzers. -// -// This map is keyed by individual Group names, to allow for different -// configurations depending on what Group the file belongs to. -type AnalyzerConfig map[GroupName]*ItemConfig - -func (a AnalyzerConfig) compile() error { - for name, gc := range a { - if err := gc.compile(); err != nil { - return fmt.Errorf("invalid group %q: %v", name, err) - } - } - return nil -} - -func (a AnalyzerConfig) merge(other AnalyzerConfig) { - // Merge all the groups. - for name, gc := range other { - old, ok := a[name] - if !ok || old == nil { - a[name] = gc // Not configured in a. - continue - } - old.merge(gc) - } -} - -func (a AnalyzerConfig) shouldReport(groupConfig *Group, fullPos, msg string) bool { - gc, ok := a[groupConfig.Name] - if !ok { - return groupConfig.Default - } - - // Note that if a section appears for a particular group - // for a particular analyzer, then it will now be enabled, - // and the group default no longer applies. - return gc.shouldReport(fullPos, msg) -} - -// Config is a nogo configuration. -type Config struct { - // Prefixes defines a set of regular expressions that - // are standard "prefixes", so that files can be grouped - // and specific rules applied to individual groups. - Groups []Group `yaml:"groups"` - - // Global is the global analyzer config. - Global AnalyzerConfig `yaml:"global"` - - // Analyzers are individual analyzer configurations. The - // key for each analyzer is the name of the analyzer. The - // value is either a boolean (enable/disable), or a map to - // the groups above. - Analyzers map[AnalyzerName]AnalyzerConfig `yaml:"analyzers"` -} - -// Merge merges two configurations. -func (c *Config) Merge(other *Config) { - // Merge all groups. - for _, g := range other.Groups { - // Is there a matching group? If yes, we just delete - // it. This will preserve the order provided in the - // overriding file, even if it differs. - for i := 0; i < len(c.Groups); i++ { - if g.Name == c.Groups[i].Name { - copy(c.Groups[i:], c.Groups[i+1:]) - c.Groups = c.Groups[:len(c.Groups)-1] - break - } - } - c.Groups = append(c.Groups, g) - } - - // Merge global configurations. - c.Global.merge(other.Global) - - // Merge all analyzer configurations. - for name, ac := range other.Analyzers { - old, ok := c.Analyzers[name] - if !ok { - c.Analyzers[name] = ac // No analyzer in original config. - continue - } - old.merge(ac) - } -} - -// Compile compiles a configuration to make it useable. -func (c *Config) Compile() error { - for i := 0; i < len(c.Groups); i++ { - if err := c.Groups[i].compile(); err != nil { - return fmt.Errorf("invalid group %q: %w", c.Groups[i].Name, err) - } - } - if err := c.Global.compile(); err != nil { - return fmt.Errorf("invalid global: %w", err) - } - for name, ac := range c.Analyzers { - if err := ac.compile(); err != nil { - return fmt.Errorf("invalid analyzer %q: %w", name, err) - } - } - return nil -} - -// ShouldReport returns true iff the finding should match the Config. -func (c *Config) ShouldReport(finding Finding) bool { - fullPos := finding.Position.String() - - // Find the matching group. - var groupConfig *Group - for i := 0; i < len(c.Groups); i++ { - if c.Groups[i].regex.MatchString(fullPos) { - groupConfig = &c.Groups[i] - break - } - } - - // If there is no group matching this path, then - // we default to accept the finding. - if groupConfig == nil { - return true - } - - // Suppress via global rule? - if !c.Global.shouldReport(groupConfig, fullPos, finding.Message) { - return false - } - - // Try the analyzer config. - ac, ok := c.Analyzers[finding.Category] - if !ok { - return groupConfig.Default - } - return ac.shouldReport(groupConfig, fullPos, finding.Message) -} diff --git a/tools/nogo/defs.bzl b/tools/nogo/defs.bzl deleted file mode 100644 index b3d297308..000000000 --- a/tools/nogo/defs.bzl +++ /dev/null @@ -1,428 +0,0 @@ -"""Nogo rules.""" - -load("//tools/bazeldefs:go.bzl", "go_context", "go_importpath", "go_rule", "go_test_library") - -NogoConfigInfo = provider( - "information about a nogo configuration", - fields = { - "srcs": "the collection of configuration files", - }, -) - -def _nogo_config_impl(ctx): - return [NogoConfigInfo( - srcs = ctx.files.srcs, - )] - -nogo_config = rule( - implementation = _nogo_config_impl, - attrs = { - "srcs": attr.label_list( - doc = "a list of yaml files (schema defined by tool/nogo/config.go).", - allow_files = True, - ), - }, -) - -NogoTargetInfo = provider( - "information about the Go target", - fields = { - "goarch": "the build architecture (GOARCH)", - "goos": "the build OS target (GOOS)", - }, -) - -def _nogo_target_impl(ctx): - return [NogoTargetInfo( - goarch = ctx.attr.goarch, - goos = ctx.attr.goos, - )] - -nogo_target = go_rule( - rule, - implementation = _nogo_target_impl, - attrs = { - "goarch": attr.string( - doc = "the Go build architecture (propagated to other rules).", - mandatory = True, - ), - "goos": attr.string( - doc = "the Go OS target (propagated to other rules).", - mandatory = True, - ), - }, -) - -def _nogo_objdump_tool_impl(ctx): - # Construct the magic dump command. - # - # Note that in some cases, the input is being fed into the tool via stdin. - # Unfortunately, the Go objdump tool expects to see a seekable file [1], so - # we need the tool to handle this case by creating a temporary file. - # - # [1] https://github.com/golang/go/issues/41051 - nogo_target_info = ctx.attr._nogo_target[NogoTargetInfo] - go_ctx = go_context(ctx, goos = nogo_target_info.goos, goarch = nogo_target_info.goarch) - env_prefix = " ".join(["%s=%s" % (key, value) for (key, value) in go_ctx.env.items()]) - dumper = ctx.actions.declare_file(ctx.label.name) - ctx.actions.write(dumper, "\n".join([ - "#!/bin/bash", - "set -euo pipefail", - "if [[ $# -eq 0 ]]; then", - " T=$(mktemp -u -t libXXXXXX.a)", - " cat /dev/stdin > ${T}", - "else", - " T=$1;", - "fi", - "%s %s tool objdump ${T}" % ( - env_prefix, - go_ctx.go.path, - ), - "if [[ $# -eq 0 ]]; then", - " rm -rf ${T}", - "fi", - "", - ]), is_executable = True) - - # Include the full runfiles. - return [DefaultInfo( - runfiles = ctx.runfiles(files = go_ctx.runfiles.to_list()), - executable = dumper, - )] - -nogo_objdump_tool = go_rule( - rule, - implementation = _nogo_objdump_tool_impl, - attrs = { - "_nogo_target": attr.label( - default = "//tools/nogo:target", - cfg = "target", - ), - }, -) - -# NogoStdlibInfo is the set of standard library facts. -NogoStdlibInfo = provider( - "information for nogo analysis (standard library facts)", - fields = { - "facts": "serialized standard library facts", - "raw_findings": "raw package findings (if relevant)", - }, -) - -def _nogo_stdlib_impl(ctx): - # Build the standard library facts. - nogo_target_info = ctx.attr._nogo_target[NogoTargetInfo] - go_ctx = go_context(ctx, goos = nogo_target_info.goos, goarch = nogo_target_info.goarch) - facts = ctx.actions.declare_file(ctx.label.name + ".facts") - raw_findings = ctx.actions.declare_file(ctx.label.name + ".raw_findings") - config = struct( - Srcs = [f.path for f in go_ctx.stdlib_srcs], - GOOS = go_ctx.goos, - GOARCH = go_ctx.goarch, - Tags = go_ctx.tags, - ) - config_file = ctx.actions.declare_file(ctx.label.name + ".cfg") - ctx.actions.write(config_file, config.to_json()) - ctx.actions.run( - inputs = [config_file] + go_ctx.stdlib_srcs, - outputs = [facts, raw_findings], - tools = depset(go_ctx.runfiles.to_list() + ctx.files._nogo_objdump_tool), - executable = ctx.files._nogo_check[0], - mnemonic = "NogoStandardLibraryAnalysis", - progress_message = "Analyzing Go Standard Library", - arguments = go_ctx.nogo_args + [ - "-objdump_tool=%s" % ctx.files._nogo_objdump_tool[0].path, - "-stdlib=%s" % config_file.path, - "-findings=%s" % raw_findings.path, - "-facts=%s" % facts.path, - ], - ) - - # Return the stdlib facts as output. - return [NogoStdlibInfo( - facts = facts, - raw_findings = raw_findings, - )] - -nogo_stdlib = go_rule( - rule, - implementation = _nogo_stdlib_impl, - attrs = { - "_nogo_check": attr.label( - default = "//tools/nogo/check:check", - cfg = "host", - ), - "_nogo_objdump_tool": attr.label( - default = "//tools/nogo:objdump_tool", - cfg = "host", - ), - "_nogo_target": attr.label( - default = "//tools/nogo:target", - cfg = "target", - ), - }, -) - -# NogoInfo is the serialized set of package facts for a nogo analysis. -# -# Each go_library rule will generate a corresponding nogo rule, which will run -# with the source files as input. Note however, that the individual nogo rules -# are simply stubs that enter into the shadow dependency tree (the "aspect"). -NogoInfo = provider( - "information for nogo analysis", - fields = { - "facts": "serialized package facts", - "raw_findings": "raw package findings (if relevant)", - "escapes": "escape-only findings (if relevant)", - "importpath": "package import path", - "binaries": "package binary files", - "srcs": "srcs (for go_test support)", - "deps": "deps (for go_test support)", - }, -) - -def _nogo_aspect_impl(target, ctx): - # If this is a nogo rule itself (and not the shadow of a go_library or - # go_binary rule created by such a rule), then we simply return nothing. - # All work is done in the shadow properties for go rules. For a proto - # library, we simply skip the analysis portion but still need to return a - # valid NogoInfo to reference the generated binary. - if ctx.rule.kind in ("go_library", "go_tool_library", "go_binary", "go_test"): - srcs = ctx.rule.files.srcs - deps = ctx.rule.attr.deps - elif ctx.rule.kind in ("go_proto_library", "go_wrap_cc"): - srcs = [] - deps = ctx.rule.attr.deps - else: - return [NogoInfo()] - - # If we're using the "library" attribute, then we need to aggregate the - # original library sources and dependencies into this target to perform - # proper type analysis. - if ctx.rule.kind == "go_test": - library = go_test_library(ctx.rule) - if library != None: - info = library[NogoInfo] - if hasattr(info, "srcs"): - srcs = srcs + info.srcs - if hasattr(info, "deps"): - deps = deps + info.deps - - # Start with all target files and srcs as input. - inputs = target.files.to_list() + srcs - - # Generate a shell script that dumps the binary. Annoyingly, this seems - # necessary as the context in which a run_shell command runs does not seem - # to cleanly allow us redirect stdout to the actual output file. Perhaps - # I'm missing something here, but the intermediate script does work. - binaries = target.files.to_list() - objfiles = [f for f in binaries if f.path.endswith(".a")] - if len(objfiles) > 0: - # Prefer the .a files for go_library targets. - target_objfile = objfiles[0] - else: - # Use the raw binary for go_binary and go_test targets. - target_objfile = binaries[0] - inputs.append(target_objfile) - - # Extract the importpath for this package. - if ctx.rule.kind == "go_test": - # If this is a test, then it will not be imported by anything else. - # We can safely set the importapth to just "test". Note that this - # is necessary if the library also imports the core library (in - # addition to including the sources directly), which happens in - # some complex cases (seccomp_victim). - importpath = "test" - else: - importpath = go_importpath(target) - - # Collect all info from shadow dependencies. - fact_map = dict() - import_map = dict() - all_raw_findings = [] - for dep in deps: - # There will be no file attribute set for all transitive dependencies - # that are not go_library or go_binary rules, such as a proto rules. - # This is handled by the ctx.rule.kind check above. - info = dep[NogoInfo] - if not hasattr(info, "facts"): - continue - - # Configure where to find the binary & fact files. Note that this will - # use .x and .a regardless of whether this is a go_binary rule, since - # these dependencies must be go_library rules. - x_files = [f.path for f in info.binaries if f.path.endswith(".x")] - if not len(x_files): - x_files = [f.path for f in info.binaries if f.path.endswith(".a")] - import_map[info.importpath] = x_files[0] - fact_map[info.importpath] = info.facts.path - - # Collect all findings; duplicates are resolved at the end. - all_raw_findings.extend(info.raw_findings) - - # Ensure the above are available as inputs. - inputs.append(info.facts) - inputs += info.binaries - - # Add the standard library facts. - stdlib_info = ctx.attr._nogo_stdlib[NogoStdlibInfo] - stdlib_facts = stdlib_info.facts - inputs.append(stdlib_facts) - - # The nogo tool operates on a configuration serialized in JSON format. - nogo_target_info = ctx.attr._nogo_target[NogoTargetInfo] - go_ctx = go_context(ctx, goos = nogo_target_info.goos, goarch = nogo_target_info.goarch) - facts = ctx.actions.declare_file(target.label.name + ".facts") - raw_findings = ctx.actions.declare_file(target.label.name + ".raw_findings") - escapes = ctx.actions.declare_file(target.label.name + ".escapes") - config = struct( - ImportPath = importpath, - GoFiles = [src.path for src in srcs if src.path.endswith(".go")], - NonGoFiles = [src.path for src in srcs if not src.path.endswith(".go")], - GOOS = go_ctx.goos, - GOARCH = go_ctx.goarch, - Tags = go_ctx.tags, - FactMap = fact_map, - ImportMap = import_map, - StdlibFacts = stdlib_facts.path, - ) - config_file = ctx.actions.declare_file(target.label.name + ".cfg") - ctx.actions.write(config_file, config.to_json()) - inputs.append(config_file) - ctx.actions.run( - inputs = inputs, - outputs = [facts, raw_findings, escapes], - tools = depset(go_ctx.runfiles.to_list() + ctx.files._nogo_objdump_tool), - executable = ctx.files._nogo_check[0], - mnemonic = "NogoAnalysis", - progress_message = "Analyzing %s" % target.label, - arguments = go_ctx.nogo_args + [ - "-binary=%s" % target_objfile.path, - "-objdump_tool=%s" % ctx.files._nogo_objdump_tool[0].path, - "-package=%s" % config_file.path, - "-findings=%s" % raw_findings.path, - "-facts=%s" % facts.path, - "-escapes=%s" % escapes.path, - ], - ) - - # Flatten all findings from all dependencies. - # - # This is done because all the filtering must be done at the - # top-level nogo_test to dynamically apply a configuration. - # This does not actually add any additional work here, but - # will simply propagate the full list of files. - all_raw_findings = [stdlib_info.raw_findings] + depset(all_raw_findings).to_list() + [raw_findings] - - # Return the package facts as output. - return [NogoInfo( - facts = facts, - raw_findings = all_raw_findings, - escapes = escapes, - importpath = importpath, - binaries = binaries, - srcs = srcs, - deps = deps, - )] - -nogo_aspect = go_rule( - aspect, - implementation = _nogo_aspect_impl, - attr_aspects = [ - "deps", - "library", - "embed", - ], - attrs = { - "_nogo_check": attr.label( - default = "//tools/nogo/check:check", - cfg = "host", - ), - "_nogo_stdlib": attr.label( - default = "//tools/nogo:stdlib", - cfg = "host", - ), - "_nogo_objdump_tool": attr.label( - default = "//tools/nogo:objdump_tool", - cfg = "host", - ), - "_nogo_target": attr.label( - default = "//tools/nogo:target", - cfg = "target", - ), - }, -) - -def _nogo_test_impl(ctx): - """Check nogo findings.""" - - # Ensure there's a single dependency. - if len(ctx.attr.deps) != 1: - fail("nogo_test requires exactly one dep.") - raw_findings = ctx.attr.deps[0][NogoInfo].raw_findings - escapes = ctx.attr.deps[0][NogoInfo].escapes - - # Build a step that applies the configuration. - config_srcs = ctx.attr.config[NogoConfigInfo].srcs - findings = ctx.actions.declare_file(ctx.label.name + ".findings") - ctx.actions.run( - inputs = raw_findings + ctx.files.srcs + config_srcs, - outputs = [findings], - tools = depset(ctx.files._filter), - executable = ctx.files._filter[0], - mnemonic = "GoStaticAnalysis", - progress_message = "Generating %s" % ctx.label, - arguments = ["-input=%s" % f.path for f in raw_findings] + - ["-config=%s" % f.path for f in config_srcs] + - ["-output=%s" % findings.path], - ) - - # Build a runner that checks the filtered facts. - # - # Note that this calls the filter binary without any configuration, so all - # findings will be included. But this is expected, since we've already - # filtered out everything that should not be included. - runner = ctx.actions.declare_file(ctx.label.name) - runner_content = [ - "#!/bin/bash", - "exec %s -input=%s" % (ctx.files._filter[0].short_path, findings.short_path), - "", - ] - ctx.actions.write(runner, "\n".join(runner_content), is_executable = True) - - return [DefaultInfo( - # The runner just executes the filter again, on the - # newly generated filtered findings. We still need - # the filter tool as part of our runfiles, however. - runfiles = ctx.runfiles(files = ctx.files._filter + [findings]), - executable = runner, - ), OutputGroupInfo( - # Propagate the filtered filters, for consumption by - # build tooling. Note that the build tooling typically - # pays attention to the mnemoic above, so this must be - # what is expected by the tooling. - nogo_findings = depset([findings]), - # Expose all escape analysis findings (see above). - nogo_escapes = depset([escapes]), - )] - -nogo_test = rule( - implementation = _nogo_test_impl, - attrs = { - "config": attr.label( - mandatory = True, - doc = "A rule of kind nogo_config.", - ), - "deps": attr.label_list( - aspects = [nogo_aspect], - doc = "Exactly one Go dependency to be analyzed.", - ), - "srcs": attr.label_list( - allow_files = True, - doc = "Relevant src files. This is ignored except to make the nogo_test directly affected by the files.", - ), - "_filter": attr.label(default = "//tools/nogo/filter:filter"), - }, - test = True, -) diff --git a/tools/nogo/filter/BUILD b/tools/nogo/filter/BUILD deleted file mode 100644 index e56a783e2..000000000 --- a/tools/nogo/filter/BUILD +++ /dev/null @@ -1,14 +0,0 @@ -load("//tools:defs.bzl", "go_binary") - -package(licenses = ["notice"]) - -go_binary( - name = "filter", - srcs = ["main.go"], - nogo = False, - visibility = ["//visibility:public"], - deps = [ - "//tools/nogo", - "@in_gopkg_yaml_v2//:go_default_library", - ], -) diff --git a/tools/nogo/filter/main.go b/tools/nogo/filter/main.go deleted file mode 100644 index 9cf41b3b0..000000000 --- a/tools/nogo/filter/main.go +++ /dev/null @@ -1,131 +0,0 @@ -// 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. - -// Binary check is the nogo entrypoint. -package main - -import ( - "flag" - "fmt" - "io/ioutil" - "log" - "os" - "strings" - - yaml "gopkg.in/yaml.v2" - "gvisor.dev/gvisor/tools/nogo" -) - -type stringList []string - -func (s *stringList) String() string { - return strings.Join(*s, ",") -} - -func (s *stringList) Set(value string) error { - *s = append(*s, value) - return nil -} - -var ( - inputFiles stringList - configFiles stringList - outputFile string - showConfig bool -) - -func init() { - flag.Var(&inputFiles, "input", "findings input files") - flag.StringVar(&outputFile, "output", "", "findings output file") - flag.Var(&configFiles, "config", "findings configuration files") - flag.BoolVar(&showConfig, "show-config", false, "dump configuration only") -} - -func main() { - flag.Parse() - - // Load all available findings. - var findings []nogo.Finding - for _, filename := range inputFiles { - inputFindings, err := nogo.ExtractFindingsFromFile(filename) - if err != nil { - log.Fatalf("unable to extract findings from %s: %v", filename, err) - } - findings = append(findings, inputFindings...) - } - - // Open and merge all configuations. - config := &nogo.Config{ - Global: make(nogo.AnalyzerConfig), - Analyzers: make(map[nogo.AnalyzerName]nogo.AnalyzerConfig), - } - for _, filename := range configFiles { - content, err := ioutil.ReadFile(filename) - if err != nil { - log.Fatalf("unable to read %s: %v", filename, err) - } - var newConfig nogo.Config // For current file. - if err := yaml.Unmarshal(content, &newConfig); err != nil { - log.Fatalf("unable to decode %s: %v", filename, err) - } - config.Merge(&newConfig) - if showConfig { - bytes, err := yaml.Marshal(&newConfig) - if err != nil { - log.Fatalf("error marshalling config: %v", err) - } - mergedBytes, err := yaml.Marshal(config) - if err != nil { - log.Fatalf("error marshalling config: %v", err) - } - fmt.Fprintf(os.Stdout, "Loaded configuration from %s:\n%s\n", filename, string(bytes)) - fmt.Fprintf(os.Stdout, "Merged configuration:\n%s\n", string(mergedBytes)) - } - } - if err := config.Compile(); err != nil { - log.Fatalf("error compiling config: %v", err) - } - if showConfig { - os.Exit(0) - } - - // Filter the findings (and aggregate by group). - filteredFindings := make([]nogo.Finding, 0, len(findings)) - for _, finding := range findings { - if ok := config.ShouldReport(finding); ok { - filteredFindings = append(filteredFindings, finding) - } - } - - // Write the output (if required). - // - // If the outputFile is specified, then we exit here. Otherwise, - // we continue to write to stdout and treat like a test. - if outputFile != "" { - if err := nogo.WriteFindingsToFile(filteredFindings, outputFile); err != nil { - log.Fatalf("unable to write findings: %v", err) - } - return - } - - // Treat the run as a test. - if len(filteredFindings) == 0 { - fmt.Fprintf(os.Stdout, "PASS\n") - os.Exit(0) - } - for _, finding := range filteredFindings { - fmt.Fprintf(os.Stdout, "%s\n", finding.String()) - } - os.Exit(1) -} diff --git a/tools/nogo/findings.go b/tools/nogo/findings.go deleted file mode 100644 index 5bd850269..000000000 --- a/tools/nogo/findings.go +++ /dev/null @@ -1,63 +0,0 @@ -// 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 nogo - -import ( - "encoding/json" - "fmt" - "go/token" - "io/ioutil" -) - -// Finding is a single finding. -type Finding struct { - Category AnalyzerName - Position token.Position - Message string -} - -// String implements fmt.Stringer.String. -func (f *Finding) String() string { - return fmt.Sprintf("%s: %s: %s", f.Category, f.Position.String(), f.Message) -} - -// WriteFindingsToFile writes findings to a file. -func WriteFindingsToFile(findings []Finding, filename string) error { - content, err := WriteFindingsToBytes(findings) - if err != nil { - return err - } - return ioutil.WriteFile(filename, content, 0644) -} - -// WriteFindingsToBytes serializes findings as bytes. -func WriteFindingsToBytes(findings []Finding) ([]byte, error) { - return json.Marshal(findings) -} - -// ExtractFindingsFromFile loads findings from a file. -func ExtractFindingsFromFile(filename string) ([]Finding, error) { - content, err := ioutil.ReadFile(filename) - if err != nil { - return nil, err - } - return ExtractFindingsFromBytes(content) -} - -// ExtractFindingsFromBytes loads findings from bytes. -func ExtractFindingsFromBytes(content []byte) (findings []Finding, err error) { - err = json.Unmarshal(content, &findings) - return findings, err -} diff --git a/tools/nogo/nogo.go b/tools/nogo/nogo.go deleted file mode 100644 index 779d4d6d8..000000000 --- a/tools/nogo/nogo.go +++ /dev/null @@ -1,473 +0,0 @@ -// 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 nogo 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 nogo - -import ( - "encoding/json" - "errors" - "fmt" - "go/ast" - "go/build" - "go/parser" - "go/token" - "go/types" - "io" - "io/ioutil" - "log" - "os" - "path" - "path/filepath" - "reflect" - "strings" - - "golang.org/x/tools/go/analysis" - "golang.org/x/tools/go/analysis/internal/facts" - "golang.org/x/tools/go/gcexportdata" - - // Special case: flags live here and change overall behavior. - "gvisor.dev/gvisor/tools/checkescape" -) - -// StdlibConfig is serialized as the configuration. -// -// This contains everything required for stdlib analysis. -type StdlibConfig struct { - Srcs []string - GOOS string - GOARCH string - Tags []string -} - -// PackageConfig is serialized as the configuration. -// -// This contains everything required for single package analysis. -type PackageConfig struct { - ImportPath string - GoFiles []string - NonGoFiles []string - Tags []string - GOOS string - GOARCH string - ImportMap map[string]string - FactMap map[string]string - StdlibFacts string -} - -// loader is a fact-loader function. -type loader func(string) ([]byte, error) - -// saver is a fact-saver function. -type saver func([]byte) error - -// factLoader returns a function that loads facts. -// -// This resolves all standard library facts and imported package facts up -// front. The returned loader function will never return an error, only -// empty facts. -// -// This is done because all stdlib data is stored together, and we don't want -// to load this data many times over. -func (c *PackageConfig) factLoader() (loader, error) { - allFacts := make(map[string][]byte) - if c.StdlibFacts != "" { - data, err := ioutil.ReadFile(c.StdlibFacts) - if err != nil { - return nil, fmt.Errorf("error loading stdlib facts from %q: %w", c.StdlibFacts, err) - } - var stdlibFacts map[string][]byte - if err := json.Unmarshal(data, &stdlibFacts); err != nil { - return nil, fmt.Errorf("error loading stdlib facts: %w", err) - } - for pkg, data := range stdlibFacts { - allFacts[pkg] = data - } - } - for pkg, file := range c.FactMap { - data, err := ioutil.ReadFile(file) - if err != nil { - return nil, fmt.Errorf("error loading %q: %w", file, err) - } - allFacts[pkg] = data - } - return func(path string) ([]byte, error) { - return allFacts[path], nil - }, nil -} - -// shouldInclude indicates whether the file should be included. -// -// NOTE: This does only basic parsing of tags. -func (c *PackageConfig) shouldInclude(path string) (bool, error) { - ctx := build.Default - ctx.GOOS = c.GOOS - ctx.GOARCH = c.GOARCH - ctx.BuildTags = c.Tags - return ctx.MatchFile(filepath.Dir(path), filepath.Base(path)) -} - -// importer is an 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 { - *PackageConfig - fset *token.FileSet - cache map[string]*types.Package - lastErr error - callback func(string) error -} - -// Import implements types.Importer.Import. -func (i *importer) Import(path string) (*types.Package, error) { - if path == "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 - } - - // Call the internal callback. This is used to resolve loading order - // for the standard library. See checkStdlib. - if i.callback != nil { - if err := i.callback(path); err != nil { - i.lastErr = err - return nil, err - } - } - - // Actually load the data. - realPath, ok := i.ImportMap[path] - var ( - rc io.ReadCloser - err error - ) - if !ok { - // Not found in the import path. Attempt to find the package - // via the standard library. - rc, err = findStdPkg(i.GOOS, i.GOARCH, path) - } else { - // Open the file. - rc, err = os.Open(realPath) - } - if err != nil { - i.lastErr = err - return nil, err - } - defer rc.Close() - - // Load all exported data. - r, err := gcexportdata.NewReader(rc) - if err != nil { - return nil, err - } - - return gcexportdata.Read(r, i.fset, i.cache, path) -} - -// ErrSkip indicates the package should be skipped. -var ErrSkip = errors.New("skipped") - -// CheckStdlib checks the standard library. -// -// This constructs a synthetic package configuration for each library in the -// standard library sources, and call CheckPackage repeatedly. -// -// Note that not all parts of the source are expected to build. We skip obvious -// test files, and cmd files, which should not be dependencies. -func CheckStdlib(config *StdlibConfig, analyzers []*analysis.Analyzer) (allFindings []Finding, facts []byte, err error) { - if len(config.Srcs) == 0 { - return nil, nil, nil - } - - // Ensure all paths are normalized. - for i := 0; i < len(config.Srcs); i++ { - config.Srcs[i] = path.Clean(config.Srcs[i]) - } - - // Calculate the root source directory. This is always a directory - // named 'src', of which we simply take the first we find. This is a - // bit fragile, but works for all currently known Go source - // configurations. - // - // Note that there may be extra files outside of the root source - // directory; we simply ignore those. - rootSrcPrefix := "" - for _, file := range config.Srcs { - const src = "/src/" - i := strings.Index(file, src) - if i == -1 { - // Superfluous file. - continue - } - - // Index of first character after /src/. - i += len(src) - rootSrcPrefix = file[:i] - break - } - - // Aggregate all files by directory. - packages := make(map[string]*PackageConfig) - for _, file := range config.Srcs { - if !strings.HasPrefix(file, rootSrcPrefix) { - // Superflouous file. - continue - } - - d := path.Dir(file) - if len(rootSrcPrefix) >= len(d) { - continue // Not a file. - } - pkg := d[len(rootSrcPrefix):] - // Skip cmd packages and obvious test files: see above. - if strings.HasPrefix(pkg, "cmd/") || strings.HasSuffix(file, "_test.go") { - continue - } - c, ok := packages[pkg] - if !ok { - c = &PackageConfig{ - ImportPath: pkg, - GOOS: config.GOOS, - GOARCH: config.GOARCH, - Tags: config.Tags, - } - packages[pkg] = c - } - // Add the files appropriately. Note that they will be further - // filtered by architecture and build tags below, so this need - // not be done immediately. - if strings.HasSuffix(file, ".go") { - c.GoFiles = append(c.GoFiles, file) - } else { - c.NonGoFiles = append(c.NonGoFiles, file) - } - } - - // Closure to check a single package. - stdlibFacts := make(map[string][]byte) - stdlibErrs := make(map[string]error) - var checkOne func(pkg string) error // Recursive. - checkOne = func(pkg string) error { - // Is this already done? - if _, ok := stdlibFacts[pkg]; ok { - return nil - } - // Did this fail previously? - if _, ok := stdlibErrs[pkg]; ok { - return nil - } - - // Lookup the configuration. - config, ok := packages[pkg] - if !ok { - return nil // Not known. - } - - // Find the binary package, and provide to objdump. - rc, err := findStdPkg(config.GOOS, config.GOARCH, pkg) - if err != nil { - // If there's no binary for this package, it is likely - // not built with the distribution. That's fine, we can - // just skip analysis. - stdlibErrs[pkg] = err - return nil - } - - // Provide the input. - oldReader := checkescape.Reader - checkescape.Reader = rc // For analysis. - defer func() { - rc.Close() - checkescape.Reader = oldReader // Restore. - }() - - // Run the analysis. - findings, factData, err := CheckPackage(config, analyzers, checkOne) - if err != nil { - // If we can't analyze a package from the standard library, - // then we skip it. It will simply not have any findings. - stdlibErrs[pkg] = err - return nil - } - stdlibFacts[pkg] = factData - allFindings = append(allFindings, findings...) - return nil - } - - // Check all packages. - // - // Note that this may call checkOne recursively, so it's not guaranteed - // to evaluate in the order provided here. We do ensure however, that - // all packages are evaluated. - for pkg := range packages { - if err := checkOne(pkg); err != nil { - return nil, nil, err - } - } - - // Sanity check. - if len(stdlibFacts) == 0 { - return nil, nil, fmt.Errorf("no stdlib facts found: misconfiguration?") - } - - // Write out all findings. - factData, err := json.Marshal(stdlibFacts) - if err != nil { - return nil, nil, fmt.Errorf("error saving stdlib facts: %w", err) - } - - // Write out all errors. - for pkg, err := range stdlibErrs { - log.Printf("WARNING: error while processing %v: %v", pkg, err) - } - - // Return all findings. - return allFindings, factData, nil -} - -// CheckPackage runs all given analyzers. -// -// The implementation was adapted from [1], which was in turn adpated from [2]. -// This returns a list of matching analysis issues, or an error if the analysis -// could not be completed. -// -// [1] bazelbuid/rules_go/tools/builders/nogo_main.go -// [2] golang.org/x/tools/go/checker/internal/checker -func CheckPackage(config *PackageConfig, analyzers []*analysis.Analyzer, importCallback func(string) error) (findings []Finding, factData []byte, err error) { - imp := &importer{ - PackageConfig: config, - fset: token.NewFileSet(), - cache: make(map[string]*types.Package), - callback: importCallback, - } - - // Load all source files. - var syntax []*ast.File - for _, file := range config.GoFiles { - include, err := config.shouldInclude(file) - if err != nil { - return nil, nil, fmt.Errorf("error evaluating file %q: %v", file, err) - } - if !include { - continue - } - s, err := parser.ParseFile(imp.fset, file, nil, parser.ParseComments) - if err != nil { - return nil, nil, fmt.Errorf("error parsing file %q: %v", file, err) - } - syntax = append(syntax, s) - } - - // Check type information. - typesSizes := types.SizesFor("gc", config.GOARCH) - typeConfig := types.Config{Importer: imp} - typesInfo := &types.Info{ - Types: make(map[ast.Expr]types.TypeAndValue), - 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), - } - types, err := typeConfig.Check(config.ImportPath, imp.fset, syntax, typesInfo) - if err != nil && imp.lastErr != ErrSkip { - return nil, nil, fmt.Errorf("error checking types: %w", err) - } - - // Load all package facts. - loader, err := config.factLoader() - if err != nil { - return nil, nil, fmt.Errorf("error loading facts: %w", err) - } - facts, err := facts.Decode(types, loader) - if err != nil { - return nil, nil, fmt.Errorf("error decoding facts: %w", err) - } - - // Register fact types and establish dependencies between analyzers. - // The visit closure will execute recursively, and populate results - // will all required analysis results. - results := make(map[*analysis.Analyzer]interface{}) - var visit func(*analysis.Analyzer) error // For recursion. - visit = func(a *analysis.Analyzer) error { - if _, ok := results[a]; ok { - return nil - } - - // Run recursively for all dependencies. - for _, req := range a.Requires { - if err := visit(req); err != nil { - return err - } - } - - // Run the analysis. - factFilter := make(map[reflect.Type]bool) - for _, f := range a.FactTypes { - factFilter[reflect.TypeOf(f)] = true - } - p := &analysis.Pass{ - Analyzer: a, - Fset: imp.fset, - Files: syntax, - Pkg: types, - TypesInfo: typesInfo, - ResultOf: results, // All results. - Report: func(d analysis.Diagnostic) { - findings = append(findings, Finding{ - Category: AnalyzerName(a.Name), - Position: imp.fset.Position(d.Pos), - Message: d.Message, - }) - }, - ImportPackageFact: facts.ImportPackageFact, - ExportPackageFact: facts.ExportPackageFact, - ImportObjectFact: facts.ImportObjectFact, - ExportObjectFact: facts.ExportObjectFact, - AllPackageFacts: func() []analysis.PackageFact { return facts.AllPackageFacts(factFilter) }, - AllObjectFacts: func() []analysis.ObjectFact { return facts.AllObjectFacts(factFilter) }, - TypesSizes: typesSizes, - } - result, err := a.Run(p) - if err != nil { - return fmt.Errorf("error running analysis %s: %v", a, err) - } - - // Sanity check & save the result. - if got, want := reflect.TypeOf(result), a.ResultType; got != want { - return fmt.Errorf("error: analyzer %s returned a result of type %v, but declared ResultType %v", a, got, want) - } - results[a] = result - return nil // Success. - } - - // Visit all analyzers recursively. - for _, a := range analyzers { - if imp.lastErr == ErrSkip { - continue // No local analysis. - } - if err := visit(a); err != nil { - return nil, nil, err // Already has context. - } - } - - // Return all findings. - return findings, facts.Encode(), nil -} |