gvisor.dev/gvisor@v0.0.0-20240520182842-f9d4d51c7e0f/tools/nogo/config/config.go (about)

     1  // Copyright 2019 The gVisor Authors.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  // Package config defines a filter configuration for nogo findings.
    16  package config
    17  
    18  import (
    19  	"fmt"
    20  	"regexp"
    21  
    22  	"gvisor.dev/gvisor/tools/nogo/check"
    23  )
    24  
    25  // GroupName is a named group.
    26  type GroupName string
    27  
    28  // Group represents a named collection of files.
    29  type Group struct {
    30  	// Name is the short name for the group.
    31  	Name GroupName `yaml:"name"`
    32  
    33  	// Regex matches all full paths in the group.
    34  	Regex string         `yaml:"regex"`
    35  	regex *regexp.Regexp `yaml:"-"`
    36  
    37  	// Default determines the default group behavior.
    38  	//
    39  	// If Default is true, all Analyzers are enabled for this
    40  	// group. Otherwise, Analyzers must be individually enabled
    41  	// by specifying a (possible empty) ItemConfig for the group
    42  	// in the AnalyzerConfig.
    43  	Default bool `yaml:"default"`
    44  }
    45  
    46  func (g *Group) compile() error {
    47  	r, err := regexp.Compile(g.Regex)
    48  	if err != nil {
    49  		return err
    50  	}
    51  	g.regex = r
    52  	return nil
    53  }
    54  
    55  // ItemConfig is an (Analyzer,Group) configuration.
    56  type ItemConfig struct {
    57  	// Exclude are analyzer exclusions.
    58  	//
    59  	// Exclude is a list of regular expressions. If the corresponding
    60  	// Analyzer emits a Finding for which Finding.Position.String()
    61  	// matches a regular expression in Exclude, the finding will not
    62  	// be reported.
    63  	Exclude []string         `yaml:"exclude,omitempty"`
    64  	exclude []*regexp.Regexp `yaml:"-"`
    65  
    66  	// Suppress are analyzer suppressions.
    67  	//
    68  	// Suppress is a list of regular expressions. If the corresponding
    69  	// Analyzer emits a Finding for which Finding.Message matches a regular
    70  	// expression in Suppress, the finding will not be reported.
    71  	Suppress []string         `yaml:"suppress,omitempty"`
    72  	suppress []*regexp.Regexp `yaml:"-"`
    73  }
    74  
    75  func compileRegexps(ss []string, rs *[]*regexp.Regexp) error {
    76  	*rs = make([]*regexp.Regexp, len(ss))
    77  	for i, s := range ss {
    78  		r, err := regexp.Compile(s)
    79  		if err != nil {
    80  			return err
    81  		}
    82  		(*rs)[i] = r
    83  	}
    84  	return nil
    85  }
    86  
    87  // RegexpCount is used by AnalyzerConfig.RegexpCount.
    88  func (i *ItemConfig) RegexpCount() int64 {
    89  	if i == nil {
    90  		// See compile.
    91  		return 0
    92  	}
    93  	// Return the number of regular expressions compiled for these items.
    94  	// This is how the cache size of the configuration is measured.
    95  	return int64(len(i.exclude) + len(i.suppress))
    96  }
    97  
    98  func (i *ItemConfig) compile() error {
    99  	if i == nil {
   100  		// This may be nil if nothing is included in the
   101  		// item configuration. That's fine, there's nothing
   102  		// to compile and nothing to exclude & suppress.
   103  		return nil
   104  	}
   105  	if err := compileRegexps(i.Exclude, &i.exclude); err != nil {
   106  		return fmt.Errorf("in exclude: %w", err)
   107  	}
   108  	if err := compileRegexps(i.Suppress, &i.suppress); err != nil {
   109  		return fmt.Errorf("in suppress: %w", err)
   110  	}
   111  	return nil
   112  }
   113  
   114  func merge(a, b []string) []string {
   115  	found := make(map[string]struct{})
   116  	result := make([]string, 0, len(a)+len(b))
   117  	for _, elem := range a {
   118  		found[elem] = struct{}{}
   119  		result = append(result, elem)
   120  	}
   121  	for _, elem := range b {
   122  		if _, ok := found[elem]; ok {
   123  			continue
   124  		}
   125  		result = append(result, elem)
   126  	}
   127  	return result
   128  }
   129  
   130  func (i *ItemConfig) merge(other *ItemConfig) {
   131  	i.Exclude = merge(i.Exclude, other.Exclude)
   132  	i.Suppress = merge(i.Suppress, other.Suppress)
   133  }
   134  
   135  func (i *ItemConfig) shouldReport(fullPos, msg string) bool {
   136  	if i == nil {
   137  		// See above.
   138  		return true
   139  	}
   140  	for _, r := range i.exclude {
   141  		if r.MatchString(fullPos) {
   142  			return false
   143  		}
   144  	}
   145  	for _, r := range i.suppress {
   146  		if r.MatchString(msg) {
   147  			return false
   148  		}
   149  	}
   150  	return true
   151  }
   152  
   153  // AnalyzerConfig is the configuration for a single analyzers.
   154  //
   155  // This map is keyed by individual Group names, to allow for different
   156  // configurations depending on what Group the file belongs to.
   157  type AnalyzerConfig map[GroupName]*ItemConfig
   158  
   159  // RegexpCount is used by Config.Size.
   160  func (a AnalyzerConfig) RegexpCount() int64 {
   161  	count := int64(0)
   162  	for _, gc := range a {
   163  		count += gc.RegexpCount()
   164  	}
   165  	return count
   166  }
   167  
   168  func (a AnalyzerConfig) compile() error {
   169  	for name, gc := range a {
   170  		if err := gc.compile(); err != nil {
   171  			return fmt.Errorf("invalid group %q: %v", name, err)
   172  		}
   173  	}
   174  	return nil
   175  }
   176  
   177  func (a AnalyzerConfig) merge(other AnalyzerConfig) {
   178  	// Merge all the groups.
   179  	for name, gc := range other {
   180  		old, ok := a[name]
   181  		if !ok || old == nil {
   182  			a[name] = gc // Not configured in a.
   183  			continue
   184  		}
   185  		old.merge(gc)
   186  	}
   187  }
   188  
   189  // shouldReport returns whether the finding should be reported or suppressed.
   190  // It returns !ok if there is no configuration sufficient to decide one way or
   191  // another.
   192  func (a AnalyzerConfig) shouldReport(groupConfig *Group, fullPos, msg string) (report, ok bool) {
   193  	gc, ok := a[groupConfig.Name]
   194  	if !ok {
   195  		return false, false
   196  	}
   197  
   198  	// Note that if a section appears for a particular group
   199  	// for a particular analyzer, then it will now be enabled,
   200  	// and the group default no longer applies.
   201  	return gc.shouldReport(fullPos, msg), true
   202  }
   203  
   204  // Config is a nogo configuration.
   205  type Config struct {
   206  	// Prefixes defines a set of regular expressions that
   207  	// are standard "prefixes", so that files can be grouped
   208  	// and specific rules applied to individual groups.
   209  	Groups []Group `yaml:"groups"`
   210  
   211  	// Global is the global analyzer config.
   212  	Global AnalyzerConfig `yaml:"global"`
   213  
   214  	// Analyzers are individual analyzer configurations. The
   215  	// key for each analyzer is the name of the analyzer. The
   216  	// value is either a boolean (enable/disable), or a map to
   217  	// the groups above.
   218  	Analyzers map[string]AnalyzerConfig `yaml:"analyzers"`
   219  }
   220  
   221  // Merge merges two configurations.
   222  func (c *Config) Merge(other *Config) {
   223  	// Merge all groups.
   224  	//
   225  	// Select the other first, as the order provided in the second will
   226  	// provide precendence over the same group defined in the first one.
   227  	seenGroups := make(map[GroupName]struct{})
   228  	newGroups := make([]Group, 0, len(c.Groups)+len(other.Groups))
   229  	for _, g := range other.Groups {
   230  		newGroups = append(newGroups, g)
   231  		seenGroups[g.Name] = struct{}{}
   232  	}
   233  	for _, g := range c.Groups {
   234  		if _, ok := seenGroups[g.Name]; ok {
   235  			continue
   236  		}
   237  		newGroups = append(newGroups, g)
   238  	}
   239  	c.Groups = newGroups
   240  
   241  	// Merge global configurations.
   242  	c.Global.merge(other.Global)
   243  
   244  	// Merge all analyzer configurations.
   245  	for name, ac := range other.Analyzers {
   246  		old, ok := c.Analyzers[name]
   247  		if !ok {
   248  			c.Analyzers[name] = ac // No analyzer in original config.
   249  			continue
   250  		}
   251  		old.merge(ac)
   252  	}
   253  }
   254  
   255  // Compile compiles a configuration to make it useable.
   256  func (c *Config) Compile() error {
   257  	for i := 0; i < len(c.Groups); i++ {
   258  		if err := c.Groups[i].compile(); err != nil {
   259  			return fmt.Errorf("invalid group %q: %w", c.Groups[i].Name, err)
   260  		}
   261  	}
   262  	if err := c.Global.compile(); err != nil {
   263  		return fmt.Errorf("invalid global: %w", err)
   264  	}
   265  	for name, ac := range c.Analyzers {
   266  		if err := ac.compile(); err != nil {
   267  			return fmt.Errorf("invalid analyzer %q: %w", name, err)
   268  		}
   269  	}
   270  	return nil
   271  }
   272  
   273  // ShouldReport returns true iff the finding should match the Config.
   274  func (c *Config) ShouldReport(finding check.Finding) bool {
   275  	fullPos := finding.Position.String()
   276  
   277  	// Find the matching group.
   278  	var groupConfig *Group
   279  	for i := 0; i < len(c.Groups); i++ {
   280  		if c.Groups[i].regex.MatchString(fullPos) {
   281  			groupConfig = &c.Groups[i]
   282  			break
   283  		}
   284  	}
   285  
   286  	// If there is no group matching this path, then
   287  	// we default to accept the finding.
   288  	if groupConfig == nil {
   289  		return true
   290  	}
   291  
   292  	// Suppress via global rule?
   293  	report, ok := c.Global.shouldReport(groupConfig, fullPos, finding.Message)
   294  	if ok && !report {
   295  		return false
   296  	}
   297  
   298  	// Try the analyzer config.
   299  	ac, ok := c.Analyzers[finding.Category]
   300  	if !ok {
   301  		return groupConfig.Default
   302  	}
   303  	report, ok = ac.shouldReport(groupConfig, fullPos, finding.Message)
   304  	if !ok {
   305  		return groupConfig.Default
   306  	}
   307  	return report
   308  }