diff --git a/composer.json b/composer.json index 54f6080..6aa6534 100644 --- a/composer.json +++ b/composer.json @@ -7,7 +7,7 @@ ], "require": { "php": "^7.2 || ^8.0", - "phpstan/phpstan": "^1.10" + "phpstan/phpstan": "^1.11.6" }, "conflict": { "nette/application": "<2.3.0", diff --git a/extension.neon b/extension.neon index 9563acb..c82e84a 100644 --- a/extension.neon +++ b/extension.neon @@ -52,6 +52,10 @@ parameters: - terminate - forward +conditionalTags: + PHPStan\Type\Nette\StringsMatchDynamicReturnTypeExtension: + phpstan.broker.dynamicStaticMethodReturnTypeExtension: %featureToggles.narrowPregMatches% + services: - class: PHPStan\Reflection\Nette\HtmlClassReflectionExtension @@ -114,3 +118,6 @@ services: class: PHPStan\Rule\Nette\PresenterInjectedPropertiesExtension tags: - phpstan.properties.readWriteExtension + + - + class: PHPStan\Type\Nette\StringsMatchDynamicReturnTypeExtension diff --git a/src/Type/Nette/StringsMatchDynamicReturnTypeExtension.php b/src/Type/Nette/StringsMatchDynamicReturnTypeExtension.php new file mode 100644 index 0000000..8a88444 --- /dev/null +++ b/src/Type/Nette/StringsMatchDynamicReturnTypeExtension.php @@ -0,0 +1,60 @@ +regexArrayShapeMatcher = $regexArrayShapeMatcher; + } + + public function getClass(): string + { + return Strings::class; + } + + public function isStaticMethodSupported(MethodReflection $methodReflection): bool + { + return $methodReflection->getName() === 'match'; + } + + public function getTypeFromStaticMethodCall(MethodReflection $methodReflection, StaticCall $methodCall, Scope $scope): ?Type + { + $args = $methodCall->getArgs(); + $patternArg = $args[1] ?? null; + if ($patternArg === null) { + return null; + } + + $patternType = $scope->getType($patternArg->value); + $flagsArg = $args[2] ?? null; + $flagsType = null; + if ($flagsArg !== null) { + $flagsType = $scope->getType($flagsArg->value); + } + + $arrayShape = $this->regexArrayShapeMatcher->matchType($patternType, $flagsType, TrinaryLogic::createYes()); + if ($arrayShape === null) { + return null; + } + + return TypeCombinator::union($arrayShape, new NullType()); + } + +} diff --git a/tests/Type/Nette/StringsMatchDynamicReturnTypeExtensionTest.php b/tests/Type/Nette/StringsMatchDynamicReturnTypeExtensionTest.php new file mode 100644 index 0000000..c21a234 --- /dev/null +++ b/tests/Type/Nette/StringsMatchDynamicReturnTypeExtensionTest.php @@ -0,0 +1,39 @@ + + */ + public function dataFileAsserts(): iterable + { + yield from $this->gatherAssertTypes(__DIR__ . '/data/strings-match.php'); + } + + /** + * @dataProvider dataFileAsserts + * @param mixed ...$args + */ + public function testFileAsserts( + string $assertType, + string $file, + ...$args + ): void + { + $this->assertFileAsserts($assertType, $file, ...$args); + } + + public static function getAdditionalConfigFiles(): array + { + return [ + 'phar://' . __DIR__ . '/../../../vendor/phpstan/phpstan/phpstan.phar/conf/bleedingEdge.neon', + __DIR__ . '/phpstan.neon', + ]; + } + +} diff --git a/tests/Type/Nette/data/strings-match.php b/tests/Type/Nette/data/strings-match.php new file mode 100644 index 0000000..5d5df66 --- /dev/null +++ b/tests/Type/Nette/data/strings-match.php @@ -0,0 +1,21 @@ +}, array{string, int<0, max>}, array{string, int<0, max>}, array{string, int<0, max>}}|null', $result); + + $result = Strings::match($s, '/(foo)(bar)(baz)/'); + assertType('array{string, string, string, string}|null', $result); +};