Skip to content
26 changes: 23 additions & 3 deletions src/Analyser/ExprHandler/AssignHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -938,7 +938,7 @@

/**
* @param list<ArrayDimFetch> $dimFetchStack
* @param list<array{Type|null, ArrayDimFetch}> $offsetTypes
* @param non-empty-list<array{Type|null, ArrayDimFetch}> $offsetTypes
*
* @return array{Type, list<array{Expr, Type}>}
*/
Expand All @@ -947,10 +947,11 @@
$originalValueToWrite = $valueToWrite;

$offsetValueTypeStack = [$offsetValueType];
$generalizeOnWrite = $offsetTypes[array_key_last($offsetTypes)][0] !== null;
foreach (array_slice($offsetTypes, 0, -1) as [$offsetType, $dimFetch]) {
if ($offsetType === null) {
$offsetValueType = new ConstantArrayType([], []);

$generalizeOnWrite = false;
} else {
$has = $offsetValueType->hasOffsetValueType($offsetType);
if ($has->yes()) {
Expand All @@ -959,9 +960,11 @@
if (!$scope->hasExpressionType($dimFetch)->yes()) {
$offsetValueType = TypeCombinator::union($offsetValueType->getOffsetValueType($offsetType), new ConstantArrayType([], []));
} else {
$generalizeOnWrite = false;
$offsetValueType = $offsetValueType->getOffsetValueType($offsetType);
}
} else {
$generalizeOnWrite = false;
$offsetValueType = new ConstantArrayType([], []);
}
}
Expand Down Expand Up @@ -1010,7 +1013,24 @@
}

} else {
$valueToWrite = $offsetValueType->setOffsetValueType($offsetType, $valueToWrite, $i === 0);
// when $unionValues=false the array item-type will be replaced with $valueToWrite
// when $unionValues=true the existing array item-type will be union'ed with $valueToWrite -> type gets wider
$unionValues = false;
if ($i === 0) {
$unionValues = true;
} elseif (
$generalizeOnWrite
&& $i === count($offsetTypes) - 1
&&
(
$originalValueToWrite->isConstantScalarValue()->yes()

Check warning on line 1026 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": @@ @@ && $i === count($offsetTypes) - 1 && ( - $originalValueToWrite->isConstantScalarValue()->yes() + !$originalValueToWrite->isConstantScalarValue()->no() || !$offsetValueType->getIterableValueType()->isSuperTypeOf($valueToWrite)->yes() ) ) {

Check warning on line 1026 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": @@ @@ && $i === count($offsetTypes) - 1 && ( - $originalValueToWrite->isConstantScalarValue()->yes() + !$originalValueToWrite->isConstantScalarValue()->no() || !$offsetValueType->getIterableValueType()->isSuperTypeOf($valueToWrite)->yes() ) ) {
|| !$offsetValueType->getIterableValueType()->isSuperTypeOf($valueToWrite)->yes()

Check warning on line 1027 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": @@ @@ && ( $originalValueToWrite->isConstantScalarValue()->yes() - || !$offsetValueType->getIterableValueType()->isSuperTypeOf($valueToWrite)->yes() + || $offsetValueType->getIterableValueType()->isSuperTypeOf($valueToWrite)->no() ) ) { $unionValues = true;

Check warning on line 1027 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": @@ @@ && ( $originalValueToWrite->isConstantScalarValue()->yes() - || !$offsetValueType->getIterableValueType()->isSuperTypeOf($valueToWrite)->yes() + || $offsetValueType->getIterableValueType()->isSuperTypeOf($valueToWrite)->no() ) ) { $unionValues = true;
)
) {
$unionValues = true;
}

$valueToWrite = $offsetValueType->setOffsetValueType($offsetType, $valueToWrite, $unionValues);
}

if ($arrayDimFetch === null || !$offsetValueType->isList()->yes()) {
Expand Down
3 changes: 2 additions & 1 deletion tests/PHPStan/Analyser/nsrt/assign-nested-arrays.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,9 @@ public function doFoo(int $i)
$array = [];

$array[$i]['bar'] = 1;
$array[$i]['baz'] = 2;
assertType('non-empty-array<int, array{bar: 1}>', $array);

$array[$i]['baz'] = 2;
assertType('non-empty-array<int, array{bar: 1, baz: 2}>', $array);
}

