Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions src/DependencyInjection/BleedingEdgeToggle.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

namespace PHPStan\DependencyInjection;

use Generator;
use PHPStan\ShouldNotHappenException;

final class BleedingEdgeToggle
{

Expand All @@ -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;
}
}

}
107 changes: 107 additions & 0 deletions tests/PHPStan/DependencyInjection/BleedingEdgeToggleTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
<?php declare(strict_types = 1);

namespace PHPStan\DependencyInjection;

use Override;
use PHPStan\ShouldNotHappenException;
use PHPUnit\Framework\TestCase;
use RuntimeException;
use Throwable;

final class BleedingEdgeToggleTest extends TestCase
{

private bool $backup;

#[Override]
protected function setUp(): void
{
$this->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';
}

}
14 changes: 4 additions & 10 deletions tests/PHPStan/Type/Constant/ConstantArrayTypeBuilderTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand Down
72 changes: 36 additions & 36 deletions tests/PHPStan/Type/TypeCombinatorTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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(
Expand All @@ -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(
Expand Down
Loading