Skip to content
Merged
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
68 changes: 50 additions & 18 deletions src/Type/Constant/ConstantArrayType.php
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@
use PHPStan\Type\IsSuperTypeOfResult;
use PHPStan\Type\MixedType;
use PHPStan\Type\NeverType;
use PHPStan\Type\NonAcceptingNeverType;
use PHPStan\Type\NullType;
use PHPStan\Type\ObjectWithoutClassType;
use PHPStan\Type\RecursionGuard;
Expand Down Expand Up @@ -2793,15 +2794,7 @@ public function tryRemove(Type $typeToRemove): ?Type
}

if ($valueIsSuperType->yes()) {
$unsetResult = $this->unsetOffset($offsetType, 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();
}
return $unsetResult;
return $this->removeOffset($offsetType);
}

$newValueType = TypeCombinator::remove($currentValueType, $valueTypeToRemove);
Expand All @@ -2822,20 +2815,59 @@ public function tryRemove(Type $typeToRemove): ?Type
}

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();
}
return $unsetResult;
return $this->removeOffset($typeToRemove->getOffsetType());
}

return null;
}

/**
* The values of this shape lacking the offset. Unlike unsetOffset(), an
* explicit key the unsealed extras can also hold becomes an optional never
* key - dropping it would let the extras supply it again, widening the
* shape instead of subtracting from it.
*/
private function removeOffset(Type $offsetType): Type
{
$unsetResult = $this->unsetOffset($offsetType, 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();
}

if ($this->unsealed === null || !$this->isUnsealed()->yes()) {
return $unsetResult;
}

$offsetType = $offsetType->toArrayKey();
$offsetValues = $offsetType->getConstantScalarValues();
if (count($offsetValues) !== 1 || $this->unsealed[0]->isSuperTypeOf($offsetType)->no()) {
return $unsetResult;
}

$offsetValue = $offsetValues[0];
if (!is_int($offsetValue) && !is_string($offsetValue)) {
return $unsetResult;
}

$i = $this->getKeyIndexMap()[$offsetValue] ?? null;
if ($i === null) {
return $unsetResult;
}

$valueTypes = $this->valueTypes;
$valueTypes[$i] = new NonAcceptingNeverType();
$optionalKeys = $this->optionalKeys;
if (!in_array($i, $optionalKeys, true)) {
$optionalKeys[] = $i;
}

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

public function traverse(callable $cb): Type
{
$valueTypes = [];
Expand Down
74 changes: 74 additions & 0 deletions tests/PHPStan/Analyser/nsrt/bug-15428.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
<?php declare(strict_types = 1);

namespace Bug15428;

use function PHPStan\Testing\assertType;

/**
* @param array{a?: string, ...} $c
*/
function repro(array &$c): void
{
$x = $c['a'] ?? ''; // this causes $c to lose it's type
assertType('array{a?: string, ...}', $c);
$c['a'] = '1';
}

/**
* @param array{a?: string, ...} $c
*/
function issetBranches(array $c): void
{
if (isset($c['a'])) {
assertType('array{a: string, ...}', $c);
} else {
assertType('array{a?: never, ...}', $c);
}
assertType('array{a?: string, ...}', $c);
}

/**
* @param array{a?: string, ...} $c
*/
function arrayKeyExistsBranches(array $c): void
{
if (array_key_exists('a', $c)) {
assertType('array{a: string, ...}', $c);
} else {
assertType('array{a?: never, ...}', $c);
}
assertType('array{a?: string, ...}', $c);
}

/**
* @param array{a?: string, b: int, ...<string, int>} $c
*/
function typedExtras(array $c): void
{
if (!isset($c['a'])) {
assertType('array{a?: never, b: int, ...<string, int>}', $c);
}
assertType('array{a?: string, b: int, ...<string, int>}', $c);
}

/**
* @param array{b: int, ...<string, int>} $c
*/
function implicitKey(array $c): void
{
if (!isset($c['a'])) {
assertType('array{b: int, ...<string, int>}', $c);
}
assertType('array{b: int, ...<string, int>}', $c);
}

/**
* @param array{a?: string, ...<int, int>} $c
*/
function extrasCannotHoldKey(array $c): void
{
if (!isset($c['a'])) {
assertType('array{...<int, int>}', $c);
}
assertType('array{a?: string, ...<int, int>}', $c);
}
Original file line number Diff line number Diff line change
Expand Up @@ -140,4 +140,9 @@ public function testBug15066(): void
]);
}

public function testBug15428(): void
{
$this->analyse([__DIR__ . '/data/bug-15428.php'], []);
}

}
12 changes: 12 additions & 0 deletions tests/PHPStan/Rules/Variables/data/bug-15428.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?php declare(strict_types = 1);

namespace Bug15428Variables;

/**
* @param array{a?: string, ...} $c
*/
function repro(array &$c): void
{
$x = $c['a'] ?? ''; // this causes $c to lose it's type
$c['a'] = '1';
}
67 changes: 62 additions & 5 deletions turbo-ext/src/ConstantArrayType.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4532,7 +4532,7 @@ class ConstantArrayType

if (valueIsSuperType == PT_TRI_NO) return zv::Val::null();

if (valueIsSuperType == PT_TRI_YES) return unsetPreservingListCertainty(offsetType.raw(), l);
if (valueIsSuperType == PT_TRI_YES) return removeOffset(offsetType.raw(), l);

