Skip to content

Fix array_column() with explicit null $index_key #3970

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: 2.1.x
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 7 additions & 6 deletions src/Type/Php/ArrayColumnFunctionReturnTypeExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
use PHPStan\Type\IntegerType;
use PHPStan\Type\MixedType;
use PHPStan\Type\NeverType;
use PHPStan\Type\NullType;
use PHPStan\Type\Type;
use PHPStan\Type\TypeCombinator;
use function count;
Expand All @@ -42,7 +43,7 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection,

$arrayType = $scope->getType($functionCall->getArgs()[0]->value);
$columnType = $scope->getType($functionCall->getArgs()[1]->value);
$indexType = $numArgs >= 3 ? $scope->getType($functionCall->getArgs()[2]->value) : null;
$indexType = $numArgs >= 3 ? $scope->getType($functionCall->getArgs()[2]->value) : new NullType();

$constantArrayTypes = $arrayType->getConstantArrays();
if (count($constantArrayTypes) === 1) {
Expand All @@ -55,7 +56,7 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection,
return $this->handleAnyArray($arrayType, $columnType, $indexType, $scope);
}

private function handleAnyArray(Type $arrayType, Type $columnType, ?Type $indexType, Scope $scope): Type
private function handleAnyArray(Type $arrayType, Type $columnType, Type $indexType, Scope $scope): Type
{
$iterableAtLeastOnce = $arrayType->isIterableAtLeastOnce();
if ($iterableAtLeastOnce->no()) {
Expand All @@ -77,7 +78,7 @@ private function handleAnyArray(Type $arrayType, Type $columnType, ?Type $indexT
return new ConstantArrayType([], []);
}

if ($indexType !== null) {
if (!$indexType->isNull()->yes()) {
$type = $this->getOffsetOrProperty($iterableValueType, $indexType, $scope, false);
if ($type !== null) {
$returnKeyType = $type;
Expand All @@ -98,14 +99,14 @@ private function handleAnyArray(Type $arrayType, Type $columnType, ?Type $indexT
if ($iterableAtLeastOnce->yes()) {
$returnType = TypeCombinator::intersect($returnType, new NonEmptyArrayType());
}
if ($indexType === null) {
if ($indexType->isNull()->yes()) {
$returnType = TypeCombinator::intersect($returnType, new AccessoryArrayListType());
}

return $returnType;
}

private function handleConstantArray(ConstantArrayType $arrayType, Type $columnType, ?Type $indexType, Scope $scope): ?Type
private function handleConstantArray(ConstantArrayType $arrayType, Type $columnType, Type $indexType, Scope $scope): ?Type
{
$builder = ConstantArrayTypeBuilder::createEmpty();

Expand All @@ -118,7 +119,7 @@ private function handleConstantArray(ConstantArrayType $arrayType, Type $columnT
continue;
}

if ($indexType !== null) {
if (!$indexType->isNull()->yes()) {
$type = $this->getOffsetOrProperty($iterableValueType, $indexType, $scope, false);
if ($type !== null) {
$keyType = $type;
Expand Down
19 changes: 19 additions & 0 deletions tests/PHPStan/Analyser/nsrt/array-column-php82.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ class ArrayColumnTest
public function testArray1(array $array): void
{
assertType('list<string>', array_column($array, 'column'));
assertType('list<string>', array_column($array, 'column', null));
assertType('array<int|string, string>', array_column($array, 'column', 'key'));
assertType('array<int|string, array<string, string>>', array_column($array, null, 'key'));
}
Expand All @@ -22,12 +23,14 @@ public function testArray2(array $array): void
{
// Note: Array may still be empty!
assertType('list<string>', array_column($array, 'column'));
assertType('list<string>', array_column($array, 'column', null));
}

/** @param array{} $array */
public function testArray3(array $array): void
{
assertType('array{}', array_column($array, 'column'));
assertType('array{}', array_column($array, 'column', null));
assertType('array{}', array_column($array, 'column', 'key'));
assertType('array{}', array_column($array, null, 'key'));
}
Expand Down Expand Up @@ -66,6 +69,7 @@ public function testArray8(array $array): void
public function testConstantArray1(array $array): void
{
assertType('list<string>', array_column($array, 'column'));
assertType('list<string>', array_column($array, 'column', null));
assertType('array<string, string>', array_column($array, 'column', 'key'));
assertType('array<string, array{column: string, key: string}>', array_column($array, null, 'key'));
}
Expand All @@ -74,13 +78,15 @@ public function testConstantArray1(array $array): void
public function testConstantArray2(array $array): void
{
assertType('array{}', array_column($array, 'foo'));
assertType('array{}', array_column($array, 'foo', null));
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{string}", array_column($array, 'column', null));
assertType("array{bar: string}", array_column($array, 'column', 'key'));
assertType("array{bar: array{column: string, key: 'bar'}}", array_column($array, null, 'key'));
}
Expand All @@ -96,6 +102,7 @@ public function testConstantArray4(array $array): void
public function testConstantArray5(array $array): void
{
assertType("list<'foo'>", array_column($array, 'column'));
assertType("list<'foo'>", array_column($array, 'column', null));
assertType("array<'bar'|int, 'foo'>", array_column($array, 'column', 'key'));
assertType("array<'bar'|int, array{column?: 'foo', key?: 'bar'}>", array_column($array, null, 'key'));
}
Expand All @@ -104,12 +111,14 @@ public function testConstantArray5(array $array): void
public function testConstantArray6(array $array): void
{
assertType('list<bool|string>', array_column($array, mt_rand(0, 1) === 0 ? 'column1' : 'column2'));
assertType('list<bool|string>', array_column($array, mt_rand(0, 1) === 0 ? 'column1' : 'column2', null));
}

/** @param non-empty-array<int, array{column: string, key: string}> $array */
public function testConstantArray7(array $array): void
{
assertType('non-empty-list<string>', array_column($array, 'column'));
assertType('non-empty-list<string>', array_column($array, 'column', null));
assertType('non-empty-array<string, string>', array_column($array, 'column', 'key'));
assertType('non-empty-array<string, array{column: string, key: string}>', array_column($array, null, 'key'));
}
Expand Down Expand Up @@ -142,6 +151,7 @@ public function testConstantArray11(array $array): void
public function testConstantArray12(array $array): void
{
assertType("array{0?: 'foo'}", array_column($array, 'column'));
assertType("array{0?: 'foo'}", array_column($array, 'column', null));
assertType("array{bar?: 'foo'}", array_column($array, 'column', 'key'));
}

Expand All @@ -151,6 +161,7 @@ public function testConstantArray12(array $array): void
public function testImprecise1(array $array): void
{
assertType("list<'foo'>", array_column($array, 'column'));
assertType("list<'foo'>", array_column($array, 'column', null));
assertType("array<'bar', 'foo'>", array_column($array, 'column', 'key'));
assertType("array{bar: array{column?: 'foo', key: 'bar'}}", array_column($array, null, 'key'));
}
Expand All @@ -166,16 +177,19 @@ public function testImprecise2(array $array): void
public function testImprecise3(array $array): void
{
assertType('list<string>', array_column($array, 'column'));
assertType('list<string>', array_column($array, 'column', null));
assertType('array<int|string, string>', array_column($array, 'column', 'key'));
}

/** @param array<int, DOMElement> $array */
public function testImprecise5(array $array): void
{
assertType('list<string>', array_column($array, 'nodeName'));
assertType('list<string>', array_column($array, 'nodeName', null));
assertType('array<string, string>', array_column($array, 'nodeName', 'tagName'));
assertType('array<string, DOMElement>', array_column($array, null, 'tagName'));
assertType('list', array_column($array, 'foo'));
assertType('list', array_column($array, 'foo', null));
assertType('array<string, mixed>', array_column($array, 'foo', 'tagName'));
assertType('array<string>', array_column($array, 'nodeName', 'foo'));
assertType('array<DOMElement>', array_column($array, null, 'foo'));
Expand All @@ -185,9 +199,11 @@ public function testImprecise5(array $array): void
public function testObjects1(array $array): void
{
assertType('non-empty-list<string>', array_column($array, 'nodeName'));
assertType('non-empty-list<string>', array_column($array, 'nodeName', null));
assertType('non-empty-array<string, string>', array_column($array, 'nodeName', 'tagName'));
assertType('non-empty-array<string, DOMElement>', array_column($array, null, 'tagName'));
assertType('list', array_column($array, 'foo'));
assertType('list', array_column($array, 'foo', null));
assertType('array<string, mixed>', array_column($array, 'foo', 'tagName'));
assertType('non-empty-array<string>', array_column($array, 'nodeName', 'foo'));
assertType('non-empty-array<DOMElement>', array_column($array, null, 'foo'));
Expand All @@ -197,9 +213,11 @@ public function testObjects1(array $array): void
public function testObjects2(array $array): void
{
assertType('array{string}', array_column($array, 'nodeName'));
assertType('array{string}', array_column($array, 'nodeName', null));
assertType('non-empty-array<string, string>', array_column($array, 'nodeName', 'tagName'));
assertType('non-empty-array<string, DOMElement>', array_column($array, null, 'tagName'));
assertType('list', array_column($array, 'foo'));
assertType('list', array_column($array, 'foo', null));
assertType('array<string, mixed>', array_column($array, 'foo', 'tagName'));
assertType('non-empty-array<string>', array_column($array, 'nodeName', 'foo'));
assertType('non-empty-array<DOMElement>', array_column($array, null, 'foo'));
Expand All @@ -214,6 +232,7 @@ final class Foo
public function doFoo(array $a): void
{
assertType('array{}', array_column($a, 'nodeName'));
assertType('array{}', array_column($a, 'nodeName', null));
assertType('array{}', array_column($a, 'nodeName', 'tagName'));
}

Expand Down
20 changes: 20 additions & 0 deletions tests/PHPStan/Analyser/nsrt/array-column.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ class ArrayColumnTest
public function testArray1(array $array): void
{
assertType('list<string>', array_column($array, 'column'));
assertType('list<string>', array_column($array, 'column', null));
assertType('array<int|string, string>', array_column($array, 'column', 'key'));
assertType('array<int|string, array<string, string>>', array_column($array, null, 'key'));
}
Expand All @@ -22,12 +23,14 @@ public function testArray2(array $array): void
{
// Note: Array may still be empty!
assertType('list<string>', array_column($array, 'column'));
assertType('list<string>', array_column($array, 'column', null));
}

/** @param array{} $array */
public function testArray3(array $array): void
{
assertType('array{}', array_column($array, 'column'));
assertType('array{}', array_column($array, 'column', null));
assertType('array{}', array_column($array, 'column', 'key'));
assertType('array{}', array_column($array, null, 'key'));
}
Expand Down Expand Up @@ -66,6 +69,7 @@ public function testArray8(array $array): void
public function testConstantArray1(array $array): void
{
assertType('list<string>', array_column($array, 'column'));
assertType('list<string>', array_column($array, 'column', null));
assertType('array<string, string>', array_column($array, 'column', 'key'));
assertType('array<string, array{column: string, key: string}>', array_column($array, null, 'key'));
}
Expand All @@ -74,13 +78,15 @@ public function testConstantArray1(array $array): void
public function testConstantArray2(array $array): void
{
assertType('array{}', array_column($array, 'foo'));
assertType('array{}', array_column($array, 'foo', null));
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{string}", array_column($array, 'column', null));
assertType("array{bar: string}", array_column($array, 'column', 'key'));
assertType("array{bar: array{column: string, key: 'bar'}}", array_column($array, null, 'key'));
}
Expand All @@ -96,6 +102,7 @@ public function testConstantArray4(array $array): void
public function testConstantArray5(array $array): void
{
assertType("list<'foo'>", array_column($array, 'column'));
assertType("list<'foo'>", array_column($array, 'column', null));
assertType("array<'bar'|int, 'foo'>", array_column($array, 'column', 'key'));
assertType("array<'bar'|int, array{column?: 'foo', key?: 'bar'}>", array_column($array, null, 'key'));
}
Expand All @@ -104,12 +111,14 @@ public function testConstantArray5(array $array): void
public function testConstantArray6(array $array): void
{
assertType('list<bool|string>', array_column($array, mt_rand(0, 1) === 0 ? 'column1' : 'column2'));
assertType('list<bool|string>', array_column($array, mt_rand(0, 1) === 0 ? 'column1' : 'column2', null));
}

/** @param non-empty-array<int, array{column: string, key: string}> $array */
public function testConstantArray7(array $array): void
{
assertType('non-empty-list<string>', array_column($array, 'column'));
assertType('non-empty-list<string>', array_column($array, 'column', null));
assertType('non-empty-array<string, string>', array_column($array, 'column', 'key'));
assertType('non-empty-array<string, array{column: string, key: string}>', array_column($array, null, 'key'));
}
Expand Down Expand Up @@ -142,20 +151,23 @@ public function testConstantArray11(array $array): void
public function testConstantArray12(array $array): void
{
assertType("array{0?: 'foo'}", array_column($array, 'column'));
assertType("array{0?: 'foo'}", array_column($array, 'column', null));
assertType("array{bar?: 'foo'}", array_column($array, 'column', 'key'));
}

/** @param array{0?: array{column: 'foo1', key: 'bar1'}, 1?: array{column: 'foo2', key: 'bar2'}} $array */
public function testConstantArray13(array $array): void
{
assertType("array{0?: 'foo1'|'foo2', 1?: 'foo2'}", array_column($array, 'column'));
assertType("array{0?: 'foo1'|'foo2', 1?: 'foo2'}", array_column($array, 'column', null));
assertType("array{bar1?: 'foo1', bar2?: 'foo2'}", array_column($array, 'column', 'key'));
}

/** @param array{0?: array{column: 'foo1', key: 'bar1'}, 1: array{column: 'foo2', key: 'bar2'}} $array */
public function testConstantArray14(array $array): void
{
assertType("array{0: 'foo1'|'foo2', 1?: 'foo2'}", array_column($array, 'column'));
assertType("array{0: 'foo1'|'foo2', 1?: 'foo2'}", array_column($array, 'column', null));
assertType("array{bar1?: 'foo1', bar2: 'foo2'}", array_column($array, 'column', 'key'));
}

Expand All @@ -165,6 +177,7 @@ public function testConstantArray14(array $array): void
public function testImprecise1(array $array): void
{
assertType("list<'foo'>", array_column($array, 'column'));
assertType("list<'foo'>", array_column($array, 'column', null));
assertType("array<'bar', 'foo'>", array_column($array, 'column', 'key'));
assertType("array{bar: array{column?: 'foo', key: 'bar'}}", array_column($array, null, 'key'));
}
Expand All @@ -180,16 +193,19 @@ public function testImprecise2(array $array): void
public function testImprecise3(array $array): void
{
assertType('list<string>', array_column($array, 'column'));
assertType('list<string>', array_column($array, 'column', null));
assertType('array<int|string, string>', array_column($array, 'column', 'key'));
}

/** @param array<int, DOMElement> $array */
public function testImprecise5(array $array): void
{
assertType('list<string>', array_column($array, 'nodeName'));
assertType('list<string>', array_column($array, 'nodeName', null));
assertType('array<string, string>', array_column($array, 'nodeName', 'tagName'));
assertType('array<string, DOMElement>', array_column($array, null, 'tagName'));
assertType('list', array_column($array, 'foo'));
assertType('list', array_column($array, 'foo', null));
assertType('array<string, mixed>', array_column($array, 'foo', 'tagName'));
assertType('array<string>', array_column($array, 'nodeName', 'foo'));
assertType('array<DOMElement>', array_column($array, null, 'foo'));
Expand All @@ -199,9 +215,11 @@ public function testImprecise5(array $array): void
public function testObjects1(array $array): void
{
assertType('non-empty-list<string>', array_column($array, 'nodeName'));
assertType('non-empty-list<string>', array_column($array, 'nodeName', null));
assertType('non-empty-array<string, string>', array_column($array, 'nodeName', 'tagName'));
assertType('non-empty-array<string, DOMElement>', array_column($array, null, 'tagName'));
assertType('list', array_column($array, 'foo'));
assertType('list', array_column($array, 'foo', null));
assertType('array<string, mixed>', array_column($array, 'foo', 'tagName'));
assertType('non-empty-array<string>', array_column($array, 'nodeName', 'foo'));
assertType('non-empty-array<DOMElement>', array_column($array, null, 'foo'));
Expand All @@ -211,9 +229,11 @@ public function testObjects1(array $array): void
public function testObjects2(array $array): void
{
assertType('array{string}', array_column($array, 'nodeName'));
assertType('array{string}', array_column($array, 'nodeName', null));
assertType('non-empty-array<string, string>', array_column($array, 'nodeName', 'tagName'));
assertType('non-empty-array<string, DOMElement>', array_column($array, null, 'tagName'));
assertType('list', array_column($array, 'foo'));
assertType('list', array_column($array, 'foo', null));
assertType('array<string, mixed>', array_column($array, 'foo', 'tagName'));
assertType('non-empty-array<string>', array_column($array, 'nodeName', 'foo'));
assertType('non-empty-array<DOMElement>', array_column($array, null, 'foo'));
Expand Down
38 changes: 38 additions & 0 deletions tests/PHPStan/Analyser/nsrt/bug-12954.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<?php declare(strict_types = 1);

namespace Bug12954;

use function PHPStan\Testing\assertType;

$plop = [
12 => [
'name' => 'ROLE_USER',
'description' => 'User role'
],
28 => [
'name' => 'ROLE_ADMIN',
'description' => 'Admin role'
],
43 => [
'name' => 'ROLE_SUPER_ADMIN',
'description' => 'SUPER Admin role'
],
];

$list = ['ROLE_USER', 'ROLE_ADMIN', 'ROLE_SUPER_ADMIN'];

$result = array_column($plop, 'name', null);

/**
* @param list<string> $array
*/
function doSomething(array $array): void
{
assertType('list<string>', $array);
}

doSomething($result);
doSomething($list);

assertType('array{\'ROLE_USER\', \'ROLE_ADMIN\', \'ROLE_SUPER_ADMIN\'}', $result);
assertType('array{\'ROLE_USER\', \'ROLE_ADMIN\', \'ROLE_SUPER_ADMIN\'}', $list);
Original file line number Diff line number Diff line change
Expand Up @@ -2084,4 +2084,10 @@ public function testBug12847(): void
]);
}

public function testBug12954(): void
{
$this->checkExplicitMixed = true;
$this->analyse([__DIR__ . '/../../Analyser/nsrt/bug-12954.php'], []);
}

}
Loading