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
41 changes: 38 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,25 @@ 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()) {
// Filter accessories that reject empty arrays — applying them
// would build a contradictory intersection (e.g.
// `array{}&oversized-array`, `array{}&hasOffset('foo')`) that
// rejects the very value it represents, breaking the super-type
// contract of the union. Today only `OversizedArrayType` can
// leak here via the partial-presence special case in
// `processArrayAccessoryTypes`; the predicate is general so
// that future accessories don't reintroduce the bug.
$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 +1075,24 @@ 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 {
// Inner traversal of the value position. Two subtleties, both
// of which produced types that failed to be super-types of
// their contributors:
// - Empty constant arrays must be left alone; wrapping them
// builds a contradictory `array{}&oversized-array`.
// - Fall through via `$innerTraverse`, not the outer
// `$traverse`. The outer callback fully generalizes a
// sealed `ConstantArrayType` into `array<intKey, V>&...`,
// which is correct at the top level but wrong inside a
// value position: it would treat a sealed `array{a: 1}`
// reached via `array{}|array{a: 1}` differently from one
// reached directly, leaving `processArrayTypes` with a
// mix of shapes it cannot unify cleanly.
$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 +1101,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
10 changes: 10 additions & 0 deletions tests/PHPStan/Rules/Generators/YieldTypeRuleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -82,4 +82,14 @@ public function testBug7484(): void
]);
}

public function testBugYieldOversizedSelfRejection(): void
{
// Regression: PHPStan inferred the closure's Generator value type from
// its yields, then rejected each yield against that inferred type. The
// oversized-array generalization in TypeCombinator::optimizeConstantArrays
// produced a constraint that was not a super-type of the variants it was
// derived from. Each yield is well-typed; no error must be reported.
$this->analyse([__DIR__ . '/data/bug-yield-oversized-self-rejection.php'], []);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
<?php declare(strict_types = 1);

namespace BugYieldOversizedSelfRejection;

/**
* Reproducer for a regression where `optimizeConstantArrays` produced a
* Generator value type that did not accept the very yields it was inferred
* from. Each yield is well-typed; the closure's inferred Generator value
* type must therefore be a super-type of every value it actually yields.
*
* @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