Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions conf/config.neon
Original file line number Diff line number Diff line change
Expand Up @@ -1491,6 +1491,9 @@ services:
-
class: PHPStan\Type\Php\RegexArrayShapeMatcher

-
class: PHPStan\Type\Php\RegexExpressionHelper

-
class: PHPStan\Type\Php\ReflectionClassConstructorThrowTypeExtension
tags:
Expand Down
10 changes: 10 additions & 0 deletions src/Rules/Regexp/RegularExpressionPatternRule.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
use PHPStan\Analyser\Scope;
use PHPStan\Rules\Rule;
use PHPStan\Rules\RuleErrorBuilder;
use PHPStan\Type\Php\RegexExpressionHelper;
use function in_array;
use function sprintf;
use function str_starts_with;
Expand All @@ -20,6 +21,12 @@
class RegularExpressionPatternRule implements Rule
{

public function __construct(
private RegexExpressionHelper $regexExpressionHelper,
)
{
}

public function getNodeType(): string
{
return FuncCall::class;
Expand Down Expand Up @@ -74,6 +81,9 @@ private function extractPatterns(FuncCall $functionCall, Scope $scope): array
'preg_filter',
], true)
) {
if ($patternNode instanceof Node\Expr\BinaryOp\Concat) {
$patternType = $this->regexExpressionHelper->resolvePatternConcat($patternNode, $scope);
}
foreach ($patternType->getConstantStrings() as $constantStringType) {
$patternStrings[] = $constantStringType->getValue();
}
Expand Down
53 changes: 2 additions & 51 deletions src/Type/Php/RegexArrayShapeMatcher.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,8 @@
use Nette\Utils\RegexpException;
use Nette\Utils\Strings;
use PhpParser\Node\Expr;
use PhpParser\Node\Name;
use PHPStan\Analyser\Scope;
use PHPStan\Php\PhpVersion;
use PHPStan\Reflection\InitializerExprTypeResolver;
use PHPStan\ShouldNotHappenException;
use PHPStan\TrinaryLogic;
use PHPStan\Type\Accessory\AccessoryNonEmptyStringType;
Expand Down Expand Up @@ -56,7 +54,7 @@ final class RegexArrayShapeMatcher

public function __construct(
private PhpVersion $phpVersion,
private InitializerExprTypeResolver $initializerExprTypeResolver,
private RegexExpressionHelper $regexExpressionHelper,
)
{
}
Expand Down Expand Up @@ -712,57 +710,10 @@ private function getLiteralValue(TreeNode $node): ?string
private function getPatternType(Expr $patternExpr, Scope $scope): Type
{
if ($patternExpr instanceof Expr\BinaryOp\Concat) {
return $this->resolvePatternConcat($patternExpr, $scope);
return $this->regexExpressionHelper->resolvePatternConcat($patternExpr, $scope);
}

return $scope->getType($patternExpr);
}

/**
* Ignores preg_quote() calls in the concatenation as these are not relevant for array-shape matching.
*
* This assumption only works for the ArrayShapeMatcher therefore it is not implemented for the common case in Scope.
*
* see https://github.com/phpstan/phpstan-src/pull/3233#discussion_r1676938085
*/
private function resolvePatternConcat(Expr\BinaryOp\Concat $concat, Scope $scope): Type
{
$resolver = new class($scope) {

public function __construct(private Scope $scope)
{
}

public function resolve(Expr $expr): Type
{
if (
$expr instanceof Expr\FuncCall
&& $expr->name instanceof Name
&& $expr->name->toLowerString() === 'preg_quote'
) {
return new ConstantStringType('');
}

if ($expr instanceof Expr\BinaryOp\Concat) {
$left = $this->resolve($expr->left);
$right = $this->resolve($expr->right);

$strings = [];
foreach ($left->toString()->getConstantStrings() as $leftString) {
foreach ($right->toString()->getConstantStrings() as $rightString) {
$strings[] = new ConstantStringType($leftString->getValue() . $rightString->getValue());
}
}

return TypeCombinator::union(...$strings);
}

return $this->scope->getType($expr);
}

};

return $this->initializerExprTypeResolver->getConcatType($concat->left, $concat->right, static fn (Expr $expr): Type => $resolver->resolve($expr));
}

}
69 changes: 69 additions & 0 deletions src/Type/Php/RegexExpressionHelper.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
<?php declare(strict_types = 1);

namespace PHPStan\Type\Php;

use PhpParser\Node\Expr;
use PhpParser\Node\Name;
use PHPStan\Analyser\Scope;
use PHPStan\Reflection\InitializerExprTypeResolver;
use PHPStan\Type\Constant\ConstantStringType;
use PHPStan\Type\Type;
use PHPStan\Type\TypeCombinator;