Expand Down
44 changes: 44 additions & 0 deletions tests/PHPStan/Analyser/nsrt/bug-10089.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
<?php

namespace Bug10089;


use function PHPStan\Testing\assertType;

class Test
{

protected function create_matrix(int $size): array
{
$size = min(8, $size);
$matrix = [];
for ($i = 0; $i < $size; $i++) {
$matrix[] = array_fill(0, $size, 0);
}

// array<int<0, max>, non-empty-array<int, 0>>
assertType('list<non-empty-list<0>>', $matrix);

$matrix[$size - 1][8] = 3;

// non-empty-array<int, non-empty-array<int, 0|3>&hasOffsetValue(8, 3)>
assertType('non-empty-list<non-empty-array<int<0, max>, 0|3>>', $matrix);

for ($i = 0; $i <= $size; $i++) {
if ($matrix[$i][8] === 0) {
// ...
}
if ($matrix[8][$i] === 0) {
// ...
}
if ($matrix[$size - 1 - $i][8] === 0) {
// ...
}
}

return $matrix;
}

}


67 changes: 67 additions & 0 deletions tests/PHPStan/Analyser/nsrt/bug-13857.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
<?php

declare(strict_types = 1);

namespace Bug13857;

use function PHPStan\Testing\assertType;

/**
* @param array<int, array{state: string}> $array
*/
function test(array $array, int $id): void {
$array[$id]['state'] = 'foo';
// only one element was set to 'foo', not all of them.
assertType("non-empty-array<int, array{state: string}>", $array);
}

/**
* @param array<int, array{state?: string}> $array
*/
function testMaybe(array $array, int $id): void {
$array[$id]['state'] = 'foo';
// only one element was set to 'foo', not all of them.
assertType("non-empty-array<int, array{state?: string}>", $array);
}

/**
* @param array<int, array{state: string|bool}> $array
*/
function testUnionValue(array $array, int $id): void {
$array[$id]['state'] = 'foo';
// only one element was set to 'foo', not all of them.
assertType("non-empty-array<int, array{state: bool|string}>", $array);
}

/**
* @param array<int, array{state: string}|array{foo: int}> $array
*/
function testUnionArray(array $array, int $id): void {
$array[$id]['state'] = 'foo';
// only one element was set to 'foo', not all of them.
assertType("non-empty-array<int, non-empty-array{foo?: int, state?: string}>", $array);
}

/**
* @param array<int, array{state: string}|array{foo: int}> $array
*/
function testUnionArrayDifferentType(array $array, int $id): void {
$array[$id]['state'] = true;
assertType("non-empty-array<int, array{state: string}|non-empty-array{foo?: int, state?: true}>", $array);
}

/**
* @param array<int, array{state: 'foo'}> $array
*/
function testConstantArray(array $array, int $id): void {
$array[$id]['state'] = 'bar';
assertType("non-empty-array<int, array{state: 'bar'}|array{state: 'foo'}>", $array);
}

/**
* @param array<int, array{state: 'foo'}> $array
*/
function testConstantArrayNonScalarAssign(array $array, int $id, bool $b): void {
$array[$id]['state'] = $b;
assertType("non-empty-array<int, array{state: 'foo'}|array{state: bool}>", $array);
}
3 changes: 2 additions & 1 deletion tests/PHPStan/Rules/Arrays/data/bug-11679.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@ public function sayHello(int $index): bool
assertType('array<int, array{foo?: bool}>', $this->arr);
if (!isset($this->arr[$index]['foo'])) {
$this->arr[$index]['foo'] = true;
assertType('non-empty-array<int, array{foo: true}>', $this->arr);
assertType('non-empty-array<int, array{foo?: bool}>', $this->arr);
assertType('true', $this->arr[$index]['foo']);
}
assertType('array<int, array{foo?: bool}>', $this->arr);
return $this->arr[$index]['foo']; // PHPStan does not realize 'foo' is set
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1046,6 +1046,11 @@ public function testBug13282(): void
$this->analyse([__DIR__ . '/../../Analyser/nsrt/bug-13282.php'], []);
}

public function testBug10089(): void
{
$this->analyse([__DIR__ . '/../../Analyser/nsrt/bug-10089.php'], []);
}

public function testBug11609(): void
{
$this->analyse([__DIR__ . '/data/bug-11609.php'], [
Expand Down
Loading