diff --git a/src/Analyser/MutatingScope.php b/src/Analyser/MutatingScope.php index 5e28dc50ee7..daf5bb9eb37 100644 --- a/src/Analyser/MutatingScope.php +++ b/src/Analyser/MutatingScope.php @@ -2021,7 +2021,7 @@ public function enterAnonymousFunctionWithoutReflection( $isNullable = $this->isParameterValueNullable($parameter); $parameterType = $this->getFunctionType($parameter->type, $isNullable, $parameter->variadic); if ($callableParameters !== null) { - $parameterType = self::intersectButNotNever($parameterType, $this->getCallableParameterType($callableParameters, $i)); + $parameterType = self::intersectButNotNever($parameterType, $this->getCallableParameterType($parameter, $callableParameters, $i)); } $holder = ExpressionTypeHolder::createYes($parameter->var, $parameterType); $expressionTypes[$paramExprString] = $holder; @@ -2221,7 +2221,7 @@ public function enterArrowFunctionWithoutReflection(Expr\ArrowFunction $arrowFun $isNullable = $this->isParameterValueNullable($parameter); $parameterType = $this->getFunctionType($parameter->type, $isNullable, $parameter->variadic); if ($callableParameters !== null) { - $parameterType = self::intersectButNotNever($parameterType, $this->getCallableParameterType($callableParameters, $i)); + $parameterType = self::intersectButNotNever($parameterType, $this->getCallableParameterType($parameter, $callableParameters, $i)); } if (!$parameter->var instanceof Variable || !is_string($parameter->var->name)) { @@ -2290,8 +2290,12 @@ public function getFunctionType($type, bool $isNullable, bool $isVariadic): Type /** * @param ParameterReflection[] $callableParameters */ - private function getCallableParameterType(array $callableParameters, int $index): Type + private function getCallableParameterType(Node\Param $parameter, array $callableParameters, int $index): Type { + if ($parameter->variadic) { + return $this->buildVariadicArrayTypeFromCallableParameters($callableParameters, $index); + } + if (isset($callableParameters[$index])) { return $callableParameters[$index]->getType(); } @@ -2308,6 +2312,40 @@ private function getCallableParameterType(array $callableParameters, int $index) return new MixedType(); } + /** + * @param array $callableParameters + */ + private function buildVariadicArrayTypeFromCallableParameters(array $callableParameters, int $startIndex): Type + { + $elementTypes = []; + $callableParametersCount = count($callableParameters); + for ($j = $startIndex; $j < $callableParametersCount; $j++) { + $elementTypes[] = $callableParameters[$j]->getType(); + if ($callableParameters[$j]->isVariadic()) { + break; + } + } + + if ($elementTypes === [] && $callableParametersCount > 0) { + $lastParameter = array_last($callableParameters); + if ($lastParameter->isVariadic()) { + $elementTypes[] = $lastParameter->getType(); + } + } + + if ($elementTypes === []) { + return new MixedType(); + } + + $elementType = TypeCombinator::union(...$elementTypes); + + if (!$this->getPhpVersion()->supportsNamedArguments()->no()) { + return new ArrayType(new UnionType([new IntegerType(), new StringType()]), $elementType); + } + + return new IntersectionType([new ArrayType(IntegerRangeType::createAllGreaterThanOrEqualTo(0), $elementType), new AccessoryArrayListType()]); + } + public static function intersectButNotNever(Type $nativeType, Type $inferredType): Type { if ($nativeType->isSuperTypeOf($inferredType)->no()) { diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index e23c982708c..507987a2a51 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -2965,7 +2965,15 @@ public function createCallableParameters(Scope $scope, Expr $closureExpr, ?array continue; } - $type = $scope->getType($args[$index]->value); + if ($callableParameter->isVariadic()) { + $argTypes = []; + for ($j = $index; $j < count($args); $j++) { + $argTypes[] = $scope->getType($args[$j]->value); + } + $type = TypeCombinator::union(...$argTypes); + } else { + $type = $scope->getType($args[$index]->value); + } $callableParameters[$index] = new NativeParameterReflection( $callableParameter->getName(), $callableParameter->isOptional(), diff --git a/tests/PHPStan/Analyser/nsrt/bug-9240-php7.php b/tests/PHPStan/Analyser/nsrt/bug-9240-php7.php new file mode 100644 index 00000000000..84abb87e6a3 --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-9240-php7.php @@ -0,0 +1,108 @@ + 1, 'name' => 'x']; + $postFiles = [$v, $v, $v]; + + return $fx(...$postFiles); + } +} + +function test(): void +{ + $u = new Upload(); + $u->onUpload(function (...$postFiles): bool { + assertType('list', $postFiles); + foreach ($postFiles as $postFile) { + assertType('array{error: int, name: string}', $postFile); + if ($postFile['error'] !== 0) { + return false; + } + } + + return true; + }); +} + +/** + * @param \Closure(int, string, float): void $fx + */ +function mixedTypes(\Closure $fx): void +{ + $fx(1, 'hello', 3.14); +} + +function testMixedTypes(): void +{ + mixedTypes(function (...$args): void { + assertType('list', $args); + }); +} + +/** + * @param \Closure(int, string): void $fx + */ +function twoParams(\Closure $fx): void +{ + $fx(1, 'hello'); +} + +function testVariadicNotFirst(): void +{ + twoParams(function (int $first, string ...$rest): void { + assertType('int', $first); + assertType('list', $rest); + }); +} + +// Arrow function version +function testArrowFunction(): void +{ + $u = new Upload(); + $u->onUpload(fn (...$postFiles) => assertType('list', $postFiles) || true); +} + +// Immediately-invoked closure with variadic +function testImmediatelyInvoked(): void +{ + $result = (function (...$args): string { + assertType('list<1|3.14|\'hello\'>', $args); + return implode(', ', $args); + })(1, 'hello', 3.14); +} + +// Immediately-invoked arrow function with variadic +function testImmediatelyInvokedArrow(): void +{ + $result = (fn (...$args) => assertType('list<1|3.14|\'hello\'>', $args))(1, 'hello', 3.14); +} + +// Variadic param with last callable parameter also variadic +/** + * @param \Closure(string, int...): void $fx + */ +function variadicExpected(\Closure $fx): void +{ +} + +function testVariadicExpected(): void +{ + variadicExpected(function (...$args): void { + assertType('list', $args); + }); +} diff --git a/tests/PHPStan/Analyser/nsrt/bug-9240.php b/tests/PHPStan/Analyser/nsrt/bug-9240.php new file mode 100644 index 00000000000..e328622ebcf --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/bug-9240.php @@ -0,0 +1,108 @@ += 8.0 + +declare(strict_types = 1); + +namespace Bug9240; + +use function PHPStan\Testing\assertType; + +/** + * @phpstan-type PhpFileArray array{error: int, name: string} + */ +class Upload +{ + /** + * @param \Closure(PhpFileArray, PhpFileArray, PhpFileArray): bool $fx + */ + public function onUpload(\Closure $fx): bool + { + $v = ['error' => 1, 'name' => 'x']; + $postFiles = [$v, $v, $v]; + + return $fx(...$postFiles); + } +} + +function test(): void +{ + $u = new Upload(); + $u->onUpload(function (...$postFiles): bool { + assertType('array', $postFiles); + foreach ($postFiles as $postFile) { + assertType('array{error: int, name: string}', $postFile); + if ($postFile['error'] !== 0) { + return false; + } + } + + return true; + }); +} + +/** + * @param \Closure(int, string, float): void $fx + */ +function mixedTypes(\Closure $fx): void +{ + $fx(1, 'hello', 3.14); +} + +function testMixedTypes(): void +{ + mixedTypes(function (...$args): void { + assertType('array', $args); + }); +} + +/** + * @param \Closure(int, string): void $fx + */ +function twoParams(\Closure $fx): void +{ + $fx(1, 'hello'); +} + +function testVariadicNotFirst(): void +{ + twoParams(function (int $first, string ...$rest): void { + assertType('int', $first); + assertType('array', $rest); + }); +} + +// Arrow function version +function testArrowFunction(): void +{ + $u = new Upload(); + $u->onUpload(fn (...$postFiles) => assertType('array', $postFiles) || true); +} + +// Immediately-invoked closure with variadic +function testImmediatelyInvoked(): void +{ + $result = (function (...$args): string { + assertType('array', $args); + return implode(', ', $args); + })(1, 'hello', 3.14); +} + +// Immediately-invoked arrow function with variadic +function testImmediatelyInvokedArrow(): void +{ + $result = (fn (...$args) => assertType('array', $args))(1, 'hello', 3.14); +} + +// Variadic param with last callable parameter also variadic +/** + * @param \Closure(string, int...): void $fx + */ +function variadicExpected(\Closure $fx): void +{ +} + +function testVariadicExpected(): void +{ + variadicExpected(function (...$args): void { + assertType('array', $args); + }); +} diff --git a/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php b/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php index 31449040b45..024f71bbcb9 100644 --- a/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php +++ b/tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php @@ -1321,6 +1321,11 @@ public function testArraySearchExisting(): void ]); } + public function testBug9240(): void + { + $this->analyse([__DIR__ . '/data/bug-9240.php'], []); + } + #[RequiresPhp('>= 8.4.0')] public function testArrayFindKeyExisting(): void { diff --git a/tests/PHPStan/Rules/Arrays/data/bug-9240.php b/tests/PHPStan/Rules/Arrays/data/bug-9240.php new file mode 100644 index 00000000000..139b49b38b1 --- /dev/null +++ b/tests/PHPStan/Rules/Arrays/data/bug-9240.php @@ -0,0 +1,36 @@ + 1, 'name' => 'x']; + $postFiles = [$v, $v, $v]; + + return $fx(...$postFiles); + } +} + +function test(): void +{ + $u = new Upload(); + $u->onUpload(function (...$postFiles): bool { + foreach ($postFiles as $postFile) { + if ($postFile['error'] !== 0) { + return false; + } + } + + return true; + }); +}