Skip to content

Commit

Permalink
Improve return types for array_fill_keys and array_combine
Browse files Browse the repository at this point in the history
  • Loading branch information
canvural authored and ondrejmirtes committed Mar 29, 2022
1 parent dc14e4a commit 3bd1bac
Show file tree
Hide file tree
Showing 6 changed files with 257 additions and 3 deletions.
27 changes: 25 additions & 2 deletions src/Type/Php/ArrayCombineFunctionReturnTypeExtension.php
Expand Up @@ -15,7 +15,10 @@
use PHPStan\Type\Constant\ConstantIntegerType;
use PHPStan\Type\Constant\ConstantStringType;
use PHPStan\Type\DynamicFunctionReturnTypeExtension;
use PHPStan\Type\ErrorType;
use PHPStan\Type\IntegerType;
use PHPStan\Type\MixedType;
use PHPStan\Type\NeverType;
use PHPStan\Type\Type;
use PHPStan\Type\TypeCombinator;
use PHPStan\Type\UnionType;
Expand Down Expand Up @@ -65,9 +68,25 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection,
}
}

if ($keysParamType->isArray()->yes()) {
$itemType = $keysParamType->getIterableValueType();

if ((new IntegerType())->isSuperTypeOf($itemType)->no()) {
if ($itemType->toString() instanceof ErrorType) {
return new NeverType();
}

$keyType = $itemType->toString();
} else {
$keyType = $itemType;
}
} else {
$keyType = new MixedType();
}

$arrayType = new ArrayType(
$keysParamType instanceof ArrayType ? $keysParamType->getItemType() : new MixedType(),
$valuesParamType instanceof ArrayType ? $valuesParamType->getItemType() : new MixedType(),
$keyType,
$valuesParamType->isArray()->yes() ? $valuesParamType->getIterableValueType() : new MixedType(),
);

