Repository: VKCOM/noverify
Pull request #1247 (2025-02-18 15:25:06)

linter: @filter regex support for strings


Patch:
diff --git a/docs/dynamic_rules.md b/docs/dynamic_rules.md
index 20e41c4..f967c40 100644
--- a/docs/dynamic_rules.md
+++ b/docs/dynamic_rules.md
@@ -510,6 +510,25 @@ function forbiddenIdUsage() {
 
 This rule will match usage if `$id` variable.
 
+Also, originally designed to apply to variables by matching their names, the functionality has been extended so that if a string literal is captured under the given name, the regular expression is applied to its literal value.
+
+When a string literal is captured, the regular expression is applied to the literal’s value.
+
+For example:
+
+```php
+function insecureUrl() {
+  /**
+   * @warning Use secure URLs
+   * @filter $url ^http://
+   */
+  callApi("http://example.com");
+}
+```
+
+In this case, the rule will match because the captured string literal "http://example.com" matches the regular expression ^http://.
+
+
 #### 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 10d69a3..b7fe6fa 100644
--- a/src/linter/root.go
+++ b/src/linter/root.go
@@ -1953,8 +1953,13 @@ func (d *rootWalker) checkFilterSet(m *phpgrep.MatchData, sc *meta.Scope, filter
 			return false
 		}
 		if filter.Regexp != nil {
-			if vr, ok := nn.(*ir.SimpleVar); ok {
-				if !filter.Regexp.MatchString(vr.Name) {
+			switch v := nn.(type) {
+			case *ir.SimpleVar:
+				if !filter.Regexp.MatchString(v.Name) {
+					return false
+				}
+			case *ir.String:
+				if !filter.Regexp.MatchString(v.Value) {
 					return false
 				}
 			}


Test patch:
diff --git a/src/tests/rules/rules_test.go b/src/tests/rules/rules_test.go
index 322bed4..a8c919d 100644
--- a/src/tests/rules/rules_test.go
+++ b/src/tests/rules/rules_test.go
@@ -733,6 +733,75 @@ function type_type_check(string $animal_name, int $animal_id) {
 	test.RunRulesTest()
 }
 
+func TestFilterLiteralNoWarning(t *testing.T) {
+	rfile := `<?php
+function literalEndpointSafe() {
+  /**
+   * @warning Literal endpoint must use HTTPS
+   * @filter $endpoint ^http://
+   */
+  callApi($endpoint);
+}
+`
+	test := linttest.NewSuite(t)
+	test.RuleFile = rfile
+	test.AddFile(`<?php
+function testLiteralSafe() {
+  callApi("https://secure.com");
+}
+`)
+
+	test.RunRulesTest()
+}
+
+func TestMultipleLiterals(t *testing.T) {
+	rfile := `<?php
+function checkEndpoint() {
+  /**
+   * @warning Endpoint must use HTTPS
+   * @filter $endpoint ^http://
+   */
+  callApi($endpoint);
+}
+`
+	test := linttest.NewSuite(t)
+	test.RuleFile = rfile
+	test.AddFile(`<?php
+function testMultipleEndpoints() {
+  callApi("http://example.com");
+  callApi("https://secure.com");
+  callApi("http://another.com");
+}
+`)
+	test.Expect = []string{
+		"Endpoint must use HTTPS",
+		"Endpoint must use HTTPS",
+	}
+	test.RunRulesTest()
+}
+
+func TestFilterVariableNoWarning(t *testing.T) {
+	rfile := `<?php
+function variableEndpointSafe() {
+  /**
+   * @warning Variable endpoint must be renamed
+   * @filter $endpoint ^endpoint$
+   */
+  callApi($endpoint);
+}
+`
+	test := linttest.NewSuite(t)
+	test.RuleFile = rfile
+	test.AddFile(`<?php
+function testVariableSafe() {
+  $safeEndpoint = "http://example.com";
+  callApi($safeEndpoint);
+}
+`)
+
+	test.RunRulesTest()
+}
+
 func TestRulePathExcludePositive(t *testing.T) {
 	rfile := `<?php
 /**