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
85 changes: 64 additions & 21 deletions src/Analyser/ExprHandler/AssignHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -1809,29 +1809,18 @@
$nativeValueToWrite = $this->readAssignedValueType($nodeScopeResolver, $assignedValueResult, $assignedExpr, $scope->doNotTreatPhpDocTypesAsCertain());
[$varType, $varNativeType] = $this->resolveContainerTypesAfterAssignedExprEval($nodeScopeResolver, $var, $varResult, $scope, null, $storage);

$offsetValueType = $varType;
$offsetNativeValueType = $varNativeType;
$offsetValueTypeStack = [$offsetValueType];
$offsetValueNativeTypeStack = [$offsetNativeValueType];
foreach (array_slice($offsetTypes, 0, -1) as [$offsetType]) {
$offsetValueType = $offsetValueType->getOffsetValueType($offsetType);
$offsetValueTypeStack[] = $offsetValueType;
}
foreach (array_slice($offsetNativeTypes, 0, -1) as [$offsetNativeType]) {
$offsetNativeValueType = $offsetNativeValueType->getOffsetValueType($offsetNativeType);
$offsetValueNativeTypeStack[] = $offsetNativeValueType;
// the chain links whose own type the scope tracks (e.g. narrowed
// by is_array($a[$k]) for a non-constant $k) - their container
// type comes from the scope, and their new type is remembered
$trackedDimFetches = [];
foreach ($offsetTypes as $i => [, $existingDimFetch]) {
$dimFetch = $this->toArrayDimFetch($existingDimFetch);
$trackedDimFetches[$i] = $scope->hasExpressionType($dimFetch)->yes() ? $dimFetch : null;
}

foreach (array_reverse($offsetTypes) as [$offsetType]) {
/** @var Type $offsetValueType */
$offsetValueType = array_pop($offsetValueTypeStack);
$valueToWrite = $offsetValueType->setExistingOffsetValueType($offsetType, $valueToWrite);
}
foreach (array_reverse($offsetNativeTypes) as [$offsetNativeType]) {
/** @var Type $offsetNativeValueType */
$offsetNativeValueType = array_pop($offsetValueNativeTypeStack);
$nativeValueToWrite = $offsetNativeValueType->setExistingOffsetValueType($offsetNativeType, $nativeValueToWrite);
}
$nativeScope = $scope->doNotTreatPhpDocTypesAsCertain();
[$valueToWrite, $linkValues] = $this->rebuildExistingOffsetChain($offsetTypes, $trackedDimFetches, $varType, $valueToWrite, $scope);
[$nativeValueToWrite, $linkNativeValues] = $this->rebuildExistingOffsetChain($offsetNativeTypes, $trackedDimFetches, $varNativeType, $nativeValueToWrite, $nativeScope);

if ($var instanceof Variable && is_string($var->name)) {
$nodeScopeResolver->callNodeCallback($nodeCallback, new VariableAssignNode($var, $assignedPropertyExpr), $scope, $storage);
Expand All @@ -1842,10 +1831,18 @@
}
$scope = $scope->assignExpression(
$var,
$valueToWrite,

Check warning on line 1834 in src/Analyser/ExprHandler/AssignHandler.php

View workflow job for this annotation

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

Escaped Mutant for Mutator "PHPStan\Infection\TrinaryLogicMutator": @@ @@ $trackedDimFetches = []; foreach ($offsetTypes as $i => [, $existingDimFetch]) { $dimFetch = $this->toArrayDimFetch($existingDimFetch); - $trackedDimFetches[$i] = $scope->hasExpressionType($dimFetch)->yes() ? $dimFetch : null; + $trackedDimFetches[$i] = !$scope->hasExpressionType($dimFetch)->no() ? $dimFetch : null; } $nativeScope = $scope->doNotTreatPhpDocTypesAsCertain();

Check warning on line 1834 in src/Analyser/ExprHandler/AssignHandler.php

View workflow job for this annotation

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

Escaped Mutant for Mutator "PHPStan\Infection\TrinaryLogicMutator": @@ @@ $trackedDimFetches = []; foreach ($offsetTypes as $i => [, $existingDimFetch]) { $dimFetch = $this->toArrayDimFetch($existingDimFetch); - $trackedDimFetches[$i] = $scope->hasExpressionType($dimFetch)->yes() ? $dimFetch : null; + $trackedDimFetches[$i] = !$scope->hasExpressionType($dimFetch)->no() ? $dimFetch : null; } $nativeScope = $scope->doNotTreatPhpDocTypesAsCertain();
$nativeValueToWrite,
);
}

