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:
commit
2bbfca6d9f
81
src/Psalm/Internal/Type/ClosedInheritanceToUnion.php
Normal file
81
src/Psalm/Internal/Type/ClosedInheritanceToUnion.php
Normal 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);
|
||||
}
|
||||
}
|
@ -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();
|
||||
|
@ -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);
|
||||
}',
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user