diff --git a/src/Rules/Regexp/RegularExpressionQuotingRule.php b/src/Rules/Regexp/RegularExpressionQuotingRule.php index 210b8d75187..546f47e44dd 100644 --- a/src/Rules/Regexp/RegularExpressionQuotingRule.php +++ b/src/Rules/Regexp/RegularExpressionQuotingRule.php @@ -3,6 +3,7 @@ namespace PHPStan\Rules\Regexp; use PhpParser\Node; +use PhpParser\Node\Expr\Array_; use PhpParser\Node\Expr\BinaryOp\Concat; use PhpParser\Node\Expr\FuncCall; use PhpParser\Node\Name; @@ -76,12 +77,42 @@ public function processNode(Node $node, Scope $scope): array if (!isset($normalizedArgs[0])) { return []; } - if (!$normalizedArgs[0]->value instanceof Concat) { + + $patternArg = $normalizedArgs[0]->value; + + // preg_replace, preg_replace_callback and preg_filter also take an array + // of patterns, which RegularExpressionPatternRule::extractPatterns() + // already reads. Each element is a pattern in its own right. + if ($patternArg instanceof Array_) { + if ( + !in_array($functionReflection->getName(), [ + 'preg_filter', + 'preg_replace', + 'preg_replace_callback', + ], true) + ) { + return []; + } + + $errors = []; + foreach ($patternArg->items as $item) { + if (!$item->value instanceof Concat) { + continue; + } + + $itemDelimiters = $this->regexExpressionHelper->getPatternDelimiters($item->value, $scope); + $errors = array_merge($errors, $this->validateQuoteDelimiters($item->value, $scope, $itemDelimiters)); + } + + return $errors; + } + + if (!$patternArg instanceof Concat) { return []; } - $patternDelimiters = $this->regexExpressionHelper->getPatternDelimiters($normalizedArgs[0]->value, $scope); - return $this->validateQuoteDelimiters($normalizedArgs[0]->value, $scope, $patternDelimiters); + $patternDelimiters = $this->regexExpressionHelper->getPatternDelimiters($patternArg, $scope); + return $this->validateQuoteDelimiters($patternArg, $scope, $patternDelimiters); } /** diff --git a/tests/PHPStan/Rules/Regexp/RegularExpressionQuotingRuleTest.php b/tests/PHPStan/Rules/Regexp/RegularExpressionQuotingRuleTest.php index 4d390107842..cdf1c76c73c 100644 --- a/tests/PHPStan/Rules/Regexp/RegularExpressionQuotingRuleTest.php +++ b/tests/PHPStan/Rules/Regexp/RegularExpressionQuotingRuleTest.php @@ -70,6 +70,22 @@ public function testRule(): void 'Call to preg_quote() is missing delimiter parameter to be effective.', 77, ], + [ + 'Call to preg_quote() uses invalid delimiter / while pattern uses &.', + 102, + ], + [ + 'Call to preg_quote() uses invalid delimiter / while pattern uses &.', + 103, + ], + [ + 'Call to preg_quote() uses invalid delimiter / while pattern uses &.', + 104, + ], + [ + 'Call to preg_quote() uses invalid delimiter / while pattern uses &.', + 105, + ], ], ); } diff --git a/tests/PHPStan/Rules/Regexp/data/preg-quote.php b/tests/PHPStan/Rules/Regexp/data/preg-quote.php index 5333d72f58a..d148ce39cd9 100644 --- a/tests/PHPStan/Rules/Regexp/data/preg-quote.php +++ b/tests/PHPStan/Rules/Regexp/data/preg-quote.php @@ -94,3 +94,16 @@ function ok(string $s): void { // ok function notAnalyzable(string $s): void { // ok preg_match($s. preg_quote('&oops') . 'pattern}', $s); } + +// preg_replace, preg_replace_callback and preg_filter accept an array of +// patterns. RegularExpressionPatternRule already reads that form. +function doFooArrayPatterns(string $s, callable $cb): void // errors +{ + preg_replace(['&' . preg_quote('&oops', '/') . 'pattern&'], 'x', $s); + preg_filter(['&' . preg_quote('&oops', '/') . 'pattern&'], 'x', $s); + preg_replace_callback(['&' . preg_quote('&oops', '/') . 'pattern&'], $cb, $s); + preg_replace(['key' => '&' . preg_quote('&oops', '/') . 'pattern&'], 'x', $s); + + // no error: delimiter matches + preg_replace(['&' . preg_quote('&oops', '&') . 'pattern&'], 'x', $s); +}