foreach ($trackedDimFetches as $i => $dimFetch) {
if ($dimFetch === null) {
continue;
}

$scope = $scope->assignExpression($dimFetch, $linkValues[$i], $linkNativeValues[$i]);
}
} else {
$result = $valueResult;
$hasYield = $hasYield || $result->hasYield();
Expand All @@ -1870,6 +1867,52 @@
);
}

/**
* @param non-empty-list<array{Type, ExistingArrayDimFetch}> $offsetTypes
* @param array<int, ArrayDimFetch|null> $trackedDimFetches
* @return array{Type, array<int, Type>}
*/
private function rebuildExistingOffsetChain(array $offsetTypes, array $trackedDimFetches, Type $offsetValueType, Type $valueToWrite, MutatingScope $scope): array
{
$offsetValueTypeStack = [$offsetValueType];
foreach (array_slice($offsetTypes, 0, -1) as $i => [$offsetType]) {
$trackedDimFetch = $trackedDimFetches[$i];
$offsetValueType = $trackedDimFetch !== null
? $scope->getStateType($trackedDimFetch)
: $offsetValueType->getOffsetValueType($offsetType);
$offsetValueTypeStack[] = $offsetValueType;
}

$linkValues = [];
for ($i = count($offsetTypes) - 1; $i >= 0; $i--) {
$linkValues[$i] = $valueToWrite;
/** @var Type $offsetValueType */
$offsetValueType = array_pop($offsetValueTypeStack);
$offsetType = $offsetTypes[$i][0];
$writtenValueType = $offsetValueType->setExistingOffsetValueType($offsetType, $valueToWrite);
if ($offsetValueType->hasOffsetValueType($offsetType)->maybe()) {
// unset() does not create a missing offset on its way
$writtenValueType = TypeCombinator::union(
$offsetValueType->isConstantArray()->yes() ? $offsetValueType->unsetOffset($offsetType) : $offsetValueType,
$writtenValueType,
);
}
$valueToWrite = $writtenValueType;
}

return [$valueToWrite, $linkValues];
}