if ($keysParamType->isIterableAtLeastOnce()->yes() && $valuesParamType->isIterableAtLeastOnce()->yes()) {
Expand Down Expand Up @@ -95,6 +114,10 @@ private function sanitizeConstantArrayKeyTypes(array $types): ?array
$sanitizedTypes = [];

foreach ($types as $type) {
if ((new IntegerType())->isSuperTypeOf($type)->no() && ! $type->toString() instanceof ErrorType) {
$type = $type->toString();
}

if (
!$type instanceof ConstantIntegerType
&& !$type instanceof ConstantStringType
Expand Down
13 changes: 12 additions & 1 deletion src/Type/Php/ArrayFillKeysFunctionReturnTypeExtension.php
Expand Up @@ -9,6 +9,9 @@
use PHPStan\Type\ArrayType;
use PHPStan\Type\Constant\ConstantArrayTypeBuilder;
use PHPStan\Type\DynamicFunctionReturnTypeExtension;
use PHPStan\Type\ErrorType;
use PHPStan\Type\IntegerType;
use PHPStan\Type\NeverType;
use PHPStan\Type\Type;
use PHPStan\Type\TypeCombinator;
use PHPStan\Type\TypeUtils;
Expand Down Expand Up @@ -39,7 +42,15 @@ public function getTypeFromFunctionCall(FunctionReflection $functionReflection,
foreach ($constantArrays as $constantArray) {
$arrayBuilder = ConstantArrayTypeBuilder::createEmpty();
foreach ($constantArray->getValueTypes() as $keyType) {
$arrayBuilder->setOffsetValueType($keyType, $valueType);
if ((new IntegerType())->isSuperTypeOf($keyType)->no()) {
if ($keyType->toString() instanceof ErrorType) {
return new NeverType();
}

$arrayBuilder->setOffsetValueType($keyType->toString(), $valueType);
} else {
$arrayBuilder->setOffsetValueType($keyType, $valueType);
}
}
$arrayTypes[] = $arrayBuilder->getArray();
}
Expand Down
8 changes: 8 additions & 0 deletions tests/PHPStan/Analyser/NodeScopeResolverTest.php
Expand Up @@ -829,6 +829,14 @@ public function dataFileAsserts(): iterable
yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-6904.php');
}

if (PHP_VERSION_ID >= 80000) {
yield from $this->gatherAssertTypes(__DIR__ . '/data/array-combine-php8.php');
} else {
yield from $this->gatherAssertTypes(__DIR__ . '/data/array-combine-php7.php');
}

yield from $this->gatherAssertTypes(__DIR__ . '/data/array-fill-keys.php');

yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-6917.php');
}

Expand Down
80 changes: 80 additions & 0 deletions tests/PHPStan/Analyser/data/array-combine-php7.php
@@ -0,0 +1,80 @@
<?php

namespace ArrayCombinePHP7;

use function PHPStan\Testing\assertType;

class Foo
{
/** @phpstan-return 'foo' */
public function __toString(): string
{
return 'foo';
}
}

class Bar
{
public function __toString(): string
{
return 'bar';
}
}

class Baz {}

function withBoolKey(): void
{
$a = [true, 'red', 'yellow'];
$b = ['avocado', 'apple', 'banana'];

assertType("array{1: 'avocado', red: 'apple', yellow: 'banana'}", array_combine($a, $b));

$c = [false, 'red', 'yellow'];
$d = ['avocado', 'apple', 'banana'];

assertType("array{: 'avocado', red: 'apple', yellow: 'banana'}", array_combine($c, $d));
}

function withFloatKey(): void
{
$a = [1.5, 'red', 'yellow'];
$b = ['avocado', 'apple', 'banana'];

assertType("array{1.5: 'avocado', red: 'apple', yellow: 'banana'}", array_combine($a, $b));
}

function withIntegerKey(): void
{
$a = [1, 2, 3];
$b = ['avocado', 'apple', 'banana'];

assertType("array{1: 'avocado', 2: 'apple', 3: 'banana'}", array_combine($a, $b));
}

function withNumericStringKey(): void
{
$a = ["1", "2", "3"];
$b = ['avocado', 'apple', 'banana'];

assertType("array{1: 'avocado', 2: 'apple', 3: 'banana'}", array_combine($a, $b));
}

function withObjectKey() : void
{
$a = [new Foo, 'red', 'yellow'];
$b = ['avocado', 'apple', 'banana'];

assertType("array{foo: 'avocado', red: 'apple', yellow: 'banana'}", array_combine($a, $b));
assertType("non-empty-array<string, 'apple'|'avocado'|'banana'>|false", array_combine([new Bar, 'red', 'yellow'], $b));
assertType("*NEVER*", array_combine([new Baz, 'red', 'yellow'], $b));
}

/**
* @param non-empty-array<int, 'foo'|'bar'|'baz'> $a
* @param non-empty-array<int, 'apple'|'avocado'|'banana'> $b
*/
function withNonEmptyArray(array $a, array $b): void
{
assertType("non-empty-array<'bar'|'baz'|'foo', 'apple'|'avocado'|'banana'>|false", array_combine($a, $b));
}
80 changes: 80 additions & 0 deletions tests/PHPStan/Analyser/data/array-combine-php8.php
@@ -0,0 +1,80 @@
<?php

namespace ArrayCombinePHP8;

use function PHPStan\Testing\assertType;

class Foo
{
/** @phpstan-return 'foo' */
public function __toString(): string
{
return 'foo';
}
}

class Bar
{
public function __toString(): string
{
return 'bar';
}
}

class Baz {}

function withBoolKey(): void
{
$a = [true, 'red', 'yellow'];
$b = ['avocado', 'apple', 'banana'];

assertType("array{1: 'avocado', red: 'apple', yellow: 'banana'}", array_combine($a, $b));

$c = [false, 'red', 'yellow'];
$d = ['avocado', 'apple', 'banana'];

assertType("array{: 'avocado', red: 'apple', yellow: 'banana'}", array_combine($c, $d));
}

function withFloatKey(): void
{
$a = [1.5, 'red', 'yellow'];
$b = ['avocado', 'apple', 'banana'];

assertType("array{1.5: 'avocado', red: 'apple', yellow: 'banana'}", array_combine($a, $b));
}

function withIntegerKey(): void
{
$a = [1, 2, 3];
$b = ['avocado', 'apple', 'banana'];

assertType("array{1: 'avocado', 2: 'apple', 3: 'banana'}", array_combine($a, $b));
}

function withNumericStringKey(): void
{
$a = ["1", "2", "3"];
$b = ['avocado', 'apple', 'banana'];

assertType("array{1: 'avocado', 2: 'apple', 3: 'banana'}", array_combine($a, $b));
}

function withObjectKey() : void
{
$a = [new Foo, 'red', 'yellow'];
$b = ['avocado', 'apple', 'banana'];

assertType("array{foo: 'avocado', red: 'apple', yellow: 'banana'}", array_combine($a, $b));
assertType("non-empty-array<string, 'apple'|'avocado'|'banana'>", array_combine([new Bar, 'red', 'yellow'], $b));
assertType("*NEVER*", array_combine([new Baz, 'red', 'yellow'], $b));
}

/**
* @param non-empty-array<int, 'foo'|'bar'|'baz'> $a
* @param non-empty-array<int, 'apple'|'avocado'|'banana'> $b
*/
function withNonEmptyArray(array $a, array $b): void
{
assertType("non-empty-array<'bar'|'baz'|'foo', 'apple'|'avocado'|'banana'>", array_combine($a, $b));
}
52 changes: 52 additions & 0 deletions tests/PHPStan/Analyser/data/array-fill-keys.php
@@ -0,0 +1,52 @@
<?php

namespace ArrayFillKeys;

use function PHPStan\Testing\assertType;

class Foo
{
/** @phpstan-return 'foo' */
public function __toString(): string
{
return 'foo';
}
}

class Bar
{
public function __toString(): string
{
return 'bar';
}
}

class Baz {}

function withBoolKey() : array
{
assertType("array{1: 'b'}", array_fill_keys([true], 'b'));
assertType("array{: 'b'}", array_fill_keys([false], 'b'));
}

function withFloatKey() : array
{
assertType("array{1.5: 'b'}", array_fill_keys([1.5], 'b'));
}

function withIntegerKey() : array
{
assertType("array{99: 'b'}", array_fill_keys([99], 'b'));
}

function withNumericStringKey() : array
{
assertType("array{999: 'b'}", array_fill_keys(["999"], 'b'));
}

function withObjectKey() : array
{
assertType("array{foo: 'b'}", array_fill_keys([new Foo()], 'b'));
assertType("non-empty-array<string, 'b'>", array_fill_keys([new Bar()], 'b'));
assertType("*NEVER*", array_fill_keys([new Baz()], 'b'));
}

0 comments on commit 3bd1bac

Please sign in to comment.