From a544d50fe47f406b03ddf49e95aece9bf150ca91 Mon Sep 17 00:00:00 2001 From: staabm <120441+staabm@users.noreply.github.com> Date: Thu, 28 May 2026 07:16:44 +0000 Subject: [PATCH 1/9] Narrow `preg_match`/`preg_match_all` subject string type when match is truthy - Add `matchSubjectExpr()` to `RegexArrayShapeMatcher` that extracts the subject base type (non-empty-string or non-falsy-string) from a regex pattern without requiring a $matches argument - Modify `PregMatchTypeSpecifyingExtension` to narrow the subject parameter (arg 1) to the regex-derived subject type in truthy context - Guard against narrowing non-string subjects (e.g. int|string) by checking `isString()->yes()` on the subject type - Skip subject narrowing when the subject expression shares a root variable with the matches argument (e.g. `preg_match($pat, $matches[2], $matches)`) to avoid conflicting type specifications --- .../Php/PregMatchTypeSpecifyingExtension.php | 47 ++++++++++- src/Type/Php/RegexArrayShapeMatcher.php | 26 ++++++ tests/PHPStan/Analyser/nsrt/bug-14710.php | 79 +++++++++++++++++++ 3 files changed, 148 insertions(+), 4 deletions(-) create mode 100644 tests/PHPStan/Analyser/nsrt/bug-14710.php diff --git a/src/Type/Php/PregMatchTypeSpecifyingExtension.php b/src/Type/Php/PregMatchTypeSpecifyingExtension.php index 399ee9126fd..f6e3cec0ff4 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?->value) ) { - 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,23 @@ 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 721c9858458..32527ff86c3 100644 --- a/src/Type/Php/RegexArrayShapeMatcher.php +++ b/src/Type/Php/RegexArrayShapeMatcher.php @@ -61,6 +61,32 @@ 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; + } + + $subjectType = $astWalkResult->getSubjectBaseType(); + if ((new StringType())->equals($subjectType)) { + return null; + } + + $subjectTypes[] = $subjectType; + } + + 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 00000000000..20adf6fd230 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-14710.php @@ -0,0 +1,79 @@ + Date: Fri, 29 May 2026 10:02:19 +0000 Subject: [PATCH 2/9] Narrow nullable string subjects in preg_match()/preg_match_all() A null subject is coerced to '' by preg_*, which cannot match a pattern whose derived subject type is non-empty, so null can be soundly removed from a nullable string subject. Non-string scalars (e.g. int) may be coerced to a matching string and are still left untouched. Also adds regression coverage for the subject-shares-variable-with-matches guard and the int|string non-narrowing case. Co-Authored-By: Claude Opus 4.8 --- .../Php/PregMatchTypeSpecifyingExtension.php | 19 +++++++++++++- tests/PHPStan/Analyser/nsrt/bug-14710.php | 25 +++++++++++++++++++ 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/src/Type/Php/PregMatchTypeSpecifyingExtension.php b/src/Type/Php/PregMatchTypeSpecifyingExtension.php index f6e3cec0ff4..d4d0af31452 100644 --- a/src/Type/Php/PregMatchTypeSpecifyingExtension.php +++ b/src/Type/Php/PregMatchTypeSpecifyingExtension.php @@ -14,6 +14,8 @@ use PHPStan\Reflection\FunctionReflection; use PHPStan\TrinaryLogic; use PHPStan\Type\FunctionTypeSpecifyingExtension; +use PHPStan\Type\Type; +use PHPStan\Type\TypeCombinator; use function in_array; use function strtolower; @@ -55,7 +57,7 @@ public function specifyTypes(FunctionReflection $functionReflection, FuncCall $n if ( $subjectArg !== null && $context->true() - && $scope->getType($subjectArg->value)->isString()->yes() + && $this->canNarrowSubject($scope->getType($subjectArg->value)) && !$this->isSubExprOfMatchesArg($subjectArg->value, $matchesArg?->value) ) { $subjectType = $this->regexShapeMatcher->matchSubjectExpr($patternArg->value, $scope); @@ -114,6 +116,21 @@ public function specifyTypes(FunctionReflection $functionReflection, FuncCall $n return $types->unionWith($subjectTypes); } + /** + * The subject is narrowed to a non-empty/non-falsy string derived from the pattern. A `null` + * subject is coerced to `''` by preg_*, which cannot produce such a match, so a nullable string + * subject can be narrowed too. Non-string scalars (e.g. `int`) may be coerced to a matching + * string, so they must be left untouched. + */ + private function canNarrowSubject(Type $subjectType): bool + { + if ($subjectType->isNull()->yes()) { + return false; + } + + return TypeCombinator::removeNull($subjectType)->isString()->yes(); + } + private function isSubExprOfMatchesArg(Expr $subject, ?Expr $matchesVar): bool { if ($matchesVar === null) { diff --git a/tests/PHPStan/Analyser/nsrt/bug-14710.php b/tests/PHPStan/Analyser/nsrt/bug-14710.php index 20adf6fd230..abca21a9726 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-14710.php +++ b/tests/PHPStan/Analyser/nsrt/bug-14710.php @@ -77,3 +77,28 @@ function pregMatchWithNonConstantPattern(string $pattern, string $x): void { assertType('string', $x); } } + +function pregMatchSubjectSharesVarWithMatches(): void { + $matches = ['', '', 'foo']; + // subject ($matches[2]) shares its root variable with the $matches output arg, + // so subject narrowing is skipped to avoid conflicting specifications + if (preg_match('/^(a|b|c)$/', $matches[2], $matches)) { + assertType("array{non-falsy-string, 'a'|'b'|'c'}", $matches); + } +} + +function pregMatchNullableSubject(?string $x): void { + // a null subject is coerced to '' which cannot match a non-empty pattern, so null is removed + if (preg_match('/^(a|b|c)$/', $x)) { + assertType('non-falsy-string', $x); + } else { + assertType('string|null', $x); + } +} + +function pregMatchIntStringSubject(int|string $x): void { + // an int subject can be coerced to a matching string, so narrowing it away would be unsound + if (preg_match('/^(a|b|c)$/', $x)) { + assertType('int|string', $x); + } +} From 86cc41539b724c221bfd4284142a7260a238801a Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Fri, 29 May 2026 12:27:37 +0200 Subject: [PATCH 3/9] simplify --- .../Php/PregMatchTypeSpecifyingExtension.php | 19 +------------------ src/Type/Php/RegexArrayShapeMatcher.php | 7 +------ 2 files changed, 2 insertions(+), 24 deletions(-) diff --git a/src/Type/Php/PregMatchTypeSpecifyingExtension.php b/src/Type/Php/PregMatchTypeSpecifyingExtension.php index d4d0af31452..4f6f36e0447 100644 --- a/src/Type/Php/PregMatchTypeSpecifyingExtension.php +++ b/src/Type/Php/PregMatchTypeSpecifyingExtension.php @@ -3,7 +3,6 @@ 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; @@ -56,9 +55,9 @@ public function specifyTypes(FunctionReflection $functionReflection, FuncCall $n $subjectTypes = new SpecifiedTypes(); if ( $subjectArg !== null + && $subjectArg->value instanceof Expr\Variable && $context->true() && $this->canNarrowSubject($scope->getType($subjectArg->value)) - && !$this->isSubExprOfMatchesArg($subjectArg->value, $matchesArg?->value) ) { $subjectType = $this->regexShapeMatcher->matchSubjectExpr($patternArg->value, $scope); if ($subjectType !== null) { @@ -131,20 +130,4 @@ private function canNarrowSubject(Type $subjectType): bool return TypeCombinator::removeNull($subjectType)->isString()->yes(); } - 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 32527ff86c3..373b0a614b2 100644 --- a/src/Type/Php/RegexArrayShapeMatcher.php +++ b/src/Type/Php/RegexArrayShapeMatcher.php @@ -76,12 +76,7 @@ public function matchSubjectExpr(Expr $patternExpr, Scope $scope): ?Type return null; } - $subjectType = $astWalkResult->getSubjectBaseType(); - if ((new StringType())->equals($subjectType)) { - return null; - } - - $subjectTypes[] = $subjectType; + $subjectTypes[] = $astWalkResult->getSubjectBaseType(); } return TypeCombinator::union(...$subjectTypes); From f0cce48bea06a2ffb382f714d744fb56bc3f8520 Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Fri, 29 May 2026 10:34:15 +0000 Subject: [PATCH 4/9] Document why preg_match subject narrowing is restricted to plain variables Make the rationale behind the `instanceof Expr\\Variable` guard explicit: narrowing only plain variables covers the vast majority of real-world code and avoids breaking exotic subjects like `preg_match($p, $matches[2], $matches)` (Rules bug-9503), where the subject is an offset of the array receiving matches. Co-Authored-By: Claude Opus 4.8 --- src/Type/Php/PregMatchTypeSpecifyingExtension.php | 3 +++ tests/PHPStan/Analyser/nsrt/bug-14710.php | 5 +++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/Type/Php/PregMatchTypeSpecifyingExtension.php b/src/Type/Php/PregMatchTypeSpecifyingExtension.php index 4f6f36e0447..e9b6b1520a1 100644 --- a/src/Type/Php/PregMatchTypeSpecifyingExtension.php +++ b/src/Type/Php/PregMatchTypeSpecifyingExtension.php @@ -53,6 +53,9 @@ public function specifyTypes(FunctionReflection $functionReflection, FuncCall $n } $subjectTypes = new SpecifiedTypes(); + // Only narrow plain variables. This covers the vast majority of real-world code and avoids + // breaking exotic subjects like `preg_match($p, $matches[2], $matches)` (see Rules bug-9503), + // where the subject is an offset of the same array that receives the matches output. if ( $subjectArg !== null && $subjectArg->value instanceof Expr\Variable diff --git a/tests/PHPStan/Analyser/nsrt/bug-14710.php b/tests/PHPStan/Analyser/nsrt/bug-14710.php index abca21a9726..e5af1e72ea5 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-14710.php +++ b/tests/PHPStan/Analyser/nsrt/bug-14710.php @@ -80,8 +80,9 @@ function pregMatchWithNonConstantPattern(string $pattern, string $x): void { function pregMatchSubjectSharesVarWithMatches(): void { $matches = ['', '', 'foo']; - // subject ($matches[2]) shares its root variable with the $matches output arg, - // so subject narrowing is skipped to avoid conflicting specifications + // the subject ($matches[2]) is not a plain variable but an array offset, so subject + // narrowing is skipped (only plain variables are narrowed) and the $matches output is + // typed normally without conflicting specifications (see Rules bug-9503) if (preg_match('/^(a|b|c)$/', $matches[2], $matches)) { assertType("array{non-falsy-string, 'a'|'b'|'c'}", $matches); } From 805983fcc92db8996be7acbd886e10d692685cb1 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Fri, 29 May 2026 13:39:10 +0200 Subject: [PATCH 5/9] simplify further --- .../Php/PregMatchTypeSpecifyingExtension.php | 24 +------------------ tests/PHPStan/Analyser/nsrt/bug-14710.php | 2 +- .../PHPStan/Rules/Variables/IssetRuleTest.php | 7 +++++- 3 files changed, 8 insertions(+), 25 deletions(-) diff --git a/src/Type/Php/PregMatchTypeSpecifyingExtension.php b/src/Type/Php/PregMatchTypeSpecifyingExtension.php index e9b6b1520a1..f3d4a04ae26 100644 --- a/src/Type/Php/PregMatchTypeSpecifyingExtension.php +++ b/src/Type/Php/PregMatchTypeSpecifyingExtension.php @@ -2,7 +2,6 @@ namespace PHPStan\Type\Php; -use PhpParser\Node\Expr; use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; use PHPStan\Analyser\SpecifiedTypes; @@ -13,8 +12,6 @@ use PHPStan\Reflection\FunctionReflection; use PHPStan\TrinaryLogic; use PHPStan\Type\FunctionTypeSpecifyingExtension; -use PHPStan\Type\Type; -use PHPStan\Type\TypeCombinator; use function in_array; use function strtolower; @@ -53,14 +50,10 @@ public function specifyTypes(FunctionReflection $functionReflection, FuncCall $n } $subjectTypes = new SpecifiedTypes(); - // Only narrow plain variables. This covers the vast majority of real-world code and avoids - // breaking exotic subjects like `preg_match($p, $matches[2], $matches)` (see Rules bug-9503), - // where the subject is an offset of the same array that receives the matches output. if ( $subjectArg !== null - && $subjectArg->value instanceof Expr\Variable && $context->true() - && $this->canNarrowSubject($scope->getType($subjectArg->value)) + && $scope->getType($subjectArg->value)->isString()->yes() ) { $subjectType = $this->regexShapeMatcher->matchSubjectExpr($patternArg->value, $scope); if ($subjectType !== null) { @@ -118,19 +111,4 @@ public function specifyTypes(FunctionReflection $functionReflection, FuncCall $n return $types->unionWith($subjectTypes); } - /** - * The subject is narrowed to a non-empty/non-falsy string derived from the pattern. A `null` - * subject is coerced to `''` by preg_*, which cannot produce such a match, so a nullable string - * subject can be narrowed too. Non-string scalars (e.g. `int`) may be coerced to a matching - * string, so they must be left untouched. - */ - private function canNarrowSubject(Type $subjectType): bool - { - if ($subjectType->isNull()->yes()) { - return false; - } - - return TypeCombinator::removeNull($subjectType)->isString()->yes(); - } - } diff --git a/tests/PHPStan/Analyser/nsrt/bug-14710.php b/tests/PHPStan/Analyser/nsrt/bug-14710.php index e5af1e72ea5..b295892cf53 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-14710.php +++ b/tests/PHPStan/Analyser/nsrt/bug-14710.php @@ -91,7 +91,7 @@ function pregMatchSubjectSharesVarWithMatches(): void { function pregMatchNullableSubject(?string $x): void { // a null subject is coerced to '' which cannot match a non-empty pattern, so null is removed if (preg_match('/^(a|b|c)$/', $x)) { - assertType('non-falsy-string', $x); + assertType('string|null', $x); // could be non-falsy-string } else { assertType('string|null', $x); } diff --git a/tests/PHPStan/Rules/Variables/IssetRuleTest.php b/tests/PHPStan/Rules/Variables/IssetRuleTest.php index ff2cf199417..ccc693f35de 100644 --- a/tests/PHPStan/Rules/Variables/IssetRuleTest.php +++ b/tests/PHPStan/Rules/Variables/IssetRuleTest.php @@ -528,7 +528,12 @@ public function testBug9503(): void { $this->treatPhpDocTypesAsCertain = true; - $this->analyse([__DIR__ . '/data/bug-9503.php'], []); + $this->analyse([__DIR__ . '/data/bug-9503.php'], [ + [ + "Offset 2 on list{0: non-falsy-string, 1?: ''|'a', 2: 'b'} in isset() always exists and is not nullable.", + 11, + ], + ]); } public function testBug14555(): void From 30d28f1459c77b2c0d3d57dba4c05e1f2625962a Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Fri, 29 May 2026 13:44:52 +0200 Subject: [PATCH 6/9] Update IssetRuleTest.php --- tests/PHPStan/Rules/Variables/IssetRuleTest.php | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/PHPStan/Rules/Variables/IssetRuleTest.php b/tests/PHPStan/Rules/Variables/IssetRuleTest.php index ccc693f35de..79aeb684027 100644 --- a/tests/PHPStan/Rules/Variables/IssetRuleTest.php +++ b/tests/PHPStan/Rules/Variables/IssetRuleTest.php @@ -530,6 +530,7 @@ public function testBug9503(): void $this->analyse([__DIR__ . '/data/bug-9503.php'], [ [ + // false positive on exotic code example "Offset 2 on list{0: non-falsy-string, 1?: ''|'a', 2: 'b'} in isset() always exists and is not nullable.", 11, ], From c8074f570db8a85da54107bf0852b841693e6551 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Fri, 29 May 2026 13:59:19 +0200 Subject: [PATCH 7/9] fix --- .../Php/PregMatchTypeSpecifyingExtension.php | 17 +++++++++++++++++ tests/PHPStan/Analyser/nsrt/bug-14710.php | 3 --- tests/PHPStan/Rules/Variables/IssetRuleTest.php | 8 +------- 3 files changed, 18 insertions(+), 10 deletions(-) diff --git a/src/Type/Php/PregMatchTypeSpecifyingExtension.php b/src/Type/Php/PregMatchTypeSpecifyingExtension.php index f3d4a04ae26..66796ac3c8e 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; @@ -54,6 +56,7 @@ public function specifyTypes(FunctionReflection $functionReflection, FuncCall $n $subjectArg !== null && $context->true() && $scope->getType($subjectArg->value)->isString()->yes() + && !$this->isSubExprOfMatchesArg($subjectArg->value, $matchesArg?->value) ) { $subjectType = $this->regexShapeMatcher->matchSubjectExpr($patternArg->value, $scope); if ($subjectType !== null) { @@ -111,4 +114,18 @@ public function specifyTypes(FunctionReflection $functionReflection, FuncCall $n 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/tests/PHPStan/Analyser/nsrt/bug-14710.php b/tests/PHPStan/Analyser/nsrt/bug-14710.php index b295892cf53..4825fd80fa1 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-14710.php +++ b/tests/PHPStan/Analyser/nsrt/bug-14710.php @@ -80,9 +80,6 @@ function pregMatchWithNonConstantPattern(string $pattern, string $x): void { function pregMatchSubjectSharesVarWithMatches(): void { $matches = ['', '', 'foo']; - // the subject ($matches[2]) is not a plain variable but an array offset, so subject - // narrowing is skipped (only plain variables are narrowed) and the $matches output is - // typed normally without conflicting specifications (see Rules bug-9503) if (preg_match('/^(a|b|c)$/', $matches[2], $matches)) { assertType("array{non-falsy-string, 'a'|'b'|'c'}", $matches); } diff --git a/tests/PHPStan/Rules/Variables/IssetRuleTest.php b/tests/PHPStan/Rules/Variables/IssetRuleTest.php index 79aeb684027..ff2cf199417 100644 --- a/tests/PHPStan/Rules/Variables/IssetRuleTest.php +++ b/tests/PHPStan/Rules/Variables/IssetRuleTest.php @@ -528,13 +528,7 @@ public function testBug9503(): void { $this->treatPhpDocTypesAsCertain = true; - $this->analyse([__DIR__ . '/data/bug-9503.php'], [ - [ - // false positive on exotic code example - "Offset 2 on list{0: non-falsy-string, 1?: ''|'a', 2: 'b'} in isset() always exists and is not nullable.", - 11, - ], - ]); + $this->analyse([__DIR__ . '/data/bug-9503.php'], []); } public function testBug14555(): void From 578be8610116e6e9b768f4e704ba06e6bcd0be9d Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Fri, 29 May 2026 14:07:32 +0200 Subject: [PATCH 8/9] Update PregMatchTypeSpecifyingExtension.php --- src/Type/Php/PregMatchTypeSpecifyingExtension.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Type/Php/PregMatchTypeSpecifyingExtension.php b/src/Type/Php/PregMatchTypeSpecifyingExtension.php index 66796ac3c8e..2082dd00484 100644 --- a/src/Type/Php/PregMatchTypeSpecifyingExtension.php +++ b/src/Type/Php/PregMatchTypeSpecifyingExtension.php @@ -56,7 +56,7 @@ public function specifyTypes(FunctionReflection $functionReflection, FuncCall $n $subjectArg !== null && $context->true() && $scope->getType($subjectArg->value)->isString()->yes() - && !$this->isSubExprOfMatchesArg($subjectArg->value, $matchesArg?->value) + && !$this->isSubExprOfMatchesArg($subjectArg->value, $matchesArg !== null ? $matchesArg->value : null) ) { $subjectType = $this->regexShapeMatcher->matchSubjectExpr($patternArg->value, $scope); if ($subjectType !== null) { From f81da84c383906e1c788717489ab3130d43e4f24 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Fri, 29 May 2026 14:22:09 +0200 Subject: [PATCH 9/9] cover more cases --- tests/PHPStan/Analyser/nsrt/bug-14710.php | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/tests/PHPStan/Analyser/nsrt/bug-14710.php b/tests/PHPStan/Analyser/nsrt/bug-14710.php index 4825fd80fa1..6d208e77ca2 100644 --- a/tests/PHPStan/Analyser/nsrt/bug-14710.php +++ b/tests/PHPStan/Analyser/nsrt/bug-14710.php @@ -66,10 +66,18 @@ function pregMatchCompare(string $x): void { } } -function pregMatchNoSubject(): void { - if (preg_match('/^(a|b|c)$/', 'a')) { - // non-variable subject, no narrowing needed +function pregMatchNotIdentical(string $x): void { + if (preg_match('#ExtensionInterface$#', $x) !== 1) { + return; } + assertType('non-falsy-string', $x); +} + +function pregMatchNotEqual(string $x): void { + if (preg_match('#ExtensionInterface$#', $x) != 1) { + return; + } + assertType('non-falsy-string', $x); } function pregMatchWithNonConstantPattern(string $pattern, string $x): void {