Repository: VKCOM/noverify
Pull request #1224 (2024-07-11 14:12:10)

dinamic rules: multiple path
More about feature [here](https://github.com/VKCOM/noverify/issues/1208).


Patch:
diff --git a/src/linter/utils.go b/src/linter/utils.go
index f673fdc..bd56092 100644
--- a/src/linter/utils.go
+++ b/src/linter/utils.go
@@ -68,15 +68,15 @@ func mergeTypeMaps(left types.Map, right types.Map) types.Map {
 // The types are inferred as follows:
 // 1. If there is a @return annotation, then its value becomes the return type;
 //
-// 2. If there is a type hint, then it is added to the types from the @return.
-//    If the @return is empty, then the type matches the type hint itself;
+//  2. If there is a type hint, then it is added to the types from the @return.
+//     If the @return is empty, then the type matches the type hint itself;
 //
-// 3. If the resulting type is mixed[], then if the actual type is a specific
-//    array type, then we use it, otherwise we combine this type with the
-//    resulting mixed[] type.
+//  3. If the resulting type is mixed[], then if the actual type is a specific
+//     array type, then we use it, otherwise we combine this type with the
+//     resulting mixed[] type.
 //
-// 4. If there is no @return annotation and type hint, then the return type is equal to
-//    the union of the types that are returned from the function by return.
+//  4. If there is no @return annotation and type hint, then the return type is equal to
+//     the union of the types that are returned from the function by return.
 func functionReturnType(phpdocReturnType types.Map, hintReturnType types.Map, actualReturnTypes types.Map) types.Map {
 	var returnTypes types.Map
 	if !phpdocReturnType.Empty() || !hintReturnType.Empty() {
@@ -494,10 +494,21 @@ func cloneRulesForFile(filename string, ruleSet *rules.ScopedSet) *rules.ScopedS
 	for kind, ruleByKind := range &ruleSet.RulesByKind {
 		res := make([]rules.Rule, 0, len(ruleByKind))
 		for _, rule := range ruleByKind {
-			if !strings.Contains(filename, rule.Path) || isFilePathExcluded(filename, rule) {
+			if isFilePathExcluded(filename, rule) {
 				continue
 			}
-			res = append(res, rule)
+
+			if rule.Paths == nil {
+				res = append(res, rule)
+				continue
+			}
+
+			for _, path := range rule.Paths {
+				if strings.Contains(filename, path) {
+					res = append(res, rule)
+					break
+				}
+			}
 		}
 		clone.Set(ir.NodeKind(kind), res)
 	}
diff --git a/src/rules/parser.go b/src/rules/parser.go
index 0e9e9be..ab7e0b8 100644
--- a/src/rules/parser.go
+++ b/src/rules/parser.go
@@ -141,7 +141,7 @@ func (p *parser) parseRuleInfo(st ir.Node, labelStmt ir.Node, proto *Rule) (Rule
 		rule.Level = proto.Level
 		rule.Message = proto.Message
 		rule.Location = proto.Location
-		rule.Path = proto.Path
+		rule.Paths = proto.Paths
 		rule.PathExcludes = proto.PathExcludes
 
 		rule.Filters = make([]map[string]Filter, len(proto.Filters))
@@ -232,10 +232,12 @@ func (p *parser) parseRuleInfo(st ir.Node, labelStmt ir.Node, proto *Rule) (Rule
 			if len(part.Params) != 1 {
 				return rule, p.errorf(st, "@path expects exactly 1 param, got %d", len(part.Params))
 			}
-			if rule.Path != "" {
-				return rule, p.errorf(st, "duplicate @path constraint")
+
+			if rule.Paths == nil {
+				rule.Paths = make([]string, 0)
 			}
-			rule.Path = part.Params[0]
+
+			rule.Paths = append(rule.Paths, part.Params...)
 		case "path-exclude":
 			if len(part.Params) != 1 {
 				return rule, p.errorf(st, "@exclude expects exactly 1 param, got %d", len(part.Params))
diff --git a/src/rules/rules.go b/src/rules/rules.go
index 5480585..8bed8f9 100644
--- a/src/rules/rules.go
+++ b/src/rules/rules.go
@@ -98,7 +98,7 @@ type Rule struct {
 
 	// Path is a filter-like rule switcher.
 	// A rule is only applied to a file that contains a Path as a substring in its name.
-	Path string
+	Paths []string
 
 	// PathExcludes is a filter-like rule switcher.
 	// A rule is not applied to a file that contains a PathExcludes as a substring in its name.
diff --git a/src/rules/util.go b/src/rules/util.go
index af84c00..4251e4d 100644
--- a/src/rules/util.go
+++ b/src/rules/util.go
@@ -20,8 +20,8 @@ func formatRule(r *Rule) string {
 		buf.WriteString(" * @maybe " + r.Message + "\n")
 	}
 
-	if r.Path != "" {
-		buf.WriteString(" * @path " + r.Path + "\n")
+	for _, path := range r.Paths {
+		buf.WriteString(" * @path " + path + "\n")
 	}
 
 	if r.PathExcludes != nil {


Test patch:
diff --git a/src/tests/rules/rules_error_test.go b/src/tests/rules/rules_error_test.go
index a2c507c..151d797 100644
--- a/src/tests/rules/rules_error_test.go
+++ b/src/tests/rules/rules_error_test.go
@@ -132,19 +132,6 @@ $_ = foo();
 `,
 			expect: "<test>:7: @path expects exactly 1 param, got 2",
 		},
-		{
-			name: `DuplicatedPath`,
-			rule: `<?php
-/**
- * @name Some
- * @maybe Some
- * @path hell;
- * @path earth;
- */
-$_ = foo();
-`,
-			expect: "<test>:8: duplicate @path constraint",
-		},
 		{
 			name: `PathExpectsExactlyTwoParam`,
 			rule: `<?php
diff --git a/src/tests/rules/rules_test.go b/src/tests/rules/rules_test.go
index cb86333..b3df058 100644
--- a/src/tests/rules/rules_test.go
+++ b/src/tests/rules/rules_test.go
@@ -105,6 +105,36 @@ eval(${"var"});
 	test.RunRulesTest()
 }
 
+func TestRuleMultiPathFilter(t *testing.T) {
+	rfile := `<?php
+/**
+ * @name varEval
+ * @warning don't eval from variable
+ * @path my/site/ads_
+ * @path my/site/admin_
+ */
+eval(${"var"});
+`
+	test := linttest.NewSuite(t)
+	test.RuleFile = rfile
+	code := `<?php
+          $hello = 'echo 123;';
+          eval($hello);
+          eval('echo 456;');
+        `
+	test.AddNamedFile("/home/john/my/site/foo.php", code)
+	test.AddNamedFile("/home/john/my/site/ads_foo.php", code)
+	test.AddNamedFile("/home/john/my/site/ads_bar.php", code)
+	test.AddNamedFile("/home/john/my/site/admin_table.php", code)
+
+	test.Expect = []string{
+		`don't eval from variable`,
+		`don't eval from variable`,
+		`don't eval from variable`,
+	}
+	test.RunRulesTest()
+}
+
 func TestAnyRules(t *testing.T) {
 	rfile := `<?php
 /**