From 26ec2df5121997171724c8baaf23ce0a499b10d4 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Thu, 4 Jun 2026 11:22:06 +0200 Subject: [PATCH 1/4] Narrow `Strings::match`/`Strings::matchAll` subject string type when match is truthy --- .../StringsMatchTypeSpecifiyingExtension.php | 81 +++++++++++++++++++ 1 file changed, 81 insertions(+) create mode 100644 src/Type/Nette/StringsMatchTypeSpecifiyingExtension.php diff --git a/src/Type/Nette/StringsMatchTypeSpecifiyingExtension.php b/src/Type/Nette/StringsMatchTypeSpecifiyingExtension.php new file mode 100644 index 0000000..53e498f --- /dev/null +++ b/src/Type/Nette/StringsMatchTypeSpecifiyingExtension.php @@ -0,0 +1,81 @@ +regexArrayShapeMatcher = $regexArrayShapeMatcher; + } + + public function setTypeSpecifier(TypeSpecifier $typeSpecifier): void + { + $this->typeSpecifier = $typeSpecifier; + } + + public function getClass(): string + { + return Strings::class; + } + + public function isStaticMethodSupported(MethodReflection $staticMethodReflection, StaticCall $node, TypeSpecifierContext $context): bool + { + return $context->true() && $staticMethodReflection->getName() === 'match'; + } + + public function specifyTypes(MethodReflection $staticMethodReflection, StaticCall $node, Scope $scope, TypeSpecifierContext $context): SpecifiedTypes + { + $args = $node->getArgs(); + $subjectArg = $args[0] ?? null; + $patternArg = $args[1] ?? null; + + $subjectTypes = new SpecifiedTypes(); + if ($patternArg === null) { + return $subjectTypes; + } + + if ( + $subjectArg !== null + && $context->true() + && $scope->getType($subjectArg->value)->isString()->yes() + ) { + $subjectType = $this->regexArrayShapeMatcher->matchSubjectExpr($patternArg->value, $scope); + if ($subjectType !== null) { + $subjectTypes = $this->typeSpecifier->create( + $subjectArg->value, + $subjectType, + $context, + $scope, + )->setRootExpr($node); + } + } + + return $subjectTypes; + } +} From a7d255df25a7e2e7369e6dba2c7f44580e29c23f Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Thu, 4 Jun 2026 11:47:09 +0200 Subject: [PATCH 2/4] matchesAll --- extension.neon | 5 ++++ .../StringsMatchTypeSpecifiyingExtension.php | 19 +++++---------- ...tringsMatchTypeInferenceExtensionTest.php} | 3 ++- .../Type/Nette/data/strings-match-subject.php | 23 +++++++++++++++++++ 4 files changed, 36 insertions(+), 14 deletions(-) rename tests/Type/Nette/{StringsMatchDynamicReturnTypeExtensionTest.php => StringsMatchTypeInferenceExtensionTest.php} (82%) create mode 100644 tests/Type/Nette/data/strings-match-subject.php diff --git a/extension.neon b/extension.neon index 49f2125..2895326 100644 --- a/extension.neon +++ b/extension.neon @@ -128,6 +128,11 @@ services: tags: - phpstan.broker.dynamicStaticMethodReturnTypeExtension + - + class: PHPStan\Type\Nette\StringsMatchTypeSpecifiyingExtension + tags: + - phpstan.typeSpecifier.staticMethodTypeSpecifyingExtension + - class: PHPStan\Type\Nette\StringsReplaceCallbackClosureTypeExtension tags: diff --git a/src/Type/Nette/StringsMatchTypeSpecifiyingExtension.php b/src/Type/Nette/StringsMatchTypeSpecifiyingExtension.php index 53e498f..35c2102 100644 --- a/src/Type/Nette/StringsMatchTypeSpecifiyingExtension.php +++ b/src/Type/Nette/StringsMatchTypeSpecifiyingExtension.php @@ -3,28 +3,20 @@ namespace PHPStan\Type\Nette; use Nette\Utils\Strings; -use PhpParser\Node\Arg; use PhpParser\Node\Expr\StaticCall; use PHPStan\Analyser\Scope; use PHPStan\Analyser\SpecifiedTypes; use PHPStan\Analyser\TypeSpecifier; +use PHPStan\Analyser\TypeSpecifierAwareExtension; use PHPStan\Analyser\TypeSpecifierContext; use PHPStan\Reflection\MethodReflection; -use PHPStan\TrinaryLogic; -use PHPStan\Type\Constant\ConstantBooleanType; -use PHPStan\Type\Constant\ConstantIntegerType; -use PHPStan\Type\DynamicStaticMethodReturnTypeExtension; -use PHPStan\Type\NullType; use PHPStan\Type\Php\RegexArrayShapeMatcher; use PHPStan\Type\StaticMethodTypeSpecifyingExtension; -use PHPStan\Type\Type; -use PHPStan\Type\TypeCombinator; -use function array_key_exists; -use const PREG_OFFSET_CAPTURE; -use const PREG_UNMATCHED_AS_NULL; +use function in_array; -class StringsMatchTypeSpecifiyingExtension implements StaticMethodTypeSpecifyingExtension +class StringsMatchTypeSpecifiyingExtension implements StaticMethodTypeSpecifyingExtension, TypeSpecifierAwareExtension { + private RegexArrayShapeMatcher $regexArrayShapeMatcher; private TypeSpecifier $typeSpecifier; @@ -46,7 +38,7 @@ public function getClass(): string public function isStaticMethodSupported(MethodReflection $staticMethodReflection, StaticCall $node, TypeSpecifierContext $context): bool { - return $context->true() && $staticMethodReflection->getName() === 'match'; + return $context->true() && in_array($staticMethodReflection->getName(), ['match', 'matchAll'], true); } public function specifyTypes(MethodReflection $staticMethodReflection, StaticCall $node, Scope $scope, TypeSpecifierContext $context): SpecifiedTypes @@ -78,4 +70,5 @@ public function specifyTypes(MethodReflection $staticMethodReflection, StaticCal return $subjectTypes; } + } diff --git a/tests/Type/Nette/StringsMatchDynamicReturnTypeExtensionTest.php b/tests/Type/Nette/StringsMatchTypeInferenceExtensionTest.php similarity index 82% rename from tests/Type/Nette/StringsMatchDynamicReturnTypeExtensionTest.php rename to tests/Type/Nette/StringsMatchTypeInferenceExtensionTest.php index 9bf6536..9686f8d 100644 --- a/tests/Type/Nette/StringsMatchDynamicReturnTypeExtensionTest.php +++ b/tests/Type/Nette/StringsMatchTypeInferenceExtensionTest.php @@ -4,13 +4,14 @@ use PHPStan\Testing\TypeInferenceTestCase; -class StringsMatchDynamicReturnTypeExtensionTest extends TypeInferenceTestCase +class StringsMatchTypeInferenceExtensionTest extends TypeInferenceTestCase { public function dataFileAsserts(): iterable { yield from self::gatherAssertTypes(__DIR__ . '/data/strings-match.php'); yield from self::gatherAssertTypes(__DIR__ . '/data/strings-match-74.php'); + yield from self::gatherAssertTypes(__DIR__ . '/data/strings-match-subject.php'); } /** diff --git a/tests/Type/Nette/data/strings-match-subject.php b/tests/Type/Nette/data/strings-match-subject.php new file mode 100644 index 0000000..408201c --- /dev/null +++ b/tests/Type/Nette/data/strings-match-subject.php @@ -0,0 +1,23 @@ + Date: Thu, 4 Jun 2026 11:49:01 +0200 Subject: [PATCH 3/4] simplify --- src/Type/Nette/StringsMatchTypeSpecifiyingExtension.php | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/Type/Nette/StringsMatchTypeSpecifiyingExtension.php b/src/Type/Nette/StringsMatchTypeSpecifiyingExtension.php index 35c2102..37977a6 100644 --- a/src/Type/Nette/StringsMatchTypeSpecifiyingExtension.php +++ b/src/Type/Nette/StringsMatchTypeSpecifiyingExtension.php @@ -48,15 +48,11 @@ public function specifyTypes(MethodReflection $staticMethodReflection, StaticCal $patternArg = $args[1] ?? null; $subjectTypes = new SpecifiedTypes(); - if ($patternArg === null) { + if ($patternArg === null || $subjectArg === null) { return $subjectTypes; } - if ( - $subjectArg !== null - && $context->true() - && $scope->getType($subjectArg->value)->isString()->yes() - ) { + if ($scope->getType($subjectArg->value)->isString()->yes()) { $subjectType = $this->regexArrayShapeMatcher->matchSubjectExpr($patternArg->value, $scope); if ($subjectType !== null) { $subjectTypes = $this->typeSpecifier->create( From 696cb653a9d5c01380e45945cfb1803ab57b9720 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Thu, 4 Jun 2026 12:09:01 +0200 Subject: [PATCH 4/4] Update strings-match-subject.php --- tests/Type/Nette/data/strings-match-subject.php | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/tests/Type/Nette/data/strings-match-subject.php b/tests/Type/Nette/data/strings-match-subject.php index 408201c..424d23a 100644 --- a/tests/Type/Nette/data/strings-match-subject.php +++ b/tests/Type/Nette/data/strings-match-subject.php @@ -21,3 +21,12 @@ function (string $s): void { } assertType("string", $s); }; + +function ($mixed): void { + if (Strings::match($mixed, '/foo/')) { + assertType("mixed", $mixed); + } else { + assertType("mixed", $mixed); + } + assertType("mixed", $mixed); +};