private function toArrayDimFetch(ExistingArrayDimFetch $dimFetch): ArrayDimFetch
{
$var = $dimFetch->getVar();
if ($var instanceof ExistingArrayDimFetch) {
$var = $this->toArrayDimFetch($var);
}

Check warning on line 1912 in src/Analyser/ExprHandler/AssignHandler.php

View workflow job for this annotation

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

Escaped Mutant for Mutator "PHPStan\Infection\TrinaryLogicMutator": @@ @@ if ($offsetValueType->hasOffsetValueType($offsetType)->maybe()) { // unset() does not create a missing offset on its way $writtenValueType = TypeCombinator::union( - $offsetValueType->isConstantArray()->yes() ? $offsetValueType->unsetOffset($offsetType) : $offsetValueType, + !$offsetValueType->isConstantArray()->no() ? $offsetValueType->unsetOffset($offsetType) : $offsetValueType, $writtenValueType, ); }

Check warning on line 1912 in src/Analyser/ExprHandler/AssignHandler.php

View workflow job for this annotation

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

Escaped Mutant for Mutator "PHPStan\Infection\TrinaryLogicMutator": @@ @@ if ($offsetValueType->hasOffsetValueType($offsetType)->maybe()) { // unset() does not create a missing offset on its way $writtenValueType = TypeCombinator::union( - $offsetValueType->isConstantArray()->yes() ? $offsetValueType->unsetOffset($offsetType) : $offsetValueType, + !$offsetValueType->isConstantArray()->no() ? $offsetValueType->unsetOffset($offsetType) : $offsetValueType, $writtenValueType, ); }
return new ArrayDimFetch($var, $dimFetch->getDim());
}

/**
* A write through ArrayAccess (`$storage[$key] = $value`) reaches the
* object's template arguments exactly like the offsetSet() call it stands
Expand Down
87 changes: 87 additions & 0 deletions tests/PHPStan/Analyser/nsrt/bug-15461.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
<?php declare(strict_types = 1);

namespace Bug15461;

use function PHPStan\Testing\assertType;

/**
* @param mixed[] $export
* @return mixed[]
*/
function f(array $export, string $key): array {
assert(isset($export[$key]));
assert(is_array($export[$key]));
unset($export[$key]['x']);
assertType("array<mixed~'x', mixed>", $export[$key]);
return $export[$key];
}

/**
* @param mixed[] $export
* @return mixed[]
*/
function g(array $export, string $key): array {
assert(isset($export[$key]));
assert(is_array($export[$key]));
unset($export[$key]['x']);
unset($export[$key]['y']);
assertType("array<mixed~('x'|'y'), mixed>", $export[$key]);
return $export;
}

/** @param mixed[] $export */
function nested(array $export, string $key, string $key2): void {
assert(is_array($export[$key]));
assert(is_array($export[$key][$key2]));
unset($export[$key][$key2]['x']);
assertType("array<mixed~'x', mixed>", $export[$key][$key2]);
assertType('array<mixed, mixed>', $export[$key]);
}

/** @param mixed[] $export */
function dimUnset(array $export, string $key, string $key2): void {
assert(is_array($export[$key]));
unset($export[$key][$key2]);
assertType('array<mixed, mixed>', $export[$key]);
}

class Foo
{
/** @var mixed[] */
public array $prop = [];

/** @var mixed[] */
public static array $staticProp = [];

public function doFoo(string $key): void
{
assert(is_array($this->prop[$key]));
unset($this->prop[$key]['x']);
assertType("array<mixed~'x', mixed>", $this->prop[$key]);

assert(is_array(self::$staticProp[$key]));
unset(self::$staticProp[$key]['x']);
assertType("array<mixed~'x', mixed>", self::$staticProp[$key]);
}
}

/** @param array<string, array<string, int>> $export */
function untracked(array $export, string $key): void {
unset($export[$key]['x']);
assertType('array<string, array<string, int>>', $export);
assertType('array<string, int>', $export[$key]);
}

/** @param array{a?: array{b: int, c: int}} $x */
function optionalIntermediateKey(array $x): void {
unset($x['a']['b']);
assertType('array{}|array{a: array{c: int}}', $x);
}

/** @param array{a: array{b?: array<int, string>, c: int}} $x */
function optionalKeyOnTrackedLink(array $x, string $key, int $j): void {
$y = [$key => $x['a']];
assert(isset($y[$key]));
unset($y[$key]['b'][$j]);
assertType('array{b?: array<int, string>, c: int}', $y[$key]);
}
Original file line number Diff line number Diff line change
Expand Up @@ -1379,6 +1379,12 @@ public function testBug13075(): void
]);
}

public function testBug15461(): void
{
$this->reportPossiblyNonexistentGeneralArrayOffset = true;
$this->analyse([__DIR__ . '/data/bug-15461.php'], []);
}

