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 }