From d5baafcfe8cf0c2534fc5beda09835cd6b8e9969 Mon Sep 17 00:00:00 2001 From: staabm <120441+staabm@users.noreply.github.com> Date: Mon, 22 Jun 2026 07:02:00 +0000 Subject: [PATCH 1/2] Add `BleedingEdgeToggle::withBleedingEdge()` and use it instead of manual set/restore in tests - Add `BleedingEdgeToggle::withBleedingEdge(bool, callable)` that sets the toggle for the duration of the callback and restores the previous value in a `finally`, so the global toggle is never observably mutated outside the call. - Reject callbacks that return a `Generator` with a `ShouldNotHappenException`: holding the toggle across a `yield` would leak it into unrelated tests, so data providers must build their data sets inside the callback. - Replace the manual `isBleedingEdge()` backup + `setBleedingEdge()` restore (including `try`/`finally` blocks) in `TypeCombinatorTest`, `ConstantArrayTypeTest` and `ConstantArrayTypeBuilderTest` with `withBleedingEdge()`. - Restructure the `dataAccepts` and `dataGetArraySize` data providers so the yielded data sets are constructed inside `withBleedingEdge()` callbacks and returned, then `yield from`-ed, instead of holding the toggle across `yield`. - Add `BleedingEdgeToggleTest` covering toggling, restoration (including on exception), return value passing, the no-yield guard, and data-set production. --- .../BleedingEdgeToggle.php | 31 ++ .../BleedingEdgeToggleTest.php | 107 ++++ .../Constant/ConstantArrayTypeBuilderTest.php | 14 +- .../Type/Constant/ConstantArrayTypeTest.php | 508 +++++++++--------- tests/PHPStan/Type/TypeCombinatorTest.php | 72 +-- 5 files changed, 425 insertions(+), 307 deletions(-) create mode 100644 tests/PHPStan/DependencyInjection/BleedingEdgeToggleTest.php diff --git a/src/DependencyInjection/BleedingEdgeToggle.php b/src/DependencyInjection/BleedingEdgeToggle.php index 29170f7fe9a..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 { @@ -17,4 +20,32 @@ public static function setBleedingEdge(bool $bleedingEdge): void self::$bleedingEdge = $bleedingEdge; } + /** + * Runs the callback with the toggle set to $bleedingEdge and restores the previous + * value before returning, so the global toggle is never observable as mutated outside + * this call. When used from a data provider, the data sets must be produced by the + * callback so that the contained objects are constructed while the toggle is set - + * holding the toggle across a `yield` would otherwise leak it into unrelated tests. + * + * @template T + * @param callable(): T $callback + * @return T + */ + 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; + } finally { + self::$bleedingEdge = $backup; + } + } + } 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'; + } + +} diff --git a/tests/PHPStan/Type/Constant/ConstantArrayTypeBuilderTest.php b/tests/PHPStan/Type/Constant/ConstantArrayTypeBuilderTest.php index bd97c440358..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,16 +326,9 @@ public function testGetArrayEmptyWithUnknownSealednessStaysConstantArrayType(): public function testGetArraySealedEmptyStaysConstantArrayType(): void { - $bleedingEdgeBackup = BleedingEdgeToggle::isBleedingEdge(); - BleedingEdgeToggle::setBleedingEdge(true); - try { - $builder = ConstantArrayTypeBuilder::createEmpty(); - $array = $builder->getArray(); - $this->assertInstanceOf(ConstantArrayType::class, $array); - $this->assertSame('array{}', $array->describe(VerbosityLevel::precise())); - } finally { - BleedingEdgeToggle::setBleedingEdge($bleedingEdgeBackup); - } + $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 diff --git a/tests/PHPStan/Type/Constant/ConstantArrayTypeTest.php b/tests/PHPStan/Type/Constant/ConstantArrayTypeTest.php index ced494973f0..e8584339749 100644 --- a/tests/PHPStan/Type/Constant/ConstantArrayTypeTest.php +++ b/tests/PHPStan/Type/Constant/ConstantArrayTypeTest.php @@ -418,204 +418,201 @@ 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(), - [], - ]; + yield from BleedingEdgeToggle::withBleedingEdge(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 BleedingEdgeToggle::withBleedingEdge(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(), - [], - ]; - - BleedingEdgeToggle::setBleedingEdge($bleedingEdgeBackup); + new UnionType([ + new ConstantArrayType([new ConstantStringType('a')], [new StringType()]), + new StringType(), + ]), + TrinaryLogic::createMaybe(), + [], + ], + ]); } /** @@ -992,19 +989,17 @@ public static function dataIsSuperTypeOf(): iterable #[DataProvider('dataIsSuperTypeOf')] public function testIsSuperTypeOf($type, $otherType, TrinaryLogic $expectedResult): void { - $bleedingEdgeBackup = BleedingEdgeToggle::isBleedingEdge(); - BleedingEdgeToggle::setBleedingEdge(true); - try { - $resolver = self::getContainer()->getByType(TypeStringResolver::class); + $resolver = self::getContainer()->getByType(TypeStringResolver::class); + [$type, $otherType] = BleedingEdgeToggle::withBleedingEdge(true, static function () use ($type, $otherType, $resolver): array { if (is_string($type)) { $type = $resolver->resolve($type, null); } if (is_string($otherType)) { $otherType = $resolver->resolve($otherType, null); } - } finally { - BleedingEdgeToggle::setBleedingEdge($bleedingEdgeBackup); - } + + return [$type, $otherType]; + }); $actualResult = $type->isSuperTypeOf($otherType); $this->assertSame( @@ -1551,118 +1546,109 @@ public function testHasOffsetValueType( public function testEqualsTreatsLegacyNullAndSealedMarkerAsEqual(): void { - $bleedingEdgeBackup = BleedingEdgeToggle::isBleedingEdge(); - - try { - // Pre-bleeding-edge construction leaves the unsealed slot null - // (`isUnsealed()` answers `Maybe`). - BleedingEdgeToggle::setBleedingEdge(false); - $legacyNull = new ConstantArrayType([new ConstantStringType('a')], [new IntegerType()]); - - // Bleeding-edge construction seeds the `[NeverType, NeverType]` - // sealed marker (`isUnsealed()` answers `No`). - BleedingEdgeToggle::setBleedingEdge(true); - $sealedMarker = new ConstantArrayType([new ConstantStringType('a')], [new IntegerType()]); - - // Both represent the same sealed shape, so they must compare - // equal in both directions — this mismatch is what made the - // `TypeToPhpDocNode` round-trip fail under old PHPUnit (data - // providers run before the container enables bleeding edge). - $this->assertTrue($legacyNull->equals($sealedMarker), 'legacy-null should equal sealed-marker'); - $this->assertTrue($sealedMarker->equals($legacyNull), 'sealed-marker should equal legacy-null'); - } finally { - BleedingEdgeToggle::setBleedingEdge($bleedingEdgeBackup); - } + // Pre-bleeding-edge construction leaves the unsealed slot null + // (`isUnsealed()` answers `Maybe`). + $legacyNull = BleedingEdgeToggle::withBleedingEdge(false, static fn (): ConstantArrayType => new ConstantArrayType([new ConstantStringType('a')], [new IntegerType()])); + + // Bleeding-edge construction seeds the `[NeverType, NeverType]` + // sealed marker (`isUnsealed()` answers `No`). + $sealedMarker = BleedingEdgeToggle::withBleedingEdge(true, static fn (): ConstantArrayType => new ConstantArrayType([new ConstantStringType('a')], [new IntegerType()])); + + // Both represent the same sealed shape, so they must compare + // equal in both directions — this mismatch is what made the + // `TypeToPhpDocNode` round-trip fail under old PHPUnit (data + // providers run before the container enables bleeding edge). + $this->assertTrue($legacyNull->equals($sealedMarker), 'legacy-null should equal sealed-marker'); + $this->assertTrue($sealedMarker->equals($legacyNull), 'sealed-marker should equal legacy-null'); } public function testSealedness(): void { - $bleedingEdgeBackup = BleedingEdgeToggle::isBleedingEdge(); - - BleedingEdgeToggle::setBleedingEdge(false); + $array = BleedingEdgeToggle::withBleedingEdge(false, static fn (): Type => ConstantArrayTypeBuilder::createEmpty()->getArray()); + $this->assertInstanceOf(ConstantArrayType::class, $array); + $this->assertSame(TrinaryLogic::createMaybe()->describe(), $array->isSealed()->describe()); + $this->assertSame(TrinaryLogic::createMaybe()->describe(), $array->isUnsealed()->describe()); - try { - $builder = ConstantArrayTypeBuilder::createEmpty(); - $array = $builder->getArray(); - $this->assertInstanceOf(ConstantArrayType::class, $array); - $this->assertSame(TrinaryLogic::createMaybe()->describe(), $array->isSealed()->describe()); - $this->assertSame(TrinaryLogic::createMaybe()->describe(), $array->isUnsealed()->describe()); - - BleedingEdgeToggle::setBleedingEdge(true); - $builder = ConstantArrayTypeBuilder::createEmpty(); - $array = $builder->getArray(); - $this->assertInstanceOf(ConstantArrayType::class, $array); - $this->assertSame(TrinaryLogic::createYes()->describe(), $array->isSealed()->describe()); - $this->assertSame(TrinaryLogic::createNo()->describe(), $array->isUnsealed()->describe()); + [$array, $unsealedArray] = BleedingEdgeToggle::withBleedingEdge(true, static function (): array { + $array = ConstantArrayTypeBuilder::createEmpty()->getArray(); $builder = ConstantArrayTypeBuilder::createEmpty(); $builder->makeUnsealed(new IntegerType(), new StringType()); - $array = $builder->getArray(); - // No known keys + real unsealed extras now collapses to a general ArrayType - // (see ConstantArrayTypeBuilder::getArray). - $this->assertInstanceOf(ArrayType::class, $array); - $this->assertSame('array', $array->describe(VerbosityLevel::precise())); - } finally { - BleedingEdgeToggle::setBleedingEdge($bleedingEdgeBackup); - } + + return [$array, $builder->getArray()]; + }); + $this->assertInstanceOf(ConstantArrayType::class, $array); + $this->assertSame(TrinaryLogic::createYes()->describe(), $array->isSealed()->describe()); + $this->assertSame(TrinaryLogic::createNo()->describe(), $array->isUnsealed()->describe()); + + // No known keys + real unsealed extras now collapses to a general ArrayType + // (see ConstantArrayTypeBuilder::getArray). + $this->assertInstanceOf(ArrayType::class, $unsealedArray); + $this->assertSame('array', $unsealedArray->describe(VerbosityLevel::precise())); } public static function dataGetArraySize(): iterable { - $bleedingEdgeBackup = BleedingEdgeToggle::isBleedingEdge(); - foreach ([false, true] as $bleedingEdge) { - BleedingEdgeToggle::setBleedingEdge($bleedingEdge); + yield from BleedingEdgeToggle::withBleedingEdge($bleedingEdge, static function (): array { + $data = []; + + $data[] = [ + new ConstantArrayType([], []), + new ConstantIntegerType(0), + ]; + + $builder = ConstantArrayTypeBuilder::createEmpty(); + $data[] = [ + $builder->getArray(), + new ConstantIntegerType(0), + ]; + + $builder->makeUnsealed(new IntegerType(), new ObjectType(stdClass::class)); + $data[] = [ + $builder->getArray(), + IntegerRangeType::createAllGreaterThanOrEqualTo(0), + ]; + + $builder->setOffsetValueType(new ConstantIntegerType(0), new ObjectType(stdClass::class)); + $data[] = [ + $builder->getArray(), + IntegerRangeType::createAllGreaterThanOrEqualTo(1), + ]; + + $builder->setOffsetValueType(new ConstantIntegerType(1), new ObjectType(stdClass::class), true); + $data[] = [ + $builder->getArray(), + IntegerRangeType::createAllGreaterThanOrEqualTo(1), + ]; + + return $data; + }); + } - yield [ - new ConstantArrayType([], []), - new ConstantIntegerType(0), - ]; + yield from BleedingEdgeToggle::withBleedingEdge(true, static function (): array { + $data = []; $builder = ConstantArrayTypeBuilder::createEmpty(); - yield [ - $builder->getArray(), - new ConstantIntegerType(0), - ]; - $builder->makeUnsealed(new IntegerType(), new ObjectType(stdClass::class)); - yield [ + $data[] = [ $builder->getArray(), IntegerRangeType::createAllGreaterThanOrEqualTo(0), ]; - $builder->setOffsetValueType(new ConstantIntegerType(0), new ObjectType(stdClass::class)); - yield [ + $data[] = [ $builder->getArray(), IntegerRangeType::createAllGreaterThanOrEqualTo(1), ]; $builder->setOffsetValueType(new ConstantIntegerType(1), new ObjectType(stdClass::class), true); - yield [ + $data[] = [ $builder->getArray(), IntegerRangeType::createAllGreaterThanOrEqualTo(1), ]; - } - $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), - ]; - - $builder->setOffsetValueType(new ConstantIntegerType(1), new ObjectType(stdClass::class), true); - yield [ - $builder->getArray(), - IntegerRangeType::createAllGreaterThanOrEqualTo(1), - ]; - - BleedingEdgeToggle::setBleedingEdge($bleedingEdgeBackup); + return $data; + }); } #[DataProvider('dataGetArraySize')] diff --git a/tests/PHPStan/Type/TypeCombinatorTest.php b/tests/PHPStan/Type/TypeCombinatorTest.php index c35d916315e..996beeeed75 100644 --- a/tests/PHPStan/Type/TypeCombinatorTest.php +++ b/tests/PHPStan/Type/TypeCombinatorTest.php @@ -3226,18 +3226,18 @@ public function testUnion( string $expectedTypeDescription, ): void { - $bleedingEdgeBackup = BleedingEdgeToggle::isBleedingEdge(); $typeStringResolver = self::getContainer()->getByType(TypeStringResolver::class); - foreach ($types as $i => $type) { - BleedingEdgeToggle::setBleedingEdge(true); - if (!is_string($type)) { - continue; - } + $types = BleedingEdgeToggle::withBleedingEdge(true, static function () use ($types, $typeStringResolver): array { + foreach ($types as $i => $type) { + if (!is_string($type)) { + continue; + } - $types[$i] = $typeStringResolver->resolve($type, null); - } + $types[$i] = $typeStringResolver->resolve($type, null); + } - BleedingEdgeToggle::setBleedingEdge($bleedingEdgeBackup); + return $types; + }); $actualType = TypeCombinator::union(...$types); $this->assertSame( $expectedTypeDescription, @@ -3276,18 +3276,18 @@ public function testUnionInversed( ): void { $types = array_reverse($types); - $bleedingEdgeBackup = BleedingEdgeToggle::isBleedingEdge(); $typeStringResolver = self::getContainer()->getByType(TypeStringResolver::class); - foreach ($types as $i => $type) { - BleedingEdgeToggle::setBleedingEdge(true); - if (!is_string($type)) { - continue; - } + $types = BleedingEdgeToggle::withBleedingEdge(true, static function () use ($types, $typeStringResolver): array { + foreach ($types as $i => $type) { + if (!is_string($type)) { + continue; + } - $types[$i] = $typeStringResolver->resolve($type, null); - } + $types[$i] = $typeStringResolver->resolve($type, null); + } - BleedingEdgeToggle::setBleedingEdge($bleedingEdgeBackup); + return $types; + }); $actualType = TypeCombinator::union(...$types); $this->assertSame( @@ -5640,18 +5640,18 @@ public function testIntersect( string $expectedTypeDescription, ): void { - $bleedingEdgeBackup = BleedingEdgeToggle::isBleedingEdge(); $typeStringResolver = self::getContainer()->getByType(TypeStringResolver::class); - foreach ($types as $i => $type) { - BleedingEdgeToggle::setBleedingEdge(true); - if (!is_string($type)) { - continue; - } + $types = BleedingEdgeToggle::withBleedingEdge(true, static function () use ($types, $typeStringResolver): array { + foreach ($types as $i => $type) { + if (!is_string($type)) { + continue; + } - $types[$i] = $typeStringResolver->resolve($type, null); - } + $types[$i] = $typeStringResolver->resolve($type, null); + } - BleedingEdgeToggle::setBleedingEdge($bleedingEdgeBackup); + return $types; + }); $actualType = TypeCombinator::intersect(...$types); $this->assertSame( @@ -5676,18 +5676,18 @@ public function testIntersectInversed( string $expectedTypeDescription, ): void { - $bleedingEdgeBackup = BleedingEdgeToggle::isBleedingEdge(); $typeStringResolver = self::getContainer()->getByType(TypeStringResolver::class); - foreach ($types as $i => $type) { - BleedingEdgeToggle::setBleedingEdge(true); - if (!is_string($type)) { - continue; - } + $types = BleedingEdgeToggle::withBleedingEdge(true, static function () use ($types, $typeStringResolver): array { + foreach ($types as $i => $type) { + if (!is_string($type)) { + continue; + } - $types[$i] = $typeStringResolver->resolve($type, null); - } + $types[$i] = $typeStringResolver->resolve($type, null); + } - BleedingEdgeToggle::setBleedingEdge($bleedingEdgeBackup); + return $types; + }); $actualType = TypeCombinator::intersect(...array_reverse($types)); $this->assertSame( From ee112912d180c55389dd18bed3f4ea5fd8ab96ae Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Mon, 22 Jun 2026 09:14:26 +0200 Subject: [PATCH 2/2] restore --- .../Type/Constant/ConstantArrayTypeTest.php | 508 +++++++++--------- 1 file changed, 261 insertions(+), 247 deletions(-) diff --git a/tests/PHPStan/Type/Constant/ConstantArrayTypeTest.php b/tests/PHPStan/Type/Constant/ConstantArrayTypeTest.php index e8584339749..ced494973f0 100644 --- a/tests/PHPStan/Type/Constant/ConstantArrayTypeTest.php +++ b/tests/PHPStan/Type/Constant/ConstantArrayTypeTest.php @@ -418,201 +418,204 @@ public static function dataAccepts(): iterable TrinaryLogic::createMaybe(), ]; - yield from BleedingEdgeToggle::withBleedingEdge(false, static fn (): array => [ - [ - new ConstantArrayType([], []), - new ConstantArrayType([], []), - TrinaryLogic::createYes(), - ], + $bleedingEdgeBackup = BleedingEdgeToggle::isBleedingEdge(); + BleedingEdgeToggle::setBleedingEdge(false); - // empty array (with unknown sealedness) does not accept extra keys - [ - new ConstantArrayType([], []), - new ConstantArrayType([new ConstantStringType('a')], [new StringType()]), - TrinaryLogic::createNo(), - [], - ], + yield [ + new ConstantArrayType([], []), + new ConstantArrayType([], []), + TrinaryLogic::createYes(), + ]; - // 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 (with unknown sealedness) does not accept extra keys + yield [ + new ConstantArrayType([], []), + new ConstantArrayType([new ConstantStringType('a')], [new StringType()]), + TrinaryLogic::createNo(), + [], + ]; - yield from BleedingEdgeToggle::withBleedingEdge(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 (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(), + [], + ]; - // 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\'.'], - ], + BleedingEdgeToggle::setBleedingEdge(true); - // 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.'], - ], + // empty array (sealed) does not accept extra keys + yield [ + new ConstantArrayType([], []), + new ConstantArrayType([new ConstantStringType('a')], [new StringType()]), + TrinaryLogic::createNo(), + [], + ]; - // 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.'], - ], + // 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\'.'], + ]; - // 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(), - [], - ], + // 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.'], + ]; - // 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(), - [], - ], + // 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.'], + ]; - // 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 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 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(), - [], - ], + // 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(), + [], + ]; - [ - 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(), - [], - ], + // 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(), + [], + ]; - [ - 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\'.', - ], - ], + // 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(), + [], + ]; + 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(), + [], + ]; + + 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(), [ - 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 key type int does not accept extra key type \'b\'.', ], + ]; - // unsealed array must check the other array unsealed types + 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(), [ - 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 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(), + [], + ]; + 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(), [ - 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.', - ], + '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(), [ - 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.', - ], + '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 UnionType([ - new ConstantArrayType([new ConstantStringType('a')], [new StringType()]), - new StringType(), - ]), - TrinaryLogic::createMaybe(), - [], - ], - ]); + new StringType(), + ]), + TrinaryLogic::createMaybe(), + [], + ]; + + BleedingEdgeToggle::setBleedingEdge($bleedingEdgeBackup); } /** @@ -989,17 +992,19 @@ public static function dataIsSuperTypeOf(): iterable #[DataProvider('dataIsSuperTypeOf')] public function testIsSuperTypeOf($type, $otherType, TrinaryLogic $expectedResult): void { - $resolver = self::getContainer()->getByType(TypeStringResolver::class); - [$type, $otherType] = BleedingEdgeToggle::withBleedingEdge(true, static function () use ($type, $otherType, $resolver): array { + $bleedingEdgeBackup = BleedingEdgeToggle::isBleedingEdge(); + BleedingEdgeToggle::setBleedingEdge(true); + try { + $resolver = self::getContainer()->getByType(TypeStringResolver::class); if (is_string($type)) { $type = $resolver->resolve($type, null); } if (is_string($otherType)) { $otherType = $resolver->resolve($otherType, null); } - - return [$type, $otherType]; - }); + } finally { + BleedingEdgeToggle::setBleedingEdge($bleedingEdgeBackup); + } $actualResult = $type->isSuperTypeOf($otherType); $this->assertSame( @@ -1546,109 +1551,118 @@ public function testHasOffsetValueType( public function testEqualsTreatsLegacyNullAndSealedMarkerAsEqual(): void { - // Pre-bleeding-edge construction leaves the unsealed slot null - // (`isUnsealed()` answers `Maybe`). - $legacyNull = BleedingEdgeToggle::withBleedingEdge(false, static fn (): ConstantArrayType => new ConstantArrayType([new ConstantStringType('a')], [new IntegerType()])); - - // Bleeding-edge construction seeds the `[NeverType, NeverType]` - // sealed marker (`isUnsealed()` answers `No`). - $sealedMarker = BleedingEdgeToggle::withBleedingEdge(true, static fn (): ConstantArrayType => new ConstantArrayType([new ConstantStringType('a')], [new IntegerType()])); - - // Both represent the same sealed shape, so they must compare - // equal in both directions — this mismatch is what made the - // `TypeToPhpDocNode` round-trip fail under old PHPUnit (data - // providers run before the container enables bleeding edge). - $this->assertTrue($legacyNull->equals($sealedMarker), 'legacy-null should equal sealed-marker'); - $this->assertTrue($sealedMarker->equals($legacyNull), 'sealed-marker should equal legacy-null'); + $bleedingEdgeBackup = BleedingEdgeToggle::isBleedingEdge(); + + try { + // Pre-bleeding-edge construction leaves the unsealed slot null + // (`isUnsealed()` answers `Maybe`). + BleedingEdgeToggle::setBleedingEdge(false); + $legacyNull = new ConstantArrayType([new ConstantStringType('a')], [new IntegerType()]); + + // Bleeding-edge construction seeds the `[NeverType, NeverType]` + // sealed marker (`isUnsealed()` answers `No`). + BleedingEdgeToggle::setBleedingEdge(true); + $sealedMarker = new ConstantArrayType([new ConstantStringType('a')], [new IntegerType()]); + + // Both represent the same sealed shape, so they must compare + // equal in both directions — this mismatch is what made the + // `TypeToPhpDocNode` round-trip fail under old PHPUnit (data + // providers run before the container enables bleeding edge). + $this->assertTrue($legacyNull->equals($sealedMarker), 'legacy-null should equal sealed-marker'); + $this->assertTrue($sealedMarker->equals($legacyNull), 'sealed-marker should equal legacy-null'); + } finally { + BleedingEdgeToggle::setBleedingEdge($bleedingEdgeBackup); + } } public function testSealedness(): void { - $array = BleedingEdgeToggle::withBleedingEdge(false, static fn (): Type => ConstantArrayTypeBuilder::createEmpty()->getArray()); - $this->assertInstanceOf(ConstantArrayType::class, $array); - $this->assertSame(TrinaryLogic::createMaybe()->describe(), $array->isSealed()->describe()); - $this->assertSame(TrinaryLogic::createMaybe()->describe(), $array->isUnsealed()->describe()); + $bleedingEdgeBackup = BleedingEdgeToggle::isBleedingEdge(); - [$array, $unsealedArray] = BleedingEdgeToggle::withBleedingEdge(true, static function (): array { - $array = ConstantArrayTypeBuilder::createEmpty()->getArray(); + BleedingEdgeToggle::setBleedingEdge(false); + try { $builder = ConstantArrayTypeBuilder::createEmpty(); - $builder->makeUnsealed(new IntegerType(), new StringType()); + $array = $builder->getArray(); + $this->assertInstanceOf(ConstantArrayType::class, $array); + $this->assertSame(TrinaryLogic::createMaybe()->describe(), $array->isSealed()->describe()); + $this->assertSame(TrinaryLogic::createMaybe()->describe(), $array->isUnsealed()->describe()); - return [$array, $builder->getArray()]; - }); - $this->assertInstanceOf(ConstantArrayType::class, $array); - $this->assertSame(TrinaryLogic::createYes()->describe(), $array->isSealed()->describe()); - $this->assertSame(TrinaryLogic::createNo()->describe(), $array->isUnsealed()->describe()); - - // No known keys + real unsealed extras now collapses to a general ArrayType - // (see ConstantArrayTypeBuilder::getArray). - $this->assertInstanceOf(ArrayType::class, $unsealedArray); - $this->assertSame('array', $unsealedArray->describe(VerbosityLevel::precise())); + BleedingEdgeToggle::setBleedingEdge(true); + $builder = ConstantArrayTypeBuilder::createEmpty(); + $array = $builder->getArray(); + $this->assertInstanceOf(ConstantArrayType::class, $array); + $this->assertSame(TrinaryLogic::createYes()->describe(), $array->isSealed()->describe()); + $this->assertSame(TrinaryLogic::createNo()->describe(), $array->isUnsealed()->describe()); + + $builder = ConstantArrayTypeBuilder::createEmpty(); + $builder->makeUnsealed(new IntegerType(), new StringType()); + $array = $builder->getArray(); + // No known keys + real unsealed extras now collapses to a general ArrayType + // (see ConstantArrayTypeBuilder::getArray). + $this->assertInstanceOf(ArrayType::class, $array); + $this->assertSame('array', $array->describe(VerbosityLevel::precise())); + } finally { + BleedingEdgeToggle::setBleedingEdge($bleedingEdgeBackup); + } } public static function dataGetArraySize(): iterable { + $bleedingEdgeBackup = BleedingEdgeToggle::isBleedingEdge(); + foreach ([false, true] as $bleedingEdge) { - yield from BleedingEdgeToggle::withBleedingEdge($bleedingEdge, static function (): array { - $data = []; - - $data[] = [ - new ConstantArrayType([], []), - new ConstantIntegerType(0), - ]; - - $builder = ConstantArrayTypeBuilder::createEmpty(); - $data[] = [ - $builder->getArray(), - new ConstantIntegerType(0), - ]; - - $builder->makeUnsealed(new IntegerType(), new ObjectType(stdClass::class)); - $data[] = [ - $builder->getArray(), - IntegerRangeType::createAllGreaterThanOrEqualTo(0), - ]; - - $builder->setOffsetValueType(new ConstantIntegerType(0), new ObjectType(stdClass::class)); - $data[] = [ - $builder->getArray(), - IntegerRangeType::createAllGreaterThanOrEqualTo(1), - ]; - - $builder->setOffsetValueType(new ConstantIntegerType(1), new ObjectType(stdClass::class), true); - $data[] = [ - $builder->getArray(), - IntegerRangeType::createAllGreaterThanOrEqualTo(1), - ]; - - return $data; - }); - } + BleedingEdgeToggle::setBleedingEdge($bleedingEdge); - yield from BleedingEdgeToggle::withBleedingEdge(true, static function (): array { - $data = []; + yield [ + new ConstantArrayType([], []), + new ConstantIntegerType(0), + ]; $builder = ConstantArrayTypeBuilder::createEmpty(); + yield [ + $builder->getArray(), + new ConstantIntegerType(0), + ]; + $builder->makeUnsealed(new IntegerType(), new ObjectType(stdClass::class)); - $data[] = [ + yield [ $builder->getArray(), IntegerRangeType::createAllGreaterThanOrEqualTo(0), ]; + $builder->setOffsetValueType(new ConstantIntegerType(0), new ObjectType(stdClass::class)); - $data[] = [ + yield [ $builder->getArray(), IntegerRangeType::createAllGreaterThanOrEqualTo(1), ]; $builder->setOffsetValueType(new ConstantIntegerType(1), new ObjectType(stdClass::class), true); - $data[] = [ + yield [ $builder->getArray(), IntegerRangeType::createAllGreaterThanOrEqualTo(1), ]; + } - return $data; - }); + $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), + ]; + + $builder->setOffsetValueType(new ConstantIntegerType(1), new ObjectType(stdClass::class), true); + yield [ + $builder->getArray(), + IntegerRangeType::createAllGreaterThanOrEqualTo(1), + ]; + + BleedingEdgeToggle::setBleedingEdge($bleedingEdgeBackup); } #[DataProvider('dataGetArraySize')]