Skip to content
Closed
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
1 change: 1 addition & 0 deletions src/Analyser/ExprHandler/AssignHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -992,6 +992,7 @@
&& $arrayDimFetch !== null
&& $scope->hasExpressionType($arrayDimFetch)->yes()
&& !$offsetValueType->hasOffsetValueType($offsetType)->no()
&& (!$offsetValueType->isList()->yes() || IntegerRangeType::fromInterval(0, null)->isSuperTypeOf($offsetType)->yes())

Check warning on line 995 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() - && (!$offsetValueType->isList()->yes() || IntegerRangeType::fromInterval(0, null)->isSuperTypeOf($offsetType)->yes()) + && ($offsetValueType->isList()->no() || IntegerRangeType::fromInterval(0, null)->isSuperTypeOf($offsetType)->yes()) ) { $hasOffsetType = null; if ($offsetType instanceof ConstantStringType || $offsetType instanceof ConstantIntegerType) {

Check warning on line 995 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() - && (!$offsetValueType->isList()->yes() || IntegerRangeType::fromInterval(0, null)->isSuperTypeOf($offsetType)->yes()) + && ($offsetValueType->isList()->no() || IntegerRangeType::fromInterval(0, null)->isSuperTypeOf($offsetType)->yes()) ) { $hasOffsetType = null; if ($offsetType instanceof ConstantStringType || $offsetType instanceof ConstantIntegerType) {
) {
$hasOffsetType = null;
if ($offsetType instanceof ConstantStringType || $offsetType instanceof ConstantIntegerType) {
Expand Down
4 changes: 3 additions & 1 deletion src/Type/IntersectionType.php
Original file line number Diff line number Diff line change
Expand Up @@ -981,7 +981,9 @@ public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $uni
}
}

if ($this->isList()->yes() && $this->getIterableValueType()->isArray()->yes()) {
if ($this->isList()->yes() && $this->getIterableValueType()->isArray()->yes()
&& ($offsetType === null || IntegerRangeType::fromInterval(0, null)->isSuperTypeOf($offsetType)->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 @@ -22,7 +22,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
2 changes: 1 addition & 1 deletion tests/PHPStan/Analyser/nsrt/bug-13629.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ function test(array $xsdFiles, array $groupedByNamespace, array $extraNamespaces
}
}
// After assigning with string keys ($viewHelper['name']), $xsdFiles[$xmlNamespace] should NOT be a list
assertType('array<int<0, max>, array{xmlNamespace: string, namespace: string, name: string}>', $xsdFiles[$xmlNamespace]);
assertType('array<int<0, max>|string, array{xmlNamespace: string, namespace: string, name: string}>', $xsdFiles[$xmlNamespace]);
$xsdFiles[$xmlNamespace] = array_values($xsdFiles[$xmlNamespace]);
}
}
2 changes: 1 addition & 1 deletion tests/PHPStan/Analyser/nsrt/bug-14245.php
Original file line number Diff line number Diff line change
Expand Up @@ -144,5 +144,5 @@ function ArrayKeyExistsKeepsList($needle): void {
if (array_key_exists($needle, $list)) {
$list[$needle] = 37;
}
assertType('list<int>', $list);
assertType('array<int|(lowercase-string&numeric-string&uppercase-string), int>', $list);
}
207 changes: 207 additions & 0 deletions tests/PHPStan/Analyser/nsrt/bug-14336.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
<?php declare(strict_types = 1);

namespace Bug14336;

use function PHPStan\Testing\assertType;

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

/**
* @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 test2(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'];

$xsdFiles[$xmlNamespace] = [];
assertType('array{}', $xsdFiles[$xmlNamespace]);
foreach ($mergedNamespace as $namespace) {
foreach ($groupedByNamespace[$namespace] ?? [] as $viewHelper) {
assertType('array{xmlNamespace: string, namespace: string, name: string}', $viewHelper);
$xsdFiles[$xmlNamespace][$int] = $viewHelper;
assertType('non-empty-array<int, array{xmlNamespace: string, namespace: string, name: string}>', $xsdFiles[$xmlNamespace]);
}
assertType('array<int, array{xmlNamespace: string, namespace: string, name: string}>', $xsdFiles[$xmlNamespace]);
}
assertType('array<int, array{xmlNamespace: string, namespace: string, name: string}>', $xsdFiles[$xmlNamespace]);
}
}

/**
* @param list<string> $list
*/
function testInLoop(array $list, int $int): void {
foreach ([1, 2, 3] as $item) {
$list[$int] = 'foo';
}
assertType('non-empty-array<int, string>', $list);
}

/**
* @param array<string, list<string>> $map
*/
function testNestedDimFetchInLoop(array $map, string $key, int $int): void {
$map[$key] = [];
foreach ([1, 2, 3] as $item) {
$map[$key][$int] = 'foo';
}
assertType('non-empty-array<int, string>', $map[$key]);
}

/**
* @param array<string, list<string>> $map
* @param list<string> $items
* @param list<string> $items2
*/
function testDoubleNestedForeachDimFetch(array $map, string $key, int $int, array $items, array $items2): void {
$map[$key] = [];
foreach ($items as $item) {
foreach ($items2 as $item2) {
$map[$key][$int] = $item2;
}
}
assertType('array<int, string>', $map[$key]);
}

/**
* @param array<string, list<string>> $map
* @param list<string> $items
*/
function testSingleVariableForeach(array $map, string $key, int $int, array $items): void {
$map[$key] = [];
foreach ($items as $item) {
$map[$key][$int] = $item;
}
assertType('array<int, string>', $map[$key]);
}

/**
* @param array<string, list<string>> $map
* @param list<string> $items
* @param list<string> $outerItems
*/
function testOuterForeach(array $map, string $key, int $int, array $items, array $outerItems): void {
foreach ($outerItems as $outerItem) {
$map[$key] = [];
foreach ($items as $item) {
$map[$key][$int] = $item;
}
assertType('array<int, string>', $map[$key]);
}
}

/**
* @param array<string, list<string>> $map
* @param list<string> $items
* @param list<string> $outerItems
*/
function testOuterForeachWithContinue(array $map, string $key, int $int, array $items, array $outerItems): void {
foreach ($outerItems as $outerItem) {
if (strlen($outerItem) < 2) {
continue;
}
$map[$key] = [];
foreach ($items as $item) {
$map[$key][$int] = $item;
}
assertType('array<int, string>', $map[$key]);
}
}

/**
* @param array<string, list<string>> $map
* @param list<list<string>> $nestedItems
* @param list<string> $outerItems
*/
function testNestedInnerForeach(array $map, string $key, int $int, array $nestedItems, array $outerItems): void {
foreach ($outerItems as $outerItem) {
if (strlen($outerItem) < 2) {
continue;
}
$map[$key] = [];
foreach ($nestedItems as $items) {
foreach ($items as $item) {
$map[$key][$int] = $item;
}
}
assertType('array<int, string>', $map[$key]);
}
}

/**
* @param array<string, list<string>> $map
* @param array<string, list<string>> $nestedItems
* @param list<string> $outerItems
*/
function testNestedInnerForeachNullCoalesce(array $map, string $key, int $int, array $nestedItems, array $outerItems): void {
foreach ($outerItems as $outerItem) {
if (strlen($outerItem) < 2) {
continue;
}
$map[$key] = [];
foreach ($outerItems as $ns) {
foreach ($nestedItems[$ns] ?? [] as $item) {
$map[$key][$int] = $item;
}
}
assertType('array<int, string>', $map[$key]);
}
}

/**
* @param array<string, list<array{ns: string, name: string}>> $map
* @param array<string, list<array{ns: string, name: string}>> $grouped
* @param array<string, list<string>> $extra
*/
function testCloseToOriginal(array $map, array $grouped, array $extra, int $int): void {
foreach ($extra as $merged) {
if (count($merged) < 2) {
continue;
}
$target = end($merged);
if (!isset($grouped[$target])) {
continue;
}
$key = $grouped[$target][0]['ns'];

$map[$key] = [];
foreach ($merged as $ns) {
foreach ($grouped[$ns] ?? [] as $item) {
$map[$key][$int] = $item;
}
}
assertType('array<int, array{ns: string, name: string}>', $map[$key]);
}
}

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

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