1
0
mirror of https://github.com/danog/psalm.git synced 2024-11-27 04:45:20 +01:00

Merge pull request #9829 from klimick/map-closed-inheritance-to-union

Mapping closed inheritance to union during assertion
This commit is contained in:
orklah 2023-05-29 20:14:31 +02:00 committed by GitHub
commit 2bbfca6d9f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 350 additions and 0 deletions

View File

@ -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);
}
}

View File

@ -59,6 +59,11 @@ class NegatedAssertionReconciler extends Reconciler
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();

View File

@ -2247,6 +2247,270 @@ class AssertAnnotationTest extends TestCase
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);
}',
],
];
}