Skip to content

Commit

Permalink
Fix array_keys() and array_values() on constant arrays
Browse files Browse the repository at this point in the history
  • Loading branch information
jlherren authored and ondrejmirtes committed Jul 11, 2023
1 parent c729d3d commit 8d5deb9
Show file tree
Hide file tree
Showing 4 changed files with 201 additions and 26 deletions.
66 changes: 40 additions & 26 deletions src/Type/Constant/ConstantArrayType.php
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@
use function is_string;
use function min;
use function pow;
use function range;
use function sort;
use function sprintf;
use function strpos;
Expand Down Expand Up @@ -1258,49 +1259,62 @@ public function generalizeToArray(): Type
*/
public function getKeysArray(): Type
{
$keyTypes = [];
$valueTypes = [];
$optionalKeys = [];
$autoIndex = 0;

foreach ($this->keyTypes as $i => $keyType) {
$keyTypes[] = new ConstantIntegerType($i);
$valueTypes[] = $keyType;
$autoIndex++;

if (!$this->isOptionalKey($i)) {
continue;
}

$optionalKeys[] = $i;
}

return new self($keyTypes, $valueTypes, $autoIndex, $optionalKeys, true);
return $this->getKeysOrValuesArray($this->keyTypes);
}

/**
* @return self
*/
public function getValuesArray(): Type
{
return $this->getKeysOrValuesArray($this->valueTypes);
}

/**
* @param array<int, Type> $types
*/
private function getKeysOrValuesArray(array $types): self
{
$count = count($types);
$autoIndexes = range($count - count($this->optionalKeys), $count);
assert($autoIndexes !== []);

if ($this->isList) {
// Optimized version for lists: Assume that if a later key exists, then earlier keys also exist.
$keyTypes = array_map(
static fn (int $i): ConstantIntegerType => new ConstantIntegerType($i),
array_keys($types),
);
return new self($keyTypes, $types, $autoIndexes, $this->optionalKeys, true);
}

$keyTypes = [];
$valueTypes = [];
$optionalKeys = [];
$autoIndex = 0;
$maxIndex = 0;

foreach ($this->valueTypes as $i => $valueType) {
foreach ($types as $i => $type) {
$keyTypes[] = new ConstantIntegerType($i);
$valueTypes[] = $valueType;
$autoIndex++;

if (!$this->isOptionalKey($i)) {
continue;
if ($this->isOptionalKey($maxIndex)) {
// move $maxIndex to next non-optional key
do {
$maxIndex++;
} while ($maxIndex < $count && $this->isOptionalKey($maxIndex));
}

$optionalKeys[] = $i;
if ($i === $maxIndex) {
$valueTypes[] = $type;
} else {
$valueTypes[] = TypeCombinator::union(...array_slice($types, $i, $maxIndex - $i + 1));
if ($maxIndex >= $count) {
$optionalKeys[] = $i;
}
}
$maxIndex++;
}

return new self($keyTypes, $valueTypes, $autoIndex, $optionalKeys, true);
return new self($keyTypes, $valueTypes, $autoIndexes, $optionalKeys, true);
}

/** @deprecated Use getArraySize() instead */
Expand Down
9 changes: 9 additions & 0 deletions tests/PHPStan/Analyser/data/array_keys.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,13 @@ public function sayHello($mixed): void
assertType('*NEVER*', array_keys($mixed));
}
}

public function constantArrayType(): void
{
$numbers = array_filter(
[1 => 'a', 2 => 'b', 3 => 'c'],
static fn ($value) => mt_rand(0, 1) === 0,
);
assertType("array{0?: 1|2|3, 1?: 2|3, 2?: 3}", array_keys($numbers));
}
}
9 changes: 9 additions & 0 deletions tests/PHPStan/Analyser/data/array_values.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,13 @@ public function foo2($list): void
assertType('*NEVER*', array_values($list));
}
}

public function constantArrayType(): void
{
$numbers = array_filter(
[1 => 'a', 2 => 'b', 3 => 'c'],
static fn ($value) => mt_rand(0, 1) === 0,
);
assertType("array{0?: 'a'|'b'|'c', 1?: 'b'|'c', 2?: 'c'}", array_values($numbers));
}
}
143 changes: 143 additions & 0 deletions tests/PHPStan/Type/Constant/ConstantArrayTypeTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
use PHPStan\Type\StringType;
use PHPStan\Type\Type;
use PHPStan\Type\TypeCombinator;
use PHPStan\Type\UnionType;
use PHPStan\Type\VerbosityLevel;
use function array_map;
use function sprintf;
Expand Down Expand Up @@ -746,4 +747,146 @@ public function dataIsCallable(): iterable
];
}

