gvisor/tools/nogo/config.go

320 lines
8.4 KiB
Go

// Copyright 2019 The gVisor Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package 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
}