Skip to content
101 changes: 101 additions & 0 deletions src/Analyser/NodeScopeResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,7 @@ class NodeScopeResolver

private const LOOP_SCOPE_ITERATIONS = 3;
private const GENERALIZE_AFTER_ITERATION = 1;
private const FOREACH_UNROLL_LIMIT = 8;

/** @var array<string, true> filePath(string) => bool(true) */
private array $analysedFiles = [];
Expand Down Expand Up @@ -1383,6 +1384,106 @@ public function processStmtNode(
// get types from finalScope, but don't create new variables
}

if (
$context->isTopLevel()
&& $isIterableAtLeastOnce->yes()
&& count($breakExitPoints) === 0
&& !$stmt->byRef
&& $exprType->isConstantArray()->yes()
&& $stmt->valueVar instanceof Variable && is_string($stmt->valueVar->name)
&& ($stmt->keyVar === null || ($stmt->keyVar instanceof Variable && is_string($stmt->keyVar->name)))
) {
$constantArraysForUnroll = $exprType->getConstantArrays();
if (
count($constantArraysForUnroll) === 1
&& count($constantArraysForUnroll[0]->getOptionalKeys()) === 0
&& ($unrollKeyCount = count($constantArraysForUnroll[0]->getKeyTypes())) > 0
&& $unrollKeyCount <= self::FOREACH_UNROLL_LIMIT
) {
$unrolledScope = $scope;
$unrollSucceeded = true;
foreach ($constantArraysForUnroll[0]->getKeyTypes() as $i => $keyType) {
$valueType = $constantArraysForUnroll[0]->getValueTypes()[$i];

$iterScope = $unrolledScope->assignVariable(
$stmt->valueVar->name,
$valueType,
$valueType,
TrinaryLogic::createYes(),
);
if ($stmt->keyVar instanceof Variable && is_string($stmt->keyVar->name)) {
$iterScope = $iterScope->assignVariable(
$stmt->keyVar->name,
$keyType,
$keyType,
TrinaryLogic::createYes(),
);
}

$iterStorage = $storage->duplicate();
$iterResult = $this->processStmtNodesInternal(
$stmt, $stmt->stmts, $iterScope, $iterStorage,
new NoopNodeCallback(), $context->enterDeep(),
)->filterOutLoopExitPoints();

$unrolledScope = $iterResult->getScope();
foreach ($iterResult->getExitPointsByType(Continue_::class) as $continueExitPoint) {
$unrolledScope = $unrolledScope->mergeWith($continueExitPoint->getScope());
}

if (
count($iterResult->getExitPointsByType(Break_::class)) > 0
|| $iterResult->isAlwaysTerminating()
) {
$unrollSucceeded = false;
break;
}
}

if ($unrollSucceeded) {
foreach ($unrolledScope->expressionTypes as $exprString => $holder) {
if (!str_starts_with($exprString, '$') || str_contains($exprString, '[') || str_contains($exprString, '>')) {
continue;
}
if (!$holder->getCertainty()->yes()) {
continue;
}
$unrolledType = $holder->getType();
if (!$unrolledType->isConstantArray()->yes()) {
continue;
}
if (!isset($finalScope->expressionTypes[$exprString])) {
continue;
}
$finalHolder = $finalScope->expressionTypes[$exprString];
$finalType = $finalHolder->getType();
if (!$finalType->isConstantArray()->yes()) {
continue;
}

$unrolledArrays = $unrolledType->getConstantArrays();
$finalArrays = $finalType->getConstantArrays();
if (
count($unrolledArrays) === 1
&& count($finalArrays) === 1
&& count($unrolledArrays[0]->getOptionalKeys()) < count($finalArrays[0]->getOptionalKeys())
&& count($unrolledArrays[0]->getKeyTypes()) === count($finalArrays[0]->getKeyTypes())
) {
$varName = substr($exprString, 1);
$varExpr = $holder->getExpr();
$nativeType = $unrolledScope->getNativeType($varExpr);
$finalScope = $finalScope->assignVariable(
$varName,
$unrolledType,
$nativeType,
TrinaryLogic::createYes(),
);
}
}
}
}
}

if (!$isIterableAtLeastOnce->no()) {
$throwPoints = array_merge($throwPoints, $finalScopeResult->getThrowPoints());
$impurePoints = array_merge($impurePoints, $finalScopeResult->getImpurePoints());
Expand Down
22 changes: 22 additions & 0 deletions src/Type/Constant/ConstantArrayTypeBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,7 @@ public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $opt
}
if (count($scalarTypes) > 0 && count($scalarTypes) < self::ARRAY_COUNT_LIMIT) {
$match = true;
$hasMatch = false;
$valueTypes = $this->valueTypes;
foreach ($scalarTypes as $scalarType) {
$offsetMatch = false;
Expand All @@ -273,6 +274,7 @@ public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $opt
}

if ($offsetMatch) {
$hasMatch = true;
continue;
}

Expand All @@ -283,6 +285,26 @@ public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $opt
$this->valueTypes = $valueTypes;
return;
}

if (!$hasMatch && count($this->keyTypes) > 0) {
foreach ($scalarTypes as $scalarType) {
$this->keyTypes[] = $scalarType;
$this->valueTypes[] = $valueType;
$this->optionalKeys[] = count($this->keyTypes) - 1;
}

$this->isList = TrinaryLogic::createNo();

if (
!$this->disableArrayDegradation
&& count($this->keyTypes) > self::ARRAY_COUNT_LIMIT
) {
$this->degradeToGeneralArray = true;
$this->oversized = true;
}

return;
}
}

$this->isList = TrinaryLogic::createNo();
Expand Down
4 changes: 2 additions & 2 deletions tests/PHPStan/Analyser/nsrt/array-fill-keys.php
Original file line number Diff line number Diff line change
Expand Up @@ -54,14 +54,14 @@ function withObjectKey() : array
function withUnionKeys(): void
{
$arr1 = ['foo', rand(0, 1) ? 'bar1' : 'bar2', 'baz'];
assertType("non-empty-array<'bar1'|'bar2'|'baz'|'foo', 'b'>", array_fill_keys($arr1, 'b'));
assertType("array{foo: 'b', bar1?: 'b', bar2?: 'b', baz: 'b'}", array_fill_keys($arr1, 'b'));

$arr2 = ['foo'];
if (rand(0, 1)) {
$arr2[] = 'bar';
}
$arr2[] = 'baz';
assertType("non-empty-array<'bar'|'baz'|'foo', 'b'>", array_fill_keys($arr2, 'b'));
assertType("array{foo: 'b', bar?: 'b', baz?: 'b'}", array_fill_keys($arr2, 'b'));
}

function withOptionalKeys(): void
Expand Down
19 changes: 19 additions & 0 deletions tests/PHPStan/Analyser/nsrt/bug-12665.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?php declare(strict_types = 1);

namespace Bug12665;

use function PHPStan\Testing\assertType;

class Broken
{
/** @return array{a: string, b: int, c: int} */
public function break(string $s, int $i): array
{
$array = ['a' => $s];
foreach (['b', 'c'] as $letter) {
$array[$letter] = $i;
}
assertType('array{a: string, b: int, c: int}', $array);
return $array;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?php

namespace SetConstantUnionOffsetOnConstantArray;

use function PHPStan\Testing\assertType;

class Foo
{

/**
* @param array{foo: int} $a
*/
public function doFoo(array $a): void
{
$k = rand(0, 1) ? 'a' : 'b';
$a[$k] = 256;
assertType('array{foo: int, a?: 256, b?: 256}', $a);
}

}
Loading