diff options
Diffstat (limited to 'tools/nogo')
-rw-r--r-- | tools/nogo/BUILD | 87 | ||||
-rw-r--r-- | tools/nogo/README.md | 31 | ||||
-rw-r--r-- | tools/nogo/analyzers.go | 129 | ||||
-rw-r--r-- | tools/nogo/build.go | 33 | ||||
-rw-r--r-- | tools/nogo/check/BUILD | 14 | ||||
-rw-r--r-- | tools/nogo/check/main.go | 115 | ||||
-rw-r--r-- | tools/nogo/config-schema.json | 97 | ||||
-rw-r--r-- | tools/nogo/config.go | 319 | ||||
-rw-r--r-- | tools/nogo/config_test.go | 301 | ||||
-rw-r--r-- | tools/nogo/defs.bzl | 506 | ||||
-rw-r--r-- | tools/nogo/filter/BUILD | 15 | ||||
-rw-r--r-- | tools/nogo/filter/main.go | 190 | ||||
-rw-r--r-- | tools/nogo/findings.go | 127 | ||||
-rw-r--r-- | tools/nogo/nogo.go | 667 | ||||
-rw-r--r-- | tools/nogo/objdump/BUILD | 10 | ||||
-rw-r--r-- | tools/nogo/objdump/objdump.go | 96 |
16 files changed, 0 insertions, 2737 deletions
diff --git a/tools/nogo/BUILD b/tools/nogo/BUILD deleted file mode 100644 index d72821377..000000000 --- a/tools/nogo/BUILD +++ /dev/null @@ -1,87 +0,0 @@ -load("//tools:defs.bzl", "bzl_library", "go_library", "go_test", "select_goarch", "select_goos") -load("//tools/nogo:defs.bzl", "nogo_objdump_tool", "nogo_stdlib", "nogo_target") - -package(licenses = ["notice"]) - -exports_files(["config-schema.json"]) - -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/checklinkname", - "//tools/checklocks", - "//tools/checkunsafe", - "//tools/nogo/objdump", - "//tools/worker", - "@co_honnef_go_tools//staticcheck:go_default_library", - "@co_honnef_go_tools//stylecheck:go_default_library", - "@org_golang_x_tools//go/analysis:go_default_library", - "@org_golang_x_tools//go/analysis/internal/facts:go_default_library", - "@org_golang_x_tools//go/analysis/passes/asmdecl:go_default_library", - "@org_golang_x_tools//go/analysis/passes/assign:go_default_library", - "@org_golang_x_tools//go/analysis/passes/atomic:go_default_library", - "@org_golang_x_tools//go/analysis/passes/bools:go_default_library", - "@org_golang_x_tools//go/analysis/passes/buildtag:go_default_library", - "@org_golang_x_tools//go/analysis/passes/cgocall:go_default_library", - "@org_golang_x_tools//go/analysis/passes/composite:go_default_library", - "@org_golang_x_tools//go/analysis/passes/copylock:go_default_library", - "@org_golang_x_tools//go/analysis/passes/errorsas:go_default_library", - "@org_golang_x_tools//go/analysis/passes/httpresponse:go_default_library", - "@org_golang_x_tools//go/analysis/passes/loopclosure:go_default_library", - "@org_golang_x_tools//go/analysis/passes/lostcancel:go_default_library", - "@org_golang_x_tools//go/analysis/passes/nilfunc:go_default_library", - "@org_golang_x_tools//go/analysis/passes/nilness:go_default_library", - "@org_golang_x_tools//go/analysis/passes/printf:go_default_library", - "@org_golang_x_tools//go/analysis/passes/shadow:go_default_library", - "@org_golang_x_tools//go/analysis/passes/shift:go_default_library", - "@org_golang_x_tools//go/analysis/passes/stdmethods:go_default_library", - "@org_golang_x_tools//go/analysis/passes/stringintconv:go_default_library", - "@org_golang_x_tools//go/analysis/passes/structtag:go_default_library", - "@org_golang_x_tools//go/analysis/passes/tests:go_default_library", - "@org_golang_x_tools//go/analysis/passes/unmarshal:go_default_library", - "@org_golang_x_tools//go/analysis/passes/unreachable:go_default_library", - "@org_golang_x_tools//go/analysis/passes/unsafeptr:go_default_library", - "@org_golang_x_tools//go/analysis/passes/unusedresult:go_default_library", - "@org_golang_x_tools//go/gcexportdata:go_default_library", - "@org_golang_x_tools//go/types/objectpath:go_default_library", - ], -) - -go_test( - name = "nogo_test", - srcs = ["config_test.go"], - library = ":nogo", -) - -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 db8bbdb8a..000000000 --- a/tools/nogo/analyzers.go +++ /dev/null @@ -1,129 +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/checklinkname" - "gvisor.dev/gvisor/tools/checklocks" - "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, - checklinkname.Analyzer, - checklocks.Analyzer, -} - -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.Analyzer) - } - // Add all stylecheck analyzers. - for _, a := range stylecheck.Analyzers { - AllAnalyzers = append(AllAnalyzers, a.Analyzer) - } - - // Register lists. - register(AllAnalyzers) -} diff --git a/tools/nogo/build.go b/tools/nogo/build.go deleted file mode 100644 index 4067bb480..000000000 --- a/tools/nogo/build.go +++ /dev/null @@ -1,33 +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. - -//go:build go1.1 -// +build go1.1 - -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 666780dd3..000000000 --- a/tools/nogo/check/BUILD +++ /dev/null @@ -1,14 +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", - "//tools/worker", - ], -) diff --git a/tools/nogo/check/main.go b/tools/nogo/check/main.go deleted file mode 100644 index 0e7e92965..000000000 --- a/tools/nogo/check/main.go +++ /dev/null @@ -1,115 +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" - "gvisor.dev/gvisor/tools/worker" -) - -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)") -) - -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() { - worker.Work(run) -} - -func run([]string) int { - var ( - findings []nogo.Finding - factData []byte - err error - ) - - // Check & load the configuration. - if *packageFile != "" && *stdlibFile != "" { - fmt.Fprintf(os.Stderr, "unable to perform stdlib and package analysis; provide only one!") - return 1 - } - - // Run the configuration. - if *stdlibFile != "" { - // Perform stdlib analysis. - c := loadConfig(*stdlibFile, new(nogo.StdlibConfig)).(*nogo.StdlibConfig) - findings, factData, err = nogo.CheckStdlib(c, nogo.AllAnalyzers) - } else if *packageFile != "" { - // Perform standard analysis. - c := loadConfig(*packageFile, new(nogo.PackageConfig)).(*nogo.PackageConfig) - findings, factData, err = nogo.CheckPackage(c, nogo.AllAnalyzers, nil) - } else { - fmt.Fprintf(os.Stderr, "please provide at least one of package or stdlib!") - return 1 - } - - // Check that analysis was successful. - if err != nil { - fmt.Fprintf(os.Stderr, "error performing analysis: %v", err) - return 1 - } - - // Save facts. - if *factsOutput != "" { - if err := ioutil.WriteFile(*factsOutput, factData, 0644); err != nil { - fmt.Fprintf(os.Stderr, "error saving findings to %q: %v", *factsOutput, err) - return 1 - } - } - - // Write all findings. - if *findingsOutput != "" { - w, err := os.OpenFile(*findingsOutput, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644) - if err != nil { - fmt.Fprintf(os.Stderr, "error opening output file %q: %v", *findingsOutput, err) - return 1 - } - if err := nogo.WriteFindingsTo(w, findings, false /* json */); err != nil { - fmt.Fprintf(os.Stderr, "error writing findings to %q: %v", *findingsOutput, err) - return 1 - } - } else { - for _, finding := range findings { - fmt.Fprintf(os.Stdout, "%s\n", finding.String()) - } - } - - return 0 -} diff --git a/tools/nogo/config-schema.json b/tools/nogo/config-schema.json deleted file mode 100644 index 3c25fe221..000000000 --- a/tools/nogo/config-schema.json +++ /dev/null @@ -1,97 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema", - "definitions": { - "group": { - "type": "object", - "properties": { - "name": { - "description": "The name of the group.", - "type": "string" - }, - "regex": { - "description": "A regular expression for matching paths.", - "type": "string" - }, - "default": { - "description": "Whether the group is enabled by default.", - "type": "boolean" - } - }, - "required": [ - "name", - "regex", - "default" - ], - "additionalProperties": false - }, - "regexlist": { - "description": "A list of regular expressions.", - "oneOf": [ - { - "type": "array", - "items": { - "type": "string" - } - }, - { - "type": "null" - } - ] - }, - "rule": { - "type": "object", - "properties": { - "exclude": { - "description": "A regular expression for paths to exclude.", - "$ref": "#/definitions/regexlist" - }, - "suppress": { - "description": "A regular expression for messages to suppress.", - "$ref": "#/definitions/regexlist" - } - }, - "additionalProperties": false - }, - "ruleList": { - "type": "object", - "additionalProperties": { - "oneOf": [ - { - "$ref": "#/definitions/rule" - }, - { - "type": "null" - } - ] - } - } - }, - "properties": { - "groups": { - "description": "A definition of all groups.", - "type": "array", - "items": { - "$ref": "#/definitions/group" - }, - "minItems": 1 - }, - "global": { - "description": "A global set of rules.", - "type": "object", - "additionalProperties": { - "$ref": "#/definitions/rule" - } - }, - "analyzers": { - "description": "A definition of all groups.", - "type": "object", - "additionalProperties": { - "$ref": "#/definitions/ruleList" - } - } - }, - "required": [ - "groups" - ], - "additionalProperties": false -} diff --git a/tools/nogo/config.go b/tools/nogo/config.go deleted file mode 100644 index ee2533610..000000000 --- a/tools/nogo/config.go +++ /dev/null @@ -1,319 +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, len(ss)) - for i, s := range ss { - r, err := regexp.Compile(s) - if err != nil { - return err - } - (*rs)[i] = r - } - return nil -} - -// RegexpCount is used by AnalyzerConfig.RegexpCount. -func (i *ItemConfig) RegexpCount() int64 { - if i == nil { - // See compile. - return 0 - } - // Return the number of regular expressions compiled for these items. - // This is how the cache size of the configuration is measured. - return int64(len(i.exclude) + len(i.suppress)) -} - -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 merge(a, b []string) []string { - found := make(map[string]struct{}) - result := make([]string, 0, len(a)+len(b)) - for _, elem := range a { - found[elem] = struct{}{} - result = append(result, elem) - } - for _, elem := range b { - if _, ok := found[elem]; ok { - continue - } - result = append(result, elem) - } - return result -} - -func (i *ItemConfig) merge(other *ItemConfig) { - i.Exclude = merge(i.Exclude, other.Exclude) - i.Suppress = merge(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 - -// RegexpCount is used by Config.Size. -func (a AnalyzerConfig) RegexpCount() int64 { - count := int64(0) - for _, gc := range a { - count += gc.RegexpCount() - } - return count -} - -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) - } -} - -// shouldReport returns whether the finding should be reported or suppressed. -// It returns !ok if there is no configuration sufficient to decide one way or -// another. -func (a AnalyzerConfig) shouldReport(groupConfig *Group, fullPos, msg string) (report, ok bool) { - gc, ok := a[groupConfig.Name] - if !ok { - return false, false - } - - // 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), true -} - -// 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"` -} - -// Size implements worker.Sizer.Size. -func (c *Config) Size() int64 { - count := c.Global.RegexpCount() - for _, config := range c.Analyzers { - count += config.RegexpCount() - } - // The size is measured as the number of regexps that are compiled - // here. We multiply by 1k to produce an estimate. - return 1024 * count -} - -// Merge merges two configurations. -func (c *Config) Merge(other *Config) { - // Merge all groups. - // - // Select the other first, as the order provided in the second will - // provide precendence over the same group defined in the first one. - seenGroups := make(map[GroupName]struct{}) - newGroups := make([]Group, 0, len(c.Groups)+len(other.Groups)) - for _, g := range other.Groups { - newGroups = append(newGroups, g) - seenGroups[g.Name] = struct{}{} - } - for _, g := range c.Groups { - if _, ok := seenGroups[g.Name]; ok { - continue - } - newGroups = append(newGroups, g) - } - c.Groups = newGroups - - // 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? - report, ok := c.Global.shouldReport(groupConfig, fullPos, finding.Message) - if ok && !report { - return false - } - - // Try the analyzer config. - ac, ok := c.Analyzers[finding.Category] - if !ok { - return groupConfig.Default - } - report, ok = ac.shouldReport(groupConfig, fullPos, finding.Message) - if !ok { - return groupConfig.Default - } - return report -} diff --git a/tools/nogo/config_test.go b/tools/nogo/config_test.go deleted file mode 100644 index 685cffbec..000000000 --- a/tools/nogo/config_test.go +++ /dev/null @@ -1,301 +0,0 @@ -// Copyright 2021 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 -package nogo - -import ( - "go/token" - "testing" -) - -// TestShouldReport validates the suppression behavior of Config.ShouldReport. -func TestShouldReport(t *testing.T) { - config := &Config{ - Groups: []Group{ - { - Name: "default-enabled", - Regex: "^default-enabled/", - Default: true, - }, - { - Name: "default-disabled", - Regex: "^default-disabled/", - Default: false, - }, - { - Name: "default-disabled-omitted-from-global", - Regex: "^default-disabled-omitted-from-global/", - Default: false, - }, - }, - Global: AnalyzerConfig{ - "default-enabled": &ItemConfig{ - Exclude: []string{"excluded.go"}, - Suppress: []string{"suppressed"}, - }, - "default-disabled": &ItemConfig{ - Exclude: []string{"excluded.go"}, - Suppress: []string{"suppressed"}, - }, - // Omitting default-disabled-omitted-from-global here - // has no effect on configuration below. - }, - Analyzers: map[AnalyzerName]AnalyzerConfig{ - "analyzer-suppressions": AnalyzerConfig{ - // Suppress some. - "default-enabled": &ItemConfig{ - Exclude: []string{"limited-exclude.go"}, - Suppress: []string{"limited suppress"}, - }, - // Enable all. - "default-disabled": nil, - }, - "enabled-for-default-disabled": AnalyzerConfig{ - "default-disabled": nil, - "default-disabled-omitted-from-global": nil, - }, - }, - } - - if err := config.Compile(); err != nil { - t.Fatalf("Compile(%+v) = %v, want nil", config, err) - } - - cases := []struct { - name string - finding Finding - want bool - }{ - { - name: "enabled", - finding: Finding{ - Category: "foo", - Position: token.Position{ - Filename: "default-enabled/file.go", - Offset: 0, - Line: 1, - Column: 1, - }, - Message: "message", - }, - want: true, - }, - { - name: "ungrouped", - finding: Finding{ - Category: "foo", - Position: token.Position{ - Filename: "ungrouped/file.go", - Offset: 0, - Line: 1, - Column: 1, - }, - Message: "message", - }, - want: true, - }, - { - name: "suppressed", - finding: Finding{ - Category: "foo", - Position: token.Position{ - Filename: "default-enabled/file.go", - Offset: 0, - Line: 1, - Column: 1, - }, - Message: "message suppressed", - }, - want: false, - }, - { - name: "excluded", - finding: Finding{ - Category: "foo", - Position: token.Position{ - Filename: "default-enabled/excluded.go", - Offset: 0, - Line: 1, - Column: 1, - }, - Message: "message", - }, - want: false, - }, - { - name: "disabled", - finding: Finding{ - Category: "foo", - Position: token.Position{ - Filename: "default-disabled/file.go", - Offset: 0, - Line: 1, - Column: 1, - }, - Message: "message", - }, - want: false, - }, - { - name: "analyzer suppressed", - finding: Finding{ - Category: "analyzer-suppressions", - Position: token.Position{ - Filename: "default-enabled/file.go", - Offset: 0, - Line: 1, - Column: 1, - }, - Message: "message limited suppress", - }, - want: false, - }, - { - name: "analyzer suppressed not global", - finding: Finding{ - // Doesn't apply outside of analyzer-suppressions. - Category: "foo", - Position: token.Position{ - Filename: "default-enabled/file.go", - Offset: 0, - Line: 1, - Column: 1, - }, - Message: "message limited suppress", - }, - want: true, - }, - { - name: "analyzer suppressed grouped", - finding: Finding{ - Category: "analyzer-suppressions", - Position: token.Position{ - // Doesn't apply outside of default-enabled. - Filename: "default-disabled/file.go", - Offset: 0, - Line: 1, - Column: 1, - }, - Message: "message limited suppress", - }, - want: true, - }, - { - name: "analyzer excluded", - finding: Finding{ - Category: "analyzer-suppressions", - Position: token.Position{ - Filename: "default-enabled/limited-exclude.go", - Offset: 0, - Line: 1, - Column: 1, - }, - Message: "message", - }, - want: false, - }, - { - name: "analyzer excluded not global", - finding: Finding{ - // Doesn't apply outside of analyzer-suppressions. - Category: "foo", - Position: token.Position{ - Filename: "default-enabled/limited-exclude.go", - Offset: 0, - Line: 1, - Column: 1, - }, - Message: "message", - }, - want: true, - }, - { - name: "analyzer excluded grouped", - finding: Finding{ - Category: "analyzer-suppressions", - Position: token.Position{ - // Doesn't apply outside of default-enabled. - Filename: "default-disabled/limited-exclude.go", - Offset: 0, - Line: 1, - Column: 1, - }, - Message: "message", - }, - want: true, - }, - { - name: "disabled-omitted", - finding: Finding{ - Category: "foo", - Position: token.Position{ - Filename: "default-disabled-omitted-from-global/file.go", - Offset: 0, - Line: 1, - Column: 1, - }, - Message: "message", - }, - want: false, - }, - { - name: "default enabled applies to customized analyzer", - finding: Finding{ - Category: "enabled-for-default-disabled", - Position: token.Position{ - Filename: "default-enabled/file.go", - Offset: 0, - Line: 1, - Column: 1, - }, - Message: "message", - }, - want: true, - }, - { - name: "default overridden in customized analyzer", - finding: Finding{ - Category: "enabled-for-default-disabled", - Position: token.Position{ - Filename: "default-disabled/file.go", - Offset: 0, - Line: 1, - Column: 1, - }, - Message: "message", - }, - want: true, - }, - { - name: "default overridden in customized analyzer even when omitted from global", - finding: Finding{ - Category: "enabled-for-default-disabled", - Position: token.Position{ - Filename: "default-disabled-omitted-from-global/file.go", - Offset: 0, - Line: 1, - Column: 1, - }, - Message: "message", - }, - want: true, - }, - } - for _, tc := range cases { - t.Run(tc.name, func(t *testing.T) { - if got := config.ShouldReport(tc.finding); got != tc.want { - t.Errorf("ShouldReport(%+v) = %v, want %v", tc.finding, got, tc.want) - } - }) - } -} diff --git a/tools/nogo/defs.bzl b/tools/nogo/defs.bzl deleted file mode 100644 index dc9a8b24e..000000000 --- a/tools/nogo/defs.bzl +++ /dev/null @@ -1,506 +0,0 @@ -"""Nogo rules.""" - -load("//tools/bazeldefs:go.bzl", "go_context", "go_embed_libraries", "go_importpath", "go_rule") - -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)", - "worker_debug": "transitive debugging", - }, -) - -def _nogo_target_impl(ctx): - return [NogoTargetInfo( - goarch = ctx.attr.goarch, - goos = ctx.attr.goos, - worker_debug = ctx.attr.worker_debug, - )] - -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, - ), - "worker_debug": attr.bool( - doc = "whether worker debugging should be enabled.", - default = False, - ), - }, -) - -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._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 = { - "_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._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.gotags, - ) - config_file = ctx.actions.declare_file(ctx.label.name + ".cfg") - ctx.actions.write(config_file, config.to_json()) - args_file = ctx.actions.declare_file(ctx.label.name + "_args_file") - ctx.actions.write( - output = args_file, - content = "\n".join(go_ctx.nogo_args + [ - "-objdump_tool=%s" % ctx.files._objdump_tool[0].path, - "-stdlib=%s" % config_file.path, - "-findings=%s" % raw_findings.path, - "-facts=%s" % facts.path, - ]), - ) - ctx.actions.run( - inputs = [config_file] + go_ctx.stdlib_srcs + [args_file], - outputs = [facts, raw_findings], - tools = depset(go_ctx.runfiles.to_list() + ctx.files._objdump_tool), - executable = ctx.files._check[0], - mnemonic = "GoStandardLibraryAnalysis", - # Note that this does not support work execution currently. There is an - # issue with stdout pollution that is not yet resolved, so this is kept - # as a separate menomic. - progress_message = "Analyzing Go Standard Library", - arguments = [ - "--worker_debug=%s" % nogo_target_info.worker_debug, - "@%s" % args_file.path, - ], - ) - - # Return the stdlib facts as output. - return [NogoStdlibInfo( - facts = facts, - raw_findings = raw_findings, - ), DefaultInfo( - # Declare the facts and findings as default outputs. This is not - # strictly required, but ensures that the target still perform analysis - # when built directly rather than just indirectly via a nogo_test. - files = depset([facts, raw_findings]), - )] - -nogo_stdlib = go_rule( - rule, - implementation = _nogo_stdlib_impl, - attrs = { - "_check": attr.label( - default = "//tools/nogo/check:check", - cfg = "host", - ), - "_objdump_tool": attr.label( - default = "//tools/nogo:objdump_tool", - cfg = "host", - ), - "_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)", - "importpath": "package import path", - "binaries": "package binary files", - "srcs": "srcs (for go_test support)", - "deps": "deps (for go_test support)", - }, -) - -def _select_objfile(files): - """Returns (.a file, .x file, is_archive). - - If no .a file is available, then the first .x file will be returned - instead, and vice versa. If neither are available, then the first provided - file will be returned.""" - a_files = [f for f in files if f.path.endswith(".a")] - x_files = [f for f in files if f.path.endswith(".x")] - if not len(x_files) and not len(a_files): - return (files[0], files[0], False) - if not len(x_files): - x_files = a_files - if not len(a_files): - a_files = x_files - return a_files[0], x_files[0], True - -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. - # - # Note that we almost exclusively use go_library, not go_tool_library. - # This is because nogo is manually annotated, so the go_tool_library kind - # is not needed to avoid dependency loops. Unfortunately, bazel coverdata - # is exported *only* as a go_tool_library. This does not cause a problem, - # since there is guaranteed to be no conflict. However for consistency, - # we should not introduce new go_tool_library dependencies unless strictly - # necessary. - 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. - for embed in go_embed_libraries(ctx.rule): - info = embed[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. - binaries = target.files.to_list() - inputs = binaries + 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. - target_objfile, target_xfile, has_objfile = _select_objfile(binaries) - 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_file, _ = _select_objfile(info.binaries) - import_map[info.importpath] = x_file.path - 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 module itself, for the type sanity check. This applies only to - # the libraries, and not binaries or tests. - if has_objfile: - import_map[importpath] = target_xfile.path - - # 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._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") - 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.gotags, - 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) - args_file = ctx.actions.declare_file(ctx.label.name + "_args_file") - ctx.actions.write( - output = args_file, - content = "\n".join(go_ctx.nogo_args + [ - "-binary=%s" % target_objfile.path, - "-objdump_tool=%s" % ctx.files._objdump_tool[0].path, - "-package=%s" % config_file.path, - "-findings=%s" % raw_findings.path, - "-facts=%s" % facts.path, - ]), - ) - ctx.actions.run( - inputs = inputs + [args_file], - outputs = [facts, raw_findings], - tools = depset(go_ctx.runfiles.to_list() + ctx.files._objdump_tool), - executable = ctx.files._check[0], - mnemonic = "GoStaticAnalysis", - progress_message = "Analyzing %s" % target.label, - execution_requirements = {"supports-workers": "1"}, - arguments = [ - "--worker_debug=%s" % nogo_target_info.worker_debug, - "@%s" % args_file.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, - importpath = importpath, - binaries = binaries, - srcs = srcs, - deps = deps, - ), - ] - -nogo_aspect = go_rule( - aspect, - implementation = _nogo_aspect_impl, - attr_aspects = [ - "deps", - "library", - "embed", - ], - attrs = { - "_check": attr.label( - default = "//tools/nogo/check:check", - cfg = "host", - ), - "_objdump_tool": attr.label( - default = "//tools/nogo:objdump_tool", - cfg = "host", - ), - "_target": attr.label( - default = "//tools/nogo:target", - cfg = "target", - ), - # The name of this attribute must not be _stdlib, since that - # appears to be reserved for some internal bazel use. - "_nogo_stdlib": attr.label( - default = "//tools/nogo:stdlib", - cfg = "host", - ), - }, -) - -def _nogo_test_impl(ctx): - """Check nogo findings.""" - nogo_target_info = ctx.attr._target[NogoTargetInfo] - - # 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 - - # Build a step that applies the configuration. - config_srcs = ctx.attr.config[NogoConfigInfo].srcs - findings = ctx.actions.declare_file(ctx.label.name + ".findings") - args_file = ctx.actions.declare_file(ctx.label.name + "_args_file") - ctx.actions.write( - output = args_file, - content = "\n".join( - ["-input=%s" % f.path for f in raw_findings] + - ["-config=%s" % f.path for f in config_srcs] + - ["-output=%s" % findings.path], - ), - ) - ctx.actions.run( - inputs = raw_findings + ctx.files.srcs + config_srcs + [args_file], - outputs = [findings], - tools = depset(ctx.files._filter), - executable = ctx.files._filter[0], - mnemonic = "GoStaticAnalysis", - progress_message = "Generating %s" % ctx.label, - execution_requirements = {"supports-workers": "1"}, - arguments = [ - "--worker_debug=%s" % nogo_target_info.worker_debug, - "@%s" % args_file.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 -check -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]), - )] - -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.", - ), - "_target": attr.label( - default = "//tools/nogo:target", - cfg = "target", - ), - "_filter": attr.label(default = "//tools/nogo/filter:filter"), - }, - test = True, -) - -def _nogo_aspect_tricorder_impl(target, ctx): - if ctx.rule.kind != "nogo_test" or OutputGroupInfo not in target: - return [] - if not hasattr(target[OutputGroupInfo], "nogo_findings"): - return [] - return [ - OutputGroupInfo(tricorder = target[OutputGroupInfo].nogo_findings), - ] - -# Trivial aspect that forwards the findings from a nogo_test rule to -# go/tricorder, which reads from the `tricorder` output group. -nogo_aspect_tricorder = aspect( - implementation = _nogo_aspect_tricorder_impl, -) diff --git a/tools/nogo/filter/BUILD b/tools/nogo/filter/BUILD deleted file mode 100644 index e3049521e..000000000 --- a/tools/nogo/filter/BUILD +++ /dev/null @@ -1,15 +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", - "//tools/worker", - "@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 4a925d03c..000000000 --- a/tools/nogo/filter/main.go +++ /dev/null @@ -1,190 +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 filter is the filters and reports nogo findings. -package main - -import ( - "bytes" - "flag" - "fmt" - "io/ioutil" - "log" - "os" - "strings" - - yaml "gopkg.in/yaml.v2" - "gvisor.dev/gvisor/tools/nogo" - "gvisor.dev/gvisor/tools/worker" -) - -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 - check bool -) - -func init() { - flag.Var(&inputFiles, "input", "findings input files (gob format)") - flag.StringVar(&outputFile, "output", "", "findings output file (json format)") - flag.Var(&configFiles, "config", "findings configuration files") - flag.BoolVar(&showConfig, "show-config", false, "dump configuration only") - flag.BoolVar(&check, "check", false, "assume input is in json format") -} - -func main() { - worker.Work(run) -} - -var ( - cachedFindings = worker.NewCache("findings") // With nogo.FindingSet. - cachedFiltered = worker.NewCache("filtered") // With nogo.FindingSet. - cachedConfigs = worker.NewCache("configs") // With nogo.Config. - cachedFullConfigs = worker.NewCache("compiled") // With nogo.Config. -) - -func loadFindings(filename string) nogo.FindingSet { - return cachedFindings.Lookup([]string{filename}, func() worker.Sizer { - r, err := os.Open(filename) - if err != nil { - log.Fatalf("unable to open input %q: %v", filename, err) - } - inputFindings, err := nogo.ExtractFindingsFrom(r, check /* json */) - if err != nil { - log.Fatalf("unable to extract findings from %s: %v", filename, err) - } - return inputFindings - }).(nogo.FindingSet) -} - -func loadConfig(filename string) *nogo.Config { - return cachedConfigs.Lookup([]string{filename}, func() worker.Sizer { - content, err := ioutil.ReadFile(filename) - if err != nil { - log.Fatalf("unable to read %s: %v", filename, err) - } - var newConfig nogo.Config // For current file. - dec := yaml.NewDecoder(bytes.NewBuffer(content)) - dec.SetStrict(true) - if err := dec.Decode(&newConfig); err != nil { - log.Fatalf("unable to decode %s: %v", filename, err) - } - if showConfig { - content, err := yaml.Marshal(&newConfig) - if err != nil { - log.Fatalf("error marshalling config: %v", err) - } - fmt.Fprintf(os.Stdout, "Loaded configuration from %s:\n%s\n", filename, string(content)) - } - return &newConfig - }).(*nogo.Config) -} - -func loadConfigs(filenames []string) *nogo.Config { - return cachedFullConfigs.Lookup(filenames, func() worker.Sizer { - config := &nogo.Config{ - Global: make(nogo.AnalyzerConfig), - Analyzers: make(map[nogo.AnalyzerName]nogo.AnalyzerConfig), - } - for _, filename := range configFiles { - config.Merge(loadConfig(filename)) - if showConfig { - mergedBytes, err := yaml.Marshal(config) - if err != nil { - log.Fatalf("error marshalling config: %v", err) - } - fmt.Fprintf(os.Stdout, "Merged configuration:\n%s\n", string(mergedBytes)) - } - } - if err := config.Compile(); err != nil { - log.Fatalf("error compiling config: %v", err) - } - return config - }).(*nogo.Config) -} - -func run([]string) int { - // Open and merge all configuations. - config := loadConfigs(configFiles) - if showConfig { - return 0 - } - - // Load and filer available findings. - var filteredFindings []nogo.Finding - for _, filename := range inputFiles { - // Note that this applies a caching strategy to the filtered - // findings, because *this is by far the most expensive part of - // evaluation*. The set of findings is large and applying the - // configuration is complex. Therefore, we segment this cache - // on each individual raw findings input file and the - // configuration files. Note that this cache is keyed on all - // the configuration files and each individual raw findings, so - // is guaranteed to be safe. This allows us to reuse the same - // filter result many times over, because e.g. all standard - // library findings will be available to all packages. - filteredFindings = append(filteredFindings, - cachedFiltered.Lookup(append(configFiles, filename), func() worker.Sizer { - inputFindings := loadFindings(filename) - filteredFindings := make(nogo.FindingSet, 0, len(inputFindings)) - for _, finding := range inputFindings { - if ok := config.ShouldReport(finding); ok { - filteredFindings = append(filteredFindings, finding) - } - } - return filteredFindings - }).(nogo.FindingSet)...) - } - - // 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. - // - // Note that the output of the filter is always json, which is - // human readable and the format that is consumed by tricorder. - if outputFile != "" { - w, err := os.OpenFile(outputFile, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644) - if err != nil { - log.Fatalf("unable to open output file %q: %v", outputFile, err) - } - if err := nogo.WriteFindingsTo(w, filteredFindings, true /* json */); err != nil { - log.Fatalf("unable to write findings: %v", err) - } - return 0 - } - - // Treat the run as a test. - if len(filteredFindings) == 0 { - fmt.Fprintf(os.Stdout, "PASS\n") - return 0 - } - for _, finding := range filteredFindings { - fmt.Fprintf(os.Stdout, "%s\n", finding.String()) - } - return 1 -} diff --git a/tools/nogo/findings.go b/tools/nogo/findings.go deleted file mode 100644 index a73bf1a09..000000000 --- a/tools/nogo/findings.go +++ /dev/null @@ -1,127 +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" - "encoding/json" - "fmt" - "go/token" - "io" - "os" - "reflect" - "sort" -) - -// Finding is a single finding. -type Finding struct { - Category AnalyzerName - Position token.Position - Message string -} - -// findingSize is the size of the finding struct itself. -var findingSize = int64(reflect.TypeOf(Finding{}).Size()) - -// Size implements worker.Sizer.Size. -func (f *Finding) Size() int64 { - return int64(len(f.Category)) + int64(len(f.Message)) + findingSize -} - -// String implements fmt.Stringer.String. -func (f *Finding) String() string { - return fmt.Sprintf("%s: %s: %s", f.Category, f.Position.String(), f.Message) -} - -// FindingSet is a collection of findings. -type FindingSet []Finding - -// Size implmements worker.Sizer.Size. -func (fs FindingSet) Size() int64 { - size := int64(0) - for _, finding := range fs { - size += finding.Size() - } - return size -} - -// Sort sorts all findings. -func (fs FindingSet) Sort() { - sort.Slice(fs, func(i, j int) bool { - switch { - case fs[i].Position.Filename < fs[j].Position.Filename: - return true - case fs[i].Position.Filename > fs[j].Position.Filename: - return false - case fs[i].Position.Line < fs[j].Position.Line: - return true - case fs[i].Position.Line > fs[j].Position.Line: - return false - case fs[i].Position.Column < fs[j].Position.Column: - return true - case fs[i].Position.Column > fs[j].Position.Column: - return false - case fs[i].Category < fs[j].Category: - return true - case fs[i].Category > fs[j].Category: - return false - case fs[i].Message < fs[j].Message: - return true - case fs[i].Message > fs[j].Message: - return false - default: - return false - } - }) -} - -// WriteFindingsTo serializes findings. -func WriteFindingsTo(w io.Writer, findings FindingSet, asJSON bool) error { - // N.B. Sort all the findings in order to maximize cacheability. - findings.Sort() - if asJSON { - enc := json.NewEncoder(w) - return enc.Encode(findings) - } - enc := gob.NewEncoder(w) - return enc.Encode(findings) -} - -// ExtractFindingsFromFile loads findings from a file. -func ExtractFindingsFromFile(filename string, asJSON bool) (FindingSet, error) { - r, err := os.Open(filename) - if err != nil { - return nil, err - } - defer r.Close() - return ExtractFindingsFrom(r, asJSON) -} - -// ExtractFindingsFrom loads findings from an io.Reader. -func ExtractFindingsFrom(r io.Reader, asJSON bool) (findings FindingSet, err error) { - if asJSON { - dec := json.NewDecoder(r) - err = dec.Decode(&findings) - } else { - dec := gob.NewDecoder(r) - err = dec.Decode(&findings) - } - return findings, err -} - -func init() { - gob.Register((*Finding)(nil)) - gob.Register((*FindingSet)(nil)) -} diff --git a/tools/nogo/nogo.go b/tools/nogo/nogo.go deleted file mode 100644 index 2f88f84db..000000000 --- a/tools/nogo/nogo.go +++ /dev/null @@ -1,667 +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 ( - "bytes" - "encoding/gob" - "errors" - "fmt" - "go/ast" - "go/build" - "go/parser" - "go/token" - "go/types" - "io" - "io/ioutil" - "log" - "os" - "path" - "path/filepath" - "reflect" - "sort" - "strings" - - "golang.org/x/tools/go/analysis" - "golang.org/x/tools/go/analysis/internal/facts" - "golang.org/x/tools/go/gcexportdata" - "golang.org/x/tools/go/types/objectpath" - - // Special case: flags live here and change overall behavior. - "gvisor.dev/gvisor/tools/nogo/objdump" - "gvisor.dev/gvisor/tools/worker" -) - -// 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 - -// stdlibFact is used for serialiation. -type stdlibFact struct { - Package string - Facts []byte -} - -// stdlibFacts is a set of standard library facts. -type stdlibFacts map[string][]byte - -// Size implements worker.Sizer.Size. -func (sf stdlibFacts) Size() int64 { - size := int64(0) - for filename, data := range sf { - size += int64(len(filename)) - size += int64(len(data)) - } - return size -} - -// EncodeTo serializes stdlibFacts. -func (sf stdlibFacts) EncodeTo(w io.Writer) error { - stdlibFactsSorted := make([]stdlibFact, 0, len(sf)) - for pkg, facts := range sf { - stdlibFactsSorted = append(stdlibFactsSorted, stdlibFact{ - Package: pkg, - Facts: facts, - }) - } - sort.Slice(stdlibFactsSorted, func(i, j int) bool { - return stdlibFactsSorted[i].Package < stdlibFactsSorted[j].Package - }) - enc := gob.NewEncoder(w) - if err := enc.Encode(stdlibFactsSorted); err != nil { - return err - } - return nil -} - -// DecodeFrom deserializes stdlibFacts. -func (sf stdlibFacts) DecodeFrom(r io.Reader) error { - var stdlibFactsSorted []stdlibFact - dec := gob.NewDecoder(r) - if err := dec.Decode(&stdlibFactsSorted); err != nil { - return err - } - for _, stdlibFact := range stdlibFactsSorted { - sf[stdlibFact.Package] = stdlibFact.Facts - } - return nil -} - -var ( - // cachedFacts caches by file (just byte data). - cachedFacts = worker.NewCache("facts") - - // stdlibCachedFacts caches the standard library (stdlibFacts). - stdlibCachedFacts = worker.NewCache("stdlib") -) - -// factLoader loads facts. -func (c *PackageConfig) factLoader(path string) (data []byte, err error) { - filename, ok := c.FactMap[path] - if ok { - cb := cachedFacts.Lookup([]string{filename}, func() worker.Sizer { - data, readErr := ioutil.ReadFile(filename) - if readErr != nil { - err = fmt.Errorf("error loading %q: %w", filename, readErr) - return nil - } - return worker.CacheBytes(data) - }) - if cb != nil { - return []byte(cb.(worker.CacheBytes)), err - } - return nil, err - } - cb := stdlibCachedFacts.Lookup([]string{c.StdlibFacts}, func() worker.Sizer { - r, openErr := os.Open(c.StdlibFacts) - if openErr != nil { - err = fmt.Errorf("error loading stdlib facts from %q: %w", c.StdlibFacts, openErr) - return nil - } - defer r.Close() - sf := make(stdlibFacts) - if readErr := sf.DecodeFrom(r); readErr != nil { - err = fmt.Errorf("error loading stdlib facts: %w", readErr) - return nil - } - return sf - }) - if cb != nil { - return (cb.(stdlibFacts))[path], err - } - return nil, err -} - -// 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 - } - } - - // Check the cache. - if pkg, ok := i.cache[path]; ok && pkg.Complete() { - return pkg, nil - } - - // 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 FindingSet, 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 - } - - // Go standard library packages using Go 1.18 type parameter features. - // - // As of writing, analysis tooling is not updated to support type - // parameters and will choke on these packages. We skip these packages - // entirely for now. - // - // TODO(b/201686256): remove once tooling can handle type parameters. - usesTypeParams := map[string]struct{}{ - "constraints": struct{}{}, // golang.org/issue/45458 - "maps": struct{}{}, // golang.org/issue/47649 - "slices": struct{}{}, // golang.org/issue/45955 - } - - // 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 - } - - if _, ok := usesTypeParams[pkg]; ok { - log.Printf("WARNING: Skipping package %q: type param analysis not yet supported", pkg) - 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. - localStdlibFacts := make(stdlibFacts) - localStdlibErrs := make(map[string]error) - stdlibCachedFacts.Lookup([]string{""}, func() worker.Sizer { - return localStdlibFacts - }) - var checkOne func(pkg string) error // Recursive. - checkOne = func(pkg string) error { - // Is this already done? - if _, ok := localStdlibFacts[pkg]; ok { - return nil - } - // Did this fail previously? - if _, ok := localStdlibErrs[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. - localStdlibErrs[pkg] = err - return nil - } - - // Provide the input. - oldReader := objdump.Reader - objdump.Reader = rc // For analysis. - defer func() { - rc.Close() - objdump.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. - localStdlibErrs[pkg] = err - return nil - } - localStdlibFacts[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(localStdlibFacts) == 0 { - return nil, nil, fmt.Errorf("no stdlib facts found: misconfiguration?") - } - - // Write out all findings. - buf := bytes.NewBuffer(nil) - if err := localStdlibFacts.EncodeTo(buf); err != nil { - return nil, nil, fmt.Errorf("error serialized stdlib facts: %v", err) - } - - // Write out all errors. - for pkg, err := range localStdlibErrs { - log.Printf("WARNING: error while processing %v: %v", pkg, err) - } - - // Return all findings. - return allFindings, buf.Bytes(), nil -} - -// sanityCheckScope checks that all object in astTypes map to the correct -// objects in binaryTypes. Note that we don't check whether the sets are the -// same, we only care about the fidelity of objects in astTypes. -// -// When an inconsistency is identified, we record it in the astToBinaryMap. -// This allows us to dynamically replace facts and correct for the issue. The -// total number of mismatches is returned. -func sanityCheckScope(astScope *types.Scope, binaryTypes *types.Package, binaryScope *types.Scope, astToBinary map[types.Object]types.Object) error { - for _, x := range astScope.Names() { - fe := astScope.Lookup(x) - path, err := objectpath.For(fe) - if err != nil { - continue // Not an encoded object. - } - se, err := objectpath.Object(binaryTypes, path) - if err != nil { - continue // May be unused, see below. - } - if fe.Id() != se.Id() { - // These types are incompatible. This means that when - // this objectpath is loading from the binaryTypes (for - // dependencies) it will resolve to a fact for that - // type. We don't actually care about this error since - // we do the rewritten, but may as well alert. - log.Printf("WARNING: Object %s is a victim of go/issues/44195.", fe.Id()) - } - se = binaryScope.Lookup(x) - if se == nil { - // The fact may not be exported in the objectdata, if - // it is package internal. This is fine, as nothing out - // of this package can use these symbols. - continue - } - // Save the translation. - astToBinary[fe] = se - } - for i := 0; i < astScope.NumChildren(); i++ { - if err := sanityCheckScope(astScope.Child(i), binaryTypes, binaryScope, astToBinary); err != nil { - return err - } - } - return nil -} - -// sanityCheckTypes checks that two types are sane. The total number of -// mismatches is returned. -func sanityCheckTypes(astTypes, binaryTypes *types.Package, astToBinary map[types.Object]types.Object) error { - return sanityCheckScope(astTypes.Scope(), binaryTypes, binaryTypes.Scope(), astToBinary) -} - -// 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), - } - astTypes, 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 facts using the astTypes, although it may need reconciling - // later on. See the fact functions below. - astFacts, err := facts.Decode(astTypes, config.factLoader) - if err != nil { - return nil, nil, fmt.Errorf("error decoding facts: %w", err) - } - - // Sanity check all types and record metadata to prevent - // https://github.com/golang/go/issues/44195. - // - // This block loads the binary types, whose encoding will be well - // defined and aligned with any downstream consumers. Below in the fact - // functions for the analysis, we serialize types to both the astFacts - // and the binaryFacts if available. The binaryFacts are the final - // encoded facts in order to ensure compatibility. We keep the - // intermediate astTypes in order to allow exporting and importing - // within the local package under analysis. - var ( - astToBinary = make(map[types.Object]types.Object) - binaryFacts *facts.Set - ) - if _, ok := config.ImportMap[config.ImportPath]; ok { - binaryTypes, err := imp.Import(config.ImportPath) - if err != nil { - return nil, nil, fmt.Errorf("error loading self: %w", err) - } - if err := sanityCheckTypes(astTypes, binaryTypes, astToBinary); err != nil { - return nil, nil, fmt.Errorf("error sanity checking types: %w", err) - } - binaryFacts, err = facts.Decode(binaryTypes, config.factLoader) - 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. - localFactsFilter := make(map[reflect.Type]bool) - for _, f := range a.FactTypes { - localFactsFilter[reflect.TypeOf(f)] = true - } - p := &analysis.Pass{ - Analyzer: a, - Fset: imp.fset, - Files: syntax, - Pkg: astTypes, - 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: astFacts.ImportPackageFact, - ExportPackageFact: func(fact analysis.Fact) { - astFacts.ExportPackageFact(fact) - if binaryFacts != nil { - binaryFacts.ExportPackageFact(fact) - } - }, - ImportObjectFact: astFacts.ImportObjectFact, - ExportObjectFact: func(obj types.Object, fact analysis.Fact) { - astFacts.ExportObjectFact(obj, fact) - // Note that if no object is recorded in - // astToBinary and binaryFacts != nil, then the - // object doesn't appear in the exported data. - // It was likely an internal object to the - // package, and there is no meaningful - // downstream consumer of the fact. - if binaryObj, ok := astToBinary[obj]; ok && binaryFacts != nil { - binaryFacts.ExportObjectFact(binaryObj, fact) - } - }, - AllPackageFacts: func() []analysis.PackageFact { return astFacts.AllPackageFacts(localFactsFilter) }, - AllObjectFacts: func() []analysis.ObjectFact { return astFacts.AllObjectFacts(localFactsFilter) }, - 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. Note that we have a preference to returning the - // binary facts if available, so that downstream consumers of these - // facts will find the export aligns with the internal type details. - // See the block above with the call to sanityCheckTypes. - if binaryFacts != nil { - return findings, binaryFacts.Encode(), nil - } - return findings, astFacts.Encode(), nil -} - -func init() { - gob.Register((*stdlibFact)(nil)) -} diff --git a/tools/nogo/objdump/BUILD b/tools/nogo/objdump/BUILD deleted file mode 100644 index da56efdf7..000000000 --- a/tools/nogo/objdump/BUILD +++ /dev/null @@ -1,10 +0,0 @@ -load("//tools:defs.bzl", "go_library") - -package(licenses = ["notice"]) - -go_library( - name = "objdump", - srcs = ["objdump.go"], - nogo = False, - visibility = ["//tools:__subpackages__"], -) diff --git a/tools/nogo/objdump/objdump.go b/tools/nogo/objdump/objdump.go deleted file mode 100644 index 48484abf3..000000000 --- a/tools/nogo/objdump/objdump.go +++ /dev/null @@ -1,96 +0,0 @@ -// Copyright 2020 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 objdump is a wrapper around relevant objdump flags. -package objdump - -import ( - "flag" - "fmt" - "io" - "os" - "os/exec" -) - -var ( - // Binary is the binary under analysis. - // - // See Reader, below. - binary = flag.String("binary", "", "binary under analysis") - - // Reader is the input stream. - // - // This may be set instead of Binary. - Reader io.Reader - - // objdumpTool is the tool used to dump a binary. - objdumpTool = flag.String("objdump_tool", "", "tool used to dump a binary") -) - -// LoadRaw reads the raw object output. -func LoadRaw(fn func(r io.Reader) error) error { - var r io.Reader - if *binary != "" { - f, err := os.Open(*binary) - if err != nil { - return err - } - defer f.Close() - r = f - } else if Reader != nil { - r = Reader - } else { - // We have no input stream. - return fmt.Errorf("no binary or reader provided") - } - return fn(r) -} - -// Load reads the objdump output. -func Load(fn func(r io.Reader) error) error { - var ( - args []string - stdin io.Reader - ) - if *binary != "" { - args = append(args, *binary) - } else if Reader != nil { - stdin = Reader - } else { - // We have no input stream or binary. - return fmt.Errorf("no binary or reader provided") - } - - // Construct our command. - cmd := exec.Command(*objdumpTool, args...) - cmd.Stdin = stdin - cmd.Stderr = os.Stderr - out, err := cmd.StdoutPipe() - if err != nil { - return err - } - if err := cmd.Start(); err != nil { - return err - } - - // Call the user hook. - userErr := fn(out) - - // Wait for the dump to finish. - if err := cmd.Wait(); userErr == nil && err != nil { - return err - } - - return userErr -} |