diff --git a/src/Type/Php/PregMatchTypeSpecifyingExtension.php b/src/Type/Php/PregMatchTypeSpecifyingExtension.php index 399ee9126f..2082dd0048 100644 --- a/src/Type/Php/PregMatchTypeSpecifyingExtension.php +++ b/src/Type/Php/PregMatchTypeSpecifyingExtension.php @@ -2,6 +2,8 @@ namespace PHPStan\Type\Php; +use PhpParser\Node\Expr; +use PhpParser\Node\Expr\ArrayDimFetch; use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; use PHPStan\Analyser\SpecifiedTypes; @@ -41,13 +43,34 @@ public function specifyTypes(FunctionReflection $functionReflection, FuncCall $n { $args = $node->getArgs(); $patternArg = $args[0] ?? null; + $subjectArg = $args[1] ?? null; $matchesArg = $args[2] ?? null; $flagsArg = $args[3] ?? null; + if ($patternArg === null) { + return new SpecifiedTypes(); + } + + $subjectTypes = new SpecifiedTypes(); if ( - $patternArg === null || $matchesArg === null + $subjectArg !== null + && $context->true() + && $scope->getType($subjectArg->value)->isString()->yes() + && !$this->isSubExprOfMatchesArg($subjectArg->value, $matchesArg !== null ? $matchesArg->value : null) ) { - return new SpecifiedTypes(); + $subjectType = $this->regexShapeMatcher->matchSubjectExpr($patternArg->value, $scope); + if ($subjectType !== null) { + $subjectTypes = $this->typeSpecifier->create( + $subjectArg->value, + $subjectType, + $context, + $scope, + )->setRootExpr($node); + } + } + + if ($matchesArg === null) { + return $subjectTypes; } $flagsType = null; @@ -69,7 +92,7 @@ public function specifyTypes(FunctionReflection $functionReflection, FuncCall $n $matchedType = $this->regexShapeMatcher->matchAllExpr($patternArg->value, $flagsType, $wasMatched, $scope); } if ($matchedType === null) { - return new SpecifiedTypes(); + return $subjectTypes; } $overwrite = false; @@ -88,7 +111,21 @@ public function specifyTypes(FunctionReflection $functionReflection, FuncCall $n $types = $types->setAlwaysOverwriteTypes(); } - return $types; + return $types->unionWith($subjectTypes); + } + + private function isSubExprOfMatchesArg(Expr $subject, ?Expr $matchesVar): bool + { + if ($matchesVar === null) { + return false; + } + $rootVar = $subject; + while ($rootVar instanceof ArrayDimFetch) { + $rootVar = $rootVar->var; + } + return $rootVar instanceof Expr\Variable + && $matchesVar instanceof Expr\Variable + && $rootVar->name === $matchesVar->name; } } diff --git a/src/Type/Php/RegexArrayShapeMatcher.php b/src/Type/Php/RegexArrayShapeMatcher.php index 721c985845..373b0a614b 100644 --- a/src/Type/Php/RegexArrayShapeMatcher.php +++ b/src/Type/Php/RegexArrayShapeMatcher.php @@ -61,6 +61,27 @@ public function matchExpr(Expr $patternExpr, ?Type $flagsType, TrinaryLogic $was return $this->matchPatternType($this->getPatternType($patternExpr, $scope), $flagsType, $wasMatched, false); } + public function matchSubjectExpr(Expr $patternExpr, Scope $scope): ?Type + { + $patternType = $this->getPatternType($patternExpr, $scope); + $constantStrings = $patternType->getConstantStrings(); + if (count($constantStrings) === 0) { + return null; + } + + $subjectTypes = []; + foreach ($constantStrings as $constantString) { + $astWalkResult = $this->regexGroupParser->parseGroups($constantString->getValue()); + if ($astWalkResult === null) { + return null; + } + + $subjectTypes[] = $astWalkResult->getSubjectBaseType(); + } + + return TypeCombinator::union(...$subjectTypes); + } + private function matchPatternType(Type $patternType, ?Type $flagsType, TrinaryLogic $wasMatched, bool $matchesAll): ?Type { if ($wasMatched->no()) { diff --git a/tests/PHPStan/Analyser/nsrt/bug-14710.php b/tests/PHPStan/Analyser/nsrt/bug-14710.php new file mode 100644 index 0000000000..6d208e77ca --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-14710.php @@ -0,0 +1,110 @@ +