Skip to content

Commit

Permalink
Merge pull request #9829 from klimick/map-closed-inheritance-to-union
Browse files Browse the repository at this point in the history
Mapping closed inheritance to union during assertion
  • Loading branch information
orklah committed May 29, 2023
2 parents eedea6b + 38c93db commit 2bbfca6
Show file tree
Hide file tree
Showing 3 changed files with 350 additions and 0 deletions.
81 changes: 81 additions & 0 deletions src/Psalm/Internal/Type/ClosedInheritanceToUnion.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
<?php

declare(strict_types=1);

namespace Psalm\Internal\Type;

use Psalm\Codebase;
use Psalm\Type\Atomic\TGenericObject;
use Psalm\Type\Atomic\TNamedObject;
use Psalm\Type\Union;

use function array_keys;

/**
* @internal
*/
final class ClosedInheritanceToUnion
{
public static function map(Union $input, Codebase $codebase): Union
{
$new_types = [];
$meet_inheritors = false;

foreach ($input->getAtomicTypes() as $atomic_type) {
if ($atomic_type instanceof TNamedObject) {
$storage = $codebase->classlikes->getStorageFor($atomic_type->value);

if (null === $storage || null === $storage->inheritors) {
$new_types[] = $atomic_type;
continue;
}

$template_result = self::getTemplateResult($atomic_type, $codebase);

$replaced_inheritors = TemplateInferredTypeReplacer::replace(
$storage->inheritors,
$template_result,
$codebase,
);

foreach ($replaced_inheritors->getAtomicTypes() as $replaced_atomic_type) {
$new_types[] = $replaced_atomic_type;
}

$meet_inheritors = true;
} else {
$new_types[] = $atomic_type;
}
}

if (!$meet_inheritors) {
return $input;
}

return $new_types ? $input->setTypes($new_types) : $input;
}

private static function getTemplateResult(TNamedObject $object, Codebase $codebase): TemplateResult
{
if (!$object instanceof TGenericObject) {
return new TemplateResult([], []);
}

$storage = $codebase->classlikes->getStorageFor($object->value);

if (null === $storage || null === $storage->template_types) {
return new TemplateResult([], []);
}

$lower_bounds = [];
$offset = 0;

foreach ($storage->template_types as $template_name => $templates) {
foreach (array_keys($templates) as $defining_class) {
$lower_bounds[$template_name][$defining_class] = $object->type_params[$offset++];
}
}

return new TemplateResult($storage->template_types, $lower_bounds);
}
}
5 changes: 5 additions & 0 deletions src/Psalm/Internal/Type/NegatedAssertionReconciler.php
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,11 @@ public static function reconcile(
int &$failed_reconciliation,
bool $inside_loop
): Union {
$existing_var_type = ClosedInheritanceToUnion::map(
$existing_var_type,
$statements_analyzer->getCodebase(),
);

$is_equality = $assertion->hasEquality();

$assertion_type = $assertion->getAtomicType();
Expand Down
264 changes: 264 additions & 0 deletions tests/AssertAnnotationTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -2247,6 +2247,270 @@ function isNonEmptyString($_str): bool
return true;
}',
],
'assertObjectWithClosedInheritance' => [
'code' => '<?php
/**
* @psalm-inheritors FirstChoice|SecondChoice|ThirdChoice
*/
interface Choice
{
}
final class FirstChoice implements Choice
{
}
final class SecondChoice implements Choice
{
}
final class ThirdChoice implements Choice
{
}
/**
* @psalm-assert-if-true FirstChoice $choice
*/
function isFirstChoice(Choice $choice): bool
{
return $choice instanceof FirstChoice;
}
/**
* @psalm-assert-if-true SecondChoice $choice
*/
function isSecondChoice(Choice $choice): bool
{
return $choice instanceof SecondChoice;
}
function testFirstChoice(Choice $choice): void
{
if (isFirstChoice($choice)) {
/** @psalm-check-type-exact $choice = FirstChoice */
} else {
/** @psalm-check-type-exact $choice = SecondChoice|ThirdChoice */
}
}
function testFirstAndSecondChoice(Choice $choice): void
{
if (isFirstChoice($choice)) {
/** @psalm-check-type-exact $choice = FirstChoice */
} elseif (isSecondChoice($choice)) {
/** @psalm-check-type-exact $choice = SecondChoice */
} else {
/** @psalm-check-type-exact $choice = ThirdChoice */
}
}',
],
'assertObjectWithClosedInheritanceWithMatch' => [
'code' => '<?php
/**
* @psalm-inheritors FirstChoice|SecondChoice|ThirdChoice
*/
interface Choice
{
}
final class FirstChoice implements Choice {}
final class SecondChoice implements Choice {}
final class ThirdChoice implements Choice {}
/**
* @psalm-assert-if-true FirstChoice $choice
*/
function isFirstChoice(Choice $choice): bool
{
return $choice instanceof FirstChoice;
}
/**
* @psalm-assert-if-true SecondChoice $choice
*/
function isSecondChoice(Choice $choice): bool
{
return $choice instanceof SecondChoice;
}
function testFirstChoice(FirstChoice $_first): string
{
return "first";
}
function testSecondOrThirdChoice(SecondChoice|ThirdChoice $_first): string
{
return "second or third";
}
function getLabel(Choice $choice): string
{
return match (true) {
isFirstChoice($choice) => testFirstChoice($choice),
default => testSecondOrThirdChoice($choice),
};
}',
'assertions' => [],
'ignored_issues' => [],
'php_version' => '8.1',
],
'assertTemplatedObjectWithClosedInheritance' => [
'code' => '<?php
/**
* @template-covariant E
* @template-covariant A
* @psalm-inheritors Left<E> | Right<A>
*/
interface Either {
/** @psalm-assert-if-true Left<E> $this */
public function isLeft(): bool;
/** @psalm-assert-if-true Right<A> $this */
public function isRight(): bool;
}
/**
* @template E
* @implements Either<E, never>
*/
final class Left implements Either {
public function isLeft(): bool
{
return true;
}
public function isRight(): bool
{
return false;
}
}
/**
* @template A
* @implements Either<never, A>
*/
final class Right implements Either {
public function isLeft(): bool
{
return false;
}
public function isRight(): bool
{
return true;
}
}
/**
* @template E
* @template A
* @param Either<E, A> $either
* @psalm-assert-if-true Left<E> $either
*/
function isLeft(Either $either): bool
{
return $either instanceof Left;
}
/**
* @template E
* @template A
* @param Either<E, A> $either
* @psalm-assert-if-true Right<A> $either
*/
function isRight(Either $either): bool
{
return $either instanceof Right;
}
/**
* @return Either<OutOfRangeException, int>
*/
function getEither(): Either
{
throw new RuntimeException("???");
}
/**
* @param Left<OutOfRangeException> $_left
*/
function testLeft(Left $_left): void {}
/**
* @param Right<int> $_right
*/
function testRight(Right $_right): void {}
/** @param Either<OutOfRangeException, int> $either */
function isLeftFunctionIfElse(Either $either): void
{
if (isLeft($either)) {
/** @psalm-check-type-exact $either = Left<OutOfRangeException> */
testLeft($either);
} else {
/** @psalm-check-type-exact $either = Right<int> */
testRight($either);
}
}
/** @param Either<OutOfRangeException, int> $either */
function isRightFunctionIfElse(Either $either): void
{
if (isRight($either)) {
testRight($either);
/** @psalm-check-type-exact $either = Right<int> */
} else {
/** @psalm-check-type-exact $either = Left<OutOfRangeException> */
testLeft($either);
}
}
/** @param Either<OutOfRangeException, int> $either */
function testRightFunctionTernary(Either $either): void
{
isRight($either) ? testRight($either) : testLeft($either);
}
/** @param Either<OutOfRangeException, int> $either */
function testLeftFunctionTernary(Either $either): void
{
isLeft($either) ? testLeft($either) : testRight($either);
}
/** @param Either<OutOfRangeException, int> $either */
function isLeftMethodIfElse(Either $either): void
{
if ($either->isLeft()) {
/** @psalm-check-type-exact $either = Left<OutOfRangeException> */
testLeft($either);
} else {
/** @psalm-check-type-exact $either = Right<int> */
testRight($either);
}
}
/** @param Either<OutOfRangeException, int> $either */
function isRightMethodIfElse(Either $either): void
{
if ($either->isRight()) {
testRight($either);
/** @psalm-check-type-exact $either = Right<int> */
} else {
/** @psalm-check-type-exact $either = Left<OutOfRangeException> */
testLeft($either);
}
}
/** @param Either<OutOfRangeException, int> $either */
function testRightMethodTernary(Either $either): void
{
$either->isRight() ? testRight($either) : testLeft($either);
}
/** @param Either<OutOfRangeException, int> $either */
function testLeftMethodTernary(Either $either): void
{
$either->isLeft() ? testLeft($either) : testRight($either);
}',
],
];
}

Expand Down

0 comments on commit 2bbfca6

Please sign in to comment.