diff --git a/src/Type/Php/ArrayColumnFunctionReturnTypeExtension.php b/src/Type/Php/ArrayColumnFunctionReturnTypeExtension.php index 43da514a62..f044cc5f8f 100644 --- a/src/Type/Php/ArrayColumnFunctionReturnTypeExtension.php +++ b/src/Type/Php/ArrayColumnFunctionReturnTypeExtension.php @@ -4,6 +4,7 @@ use PhpParser\Node\Expr\FuncCall; use PHPStan\Analyser\Scope; +use PHPStan\Php\PhpVersion; use PHPStan\Reflection\FunctionReflection; use PHPStan\Reflection\ParametersAcceptorSelector; use PHPStan\ShouldNotHappenException; @@ -25,6 +26,10 @@ class ArrayColumnFunctionReturnTypeExtension implements DynamicFunctionReturnTypeExtension { + public function __construct(private PhpVersion $phpVersion) + { + } + public function isFunctionSupported(FunctionReflection $functionReflection): bool { return $functionReflection->getName() === 'array_column'; @@ -187,15 +192,17 @@ private function castToArrayKeyType(Type $type): Type { $isArray = $type->isArray(); if ($isArray->yes()) { - return new IntegerType(); + return $this->phpVersion->throwsTypeErrorForInternalFunctions() ? new NeverType() : new IntegerType(); } if ($isArray->no()) { return ArrayType::castToArrayKeyType($type); } - return TypeCombinator::union( - ArrayType::castToArrayKeyType(TypeCombinator::remove($type, new ArrayType(new MixedType(), new MixedType()))), - new IntegerType(), - ); + $withoutArrayType = TypeCombinator::remove($type, new ArrayType(new MixedType(), new MixedType())); + $keyType = ArrayType::castToArrayKeyType($withoutArrayType); + if ($this->phpVersion->throwsTypeErrorForInternalFunctions()) { + return $keyType; + } + return TypeCombinator::union($keyType, new IntegerType()); } } diff --git a/tests/PHPStan/Analyser/NodeScopeResolverTest.php b/tests/PHPStan/Analyser/NodeScopeResolverTest.php index 86a9cd075f..8e2186cab3 100644 --- a/tests/PHPStan/Analyser/NodeScopeResolverTest.php +++ b/tests/PHPStan/Analyser/NodeScopeResolverTest.php @@ -666,7 +666,14 @@ public function dataFileAsserts(): iterable yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-6399.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-4357.php'); yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-5817.php'); + yield from $this->gatherAssertTypes(__DIR__ . '/data/array-column.php'); + if (PHP_VERSION_ID >= 80000) { + yield from $this->gatherAssertTypes(__DIR__ . '/data/array-column-php8.php'); + } else { + yield from $this->gatherAssertTypes(__DIR__ . '/data/array-column-php7.php'); + } + yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-6497.php'); if (PHP_VERSION_ID >= 70400) { diff --git a/tests/PHPStan/Analyser/data/array-column-php7.php b/tests/PHPStan/Analyser/data/array-column-php7.php new file mode 100644 index 0000000000..5b634d5146 --- /dev/null +++ b/tests/PHPStan/Analyser/data/array-column-php7.php @@ -0,0 +1,22 @@ + $array */ + public function testConstantArray1(array $array): void + { + assertType('array', array_column($array, 'column', 'key')); + } + + /** @param array $array */ + public function testConstantArray2(array $array): void + { + assertType('array', array_column($array, 'column', 'key')); + } + +} diff --git a/tests/PHPStan/Analyser/data/array-column-php8.php b/tests/PHPStan/Analyser/data/array-column-php8.php new file mode 100644 index 0000000000..78d96aa109 --- /dev/null +++ b/tests/PHPStan/Analyser/data/array-column-php8.php @@ -0,0 +1,22 @@ + $array */ + public function testConstantArray1(array $array): void + { + assertType('array<*NEVER*, string>', array_column($array, 'column', 'key')); + } + + /** @param array $array */ + public function testConstantArray2(array $array): void + { + assertType('array', array_column($array, 'column', 'key')); + } + +} diff --git a/tests/PHPStan/Analyser/data/array-column.php b/tests/PHPStan/Analyser/data/array-column.php index 8a48236ad4..c76aaa017a 100644 --- a/tests/PHPStan/Analyser/data/array-column.php +++ b/tests/PHPStan/Analyser/data/array-column.php @@ -5,125 +5,204 @@ use DOMElement; use function PHPStan\Testing\assertType; -function testArrays(array $array): void -{ - /** @var array> $array */ - assertType('array', array_column($array, 'column')); - assertType('array', array_column($array, 'column', 'key')); - assertType('array>', array_column($array, null, 'key')); - - /** @var non-empty-array> $array */ - // Note: Array may still be empty! - assertType('array', array_column($array, 'column')); - - /** @var array{} $array */ - assertType('array{}', array_column($array, 'column')); - assertType('array{}', array_column($array, 'column', 'key')); - assertType('array{}', array_column($array, null, 'key')); - - /** @var array> $array */ - assertType('array', array_column($array, 'column', 'key')); - /** @var array> $array */ - assertType('array', array_column($array, 'column', 'key')); - /** @var array> $array */ - assertType('array', array_column($array, 'column', 'key')); - /** @var array> $array */ - assertType('array<\'\'|int, null>', array_column($array, 'column', 'key')); - /** @var array> $array */ - assertType('array', array_column($array, 'column', 'key')); -} -function testConstantArrays(array $array): void +class ArrayColumnTest { - /** @var array $array */ - assertType('array', array_column($array, 'column')); - assertType('array', array_column($array, 'column', 'key')); - assertType('array', array_column($array, null, 'key')); - - /** @var array $array */ - assertType('array{}', array_column($array, 'foo')); - assertType('array{}', array_column($array, 'foo', 'key')); - - /** @var array{array{column: string, key: 'bar'}} $array */ - assertType("array{string}", array_column($array, 'column')); - assertType("array{bar: string}", array_column($array, 'column', 'key')); - assertType("array{bar: array{column: string, key: 'bar'}}", array_column($array, null, 'key')); - - /** @var array{array{column: string, key: string}} $array */ - assertType("non-empty-array", array_column($array, 'column', 'key')); - assertType("non-empty-array", array_column($array, null, 'key')); - - /** @var array $array */ - assertType("array", array_column($array, 'column')); - assertType("array<'bar'|int, 'foo'>", array_column($array, 'column', 'key')); - assertType("array<'bar'|int, array{column?: 'foo', key?: 'bar'}>", array_column($array, null, 'key')); - - /** @var array $array */ - assertType('array', array_column($array, mt_rand(0, 1) === 0 ? 'column1' : 'column2')); - - /** @var non-empty-array $array */ - assertType('non-empty-array', array_column($array, 'column')); - assertType('non-empty-array', array_column($array, 'column', 'key')); - assertType('non-empty-array', array_column($array, null, 'key')); - - /** @var array $array */ - assertType('array', array_column($array, 'column', 'key')); - /** @var array $array */ - assertType('array<0|1, string>', array_column($array, 'column', 'key')); - /** @var array $array */ - assertType('array<1, string>', array_column($array, 'column', 'key')); - /** @var array $array */ - assertType('array<\'\', string>', array_column($array, 'column', 'key')); - /** @var array $array */ - assertType('array', array_column($array, 'column', 'key')); -} - -function testImprecise(array $array): void { - // These cases aren't handled precisely and will return non-constant arrays. - - /** @var array{array{column?: 'foo', key: 'bar'}} $array */ - assertType("array", array_column($array, 'column')); - assertType("array<'bar', 'foo'>", array_column($array, 'column', 'key')); - assertType("array{bar: array{column?: 'foo', key: 'bar'}}", array_column($array, null, 'key')); - /** @var array{array{column: 'foo', key?: 'bar'}} $array */ - assertType("non-empty-array<'bar'|int, 'foo'>", array_column($array, 'column', 'key')); - assertType("non-empty-array<'bar'|int, array{column: 'foo', key?: 'bar'}>", array_column($array, null, 'key')); + /** @param array> $array */ + public function testArray1(array $array): void + { + assertType('array', array_column($array, 'column')); + assertType('array', array_column($array, 'column', 'key')); + assertType('array>', array_column($array, null, 'key')); + } + + /** @param non-empty-array> $array */ + public function testArray2(array $array): void + { + // Note: Array may still be empty! + assertType('array', array_column($array, 'column')); + } + + /** @param array{} $array */ + public function testArray3(array $array): void + { + assertType('array{}', array_column($array, 'column')); + assertType('array{}', array_column($array, 'column', 'key')); + assertType('array{}', array_column($array, null, 'key')); + } + + /** @param array> $array */ + public function testArray4(array $array): void + { + assertType('array', array_column($array, 'column', 'key')); + } + + /** @param array> $array */ + public function testArray5(array $array): void + { + assertType('array', array_column($array, 'column', 'key')); + } + + /** @param array> $array */ + public function testArray6(array $array): void + { + assertType('array', array_column($array, 'column', 'key')); + } + + /** @param array> $array */ + public function testArray7(array $array): void + { + assertType('array<\'\'|int, null>', array_column($array, 'column', 'key')); + } + + /** @param array> $array */ + public function testArray8(array $array): void + { + assertType('array', array_column($array, 'column', 'key')); + } + + /** @param array $array */ + public function testConstantArray1(array $array): void + { + assertType('array', array_column($array, 'column')); + assertType('array', array_column($array, 'column', 'key')); + assertType('array', array_column($array, null, 'key')); + } + + /** @param array $array */ + public function testConstantArray2(array $array): void + { + assertType('array{}', array_column($array, 'foo')); + assertType('array{}', array_column($array, 'foo', 'key')); + } + + /** @param array{array{column: string, key: 'bar'}} $array */ + public function testConstantArray3(array $array): void + { + assertType("array{string}", array_column($array, 'column')); + assertType("array{bar: string}", array_column($array, 'column', 'key')); + assertType("array{bar: array{column: string, key: 'bar'}}", array_column($array, null, 'key')); + } + + /** @param array{array{column: string, key: string}} $array */ + public function testConstantArray4(array $array): void + { + assertType("non-empty-array", array_column($array, 'column', 'key')); + assertType("non-empty-array", array_column($array, null, 'key')); + } + + /** @param array $array */ + public function testConstantArray5(array $array): void + { + assertType("array", array_column($array, 'column')); + assertType("array<'bar'|int, 'foo'>", array_column($array, 'column', 'key')); + assertType("array<'bar'|int, array{column?: 'foo', key?: 'bar'}>", array_column($array, null, 'key')); + } + + /** @param array $array */ + public function testConstantArray6(array $array): void + { + assertType('array', array_column($array, mt_rand(0, 1) === 0 ? 'column1' : 'column2')); + } + + /** @param non-empty-array $array */ + public function testConstantArray7(array $array): void + { + assertType('non-empty-array', array_column($array, 'column')); + assertType('non-empty-array', array_column($array, 'column', 'key')); + assertType('non-empty-array', array_column($array, null, 'key')); + } + + /** @param array $array */ + public function testConstantArray8(array $array): void + { + assertType('array', array_column($array, 'column', 'key')); + } + + /** @param array $array */ + public function testConstantArray9(array $array): void + { + assertType('array<0|1, string>', array_column($array, 'column', 'key')); + } + + /** @param array $array */ + public function testConstantArray10(array $array): void + { + assertType('array<1, string>', array_column($array, 'column', 'key')); + } + + /** @param array $array */ + public function testConstantArray11(array $array): void + { + assertType('array<\'\', string>', array_column($array, 'column', 'key')); + } - /** @var array{array{column: 'foo', key: 'bar'}}|array> $array */ - assertType('array', array_column($array, 'column')); - assertType('array', array_column($array, 'column', 'key')); + // These cases aren't handled precisely and will return non-constant arrays. - /** @var array{0?: array{column: 'foo', key: 'bar'}} $array */ - assertType("array", array_column($array, 'column')); - assertType("array<'bar', 'foo'>", array_column($array, 'column', 'key')); -} + /** @param array{array{column?: 'foo', key: 'bar'}} $array */ + public function testImprecise1(array $array): void + { + assertType("array", array_column($array, 'column')); + assertType("array<'bar', 'foo'>", array_column($array, 'column', 'key')); + assertType("array{bar: array{column?: 'foo', key: 'bar'}}", array_column($array, null, 'key')); + } + + /** @param array{array{column: 'foo', key?: 'bar'}} $array */ + public function testImprecise2(array $array): void + { + assertType("non-empty-array<'bar'|int, 'foo'>", array_column($array, 'column', 'key')); + assertType("non-empty-array<'bar'|int, array{column: 'foo', key?: 'bar'}>", array_column($array, null, 'key')); + } + + /** @param array{array{column: 'foo', key: 'bar'}}|array> $array */ + public function testImprecise3(array $array): void + { + assertType('array', array_column($array, 'column')); + assertType('array', array_column($array, 'column', 'key')); + } + + /** @param array{0?: array{column: 'foo', key: 'bar'}} $array */ + public function testImprecise4(array $array): void + { + assertType("array", array_column($array, 'column')); + assertType("array<'bar', 'foo'>", array_column($array, 'column', 'key')); + } + + /** @param array $array */ + public function testImprecise5(array $array): void + { + assertType('array', array_column($array, 'nodeName')); + assertType('array', array_column($array, 'nodeName', 'tagName')); + assertType('array', array_column($array, null, 'tagName')); + assertType('array', array_column($array, 'foo')); + assertType('array', array_column($array, 'foo', 'tagName')); + assertType('array', array_column($array, 'nodeName', 'foo')); + assertType('array', array_column($array, null, 'foo')); + } + + /** @param non-empty-array $array */ + public function testObjects1(array $array): void + { + assertType('non-empty-array', array_column($array, 'nodeName')); + assertType('non-empty-array', array_column($array, 'nodeName', 'tagName')); + assertType('non-empty-array', array_column($array, null, 'tagName')); + assertType('array', array_column($array, 'foo')); + assertType('array', array_column($array, 'foo', 'tagName')); + assertType('non-empty-array', array_column($array, 'nodeName', 'foo')); + assertType('non-empty-array', array_column($array, null, 'foo')); + } + + /** @param array{DOMElement} $array */ + public function testObjects2(array $array): void + { + assertType('array{string}', array_column($array, 'nodeName')); + assertType('non-empty-array', array_column($array, 'nodeName', 'tagName')); + assertType('non-empty-array', array_column($array, null, 'tagName')); + assertType('array', array_column($array, 'foo')); + assertType('array', array_column($array, 'foo', 'tagName')); + assertType('non-empty-array', array_column($array, 'nodeName', 'foo')); + assertType('non-empty-array', array_column($array, null, 'foo')); + } -function testObjects(array $array): void { - /** @var array $array */ - assertType('array', array_column($array, 'nodeName')); - assertType('array', array_column($array, 'nodeName', 'tagName')); - assertType('array', array_column($array, null, 'tagName')); - assertType('array', array_column($array, 'foo')); - assertType('array', array_column($array, 'foo', 'tagName')); - assertType('array', array_column($array, 'nodeName', 'foo')); - assertType('array', array_column($array, null, 'foo')); - - /** @var non-empty-array $array */ - assertType('non-empty-array', array_column($array, 'nodeName')); - assertType('non-empty-array', array_column($array, 'nodeName', 'tagName')); - assertType('non-empty-array', array_column($array, null, 'tagName')); - assertType('array', array_column($array, 'foo')); - assertType('array', array_column($array, 'foo', 'tagName')); - assertType('non-empty-array', array_column($array, 'nodeName', 'foo')); - assertType('non-empty-array', array_column($array, null, 'foo')); - - /** @var array{DOMElement} $array */ - assertType('array{string}', array_column($array, 'nodeName')); - assertType('non-empty-array', array_column($array, 'nodeName', 'tagName')); - assertType('non-empty-array', array_column($array, null, 'tagName')); - assertType('array', array_column($array, 'foo')); - assertType('array', array_column($array, 'foo', 'tagName')); - assertType('non-empty-array', array_column($array, 'nodeName', 'foo')); - assertType('non-empty-array', array_column($array, null, 'foo')); }