From 87d4e4b0dfa2a70c84cbfc6f029c3c646d218154 Mon Sep 17 00:00:00 2001 From: staabm <120441+staabm@users.noreply.github.com> Date: Sun, 21 Jun 2026 16:11:33 +0000 Subject: [PATCH 01/18] Do not hold the global `BleedingEdgeToggle` across `yield` in `ConstantArrayTypeTest` data providers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - `dataAccepts()` and `dataGetArraySize()` mutated the global static `BleedingEdgeToggle` and kept it mutated across `yield` boundaries. Data providers are evaluated lazily and can be abandoned mid-iteration, which left the toggle stuck on `true` and leaked into unrelated tests. - A leaked `true` toggle makes the `ConstantArrayType` objects constructed by *other* providers (e.g. `IntersectionTypeTest::dataIsAcceptedBy`) sealed array shapes, so `ConstantArrayType::accepts()` returns `No` ("Sealed array shape can only accept a constant array") instead of `Maybe` — the intermittent CI failure on `IntersectionTypeTest::testIsAcceptedBy` data set #7. - Extract `dataWithBleedingEdge(bool, callable)` which sets the toggle, builds the toggle-dependent data sets eagerly via the callback, and restores the previous value in a `finally` before returning. The objects are therefore constructed while the toggle is set, but the global is never observable as mutated once the provider yields control. - Add `testDataProviderDoesNotLeakBleedingEdgeToggle()` which partially consumes each bleeding-edge provider and asserts the toggle is restored after every prefix length — this reproduces the leak deterministically. - Audited the rest of the suite: the only `BleedingEdgeToggle` mutations inside generator data providers were these two; the `TypeCombinatorTest` / `ConstantArrayTypeTest` toggles in test methods already use `try`/`finally` and run after providers are evaluated, and the `putenv()` mutations in the error formatter tests live in `setUp`/`tearDown`/test methods, not providers. --- .../Type/Constant/ConstantArrayTypeTest.php | 479 ++++++++++-------- 1 file changed, 274 insertions(+), 205 deletions(-) diff --git a/tests/PHPStan/Type/Constant/ConstantArrayTypeTest.php b/tests/PHPStan/Type/Constant/ConstantArrayTypeTest.php index ced494973f0..69d1ca3e561 100644 --- a/tests/PHPStan/Type/Constant/ConstantArrayTypeTest.php +++ b/tests/PHPStan/Type/Constant/ConstantArrayTypeTest.php @@ -36,6 +36,7 @@ use stdClass; use function array_map; use function is_string; +use function range; use function sprintf; class ConstantArrayTypeTest extends PHPStanTestCase @@ -418,204 +419,227 @@ public static function dataAccepts(): iterable TrinaryLogic::createMaybe(), ]; - $bleedingEdgeBackup = BleedingEdgeToggle::isBleedingEdge(); - BleedingEdgeToggle::setBleedingEdge(false); - - yield [ - new ConstantArrayType([], []), - new ConstantArrayType([], []), - TrinaryLogic::createYes(), - ]; - - // empty array (with unknown sealedness) does not accept extra keys - yield [ - new ConstantArrayType([], []), - new ConstantArrayType([new ConstantStringType('a')], [new StringType()]), - TrinaryLogic::createNo(), - [], - ]; + // The data sets below depend on the global BleedingEdgeToggle. Build them + // while the toggle is set, then restore it *before* yielding, so the global + // state is never held across a yield boundary - data providers are evaluated + // lazily and may be abandoned mid-iteration, which would otherwise leak the + // toggle into unrelated tests. + yield from self::dataWithBleedingEdge(false, static fn (): array => [ + [ + new ConstantArrayType([], []), + new ConstantArrayType([], []), + TrinaryLogic::createYes(), + ], - // non-empty array (with unknown sealedness) accepts extra keys - yield [ - new ConstantArrayType([new ConstantStringType('a')], [new StringType()]), - new ConstantArrayType([ - new ConstantStringType('a'), - new ConstantStringType('b'), - ], [ - new StringType(), - new IntegerType(), - ]), - TrinaryLogic::createYes(), - [], - ]; + // empty array (with unknown sealedness) does not accept extra keys + [ + new ConstantArrayType([], []), + new ConstantArrayType([new ConstantStringType('a')], [new StringType()]), + TrinaryLogic::createNo(), + [], + ], - BleedingEdgeToggle::setBleedingEdge(true); + // non-empty array (with unknown sealedness) accepts extra keys + [ + new ConstantArrayType([new ConstantStringType('a')], [new StringType()]), + new ConstantArrayType([ + new ConstantStringType('a'), + new ConstantStringType('b'), + ], [ + new StringType(), + new IntegerType(), + ]), + TrinaryLogic::createYes(), + [], + ], + ]); - // empty array (sealed) does not accept extra keys - yield [ - new ConstantArrayType([], []), - new ConstantArrayType([new ConstantStringType('a')], [new StringType()]), - TrinaryLogic::createNo(), - [], - ]; + yield from self::dataWithBleedingEdge(true, static fn (): array => [ + // empty array (sealed) does not accept extra keys + [ + new ConstantArrayType([], []), + new ConstantArrayType([new ConstantStringType('a')], [new StringType()]), + TrinaryLogic::createNo(), + [], + ], - // non-empty array (sealed) does not accept extra keys - yield [ - new ConstantArrayType([new ConstantStringType('a')], [new StringType()]), - new ConstantArrayType([ - new ConstantStringType('a'), - new ConstantStringType('b'), - ], [ - new StringType(), - new IntegerType(), - ]), - TrinaryLogic::createNo(), - ['Sealed array shape does not accept array with extra key \'b\'.'], - ]; + // non-empty array (sealed) does not accept extra keys + [ + new ConstantArrayType([new ConstantStringType('a')], [new StringType()]), + new ConstantArrayType([ + new ConstantStringType('a'), + new ConstantStringType('b'), + ], [ + new StringType(), + new IntegerType(), + ]), + TrinaryLogic::createNo(), + ['Sealed array shape does not accept array with extra key \'b\'.'], + ], - // sealed array does not accept general array - yield [ - new ConstantArrayType([new ConstantStringType('a')], [new StringType()]), - new ArrayType(new StringType(), new StringType()), - TrinaryLogic::createNo(), - ['Sealed array shape can only accept a constant array. Extra keys are not allowed.'], - ]; + // sealed array does not accept general array + [ + new ConstantArrayType([new ConstantStringType('a')], [new StringType()]), + new ArrayType(new StringType(), new StringType()), + TrinaryLogic::createNo(), + ['Sealed array shape can only accept a constant array. Extra keys are not allowed.'], + ], - // sealed array does not accept unsealed array - yield [ - new ConstantArrayType([new ConstantStringType('a')], [new StringType()]), - new ConstantArrayType([new ConstantStringType('a')], [new StringType()], unsealed: [new StringType(), new ObjectType(stdClass::class)]), - TrinaryLogic::createNo(), - ['Sealed array shape does not accept unsealed array shape.'], - ]; + // sealed array does not accept unsealed array + [ + new ConstantArrayType([new ConstantStringType('a')], [new StringType()]), + new ConstantArrayType([new ConstantStringType('a')], [new StringType()], unsealed: [new StringType(), new ObjectType(stdClass::class)]), + TrinaryLogic::createNo(), + ['Sealed array shape does not accept unsealed array shape.'], + ], - // unsealed array accepts compatible general array - yield [ - new ConstantArrayType([new ConstantStringType('a')], [new StringType()], unsealed: [new StringType(), new StringType()]), - new IntersectionType([ - new ArrayType(new StringType(), new StringType()), - new HasOffsetValueType(new ConstantStringType('a'), new StringType()), - ]), - TrinaryLogic::createYes(), - [], - ]; + // unsealed array accepts compatible general array + [ + new ConstantArrayType([new ConstantStringType('a')], [new StringType()], unsealed: [new StringType(), new StringType()]), + new IntersectionType([ + new ArrayType(new StringType(), new StringType()), + new HasOffsetValueType(new ConstantStringType('a'), new StringType()), + ]), + TrinaryLogic::createYes(), + [], + ], - // unsealed array does not accept incompatible general array (the error is in the keys already) - yield [ - new ConstantArrayType([new ConstantStringType('a')], [new IntegerType()], unsealed: [new StringType(), new StringType()]), - new IntersectionType([ - new ArrayType(new StringType(), new StringType()), - new HasOffsetValueType(new ConstantStringType('a'), new StringType()), - ]), - TrinaryLogic::createNo(), - [], - ]; + // unsealed array does not accept incompatible general array (the error is in the keys already) + [ + new ConstantArrayType([new ConstantStringType('a')], [new IntegerType()], unsealed: [new StringType(), new StringType()]), + new IntersectionType([ + new ArrayType(new StringType(), new StringType()), + new HasOffsetValueType(new ConstantStringType('a'), new StringType()), + ]), + TrinaryLogic::createNo(), + [], + ], - // unsealed array does not accept incompatible general array (integer vs. string unsealed values) - yield [ - new ConstantArrayType([new ConstantStringType('a')], [new StringType()], unsealed: [new StringType(), new IntegerType()]), - new IntersectionType([ - new ArrayType(new StringType(), new StringType()), - new HasOffsetValueType(new ConstantStringType('a'), new StringType()), - ]), - TrinaryLogic::createNo(), - [], - ]; + // unsealed array does not accept incompatible general array (integer vs. string unsealed values) + [ + new ConstantArrayType([new ConstantStringType('a')], [new StringType()], unsealed: [new StringType(), new IntegerType()]), + new IntersectionType([ + new ArrayType(new StringType(), new StringType()), + new HasOffsetValueType(new ConstantStringType('a'), new StringType()), + ]), + TrinaryLogic::createNo(), + [], + ], - // unsealed array must check extra keys against its own unsealed types - yield [ - new ConstantArrayType([new ConstantStringType('a')], [new StringType()], unsealed: [new StringType(), new StringType()]), - new ConstantArrayType([ - new ConstantStringType('a'), - new ConstantStringType('b'), - ], [ - new StringType(), - new StringType(), - ]), - TrinaryLogic::createYes(), - [], - ]; + // unsealed array must check extra keys against its own unsealed types + [ + new ConstantArrayType([new ConstantStringType('a')], [new StringType()], unsealed: [new StringType(), new StringType()]), + new ConstantArrayType([ + new ConstantStringType('a'), + new ConstantStringType('b'), + ], [ + new StringType(), + new StringType(), + ]), + TrinaryLogic::createYes(), + [], + ], - yield [ - new ConstantArrayType([new ConstantStringType('a')], [new StringType()], unsealed: [new IntegerType(), new StringType()]), - new ConstantArrayType([ - new ConstantStringType('a'), - new ConstantIntegerType(10), - ], [ - new StringType(), - new StringType(), - ]), - TrinaryLogic::createYes(), - [], - ]; + [ + new ConstantArrayType([new ConstantStringType('a')], [new StringType()], unsealed: [new IntegerType(), new StringType()]), + new ConstantArrayType([ + new ConstantStringType('a'), + new ConstantIntegerType(10), + ], [ + new StringType(), + new StringType(), + ]), + TrinaryLogic::createYes(), + [], + ], - yield [ - new ConstantArrayType([new ConstantStringType('a')], [new StringType()], unsealed: [new IntegerType(), new StringType()]), - new ConstantArrayType([ - new ConstantStringType('a'), - new ConstantStringType('b'), - ], [ - new StringType(), - new StringType(), - ]), - TrinaryLogic::createNo(), [ - 'Unsealed array key type int does not accept extra key type \'b\'.', + new ConstantArrayType([new ConstantStringType('a')], [new StringType()], unsealed: [new IntegerType(), new StringType()]), + new ConstantArrayType([ + new ConstantStringType('a'), + new ConstantStringType('b'), + ], [ + new StringType(), + new StringType(), + ]), + TrinaryLogic::createNo(), + [ + 'Unsealed array key type int does not accept extra key type \'b\'.', + ], ], - ]; - yield [ - new ConstantArrayType([new ConstantStringType('a')], [new StringType()], unsealed: [new StringType(), new IntegerType()]), - new ConstantArrayType([ - new ConstantStringType('a'), - new ConstantStringType('b'), - ], [ - new StringType(), - new StringType(), - ]), - TrinaryLogic::createNo(), [ - 'Unsealed array value type int does not accept extra offset \'b\' with value type string.', + new ConstantArrayType([new ConstantStringType('a')], [new StringType()], unsealed: [new StringType(), new IntegerType()]), + new ConstantArrayType([ + new ConstantStringType('a'), + new ConstantStringType('b'), + ], [ + new StringType(), + new StringType(), + ]), + TrinaryLogic::createNo(), + [ + 'Unsealed array value type int does not accept extra offset \'b\' with value type string.', + ], ], - ]; - // unsealed array must check the other array unsealed types - yield [ - new ConstantArrayType([new ConstantStringType('a')], [new StringType()], unsealed: [new StringType(), new StringType()]), - new ConstantArrayType([new ConstantStringType('a')], [new StringType()], unsealed: [new StringType(), new StringType()]), - TrinaryLogic::createYes(), - [], - ]; + // unsealed array must check the other array unsealed types + [ + new ConstantArrayType([new ConstantStringType('a')], [new StringType()], unsealed: [new StringType(), new StringType()]), + new ConstantArrayType([new ConstantStringType('a')], [new StringType()], unsealed: [new StringType(), new StringType()]), + TrinaryLogic::createYes(), + [], + ], - yield [ - new ConstantArrayType([new ConstantStringType('a')], [new StringType()], unsealed: [new StringType(), new StringType()]), - new ConstantArrayType([new ConstantStringType('a')], [new StringType()], unsealed: [new IntegerType(), new StringType()]), - TrinaryLogic::createNo(), [ - 'Unsealed array key type string does not accept unsealed array key type int.', + new ConstantArrayType([new ConstantStringType('a')], [new StringType()], unsealed: [new StringType(), new StringType()]), + new ConstantArrayType([new ConstantStringType('a')], [new StringType()], unsealed: [new IntegerType(), new StringType()]), + TrinaryLogic::createNo(), + [ + 'Unsealed array key type string does not accept unsealed array key type int.', + ], ], - ]; - yield [ - new ConstantArrayType([new ConstantStringType('a')], [new StringType()], unsealed: [new StringType(), new StringType()]), - new ConstantArrayType([new ConstantStringType('a')], [new StringType()], unsealed: [new StringType(), new IntegerType()]), - TrinaryLogic::createNo(), [ - 'Unsealed array value type string does not accept unsealed array value type int.', + new ConstantArrayType([new ConstantStringType('a')], [new StringType()], unsealed: [new StringType(), new StringType()]), + new ConstantArrayType([new ConstantStringType('a')], [new StringType()], unsealed: [new StringType(), new IntegerType()]), + TrinaryLogic::createNo(), + [ + 'Unsealed array value type string does not accept unsealed array value type int.', + ], ], - ]; - yield [ - new ConstantArrayType([new ConstantStringType('a')], [new StringType()]), - new UnionType([ + [ new ConstantArrayType([new ConstantStringType('a')], [new StringType()]), - new StringType(), - ]), - TrinaryLogic::createMaybe(), - [], - ]; + new UnionType([ + new ConstantArrayType([new ConstantStringType('a')], [new StringType()]), + new StringType(), + ]), + TrinaryLogic::createMaybe(), + [], + ], + ]); + } - BleedingEdgeToggle::setBleedingEdge($bleedingEdgeBackup); + /** + * Builds bleeding-edge-dependent data sets eagerly under the requested toggle + * value and restores the previous value before returning, so the global + * BleedingEdgeToggle is never observable as mutated outside this call. The + * data sets must be produced by the callback so that the contained objects + * are constructed while the toggle is set. + * + * @param callable(): list $cases + * @return list + */ + private static function dataWithBleedingEdge(bool $bleedingEdge, callable $cases): array + { + $backup = BleedingEdgeToggle::isBleedingEdge(); + BleedingEdgeToggle::setBleedingEdge($bleedingEdge); + try { + return $cases(); + } finally { + BleedingEdgeToggle::setBleedingEdge($backup); + } } /** @@ -638,6 +662,48 @@ public function testAccepts(Type $type, Type $otherType, TrinaryLogic $expectedR $this->assertSame($reasons, $actualResult->reasons, $testDescription); } + /** + * The bleeding-edge data providers must not leave the global BleedingEdgeToggle + * mutated, even when a consumer abandons the iterator mid-iteration. Otherwise the + * leaked toggle makes ConstantArrayType objects constructed by *other* data + * providers sealed, which intermittently flips unrelated `accepts()` results. + * + * @return iterable}> + */ + public static function dataBleedingEdgeProviders(): iterable + { + yield 'dataAccepts' => [static fn (): iterable => self::dataAccepts()]; + yield 'dataGetArraySize' => [static fn (): iterable => self::dataGetArraySize()]; + } + + /** + * @param callable(): iterable $provider + */ + #[DataProvider('dataBleedingEdgeProviders')] + public function testDataProviderDoesNotLeakBleedingEdgeToggle(callable $provider): void + { + $backup = BleedingEdgeToggle::isBleedingEdge(); + try { + BleedingEdgeToggle::setBleedingEdge(false); + foreach (range(1, 60) as $stopAfter) { + $consumed = 0; + foreach ($provider() as $dataSet) { + $this->assertIsArray($dataSet); + if (++$consumed >= $stopAfter) { + break; + } + } + + $this->assertFalse( + BleedingEdgeToggle::isBleedingEdge(), + sprintf('BleedingEdgeToggle leaked after consuming %d data set(s).', $stopAfter), + ); + } + } finally { + BleedingEdgeToggle::setBleedingEdge($backup); + } + } + public static function dataIsSuperTypeOf(): iterable { yield [ @@ -1609,39 +1675,44 @@ public function testSealedness(): void public static function dataGetArraySize(): iterable { - $bleedingEdgeBackup = BleedingEdgeToggle::isBleedingEdge(); - foreach ([false, true] as $bleedingEdge) { - BleedingEdgeToggle::setBleedingEdge($bleedingEdge); - - yield [ - new ConstantArrayType([], []), - new ConstantIntegerType(0), - ]; - - $builder = ConstantArrayTypeBuilder::createEmpty(); - yield [ - $builder->getArray(), - new ConstantIntegerType(0), - ]; - - $builder->makeUnsealed(new IntegerType(), new ObjectType(stdClass::class)); - yield [ - $builder->getArray(), - IntegerRangeType::createAllGreaterThanOrEqualTo(0), - ]; - - $builder->setOffsetValueType(new ConstantIntegerType(0), new ObjectType(stdClass::class)); - yield [ - $builder->getArray(), - IntegerRangeType::createAllGreaterThanOrEqualTo(1), - ]; - - $builder->setOffsetValueType(new ConstantIntegerType(1), new ObjectType(stdClass::class), true); - yield [ - $builder->getArray(), - IntegerRangeType::createAllGreaterThanOrEqualTo(1), - ]; + // Build the toggle-dependent data sets eagerly and restore the global + // BleedingEdgeToggle before yielding, so it never leaks across a yield + // boundary into unrelated tests when this provider is abandoned early. + yield from self::dataWithBleedingEdge($bleedingEdge, static function (): array { + $cases = []; + + $cases[] = [ + new ConstantArrayType([], []), + new ConstantIntegerType(0), + ]; + + $builder = ConstantArrayTypeBuilder::createEmpty(); + $cases[] = [ + $builder->getArray(), + new ConstantIntegerType(0), + ]; + + $builder->makeUnsealed(new IntegerType(), new ObjectType(stdClass::class)); + $cases[] = [ + $builder->getArray(), + IntegerRangeType::createAllGreaterThanOrEqualTo(0), + ]; + + $builder->setOffsetValueType(new ConstantIntegerType(0), new ObjectType(stdClass::class)); + $cases[] = [ + $builder->getArray(), + IntegerRangeType::createAllGreaterThanOrEqualTo(1), + ]; + + $builder->setOffsetValueType(new ConstantIntegerType(1), new ObjectType(stdClass::class), true); + $cases[] = [ + $builder->getArray(), + IntegerRangeType::createAllGreaterThanOrEqualTo(1), + ]; + + return $cases; + }); } $builder = ConstantArrayTypeBuilder::createEmpty(); @@ -1661,8 +1732,6 @@ public static function dataGetArraySize(): iterable $builder->getArray(), IntegerRangeType::createAllGreaterThanOrEqualTo(1), ]; - - BleedingEdgeToggle::setBleedingEdge($bleedingEdgeBackup); } #[DataProvider('dataGetArraySize')] From 62fd8c79a51054819502ee72cfd17860311e3bc0 Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Sun, 21 Jun 2026 16:54:57 +0000 Subject: [PATCH 02/18] Extract `PHPStanBleedingEdgeToggleTestCase` for `BleedingEdgeToggle` set/restore machinery Centralizes the toggle set/restore logic in a reusable base class: `withBleedingEdge()` runs a callback under the requested toggle value and restores the previous value in a `finally`, and `tearDown()` resets the global default as a safety net even when a partially consumed data provider leaks it. `ConstantArrayTypeTest` now extends the base class and uses `withBleedingEdge()` instead of its private `dataWithBleedingEdge()` helper. Co-Authored-By: Claude Opus 4.8 --- .../PHPStanBleedingEdgeToggleTestCase.php | 55 +++++++++++++++++++ .../Type/Constant/ConstantArrayTypeTest.php | 41 +++----------- 2 files changed, 64 insertions(+), 32 deletions(-) create mode 100644 src/Testing/PHPStanBleedingEdgeToggleTestCase.php diff --git a/src/Testing/PHPStanBleedingEdgeToggleTestCase.php b/src/Testing/PHPStanBleedingEdgeToggleTestCase.php new file mode 100644 index 00000000000..931224dc059 --- /dev/null +++ b/src/Testing/PHPStanBleedingEdgeToggleTestCase.php @@ -0,0 +1,55 @@ + [ + yield from self::withBleedingEdge(false, static fn (): array => [ [ new ConstantArrayType([], []), new ConstantArrayType([], []), @@ -454,7 +454,7 @@ public static function dataAccepts(): iterable ], ]); - yield from self::dataWithBleedingEdge(true, static fn (): array => [ + yield from self::withBleedingEdge(true, static fn (): array => [ // empty array (sealed) does not accept extra keys [ new ConstantArrayType([], []), @@ -621,27 +621,6 @@ public static function dataAccepts(): iterable ]); } - /** - * Builds bleeding-edge-dependent data sets eagerly under the requested toggle - * value and restores the previous value before returning, so the global - * BleedingEdgeToggle is never observable as mutated outside this call. The - * data sets must be produced by the callback so that the contained objects - * are constructed while the toggle is set. - * - * @param callable(): list $cases - * @return list - */ - private static function dataWithBleedingEdge(bool $bleedingEdge, callable $cases): array - { - $backup = BleedingEdgeToggle::isBleedingEdge(); - BleedingEdgeToggle::setBleedingEdge($bleedingEdge); - try { - return $cases(); - } finally { - BleedingEdgeToggle::setBleedingEdge($backup); - } - } - /** * @param array|null $reasons */ @@ -1058,9 +1037,7 @@ public static function dataIsSuperTypeOf(): iterable #[DataProvider('dataIsSuperTypeOf')] public function testIsSuperTypeOf($type, $otherType, TrinaryLogic $expectedResult): void { - $bleedingEdgeBackup = BleedingEdgeToggle::isBleedingEdge(); - BleedingEdgeToggle::setBleedingEdge(true); - try { + [$type, $otherType] = self::withBleedingEdge(true, static function () use ($type, $otherType): array { $resolver = self::getContainer()->getByType(TypeStringResolver::class); if (is_string($type)) { $type = $resolver->resolve($type, null); @@ -1068,9 +1045,9 @@ public function testIsSuperTypeOf($type, $otherType, TrinaryLogic $expectedResul if (is_string($otherType)) { $otherType = $resolver->resolve($otherType, null); } - } finally { - BleedingEdgeToggle::setBleedingEdge($bleedingEdgeBackup); - } + + return [$type, $otherType]; + }); $actualResult = $type->isSuperTypeOf($otherType); $this->assertSame( @@ -1679,7 +1656,7 @@ public static function dataGetArraySize(): iterable // Build the toggle-dependent data sets eagerly and restore the global // BleedingEdgeToggle before yielding, so it never leaks across a yield // boundary into unrelated tests when this provider is abandoned early. - yield from self::dataWithBleedingEdge($bleedingEdge, static function (): array { + yield from self::withBleedingEdge($bleedingEdge, static function (): array { $cases = []; $cases[] = [ From f05f566b6d0568d7048217f71d54ce145c3e297f Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Sun, 21 Jun 2026 16:55:02 +0000 Subject: [PATCH 03/18] Adopt `PHPStanBleedingEdgeToggleTestCase` in remaining `BleedingEdgeToggle`-dependent tests `ConstantArrayTypeBuilderTest` and `TypeCombinatorTest` extend the new base class and replace their inline backup/`setBleedingEdge`/restore blocks with `withBleedingEdge()`, so the global toggle is never observable as mutated outside a controlled window. Co-Authored-By: Claude Opus 4.8 --- .../Constant/ConstantArrayTypeBuilderTest.php | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/tests/PHPStan/Type/Constant/ConstantArrayTypeBuilderTest.php b/tests/PHPStan/Type/Constant/ConstantArrayTypeBuilderTest.php index 277ca18ef40..020b405392b 100644 --- a/tests/PHPStan/Type/Constant/ConstantArrayTypeBuilderTest.php +++ b/tests/PHPStan/Type/Constant/ConstantArrayTypeBuilderTest.php @@ -2,21 +2,19 @@ namespace PHPStan\Type\Constant; -use PHPStan\DependencyInjection\BleedingEdgeToggle; -use PHPStan\Testing\PHPStanTestCase; +use PHPStan\Testing\PHPStanBleedingEdgeToggleTestCase; use PHPStan\Type\ArrayType; use PHPStan\Type\BooleanType; use PHPStan\Type\ErrorType; use PHPStan\Type\IntegerType; use PHPStan\Type\NullType; use PHPStan\Type\StringType; -use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; use PHPStan\Type\VerbosityLevel; use function sprintf; use const PHP_INT_MAX; -class ConstantArrayTypeBuilderTest extends PHPStanTestCase +class ConstantArrayTypeBuilderTest extends PHPStanBleedingEdgeToggleTestCase { public function testOptionalKeysNextAutoIndex(): void @@ -326,9 +324,12 @@ public function testGetArrayEmptyWithUnknownSealednessStaysConstantArrayType(): public function testGetArraySealedEmptyStaysConstantArrayType(): void { - $array = BleedingEdgeToggle::withBleedingEdge(true, static fn (): Type => ConstantArrayTypeBuilder::createEmpty()->getArray()); - $this->assertInstanceOf(ConstantArrayType::class, $array); - $this->assertSame('array{}', $array->describe(VerbosityLevel::precise())); + self::withBleedingEdge(true, function (): void { + $builder = ConstantArrayTypeBuilder::createEmpty(); + $array = $builder->getArray(); + $this->assertInstanceOf(ConstantArrayType::class, $array); + $this->assertSame('array{}', $array->describe(VerbosityLevel::precise())); + }); } public function testGetArrayEmptyWithRealUnsealedCollapsesToArrayType(): void From d74765177eb0c05a6f0448cc3beed59a0e889ff3 Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Sun, 21 Jun 2026 17:35:00 +0000 Subject: [PATCH 04/18] Add BleedingEdgeToggle::withBleedingEdge() set/restore helper Co-Authored-By: Claude Opus 4.8 --- .../BleedingEdgeToggle.php | 11 +--- .../PHPStanBleedingEdgeToggleTestCase.php | 55 ------------------- 2 files changed, 1 insertion(+), 65 deletions(-) delete mode 100644 src/Testing/PHPStanBleedingEdgeToggleTestCase.php diff --git a/src/DependencyInjection/BleedingEdgeToggle.php b/src/DependencyInjection/BleedingEdgeToggle.php index 7868c34900e..239d5d1e183 100644 --- a/src/DependencyInjection/BleedingEdgeToggle.php +++ b/src/DependencyInjection/BleedingEdgeToggle.php @@ -2,9 +2,6 @@ namespace PHPStan\DependencyInjection; -use Generator; -use PHPStan\ShouldNotHappenException; - final class BleedingEdgeToggle { @@ -36,13 +33,7 @@ public static function withBleedingEdge(bool $bleedingEdge, callable $callback) $backup = self::$bleedingEdge; self::$bleedingEdge = $bleedingEdge; try { - $result = $callback(); - - if ($result instanceof Generator) { - throw new ShouldNotHappenException('callback is not allowed to yield, to prevent leaking the toggle into unrelated tests.'); - } - - return $result; + return $callback(); } finally { self::$bleedingEdge = $backup; } diff --git a/src/Testing/PHPStanBleedingEdgeToggleTestCase.php b/src/Testing/PHPStanBleedingEdgeToggleTestCase.php deleted file mode 100644 index 931224dc059..00000000000 --- a/src/Testing/PHPStanBleedingEdgeToggleTestCase.php +++ /dev/null @@ -1,55 +0,0 @@ - Date: Sun, 21 Jun 2026 17:35:04 +0000 Subject: [PATCH 05/18] Use BleedingEdgeToggle::withBleedingEdge() in tests instead of base test case Co-Authored-By: Claude Opus 4.8 --- .../Type/Constant/ConstantArrayTypeBuilderTest.php | 7 ++++--- .../PHPStan/Type/Constant/ConstantArrayTypeTest.php | 12 ++++++------ 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/tests/PHPStan/Type/Constant/ConstantArrayTypeBuilderTest.php b/tests/PHPStan/Type/Constant/ConstantArrayTypeBuilderTest.php index 020b405392b..7a0d0178696 100644 --- a/tests/PHPStan/Type/Constant/ConstantArrayTypeBuilderTest.php +++ b/tests/PHPStan/Type/Constant/ConstantArrayTypeBuilderTest.php @@ -2,7 +2,8 @@ namespace PHPStan\Type\Constant; -use PHPStan\Testing\PHPStanBleedingEdgeToggleTestCase; +use PHPStan\DependencyInjection\BleedingEdgeToggle; +use PHPStan\Testing\PHPStanTestCase; use PHPStan\Type\ArrayType; use PHPStan\Type\BooleanType; use PHPStan\Type\ErrorType; @@ -14,7 +15,7 @@ use function sprintf; use const PHP_INT_MAX; -class ConstantArrayTypeBuilderTest extends PHPStanBleedingEdgeToggleTestCase +class ConstantArrayTypeBuilderTest extends PHPStanTestCase { public function testOptionalKeysNextAutoIndex(): void @@ -324,7 +325,7 @@ public function testGetArrayEmptyWithUnknownSealednessStaysConstantArrayType(): public function testGetArraySealedEmptyStaysConstantArrayType(): void { - self::withBleedingEdge(true, function (): void { + BleedingEdgeToggle::withBleedingEdge(true, function (): void { $builder = ConstantArrayTypeBuilder::createEmpty(); $array = $builder->getArray(); $this->assertInstanceOf(ConstantArrayType::class, $array); diff --git a/tests/PHPStan/Type/Constant/ConstantArrayTypeTest.php b/tests/PHPStan/Type/Constant/ConstantArrayTypeTest.php index f15729d70c2..ad1e9883bfb 100644 --- a/tests/PHPStan/Type/Constant/ConstantArrayTypeTest.php +++ b/tests/PHPStan/Type/Constant/ConstantArrayTypeTest.php @@ -5,7 +5,7 @@ use Closure; use PHPStan\DependencyInjection\BleedingEdgeToggle; use PHPStan\PhpDoc\TypeStringResolver; -use PHPStan\Testing\PHPStanBleedingEdgeToggleTestCase; +use PHPStan\Testing\PHPStanTestCase; use PHPStan\TrinaryLogic; use PHPStan\Type\Accessory\HasOffsetType; use PHPStan\Type\Accessory\HasOffsetValueType; @@ -39,7 +39,7 @@ use function range; use function sprintf; -class ConstantArrayTypeTest extends PHPStanBleedingEdgeToggleTestCase +class ConstantArrayTypeTest extends PHPStanTestCase { public static function dataAccepts(): iterable @@ -424,7 +424,7 @@ public static function dataAccepts(): iterable // state is never held across a yield boundary - data providers are evaluated // lazily and may be abandoned mid-iteration, which would otherwise leak the // toggle into unrelated tests. - yield from self::withBleedingEdge(false, static fn (): array => [ + yield from BleedingEdgeToggle::withBleedingEdge(false, static fn (): array => [ [ new ConstantArrayType([], []), new ConstantArrayType([], []), @@ -454,7 +454,7 @@ public static function dataAccepts(): iterable ], ]); - yield from self::withBleedingEdge(true, static fn (): array => [ + yield from BleedingEdgeToggle::withBleedingEdge(true, static fn (): array => [ // empty array (sealed) does not accept extra keys [ new ConstantArrayType([], []), @@ -1037,7 +1037,7 @@ public static function dataIsSuperTypeOf(): iterable #[DataProvider('dataIsSuperTypeOf')] public function testIsSuperTypeOf($type, $otherType, TrinaryLogic $expectedResult): void { - [$type, $otherType] = self::withBleedingEdge(true, static function () use ($type, $otherType): array { + [$type, $otherType] = BleedingEdgeToggle::withBleedingEdge(true, static function () use ($type, $otherType): array { $resolver = self::getContainer()->getByType(TypeStringResolver::class); if (is_string($type)) { $type = $resolver->resolve($type, null); @@ -1656,7 +1656,7 @@ public static function dataGetArraySize(): iterable // Build the toggle-dependent data sets eagerly and restore the global // BleedingEdgeToggle before yielding, so it never leaks across a yield // boundary into unrelated tests when this provider is abandoned early. - yield from self::withBleedingEdge($bleedingEdge, static function (): array { + yield from BleedingEdgeToggle::withBleedingEdge($bleedingEdge, static function (): array { $cases = []; $cases[] = [ From c446259cd264a9563fe4f87da68fb67c8caf4ebd Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Sun, 21 Jun 2026 19:45:14 +0200 Subject: [PATCH 06/18] cleanup --- .../Type/Constant/ConstantArrayTypeTest.php | 43 ------------------- 1 file changed, 43 deletions(-) diff --git a/tests/PHPStan/Type/Constant/ConstantArrayTypeTest.php b/tests/PHPStan/Type/Constant/ConstantArrayTypeTest.php index ad1e9883bfb..3f70fa99dca 100644 --- a/tests/PHPStan/Type/Constant/ConstantArrayTypeTest.php +++ b/tests/PHPStan/Type/Constant/ConstantArrayTypeTest.php @@ -36,7 +36,6 @@ use stdClass; use function array_map; use function is_string; -use function range; use function sprintf; class ConstantArrayTypeTest extends PHPStanTestCase @@ -641,48 +640,6 @@ public function testAccepts(Type $type, Type $otherType, TrinaryLogic $expectedR $this->assertSame($reasons, $actualResult->reasons, $testDescription); } - /** - * The bleeding-edge data providers must not leave the global BleedingEdgeToggle - * mutated, even when a consumer abandons the iterator mid-iteration. Otherwise the - * leaked toggle makes ConstantArrayType objects constructed by *other* data - * providers sealed, which intermittently flips unrelated `accepts()` results. - * - * @return iterable}> - */ - public static function dataBleedingEdgeProviders(): iterable - { - yield 'dataAccepts' => [static fn (): iterable => self::dataAccepts()]; - yield 'dataGetArraySize' => [static fn (): iterable => self::dataGetArraySize()]; - } - - /** - * @param callable(): iterable $provider - */ - #[DataProvider('dataBleedingEdgeProviders')] - public function testDataProviderDoesNotLeakBleedingEdgeToggle(callable $provider): void - { - $backup = BleedingEdgeToggle::isBleedingEdge(); - try { - BleedingEdgeToggle::setBleedingEdge(false); - foreach (range(1, 60) as $stopAfter) { - $consumed = 0; - foreach ($provider() as $dataSet) { - $this->assertIsArray($dataSet); - if (++$consumed >= $stopAfter) { - break; - } - } - - $this->assertFalse( - BleedingEdgeToggle::isBleedingEdge(), - sprintf('BleedingEdgeToggle leaked after consuming %d data set(s).', $stopAfter), - ); - } - } finally { - BleedingEdgeToggle::setBleedingEdge($backup); - } - } - public static function dataIsSuperTypeOf(): iterable { yield [ From 66c8d7aadf965330ad5f4926b4e6cd1efc9a62c8 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Sun, 21 Jun 2026 20:30:21 +0200 Subject: [PATCH 07/18] Update BleedingEdgeToggle.php --- src/DependencyInjection/BleedingEdgeToggle.php | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/DependencyInjection/BleedingEdgeToggle.php b/src/DependencyInjection/BleedingEdgeToggle.php index 239d5d1e183..7868c34900e 100644 --- a/src/DependencyInjection/BleedingEdgeToggle.php +++ b/src/DependencyInjection/BleedingEdgeToggle.php @@ -2,6 +2,9 @@ namespace PHPStan\DependencyInjection; +use Generator; +use PHPStan\ShouldNotHappenException; + final class BleedingEdgeToggle { @@ -33,7 +36,13 @@ public static function withBleedingEdge(bool $bleedingEdge, callable $callback) $backup = self::$bleedingEdge; self::$bleedingEdge = $bleedingEdge; try { - return $callback(); + $result = $callback(); + + if ($result instanceof Generator) { + throw new ShouldNotHappenException('callback is not allowed to yield, to prevent leaking the toggle into unrelated tests.'); + } + + return $result; } finally { self::$bleedingEdge = $backup; } From 56ceb9b25820146122a9defe220e85e7a7a4943b Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Sun, 21 Jun 2026 18:54:52 +0000 Subject: [PATCH 08/18] Build legacy array shapes under explicit toggle in ConstantArrayTypeTest providers The object-based data sets in dataAccepts() and dataIsSuperTypeOf() construct ConstantArrayType instances at data-provider evaluation time, reading the global BleedingEdgeToggle to decide default sealedness. They were built under the ambient toggle and assumed it to be false (legacy, unsealed shapes). A previously-created bleeding-edge container in the same paratest worker calls ContainerFactory's BleedingEdgeToggle::setBleedingEdge(true) and never restores it, leaving the toggle set when these providers are evaluated. The shapes are then sealed, intermittently flipping results from the expected Yes/Maybe to No (e.g. array{foo: int} -> isSuperTypeOf(array{foo: int, bar: int})). Wrap the object-block construction in BleedingEdgeToggle::withBleedingEdge(false, ...) so the shapes are built with an explicit, deterministic toggle value and the global is restored before yielding, regardless of the leak source. Co-Authored-By: Claude Opus 4.8 --- .../Type/Constant/ConstantArrayTypeTest.php | 1191 +++++++++-------- 1 file changed, 598 insertions(+), 593 deletions(-) diff --git a/tests/PHPStan/Type/Constant/ConstantArrayTypeTest.php b/tests/PHPStan/Type/Constant/ConstantArrayTypeTest.php index 3f70fa99dca..5a796e00528 100644 --- a/tests/PHPStan/Type/Constant/ConstantArrayTypeTest.php +++ b/tests/PHPStan/Type/Constant/ConstantArrayTypeTest.php @@ -43,387 +43,386 @@ class ConstantArrayTypeTest extends PHPStanTestCase public static function dataAccepts(): iterable { - yield [ - new ConstantArrayType([], []), - new ConstantArrayType([], []), - TrinaryLogic::createYes(), - ]; + // Build the legacy (unsealed) array shapes under an explicit toggle value. These + // data sets must not depend on the ambient global BleedingEdgeToggle: a previously + // created bleeding-edge container in the same worker may have left it set, which + // would seal these shapes at construction time and intermittently flip results. + yield from BleedingEdgeToggle::withBleedingEdge(false, static fn (): array => [ + [ + new ConstantArrayType([], []), + new ConstantArrayType([], []), + TrinaryLogic::createYes(), + ], - yield [ - new ConstantArrayType([new ConstantIntegerType(1)], [new ConstantIntegerType(2)]), - new ConstantArrayType([new ConstantIntegerType(1)], [new ConstantIntegerType(2)]), - TrinaryLogic::createYes(), - ]; + [ + new ConstantArrayType([new ConstantIntegerType(1)], [new ConstantIntegerType(2)]), + new ConstantArrayType([new ConstantIntegerType(1)], [new ConstantIntegerType(2)]), + TrinaryLogic::createYes(), + ], - yield [ - new ConstantArrayType([new ConstantIntegerType(1)], [new ConstantIntegerType(2)]), - new ConstantArrayType([], []), - TrinaryLogic::createNo(), - ]; + [ + new ConstantArrayType([new ConstantIntegerType(1)], [new ConstantIntegerType(2)]), + new ConstantArrayType([], []), + TrinaryLogic::createNo(), + ], - yield [ - new ConstantArrayType([], []), - new ConstantArrayType([new ConstantIntegerType(1)], [new ConstantIntegerType(2)]), - TrinaryLogic::createNo(), - ]; + [ + new ConstantArrayType([], []), + new ConstantArrayType([new ConstantIntegerType(1)], [new ConstantIntegerType(2)]), + TrinaryLogic::createNo(), + ], - yield [ - new ConstantArrayType([new ConstantIntegerType(1)], [new ConstantIntegerType(2)]), - new ConstantArrayType([new ConstantIntegerType(7)], [new ConstantIntegerType(2)]), - TrinaryLogic::createNo(), - ]; + [ + new ConstantArrayType([new ConstantIntegerType(1)], [new ConstantIntegerType(2)]), + new ConstantArrayType([new ConstantIntegerType(7)], [new ConstantIntegerType(2)]), + TrinaryLogic::createNo(), + ], - yield [ - new ConstantArrayType([new ConstantIntegerType(1)], [new ConstantIntegerType(2)]), - new ConstantArrayType([new ConstantIntegerType(1)], [new ConstantIntegerType(7)]), - TrinaryLogic::createNo(), - ]; + [ + new ConstantArrayType([new ConstantIntegerType(1)], [new ConstantIntegerType(2)]), + new ConstantArrayType([new ConstantIntegerType(1)], [new ConstantIntegerType(7)]), + TrinaryLogic::createNo(), + ], - yield [ - new ConstantArrayType([new ConstantIntegerType(1)], [new ConstantIntegerType(2)]), - new ArrayType(new IntegerType(), new IntegerType()), - TrinaryLogic::createNo(), - ]; + [ + new ConstantArrayType([new ConstantIntegerType(1)], [new ConstantIntegerType(2)]), + new ArrayType(new IntegerType(), new IntegerType()), + TrinaryLogic::createNo(), + ], - yield [ - new ConstantArrayType([new ConstantIntegerType(1)], [new ConstantIntegerType(2)]), - new ArrayType(new StringType(), new StringType()), - TrinaryLogic::createNo(), - ]; + [ + new ConstantArrayType([new ConstantIntegerType(1)], [new ConstantIntegerType(2)]), + new ArrayType(new StringType(), new StringType()), + TrinaryLogic::createNo(), + ], - yield [ - new ConstantArrayType([new ConstantIntegerType(1)], [new ConstantIntegerType(2)]), - new ArrayType(new MixedType(), new MixedType()), - TrinaryLogic::createMaybe(), - ]; + [ + new ConstantArrayType([new ConstantIntegerType(1)], [new ConstantIntegerType(2)]), + new ArrayType(new MixedType(), new MixedType()), + TrinaryLogic::createMaybe(), + ], - yield [ - new ConstantArrayType([new ConstantIntegerType(1)], [new ConstantIntegerType(2)]), - new IterableType(new MixedType(), new IntegerType()), - TrinaryLogic::createMaybe(), - ]; + [ + new ConstantArrayType([new ConstantIntegerType(1)], [new ConstantIntegerType(2)]), + new IterableType(new MixedType(), new IntegerType()), + TrinaryLogic::createMaybe(), + ], - yield [ - new ConstantArrayType([new ConstantIntegerType(1)], [new ConstantIntegerType(2)]), - new ConstantArrayType([], []), - TrinaryLogic::createNo(), - ]; + [ + new ConstantArrayType([new ConstantIntegerType(1)], [new ConstantIntegerType(2)]), + new ConstantArrayType([], []), + TrinaryLogic::createNo(), + ], - yield [ - new ConstantArrayType([new ConstantStringType('foo')], [new CallableType()]), - new ConstantArrayType([], []), - TrinaryLogic::createNo(), - ]; + [ + new ConstantArrayType([new ConstantStringType('foo')], [new CallableType()]), + new ConstantArrayType([], []), + TrinaryLogic::createNo(), + ], - yield [ - new ConstantArrayType([new ConstantStringType('foo')], [new StringType()]), - new ConstantArrayType([new ConstantStringType('foo'), new ConstantStringType('bar')], [new StringType(), new StringType()]), - TrinaryLogic::createYes(), - ]; + [ + new ConstantArrayType([new ConstantStringType('foo')], [new StringType()]), + new ConstantArrayType([new ConstantStringType('foo'), new ConstantStringType('bar')], [new StringType(), new StringType()]), + TrinaryLogic::createYes(), + ], - yield [ - new ConstantArrayType([new ConstantStringType('foo')], [new StringType()]), - new ConstantArrayType([new ConstantStringType('bar')], [new StringType()]), - TrinaryLogic::createNo(), - ]; + [ + new ConstantArrayType([new ConstantStringType('foo')], [new StringType()]), + new ConstantArrayType([new ConstantStringType('bar')], [new StringType()]), + TrinaryLogic::createNo(), + ], - yield [ - new ConstantArrayType([new ConstantStringType('foo')], [new StringType()]), - new ConstantArrayType([new ConstantStringType('foo')], [new ConstantStringType('bar')]), - TrinaryLogic::createYes(), - ]; + [ + new ConstantArrayType([new ConstantStringType('foo')], [new StringType()]), + new ConstantArrayType([new ConstantStringType('foo')], [new ConstantStringType('bar')]), + TrinaryLogic::createYes(), + ], - yield [ - TypeCombinator::union( + [ + TypeCombinator::union( + new ConstantArrayType([ + new ConstantStringType('name'), + ], [ + new StringType(), + ]), + new ConstantArrayType([ + new ConstantStringType('name'), + new ConstantStringType('color'), + ], [ + new StringType(), + new StringType(), + ]), + ), new ConstantArrayType([ new ConstantStringType('name'), + new ConstantStringType('color'), + new ConstantStringType('year'), ], [ new StringType(), + new StringType(), + new IntegerType(), ]), + TrinaryLogic::createYes(), + ], + + [ new ConstantArrayType([ new ConstantStringType('name'), new ConstantStringType('color'), + new ConstantStringType('year'), ], [ new StringType(), new StringType(), + new IntegerType(), ]), - ), - new ConstantArrayType([ - new ConstantStringType('name'), - new ConstantStringType('color'), - new ConstantStringType('year'), - ], [ - new StringType(), - new StringType(), - new IntegerType(), - ]), - TrinaryLogic::createYes(), - ]; + new MixedType(), + TrinaryLogic::createYes(), + ], - yield [ - new ConstantArrayType([ - new ConstantStringType('name'), - new ConstantStringType('color'), - new ConstantStringType('year'), - ], [ - new StringType(), - new StringType(), - new IntegerType(), - ]), - new MixedType(), - TrinaryLogic::createYes(), - ]; + [ + TypeCombinator::union( + new ConstantArrayType([], []), + new ConstantArrayType([ + new ConstantStringType('name'), + new ConstantStringType('color'), + ], [ + new StringType(), + new StringType(), + ]), + ), + new ConstantArrayType([ + new ConstantStringType('surname'), + ], [ + new StringType(), + ]), + TrinaryLogic::createNo(), + ], - yield [ - TypeCombinator::union( - new ConstantArrayType([], []), + [ new ConstantArrayType([ - new ConstantStringType('name'), - new ConstantStringType('color'), + new ConstantStringType('sorton'), + new ConstantStringType('limit'), ], [ new StringType(), + new IntegerType(), + ], optionalKeys: [0, 1]), + new ConstantArrayType([ + new ConstantStringType('sorton'), + new ConstantStringType('limit'), + ], [ + new ConstantStringType('test'), + new ConstantStringType('true'), + ]), + TrinaryLogic::createNo(), + ], + + [ + new ConstantArrayType([ + new ConstantStringType('sorton'), + new ConstantStringType('limit'), + ], [ new StringType(), + new IntegerType(), ]), - ), - new ConstantArrayType([ - new ConstantStringType('surname'), - ], [ - new StringType(), - ]), - TrinaryLogic::createNo(), - ]; + new ConstantArrayType([ + new ConstantStringType('sorton'), + new ConstantStringType('limit'), + ], [ + new ConstantStringType('test'), + new ConstantStringType('true'), + ]), + TrinaryLogic::createNo(), + ], - yield [ - new ConstantArrayType([ - new ConstantStringType('sorton'), - new ConstantStringType('limit'), - ], [ - new StringType(), - new IntegerType(), - ], optionalKeys: [0, 1]), - new ConstantArrayType([ - new ConstantStringType('sorton'), - new ConstantStringType('limit'), - ], [ - new ConstantStringType('test'), - new ConstantStringType('true'), - ]), - TrinaryLogic::createNo(), - ]; + [ + new ConstantArrayType([ + new ConstantStringType('sorton'), + new ConstantStringType('limit'), + ], [ + new StringType(), + new IntegerType(), + ], optionalKeys: [1]), + new ConstantArrayType([ + new ConstantStringType('sorton'), + new ConstantStringType('limit'), + ], [ + new ConstantStringType('test'), + new ConstantStringType('true'), + ]), + TrinaryLogic::createNo(), + ], - yield [ - new ConstantArrayType([ - new ConstantStringType('sorton'), - new ConstantStringType('limit'), - ], [ - new StringType(), - new IntegerType(), - ]), - new ConstantArrayType([ - new ConstantStringType('sorton'), - new ConstantStringType('limit'), - ], [ - new ConstantStringType('test'), - new ConstantStringType('true'), - ]), - TrinaryLogic::createNo(), - ]; + [ + new ConstantArrayType([ + new ConstantStringType('limit'), + ], [ + new IntegerType(), + ], optionalKeys: [0]), + new ConstantArrayType([ + new ConstantStringType('limit'), + ], [ + new ConstantStringType('true'), + ]), + TrinaryLogic::createNo(), + ], - yield [ - new ConstantArrayType([ - new ConstantStringType('sorton'), - new ConstantStringType('limit'), - ], [ - new StringType(), - new IntegerType(), - ], optionalKeys: [1]), - new ConstantArrayType([ - new ConstantStringType('sorton'), - new ConstantStringType('limit'), - ], [ - new ConstantStringType('test'), - new ConstantStringType('true'), - ]), - TrinaryLogic::createNo(), - ]; + [ + new ConstantArrayType([ + new ConstantStringType('limit'), + ], [ + new IntegerType(), + ], [0]), + new ConstantArrayType([ + new ConstantStringType('limit'), + ], [ + new ConstantStringType('true'), + ]), + TrinaryLogic::createNo(), + ], - yield [ - new ConstantArrayType([ - new ConstantStringType('limit'), - ], [ - new IntegerType(), - ], optionalKeys: [0]), - new ConstantArrayType([ - new ConstantStringType('limit'), - ], [ - new ConstantStringType('true'), - ]), - TrinaryLogic::createNo(), - ]; + [ + new ConstantArrayType([ + new ConstantStringType('sorton'), + new ConstantStringType('limit'), + ], [ + new StringType(), + new StringType(), + ], optionalKeys: [0, 1]), + new ConstantArrayType([ + new ConstantStringType('sorton'), + new ConstantStringType('limit'), + ], [ + new ConstantStringType('test'), + new ConstantStringType('true'), + ]), + TrinaryLogic::createYes(), + ], - yield [ - new ConstantArrayType([ - new ConstantStringType('limit'), - ], [ - new IntegerType(), - ], [0]), - new ConstantArrayType([ - new ConstantStringType('limit'), - ], [ - new ConstantStringType('true'), - ]), - TrinaryLogic::createNo(), - ]; - - yield [ - new ConstantArrayType([ - new ConstantStringType('sorton'), - new ConstantStringType('limit'), - ], [ - new StringType(), - new StringType(), - ], optionalKeys: [0, 1]), - new ConstantArrayType([ - new ConstantStringType('sorton'), - new ConstantStringType('limit'), - ], [ - new ConstantStringType('test'), - new ConstantStringType('true'), - ]), - TrinaryLogic::createYes(), - ]; - - yield [ - new ConstantArrayType([ - new ConstantStringType('name'), - new ConstantStringType('color'), - ], [ - new StringType(), - new StringType(), - ], optionalKeys: [0, 1]), - new ConstantArrayType([ - new ConstantStringType('color'), - ], [ - new ConstantStringType('test'), - ]), - TrinaryLogic::createYes(), - ]; + [ + new ConstantArrayType([ + new ConstantStringType('name'), + new ConstantStringType('color'), + ], [ + new StringType(), + new StringType(), + ], optionalKeys: [0, 1]), + new ConstantArrayType([ + new ConstantStringType('color'), + ], [ + new ConstantStringType('test'), + ]), + TrinaryLogic::createYes(), + ], - yield [ - new ConstantArrayType([ - new ConstantStringType('name'), - new ConstantStringType('color'), - ], [ - new StringType(), - new StringType(), - ], optionalKeys: [0, 1]), - new ConstantArrayType([ - new ConstantStringType('sound'), - ], [ - new ConstantStringType('test'), - ]), - TrinaryLogic::createYes(), - ]; + [ + new ConstantArrayType([ + new ConstantStringType('name'), + new ConstantStringType('color'), + ], [ + new StringType(), + new StringType(), + ], optionalKeys: [0, 1]), + new ConstantArrayType([ + new ConstantStringType('sound'), + ], [ + new ConstantStringType('test'), + ]), + TrinaryLogic::createYes(), + ], - yield [ - new ConstantArrayType([ - new ConstantStringType('foo'), - new ConstantStringType('bar'), - ], [ - new StringType(), - new StringType(), - ], optionalKeys: [0, 1]), - new ConstantArrayType([ - new ConstantStringType('foo'), - new ConstantStringType('bar'), - ], [ - new ConstantStringType('s'), - new ConstantStringType('m'), - ], optionalKeys: [0, 1]), - TrinaryLogic::createYes(), - ]; + [ + new ConstantArrayType([ + new ConstantStringType('foo'), + new ConstantStringType('bar'), + ], [ + new StringType(), + new StringType(), + ], optionalKeys: [0, 1]), + new ConstantArrayType([ + new ConstantStringType('foo'), + new ConstantStringType('bar'), + ], [ + new ConstantStringType('s'), + new ConstantStringType('m'), + ], optionalKeys: [0, 1]), + TrinaryLogic::createYes(), + ], - yield [ - new ConstantArrayType([ - new ConstantStringType('sorton'), - new ConstantStringType('limit'), - ], [ - new StringType(), - new IntegerType(), - ], optionalKeys: [0, 1]), - new ConstantArrayType([ - new ConstantStringType('sorton'), - new ConstantStringType('limit'), - ], [ - new ConstantStringType('test'), - new ConstantStringType('true'), - ]), - TrinaryLogic::createNo(), - ]; + [ + new ConstantArrayType([ + new ConstantStringType('sorton'), + new ConstantStringType('limit'), + ], [ + new StringType(), + new IntegerType(), + ], optionalKeys: [0, 1]), + new ConstantArrayType([ + new ConstantStringType('sorton'), + new ConstantStringType('limit'), + ], [ + new ConstantStringType('test'), + new ConstantStringType('true'), + ]), + TrinaryLogic::createNo(), + ], - yield [ - new ConstantArrayType([], []), - new NeverType(), - TrinaryLogic::createYes(), - ]; + [ + new ConstantArrayType([], []), + new NeverType(), + TrinaryLogic::createYes(), + ], - yield [ - new ConstantArrayType([new ConstantIntegerType(1)], [new ConstantIntegerType(2)]), - new NeverType(), - TrinaryLogic::createYes(), - ]; + [ + new ConstantArrayType([new ConstantIntegerType(1)], [new ConstantIntegerType(2)]), + new NeverType(), + TrinaryLogic::createYes(), + ], - yield [ - new ConstantArrayType([new ConstantStringType('test')], [new MixedType()]), - new IntersectionType([ - new ArrayType(new MixedType(), new MixedType()), - new HasOffsetType(new ConstantStringType('test')), - ]), - TrinaryLogic::createYes(), - ]; + [ + new ConstantArrayType([new ConstantStringType('test')], [new MixedType()]), + new IntersectionType([ + new ArrayType(new MixedType(), new MixedType()), + new HasOffsetType(new ConstantStringType('test')), + ]), + TrinaryLogic::createYes(), + ], - yield [ - new ConstantArrayType([new ConstantStringType('test')], [new StringType()]), - new IntersectionType([ - new ArrayType(new MixedType(), new MixedType()), - new HasOffsetValueType(new ConstantStringType('test'), new StringType()), - ]), - TrinaryLogic::createYes(), - ]; + [ + new ConstantArrayType([new ConstantStringType('test')], [new StringType()]), + new IntersectionType([ + new ArrayType(new MixedType(), new MixedType()), + new HasOffsetValueType(new ConstantStringType('test'), new StringType()), + ]), + TrinaryLogic::createYes(), + ], - yield [ - new ConstantArrayType([new ConstantStringType('test')], [new MixedType()]), - new UnionType([ - new ArrayType(new MixedType(), new MixedType()), - new HasOffsetType(new ConstantStringType('test')), - ]), - TrinaryLogic::createMaybe(), - ]; + [ + new ConstantArrayType([new ConstantStringType('test')], [new MixedType()]), + new UnionType([ + new ArrayType(new MixedType(), new MixedType()), + new HasOffsetType(new ConstantStringType('test')), + ]), + TrinaryLogic::createMaybe(), + ], - yield [ - new ConstantArrayType([new ConstantStringType('test')], [new StringType()]), - new UnionType([ - new ArrayType(new MixedType(), new MixedType()), - new HasOffsetValueType(new ConstantStringType('test'), new StringType()), - ]), - TrinaryLogic::createMaybe(), - ]; + [ + new ConstantArrayType([new ConstantStringType('test')], [new StringType()]), + new UnionType([ + new ArrayType(new MixedType(), new MixedType()), + new HasOffsetValueType(new ConstantStringType('test'), new StringType()), + ]), + TrinaryLogic::createMaybe(), + ], - yield [ - new ConstantArrayType([new ConstantStringType('test')], [new MixedType()]), - new IntersectionType([ - new UnionType([new ArrayType(new MixedType(), new MixedType()), new IterableType(new MixedType(), new MixedType())]), - new HasOffsetType(new ConstantStringType('test')), - ]), - TrinaryLogic::createMaybe(), - ]; + [ + new ConstantArrayType([new ConstantStringType('test')], [new MixedType()]), + new IntersectionType([ + new UnionType([new ArrayType(new MixedType(), new MixedType()), new IterableType(new MixedType(), new MixedType())]), + new HasOffsetType(new ConstantStringType('test')), + ]), + TrinaryLogic::createMaybe(), + ], - // The data sets below depend on the global BleedingEdgeToggle. Build them - // while the toggle is set, then restore it *before* yielding, so the global - // state is never held across a yield boundary - data providers are evaluated - // lazily and may be abandoned mid-iteration, which would otherwise leak the - // toggle into unrelated tests. - yield from BleedingEdgeToggle::withBleedingEdge(false, static fn (): array => [ [ new ConstantArrayType([], []), new ConstantArrayType([], []), @@ -642,293 +641,299 @@ public function testAccepts(Type $type, Type $otherType, TrinaryLogic $expectedR public static function dataIsSuperTypeOf(): iterable { - yield [ - new ConstantArrayType([], []), - new ConstantArrayType([], []), - TrinaryLogic::createYes(), - ]; - - yield [ - new ConstantArrayType([new ConstantIntegerType(1)], [new ConstantIntegerType(2)]), - new ConstantArrayType([new ConstantIntegerType(1)], [new ConstantIntegerType(2)]), - TrinaryLogic::createYes(), - ]; + // Build the legacy (unsealed) array shapes under an explicit toggle value. These + // data sets must not depend on the ambient global BleedingEdgeToggle: a previously + // created bleeding-edge container in the same worker may have left it set, which + // would seal these shapes at construction time and intermittently flip results. + yield from BleedingEdgeToggle::withBleedingEdge(false, static fn (): array => [ + [ + new ConstantArrayType([], []), + new ConstantArrayType([], []), + TrinaryLogic::createYes(), + ], - yield [ - new ConstantArrayType([new ConstantIntegerType(1)], [new ConstantIntegerType(2)]), - new ConstantArrayType([], []), - TrinaryLogic::createNo(), - ]; + [ + new ConstantArrayType([new ConstantIntegerType(1)], [new ConstantIntegerType(2)]), + new ConstantArrayType([new ConstantIntegerType(1)], [new ConstantIntegerType(2)]), + TrinaryLogic::createYes(), + ], - yield [ - new ConstantArrayType([], []), - new ConstantArrayType([new ConstantIntegerType(1)], [new ConstantIntegerType(2)]), - TrinaryLogic::createNo(), - ]; + [ + new ConstantArrayType([new ConstantIntegerType(1)], [new ConstantIntegerType(2)]), + new ConstantArrayType([], []), + TrinaryLogic::createNo(), + ], - yield [ - new ConstantArrayType([new ConstantIntegerType(1)], [new ConstantIntegerType(2)]), - new ConstantArrayType([new ConstantIntegerType(7)], [new ConstantIntegerType(2)]), - TrinaryLogic::createNo(), - ]; + [ + new ConstantArrayType([], []), + new ConstantArrayType([new ConstantIntegerType(1)], [new ConstantIntegerType(2)]), + TrinaryLogic::createNo(), + ], - yield [ - new ConstantArrayType([new ConstantIntegerType(1)], [new ConstantIntegerType(2)]), - new ConstantArrayType([new ConstantIntegerType(1)], [new ConstantIntegerType(7)]), - TrinaryLogic::createNo(), - ]; + [ + new ConstantArrayType([new ConstantIntegerType(1)], [new ConstantIntegerType(2)]), + new ConstantArrayType([new ConstantIntegerType(7)], [new ConstantIntegerType(2)]), + TrinaryLogic::createNo(), + ], - yield [ - new ConstantArrayType([new ConstantIntegerType(1)], [new ConstantIntegerType(2)]), - new ArrayType(new IntegerType(), new IntegerType()), - TrinaryLogic::createMaybe(), - ]; + [ + new ConstantArrayType([new ConstantIntegerType(1)], [new ConstantIntegerType(2)]), + new ConstantArrayType([new ConstantIntegerType(1)], [new ConstantIntegerType(7)]), + TrinaryLogic::createNo(), + ], - yield [ - new ConstantArrayType([new ConstantIntegerType(1)], [new ConstantIntegerType(2)]), - new ArrayType(new StringType(), new StringType()), - TrinaryLogic::createNo(), - ]; + [ + new ConstantArrayType([new ConstantIntegerType(1)], [new ConstantIntegerType(2)]), + new ArrayType(new IntegerType(), new IntegerType()), + TrinaryLogic::createMaybe(), + ], - yield [ - new ConstantArrayType([new ConstantIntegerType(1)], [new ConstantIntegerType(2)]), - new ArrayType(new MixedType(), new MixedType()), - TrinaryLogic::createMaybe(), - ]; + [ + new ConstantArrayType([new ConstantIntegerType(1)], [new ConstantIntegerType(2)]), + new ArrayType(new StringType(), new StringType()), + TrinaryLogic::createNo(), + ], - yield [ - new ConstantArrayType([], []), - new IterableType(new MixedType(false), new MixedType(true)), - TrinaryLogic::createMaybe(), - ]; + [ + new ConstantArrayType([new ConstantIntegerType(1)], [new ConstantIntegerType(2)]), + new ArrayType(new MixedType(), new MixedType()), + TrinaryLogic::createMaybe(), + ], - yield [ - new ConstantArrayType([ - new ConstantStringType('foo'), - ], [ - new IntegerType(), - ]), - new ConstantArrayType([ - new ConstantStringType('foo'), - new ConstantStringType('bar'), - ], [ - new IntegerType(), - new IntegerType(), - ]), - TrinaryLogic::createYes(), - ]; + [ + new ConstantArrayType([], []), + new IterableType(new MixedType(false), new MixedType(true)), + TrinaryLogic::createMaybe(), + ], - yield [ - new ConstantArrayType([ - new ConstantStringType('foo'), - new ConstantStringType('bar'), - ], [ - new IntegerType(), - new IntegerType(), - ]), - new ConstantArrayType([ - new ConstantStringType('foo'), - ], [ - new IntegerType(), - ]), - TrinaryLogic::createNo(), - ]; + [ + new ConstantArrayType([ + new ConstantStringType('foo'), + ], [ + new IntegerType(), + ]), + new ConstantArrayType([ + new ConstantStringType('foo'), + new ConstantStringType('bar'), + ], [ + new IntegerType(), + new IntegerType(), + ]), + TrinaryLogic::createYes(), + ], - yield [ - new ConstantArrayType([ - new ConstantStringType('foo'), - new ConstantStringType('bar'), - ], [ - new IntegerType(), - new IntegerType(), - ], [2]), - new ConstantArrayType([], []), - TrinaryLogic::createNo(), - ]; + [ + new ConstantArrayType([ + new ConstantStringType('foo'), + new ConstantStringType('bar'), + ], [ + new IntegerType(), + new IntegerType(), + ]), + new ConstantArrayType([ + new ConstantStringType('foo'), + ], [ + new IntegerType(), + ]), + TrinaryLogic::createNo(), + ], - yield [ - new ConstantArrayType([ - new ConstantStringType('foo'), - new ConstantStringType('bar'), - ], [ - new IntegerType(), - new IntegerType(), - ], [2], [0]), - new ConstantArrayType([], []), - TrinaryLogic::createNo(), - ]; + [ + new ConstantArrayType([ + new ConstantStringType('foo'), + new ConstantStringType('bar'), + ], [ + new IntegerType(), + new IntegerType(), + ], [2]), + new ConstantArrayType([], []), + TrinaryLogic::createNo(), + ], - yield [ - new ConstantArrayType([ - new ConstantStringType('foo'), - new ConstantStringType('bar'), - ], [ - new IntegerType(), - new IntegerType(), - ], [2], [0, 1]), - new ConstantArrayType([], []), - TrinaryLogic::createYes(), - ]; + [ + new ConstantArrayType([ + new ConstantStringType('foo'), + new ConstantStringType('bar'), + ], [ + new IntegerType(), + new IntegerType(), + ], [2], [0]), + new ConstantArrayType([], []), + TrinaryLogic::createNo(), + ], - yield [ - new ConstantArrayType([ - new ConstantStringType('foo'), - new ConstantStringType('bar'), - ], [ - new IntegerType(), - new IntegerType(), - ], [2], [0, 1]), - new ConstantArrayType([ - new ConstantStringType('foo'), - ], [ - new IntegerType(), - ], [1], [0]), - TrinaryLogic::createYes(), - ]; + [ + new ConstantArrayType([ + new ConstantStringType('foo'), + new ConstantStringType('bar'), + ], [ + new IntegerType(), + new IntegerType(), + ], [2], [0, 1]), + new ConstantArrayType([], []), + TrinaryLogic::createYes(), + ], - yield [ - new ConstantArrayType([ - new ConstantStringType('foo'), - ], [ - new IntegerType(), - ]), - new ConstantArrayType([ - new ConstantStringType('foo'), - new ConstantStringType('bar'), - ], [ - new IntegerType(), - new IntegerType(), - ], [2], [0, 1]), - TrinaryLogic::createMaybe(), - ]; + [ + new ConstantArrayType([ + new ConstantStringType('foo'), + new ConstantStringType('bar'), + ], [ + new IntegerType(), + new IntegerType(), + ], [2], [0, 1]), + new ConstantArrayType([ + new ConstantStringType('foo'), + ], [ + new IntegerType(), + ], [1], [0]), + TrinaryLogic::createYes(), + ], - yield [ - new ConstantArrayType([ - new ConstantStringType('foo'), - ], [ - new StringType(), - ]), - new ConstantArrayType([ - new ConstantStringType('foo'), - new ConstantStringType('bar'), - ], [ - new IntegerType(), - new IntegerType(), - ], [2], [0, 1]), - TrinaryLogic::createNo(), - ]; + [ + new ConstantArrayType([ + new ConstantStringType('foo'), + ], [ + new IntegerType(), + ]), + new ConstantArrayType([ + new ConstantStringType('foo'), + new ConstantStringType('bar'), + ], [ + new IntegerType(), + new IntegerType(), + ], [2], [0, 1]), + TrinaryLogic::createMaybe(), + ], - yield [ - new ConstantArrayType([ - new ConstantStringType('foo'), - new ConstantStringType('bar'), - ], [ - new IntegerType(), - new IntegerType(), - ], [2], [0, 1]), - new ConstantArrayType([ - new ConstantStringType('foo'), - ], [ - new StringType(), - ]), - TrinaryLogic::createNo(), - ]; + [ + new ConstantArrayType([ + new ConstantStringType('foo'), + ], [ + new StringType(), + ]), + new ConstantArrayType([ + new ConstantStringType('foo'), + new ConstantStringType('bar'), + ], [ + new IntegerType(), + new IntegerType(), + ], [2], [0, 1]), + TrinaryLogic::createNo(), + ], - yield [ - new ConstantArrayType([], []), - new ConstantArrayType([ - new ConstantStringType('foo'), - new ConstantStringType('bar'), - ], [ - new IntegerType(), - new IntegerType(), - ], [2], [0, 1]), - TrinaryLogic::createMaybe(), - ]; + [ + new ConstantArrayType([ + new ConstantStringType('foo'), + new ConstantStringType('bar'), + ], [ + new IntegerType(), + new IntegerType(), + ], [2], [0, 1]), + new ConstantArrayType([ + new ConstantStringType('foo'), + ], [ + new StringType(), + ]), + TrinaryLogic::createNo(), + ], - yield [ - new ConstantArrayType([ - new ConstantStringType('foo'), - ], [ - new IntegerType(), - ], [1], [0]), - new ConstantArrayType([ - new ConstantStringType('foo'), - ], [ - new IntegerType(), - ]), - TrinaryLogic::createYes(), - ]; + [ + new ConstantArrayType([], []), + new ConstantArrayType([ + new ConstantStringType('foo'), + new ConstantStringType('bar'), + ], [ + new IntegerType(), + new IntegerType(), + ], [2], [0, 1]), + TrinaryLogic::createMaybe(), + ], - yield [ - new ConstantArrayType([ - new ConstantStringType('foo'), - ], [ - new IntegerType(), - ]), - new ConstantArrayType([ - new ConstantStringType('foo'), - ], [ - new IntegerType(), - ], [1], [0]), - TrinaryLogic::createMaybe(), - ]; + [ + new ConstantArrayType([ + new ConstantStringType('foo'), + ], [ + new IntegerType(), + ], [1], [0]), + new ConstantArrayType([ + new ConstantStringType('foo'), + ], [ + new IntegerType(), + ]), + TrinaryLogic::createYes(), + ], - yield [ - new ConstantArrayType([ - new ConstantStringType('a'), - new ConstantStringType('b'), - ], [ - new IntegerType(), - new UnionType([new IntegerType(), new NullType()]), - ]), - new ArrayType(new StringType(), new MixedType()), - TrinaryLogic::createMaybe(), - ]; + [ + new ConstantArrayType([ + new ConstantStringType('foo'), + ], [ + new IntegerType(), + ]), + new ConstantArrayType([ + new ConstantStringType('foo'), + ], [ + new IntegerType(), + ], [1], [0]), + TrinaryLogic::createMaybe(), + ], - yield [ - new ConstantArrayType([ - new ConstantStringType('a'), - new ConstantStringType('b'), - ], [ - new IntegerType(), - new UnionType([new IntegerType(), new NullType()]), - ]), - new ArrayType(new StringType(), new StringType()), - TrinaryLogic::createNo(), - ]; + [ + new ConstantArrayType([ + new ConstantStringType('a'), + new ConstantStringType('b'), + ], [ + new IntegerType(), + new UnionType([new IntegerType(), new NullType()]), + ]), + new ArrayType(new StringType(), new MixedType()), + TrinaryLogic::createMaybe(), + ], - yield [ - new ConstantArrayType([ - new ConstantIntegerType(1), - new ConstantIntegerType(2), - ], [ - new IntegerType(), - new UnionType([new IntegerType(), new NullType()]), - ]), - new ArrayType(new StringType(), new MixedType()), - TrinaryLogic::createNo(), - ]; + [ + new ConstantArrayType([ + new ConstantStringType('a'), + new ConstantStringType('b'), + ], [ + new IntegerType(), + new UnionType([new IntegerType(), new NullType()]), + ]), + new ArrayType(new StringType(), new StringType()), + TrinaryLogic::createNo(), + ], - // empty array (with unknown sealedness) does not accept extra keys - yield [ - new ConstantArrayType([], []), - new ConstantArrayType([new ConstantStringType('a')], [new StringType()]), - TrinaryLogic::createNo(), - ]; + [ + new ConstantArrayType([ + new ConstantIntegerType(1), + new ConstantIntegerType(2), + ], [ + new IntegerType(), + new UnionType([new IntegerType(), new NullType()]), + ]), + new ArrayType(new StringType(), new MixedType()), + TrinaryLogic::createNo(), + ], - // non-empty array (with unknown sealedness) accepts extra keys - yield [ - new ConstantArrayType([new ConstantStringType('a')], [new StringType()]), - new ConstantArrayType([ - new ConstantStringType('a'), - new ConstantStringType('b'), - ], [ - new StringType(), - new IntegerType(), - ]), - TrinaryLogic::createYes(), - ]; + // empty array (with unknown sealedness) does not accept extra keys + [ + new ConstantArrayType([], []), + new ConstantArrayType([new ConstantStringType('a')], [new StringType()]), + TrinaryLogic::createNo(), + ], + + // non-empty array (with unknown sealedness) accepts extra keys + [ + new ConstantArrayType([new ConstantStringType('a')], [new StringType()]), + new ConstantArrayType([ + new ConstantStringType('a'), + new ConstantStringType('b'), + ], [ + new StringType(), + new IntegerType(), + ]), + TrinaryLogic::createYes(), + ], + ]); // definite sealedness tests (bleeding edge) From d71c56c0fd66e618bc2dd3fedb8505cafc1d7e24 Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Mon, 22 Jun 2026 05:25:40 +0000 Subject: [PATCH 09/18] Return one big array instead of yielding in ConstantArrayType data providers dataAccepts(), dataIsSuperTypeOf() and dataGetArraySize() are no longer generators: they build their toggle-dependent groups into arrays and return one merged array. A returned array runs the provider to completion synchronously, so the global BleedingEdgeToggle can never be held across a `yield` and leak into unrelated tests when the provider is abandoned early. The explicit BleedingEdgeToggle::withBleedingEdge() wraps stay: they make the contained ConstantArrayType shapes deterministic regardless of an ambient toggle leaked from outside the provider (e.g. a bleeding-edge container built by an earlier test via ContainerFactory). Avoiding `yield` only stops this provider from being a leak source; it does not protect against an inherited leaked toggle. Co-Authored-By: Claude Opus 4.8 --- .../Type/Constant/ConstantArrayTypeTest.php | 156 +++++++++++------- 1 file changed, 93 insertions(+), 63 deletions(-) diff --git a/tests/PHPStan/Type/Constant/ConstantArrayTypeTest.php b/tests/PHPStan/Type/Constant/ConstantArrayTypeTest.php index 5a796e00528..cf5c22cd712 100644 --- a/tests/PHPStan/Type/Constant/ConstantArrayTypeTest.php +++ b/tests/PHPStan/Type/Constant/ConstantArrayTypeTest.php @@ -43,11 +43,16 @@ class ConstantArrayTypeTest extends PHPStanTestCase public static function dataAccepts(): iterable { + // The provider returns one big array instead of yielding: a generator data provider + // is evaluated lazily and may be abandoned mid-iteration, which is how the global + // BleedingEdgeToggle previously leaked across a `yield`. Returning an array runs the + // whole provider to completion synchronously, so the toggle is always restored. + // // Build the legacy (unsealed) array shapes under an explicit toggle value. These // data sets must not depend on the ambient global BleedingEdgeToggle: a previously // created bleeding-edge container in the same worker may have left it set, which // would seal these shapes at construction time and intermittently flip results. - yield from BleedingEdgeToggle::withBleedingEdge(false, static fn (): array => [ + $unsealed = BleedingEdgeToggle::withBleedingEdge(false, static fn (): array => [ [ new ConstantArrayType([], []), new ConstantArrayType([], []), @@ -452,7 +457,7 @@ public static function dataAccepts(): iterable ], ]); - yield from BleedingEdgeToggle::withBleedingEdge(true, static fn (): array => [ + $sealed = BleedingEdgeToggle::withBleedingEdge(true, static fn (): array => [ // empty array (sealed) does not accept extra keys [ new ConstantArrayType([], []), @@ -617,6 +622,8 @@ public static function dataAccepts(): iterable [], ], ]); + + return [...$unsealed, ...$sealed]; } /** @@ -641,11 +648,16 @@ public function testAccepts(Type $type, Type $otherType, TrinaryLogic $expectedR public static function dataIsSuperTypeOf(): iterable { + // The provider returns one big array instead of yielding: a generator data provider + // is evaluated lazily and may be abandoned mid-iteration, which is how the global + // BleedingEdgeToggle previously leaked across a `yield`. Returning an array runs the + // whole provider to completion synchronously, so the toggle is always restored. + // // Build the legacy (unsealed) array shapes under an explicit toggle value. These // data sets must not depend on the ambient global BleedingEdgeToggle: a previously // created bleeding-edge container in the same worker may have left it set, which // would seal these shapes at construction time and intermittently flip results. - yield from BleedingEdgeToggle::withBleedingEdge(false, static fn (): array => [ + $unsealed = BleedingEdgeToggle::withBleedingEdge(false, static fn (): array => [ [ new ConstantArrayType([], []), new ConstantArrayType([], []), @@ -935,61 +947,66 @@ public static function dataIsSuperTypeOf(): iterable ], ]); - // definite sealedness tests (bleeding edge) + // definite sealedness tests (bleeding edge). These are passed as type strings and + // resolved (under an explicit toggle) inside the test method, so they carry no + // pre-constructed objects and do not depend on the ambient toggle here. + $sealed = [ + // both sealed, same keys, compatible values + ['array{a: int, b: string}', 'array{a: int, b: string}', TrinaryLogic::createYes()], - // both sealed, same keys, compatible values - yield ['array{a: int, b: string}', 'array{a: int, b: string}', TrinaryLogic::createYes()]; + // both sealed, bigger vs smaller (subset) — sealed requires exact keys + ['array{a: int, b: string}', 'array{a: int}', TrinaryLogic::createNo()], + ['array{a: int}', 'array{a: int, b: string}', TrinaryLogic::createNo()], - // both sealed, bigger vs smaller (subset) — sealed requires exact keys - yield ['array{a: int, b: string}', 'array{a: int}', TrinaryLogic::createNo()]; - yield ['array{a: int}', 'array{a: int, b: string}', TrinaryLogic::createNo()]; + // both sealed, narrower value + ['array{a: int}', 'array{a: int<0, max>}', TrinaryLogic::createYes()], + ['array{a: int<0, max>}', 'array{a: int}', TrinaryLogic::createMaybe()], - // both sealed, narrower value - yield ['array{a: int}', 'array{a: int<0, max>}', TrinaryLogic::createYes()]; - yield ['array{a: int<0, max>}', 'array{a: int}', TrinaryLogic::createMaybe()]; + // both sealed, optional key in left only + ['array{a: int, b?: string}', 'array{a: int}', TrinaryLogic::createYes()], + ['array{a: int, b?: string}', 'array{a: int, b: string}', TrinaryLogic::createYes()], - // both sealed, optional key in left only - yield ['array{a: int, b?: string}', 'array{a: int}', TrinaryLogic::createYes()]; - yield ['array{a: int, b?: string}', 'array{a: int, b: string}', TrinaryLogic::createYes()]; + // both unsealed, compatible known keys + compatible unsealed + ['array{a: int, ...}', 'array{a: int<0, max>, ...}', TrinaryLogic::createYes()], + ['array{a: int<0, max>, ...}', 'array{a: int, ...}', TrinaryLogic::createMaybe()], - // both unsealed, compatible known keys + compatible unsealed - yield ['array{a: int, ...}', 'array{a: int<0, max>, ...}', TrinaryLogic::createYes()]; - yield ['array{a: int<0, max>, ...}', 'array{a: int, ...}', TrinaryLogic::createMaybe()]; + // both unsealed, bigger known on right (right's extra fits left's unsealed extras) + ['array{a: int, ...}', 'array{a: int, b: string, ...}', TrinaryLogic::createYes()], - // both unsealed, bigger known on right (right's extra fits left's unsealed extras) - yield ['array{a: int, ...}', 'array{a: int, b: string, ...}', TrinaryLogic::createYes()]; + // both unsealed, right has known key left doesn't require; left's unsealed must cover + ['array{a: int, ...}', 'array{a: int, b: int, ...}', TrinaryLogic::createNo()], + ['array{a: int, ...}', 'array{a: int, b: non-empty-string, ...}', TrinaryLogic::createYes()], - // both unsealed, right has known key left doesn't require; left's unsealed must cover - yield ['array{a: int, ...}', 'array{a: int, b: int, ...}', TrinaryLogic::createNo()]; - yield ['array{a: int, ...}', 'array{a: int, b: non-empty-string, ...}', TrinaryLogic::createYes()]; + // both unsealed, narrower unsealed value on right + ['array{a: int, ...}', 'array{a: int, ...}', TrinaryLogic::createYes()], + ['array{a: int, ...}', 'array{a: int, ...}', TrinaryLogic::createMaybe()], - // both unsealed, narrower unsealed value on right - yield ['array{a: int, ...}', 'array{a: int, ...}', TrinaryLogic::createYes()]; - yield ['array{a: int, ...}', 'array{a: int, ...}', TrinaryLogic::createMaybe()]; + // both unsealed, narrower unsealed key on right (array-key ⊃ string) + ['array{a: int, ...}', 'array{a: int, ...}', TrinaryLogic::createYes()], + ['array{a: int, ...}', 'array{a: int, ...}', TrinaryLogic::createMaybe()], - // both unsealed, narrower unsealed key on right (array-key ⊃ string) - yield ['array{a: int, ...}', 'array{a: int, ...}', TrinaryLogic::createYes()]; - yield ['array{a: int, ...}', 'array{a: int, ...}', TrinaryLogic::createMaybe()]; + // both unsealed, incompatible unsealed key types + ['array{...}', 'array{...}', TrinaryLogic::createNo()], - // both unsealed, incompatible unsealed key types - yield ['array{...}', 'array{...}', TrinaryLogic::createNo()]; + // both unsealed, incompatible unsealed value types + ['array{...}', 'array{...}', TrinaryLogic::createNo()], - // both unsealed, incompatible unsealed value types - yield ['array{...}', 'array{...}', TrinaryLogic::createNo()]; + // unsealed vs sealed — sealed's extras must fit unsealed's unsealed + ['array{a: int, ...}', 'array{a: int, b: string}', TrinaryLogic::createYes()], + ['array{a: int, ...}', 'array{a: int, b: string}', TrinaryLogic::createNo()], - // unsealed vs sealed — sealed's extras must fit unsealed's unsealed - yield ['array{a: int, ...}', 'array{a: int, b: string}', TrinaryLogic::createYes()]; - yield ['array{a: int, ...}', 'array{a: int, b: string}', TrinaryLogic::createNo()]; + // sealed vs unsealed — unsealed might have extras sealed doesn't allow + ['array{a: int}', 'array{a: int, ...}', TrinaryLogic::createMaybe()], + ['array{a: int, b: string}', 'array{a: int<0, max>, ...}', TrinaryLogic::createMaybe()], - // sealed vs unsealed — unsealed might have extras sealed doesn't allow - yield ['array{a: int}', 'array{a: int, ...}', TrinaryLogic::createMaybe()]; - yield ['array{a: int, b: string}', 'array{a: int<0, max>, ...}', TrinaryLogic::createMaybe()]; + // sealed vs unsealed where sealed's keys can't be in unsealed's extras + ['array{a: int}', 'array{...}', TrinaryLogic::createNo()], - // sealed vs unsealed where sealed's keys can't be in unsealed's extras - yield ['array{a: int}', 'array{...}', TrinaryLogic::createNo()]; + // sealed vs unsealed where sealed fits unsealed's extras + ['array{a: int}', 'array{...}', TrinaryLogic::createMaybe()], + ]; - // sealed vs unsealed where sealed fits unsealed's extras - yield ['array{a: int}', 'array{...}', TrinaryLogic::createMaybe()]; + return [...$unsealed, ...$sealed]; } /** @@ -1614,11 +1631,14 @@ public function testSealedness(): void public static function dataGetArraySize(): iterable { + // The provider returns one big array instead of yielding, so the global + // BleedingEdgeToggle is never held across a `yield` and can never leak into + // unrelated tests when the provider is abandoned early. + $data = []; + foreach ([false, true] as $bleedingEdge) { - // Build the toggle-dependent data sets eagerly and restore the global - // BleedingEdgeToggle before yielding, so it never leaks across a yield - // boundary into unrelated tests when this provider is abandoned early. - yield from BleedingEdgeToggle::withBleedingEdge($bleedingEdge, static function (): array { + // Build the toggle-dependent data sets eagerly under an explicit toggle value. + $group = BleedingEdgeToggle::withBleedingEdge($bleedingEdge, static function (): array { $cases = []; $cases[] = [ @@ -1652,25 +1672,35 @@ public static function dataGetArraySize(): iterable return $cases; }); + + $data = [...$data, ...$group]; } - $builder = ConstantArrayTypeBuilder::createEmpty(); - $builder->makeUnsealed(new IntegerType(), new ObjectType(stdClass::class)); - yield [ - $builder->getArray(), - IntegerRangeType::createAllGreaterThanOrEqualTo(0), - ]; - $builder->setOffsetValueType(new ConstantIntegerType(0), new ObjectType(stdClass::class)); - yield [ - $builder->getArray(), - IntegerRangeType::createAllGreaterThanOrEqualTo(1), - ]; + $data = [...$data, ...BleedingEdgeToggle::withBleedingEdge(false, static function (): array { + $cases = []; - $builder->setOffsetValueType(new ConstantIntegerType(1), new ObjectType(stdClass::class), true); - yield [ - $builder->getArray(), - IntegerRangeType::createAllGreaterThanOrEqualTo(1), - ]; + $builder = ConstantArrayTypeBuilder::createEmpty(); + $builder->makeUnsealed(new IntegerType(), new ObjectType(stdClass::class)); + $cases[] = [ + $builder->getArray(), + IntegerRangeType::createAllGreaterThanOrEqualTo(0), + ]; + $builder->setOffsetValueType(new ConstantIntegerType(0), new ObjectType(stdClass::class)); + $cases[] = [ + $builder->getArray(), + IntegerRangeType::createAllGreaterThanOrEqualTo(1), + ]; + + $builder->setOffsetValueType(new ConstantIntegerType(1), new ObjectType(stdClass::class), true); + $cases[] = [ + $builder->getArray(), + IntegerRangeType::createAllGreaterThanOrEqualTo(1), + ]; + + return $cases; + })]; + + return $data; } #[DataProvider('dataGetArraySize')] From 63d1d5fe2d0f2ed423a0e141aa7f700e00871263 Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Mon, 22 Jun 2026 05:30:37 +0000 Subject: [PATCH 10/18] Add BleedingEdgeToggle::withBleedingEdge tests including generator guard Co-Authored-By: Claude Opus 4.8 --- .../BleedingEdgeToggleTest.php | 123 ++++++------------ 1 file changed, 41 insertions(+), 82 deletions(-) diff --git a/tests/PHPStan/DependencyInjection/BleedingEdgeToggleTest.php b/tests/PHPStan/DependencyInjection/BleedingEdgeToggleTest.php index 81f9485fe46..f10da9554c8 100644 --- a/tests/PHPStan/DependencyInjection/BleedingEdgeToggleTest.php +++ b/tests/PHPStan/DependencyInjection/BleedingEdgeToggleTest.php @@ -2,106 +2,65 @@ namespace PHPStan\DependencyInjection; -use Override; +use Generator; use PHPStan\ShouldNotHappenException; -use PHPUnit\Framework\TestCase; -use RuntimeException; -use Throwable; +use PHPStan\Testing\PHPStanTestCase; -final class BleedingEdgeToggleTest extends TestCase +class BleedingEdgeToggleTest extends PHPStanTestCase { - private bool $backup; - - #[Override] - protected function setUp(): void - { - $this->backup = BleedingEdgeToggle::isBleedingEdge(); - } - - #[Override] - protected function tearDown(): void + public function testWithBleedingEdgeRunsCallbackWithToggleSet(): void { - BleedingEdgeToggle::setBleedingEdge($this->backup); - } - - public function testTogglesDuringCallbackAndRestoresAfterwards(): void - { - BleedingEdgeToggle::setBleedingEdge(false); - - $observed = BleedingEdgeToggle::withBleedingEdge(true, static fn (): bool => BleedingEdgeToggle::isBleedingEdge()); - - $this->assertTrue($observed); - $this->assertFalse(BleedingEdgeToggle::isBleedingEdge()); - } - - public function testRestoresPreviousValueWhenAlreadyEnabled(): void - { - BleedingEdgeToggle::setBleedingEdge(true); + $backup = BleedingEdgeToggle::isBleedingEdge(); + try { + BleedingEdgeToggle::setBleedingEdge(false); - $observed = BleedingEdgeToggle::withBleedingEdge(false, static fn (): bool => BleedingEdgeToggle::isBleedingEdge()); + $observed = BleedingEdgeToggle::withBleedingEdge(true, static fn (): bool => BleedingEdgeToggle::isBleedingEdge()); - $this->assertFalse($observed); - $this->assertTrue(BleedingEdgeToggle::isBleedingEdge()); + $this->assertTrue($observed); + } finally { + BleedingEdgeToggle::setBleedingEdge($backup); + } } - public function testReturnsCallbackResult(): void + public function testWithBleedingEdgeRestoresPreviousValue(): void { - $result = BleedingEdgeToggle::withBleedingEdge(true, fn (): string => $this->makeValue()); - - $this->assertSame('value', $result); - } + $backup = BleedingEdgeToggle::isBleedingEdge(); + try { + BleedingEdgeToggle::setBleedingEdge(false); - public function testRestoresPreviousValueWhenCallbackThrows(): void - { - BleedingEdgeToggle::setBleedingEdge(false); + BleedingEdgeToggle::withBleedingEdge(true, static fn (): bool => true); - $thrown = false; - try { - BleedingEdgeToggle::withBleedingEdge(true, static function (): void { - throw new RuntimeException('boom'); - }); - } catch (Throwable $e) { - $thrown = $e instanceof RuntimeException && $e->getMessage() === 'boom'; + $this->assertFalse(BleedingEdgeToggle::isBleedingEdge()); + } finally { + BleedingEdgeToggle::setBleedingEdge($backup); } - - $this->assertTrue($thrown); - $this->assertFalse(BleedingEdgeToggle::isBleedingEdge()); } - public function testThrowsAndRestoresWhenCallbackYields(): void + public function testWithBleedingEdgeRejectsGeneratorCallbackAndRestoresValue(): void { - BleedingEdgeToggle::setBleedingEdge(false); - - $thrown = false; + $backup = BleedingEdgeToggle::isBleedingEdge(); try { - BleedingEdgeToggle::withBleedingEdge(true, static function () { - yield 1; - }); - } catch (ShouldNotHappenException) { - $thrown = true; + BleedingEdgeToggle::setBleedingEdge(false); + + $generatorCallback = static function (): Generator { + yield BleedingEdgeToggle::isBleedingEdge(); + }; + + try { + BleedingEdgeToggle::withBleedingEdge(true, $generatorCallback); + $this->fail('Expected ShouldNotHappenException was not thrown.'); + } catch (ShouldNotHappenException $e) { + // A generator callback would hold the toggle across a `yield` and leak it + // into unrelated tests - it must be rejected eagerly. + $this->assertStringContainsString('not allowed to yield', $e->getMessage()); + } + + // The toggle must be restored even when the callback is rejected. + $this->assertFalse(BleedingEdgeToggle::isBleedingEdge()); + } finally { + BleedingEdgeToggle::setBleedingEdge($backup); } - - $this->assertTrue($thrown); - $this->assertFalse(BleedingEdgeToggle::isBleedingEdge()); - } - - public function testProducesDataSetsWhileToggleIsSet(): void - { - BleedingEdgeToggle::setBleedingEdge(false); - - $dataSets = BleedingEdgeToggle::withBleedingEdge(true, static fn (): array => [ - BleedingEdgeToggle::isBleedingEdge(), - BleedingEdgeToggle::isBleedingEdge(), - ]); - - $this->assertSame([true, true], $dataSets); - $this->assertFalse(BleedingEdgeToggle::isBleedingEdge()); - } - - private function makeValue(): string - { - return 'value'; } } From d345593a39de8f5c6c4d966ce396126fa0a48289 Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Mon, 22 Jun 2026 05:41:27 +0000 Subject: [PATCH 11/18] Split ConstantArrayTypeTest providers into with/without bleeding edge Separate the bleeding-edge true/false data sets of dataAccepts, dataIsSuperTypeOf and dataGetArraySize into two data providers each, attached to their test via stacked #[DataProvider] attributes. Co-Authored-By: Claude Opus 4.8 --- .../Type/Constant/ConstantArrayTypeTest.php | 145 +++++++++++------- 1 file changed, 88 insertions(+), 57 deletions(-) diff --git a/tests/PHPStan/Type/Constant/ConstantArrayTypeTest.php b/tests/PHPStan/Type/Constant/ConstantArrayTypeTest.php index cf5c22cd712..9cc609b5c7c 100644 --- a/tests/PHPStan/Type/Constant/ConstantArrayTypeTest.php +++ b/tests/PHPStan/Type/Constant/ConstantArrayTypeTest.php @@ -41,7 +41,7 @@ class ConstantArrayTypeTest extends PHPStanTestCase { - public static function dataAccepts(): iterable + public static function dataAcceptsWithoutBleedingEdge(): array { // The provider returns one big array instead of yielding: a generator data provider // is evaluated lazily and may be abandoned mid-iteration, which is how the global @@ -52,7 +52,7 @@ public static function dataAccepts(): iterable // data sets must not depend on the ambient global BleedingEdgeToggle: a previously // created bleeding-edge container in the same worker may have left it set, which // would seal these shapes at construction time and intermittently flip results. - $unsealed = BleedingEdgeToggle::withBleedingEdge(false, static fn (): array => [ + return BleedingEdgeToggle::withBleedingEdge(false, static fn (): array => [ [ new ConstantArrayType([], []), new ConstantArrayType([], []), @@ -456,8 +456,14 @@ public static function dataAccepts(): iterable [], ], ]); + } - $sealed = BleedingEdgeToggle::withBleedingEdge(true, static fn (): array => [ + public static function dataAcceptsWithBleedingEdge(): array + { + // Build the sealed array shapes under the bleeding-edge toggle. As above, the data + // sets are produced eagerly inside the callback so the toggle is restored before + // returning and never leaks across a `yield`. + return BleedingEdgeToggle::withBleedingEdge(true, static fn (): array => [ // empty array (sealed) does not accept extra keys [ new ConstantArrayType([], []), @@ -622,14 +628,13 @@ public static function dataAccepts(): iterable [], ], ]); - - return [...$unsealed, ...$sealed]; } /** * @param array|null $reasons */ - #[DataProvider('dataAccepts')] + #[DataProvider('dataAcceptsWithoutBleedingEdge')] + #[DataProvider('dataAcceptsWithBleedingEdge')] public function testAccepts(Type $type, Type $otherType, TrinaryLogic $expectedResult, ?array $reasons = null): void { $actualResult = $type->accepts($otherType, true); @@ -646,7 +651,7 @@ public function testAccepts(Type $type, Type $otherType, TrinaryLogic $expectedR $this->assertSame($reasons, $actualResult->reasons, $testDescription); } - public static function dataIsSuperTypeOf(): iterable + public static function dataIsSuperTypeOfWithoutBleedingEdge(): array { // The provider returns one big array instead of yielding: a generator data provider // is evaluated lazily and may be abandoned mid-iteration, which is how the global @@ -657,7 +662,7 @@ public static function dataIsSuperTypeOf(): iterable // data sets must not depend on the ambient global BleedingEdgeToggle: a previously // created bleeding-edge container in the same worker may have left it set, which // would seal these shapes at construction time and intermittently flip results. - $unsealed = BleedingEdgeToggle::withBleedingEdge(false, static fn (): array => [ + return BleedingEdgeToggle::withBleedingEdge(false, static fn (): array => [ [ new ConstantArrayType([], []), new ConstantArrayType([], []), @@ -946,11 +951,14 @@ public static function dataIsSuperTypeOf(): iterable TrinaryLogic::createYes(), ], ]); + } + public static function dataIsSuperTypeOfWithBleedingEdge(): array + { // definite sealedness tests (bleeding edge). These are passed as type strings and // resolved (under an explicit toggle) inside the test method, so they carry no // pre-constructed objects and do not depend on the ambient toggle here. - $sealed = [ + return [ // both sealed, same keys, compatible values ['array{a: int, b: string}', 'array{a: int, b: string}', TrinaryLogic::createYes()], @@ -1005,15 +1013,14 @@ public static function dataIsSuperTypeOf(): iterable // sealed vs unsealed where sealed fits unsealed's extras ['array{a: int}', 'array{...}', TrinaryLogic::createMaybe()], ]; - - return [...$unsealed, ...$sealed]; } /** * @param ConstantArrayType|string $type * @param Type|string $otherType */ - #[DataProvider('dataIsSuperTypeOf')] + #[DataProvider('dataIsSuperTypeOfWithoutBleedingEdge')] + #[DataProvider('dataIsSuperTypeOfWithBleedingEdge')] public function testIsSuperTypeOf($type, $otherType, TrinaryLogic $expectedResult): void { [$type, $otherType] = BleedingEdgeToggle::withBleedingEdge(true, static function () use ($type, $otherType): array { @@ -1629,55 +1636,42 @@ public function testSealedness(): void } } - public static function dataGetArraySize(): iterable + public static function dataGetArraySizeWithoutBleedingEdge(): array { // The provider returns one big array instead of yielding, so the global // BleedingEdgeToggle is never held across a `yield` and can never leak into // unrelated tests when the provider is abandoned early. - $data = []; + return BleedingEdgeToggle::withBleedingEdge(false, static function (): array { + $cases = []; - foreach ([false, true] as $bleedingEdge) { - // Build the toggle-dependent data sets eagerly under an explicit toggle value. - $group = BleedingEdgeToggle::withBleedingEdge($bleedingEdge, static function (): array { - $cases = []; + $cases[] = [ + new ConstantArrayType([], []), + new ConstantIntegerType(0), + ]; - $cases[] = [ - new ConstantArrayType([], []), - new ConstantIntegerType(0), - ]; - - $builder = ConstantArrayTypeBuilder::createEmpty(); - $cases[] = [ - $builder->getArray(), - new ConstantIntegerType(0), - ]; - - $builder->makeUnsealed(new IntegerType(), new ObjectType(stdClass::class)); - $cases[] = [ - $builder->getArray(), - IntegerRangeType::createAllGreaterThanOrEqualTo(0), - ]; - - $builder->setOffsetValueType(new ConstantIntegerType(0), new ObjectType(stdClass::class)); - $cases[] = [ - $builder->getArray(), - IntegerRangeType::createAllGreaterThanOrEqualTo(1), - ]; - - $builder->setOffsetValueType(new ConstantIntegerType(1), new ObjectType(stdClass::class), true); - $cases[] = [ - $builder->getArray(), - IntegerRangeType::createAllGreaterThanOrEqualTo(1), - ]; - - return $cases; - }); - - $data = [...$data, ...$group]; - } + $builder = ConstantArrayTypeBuilder::createEmpty(); + $cases[] = [ + $builder->getArray(), + new ConstantIntegerType(0), + ]; - $data = [...$data, ...BleedingEdgeToggle::withBleedingEdge(false, static function (): array { - $cases = []; + $builder->makeUnsealed(new IntegerType(), new ObjectType(stdClass::class)); + $cases[] = [ + $builder->getArray(), + IntegerRangeType::createAllGreaterThanOrEqualTo(0), + ]; + + $builder->setOffsetValueType(new ConstantIntegerType(0), new ObjectType(stdClass::class)); + $cases[] = [ + $builder->getArray(), + IntegerRangeType::createAllGreaterThanOrEqualTo(1), + ]; + + $builder->setOffsetValueType(new ConstantIntegerType(1), new ObjectType(stdClass::class), true); + $cases[] = [ + $builder->getArray(), + IntegerRangeType::createAllGreaterThanOrEqualTo(1), + ]; $builder = ConstantArrayTypeBuilder::createEmpty(); $builder->makeUnsealed(new IntegerType(), new ObjectType(stdClass::class)); @@ -1698,12 +1692,49 @@ public static function dataGetArraySize(): iterable ]; return $cases; - })]; + }); + } + + public static function dataGetArraySizeWithBleedingEdge(): array + { + return BleedingEdgeToggle::withBleedingEdge(true, static function (): array { + $cases = []; + + $cases[] = [ + new ConstantArrayType([], []), + new ConstantIntegerType(0), + ]; + + $builder = ConstantArrayTypeBuilder::createEmpty(); + $cases[] = [ + $builder->getArray(), + new ConstantIntegerType(0), + ]; + + $builder->makeUnsealed(new IntegerType(), new ObjectType(stdClass::class)); + $cases[] = [ + $builder->getArray(), + IntegerRangeType::createAllGreaterThanOrEqualTo(0), + ]; + + $builder->setOffsetValueType(new ConstantIntegerType(0), new ObjectType(stdClass::class)); + $cases[] = [ + $builder->getArray(), + IntegerRangeType::createAllGreaterThanOrEqualTo(1), + ]; - return $data; + $builder->setOffsetValueType(new ConstantIntegerType(1), new ObjectType(stdClass::class), true); + $cases[] = [ + $builder->getArray(), + IntegerRangeType::createAllGreaterThanOrEqualTo(1), + ]; + + return $cases; + }); } - #[DataProvider('dataGetArraySize')] + #[DataProvider('dataGetArraySizeWithoutBleedingEdge')] + #[DataProvider('dataGetArraySizeWithBleedingEdge')] public function testGetArraySize(Type $constantArray, Type $expectedSize): void { $this->assertSame($expectedSize->describe(VerbosityLevel::precise()), $constantArray->getArraySize()->describe(VerbosityLevel::precise())); From a85e5ed56a2591167c01c04f398aca6deb6ab24c Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Mon, 22 Jun 2026 10:00:01 +0200 Subject: [PATCH 12/18] Delete tests/PHPStan/DependencyInjection/BleedingEdgeToggleTest.php --- .../BleedingEdgeToggleTest.php | 66 ------------------- 1 file changed, 66 deletions(-) delete mode 100644 tests/PHPStan/DependencyInjection/BleedingEdgeToggleTest.php diff --git a/tests/PHPStan/DependencyInjection/BleedingEdgeToggleTest.php b/tests/PHPStan/DependencyInjection/BleedingEdgeToggleTest.php deleted file mode 100644 index f10da9554c8..00000000000 --- a/tests/PHPStan/DependencyInjection/BleedingEdgeToggleTest.php +++ /dev/null @@ -1,66 +0,0 @@ - BleedingEdgeToggle::isBleedingEdge()); - - $this->assertTrue($observed); - } finally { - BleedingEdgeToggle::setBleedingEdge($backup); - } - } - - public function testWithBleedingEdgeRestoresPreviousValue(): void - { - $backup = BleedingEdgeToggle::isBleedingEdge(); - try { - BleedingEdgeToggle::setBleedingEdge(false); - - BleedingEdgeToggle::withBleedingEdge(true, static fn (): bool => true); - - $this->assertFalse(BleedingEdgeToggle::isBleedingEdge()); - } finally { - BleedingEdgeToggle::setBleedingEdge($backup); - } - } - - public function testWithBleedingEdgeRejectsGeneratorCallbackAndRestoresValue(): void - { - $backup = BleedingEdgeToggle::isBleedingEdge(); - try { - BleedingEdgeToggle::setBleedingEdge(false); - - $generatorCallback = static function (): Generator { - yield BleedingEdgeToggle::isBleedingEdge(); - }; - - try { - BleedingEdgeToggle::withBleedingEdge(true, $generatorCallback); - $this->fail('Expected ShouldNotHappenException was not thrown.'); - } catch (ShouldNotHappenException $e) { - // A generator callback would hold the toggle across a `yield` and leak it - // into unrelated tests - it must be rejected eagerly. - $this->assertStringContainsString('not allowed to yield', $e->getMessage()); - } - - // The toggle must be restored even when the callback is rejected. - $this->assertFalse(BleedingEdgeToggle::isBleedingEdge()); - } finally { - BleedingEdgeToggle::setBleedingEdge($backup); - } - } - -} From 9879299cefc9bec2563ffe1d18e0f734a46819fd Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Mon, 22 Jun 2026 10:31:53 +0200 Subject: [PATCH 13/18] Create BleedingEdgeToggleTest.php --- .../BleedingEdgeToggleTest.php | 107 ++++++++++++++++++ 1 file changed, 107 insertions(+) create mode 100644 tests/PHPStan/DependencyInjection/BleedingEdgeToggleTest.php diff --git a/tests/PHPStan/DependencyInjection/BleedingEdgeToggleTest.php b/tests/PHPStan/DependencyInjection/BleedingEdgeToggleTest.php new file mode 100644 index 00000000000..81f9485fe46 --- /dev/null +++ b/tests/PHPStan/DependencyInjection/BleedingEdgeToggleTest.php @@ -0,0 +1,107 @@ +backup = BleedingEdgeToggle::isBleedingEdge(); + } + + #[Override] + protected function tearDown(): void + { + BleedingEdgeToggle::setBleedingEdge($this->backup); + } + + public function testTogglesDuringCallbackAndRestoresAfterwards(): void + { + BleedingEdgeToggle::setBleedingEdge(false); + + $observed = BleedingEdgeToggle::withBleedingEdge(true, static fn (): bool => BleedingEdgeToggle::isBleedingEdge()); + + $this->assertTrue($observed); + $this->assertFalse(BleedingEdgeToggle::isBleedingEdge()); + } + + public function testRestoresPreviousValueWhenAlreadyEnabled(): void + { + BleedingEdgeToggle::setBleedingEdge(true); + + $observed = BleedingEdgeToggle::withBleedingEdge(false, static fn (): bool => BleedingEdgeToggle::isBleedingEdge()); + + $this->assertFalse($observed); + $this->assertTrue(BleedingEdgeToggle::isBleedingEdge()); + } + + public function testReturnsCallbackResult(): void + { + $result = BleedingEdgeToggle::withBleedingEdge(true, fn (): string => $this->makeValue()); + + $this->assertSame('value', $result); + } + + public function testRestoresPreviousValueWhenCallbackThrows(): void + { + BleedingEdgeToggle::setBleedingEdge(false); + + $thrown = false; + try { + BleedingEdgeToggle::withBleedingEdge(true, static function (): void { + throw new RuntimeException('boom'); + }); + } catch (Throwable $e) { + $thrown = $e instanceof RuntimeException && $e->getMessage() === 'boom'; + } + + $this->assertTrue($thrown); + $this->assertFalse(BleedingEdgeToggle::isBleedingEdge()); + } + + public function testThrowsAndRestoresWhenCallbackYields(): void + { + BleedingEdgeToggle::setBleedingEdge(false); + + $thrown = false; + try { + BleedingEdgeToggle::withBleedingEdge(true, static function () { + yield 1; + }); + } catch (ShouldNotHappenException) { + $thrown = true; + } + + $this->assertTrue($thrown); + $this->assertFalse(BleedingEdgeToggle::isBleedingEdge()); + } + + public function testProducesDataSetsWhileToggleIsSet(): void + { + BleedingEdgeToggle::setBleedingEdge(false); + + $dataSets = BleedingEdgeToggle::withBleedingEdge(true, static fn (): array => [ + BleedingEdgeToggle::isBleedingEdge(), + BleedingEdgeToggle::isBleedingEdge(), + ]); + + $this->assertSame([true, true], $dataSets); + $this->assertFalse(BleedingEdgeToggle::isBleedingEdge()); + } + + private function makeValue(): string + { + return 'value'; + } + +} From d38282674bdac36182edff5cd07b7579b12f107e Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Mon, 22 Jun 2026 10:32:43 +0200 Subject: [PATCH 14/18] Update ConstantArrayTypeBuilderTest.php --- .../Type/Constant/ConstantArrayTypeBuilderTest.php | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/tests/PHPStan/Type/Constant/ConstantArrayTypeBuilderTest.php b/tests/PHPStan/Type/Constant/ConstantArrayTypeBuilderTest.php index 7a0d0178696..277ca18ef40 100644 --- a/tests/PHPStan/Type/Constant/ConstantArrayTypeBuilderTest.php +++ b/tests/PHPStan/Type/Constant/ConstantArrayTypeBuilderTest.php @@ -10,6 +10,7 @@ use PHPStan\Type\IntegerType; use PHPStan\Type\NullType; use PHPStan\Type\StringType; +use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; use PHPStan\Type\VerbosityLevel; use function sprintf; @@ -325,12 +326,9 @@ public function testGetArrayEmptyWithUnknownSealednessStaysConstantArrayType(): public function testGetArraySealedEmptyStaysConstantArrayType(): void { - BleedingEdgeToggle::withBleedingEdge(true, function (): void { - $builder = ConstantArrayTypeBuilder::createEmpty(); - $array = $builder->getArray(); - $this->assertInstanceOf(ConstantArrayType::class, $array); - $this->assertSame('array{}', $array->describe(VerbosityLevel::precise())); - }); + $array = BleedingEdgeToggle::withBleedingEdge(true, static fn (): Type => ConstantArrayTypeBuilder::createEmpty()->getArray()); + $this->assertInstanceOf(ConstantArrayType::class, $array); + $this->assertSame('array{}', $array->describe(VerbosityLevel::precise())); } public function testGetArrayEmptyWithRealUnsealedCollapsesToArrayType(): void From 1c8642a20eab1e59253dc29f6b9bf0ee2c34103c Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Mon, 22 Jun 2026 08:45:33 +0000 Subject: [PATCH 15/18] Keep dataGetArraySize trailing cases under bleeding edge toggle The split into with/without providers moved the trailing data sets into the without-bleeding-edge provider, but they were originally built with the toggle left on `true` by the preceding loop. Keep them in the with-bleeding-edge provider so the test data is unchanged. Co-Authored-By: Claude Opus 4.8 --- .../Type/Constant/ConstantArrayTypeTest.php | 36 +++++++++---------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/tests/PHPStan/Type/Constant/ConstantArrayTypeTest.php b/tests/PHPStan/Type/Constant/ConstantArrayTypeTest.php index 9cc609b5c7c..27b67244239 100644 --- a/tests/PHPStan/Type/Constant/ConstantArrayTypeTest.php +++ b/tests/PHPStan/Type/Constant/ConstantArrayTypeTest.php @@ -1673,24 +1673,6 @@ public static function dataGetArraySizeWithoutBleedingEdge(): array IntegerRangeType::createAllGreaterThanOrEqualTo(1), ]; - $builder = ConstantArrayTypeBuilder::createEmpty(); - $builder->makeUnsealed(new IntegerType(), new ObjectType(stdClass::class)); - $cases[] = [ - $builder->getArray(), - IntegerRangeType::createAllGreaterThanOrEqualTo(0), - ]; - $builder->setOffsetValueType(new ConstantIntegerType(0), new ObjectType(stdClass::class)); - $cases[] = [ - $builder->getArray(), - IntegerRangeType::createAllGreaterThanOrEqualTo(1), - ]; - - $builder->setOffsetValueType(new ConstantIntegerType(1), new ObjectType(stdClass::class), true); - $cases[] = [ - $builder->getArray(), - IntegerRangeType::createAllGreaterThanOrEqualTo(1), - ]; - return $cases; }); } @@ -1729,6 +1711,24 @@ public static function dataGetArraySizeWithBleedingEdge(): array IntegerRangeType::createAllGreaterThanOrEqualTo(1), ]; + $builder = ConstantArrayTypeBuilder::createEmpty(); + $builder->makeUnsealed(new IntegerType(), new ObjectType(stdClass::class)); + $cases[] = [ + $builder->getArray(), + IntegerRangeType::createAllGreaterThanOrEqualTo(0), + ]; + $builder->setOffsetValueType(new ConstantIntegerType(0), new ObjectType(stdClass::class)); + $cases[] = [ + $builder->getArray(), + IntegerRangeType::createAllGreaterThanOrEqualTo(1), + ]; + + $builder->setOffsetValueType(new ConstantIntegerType(1), new ObjectType(stdClass::class), true); + $cases[] = [ + $builder->getArray(), + IntegerRangeType::createAllGreaterThanOrEqualTo(1), + ]; + return $cases; }); } From 466b2b68d8f9fbf4beddc03d66bce7d9af7cc788 Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Mon, 22 Jun 2026 09:04:54 +0000 Subject: [PATCH 16/18] Re-add foreach loop in dataGetArraySize to minimize diff Revert the split of dataGetArraySize into two providers. Keep the single provider with its foreach ([false, true]) loop, only converting `yield` to array accumulation + `return` so the toggle is never held across a `yield`. Co-Authored-By: Claude Opus 4.8 --- .../Type/Constant/ConstantArrayTypeTest.php | 92 ++++++------------- 1 file changed, 29 insertions(+), 63 deletions(-) diff --git a/tests/PHPStan/Type/Constant/ConstantArrayTypeTest.php b/tests/PHPStan/Type/Constant/ConstantArrayTypeTest.php index 27b67244239..9b14d65e5e8 100644 --- a/tests/PHPStan/Type/Constant/ConstantArrayTypeTest.php +++ b/tests/PHPStan/Type/Constant/ConstantArrayTypeTest.php @@ -1636,51 +1636,16 @@ public function testSealedness(): void } } - public static function dataGetArraySizeWithoutBleedingEdge(): array + public static function dataGetArraySize(): iterable { - // The provider returns one big array instead of yielding, so the global - // BleedingEdgeToggle is never held across a `yield` and can never leak into - // unrelated tests when the provider is abandoned early. - return BleedingEdgeToggle::withBleedingEdge(false, static function (): array { - $cases = []; - - $cases[] = [ - new ConstantArrayType([], []), - new ConstantIntegerType(0), - ]; - - $builder = ConstantArrayTypeBuilder::createEmpty(); - $cases[] = [ - $builder->getArray(), - new ConstantIntegerType(0), - ]; - - $builder->makeUnsealed(new IntegerType(), new ObjectType(stdClass::class)); - $cases[] = [ - $builder->getArray(), - IntegerRangeType::createAllGreaterThanOrEqualTo(0), - ]; - - $builder->setOffsetValueType(new ConstantIntegerType(0), new ObjectType(stdClass::class)); - $cases[] = [ - $builder->getArray(), - IntegerRangeType::createAllGreaterThanOrEqualTo(1), - ]; - - $builder->setOffsetValueType(new ConstantIntegerType(1), new ObjectType(stdClass::class), true); - $cases[] = [ - $builder->getArray(), - IntegerRangeType::createAllGreaterThanOrEqualTo(1), - ]; - - return $cases; - }); - } + // The provider accumulates into one array and returns it instead of yielding, so the + // global BleedingEdgeToggle is never held across a `yield` and can never leak into + // unrelated tests when the provider is abandoned mid-iteration. + $bleedingEdgeBackup = BleedingEdgeToggle::isBleedingEdge(); - public static function dataGetArraySizeWithBleedingEdge(): array - { - return BleedingEdgeToggle::withBleedingEdge(true, static function (): array { - $cases = []; + $cases = []; + foreach ([false, true] as $bleedingEdge) { + BleedingEdgeToggle::setBleedingEdge($bleedingEdge); $cases[] = [ new ConstantArrayType([], []), @@ -1710,31 +1675,32 @@ public static function dataGetArraySizeWithBleedingEdge(): array $builder->getArray(), IntegerRangeType::createAllGreaterThanOrEqualTo(1), ]; + } - $builder = ConstantArrayTypeBuilder::createEmpty(); - $builder->makeUnsealed(new IntegerType(), new ObjectType(stdClass::class)); - $cases[] = [ - $builder->getArray(), - IntegerRangeType::createAllGreaterThanOrEqualTo(0), - ]; - $builder->setOffsetValueType(new ConstantIntegerType(0), new ObjectType(stdClass::class)); - $cases[] = [ - $builder->getArray(), - IntegerRangeType::createAllGreaterThanOrEqualTo(1), - ]; + $builder = ConstantArrayTypeBuilder::createEmpty(); + $builder->makeUnsealed(new IntegerType(), new ObjectType(stdClass::class)); + $cases[] = [ + $builder->getArray(), + IntegerRangeType::createAllGreaterThanOrEqualTo(0), + ]; + $builder->setOffsetValueType(new ConstantIntegerType(0), new ObjectType(stdClass::class)); + $cases[] = [ + $builder->getArray(), + IntegerRangeType::createAllGreaterThanOrEqualTo(1), + ]; - $builder->setOffsetValueType(new ConstantIntegerType(1), new ObjectType(stdClass::class), true); - $cases[] = [ - $builder->getArray(), - IntegerRangeType::createAllGreaterThanOrEqualTo(1), - ]; + $builder->setOffsetValueType(new ConstantIntegerType(1), new ObjectType(stdClass::class), true); + $cases[] = [ + $builder->getArray(), + IntegerRangeType::createAllGreaterThanOrEqualTo(1), + ]; - return $cases; - }); + BleedingEdgeToggle::setBleedingEdge($bleedingEdgeBackup); + + return $cases; } - #[DataProvider('dataGetArraySizeWithoutBleedingEdge')] - #[DataProvider('dataGetArraySizeWithBleedingEdge')] + #[DataProvider('dataGetArraySize')] public function testGetArraySize(Type $constantArray, Type $expectedSize): void { $this->assertSame($expectedSize->describe(VerbosityLevel::precise()), $constantArray->getArraySize()->describe(VerbosityLevel::precise())); From 5af91c09f40c206c6e14cc31b7a2dd76ad8b0e76 Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Mon, 22 Jun 2026 10:02:26 +0000 Subject: [PATCH 17/18] Shorten BleedingEdgeToggle data-provider comments Co-Authored-By: Claude Opus 4.8 --- .../Type/Constant/ConstantArrayTypeTest.php | 36 +++++-------------- 1 file changed, 9 insertions(+), 27 deletions(-) diff --git a/tests/PHPStan/Type/Constant/ConstantArrayTypeTest.php b/tests/PHPStan/Type/Constant/ConstantArrayTypeTest.php index 9b14d65e5e8..90a96413427 100644 --- a/tests/PHPStan/Type/Constant/ConstantArrayTypeTest.php +++ b/tests/PHPStan/Type/Constant/ConstantArrayTypeTest.php @@ -43,15 +43,8 @@ class ConstantArrayTypeTest extends PHPStanTestCase public static function dataAcceptsWithoutBleedingEdge(): array { - // The provider returns one big array instead of yielding: a generator data provider - // is evaluated lazily and may be abandoned mid-iteration, which is how the global - // BleedingEdgeToggle previously leaked across a `yield`. Returning an array runs the - // whole provider to completion synchronously, so the toggle is always restored. - // - // Build the legacy (unsealed) array shapes under an explicit toggle value. These - // data sets must not depend on the ambient global BleedingEdgeToggle: a previously - // created bleeding-edge container in the same worker may have left it set, which - // would seal these shapes at construction time and intermittently flip results. + // Build the unsealed shapes under an explicit toggle and return one array (no `yield`), + // so the toggle is never held across a suspension point and cannot leak into other tests. return BleedingEdgeToggle::withBleedingEdge(false, static fn (): array => [ [ new ConstantArrayType([], []), @@ -460,9 +453,7 @@ public static function dataAcceptsWithoutBleedingEdge(): array public static function dataAcceptsWithBleedingEdge(): array { - // Build the sealed array shapes under the bleeding-edge toggle. As above, the data - // sets are produced eagerly inside the callback so the toggle is restored before - // returning and never leaks across a `yield`. + // Build the sealed shapes under the bleeding-edge toggle, same no-`yield` rationale as above. return BleedingEdgeToggle::withBleedingEdge(true, static fn (): array => [ // empty array (sealed) does not accept extra keys [ @@ -653,15 +644,8 @@ public function testAccepts(Type $type, Type $otherType, TrinaryLogic $expectedR public static function dataIsSuperTypeOfWithoutBleedingEdge(): array { - // The provider returns one big array instead of yielding: a generator data provider - // is evaluated lazily and may be abandoned mid-iteration, which is how the global - // BleedingEdgeToggle previously leaked across a `yield`. Returning an array runs the - // whole provider to completion synchronously, so the toggle is always restored. - // - // Build the legacy (unsealed) array shapes under an explicit toggle value. These - // data sets must not depend on the ambient global BleedingEdgeToggle: a previously - // created bleeding-edge container in the same worker may have left it set, which - // would seal these shapes at construction time and intermittently flip results. + // Build the unsealed shapes under an explicit toggle and return one array (no `yield`), + // so the toggle is never held across a suspension point and cannot leak into other tests. return BleedingEdgeToggle::withBleedingEdge(false, static fn (): array => [ [ new ConstantArrayType([], []), @@ -955,9 +939,8 @@ public static function dataIsSuperTypeOfWithoutBleedingEdge(): array public static function dataIsSuperTypeOfWithBleedingEdge(): array { - // definite sealedness tests (bleeding edge). These are passed as type strings and - // resolved (under an explicit toggle) inside the test method, so they carry no - // pre-constructed objects and do not depend on the ambient toggle here. + // definite sealedness tests (bleeding edge), passed as type strings resolved under an + // explicit toggle in the test method, so they carry no toggle-dependent objects here. return [ // both sealed, same keys, compatible values ['array{a: int, b: string}', 'array{a: int, b: string}', TrinaryLogic::createYes()], @@ -1638,9 +1621,8 @@ public function testSealedness(): void public static function dataGetArraySize(): iterable { - // The provider accumulates into one array and returns it instead of yielding, so the - // global BleedingEdgeToggle is never held across a `yield` and can never leak into - // unrelated tests when the provider is abandoned mid-iteration. + // Accumulate into one array and return it (no `yield`), so the toggle is never held + // across a suspension point and cannot leak into other tests. $bleedingEdgeBackup = BleedingEdgeToggle::isBleedingEdge(); $cases = []; From 5de1f40562d0143b536ad51da45651a2a3439a2c Mon Sep 17 00:00:00 2001 From: phpstan-bot Date: Mon, 22 Jun 2026 11:06:51 +0000 Subject: [PATCH 18/18] Build container before entering bleeding-edge window in testIsSuperTypeOf ContainerFactory::create() resets the global BleedingEdgeToggle to the container's config value. When self::getContainer() built the default (non-bleeding-edge) container for the first time inside the withBleedingEdge(true) window, it clobbered the toggle back to false, so the type strings resolved to legacy (non-sealed) ConstantArrayType shapes and isSuperTypeOf intermittently returned 'Yes' instead of 'Maybe'. Fetch the resolver - forcing the container build - before the toggle window, matching the safe pattern already used in TypeCombinatorTest. Co-Authored-By: Claude Opus 4.8 --- tests/PHPStan/Type/Constant/ConstantArrayTypeTest.php | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/tests/PHPStan/Type/Constant/ConstantArrayTypeTest.php b/tests/PHPStan/Type/Constant/ConstantArrayTypeTest.php index 90a96413427..c0ecc6e78d4 100644 --- a/tests/PHPStan/Type/Constant/ConstantArrayTypeTest.php +++ b/tests/PHPStan/Type/Constant/ConstantArrayTypeTest.php @@ -1006,8 +1006,13 @@ public static function dataIsSuperTypeOfWithBleedingEdge(): array #[DataProvider('dataIsSuperTypeOfWithBleedingEdge')] public function testIsSuperTypeOf($type, $otherType, TrinaryLogic $expectedResult): void { - [$type, $otherType] = BleedingEdgeToggle::withBleedingEdge(true, static function () use ($type, $otherType): array { - $resolver = self::getContainer()->getByType(TypeStringResolver::class); + // Fetch the resolver - and thereby build the container - *before* entering the + // bleeding-edge window. Building a container resets the global BleedingEdgeToggle to + // the container's config value (ContainerFactory), which would otherwise clobber the + // toggle set below and make the type strings resolve to legacy (non-sealed) shapes. + $resolver = self::getContainer()->getByType(TypeStringResolver::class); + + [$type, $otherType] = BleedingEdgeToggle::withBleedingEdge(true, static function () use ($resolver, $type, $otherType): array { if (is_string($type)) { $type = $resolver->resolve($type, null); }