final class RegexExpressionHelper
{

public function __construct(
private InitializerExprTypeResolver $initializerExprTypeResolver,
)
{
}

/**
* Ignores preg_quote() calls in the concatenation as these are not relevant for array-shape matching.
*
* This assumption only works for the ArrayShapeMatcher therefore it is not implemented for the common case in Scope.
*
* see https://github.com/phpstan/phpstan-src/pull/3233#discussion_r1676938085
*/
public function resolvePatternConcat(Expr\BinaryOp\Concat $concat, Scope $scope): Type
{
$resolver = new class($scope) {

public function __construct(private Scope $scope)
{
}

public function resolve(Expr $expr): Type
{
if (
$expr instanceof Expr\FuncCall
&& $expr->name instanceof Name
&& $expr->name->toLowerString() === 'preg_quote'
) {
return new ConstantStringType('');
}

if ($expr instanceof Expr\BinaryOp\Concat) {
$left = $this->resolve($expr->left);
$right = $this->resolve($expr->right);

$strings = [];
foreach ($left->toString()->getConstantStrings() as $leftString) {
foreach ($right->toString()->getConstantStrings() as $rightString) {
$strings[] = new ConstantStringType($leftString->getValue() . $rightString->getValue());
}
}

return TypeCombinator::union(...$strings);
}

return $this->scope->getType($expr);
}

};

return $this->initializerExprTypeResolver->getConcatType($concat->left, $concat->right, static fn (Expr $expr): Type => $resolver->resolve($expr));
}

}
53 changes: 52 additions & 1 deletion tests/PHPStan/Rules/Regexp/RegularExpressionPatternRuleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

use PHPStan\Rules\Rule;
use PHPStan\Testing\RuleTestCase;
use PHPStan\Type\Php\RegexExpressionHelper;
use function sprintf;
use const PHP_VERSION_ID;

Expand All @@ -15,7 +16,9 @@ class RegularExpressionPatternRuleTest extends RuleTestCase

protected function getRule(): Rule
{
return new RegularExpressionPatternRule();
return new RegularExpressionPatternRule(
self::getContainer()->getByType(RegexExpressionHelper::class),
);
}

public function testValidRegexPatternBefore73(): void
Expand Down Expand Up @@ -115,6 +118,30 @@ public function testValidRegexPatternBefore73(): void
'Regex pattern is invalid: Compilation failed: missing ) at offset 1 in pattern: ~(~',
43,
],
[
'Regex pattern is invalid: Delimiter must not be alphanumeric, backslash, or NUL in pattern: nok',
57,
],
[
'Regex pattern is invalid: Delimiter must not be alphanumeric, backslash, or NUL in pattern: nok',
58,
],
[
'Regex pattern is invalid: Compilation failed: missing closing parenthesis at offset 1 in pattern: ~(~',
59,
],
[
'Regex pattern is invalid: Delimiter must not be alphanumeric, backslash, or NUL in pattern: noknono',
61,
],
[
'Regex pattern is invalid: Delimiter must not be alphanumeric, backslash, or NUL in pattern: noknope',
62,
],
[
'Regex pattern is invalid: Compilation failed: missing closing parenthesis at offset 1 in pattern: ~(~',
63,
],
],
);
}
Expand Down Expand Up @@ -221,6 +248,30 @@ public function testValidRegexPatternAfter73(): void
'Regex pattern is invalid: Compilation failed: missing closing parenthesis at offset 1 in pattern: ~(~',
43,
],
[
sprintf('Regex pattern is invalid: Delimiter must not be %s in pattern: nok', $messagePart),
57,
],
[
sprintf('Regex pattern is invalid: Delimiter must not be %s in pattern: nok', $messagePart),
58,
],
[
'Regex pattern is invalid: Compilation failed: missing closing parenthesis at offset 1 in pattern: ~(~',
59,
],
[
sprintf('Regex pattern is invalid: Delimiter must not be %s in pattern: noknono', $messagePart),
61,
],
[
sprintf('Regex pattern is invalid: Delimiter must not be %s in pattern: noknope', $messagePart),
62,
],
[
'Regex pattern is invalid: Compilation failed: missing closing parenthesis at offset 1 in pattern: ~(~',
63,
],
],
);
}
Expand Down
16 changes: 15 additions & 1 deletion tests/PHPStan/Rules/Regexp/data/valid-regex-pattern.php
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<?php
<?php namespace RegexExpressionPatterns;

$string = (function (): string {})();

Expand Down Expand Up @@ -48,3 +48,17 @@
],
''
);

function doFoo(string $s) {
preg_match('~ok'. preg_quote($s, '~') .'~', '');
preg_match('~ok'. preg_quote($s) .'~', '');

// invalid preg_quote delimiters will be reported by RegularExpressionQuotingRule
preg_match('nok'. preg_quote($s), '');
preg_match('nok'. preg_quote($s), '');
preg_match('~('. preg_quote($s, '~') .'~', '');

preg_replace('nok'. preg_quote($s).'nono', '');
preg_replace('nok'. preg_quote($s).'nope', '');
preg_replace('~('. preg_quote($s, '~') .'~', '');
}