Skip to content

Commit b5c64fb

Browse files
committed
isSuperTypeOf
1 parent a1dd1d8 commit b5c64fb

2 files changed

Lines changed: 153 additions & 3 deletions

File tree

src/Type/Constant/ConstantArrayType.php

Lines changed: 75 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@
5757
use PHPStan\Type\TypeCombinator;
5858
use PHPStan\Type\UnionType;
5959
use PHPStan\Type\VerbosityLevel;
60+
use function array_key_exists;
6061
use function array_keys;
6162
use function array_map;
6263
use function array_merge;
@@ -647,13 +648,29 @@ private function checkOurKeys(Type $type, bool $strictTypes): AcceptsResult
647648
public function isSuperTypeOf(Type $type): IsSuperTypeOfResult
648649
{
649650
if ($type instanceof self) {
651+
$thisUnsealedness = $this->isUnsealed();
652+
$typeUnsealedness = $type->isUnsealed();
653+
$bothDefinite = !$thisUnsealedness->maybe() && !$typeUnsealedness->maybe();
654+
650655
if (count($this->keyTypes) === 0) {
651-
return new IsSuperTypeOfResult($type->isIterableAtLeastOnce()->negate(), []);
656+
if (!$bothDefinite) {
657+
return new IsSuperTypeOfResult($type->isIterableAtLeastOnce()->negate(), []);
658+
}
659+
if ($thisUnsealedness->no()) {
660+
return new IsSuperTypeOfResult($type->isIterableAtLeastOnce()->negate(), []);
661+
}
662+
// $this is unsealed with no known keys — fall through to extras/unsealed-part checks below
652663
}
653664

654665
$results = [];
655666
foreach ($this->keyTypes as $i => $keyType) {
656667
$hasOffset = $type->hasOffsetValueType($keyType);
668+
if ($bothDefinite && $hasOffset->no() && $typeUnsealedness->yes()) {
669+
[$typeUnsealedKey] = $type->getUnsealedTypes();
670+
if (!$typeUnsealedKey->isSuperTypeOf($keyType)->no()) {
671+
$hasOffset = TrinaryLogic::createMaybe();
672+
}
673+
}
657674
if ($hasOffset->no()) {
658675
if (!$this->isOptionalKey($i)) {
659676
return IsSuperTypeOfResult::createNo();
@@ -665,13 +682,69 @@ public function isSuperTypeOf(Type $type): IsSuperTypeOfResult
665682
$results[] = IsSuperTypeOfResult::createMaybe();
666683
}
667684

668-
$isValueSuperType = $this->valueTypes[$i]->isSuperTypeOf($type->getOffsetValueType($keyType));
685+
$otherValueType = $type->getOffsetValueType($keyType);
686+
if ($otherValueType instanceof ErrorType && $bothDefinite && $typeUnsealedness->yes()) {
687+
[, $typeUnsealedValue] = $type->getUnsealedTypes();
688+
$otherValueType = $typeUnsealedValue;
689+
}
690+
$isValueSuperType = $this->valueTypes[$i]->isSuperTypeOf($otherValueType);
669691
if ($isValueSuperType->no()) {
670692
return $isValueSuperType->decorateReasons(static fn (string $reason) => sprintf('Offset %s: %s', $keyType->describe(VerbosityLevel::value()), $reason));
671693
}
672694
$results[] = $isValueSuperType;
673695
}
674696

697+
if ($bothDefinite) {
698+
$thisKeyValues = [];
699+
foreach ($this->keyTypes as $thisKeyType) {
700+
$thisKeyValues[$thisKeyType->getValue()] = true;
701+
}
702+
703+
foreach ($type->getKeyTypes() as $i => $typeKey) {
704+
if (array_key_exists($typeKey->getValue(), $thisKeyValues)) {
705+
continue;
706+
}
707+
708+
if ($thisUnsealedness->no()) {
709+
if (!$type->isOptionalKey($i)) {
710+
return IsSuperTypeOfResult::createNo();
711+
}
712+
$results[] = IsSuperTypeOfResult::createMaybe();
713+
continue;
714+
}
715+
716+
[$thisUnsealedKey, $thisUnsealedValue] = $this->getUnsealedTypes();
717+
$keyCheck = $thisUnsealedKey->isSuperTypeOf($typeKey);
718+
if ($keyCheck->no()) {
719+
if ($type->isOptionalKey($i)) {
720+
$results[] = IsSuperTypeOfResult::createMaybe();
721+
continue;
722+
}
723+
return IsSuperTypeOfResult::createNo();
724+
}
725+
$valueCheck = $thisUnsealedValue->isSuperTypeOf($type->getValueTypes()[$i]);
726+
if ($valueCheck->no()) {
727+
if ($type->isOptionalKey($i)) {
728+
$results[] = IsSuperTypeOfResult::createMaybe();
729+
continue;
730+
}
731+
return IsSuperTypeOfResult::createNo();
732+
}
733+
$results[] = $keyCheck->and($valueCheck);
734+
}
735+
736+
if ($typeUnsealedness->yes()) {
737+
if ($thisUnsealedness->no()) {
738+
$results[] = IsSuperTypeOfResult::createMaybe();
739+
} else {
740+
[$thisUnsealedKey, $thisUnsealedValue] = $this->getUnsealedTypes();
741+
[$typeUnsealedKey, $typeUnsealedValue] = $type->getUnsealedTypes();
742+
$results[] = $thisUnsealedKey->isSuperTypeOf($typeUnsealedKey);
743+
$results[] = $thisUnsealedValue->isSuperTypeOf($typeUnsealedValue);
744+
}
745+
}
746+
}
747+
675748
return IsSuperTypeOfResult::createYes()->and(...$results);
676749
}
677750

tests/PHPStan/Type/Constant/ConstantArrayTypeTest.php

Lines changed: 78 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
use Closure;
66
use PHPStan\DependencyInjection\BleedingEdgeToggle;
7+
use PHPStan\PhpDoc\TypeStringResolver;
78
use PHPStan\Testing\PHPStanTestCase;
89
use PHPStan\TrinaryLogic;
910
use PHPStan\Type\Accessory\HasOffsetType;
@@ -909,11 +910,87 @@ public static function dataIsSuperTypeOf(): iterable
909910
]),
910911
TrinaryLogic::createYes(),
911912
];
913+
914+
// definite sealedness tests (bleeding edge)
915+
916+
// both sealed, same keys, compatible values
917+
yield ['array{a: int, b: string}', 'array{a: int, b: string}', TrinaryLogic::createYes()];
918+
919+
// both sealed, bigger vs smaller (subset) — sealed requires exact keys
920+
yield ['array{a: int, b: string}', 'array{a: int}', TrinaryLogic::createNo()];
921+
yield ['array{a: int}', 'array{a: int, b: string}', TrinaryLogic::createNo()];
922+
923+
// both sealed, narrower value
924+
yield ['array{a: int}', 'array{a: int<0, max>}', TrinaryLogic::createYes()];
925+
yield ['array{a: int<0, max>}', 'array{a: int}', TrinaryLogic::createMaybe()];
926+
927+
// both sealed, optional key in left only
928+
yield ['array{a: int, b?: string}', 'array{a: int}', TrinaryLogic::createYes()];
929+
yield ['array{a: int, b?: string}', 'array{a: int, b: string}', TrinaryLogic::createYes()];
930+
931+
// both unsealed, compatible known keys + compatible unsealed
932+
yield ['array{a: int, ...}', 'array{a: int<0, max>, ...}', TrinaryLogic::createYes()];
933+
yield ['array{a: int<0, max>, ...}', 'array{a: int, ...}', TrinaryLogic::createMaybe()];
934+
935+
// both unsealed, bigger known on right (right's extra fits left's unsealed extras)
936+
yield ['array{a: int, ...}', 'array{a: int, b: string, ...}', TrinaryLogic::createYes()];
937+
938+
// both unsealed, right has known key left doesn't require; left's unsealed must cover
939+
yield ['array{a: int, ...<string, string>}', 'array{a: int, b: int, ...<string, string>}', TrinaryLogic::createNo()];
940+
yield ['array{a: int, ...<string, string>}', 'array{a: int, b: non-empty-string, ...<string, string>}', TrinaryLogic::createYes()];
941+
942+
// both unsealed, narrower unsealed value on right
943+
yield ['array{a: int, ...<string, string>}', 'array{a: int, ...<string, non-empty-string>}', TrinaryLogic::createYes()];
944+
yield ['array{a: int, ...<string, non-empty-string>}', 'array{a: int, ...<string, string>}', TrinaryLogic::createMaybe()];
945+
946+
// both unsealed, narrower unsealed key on right (array-key ⊃ string)
947+
yield ['array{a: int, ...<array-key, string>}', 'array{a: int, ...<string, string>}', TrinaryLogic::createYes()];
948+
yield ['array{a: int, ...<string, string>}', 'array{a: int, ...<array-key, string>}', TrinaryLogic::createMaybe()];
949+
950+
// both unsealed, incompatible unsealed key types
951+
yield ['array{...<int, string>}', 'array{...<string, string>}', TrinaryLogic::createNo()];
952+
953+
// both unsealed, incompatible unsealed value types
954+
yield ['array{...<int, string>}', 'array{...<int, int>}', TrinaryLogic::createNo()];
955+
956+
// unsealed vs sealed — sealed's extras must fit unsealed's unsealed
957+
yield ['array{a: int, ...}', 'array{a: int, b: string}', TrinaryLogic::createYes()];
958+
yield ['array{a: int, ...<string, int>}', 'array{a: int, b: string}', TrinaryLogic::createNo()];
959+
960+
// sealed vs unsealed — unsealed might have extras sealed doesn't allow
961+
yield ['array{a: int}', 'array{a: int, ...}', TrinaryLogic::createMaybe()];
962+
yield ['array{a: int, b: string}', 'array{a: int<0, max>, ...}', TrinaryLogic::createMaybe()];
963+
964+
// sealed vs unsealed where sealed's keys can't be in unsealed's extras
965+
yield ['array{a: int}', 'array{...<int, int>}', TrinaryLogic::createNo()];
966+
967+
// sealed vs unsealed where sealed fits unsealed's extras
968+
yield ['array{a: int}', 'array{...<string, int>}', TrinaryLogic::createMaybe()];
912969
}
913970

