diff --git a/src/PHPStan/PregMatchFlags.php b/src/PHPStan/PregMatchFlags.php index ace3e8a..3daf8a4 100644 --- a/src/PHPStan/PregMatchFlags.php +++ b/src/PHPStan/PregMatchFlags.php @@ -3,11 +3,16 @@ namespace Composer\Pcre\PHPStan; use PHPStan\Analyser\Scope; +use PHPStan\Type\ArrayType; +use PHPStan\Type\Constant\ConstantArrayType; use PHPStan\Type\Constant\ConstantIntegerType; +use PHPStan\Type\IntersectionType; use PHPStan\Type\TypeCombinator; use PHPStan\Type\Type; use PhpParser\Node\Arg; use PHPStan\Type\Php\RegexArrayShapeMatcher; +use PHPStan\Type\TypeTraverser; +use PHPStan\Type\UnionType; final class PregMatchFlags { @@ -34,4 +39,32 @@ static public function getType(?Arg $flagsArg, Scope $scope): ?Type } return TypeCombinator::union(...$internalFlagsTypes); } + + static public function removeNullFromMatches(Type $matchesType): Type + { + return TypeTraverser::map($matchesType, static function (Type $type, callable $traverse): Type { + if ($type instanceof UnionType || $type instanceof IntersectionType) { + return $traverse($type); + } + + if ($type instanceof ConstantArrayType) { + return new ConstantArrayType( + $type->getKeyTypes(), + array_map(static function (Type $valueType) use ($traverse): Type { + return $traverse($valueType); + }, $type->getValueTypes()), + $type->getNextAutoIndexes(), + [], + $type->isList() + ); + } + + if ($type instanceof ArrayType) { + return new ArrayType($type->getKeyType(), $traverse($type->getItemType())); + } + + return TypeCombinator::removeNull($type); + }); + } + } diff --git a/src/PHPStan/PregMatchTypeSpecifyingExtension.php b/src/PHPStan/PregMatchTypeSpecifyingExtension.php index 6d602bb..cf22f60 100644 --- a/src/PHPStan/PregMatchTypeSpecifyingExtension.php +++ b/src/PHPStan/PregMatchTypeSpecifyingExtension.php @@ -82,19 +82,9 @@ public function specifyTypes(MethodReflection $methodReflection, StaticCall $nod } if ( - in_array($methodReflection->getName(), ['matchStrictGroups', 'isMatchStrictGroups'], true) - && count($matchedType->getConstantArrays()) === 1 + in_array($methodReflection->getName(), ['matchStrictGroups', 'isMatchStrictGroups', 'matchAllStrictGroups', 'isMatchAllStrictGroups'], true) ) { - $matchedType = $matchedType->getConstantArrays()[0]; - $matchedType = new ConstantArrayType( - $matchedType->getKeyTypes(), - array_map(static function (Type $valueType): Type { - return TypeCombinator::removeNull($valueType); - }, $matchedType->getValueTypes()), - $matchedType->getNextAutoIndexes(), - [], - $matchedType->isList() - ); + $matchedType = PregMatchFlags::removeNullFromMatches($matchedType); } $overwrite = false; diff --git a/tests/PHPStanTests/UnsafeStrictGroupsCallRuleTest.php b/tests/PHPStanTests/UnsafeStrictGroupsCallRuleTest.php index 2264efd..3ebb382 100644 --- a/tests/PHPStanTests/UnsafeStrictGroupsCallRuleTest.php +++ b/tests/PHPStanTests/UnsafeStrictGroupsCallRuleTest.php @@ -44,6 +44,10 @@ public function testRule(): void 'The isMatchStrictGroups call is potentially unsafe as $matches\' type could not be inferred.', 86, ], + [ + 'The isMatchAllStrictGroups call is unsafe as match groups "test", "1" are optional and may be null.', + 114 + ] ]); } diff --git a/tests/PHPStanTests/nsrt/preg-match.php b/tests/PHPStanTests/nsrt/preg-match.php index c71875f..38957c7 100644 --- a/tests/PHPStanTests/nsrt/preg-match.php +++ b/tests/PHPStanTests/nsrt/preg-match.php @@ -110,6 +110,10 @@ function doMatchAllStrictGroups(string $s): void assertType('array{}', $matches); } assertType('array{}|array{0: list, test: list, 1: list}', $matches); + + if (Preg::isMatchAllStrictGroups('/Price: (?£|€)?\d+/', $s, $matches)) { + assertType('array{0: list, test: list, 1: list}', $matches); + } } // disabled until https://github.com/phpstan/phpstan-src/pull/3185 can be resolved