public static function dataUnsealedArrayShapes(): iterable
{
foreach ([false, true] as $reportPossiblyNonexistentGeneralArrayOffset) {
Expand Down
30 changes: 30 additions & 0 deletions tests/PHPStan/Rules/Arrays/data/bug-15461.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<?php declare(strict_types = 1);

namespace Bug15461Rule;

/**
* @param mixed[] $export
* @param string $key
*
* @return mixed[]
*/
function f(array $export, string $key): array {
assert(isset($export[$key]));
assert(is_array($export[$key]));
unset($export[$key]['x']);
return $export[$key];
}

/**
* @param mixed[] $export
* @param string $key
*
* @return mixed[]
*/
function g(array $export, string $key): array {
assert(isset($export[$key]));
assert(is_array($export[$key]));
unset($export[$key]['x']);
unset($export[$key]['y']);
return $export;
}
2 changes: 1 addition & 1 deletion tests/PHPStan/Rules/Methods/data/bug-12927.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ public function sayHello(array $list): array
foreach($list as $k => $v) {
unset($list[$k]['abc']);
assertType('non-empty-list<array{}|array{abc: string}>', $list);
assertType('array{}|array{abc: string}', $list[$k]);
assertType('array{}', $list[$k]);
}
return $list;
}
Expand Down
80 changes: 71 additions & 9 deletions turbo-ext/src/AssignHandler.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4150,8 +4150,23 @@ class AssignHandler
zv::Val varNativeType;
AH_OKB(resolveContainerTypesAfterAssignedExprEval(nsr, var, varResult, scope.raw(), NULL, storage, varType, varNativeType));

AH_OKB(rebuildExistingOffsetChain(Z_ARRVAL_P(offsetTypes), varType.raw(), valueToWrite));
AH_OKB(rebuildExistingOffsetChain(Z_ARRVAL_P(offsetNativeTypes), varNativeType.raw(), nativeValueToWrite));
// the chain links whose own type the scope tracks (e.g. narrowed
// by is_array($a[$k]) for a non-constant $k) - their container
// type comes from the scope, and their new type is remembered
uint32_t linkCount = zend_hash_num_elements(Z_ARRVAL_P(offsetTypes));
zv::Arr trackedDimFetches = zv::Arr::create(linkCount);
for (uint32_t i = 0; i < linkCount; i++) {
zval *existingDimFetch = zend_hash_index_find(Z_ARRVAL_P(zend_hash_index_find(Z_ARRVAL_P(offsetTypes), i)), 1);
AH_VALB(dimFetch, toArrayDimFetch(existingDimFetch));
zend_long tracked = pt_mutating_scope_has_expression_type(Z_OBJ_P(scope.raw()), dimFetch.raw());
AH_OKB(tracked >= 0);
trackedDimFetches.push(tracked == PT_TRI_YES ? std::move(dimFetch) : zv::Val::null());
}

zv::Arr linkValues = zv::Arr::create(linkCount);
zv::Arr linkNativeValues = zv::Arr::create(linkCount);
AH_OKB(rebuildExistingOffsetChain(Z_ARRVAL_P(offsetTypes), trackedDimFetches.table(), varType.raw(), valueToWrite, scope.raw(), linkValues));
AH_OKB(rebuildExistingOffsetChain(Z_ARRVAL_P(offsetNativeTypes), trackedDimFetches.table(), varNativeType.raw(), nativeValueToWrite, nativeScope.raw(), linkNativeValues));

zend_string *varName = ahStringVariableName(var);
if (varName != NULL) {
Expand All @@ -4166,32 +4181,79 @@ class AssignHandler
AH_SETB(scope, pt_mutating_scope_assign_expression(Z_OBJ_P(scope.raw()), Z_OBJ_P(var), valueToWrite.raw(), nativeValueToWrite.raw()));
}

for (uint32_t i = 0; i < linkCount; i++) {
zval *dimFetch = zend_hash_index_find(trackedDimFetches.table(), i);
if (Z_TYPE_P(dimFetch) == IS_NULL) {
continue;
}

AH_SETB(scope, pt_mutating_scope_assign_expression(Z_OBJ_P(scope.raw()), Z_OBJ_P(dimFetch), zend_hash_index_find(linkValues.table(), i), zend_hash_index_find(linkNativeValues.table(), i)));
}

return true;
}

