Skip to content
Closed
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
20 changes: 17 additions & 3 deletions src/Type/TypeCombinator.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
use PHPStan\Type\Generic\TemplateTypeFactory;
use PHPStan\Type\Generic\TemplateUnionType;
use function array_fill;
use function array_filter;
use function array_key_exists;
use function array_key_first;
use function array_merge;
Expand Down Expand Up @@ -959,8 +960,17 @@ private static function processArrayTypes(array $arrayTypes): array
}

$reducedArrayTypes = self::optimizeConstantArrays(self::reduceArrays($arrayTypes, true));
$emptyArrayType = null;
foreach ($reducedArrayTypes as $idx => $reducedArray) {
$reducedArrayTypes[$idx] = self::intersect($reducedArray, ...$accessoryTypes);
$applied = $accessoryTypes;
if ($reducedArray->isIterableAtLeastOnce()->no()) {
$emptyArrayType ??= new ConstantArrayType([], []);
$applied = array_values(array_filter(
$applied,
static fn (Type $t): bool => !$t->accepts($emptyArrayType, true)->no(),
));
}
$reducedArrayTypes[$idx] = self::intersect($reducedArray, ...$applied);
}
return $reducedArrayTypes;
}
Expand Down Expand Up @@ -1057,7 +1067,11 @@ private static function optimizeConstantArrays(array $types): array
$generalizedKeyType = $innerKeyType->generalize(GeneralizePrecision::moreSpecific());
$keyTypes[$generalizedKeyType->describe(VerbosityLevel::precise())] = $generalizedKeyType;

$generalizedValueType = TypeTraverser::map($innerValueTypes[$i], static function (Type $type) use ($traverse): Type {
$generalizedValueType = TypeTraverser::map($innerValueTypes[$i], static function (Type $type, callable $innerTraverse): Type {
if ($type instanceof ConstantArrayType && $type->isIterableAtLeastOnce()->no()) {
return $type;
}

if ($type instanceof ArrayType || $type instanceof ConstantArrayType) {
return new IntersectionType([$type, new OversizedArrayType()]);
}
Expand All @@ -1066,7 +1080,7 @@ private static function optimizeConstantArrays(array $types): array
return $type->generalize(GeneralizePrecision::moreSpecific());
}

return $traverse($type);
return $innerTraverse($type);
});
$valueTypes[$generalizedValueType->describe(VerbosityLevel::precise())] = $generalizedValueType;
}
Expand Down
51 changes: 51 additions & 0 deletions tests/PHPStan/Analyser/nsrt/bug-14560.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
<?php

namespace Bug14560TypeInference;

use function PHPStan\Testing\assertType;

/**
* Verify that oversized-array generalization in TypeCombinator::optimizeConstantArrays
* produces types that accept empty sub-arrays and don't over-generalize nested values.
*/
function oversizedWithEmptySubArrays(): void
{
$items = [];

if (rand()) {
$items[] = ['kind' => 'a', 'data' => [['x' => 1]], 'extra' => []];
}
if (rand()) {
$items[] = ['kind' => 'b', 'data' => [['x' => 2]], 'extra' => []];
}
if (rand()) {
$items[] = ['kind' => 'c', 'data' => [['x' => 3]], 'extra' => []];
}
if (rand()) {
$items[] = ['kind' => 'd', 'data' => [['x' => 4]], 'extra' => []];
}
if (rand()) {
$items[] = ['kind' => 'e', 'data' => [['x' => 5]], 'extra' => []];
}
if (rand()) {
$items[] = ['kind' => 'f', 'data' => [['x' => 6]], 'extra' => []];
}
if (rand()) {
$items[] = ['kind' => 'g', 'data' => [['x' => 7]], 'extra' => []];
}
if (rand()) {
$items[] = ['kind' => 'h', 'data' => [['x' => 8]], 'extra' => []];
}

if ($items === []) {
return;
}

foreach ($items as $item) {
// The type of $item['extra'] must accept array{} — the empty array that
// every branch actually writes. Before the fix, optimizeConstantArrays
// would tag it with OversizedArrayType, producing array{}&oversized-array
// which contradicts itself (empty but oversized).
assertType('array{}', $item['extra']);
}
}
5 changes: 5 additions & 0 deletions tests/PHPStan/Rules/Generators/YieldTypeRuleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -82,4 +82,9 @@ public function testBug7484(): void
]);
}

