Skip to content

Commit 5c0e37f

Browse files
phpstan-botclaudeVincentLanglet
authored
Fix phpstan/phpstan#14308: Offset 0 should not be optional on non empty list (#5236)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: Vincent Langlet <vincentlanglet@hotmail.fr>
1 parent 584420d commit 5c0e37f

File tree

9 files changed

+126
-15
lines changed

9 files changed

+126
-15
lines changed

src/Rules/Arrays/NonexistentOffsetInArrayDimFetchCheck.php

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,13 @@ public function check(
6262
return [];
6363
}
6464

65-
if ($type->hasOffsetValueType($dimType)->no()) {
65+
$hasOffsetValueType = $type->hasOffsetValueType($dimType);
66+
67+
if ($hasOffsetValueType->yes()) {
68+
return [];
69+
}
70+
71+
if ($hasOffsetValueType->no()) {
6672
if ($type->isArray()->yes()) {
6773
$validArrayDimType = TypeCombinator::intersect(AllowedArrayKeysTypes::getType(), $dimType);
6874
if ($validArrayDimType instanceof NeverType) {

src/Type/TypeCombinator.php

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1349,9 +1349,9 @@ public static function intersect(Type ...$types): Type
13491349

13501350
if (
13511351
$types[$i] instanceof ConstantArrayType
1352-
&& count($types[$i]->getKeyTypes()) === 1
1353-
&& $types[$i]->isOptionalKey(0)
13541352
&& $types[$j] instanceof NonEmptyArrayType
1353+
&& (count($types[$i]->getKeyTypes()) === 1 || $types[$i]->isList()->yes())
1354+
&& $types[$i]->isOptionalKey(0)
13551355
) {
13561356
$types[$i] = $types[$i]->makeOffsetRequired($types[$i]->getKeyTypes()[0]);
13571357
array_splice($types, $j--, 1);
@@ -1361,9 +1361,9 @@ public static function intersect(Type ...$types): Type
13611361

13621362
if (
13631363
$types[$j] instanceof ConstantArrayType
1364-
&& count($types[$j]->getKeyTypes()) === 1
1365-
&& $types[$j]->isOptionalKey(0)
13661364
&& $types[$i] instanceof NonEmptyArrayType
1365+
&& (count($types[$j]->getKeyTypes()) === 1 || $types[$j]->isList()->yes())
1366+
&& $types[$j]->isOptionalKey(0)
13671367
) {
13681368
$types[$j] = $types[$j]->makeOffsetRequired($types[$j]->getKeyTypes()[0]);
13691369
array_splice($types, $i--, 1);

tests/PHPStan/Analyser/NodeScopeResolverTest.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -262,6 +262,7 @@ private static function findTestFiles(): iterable
262262
yield __DIR__ . '/../Rules/Properties/data/bug-14012.php';
263263
yield __DIR__ . '/../Rules/Variables/data/bug-14124.php';
264264
yield __DIR__ . '/../Rules/Variables/data/bug-14124b.php';
265+
yield __DIR__ . '/../Rules/Arrays/data/bug-14308.php';
265266
}
266267

267268
/**

tests/PHPStan/Analyser/nsrt/array-shape-list-optional.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ public function doFoo(
2323
): void
2424
{
2525
assertType('list{0: string, 1: int, 2?: string, 3?: string}', $valid1);
26-
assertType('non-empty-list{0?: string, 1?: int, 2?: string, 3?: string}', $valid2);
26+
assertType('list{0: string, 1?: int, 2?: string, 3?: string}', $valid2);
2727
assertType('non-empty-array{0?: string, 1?: int, 2?: string, 3?: string}', $valid3);
2828
assertType('*NEVER*', $invalid1);
2929
assertType('*NEVER*', $invalid2);

tests/PHPStan/Analyser/nsrt/bug-14297.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ function (): void {
1515
return;
1616
}
1717

18-
assertType("non-empty-list{0?: 'a'|'b', 1?: 'b'}", $a);
18+
assertType("array{0: 'a'|'b', 1?: 'b'}", $a);
1919
assertType("int<1, 2>", count($a));
2020

2121
if (count($a) === 2) {

tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1256,4 +1256,11 @@ public function testBug13773(): void
12561256
]);
12571257
}
12581258

1259+
public function testBug14308(): void
1260+
{
1261+
$this->reportPossiblyNonexistentConstantArrayOffset = true;
1262+
1263+
$this->analyse([__DIR__ . '/data/bug-14308.php'], []);
1264+
}
1265+
12591266
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace Bug14308;
4+
5+
use RuntimeException;
6+
use function PHPStan\Testing\assertType;
7+
8+
function getUi(string $s1, string $s2, string $s3): string
9+
{
10+
$available = array_keys(array_filter([
11+
'swagger' => $s1,
12+
'redoc' => $s2,
13+
'scalar' => $s3,
14+
]));
15+
16+
if ([] === $available) {
17+
throw new RuntimeException('No documentation UI is enabled.');
18+
}
19+
20+
assertType("list{0: 'redoc'|'scalar'|'swagger', 1?: 'redoc'|'scalar', 2?: 'scalar'}", $available);
21+
22+
return $available[0];
23+
}

tests/PHPStan/Type/TypeCombinatorTest.php

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3485,6 +3485,70 @@ public static function dataIntersect(): iterable
34853485
ConstantArrayType::class,
34863486
'array{string}',
34873487
],
3488+
[
3489+
[
3490+
new ConstantArrayType([
3491+
new ConstantIntegerType(0),
3492+
new ConstantIntegerType(1),
3493+
], [
3494+
new StringType(),
3495+
new StringType(),
3496+
], optionalKeys: [0, 1], isList: TrinaryLogic::createYes()),
3497+
new NonEmptyArrayType(),
3498+
],
3499+
ConstantArrayType::class,
3500+
'array{0: string, 1?: string}',
3501+
],
3502+
[
3503+
[
3504+
new ConstantArrayType([
3505+
new ConstantIntegerType(0),
3506+
new ConstantIntegerType(1),
3507+
new ConstantIntegerType(2),
3508+
new ConstantIntegerType(3),
3509+
], [
3510+
new StringType(),
3511+
new StringType(),
3512+
new StringType(),
3513+
new StringType(),
3514+
], nextAutoIndexes: [3], optionalKeys: [0, 1, 2, 3], isList: TrinaryLogic::createYes()),
3515+
new NonEmptyArrayType(),
3516+
],
3517+
ConstantArrayType::class,
3518+
'list{0: string, 1?: string, 2?: string, 3?: string}',
3519+
],
3520+
[
3521+
[
3522+
new ConstantArrayType([
3523+
new ConstantIntegerType(0),
3524+
new ConstantIntegerType(1),
3525+
], [
3526+
new StringType(),
3527+
new StringType(),
3528+
], optionalKeys: [0, 1]),
3529+
new NonEmptyArrayType(),
3530+
],
3531+
IntersectionType::class,
3532+
'non-empty-array{0?: string, 1?: string}',
3533+
],
3534+
[
3535+
[
3536+
new ConstantArrayType([
3537+
new ConstantIntegerType(0),
3538+
new ConstantIntegerType(1),
3539+
new ConstantIntegerType(2),
3540+
new ConstantIntegerType(3),
3541+
], [
3542+
new StringType(),
3543+
new StringType(),
3544+
new StringType(),
3545+
new StringType(),
3546+
], [3], [0, 1, 2, 3], TrinaryLogic::createYes()),
3547+
new NonEmptyArrayType(),
3548+
],
3549+
ConstantArrayType::class,
3550+
'list{0: string, 1?: string, 2?: string, 3?: string}',
3551+
],
34883552
[
34893553
[
34903554
new ConstantArrayType([], []),

tests/PHPStan/Type/TypeToPhpDocNodeTest.php

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -404,14 +404,6 @@ public static function dataToPhpDocNode(): iterable
404404
'list{0?: string, 1?: string, 2?: string, 3?: string}',
405405
];
406406

407-
yield [
408-
new IntersectionType([
409-
$listArrayWithAllOptionalKeys,
410-
new NonEmptyArrayType(),
411-
]),
412-
'non-empty-list{0?: string, 1?: string, 2?: string, 3?: string}',
413-
];
414-
415407
$constantArrayWithAllOptionalKeys = new ConstantArrayType([
416408
new ConstantIntegerType(0),
417409
new ConstantIntegerType(1),
@@ -448,6 +440,24 @@ public function testToPhpDocNode(Type $type, string $expected): void
448440

449441
public static function dataToPhpDocNodeWithoutCheckingEquals(): iterable
450442
{
443+
yield [
444+
new IntersectionType([
445+
new ConstantArrayType([
446+
new ConstantIntegerType(0),
447+
new ConstantIntegerType(1),
448+
new ConstantIntegerType(2),
449+
new ConstantIntegerType(3),
450+
], [
451+
new StringType(),
452+
new StringType(),
453+
new StringType(),
454+
new StringType(),
455+
], [3], [0, 1, 2, 3], TrinaryLogic::createYes()),
456+
new NonEmptyArrayType(),
457+
]),
458+
'non-empty-list{0?: string, 1?: string, 2?: string, 3?: string}',
459+
];
460+
451461
yield [
452462
new ConstantStringType("foo\nbar\nbaz"),
453463
'(literal-string & lowercase-string & non-falsy-string)',

0 commit comments

Comments
 (0)