diff --git a/src/Type/Nette/StringsMatchAllDynamicReturnTypeExtension.php b/src/Type/Nette/StringsMatchAllDynamicReturnTypeExtension.php index 9ca2f94..3640806 100644 --- a/src/Type/Nette/StringsMatchAllDynamicReturnTypeExtension.php +++ b/src/Type/Nette/StringsMatchAllDynamicReturnTypeExtension.php @@ -3,13 +3,21 @@ namespace PHPStan\Type\Nette; use Nette\Utils\Strings; +use PhpParser\Node\Arg; use PhpParser\Node\Expr\StaticCall; use PHPStan\Analyser\Scope; use PHPStan\Reflection\MethodReflection; use PHPStan\TrinaryLogic; +use PHPStan\Type\Constant\ConstantBooleanType; +use PHPStan\Type\Constant\ConstantIntegerType; use PHPStan\Type\DynamicStaticMethodReturnTypeExtension; use PHPStan\Type\Php\RegexArrayShapeMatcher; use PHPStan\Type\Type; +use function array_key_exists; +use const PREG_OFFSET_CAPTURE; +use const PREG_PATTERN_ORDER; +use const PREG_SET_ORDER; +use const PREG_UNMATCHED_AS_NULL; class StringsMatchAllDynamicReturnTypeExtension implements DynamicStaticMethodReturnTypeExtension { @@ -36,17 +44,47 @@ public function getTypeFromStaticMethodCall(MethodReflection $methodReflection, { $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); + return $this->regexArrayShapeMatcher->matchAllExpr( + $patternArg->value, + $this->resolveFlagsType($args, $scope), + TrinaryLogic::createYes(), + $scope + ); + } + + /** + * @param array $args + */ + private function resolveFlagsType(array $args, Scope $scope): ConstantIntegerType + { + if (!array_key_exists(2, $args)) { + return new ConstantIntegerType(PREG_SET_ORDER); + } + + $captureOffsetType = $scope->getType($args[2]->value); + + if ($captureOffsetType instanceof ConstantIntegerType) { + $captureOffset = $captureOffsetType->getValue(); + $flags = ($captureOffset & PREG_PATTERN_ORDER) === PREG_PATTERN_ORDER ? $captureOffset : ($captureOffset | PREG_SET_ORDER); + + return new ConstantIntegerType($flags); } - return $this->regexArrayShapeMatcher->matchAllExpr($patternArg->value, $flagsType, TrinaryLogic::createYes(), $scope); + $unmatchedAsNullType = array_key_exists(4, $args) ? $scope->getType($args[4]->value) : new ConstantBooleanType(false); + $patternOrderType = array_key_exists(5, $args) ? $scope->getType($args[5]->value) : new ConstantBooleanType(false); + + $captureOffset = $captureOffsetType->isTrue()->yes(); + $unmatchedAsNull = $unmatchedAsNullType->isTrue()->yes(); + $patternOrder = $patternOrderType->isTrue()->yes(); + + return new ConstantIntegerType( + ($captureOffset ? PREG_OFFSET_CAPTURE : 0) | ($unmatchedAsNull ? PREG_UNMATCHED_AS_NULL : 0) | ($patternOrder ? PREG_PATTERN_ORDER : 0) + ); } } diff --git a/src/Type/Nette/StringsMatchDynamicReturnTypeExtension.php b/src/Type/Nette/StringsMatchDynamicReturnTypeExtension.php index a2489c5..cbe732f 100644 --- a/src/Type/Nette/StringsMatchDynamicReturnTypeExtension.php +++ b/src/Type/Nette/StringsMatchDynamicReturnTypeExtension.php @@ -3,15 +3,21 @@ namespace PHPStan\Type\Nette; use Nette\Utils\Strings; +use PhpParser\Node\Arg; use PhpParser\Node\Expr\StaticCall; use PHPStan\Analyser\Scope; 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\Type; use PHPStan\Type\TypeCombinator; +use function array_key_exists; +use const PREG_OFFSET_CAPTURE; +use const PREG_UNMATCHED_AS_NULL; class StringsMatchDynamicReturnTypeExtension implements DynamicStaticMethodReturnTypeExtension { @@ -38,17 +44,18 @@ public function getTypeFromStaticMethodCall(MethodReflection $methodReflection, { $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->matchExpr( + $patternArg->value, + $this->resolveFlagsType($args, $scope), + TrinaryLogic::createYes(), + $scope + ); - $arrayShape = $this->regexArrayShapeMatcher->matchExpr($patternArg->value, $flagsType, TrinaryLogic::createYes(), $scope); if ($arrayShape === null) { return null; } @@ -56,4 +63,29 @@ public function getTypeFromStaticMethodCall(MethodReflection $methodReflection, return TypeCombinator::union($arrayShape, new NullType()); } + /** + * @param array $args + */ + private function resolveFlagsType(array $args, Scope $scope): ConstantIntegerType + { + if (!array_key_exists(2, $args)) { + return new ConstantIntegerType(0); + } + + $captureOffsetType = $scope->getType($args[2]->value); + + if ($captureOffsetType instanceof ConstantIntegerType) { + return $captureOffsetType; + } + + $unmatchedAsNullType = array_key_exists(4, $args) ? $scope->getType($args[4]->value) : new ConstantBooleanType(false); + + $captureOffset = $captureOffsetType->isTrue()->yes(); + $unmatchedAsNull = $unmatchedAsNullType->isTrue()->yes(); + + return new ConstantIntegerType( + ($captureOffset ? PREG_OFFSET_CAPTURE : 0) | ($unmatchedAsNull ? PREG_UNMATCHED_AS_NULL : 0) + ); + } + } diff --git a/tests/Type/Nette/data/strings-match.php b/tests/Type/Nette/data/strings-match.php index b47ea0f..eb40fed 100644 --- a/tests/Type/Nette/data/strings-match.php +++ b/tests/Type/Nette/data/strings-match.php @@ -33,3 +33,13 @@ 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<''|'ab'>, 2: list<''|'ab'>}", $result); }; + +function (string $s): void { + $result = Strings::matchAll($s, '/ab(?P\d+)(?Pab)?/', false, 0, false, true); + assertType("array{0: list, num: list, 1: list, suffix: list<''|'ab'>, 2: list<''|'ab'>}", $result); +}; + +function (string $s): void { + $result = Strings::matchAll($lineContent, '~\[gallery ids=(„|")(?([0-9]+,? ?)+)(“|")~'); + assertType('list', $result); +};