public function testBug14560(): void
{
$this->analyse([__DIR__ . '/data/bug-14560.php'], []);
}

}
90 changes: 90 additions & 0 deletions tests/PHPStan/Rules/Generators/data/bug-14560.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
<?php declare(strict_types = 1);

namespace Bug14560;

/**
* @param callable(string): iterable<string, mixed> $fn
* @return \Generator<string, mixed>
*/
function bridge(callable $fn): \Generator
{
foreach (['a', 'b'] as $kind) {
yield from $fn($kind);
}
}

bridge(static function (string $kind): iterable {
$key = 'b' === $kind ? 'x' : 'y';

yield 'one' => [
'kind' => $kind,
'entries' => [[$key => [1, '2022-08-04', [['08:00', '12:00']]]]],
'lookup' => [],
'targets' => [[1, '2022-08-04']],
];
yield 'two' => [
'kind' => $kind,
'entries' => [[$key => [1, '2022-08-04', [['08:00', '12:00'], ['14:00', '18:00']]]]],
'lookup' => [],
'targets' => [[1, '2022-08-04']],
];
yield 'three' => [
'kind' => $kind,
'entries' => [[$key => [1, '2022-08-04', [['00:00', '00:00']]]]],
'lookup' => [],
'targets' => [[1, '2022-08-04']],
];
yield 'four' => [
'kind' => $kind,
'entries' => [[$key => [1, '2022-08-04', [['22:00', '04:00']]]]],
'lookup' => [],
'targets' => [[1, '2022-08-04/2022-08-05']],
];
yield 'five' => [
'kind' => $kind,
'entries' => [[$key => [1, '2022-08-04', [['16:00', '23:00']], 'lookupIds' => [42]]]],
'lookup' => [42 => [1, '2022-08-05T00:05/2022-08-05T02:00']],
'targets' => [[1, '2022-08-04/2022-08-05']],
];
yield 'six' => [
'kind' => $kind,
'entries' => [
[$key => [1, '2022-08-04', [['08:00', '12:00']]]],
[$key => [1, '2022-08-10', [['08:00', '12:00']]]],
],
'lookup' => [],
'targets' => [[1, '2022-08-04'], [1, '2022-08-10']],
];
yield 'seven' => [
'kind' => $kind,
'entries' => [
[$key => [1, '2022-08-04', [['08:00', '12:00']]]],
[$key => [1, '2022-08-05', [['08:00', '12:00']]]],
[$key => [1, '2022-08-06', [['08:00', '12:00']]]],
],
'lookup' => [],
'targets' => [[1, '2022-08-04/2022-08-06']],
];
yield 'eight' => [
'kind' => $kind,
'entries' => [
[$key => [1, '2022-08-04', [['08:00', '12:00']]]],
[$key => [2, '2022-08-05', [['08:00', '12:00']]]],
[$key => [2, '2022-08-06', [['08:00', '12:00']]]],
[$key => [3, '2022-08-06', [['08:00', '12:00']]]],
[$key => [3, '2022-08-10', [['08:00', '12:00']]]],
],
'lookup' => [],
'targets' => [[1, '2022-08-04'], [2, '2022-08-05/2022-08-06'], [3, '2022-08-06'], [3, '2022-08-10']],
];
yield 'nine' => [
'kind' => $kind,
'entries' => [
[$key => [1, '2022-08-04', [['08:00', '12:00']]]],
[$key => [1, '2022-08-05', [['08:00', '12:00']]]],
],
'lookup' => [],
'targets' => [],
'flag' => false,
];
});
Loading