From 462abfbf3062fb3a841655697fc5d05360a32052 Mon Sep 17 00:00:00 2001 From: Richardas Kuchinskas <36065987+Hidanio@users.noreply.github.com> Date: Thu, 26 Dec 2024 17:14:49 +0300 Subject: [PATCH] linter: base system for on path rule filtering (#1237) --- src/linter/conf.go | 56 ++++ src/linter/root.go | 36 +++ src/tests/infra/pathRulesSet_config_test.go | 285 ++++++++++++++++++++ 3 files changed, 377 insertions(+) create mode 100644 src/tests/infra/pathRulesSet_config_test.go diff --git a/src/linter/conf.go b/src/linter/conf.go index 9055f2ea6..d720736fd 100644 --- a/src/linter/conf.go +++ b/src/linter/conf.go @@ -1,8 +1,10 @@ package linter import ( + "path/filepath" "regexp" "runtime" + "strings" "time" "github.com/VKCOM/php-parser/pkg/version" @@ -38,8 +40,13 @@ type Config struct { // Rules is a set of dynamically loaded linter diagnostics. Rules *rules.Set + // PathRules is a set of specific rules for paths. + PathRules *RuleNode + // settings + ProjectPath string + StubsDir string Debug bool @@ -68,6 +75,54 @@ type Config struct { StrictMixed bool } +type RuleNode struct { + Children map[string]*RuleNode // Child nodes (subdirectories and files) + Enabled map[string]bool // Rules enabled at this level + Disabled map[string]bool // Disabled rules at this level +} + +func NewRuleNode() *RuleNode { + return &RuleNode{ + Children: make(map[string]*RuleNode), + Enabled: make(map[string]bool), + Disabled: make(map[string]bool), + } +} + +type PathRuleSet struct { + Enabled map[string]bool // Rules that are enabled for this path + Disabled map[string]bool // Rules that are disabled for this path +} + +func BuildRuleTree(pathRules map[string]*PathRuleSet) *RuleNode { + root := NewRuleNode() + + for path, ruleSet := range pathRules { + normalizedPath := filepath.ToSlash(filepath.Clean(path)) + parts := strings.Split(normalizedPath, "/") + currentNode := root + + for _, part := range parts { + if part == "" { + continue + } + if _, exists := currentNode.Children[part]; !exists { + currentNode.Children[part] = NewRuleNode() + } + currentNode = currentNode.Children[part] + } + + for rule := range ruleSet.Enabled { + currentNode.Enabled[rule] = true + } + for rule := range ruleSet.Disabled { + currentNode.Disabled[rule] = true + } + } + + return root +} + func NewConfig(ver string) *Config { reg := &CheckersRegistry{ info: map[string]CheckerInfo{}, @@ -79,6 +134,7 @@ func NewConfig(ver string) *Config { return &Config{ SrcInput: inputs.NewDefaultSourceInput(), Rules: rules.NewSet(), + PathRules: NewRuleNode(), MaxConcurrency: runtime.NumCPU(), IsDiscardVar: isUnderscore, Checkers: reg, diff --git a/src/linter/root.go b/src/linter/root.go index 1f3c4ae53..8f36140ee 100644 --- a/src/linter/root.go +++ b/src/linter/root.go @@ -1518,6 +1518,34 @@ func (d *rootWalker) ReportPHPDoc(phpDocLocation PHPDocLocation, level int, chec d.ReportLocation(loc, level, checkName, msg, args...) } +func IsRuleEnabledForPath(root *RuleNode, filePath string, checkRule string) bool { + normalizedPath := filepath.ToSlash(filepath.Clean(filePath)) + parts := strings.Split(normalizedPath, "/") + currentNode := root + + // Starting with global state. We have guarantee while parsing config that rule is `on` and exist + ruleState := true + + for _, part := range parts { + if part == "" { + continue + } + if node, exists := currentNode.Children[part]; exists { + if node.Disabled[checkRule] { + ruleState = false // Disable on this path + } + if node.Enabled[checkRule] { + ruleState = true // Enable on this path + } + currentNode = node + } else { + break + } + } + + return ruleState +} + func (d *rootWalker) Report(n ir.Node, level int, checkName, msg string, args ...interface{}) { var pos position.Position @@ -1606,6 +1634,14 @@ func (d *rootWalker) ReportLocation(loc ir.Location, level int, checkName, msg s } } + filePath := d.file.Name() + + rootNode := d.config.PathRules + + if !IsRuleEnabledForPath(rootNode, filePath, checkName) { + return + } + d.reports = append(d.reports, &Report{ CheckName: checkName, Context: string(contextLine), diff --git a/src/tests/infra/pathRulesSet_config_test.go b/src/tests/infra/pathRulesSet_config_test.go new file mode 100644 index 000000000..df027b21c --- /dev/null +++ b/src/tests/infra/pathRulesSet_config_test.go @@ -0,0 +1,285 @@ +package checkers + +import ( + "testing" + + "github.com/VKCOM/noverify/src/linter" + "github.com/VKCOM/noverify/src/linttest" +) + +func pathRulesSetInit(t *testing.T) *linttest.Suite { + t.Helper() + linterConfig := linter.NewConfig("8.1") + linterConfig.ProjectPath = "dev/" + linterConfig.StrictMixed = true + + linterConfig.PathRules = linter.BuildRuleTree(map[string]*linter.PathRuleSet{ + "disable-emptyStmt": { + Enabled: map[string]bool{}, + Disabled: map[string]bool{"emptyStmt": true}, + }, + "enable-emptyStmt": { + Enabled: map[string]bool{ + "emptyStmt": true, + }, + Disabled: map[string]bool{}, + }, + "mixed/foo.php": { + Enabled: map[string]bool{ + "emptyStmt": true, + }, + Disabled: map[string]bool{ + "undefinedFunction": true, + }, + }, + }) + + var suite = linttest.NewSuite(t) + suite.IgnoreUndeclaredChecks() + suite.UseConfig(linterConfig) + return suite +} + +func TestDisablePath(t *testing.T) { + test := pathRulesSetInit(t) + code := `