Repository: VKCOM/noverify (go)

Issue:
Add dynamic plugins filters for variable names from phpgrep
Sometimes in dynamic rules we want to be able to specify a specific name or regular expression for a variable to match.

For example:

```php
function forbid_id_var() {
  /**
   * @maybe Not use $id variable
   * @filter id "^id$"
   */
  $id;
}
```


Pull request description:
added @filter for dynamic rules
Fixes #1179

Patch:
diff --git a/docs/dynamic_rules.md b/docs/dynamic_rules.md
index 2ea2bd8..5bfa56b 100644
--- a/docs/dynamic_rules.md
+++ b/docs/dynamic_rules.md
@@ -422,6 +422,26 @@ function ternarySimplify() {
 
 This rule will now don't apply to files with the `common/` folder in the path.
 
+##### `@filter`
+
+The `@filter` restriction allows you to restrict the rule by name of matched variable.
+
+Thus, the rule will be applied only if there is a matched variable that matches passed regexp.
+
+For example:
+
+```php
+function forbiddenIdUsage() {
+  /**
+   * @maybe Not use $id
+   * @filter $id ^id$
+   */
+  $id;
+}
+```
+
+This rule will match usage if `$id` variable.
+
 #### Underline location (`@location`)
 
 For every warning that NoVerify finds, it underlines the location. However, for dynamic rules, the right place is not always underlined.
diff --git a/src/linter/root.go b/src/linter/root.go
index d27157e..0a3be47 100644
--- a/src/linter/root.go
+++ b/src/linter/root.go
@@ -1823,6 +1823,13 @@ func (d *rootWalker) checkFilterSet(m *phpgrep.MatchData, sc *meta.Scope, filter
 		if filter.Pure && !solver.SideEffectFree(d.scope(), d.ctx.st, nil, nn) {
 			return false
 		}
+		if filter.Regexp != nil {
+			if vr, ok := nn.(*ir.SimpleVar); ok {
+				if filter.Regexp.MatchString(vr.Name) {
+					return true
+				}
+			}
+		}
 	}
 
 	return true
diff --git a/src/rules/parser.go b/src/rules/parser.go
index 986d746..3eb6fd3 100644
--- a/src/rules/parser.go
+++ b/src/rules/parser.go
@@ -291,6 +291,30 @@ func (p *parser) parseRuleInfo(st ir.Node, labelStmt ir.Node, proto *Rule) (Rule
 			filter := filterSet[name]
 			filter.Pure = true
 			filterSet[name] = filter
+		case "filter":
+			if len(part.Params) != 2 {
+				return rule, p.errorf(st, "@filter expects exactly 2 param, got %d", len(part.Params))
+			}
+			name := part.Params[0]
+			if !strings.HasPrefix(name, "$") {
+				return rule, p.errorf(st, "@filter param must be a phpgrep variable")
+			}
+			name = strings.TrimPrefix(name, "$")
+			found := p.checkForVariableInPattern(name, patternStmt, verifiedVars)
+			if !found {
+				return rule, p.errorf(st, "@filter contains a reference to a variable %s that is not present in the pattern", name)
+			}
+			if filterSet == nil {
+				filterSet = map[string]Filter{}
+			}
+			regexString := part.Params[1]
+			filter := filterSet[name]
+			regex, err := regexp.Compile(regexString)
+			if err != nil {
+				return rule, p.errorf(st, "@filter %s: can't compile regexp %s", regexString, err)
+			}
+			filter.Regexp = regex
+			filterSet[name] = filter
 
 		default:
 			return rule, p.errorf(st, "unknown attribute @%s on line %d", part.Name(), part.Line())
diff --git a/src/rules/rules.go b/src/rules/rules.go
index 29adaa3..5480585 100644
--- a/src/rules/rules.go
+++ b/src/rules/rules.go
@@ -2,6 +2,7 @@ package rules
 
 import (
 	"io"
+	"regexp"
 
 	"github.com/VKCOM/noverify/src/ir"
 	"github.com/VKCOM/noverify/src/phpdoc"
@@ -117,6 +118,7 @@ func (r *Rule) String() string {
 
 // Filter describes constraints that should be applied to a given phpgrep variable.
 type Filter struct {
-	Type *phpdoc.Type
-	Pure bool
+	Type   *phpdoc.Type
+	Pure   bool
+	Regexp *regexp.Regexp
 }


Test patch:
diff --git a/src/tests/rules/rules_test.go b/src/tests/rules/rules_test.go
index 95af846..1e32fcc 100644
--- a/src/tests/rules/rules_test.go
+++ b/src/tests/rules/rules_test.go
@@ -416,3 +416,29 @@ function bad(string $x) {
 	}
 	test.RunRulesTest()
 }
+
+func TestRulesFilter(t *testing.T) {
+	rfile := `<?php
+function id_check() {
+  /**
+   * @warning Don't use $id variable
+   * @filter $id ^id$
+   */
+  $id;
+}
+`
+	test := linttest.NewSuite(t)
+	test.RuleFile = rfile
+	test.AddFile(`<?php
+$id = 100;
+echo $id;
+echo $id == 0;
+if ($id == 0) {}
+`)
+	test.Expect = []string{
+		`Don't use $id variable`,
+		`Don't use $id variable`,
+		`Don't use $id variable`,
+	}
+	test.RunRulesTest()
+}