/* the offset value type stack over all but the last offset, then
* setExistingOffsetValueType() back out from the outermost (twin
* 1758-1780, one flavour) */
[[nodiscard]] static bool rebuildExistingOffsetChain(HashTable *offsetTypes, zval *containerType, zv::Val &valueToWrite)
/* the offset value type stack over all but the last offset (a tracked
* link's from the scope), then setExistingOffsetValueType() back out
* from the outermost, collecting each link's new value (twin
* rebuildExistingOffsetChain(), one flavour) */
[[nodiscard]] static bool rebuildExistingOffsetChain(HashTable *offsetTypes, HashTable *trackedDimFetches, zval *containerType, zv::Val &valueToWrite, zval *scope, zv::Arr &linkValues)
{
uint32_t count = zend_hash_num_elements(offsetTypes);
zv::Arr stack = zv::Arr::create(count);
zv::Val offsetValueType = zv::Val::copyOf(zv::Ref(containerType));
stack.push(offsetValueType.ref());
for (uint32_t i = 0; i + 1 < count; i++) {
zval *offsetType = zend_hash_index_find(Z_ARRVAL_P(zend_hash_index_find(offsetTypes, i)), 0);
AH_SETB(offsetValueType, ahTypeOp(offsetValueType.raw(), PT_OP_GET_OFFSET_VALUE_TYPE, 1, offsetType));
zval *trackedDimFetch = zend_hash_index_find(trackedDimFetches, i);
if (Z_TYPE_P(trackedDimFetch) != IS_NULL) {
AH_SETB(offsetValueType, pt_mutating_scope_get_state_type(Z_OBJ_P(scope), Z_OBJ_P(trackedDimFetch)));
} else {
zval *offsetType = zend_hash_index_find(Z_ARRVAL_P(zend_hash_index_find(offsetTypes, i)), 0);
AH_SETB(offsetValueType, ahTypeOp(offsetValueType.raw(), PT_OP_GET_OFFSET_VALUE_TYPE, 1, offsetType));
}
stack.push(offsetValueType.ref());
}
for (uint32_t i = 0; i < count; i++) {
linkValues.push(zv::Val::null());
}
for (uint32_t i = count; i > 0; i--) {
zend_hash_index_update(linkValues.table(), i - 1, valueToWrite.raw());
Z_TRY_ADDREF_P(valueToWrite.raw());
zval *offsetType = zend_hash_index_find(Z_ARRVAL_P(zend_hash_index_find(offsetTypes, i - 1)), 0);
zval *container = zend_hash_index_find(stack.table(), i - 1);
zv::Args setArgs{offsetType, valueToWrite.raw()};
AH_SETB(valueToWrite, ahCall(container, PT_LC("setexistingoffsetvaluetype"), 2, setArgs));
AH_VALB(writtenValueType, ahCall(container, PT_LC("setexistingoffsetvaluetype"), 2, setArgs));
zend_long has = ahTypeOpTri(container, PT_OP_HAS_OFFSET_VALUE_TYPE, 1, offsetType);
AH_OKB(has >= 0);
if (has == PT_TRI_MAYBE) {
// unset() does not create a missing offset on its way
zend_long isConstantArray = ahTypeOpTri(container, PT_OP_IS_CONSTANT_ARRAY, 0, NULL);
AH_OKB(isConstantArray >= 0);
zv::Val absentValueType = zv::Val::copyOf(zv::Ref(container));
if (isConstantArray == PT_TRI_YES) {
AH_SETB(absentValueType, ahCall(container, PT_LC("unsetoffset"), 1, offsetType));
}
zv::Args unionArgs{absentValueType.raw(), writtenValueType.raw()};
AH_SETB(writtenValueType, pt_type_combinator_union(2, unionArgs));
}
valueToWrite = std::move(writtenValueType);
}
return true;
}

/* the ArrayDimFetch chain an ExistingArrayDimFetch chain stands for
* (twin toArrayDimFetch()) */
static zv::Val toArrayDimFetch(zval *dimFetch)
{
zval *var = AH_PROP(dimFetch, var);
if (ahIs(var, PT_CLASS_EXISTING_ARRAY_DIM_FETCH)) {
AH_VAL(converted, toArrayDimFetch(var));
return newArrayDimFetch(converted.raw(), AH_PROP(dimFetch, dim));
}

return newArrayDimFetch(var, AH_PROP(dimFetch, dim));
}

/* }}} */

/* {{{ the narrowing callbacks of an Assign (twin 313-551, 1844) */
Expand Down
Loading