diff --git a/conf/config.neon b/conf/config.neon index 6b54eb7093..5b4aa5a7f4 100644 --- a/conf/config.neon +++ b/conf/config.neon @@ -895,6 +895,11 @@ services: tags: - phpstan.broker.dynamicFunctionReturnTypeExtension + - + class: PHPStan\Type\Php\PregSplitDynamicReturnTypeExtension + tags: + - phpstan.broker.dynamicFunctionReturnTypeExtension + - class: PHPStan\Type\Php\ReplaceFunctionsDynamicReturnTypeExtension tags: diff --git a/src/Type/Php/PregSplitDynamicReturnTypeExtension.php b/src/Type/Php/PregSplitDynamicReturnTypeExtension.php new file mode 100644 index 0000000000..912363f117 --- /dev/null +++ b/src/Type/Php/PregSplitDynamicReturnTypeExtension.php @@ -0,0 +1,77 @@ +reflectionProvider = $reflectionProvider; + } + + + public function isFunctionSupported(FunctionReflection $functionReflection): bool + { + return strtolower($functionReflection->getName()) === 'preg_split'; + } + + + public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): Type + { + $flagsArg = $functionCall->args[3] ?? null; + + if ($this->hasFlag($this->getConstant('PREG_SPLIT_OFFSET_CAPTURE'), $flagsArg, $scope)) { + $type = new ArrayType( + new IntegerType(), + new ConstantArrayType([new ConstantIntegerType(0), new ConstantIntegerType(1)], [new StringType(), new IntegerType()]) + ); + return TypeCombinator::union($type, new ConstantBooleanType(false)); + } + + return ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType(); + } + + + private function hasFlag(int $flag, ?Arg $expression, Scope $scope): bool + { + if ($expression === null) { + return false; + } + + $type = $scope->getType($expression->value); + return $type instanceof ConstantIntegerType && ($type->getValue() & $flag) === $flag; + } + + + private function getConstant(string $constantName): int + { + $constant = $this->reflectionProvider->getConstant(new Name($constantName), null); + $valueType = $constant->getValueType(); + if (!$valueType instanceof ConstantIntegerType) { + throw new \PHPStan\ShouldNotHappenException(sprintf('Constant %s does not have integer type.', $constantName)); + } + + return $valueType->getValue(); + } + +} diff --git a/tests/PHPStan/Analyser/NodeScopeResolverTest.php b/tests/PHPStan/Analyser/NodeScopeResolverTest.php index 4424ae926a..f7613e0172 100644 --- a/tests/PHPStan/Analyser/NodeScopeResolverTest.php +++ b/tests/PHPStan/Analyser/NodeScopeResolverTest.php @@ -10048,6 +10048,11 @@ public function dataBug2899(): array return $this->gatherAssertTypes(__DIR__ . '/data/bug-2899.php'); } + public function dataPregSplitReturnType(): array + { + return $this->gatherAssertTypes(__DIR__ . '/data/preg_split.php'); + } + /** * @dataProvider dataBug2574 * @dataProvider dataBug2577 @@ -10116,6 +10121,7 @@ public function dataBug2899(): array * @dataProvider dataBug3133 * @dataProvider dataBug2550 * @dataProvider dataBug2899 + * @dataProvider dataPregSplitReturnType * @param string $assertType * @param string $file * @param mixed ...$args diff --git a/tests/PHPStan/Analyser/data/preg_split.php b/tests/PHPStan/Analyser/data/preg_split.php new file mode 100644 index 0000000000..2954fdc6fc --- /dev/null +++ b/tests/PHPStan/Analyser/data/preg_split.php @@ -0,0 +1,8 @@ +|false', preg_split('/-/', '1-2-3')); +assertType('array|false', preg_split('/-/', '1-2-3', -1, PREG_SPLIT_NO_EMPTY)); +assertType('array|false', preg_split('/-/', '1-2-3', -1, PREG_SPLIT_OFFSET_CAPTURE)); +assertType('array|false', preg_split('/-/', '1-2-3', -1, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_OFFSET_CAPTURE));