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
2 changes: 2 additions & 0 deletions src/Analyser/ExprHandler/AssignHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -1057,11 +1057,13 @@
}

$arrayDimFetch = $dimFetchStack[$i] ?? null;
$isContainerLevel = $i + 1 < count($dimFetchStack);
if (
$offsetType !== null
&& $arrayDimFetch !== null
&& $scope->hasExpressionType($arrayDimFetch)->yes()
&& !$offsetValueType->hasOffsetValueType($offsetType)->no()
&& (!$isContainerLevel || !$scope->getType($arrayDimFetch)->hasOffsetValueType($offsetType)->no())

Check warning on line 1066 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": @@ @@ && $arrayDimFetch !== null && $scope->hasExpressionType($arrayDimFetch)->yes() && !$offsetValueType->hasOffsetValueType($offsetType)->no() - && (!$isContainerLevel || !$scope->getType($arrayDimFetch)->hasOffsetValueType($offsetType)->no()) + && (!$isContainerLevel || $scope->getType($arrayDimFetch)->hasOffsetValueType($offsetType)->yes()) ) { $hasOffsetType = null; if ($offsetType instanceof ConstantStringType || $offsetType instanceof ConstantIntegerType) {

Check warning on line 1066 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": @@ @@ && $arrayDimFetch !== null && $scope->hasExpressionType($arrayDimFetch)->yes() && !$offsetValueType->hasOffsetValueType($offsetType)->no() - && (!$isContainerLevel || !$scope->getType($arrayDimFetch)->hasOffsetValueType($offsetType)->no()) + && (!$isContainerLevel || $scope->getType($arrayDimFetch)->hasOffsetValueType($offsetType)->yes()) ) { $hasOffsetType = null; if ($offsetType instanceof ConstantStringType || $offsetType instanceof ConstantIntegerType) {
) {
$hasOffsetType = null;
if ($offsetType instanceof ConstantStringType || $offsetType instanceof ConstantIntegerType) {
Expand Down
2 changes: 1 addition & 1 deletion src/Type/IntersectionType.php
Original file line number Diff line number Diff line change
Expand Up @@ -987,7 +987,7 @@ public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $uni
if (
$this->isList()->yes()
&& $offsetType !== null
&& $offsetType->toArrayKey()->isInteger()->yes()
&& $this->getIterableKeyType()->isSuperTypeOf($offsetType)->yes()
&& $this->getIterableValueType()->isArray()->yes()
) {
$result = TypeCombinator::intersect($result, new AccessoryArrayListType());
Expand Down
2 changes: 1 addition & 1 deletion tests/PHPStan/Analyser/nsrt/bug-10089.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ protected function create_matrix(int $size): array
$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);
assertType('non-empty-array<int, non-empty-array<int<0, max>, 0|3>>', $matrix);

for ($i = 0; $i <= $size; $i++) {
if ($matrix[$i][8] === 0) {
Expand Down
156 changes: 156 additions & 0 deletions tests/PHPStan/Analyser/nsrt/bug-14336.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
<?php

namespace Bug14336;

use function PHPStan\Testing\assertType;

/**
* Actual reproducer from the issue: nested dim fetch with arbitrary int key
* after resetting array element to [].
*
* @param array<string, list<array{xmlNamespace: string, namespace: string, name: string}>> $xsdFiles
* @param array<string, list<array{xmlNamespace: string, namespace: string, name: string}>> $groupedByNamespace
* @param array<string, list<string>> $extraNamespaces
*/
function testIssueReproducer(array $xsdFiles, array $groupedByNamespace, array $extraNamespaces, int $int): void
{
foreach ($extraNamespaces as $mergedNamespace) {
if (count($mergedNamespace) < 2) {
continue;
}

$targetNamespace = end($mergedNamespace);
if (!isset($groupedByNamespace[$targetNamespace])) {
continue;
}
$xmlNamespace = $groupedByNamespace[$targetNamespace][0]['xmlNamespace'];

assertType('string', $xmlNamespace);
assertType('non-empty-list<string>&hasOffsetValue(1, string)', $mergedNamespace);

$xsdFiles[$xmlNamespace] = [];
foreach ($mergedNamespace as $namespace) {
foreach ($groupedByNamespace[$namespace] ?? [] as $viewHelper) {
$xsdFiles[$xmlNamespace][$int] = $viewHelper;
}
}
// After assigning any int, $xsdFiles[$xmlNamespace] should NOT be a list
assertType('array<int, array{xmlNamespace: string, namespace: string, name: string}>', $xsdFiles[$xmlNamespace]);
$xsdFiles[$xmlNamespace] = array_values($xsdFiles[$xmlNamespace]);
}
}

/**
* Simplified: nested dim fetch after reset with arbitrary int key.
*
* @param array<string, list<array{name: string}>> $arr
*/
function testNestedDimFetchAfterReset(array $arr, int $int, string $key): void
{
$arr[$key] = [];
$arr[$key][$int] = ['name' => 'test'];
assertType("non-empty-array<int, array{name: string}>", $arr[$key]);
}

/**
* Assigning with arbitrary int key in a loop should degrade list to array.
*
* @param list<array{abc: string}> $list
* @param array<int, int> $intMap
*/
function testAssignAnyIntInLoop(array $list, array $intMap): void
{
foreach ($intMap as $intKey => $intValue) {
$list[$intKey] = ['abc' => 'def'];
}
assertType("array<int, array{abc: string}>", $list);
}

/**
* @param list<string> $list
* @param int $intKey
*/
function testAssignAnyIntOutsideLoop(array $list, int $intKey): void
{
$list[$intKey] = 'foo';
assertType("non-empty-array<int, string>", $list);
}

/**
* Safe patterns should still preserve list.
*
* @param list<string> $list
*/
function testKeepListWithAppend(array $list): void
{
$list[] = 'foo';
assertType("non-empty-list<string>", $list);
}

/**
* @param list<string> $list
*/
function testKeepListWithConstantZero(array $list): void
{
$list[0] = 'foo';
assertType("non-empty-list<string>&hasOffsetValue(0, 'foo')", $list);
}

/**
* Nested array assignment in loop should keep outer list when key comes from iteration.
*
* @param list<array<string, string>> $list
*/
function testNestedAssignKeepsList(array $list): void
{
foreach ($list as $k => $v) {
$list[$k]['abc'] = 'world';
}
assertType("list<non-empty-array<string, string>&hasOffsetValue('abc', 'world')>", $list);
}

/**
* @param list<list<string>> $list
* @param int $intKey
*/
function testNestedListAssignWithAnyInt(array $list, int $intKey): void
{
$list[$intKey] = ['foo'];
assertType("non-empty-array<int, list<string>>", $list);
}

/**
* Assigning with negative int key should also degrade list.
*
* @param list<string> $list
* @param int<min, -1> $negativeKey
*/
function testAssignNegativeInt(array $list, int $negativeKey): void
{
$list[$negativeKey] = 'foo';
assertType("non-empty-array<int, string>", $list);
}

/**
* Assigning with int<0, max> should still keep list (valid range).
*
* @param list<array<string>> $list
* @param int<0, max> $nonNegativeKey
*/
function testAssignNonNegativeIntWithArrayValue(array $list, int $nonNegativeKey): void
{
$list[$nonNegativeKey] = ['foo'];
assertType("non-empty-list<array<string>>", $list);
}

/**
* Direct scalar assignment with int<0, max> key.
*
* @param list<string> $list
* @param int<0, max> $nonNegativeKey
*/
function testAssignNonNegativeIntWithScalarValue(array $list, int $nonNegativeKey): void
{
$list[$nonNegativeKey] = 'foo';
assertType("non-empty-array<int<0, max>, string>", $list);
}
Loading