zv::Val newValueType = combinator2(PT_LC("remove"), currentValueType, valueTypeToRemove.raw());
if (UNEXPECTED(newValueType.isUndef())) return zv::Val();
Expand All @@ -4550,7 +4550,7 @@ class ConstantArrayType
zv::Val offsetType = pt_has_offset_type_get_offset_type(Z_OBJ_P(typeToRemove));
zval *l = isListSlot();
if (UNEXPECTED(offsetType.isUndef() || l == NULL)) return zv::Val();
return unsetPreservingListCertainty(offsetType.raw(), l);
return removeOffset(offsetType.raw(), l);
}

return zv::Val::null();
Expand Down Expand Up @@ -6621,9 +6621,12 @@ class ConstantArrayType

/* $this->unsetOffset($offsetType, true), never when that turns a
* definite list into a definite non-list; UNDEF = pending exception */
zv::Val unsetPreservingListCertainty(zval *offsetType, zval *isListSlotValue) const
/* removeOffset(): the values of $this lacking the offset — an explicit
* key the unsealed extras can also hold stays as an optional never key
* instead of being dropped; UNDEF = pending exception */
zv::Val removeOffset(zval *offsetTypeArg, zval *isListSlotValue) const
{
zv::Val unsetResult = thisUnsetOffset(offsetType, true);
zv::Val unsetResult = thisUnsetOffset(offsetTypeArg, true);
if (UNEXPECTED(unsetResult.isUndef())) return zv::Val();
/* When the source was definitely a list but the post-unset shape
* definitely isn't (e.g. unsetting a non-optional leading key
Expand All @@ -6636,7 +6639,61 @@ class ConstantArrayType
if (UNEXPECTED(resultIsList < 0)) return zv::Val();
if (resultIsList == PT_TRI_NO) return neverType();
}
return unsetResult;

/* $this->unsealed === null || !$this->isUnsealed()->yes() */
zval *u = unsealedSlot();
if (UNEXPECTED(u == NULL)) return zv::Val();
if (Z_TYPE_P(u) == IS_NULL) return unsetResult;
zend_long unsealedness = thisIsUnsealed();
if (UNEXPECTED(unsealedness < 0)) return zv::Val();
if (unsealedness != PT_TRI_YES) return unsetResult;

zv::Val offsetType = callType(Z_OBJ_P(offsetTypeArg), PT_LC("toarraykey"), 0, NULL);
if (UNEXPECTED(offsetType.isUndef())) return zv::Val();
zv::Val offsetValues = pt_type_call_array(Z_OBJ_P(offsetType.raw()), PT_LC("getconstantscalarvalues"), 0, NULL);
if (UNEXPECTED(offsetValues.isUndef())) return zv::Val();
if (arrayCount(offsetValues.raw()) != 1) return unsetResult;
zval *unsealedKeyType, *unsealedValueType;
if (UNEXPECTED(!unsealedPair(u, unsealedKeyType, unsealedValueType))) return zv::Val();
zend_long extrasHoldOffset = isSuperTypeOfValue(unsealedKeyType, offsetType.raw());
if (UNEXPECTED(extrasHoldOffset < 0)) return zv::Val();
if (extrasHoldOffset == PT_TRI_NO) return unsetResult;

/* $offsetValues[0], an int or a string */
zval *offsetValue = zend_hash_index_find(Z_ARRVAL_P(offsetValues.raw()), 0);
if (offsetValue == NULL) return unsetResult;
ZVAL_DEREF(offsetValue);
if (Z_TYPE_P(offsetValue) != IS_LONG && Z_TYPE_P(offsetValue) != IS_STRING) return unsetResult;

/* $this->getKeyIndexMap()[$offsetValue] ?? null */
zv::Val map = getKeyIndexMap();
if (UNEXPECTED(map.isUndef())) return zv::Val();
zval *found = mapFind(Z_ARRVAL_P(map.raw()), offsetValue);
if (found == NULL || Z_TYPE_P(found) != IS_LONG) return unsetResult;
zend_long i = Z_LVAL_P(found);

zval *k = keyTypes();
zval *v = k != NULL ? valueTypes() : NULL;
zval *n = v != NULL ? nextAutoIndexes() : NULL;
zval *o = n != NULL ? optionalKeys() : NULL;
if (UNEXPECTED(o == NULL)) return zv::Val();

zval never;
if (UNEXPECTED(!pt_non_accepting_never_type_new(&never))) return zv::Val();
zv::Val neverValue = zv::Val::adopt(never);
zv::Arr valueTypesCopy = zv::Arr::adoptVal(zv::Val::copyOf(zv::Ref(v)));
valueTypesCopy.separate();
zv::ArrRef(valueTypesCopy.raw()).setIndex((zend_ulong) i, zv::Ref(neverValue.raw()));

zv::Arr optionalKeysCopy = zv::Arr::adoptVal(zv::Val::copyOf(zv::Ref(o)));
if (!inArrayStrictLong(o, i)) {
optionalKeysCopy.separate();
optionalKeysCopy.push(zv::Val::integer(i));
}

zend_long resultIsList = pt_type_op_trinary(Z_OBJ_P(unsetResult.raw()), PT_OP_IS_LIST, 0, NULL);
if (UNEXPECTED(resultIsList < 0)) return zv::Val();
return thisRecreate(k, valueTypesCopy.raw(), n, optionalKeysCopy.raw(), pt_trinary_singleton(resultIsList), u);
}

/* TypeCombinator::intersect($this, new NonEmptyArrayType()) */
Expand Down
Loading
Loading