Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
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
9 changes: 8 additions & 1 deletion src/Analyser/MutatingScope.php
Original file line number Diff line number Diff line change
Expand Up @@ -4308,7 +4308,14 @@ public function specifyExpressionType(Expr $expr, Type $type, Type $nativeType,
}

$scope = $this;
if ($expr instanceof Expr\ArrayDimFetch && $expr->dim !== null) {
if (
$expr instanceof Expr\ArrayDimFetch
&& $expr->dim !== null
&& !$expr->dim instanceof Expr\PreInc
&& !$expr->dim instanceof Expr\PreDec
&& !$expr->dim instanceof Expr\PostDec
&& !$expr->dim instanceof Expr\PostInc
Comment on lines +4314 to +4317
Copy link
Contributor Author

@staabm staabm Sep 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

required change to not regress existing tests like

<?php

$anotherIndex = 0;
$postIncArray = [];
$postIncArray[$anotherIndex++] = $anotherIndex++;
\PHPStan\Testing\assertType('array{1}', $postIncArray);

) {
$dimType = $scope->getType($expr->dim)->toArrayKey();
if ($dimType->isInteger()->yes() || $dimType->isString()->yes()) {
$exprVarType = $scope->getType($expr->var);
Expand Down
77 changes: 55 additions & 22 deletions src/Analyser/NodeScopeResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -5706,10 +5706,10 @@ private function processAssignVar(
$offsetValueType = $varType;
$offsetNativeValueType = $varNativeType;

$valueToWrite = $this->produceArrayDimFetchAssignValueToWrite($dimFetchStack, $offsetTypes, $offsetValueType, $valueToWrite, $scope);
[$valueToWrite, $additionalExpressions] = $this->produceArrayDimFetchAssignValueToWrite($dimFetchStack, $offsetTypes, $offsetValueType, $valueToWrite, $scope);

if (!$offsetValueType->equals($offsetNativeValueType) || !$valueToWrite->equals($nativeValueToWrite)) {
$nativeValueToWrite = $this->produceArrayDimFetchAssignValueToWrite($dimFetchStack, $offsetNativeTypes, $offsetNativeValueType, $nativeValueToWrite, $scope);
[$nativeValueToWrite, $additionalNativeExpressions] = $this->produceArrayDimFetchAssignValueToWrite($dimFetchStack, $offsetNativeTypes, $offsetNativeValueType, $nativeValueToWrite, $scope);
} else {
$rewritten = false;
foreach ($offsetTypes as $i => $offsetType) {
Expand All @@ -5728,7 +5728,7 @@ private function processAssignVar(
continue;
}

$nativeValueToWrite = $this->produceArrayDimFetchAssignValueToWrite($dimFetchStack, $offsetNativeTypes, $offsetNativeValueType, $nativeValueToWrite, $scope);
[$nativeValueToWrite] = $this->produceArrayDimFetchAssignValueToWrite($dimFetchStack, $offsetNativeTypes, $offsetNativeValueType, $nativeValueToWrite, $scope);
$rewritten = true;
break;
}
Expand Down Expand Up @@ -5781,6 +5781,16 @@ private function processAssignVar(
}
}

foreach ($additionalExpressions as $k => $additionalExpression) {
[$expr, $type] = $additionalExpression;
$nativeType = $type;
if (isset($additionalNativeExpressions[$k])) {
[, $nativeType] = $additionalNativeExpressions[$k];
}

$scope = $scope->assignExpression($expr, $type, $nativeType);
}

if (!$varType->isArray()->yes() && !(new ObjectType(ArrayAccess::class))->isSuperTypeOf($varType)->no()) {
$throwPoints = array_merge($throwPoints, $this->processExprNode(
$stmt,
Expand Down Expand Up @@ -6134,9 +6144,13 @@ private function isImplicitArrayCreation(array $dimFetchStack, Scope $scope): Tr
/**
* @param list<ArrayDimFetch> $dimFetchStack
* @param list<Type|null> $offsetTypes
*
* @return array{Type, list<array{Expr, Type}>}
*/
private function produceArrayDimFetchAssignValueToWrite(array $dimFetchStack, array $offsetTypes, Type $offsetValueType, Type $valueToWrite, Scope $scope): Type
private function produceArrayDimFetchAssignValueToWrite(array $dimFetchStack, array $offsetTypes, Type $offsetValueType, Type $valueToWrite, Scope $scope): array
{
$originalValueToWrite = $valueToWrite;

$offsetValueTypeStack = [$offsetValueType];
foreach (array_slice($offsetTypes, 0, -1) as $offsetType) {
if ($offsetType === null) {
Expand Down Expand Up @@ -6204,28 +6218,47 @@ private function produceArrayDimFetchAssignValueToWrite(array $dimFetchStack, ar
continue;
}

if ($scope->hasExpressionType($arrayDimFetch)->yes()) { // keep list for $list[$index] assignments
if (!$arrayDimFetch->dim instanceof BinaryOp\Plus) {
Copy link
Contributor Author

@staabm staabm Sep 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ondrejmirtes why is the issue related to Plus? there is no + contained in the related code example?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ohh.. the Plus is related to the list-inference logic below.

so it was a bug lingering for longer already :)

continue;
}

if ( // keep list for $list[$index + 1] assignments
$arrayDimFetch->dim->right instanceof Variable
&& $arrayDimFetch->dim->left instanceof Node\Scalar\Int_
&& $arrayDimFetch->dim->left->value === 1
&& $scope->hasExpressionType(new ArrayDimFetch($arrayDimFetch->var, $arrayDimFetch->dim->right))->yes()
) {
$valueToWrite = TypeCombinator::intersect($valueToWrite, new AccessoryArrayListType());
} elseif ($arrayDimFetch->dim instanceof BinaryOp\Plus) {
if ( // keep list for $list[$index + 1] assignments
$arrayDimFetch->dim->right instanceof Variable
&& $arrayDimFetch->dim->left instanceof Node\Scalar\Int_
&& $arrayDimFetch->dim->left->value === 1
&& $scope->hasExpressionType(new ArrayDimFetch($arrayDimFetch->var, $arrayDimFetch->dim->right))->yes()
) {
$valueToWrite = TypeCombinator::intersect($valueToWrite, new AccessoryArrayListType());
} elseif ( // keep list for $list[1 + $index] assignments
$arrayDimFetch->dim->left instanceof Variable
&& $arrayDimFetch->dim->right instanceof Node\Scalar\Int_
&& $arrayDimFetch->dim->right->value === 1
&& $scope->hasExpressionType(new ArrayDimFetch($arrayDimFetch->var, $arrayDimFetch->dim->left))->yes()
) {
$valueToWrite = TypeCombinator::intersect($valueToWrite, new AccessoryArrayListType());
}
} elseif ( // keep list for $list[1 + $index] assignments
$arrayDimFetch->dim->left instanceof Variable
&& $arrayDimFetch->dim->right instanceof Node\Scalar\Int_
&& $arrayDimFetch->dim->right->value === 1
&& $scope->hasExpressionType(new ArrayDimFetch($arrayDimFetch->var, $arrayDimFetch->dim->left))->yes()
) {
$valueToWrite = TypeCombinator::intersect($valueToWrite, new AccessoryArrayListType());
}
}

$additionalExpressions = [];
$offsetValueType = $valueToWrite;
$lastDimKey = array_key_last($dimFetchStack);
foreach ($dimFetchStack as $key => $dimFetch) {
if ($dimFetch->dim === null) {
$additionalExpressions = [];
break;
}

if ($key === $lastDimKey) {
$offsetValueType = $originalValueToWrite;
} else {
$offsetType = $scope->getType($dimFetch->dim);
$offsetValueType = $offsetValueType->getOffsetValueType($offsetType);
}

$additionalExpressions[] = [$dimFetch, $offsetValueType];
}

return $valueToWrite;
return [$valueToWrite, $additionalExpressions];
}

private function unwrapAssign(Expr $expr): Expr
Expand Down
1 change: 1 addition & 0 deletions tests/PHPStan/Analyser/NodeScopeResolverTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ private static function findTestFiles(): iterable
yield __DIR__ . '/../Rules/Generics/data/bug-3769.php';
yield __DIR__ . '/../Rules/Generics/data/bug-6301.php';
yield __DIR__ . '/../Rules/PhpDoc/data/bug-4643.php';
yield __DIR__ . '/../Rules/Arrays/data/bug-13538.php';

if (PHP_VERSION_ID >= 80000) {
yield __DIR__ . '/../Rules/Comparison/data/bug-4857.php';
Expand Down
21 changes: 21 additions & 0 deletions tests/PHPStan/Analyser/nsrt/bug-13039.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php declare(strict_types=1);

namespace Bug13039;

use function PHPStan\Testing\assertType;

function doFoo() {
/** @var list<array<string, mixed>> */
$transactions = [];

assertType('list<array<string, mixed>>', $transactions);

foreach (array_keys($transactions) as $k) {
$transactions[$k]['Shares'] = [];
$transactions[$k]['Shares']['Projects'] = [];
$transactions[$k]['Shares']['People'] = [];
}

assertType('list<array<string, mixed>>', $transactions);
}

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

namespace Bug13214;

use function PHPStan\Testing\assertType;
use stdClass;

class HelloWorld
{
/**
* @param ArrayAccess<int, ?object> $array
*/
public function sayHello(ArrayAccess $array): void
{
$child = new stdClass();

assert($array[1] === null);

assertType('null', $array[1]);

$array[1] = $child;

assertType(stdClass::class, $array[1]);
}

/**
* @param array<int, ?object> $array
*/
public function sayHelloArray(array $array): void
{
$child = new stdClass();

assert(($array[1] ?? null) === null);

assertType('object|null', $array[1]);

$array[1] = $child;

assertType(stdClass::class, $array[1]);
}
}
14 changes: 14 additions & 0 deletions tests/PHPStan/Levels/data/arrayAccess.php
Original file line number Diff line number Diff line change
Expand Up @@ -44,3 +44,17 @@ public function doLorem(
}

}

/**
* @return mixed[]
*/
function bug12931():array {
/** @var array<string, array<string, int>> $data */
$data = [];
$data['attr'] = [];
$data['attr']['first'] = 1;
$data['attr']['second'] = 2;
$data['attr']['third'] = 3;

return $data;
}
Original file line number Diff line number Diff line change
Expand Up @@ -1007,4 +1007,28 @@ public function testBug12926(): void
$this->analyse([__DIR__ . '/data/bug-12926.php'], []);
}

public function testBug13538(): void
{
$this->reportPossiblyNonexistentConstantArrayOffset = true;
$this->reportPossiblyNonexistentGeneralArrayOffset = true;

$this->analyse([__DIR__ . '/data/bug-13538.php'], [
[
"Offset int might not exist on non-empty-array<int, ''>.",
13,
],
[
"Offset int might not exist on non-empty-array<int, ''>.",
17,
],
]);
}

public function testBug12805(): void
{
$this->reportPossiblyNonexistentGeneralArrayOffset = true;

$this->analyse([__DIR__ . '/data/bug-12805.php'], []);
}

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

namespace Bug12805;

/**
* @param array<string, array{ rtx?: int }> $operations
* @return array<string, array{ rtx: int }>
*/
function bug(array $operations): array {
$base = [];

foreach ($operations as $operationName => $operation) {
if (!isset($base[$operationName])) {
$base[$operationName] = [];
}
if (!isset($base[$operationName]['rtx'])) {
$base[$operationName]['rtx'] = 0;
}
$base[$operationName]['rtx'] += $operation['rtx'] ?? 0;
}

return $base;
}
61 changes: 61 additions & 0 deletions tests/PHPStan/Rules/Arrays/data/bug-13538.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
<?php

namespace Bug13538;

use LogicException;
use function PHPStan\Testing\assertType;

/** @param list<string> $arr */
function doFoo(array $arr, int $i, int $i2): void
{
$logs = [];
$logs[$i] = '';
echo $logs[$i2];

assertType("non-empty-array<int, ''>", $logs);
assertType("''", $logs[$i]);
assertType("''", $logs[$i2]); // could be mixed

foreach ($arr as $value) {
echo $logs[$i];

assertType("non-empty-array<int, ''>", $logs);
assertType("''", $logs[$i]);
}
}

/** @param list<string> $arr */
function doFooBar(array $arr): void
{
if (!defined('LOG_DIR')) {
throw new LogicException();
}

$logs = [];
$logs[LOG_DIR] = '';

assertType("non-empty-array<''>", $logs);
assertType("''", $logs[LOG_DIR]);

foreach ($arr as $value) {
echo $logs[LOG_DIR];

assertType("non-empty-array<''>", $logs);
assertType("''", $logs[LOG_DIR]);
}
}

function doBar(array $arr, int $i, string $s): void
{
$logs = [];
$logs[$i][$s] = '';
assertType("non-empty-array<int, non-empty-array<string, ''>>", $logs);
assertType("non-empty-array<string, ''>", $logs[$i]);
assertType("''", $logs[$i][$s]);
foreach ($arr as $value) {
assertType("non-empty-array<int, non-empty-array<string, ''>>", $logs);
assertType("non-empty-array<string, ''>", $logs[$i]);
assertType("''", $logs[$i][$s]);
echo $logs[$i][$s];
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -1121,4 +1121,10 @@ public function testBug7773(): void
$this->analyse([__DIR__ . '/data/bug-7773.php'], []);
}

public function testPr4375(): void
{
$this->treatPhpDocTypesAsCertain = true;
$this->analyse([__DIR__ . '/data/pr-4375.php'], []);
}

}
27 changes: 27 additions & 0 deletions tests/PHPStan/Rules/Comparison/data/pr-4375.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?php

namespace PR4375;

final class Foo
{
public function processNode(): array
{
$methods = [];
foreach ($this->get() as $collected) {
foreach ($collected as [$className, $methodName, $classDisplayName]) {
$className = strtolower($className);

if (!array_key_exists($className, $methods)) {
$methods[$className] = [];
}
$methods[$className][strtolower($methodName)] = $classDisplayName . '::' . $methodName;
}
}

return [];
}

private function get(): array {
return [];
}
}
11 changes: 11 additions & 0 deletions tests/PHPStan/Rules/Methods/ReturnTypeRuleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -1265,4 +1265,15 @@ public function testBug7225(): void
$this->analyse([__DIR__ . '/data/bug-7225.php'], []);
}

public function testDeepDimFetch(): void
{
$this->analyse([__DIR__ . '/data/deep-dim-fetch.php'], []);
}

#[RequiresPhp('>= 8.0')]
public function testBug9494(): void
{
$this->analyse([__DIR__ . '/data/bug-9494.php'], []);
}

}
Loading
Loading