971+
/**
972+
* @param ConstantArrayType|string $type
973+
* @param Type|string $otherType
974+
*/
914975
#[DataProvider('dataIsSuperTypeOf')]
915-
public function testIsSuperTypeOf(ConstantArrayType $type, Type $otherType, TrinaryLogic $expectedResult): void
976+
public function testIsSuperTypeOf($type, $otherType, TrinaryLogic $expectedResult): void
916977
{
978+
$bleedingEdgeBackup = BleedingEdgeToggle::isBleedingEdge();
979+
BleedingEdgeToggle::setBleedingEdge(true);
980+
try {
981+
$resolver = self::getContainer()->getByType(TypeStringResolver::class);
982+
if (is_string($type)) {
983+
$resolved = $resolver->resolve($type, null);
984+
$this->assertInstanceOf(ConstantArrayType::class, $resolved);
985+
$type = $resolved;
986+
}
987+
if (is_string($otherType)) {
988+
$otherType = $resolver->resolve($otherType, null);
989+
}
990+
} finally {
991+
BleedingEdgeToggle::setBleedingEdge($bleedingEdgeBackup);
992+
}
993+
917994
$actualResult = $type->isSuperTypeOf($otherType);
918995
$this->assertSame(
919996
$expectedResult->describe(),

0 commit comments

Comments
 (0)