diff --git a/composer.json b/composer.json index ae9dca5..99a58ab 100644 --- a/composer.json +++ b/composer.json @@ -7,7 +7,7 @@ ], "require": { "php": "^7.2 || ^8.0", - "phpstan/phpstan": "^1.11.8" + "phpstan/phpstan": "^1.11.9" }, "conflict": { "nette/application": "<2.3.0", diff --git a/extension.neon b/extension.neon index c82e84a..b641f0f 100644 --- a/extension.neon +++ b/extension.neon @@ -55,6 +55,8 @@ parameters: conditionalTags: PHPStan\Type\Nette\StringsMatchDynamicReturnTypeExtension: phpstan.broker.dynamicStaticMethodReturnTypeExtension: %featureToggles.narrowPregMatches% + PHPStan\Type\Nette\StringsMatchAllDynamicReturnTypeExtension: + phpstan.broker.dynamicStaticMethodReturnTypeExtension: %featureToggles.narrowPregMatches% services: - @@ -119,5 +121,8 @@ services: tags: - phpstan.properties.readWriteExtension + - + class: PHPStan\Type\Nette\StringsMatchAllDynamicReturnTypeExtension + - class: PHPStan\Type\Nette\StringsMatchDynamicReturnTypeExtension diff --git a/src/Type/Nette/StringsMatchAllDynamicReturnTypeExtension.php b/src/Type/Nette/StringsMatchAllDynamicReturnTypeExtension.php new file mode 100644 index 0000000..35fc240 --- /dev/null +++ b/src/Type/Nette/StringsMatchAllDynamicReturnTypeExtension.php @@ -0,0 +1,59 @@ +regexArrayShapeMatcher = $regexArrayShapeMatcher; + } + + public function getClass(): string + { + return Strings::class; + } + + public function isStaticMethodSupported(MethodReflection $methodReflection): bool + { + return $methodReflection->getName() === 'matchAll'; + } + + public function getTypeFromStaticMethodCall(MethodReflection $methodReflection, StaticCall $methodCall, Scope $scope): ?Type + { + $args = $methodCall->getArgs(); + $patternArg = $args[1] ?? null; + if ($patternArg === null) { + return null; + } + + $flagsArg = $args[2] ?? null; + $flagsType = null; + if ($flagsArg !== null) { + $flagsType = $scope->getType($flagsArg->value); + } + + $arrayShape = $this->regexArrayShapeMatcher->matchAllExpr($patternArg->value, $flagsType, TrinaryLogic::createYes(), $scope); + if ($arrayShape === null) { + return null; + } + + return TypeCombinator::union($arrayShape, new NullType()); + } + +} diff --git a/tests/Type/Nette/data/strings-match.php b/tests/Type/Nette/data/strings-match.php index 9ff1c1c..2449ccd 100644 --- a/tests/Type/Nette/data/strings-match.php +++ b/tests/Type/Nette/data/strings-match.php @@ -5,6 +5,7 @@ use Nette\Utils\Strings; use function PHPStan\Testing\assertType; use const PREG_OFFSET_CAPTURE; +use const PREG_SET_ORDER; function (string $s): void { $result = Strings::match($s, '/%env\((.*)\:.*\)%/U'); @@ -22,3 +23,13 @@ function (string $s): void { $result = Strings::match($s, '/(foo)(bar)'. preg_quote($s) .'(baz)/'); assertType('array{string, non-empty-string, non-empty-string, non-empty-string}|null', $result); }; + +function (string $s): void { + $result = Strings::matchAll($s, '/ab(?P\d+)(?Pab)?/', PREG_SET_ORDER); + assertType("list|null", $result); +}; + +function (string $s): void { + $result = Strings::matchAll($s, '/ab(?P\d+)(?Pab)?/', PREG_PATTERN_ORDER); + assertType("array{0: list, num: list, 1: list, suffix: list, 2: list}|null", $result); +};