diff --git a/docs/dynamic_rules.md b/docs/dynamic_rules.md index 2ea2bd81..5bfa56b6 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 d27157e4..0a3be479 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 986d746e..3eb6fd3e 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 29adaa39..5480585b 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 } diff --git a/src/tests/rules/rules_test.go b/src/tests/rules/rules_test.go index 95af8463..1e32fcc6 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 := `