Skip to content
Open
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
2 changes: 1 addition & 1 deletion src/Turbo/TurboExtensionEnabler.php
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
final class TurboExtensionEnabler
{

public const EXPECTED_EXTENSION_VERSION = '28c29b8';
public const EXPECTED_EXTENSION_VERSION = '19e694d';

private static bool $active = false;

Expand Down
4 changes: 4 additions & 0 deletions src/Type/ObjectType.php
Original file line number Diff line number Diff line change
Expand Up @@ -2094,6 +2094,10 @@ public function tryRemove(Type $typeToRemove): ?Type
// time, without rebuilding the subtracted union once per member.
$membersToRemove = [];
foreach ($typeToRemove->getTypes() as $member) {
if ($member->isSuperTypeOf($this)->yes()) {
return new NeverType();
}

$isSuperTypeOfMember = $this->isSuperTypeOf($member);
if ($isSuperTypeOfMember->yes()) {
$membersToRemove[] = $member;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,4 +83,15 @@ public function testConditionalThrows(): void
]);
}

public function testBug15392(): void
{
require_once __DIR__ . '/data/bug-15392.php';
$this->analyse([__DIR__ . '/data/bug-15392.php'], [
[
'Function Bug15392\\multiCatchLeaksOtherSubtype() throws checked exception Bug15392\\SubB but it\'s missing from the PHPDoc @throws tag.',
84,
],
]);
}

}
87 changes: 87 additions & 0 deletions tests/PHPStan/Rules/Exceptions/data/bug-15392.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
<?php

namespace Bug15392;

/** @phpstan-sealed SubA|SubB */
abstract class SealedException extends \RuntimeException
{

}

final class SubA extends SealedException
{

}

final class SubB extends SealedException
{

}

final class OtherException extends \RuntimeException
{

}

/** @phpstan-sealed SubC */
class ConcreteSealedException extends \RuntimeException
{

}

final class SubC extends ConcreteSealedException
{

}

/** @throws SealedException */
function throwsSealed(): void
{
throw new SubA();
}

/** @throws ConcreteSealedException */
function throwsConcreteSealed(): void
{
throw new SubC();
}

function singleCatch(): void
{
try {
throwsSealed();
} catch (SealedException $e) {
}
}

function multiCatch(): void
{
try {
throwsSealed();
} catch (SealedException | OtherException $e) {
}
}

function multiCatchReversed(): void
{
try {
throwsSealed();
} catch (OtherException | SealedException $e) {
}
}

function multiCatchConcrete(): void
{
try {
throwsConcreteSealed();
} catch (ConcreteSealedException | OtherException $e) {
}
}

function multiCatchLeaksOtherSubtype(): void
{
try {
throwsSealed();
} catch (SubA | OtherException $e) {
}
}
27 changes: 27 additions & 0 deletions tests/PHPStan/Type/TypeCombinatorTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -6530,6 +6530,33 @@ public static function dataRemove(): array
IntersectionType::class,
'non-empty-array&hasOffsetValue(\'a\', string)',
],
[
new ObjectType('Bug15392Types\\SealedBase'),
new UnionType([
new ObjectType('Bug15392Types\\SealedBase'),
new ObjectType('Bug15392Types\\Other'),
]),
NeverType::class,
'*NEVER*=implicit',
],
[
new ObjectType('Bug15392Types\\SealedBase'),
new UnionType([
new ObjectType('Bug15392Types\\SubA'),
new ObjectType('Bug15392Types\\Other'),
]),
ObjectType::class,
'Bug15392Types\\SubB',
],
[
new ObjectType('PHPStan\\Fixture\\TestEnum'),
new UnionType([
new ObjectType('PHPStan\\Fixture\\TestEnum'),
new ObjectType('stdClass'),
]),
NeverType::class,
'*NEVER*=implicit',
],
];
}

Expand Down
24 changes: 24 additions & 0 deletions tests/PHPStan/Type/data/bug-15392.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?php

