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
19 changes: 17 additions & 2 deletions src/Type/Accessory/HasOffsetValueType.php
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@
use PHPStan\Type\Traits\MaybeStringTypeTrait;
use PHPStan\Type\Traits\NonGeneralizableTypeTrait;
use PHPStan\Type\Traits\NonGenericTypeTrait;
use PHPStan\Type\Traits\NonRemoveableTypeTrait;
use PHPStan\Type\Traits\TruthyBooleanTypeTrait;
use PHPStan\Type\Traits\UndecidedComparisonCompoundTypeTrait;
use PHPStan\Type\Type;
Expand All @@ -57,7 +56,6 @@
use TruthyBooleanTypeTrait;
use NonGenericTypeTrait;
use UndecidedComparisonCompoundTypeTrait;
use NonRemoveableTypeTrait;
use NonGeneralizableTypeTrait;

public function __construct(private ConstantStringType|ConstantIntegerType $offsetType, private Type $valueType)
Expand Down Expand Up @@ -213,6 +211,23 @@
return $this;
}

public function tryRemove(Type $typeToRemove): ?Type
{
if ($typeToRemove instanceof self && $this->offsetType->equals($typeToRemove->getOffsetType())) {
$valueIsSuperType = $typeToRemove->getValueType()->isSuperTypeOf($this->valueType);

Check warning on line 217 in src/Type/Accessory/HasOffsetValueType.php

View workflow job for this annotation

GitHub Actions / Mutation Testing (8.3, ubuntu-latest)

Escaped Mutant for Mutator "PHPStan\Infection\IsSuperTypeOfCalleeAndArgumentMutator": @@ @@ public function tryRemove(Type $typeToRemove): ?Type { if ($typeToRemove instanceof self && $this->offsetType->equals($typeToRemove->getOffsetType())) { - $valueIsSuperType = $typeToRemove->getValueType()->isSuperTypeOf($this->valueType); + $valueIsSuperType = $this->valueType->isSuperTypeOf($typeToRemove->getValueType()); if ($valueIsSuperType->no()) { return null;

Check warning on line 217 in src/Type/Accessory/HasOffsetValueType.php

View workflow job for this annotation

GitHub Actions / Mutation Testing (8.4, ubuntu-latest)

Escaped Mutant for Mutator "PHPStan\Infection\IsSuperTypeOfCalleeAndArgumentMutator": @@ @@ public function tryRemove(Type $typeToRemove): ?Type { if ($typeToRemove instanceof self && $this->offsetType->equals($typeToRemove->getOffsetType())) { - $valueIsSuperType = $typeToRemove->getValueType()->isSuperTypeOf($this->valueType); + $valueIsSuperType = $this->valueType->isSuperTypeOf($typeToRemove->getValueType()); if ($valueIsSuperType->no()) { return null;

if ($valueIsSuperType->no()) {
return null;
}

$newValueType = TypeCombinator::remove($this->valueType, $typeToRemove->getValueType());

return new self($this->offsetType, $newValueType);
}

return null;
}

public function getKeysArrayFiltered(Type $filterValueType, TrinaryLogic $strict): Type
{
return $this->getKeysArray();
Expand Down
47 changes: 42 additions & 5 deletions src/Type/Constant/ConstantArrayType.php
Original file line number Diff line number Diff line change
Expand Up @@ -2629,12 +2629,49 @@ public function tryRemove(Type $typeToRemove): ?Type
return new ConstantArrayType([], []);
}

if ($typeToRemove instanceof HasOffsetType || $typeToRemove instanceof HasOffsetValueType) {
if ($typeToRemove instanceof HasOffsetValueType) {
$offsetType = $typeToRemove->getOffsetType();
$valueTypeToRemove = $typeToRemove->getValueType();

foreach ($this->keyTypes as $i => $keyType) {
Comment thread
VincentLanglet marked this conversation as resolved.
if ($keyType->getValue() !== $offsetType->getValue()) {
continue;
}

$currentValueType = $this->valueTypes[$i];
$valueIsSuperType = $valueTypeToRemove->isSuperTypeOf($currentValueType);

if ($valueIsSuperType->no()) {
return null;
}

if ($valueIsSuperType->yes()) {
$unsetResult = $this->unsetOffset($offsetType, true);
if ($this->isList->yes() && $unsetResult->isList()->no()) {
return new NeverType();
}
return $unsetResult;
}

$newValueType = TypeCombinator::remove($currentValueType, $valueTypeToRemove);
$valueTypes = $this->valueTypes;
$valueTypes[$i] = $newValueType;

return $this->recreate(
$this->keyTypes,
$valueTypes,
$this->nextAutoIndexes,
$this->optionalKeys,
$this->isList,
$this->unsealed,
);
}

return null;
}

if ($typeToRemove instanceof HasOffsetType) {
$unsetResult = $this->unsetOffset($typeToRemove->getOffsetType(), true);
// When the source was definitely a list but the post-unset shape
// definitely isn't (e.g. unsetting a non-optional leading key
// creates a hole), no value of $this could have lacked the
// removed key — the subtraction yields the empty set.
if ($this->isList->yes() && $unsetResult->isList()->no()) {
return new NeverType();
}
Expand Down
61 changes: 61 additions & 0 deletions tests/PHPStan/Type/TypeCombinatorTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -6167,6 +6167,67 @@ public static function dataRemove(): array
IntersectionType::class,
'non-empty-array<string, string>&oversized-array',
],
// HasOffsetValueType with partial value overlap — narrow value, keep key mandatory
[
new ConstantArrayType(
[new ConstantStringType('a')],
[new UnionType([new StringType(), new IntegerType()])],
),
new HasOffsetValueType(new ConstantStringType('a'), new IntegerType()),
ConstantArrayType::class,
'array{a: string}',
],
// HasOffsetValueType with partial value overlap — narrow value, keep key optional
[
new ConstantArrayType(
[new ConstantStringType('a')],
[new UnionType([new StringType(), new IntegerType()])],
[0],
[0],
),
new HasOffsetValueType(new ConstantStringType('a'), new IntegerType()),
ConstantArrayType::class,
'array{a?: string}',
],
// HasOffsetValueType with full value overlap on optional key — remove key entirely
[
new ConstantArrayType(
[new ConstantStringType('a')],
[new IntegerType()],
[0],
[0],
),
new HasOffsetValueType(new ConstantStringType('a'), new IntegerType()),
ConstantArrayType::class,
'array{}',
],
// HasOffsetValueType with partial value overlap — multi-key array
[
new ConstantArrayType(
[new ConstantStringType('a'), new ConstantStringType('b')],
[new UnionType([new StringType(), new IntegerType()]), new StringType()],
),
new HasOffsetValueType(new ConstantStringType('a'), new IntegerType()),
ConstantArrayType::class,
'array{a: string, b: string}',
],
// HasOffsetValueType removing another HasOffsetValueType with same offset
[
new HasOffsetValueType(new ConstantStringType('a'), new UnionType([new StringType(), new IntegerType()])),
new HasOffsetValueType(new ConstantStringType('a'), new IntegerType()),
HasOffsetValueType::class,
'hasOffsetValue(\'a\', string)',
],
// HasOffsetValueType on IntersectionType — narrow value type through intersection
[
new IntersectionType([
new ArrayType(new MixedType(), new MixedType()),
new HasOffsetValueType(new ConstantStringType('a'), new UnionType([new StringType(), new IntegerType()])),
]),
new HasOffsetValueType(new ConstantStringType('a'), new IntegerType()),
IntersectionType::class,
'non-empty-array&hasOffsetValue(\'a\', string)',
],
];
}

Expand Down
Loading