public function dataValuesArray(): iterable
{
yield 'empty' => [
new ConstantArrayType([], []),
new ConstantArrayType([], []),
];

yield 'non-optional' => [
new ConstantArrayType([
new ConstantIntegerType(10),
new ConstantIntegerType(11),
], [
new ConstantStringType('a'),
new ConstantStringType('b'),
], [20], [], false),
new ConstantArrayType([
new ConstantIntegerType(0),
new ConstantIntegerType(1),
], [
new ConstantStringType('a'),
new ConstantStringType('b'),
], [2], [], true),
];

yield 'optional-1' => [
new ConstantArrayType([
new ConstantIntegerType(10),
new ConstantIntegerType(11),
new ConstantIntegerType(12),
new ConstantIntegerType(13),
new ConstantIntegerType(14),
], [
new ConstantStringType('a'),
new ConstantStringType('b'),
new ConstantStringType('c'),
new ConstantStringType('d'),
new ConstantStringType('e'),
], [15], [1, 3], false),
new ConstantArrayType([
new ConstantIntegerType(0),
new ConstantIntegerType(1),
new ConstantIntegerType(2),
new ConstantIntegerType(3),
new ConstantIntegerType(4),
], [
new ConstantStringType('a'),
new UnionType([new ConstantStringType('b'), new ConstantStringType('c')]),
new UnionType([new ConstantStringType('c'), new ConstantStringType('d'), new ConstantStringType('e')]),
new UnionType([new ConstantStringType('d'), new ConstantStringType('e')]),
new ConstantStringType('e'),
], [3, 4, 5], [3, 4], true),
];

yield 'optional-2' => [
new ConstantArrayType([
new ConstantIntegerType(10),
new ConstantIntegerType(11),
new ConstantIntegerType(12),
new ConstantIntegerType(13),
new ConstantIntegerType(14),
], [
new ConstantStringType('a'),
new ConstantStringType('b'),
new ConstantStringType('c'),
new ConstantStringType('d'),
new ConstantStringType('e'),
], [15], [0, 2, 4], false),
new ConstantArrayType([
new ConstantIntegerType(0),
new ConstantIntegerType(1),
new ConstantIntegerType(2),
new ConstantIntegerType(3),
new ConstantIntegerType(4),
], [
new UnionType([new ConstantStringType('a'), new ConstantStringType('b')]),
new UnionType([new ConstantStringType('b'), new ConstantStringType('c'), new ConstantStringType('d')]),
new UnionType([new ConstantStringType('c'), new ConstantStringType('d'), new ConstantStringType('e')]),
new UnionType([new ConstantStringType('d'), new ConstantStringType('e')]),
new ConstantStringType('e'),
], [2, 3, 4, 5], [2, 3, 4], true),
];

yield 'optional-at-end-and-list' => [
new ConstantArrayType([
new ConstantIntegerType(10),
new ConstantIntegerType(11),
new ConstantIntegerType(12),
], [
new ConstantStringType('a'),
new ConstantStringType('b'),
new ConstantStringType('c'),
], [11, 12, 13], [1, 2], true),
new ConstantArrayType([
new ConstantIntegerType(0),
new ConstantIntegerType(1),
new ConstantIntegerType(2),
], [
new ConstantStringType('a'),
new ConstantStringType('b'),
new ConstantStringType('c'),
], [1, 2, 3], [1, 2], true),
];

yield 'optional-at-end-but-not-list' => [
new ConstantArrayType([
new ConstantIntegerType(10),
new ConstantIntegerType(11),
new ConstantIntegerType(12),
], [
new ConstantStringType('a'),
new ConstantStringType('b'),
new ConstantStringType('c'),
], [11, 12, 13], [1, 2], false),
new ConstantArrayType([
new ConstantIntegerType(0),
new ConstantIntegerType(1),
new ConstantIntegerType(2),
], [
new ConstantStringType('a'),
new UnionType([new ConstantStringType('b'), new ConstantStringType('c')]),
new ConstantStringType('c'),
], [1, 2, 3], [1, 2], true),
];
}

/**
* @dataProvider dataValuesArray
*/
public function testValuesArray(ConstantArrayType $type, ConstantArrayType $expectedType): void
{
$actualType = $type->getValuesArray();
$message = sprintf(
'Values array of %s is %s, but should be %s',
$type->describe(VerbosityLevel::precise()),
$actualType->describe(VerbosityLevel::precise()),
$expectedType->describe(VerbosityLevel::precise()),
);
$this->assertTrue($expectedType->equals($actualType), $message);
$this->assertSame($expectedType->isList(), $actualType->isList());
$this->assertSame($expectedType->getNextAutoIndexes(), $actualType->getNextAutoIndexes());
}

}

0 comments on commit 8d5deb9

Please sign in to comment.