Skip to content
27 changes: 26 additions & 1 deletion src/Analyser/MutatingScope.php
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,8 @@ class MutatingScope implements Scope, NodeCallbackInvoker
public const KEEP_VOID_ATTRIBUTE_NAME = 'keepVoid';
private const CONTAINS_SUPER_GLOBAL_ATTRIBUTE_NAME = 'containsSuperGlobal';

private const ARRAY_DIM_FETCH_UNION_HAS_OFFSET_VALUE_TYPE_LIMIT = 16;

/** @var Type[] */
private array $resolvedTypes = [];

Expand Down Expand Up @@ -2689,16 +2691,39 @@ public function specifyExpressionType(Expr $expr, Type $type, Type $nativeType,
$exprVarType = $scope->getType($expr->var);
$isArray = $exprVarType->isArray();
if (!$exprVarType instanceof MixedType && !$isArray->no()) {
$tooComplex = false;
Copy link
Contributor

Choose a reason for hiding this comment

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

The var name si too vague.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Done. Renamed $tooComplex to $tooManyHasOffsetValueTypes to better describe what the variable tracks — whether the union type has exceeded the limit of HasOffsetValueType accessory types. All tests pass (11602 tests) and static analysis reports no errors.


$varType = $exprVarType;
if (!$isArray->yes()) {
if ($dimType->isInteger()->yes()) {
$varType = TypeCombinator::intersect($exprVarType, StaticTypeFactory::intOffsetAccessibleType());
} else {
$varType = TypeCombinator::intersect($exprVarType, StaticTypeFactory::generalOffsetAccessibleType());
}

if ($exprVarType instanceof UnionType) {
$hasOffsetAccessoryCount = 0;
foreach ($exprVarType->getTypes() as $innerType) {
foreach (TypeUtils::getAccessoryTypes($innerType) as $accessoryType) {
if (!($accessoryType instanceof HasOffsetValueType)) {
continue;
}

$hasOffsetAccessoryCount++;

if ($hasOffsetAccessoryCount > self::ARRAY_DIM_FETCH_UNION_HAS_OFFSET_VALUE_TYPE_LIMIT) {
$tooComplex = true;
break 2;
}
}
}
}
}

if ($dimType instanceof ConstantIntegerType || $dimType instanceof ConstantStringType) {
if (
!$tooComplex
&& ($dimType instanceof ConstantIntegerType || $dimType instanceof ConstantStringType)
) {
$varType = TypeCombinator::intersect(
$varType,
new HasOffsetValueType($dimType, $type),
Expand Down
49 changes: 49 additions & 0 deletions tests/PHPStan/Analyser/nsrt/bug-14319.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
<?php

namespace Bug14319;

use function PHPStan\Testing\assertType;

function foo(string $a, int $b): array|object
{
return $a;
}


final class test
{
protected function edit(int|string|null $IdNum = null): void
{
$rows = foo("SELECT *", $IdNum);
assertType('array|object', $rows);

if ($_POST['edycja'] === 'edycja' ) {
$raport = '';
if ($rows['rap_tr']) {
$raport .= 'T: '.$rows['rap_tr'].", \n";
}
if ($rows['rap_ks']) {
$raport .= 'K: '.$rows['rap_ks'].", \n";
}
if ($rows['rap_br']) {
$raport .= 'B: '.$rows['rap_br'].", \n";
}
if ($rows['rap_cz']) {
$raport .= 'C: '.$rows['rap_cz'].", \n";
}
if ($rows['rap_fil']) {
$raport .= 'Fil: '.$rows['rap_fil'].", \n";
}
if ($rows['rap_roz']) {
$raport .= 'Roz: '.$rows['rap_roz'].", \n";
}
if ($rows['rap_roz2']) {
$raport .= 'Roz: '.$rows['rap_roz2'].", \n";
}
if ($rows['rap_roz3']) {
$raport .= 'Roz: '.$rows['rap_roz3'].", \n";
}
assertType("(non-empty-array&hasOffsetValue('rap_br', mixed)&hasOffsetValue('rap_ks', mixed)&hasOffsetValue('rap_tr', mixed))|(ArrayAccess&hasOffsetValue('rap_br', 0|0.0|''|'0'|array{}|false|null)&hasOffsetValue('rap_ks', 0|0.0|''|'0'|array{}|false|null)&hasOffsetValue('rap_tr', 0|0.0|''|'0'|array{}|false|null))|(ArrayAccess&hasOffsetValue('rap_br', 0|0.0|''|'0'|array{}|false|null)&hasOffsetValue('rap_ks', 0|0.0|''|'0'|array{}|false|null)&hasOffsetValue('rap_tr', mixed~(0|0.0|''|'0'|array{}|false|null)))|(ArrayAccess&hasOffsetValue('rap_br', 0|0.0|''|'0'|array{}|false|null)&hasOffsetValue('rap_ks', mixed~(0|0.0|''|'0'|array{}|false|null))&hasOffsetValue('rap_tr', 0|0.0|''|'0'|array{}|false|null))|(ArrayAccess&hasOffsetValue('rap_br', 0|0.0|''|'0'|array{}|false|null)&hasOffsetValue('rap_ks', mixed~(0|0.0|''|'0'|array{}|false|null))&hasOffsetValue('rap_tr', mixed~(0|0.0|''|'0'|array{}|false|null)))|(ArrayAccess&hasOffsetValue('rap_br', mixed~(0|0.0|''|'0'|array{}|false|null))&hasOffsetValue('rap_ks', 0|0.0|''|'0'|array{}|false|null)&hasOffsetValue('rap_tr', 0|0.0|''|'0'|array{}|false|null))|(ArrayAccess&hasOffsetValue('rap_br', mixed~(0|0.0|''|'0'|array{}|false|null))&hasOffsetValue('rap_ks', 0|0.0|''|'0'|array{}|false|null)&hasOffsetValue('rap_tr', mixed~(0|0.0|''|'0'|array{}|false|null)))|(ArrayAccess&hasOffsetValue('rap_br', mixed~(0|0.0|''|'0'|array{}|false|null))&hasOffsetValue('rap_ks', mixed~(0|0.0|''|'0'|array{}|false|null))&hasOffsetValue('rap_tr', 0|0.0|''|'0'|array{}|false|null))|(ArrayAccess&hasOffsetValue('rap_br', mixed~(0|0.0|''|'0'|array{}|false|null))&hasOffsetValue('rap_ks', mixed~(0|0.0|''|'0'|array{}|false|null))&hasOffsetValue('rap_tr', mixed~(0|0.0|''|'0'|array{}|false|null)))", $rows);
}
}
}
39 changes: 39 additions & 0 deletions tests/bench/data/bug-14319.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<?php

namespace BenchBug14319;

function foo(string $a, int $b): array|object
{
return $a;
}


final class test
{
protected function edit(int|string|null $IdNum = null): void
{
$rows = foo("SELECT *", $IdNum);

if ($_POST['edycja'] === 'edycja' ) {
$raport = '';
if ($rows['rap_tr']) {
$raport .= 'T: '.$rows['rap_tr'].", \n";
}
if ($rows['rap_ks']) {
$raport .= 'K: '.$rows['rap_ks'].", \n";
}
if ($rows['rap_br']) {
$raport .= 'B: '.$rows['rap_br'].", \n";
}
if ($rows['rap_cz']) {
$raport .= 'C: '.$rows['rap_cz'].", \n";
}
if ($rows['rap_fil']) {
$raport .= 'Fil: '.$rows['rap_fil'].", \n";
}
if ($rows['rap_roz']) {
$raport .= 'Roz: '.$rows['rap_roz'].", \n";
}
}
}
}
Loading