namespace Bug15392Types;

/** @phpstan-sealed SubA|SubB */
abstract class SealedBase
{

}

final class SubA extends SealedBase
{

}

final class SubB extends SealedBase
{

}

final class Other
{

}
5 changes: 5 additions & 0 deletions turbo-ext/src/ObjectType.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2785,6 +2785,11 @@ class ObjectType
zend_type_error("phpstan_turbo: a union member must be %s", ptcls::type);
return zv::Val();
}
zval selfZv;
ZVAL_OBJ(&selfZv, self);
zend_long memberIsSuperType = isSuperTypeOfTrinary(member, &selfZv);
if (UNEXPECTED(memberIsSuperType < 0)) return zv::Val();
if (memberIsSuperType == PT_TRI_YES) return pt_type_new_never_type();
zv::Val isSuperTypeOfMember = thisCall(PT_LC("issupertypeof"), otIsSuperTypeOf, 1, member, [&]() { return isSuperTypeOf(member); });
if (UNEXPECTED(isSuperTypeOfMember.isUndef())) return zv::Val();
zend_long memberValue = pt_type_result_trinary(isSuperTypeOfMember.raw());
Expand Down
13 changes: 13 additions & 0 deletions turbo-ext/tests/type-family.php
Original file line number Diff line number Diff line change
Expand Up @@ -2492,6 +2492,19 @@ public function canAccessConstant(\PHPStan\Reflection\ClassConstantReflection $c
$r["enum minus cases $caseName"] = $view($remaining);
}
$r['equal union classes'] = [$view($subjects['throwable']->tryRemove(new $objectClass(\Error::class))), $view($subjects['dateTimeInterface']->tryRemove(new $objectClass(\DateTime::class))), $view($subjects['dateTimeInterface']->tryRemove(new $objectClass(\DateTimeImmutable::class)))];
// a sealed type minus a union: a member that is a supertype of the whole
// type removes everything, whatever the other members are
$sealedUnion = static fn (\PHPStan\Type\Type ...$members): \PHPStan\Type\Type => new \PHPStan\Type\UnionType($members);
$r['sealed tryRemove union'] = [
$view($subjects['enum']->tryRemove($sealedUnion(new $objectClass($objectEnum), $others['stdClass']))),
$view($subjects['enum']->tryRemove($sealedUnion($others['stdClass'], new $objectClass($objectEnum)))),
$view($subjects['enum']->tryRemove($sealedUnion(new $objectClass(\UnitEnum::class), $others['exception']))),
$view($subjects['enum']->tryRemove($sealedUnion($others['caseFoo'], new $objectClass(\UnitEnum::class)))),
$view($subjects['enum']->tryRemove($sealedUnion($others['caseFoo'], $others['stdClass']))),
$view($subjects['enumMinusFoo']->tryRemove($sealedUnion(new $objectClass($objectEnum), $others['stdClass']))),
$view($subjects['enumWithReflection']->tryRemove($sealedUnion(new $objectClass($objectEnum), $others['stdClass']))),
$view(\PHPStan\Type\TypeCombinator::remove($subjects['enum'], $sealedUnion(new $objectClass($objectEnum), $others['stdClass']))),
];
$r['object equals object'] = [(new $objectClass(\stdClass::class))->equals(new $objectClass(\stdClass::class)), (new $objectClass(\stdClass::class))->equals(new $objectClass('stdclass')), (new $objectClass(\stdClass::class))->equals(new $genericClass(\stdClass::class, [])), (new $genericClass(\stdClass::class, []))->equals(new $objectClass(\stdClass::class)), (new $caseClass($objectEnum, 'FOO'))->equals(new $caseClass($objectEnum, 'FOO')), (new $caseClass($objectEnum, 'FOO'))->equals(new $objectClass($objectEnum))];
// the constructors by named arguments
$r['object named'] = $view(new $objectClass(className: \stdClass::class, classReflection: null));
Expand Down
Loading