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..f1127f4def 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,56 @@ 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); + } + + /** + * 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 ( + $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..a3bb2c2b4a 100644 --- a/tests/PHPStan/Analyser/nsrt/preg_match_shapes.php +++ b/tests/PHPStan/Analyser/nsrt/preg_match_shapes.php @@ -393,3 +393,47 @@ 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, '/') . '(\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); + } else { + assertType('array{}', $matches); + } + 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); +};