From 4b581de31d0984a6105306fd3608164f0b0e6d82 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Sat, 13 Jul 2024 15:04:59 +0200 Subject: [PATCH 1/4] RegexArrayShapeMatcher - Support preg_quote()'d patterns --- .../PregMatchParameterOutTypeExtension.php | 3 +- .../Php/PregMatchTypeSpecifyingExtension.php | 3 +- src/Type/Php/RegexArrayShapeMatcher.php | 61 +++++++++++++++++++ .../Analyser/nsrt/preg_match_shapes.php | 26 ++++++++ 4 files changed, 89 insertions(+), 4 deletions(-) diff --git a/src/Type/Php/PregMatchParameterOutTypeExtension.php b/src/Type/Php/PregMatchParameterOutTypeExtension.php index 3b8193d00b..23838c4e2e 100644 --- a/src/Type/Php/PregMatchParameterOutTypeExtension.php +++ b/src/Type/Php/PregMatchParameterOutTypeExtension.php @@ -41,13 +41,12 @@ public function getParameterOutTypeFromFunctionCall(FunctionReflection $function return null; } - $patternType = $scope->getType($patternArg->value); $flagsType = null; if ($flagsArg !== null) { $flagsType = $scope->getType($flagsArg->value); } - return $this->regexShapeMatcher->matchType($patternType, $flagsType, TrinaryLogic::createMaybe()); + return $this->regexShapeMatcher->matchExpr($patternArg->value, $flagsType, TrinaryLogic::createMaybe(), $scope); } } diff --git a/src/Type/Php/PregMatchTypeSpecifyingExtension.php b/src/Type/Php/PregMatchTypeSpecifyingExtension.php index 49d82df6f0..7ea40f4faa 100644 --- a/src/Type/Php/PregMatchTypeSpecifyingExtension.php +++ b/src/Type/Php/PregMatchTypeSpecifyingExtension.php @@ -48,13 +48,12 @@ public function specifyTypes(FunctionReflection $functionReflection, FuncCall $n return new SpecifiedTypes(); } - $patternType = $scope->getType($patternArg->value); $flagsType = null; if ($flagsArg !== null) { $flagsType = $scope->getType($flagsArg->value); } - $matchedType = $this->regexShapeMatcher->matchType($patternType, $flagsType, TrinaryLogic::createFromBoolean($context->true())); + $matchedType = $this->regexShapeMatcher->matchExpr($patternArg->value, $flagsType, TrinaryLogic::createFromBoolean($context->true()), $scope); if ($matchedType === null) { return new SpecifiedTypes(); } diff --git a/src/Type/Php/RegexArrayShapeMatcher.php b/src/Type/Php/RegexArrayShapeMatcher.php index d2fcc02336..0f9cf40bca 100644 --- a/src/Type/Php/RegexArrayShapeMatcher.php +++ b/src/Type/Php/RegexArrayShapeMatcher.php @@ -7,6 +7,9 @@ use Hoa\Compiler\Llk\TreeNode; use Hoa\Exception\Exception; use Hoa\File\Read; +use PhpParser\Node\Expr; +use PhpParser\Node\Name; +use PHPStan\Analyser\Scope; use PHPStan\Php\PhpVersion; use PHPStan\TrinaryLogic; use PHPStan\Type\Constant\ConstantArrayType; @@ -45,7 +48,20 @@ public function __construct( { } + public function matchExpr(Expr $patternExpr, ?Type $flagsType, TrinaryLogic $wasMatched, Scope $scope): ?Type + { + return $this->matchPatternType($this->getPatternType($patternExpr, $scope), $flagsType, $wasMatched); + } + + /** + * @deprecated use matchExpr() instead for a more precise result + */ public function matchType(Type $patternType, ?Type $flagsType, TrinaryLogic $wasMatched): ?Type + { + return $this->matchPatternType($patternType, $flagsType, $wasMatched); + } + + private function matchPatternType(Type $patternType, ?Type $flagsType, TrinaryLogic $wasMatched): ?Type { if ($wasMatched->no()) { return new ConstantArrayType([], []); @@ -484,4 +500,49 @@ private function walkRegexAst( } } + private function getPatternType(Expr $patternExpr, Scope $scope): Type + { + if ($patternExpr instanceof Expr\BinaryOp\Concat) { + return $this->resolvePatternConcat($patternExpr, $scope); + } + + return $scope->getType($patternExpr); + } + + private function resolvePatternConcat(Expr\BinaryOp\Concat $concat, Scope $scope): Type + { + if ( + $concat->left instanceof Expr\FuncCall + && $concat->left->name instanceof Name + && $concat->left->name->toLowerString() === 'preg_quote' + ) { + $left = new ConstantStringType(''); + } elseif ($concat->left instanceof Expr\BinaryOp\Concat) { + $left = $this->resolvePatternConcat($concat->left, $scope); + } else { + $left = $scope->getType($concat->left); + } + + if ( + $concat->right instanceof Expr\FuncCall + && $concat->right->name instanceof Name + && $concat->right->name->toLowerString() === 'preg_quote' + ) { + $right = new ConstantStringType(''); + } elseif ($concat->right instanceof Expr\BinaryOp\Concat) { + $right = $this->resolvePatternConcat($concat->right, $scope); + } else { + $right = $scope->getType($concat->right); + } + + $strings = []; + foreach ($left->getConstantStrings() as $leftString) { + foreach ($right->getConstantStrings() as $rightString) { + $strings[] = new ConstantStringType($leftString->getValue() . $rightString->getValue()); + } + } + + return TypeCombinator::union(...$strings); + } + } diff --git a/tests/PHPStan/Analyser/nsrt/preg_match_shapes.php b/tests/PHPStan/Analyser/nsrt/preg_match_shapes.php index 2b1b774a48..6ce6bbabad 100644 --- a/tests/PHPStan/Analyser/nsrt/preg_match_shapes.php +++ b/tests/PHPStan/Analyser/nsrt/preg_match_shapes.php @@ -393,3 +393,29 @@ function unmatchedAsNullWithMandatoryGroup(string $s): void { assertType('array{}|array{0: string, currency: string, 1: string}', $matches); } +function (string $s): void { + if (preg_match('{' . preg_quote('xxx') . '(z)}', $s, $matches)) { + assertType('array{string, string}', $matches); + } else { + assertType('array{}', $matches); + } + assertType('array{}|array{string, string}', $matches); +}; + +function (string $s): void { + if (preg_match('{' . preg_quote($s) . '(z)}', $s, $matches)) { + assertType('array{string, string}', $matches); + } else { + assertType('array{}', $matches); + } + assertType('array{}|array{string, string}', $matches); +}; + +function (string $s): void { + if (preg_match('{' . preg_quote($s) . '(z)' . preg_quote($s) . '(?:abc)(def)?}', $s, $matches)) { + assertType('array{0: string, 1: string, 2?: string}', $matches); + } else { + assertType('array{}', $matches); + } + assertType('array{}|array{0: string, 1: string, 2?: string}', $matches); +}; From cfe0d1912fd8f4552ee281660b0f923e3db8379f Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Sun, 14 Jul 2024 07:21:41 +0200 Subject: [PATCH 2/4] test preg_quote() with non-constant concat --- tests/PHPStan/Analyser/nsrt/preg_match_shapes.php | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/tests/PHPStan/Analyser/nsrt/preg_match_shapes.php b/tests/PHPStan/Analyser/nsrt/preg_match_shapes.php index 6ce6bbabad..cd2dfb1519 100644 --- a/tests/PHPStan/Analyser/nsrt/preg_match_shapes.php +++ b/tests/PHPStan/Analyser/nsrt/preg_match_shapes.php @@ -419,3 +419,12 @@ function (string $s): void { } assertType('array{}|array{0: string, 1: string, 2?: string}', $matches); }; + +function (string $s, $mixed): void { + if (preg_match('{' . preg_quote($s) . '(z)' . preg_quote($s) . '(?:abc)'. $mixed .'(def)?}', $s, $matches)) { + assertType('array', $matches); + } else { + assertType('array{}', $matches); + } + assertType('array', $matches); +}; From d05a40f4725e128aba78d8ab63df4a03a1b021ee Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Sun, 14 Jul 2024 07:33:19 +0200 Subject: [PATCH 3/4] added preg_quote() with delimiter arg test --- tests/PHPStan/Analyser/nsrt/preg_match_shapes.php | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/tests/PHPStan/Analyser/nsrt/preg_match_shapes.php b/tests/PHPStan/Analyser/nsrt/preg_match_shapes.php index cd2dfb1519..a3bb2c2b4a 100644 --- a/tests/PHPStan/Analyser/nsrt/preg_match_shapes.php +++ b/tests/PHPStan/Analyser/nsrt/preg_match_shapes.php @@ -411,6 +411,15 @@ function (string $s): void { assertType('array{}|array{string, string}', $matches); }; +function (string $s): void { + if (preg_match('/' . preg_quote($s, '/') . '(\d)/', $s, $matches)) { + assertType('array{string, string}', $matches); + } else { + assertType('array{}', $matches); + } + assertType('array{}|array{string, string}', $matches); +}; + function (string $s): void { if (preg_match('{' . preg_quote($s) . '(z)' . preg_quote($s) . '(?:abc)(def)?}', $s, $matches)) { assertType('array{0: string, 1: string, 2?: string}', $matches); From 919abe34d4a748e9fdcaa01e07c51c095619d9c0 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Sun, 14 Jul 2024 09:05:19 +0200 Subject: [PATCH 4/4] Update RegexArrayShapeMatcher.php --- src/Type/Php/RegexArrayShapeMatcher.php | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/Type/Php/RegexArrayShapeMatcher.php b/src/Type/Php/RegexArrayShapeMatcher.php index 0f9cf40bca..f1127f4def 100644 --- a/src/Type/Php/RegexArrayShapeMatcher.php +++ b/src/Type/Php/RegexArrayShapeMatcher.php @@ -509,6 +509,13 @@ private function getPatternType(Expr $patternExpr, Scope $scope): Type 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 { if (