Skip to content

Commit 2fea15d

Browse files
authored
Keep list on unset() with nested dim fetch
1 parent 6881423 commit 2fea15d

18 files changed

+267
-19
lines changed

phpstan-baseline.neon

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,12 @@ parameters:
4848
count: 2
4949
path: src/Analyser/NodeScopeResolver.php
5050

51+
-
52+
message: '#^Doing instanceof PHPStan\\Type\\Constant\\ConstantStringType is error\-prone and deprecated\. Use Type\:\:getConstantStrings\(\) instead\.$#'
53+
identifier: phpstanApi.instanceofType
54+
count: 1
55+
path: src/Analyser/NodeScopeResolver.php
56+
5157
-
5258
message: '#^Parameter \#2 \$node of method PHPStan\\BetterReflection\\SourceLocator\\Ast\\Strategy\\NodeToReflection\:\:__invoke\(\) expects PhpParser\\Node\\Expr\\ArrowFunction\|PhpParser\\Node\\Expr\\Closure\|PhpParser\\Node\\Expr\\FuncCall\|PhpParser\\Node\\Stmt\\Class_\|PhpParser\\Node\\Stmt\\Const_\|PhpParser\\Node\\Stmt\\Enum_\|PhpParser\\Node\\Stmt\\Function_\|PhpParser\\Node\\Stmt\\Interface_\|PhpParser\\Node\\Stmt\\Trait_, PhpParser\\Node\\Stmt\\ClassLike given\.$#'
5359
identifier: argument.type

src/Analyser/NodeScopeResolver.php

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -163,13 +163,15 @@
163163
use PHPStan\ShouldNotHappenException;
164164
use PHPStan\TrinaryLogic;
165165
use PHPStan\Type\Accessory\AccessoryArrayListType;
166+
use PHPStan\Type\Accessory\HasOffsetValueType;
166167
use PHPStan\Type\Accessory\NonEmptyArrayType;
167168
use PHPStan\Type\ArrayType;
168169
use PHPStan\Type\ClosureType;
169170
use PHPStan\Type\Constant\ConstantArrayType;
170171
use PHPStan\Type\Constant\ConstantArrayTypeBuilder;
171172
use PHPStan\Type\Constant\ConstantBooleanType;
172173
use PHPStan\Type\Constant\ConstantIntegerType;
174+
use PHPStan\Type\Constant\ConstantStringType;
173175
use PHPStan\Type\ErrorType;
174176
use PHPStan\Type\FileTypeMapper;
175177
use PHPStan\Type\GeneralizePrecision;
@@ -5936,9 +5938,35 @@ private function produceArrayDimFetchAssignValueToWrite(array $dimFetchStack, ar
59365938
}
59375939
$offsetValueType = TypeCombinator::intersect($offsetValueType, TypeCombinator::union(...$types));
59385940
}
5939-
$valueToWrite = $offsetValueType->setOffsetValueType($offsetType, $valueToWrite, $i === 0);
59405941

59415942
$arrayDimFetch = $dimFetchStack[$i] ?? null;
5943+
if (
5944+
$offsetType !== null
5945+
&& $arrayDimFetch !== null
5946+
&& $scope->hasExpressionType($arrayDimFetch)->yes()
5947+
) {
5948+
$hasOffsetType = null;
5949+
if ($offsetType instanceof ConstantStringType || $offsetType instanceof ConstantIntegerType) {
5950+
$hasOffsetType = new HasOffsetValueType($offsetType, $valueToWrite);
5951+
}
5952+
$valueToWrite = $offsetValueType->setExistingOffsetValueType($offsetType, $valueToWrite);
5953+
5954+
if ($hasOffsetType !== null) {
5955+
$valueToWrite = TypeCombinator::intersect(
5956+
$valueToWrite,
5957+
$hasOffsetType,
5958+
);
5959+
} elseif ($valueToWrite->isArray()->yes()) {
5960+
$valueToWrite = TypeCombinator::intersect(
5961+
$valueToWrite,
5962+
new NonEmptyArrayType(),
5963+
);
5964+
}
5965+
5966+
} else {
5967+
$valueToWrite = $offsetValueType->setOffsetValueType($offsetType, $valueToWrite, $i === 0);
5968+
}
5969+
59425970
if ($arrayDimFetch === null || !$offsetValueType->isList()->yes()) {
59435971
continue;
59445972
}

src/Type/Accessory/AccessoryArrayListType.php

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -156,11 +156,7 @@ public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $uni
156156

157157
public function setExistingOffsetValueType(Type $offsetType, Type $valueType): Type
158158
{
159-
if ((new ConstantIntegerType(0))->isSuperTypeOf($offsetType)->yes()) {
160-
return $this;
161-
}
162-
163-
return new ErrorType();
159+
return $this;
164160
}
165161

166162
public function unsetOffset(Type $offsetType): Type

src/Type/Accessory/HasOffsetValueType.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,10 @@ public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $uni
184184

185185
public function setExistingOffsetValueType(Type $offsetType, Type $valueType): Type
186186
{
187+
if (!$offsetType->equals($this->offsetType)) {
188+
return $this;
189+
}
190+
187191
return new self($this->offsetType, $valueType);
188192
}
189193

src/Type/Constant/ConstantArrayType.php

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -693,15 +693,8 @@ public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $uni
693693

694694
public function setExistingOffsetValueType(Type $offsetType, Type $valueType): Type
695695
{
696-
$offsetType = $offsetType->toArrayKey();
697696
$builder = ConstantArrayTypeBuilder::createFromConstantArray($this);
698-
foreach ($this->keyTypes as $keyType) {
699-
if ($offsetType->isSuperTypeOf($keyType)->no()) {
700-
continue;
701-
}
702-
703-
$builder->setOffsetValueType($keyType, $valueType);
704-
}
697+
$builder->setOffsetValueType($offsetType, $valueType);
705698

706699
return $builder->getArray();
707700
}

src/Type/IntersectionType.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -829,6 +829,10 @@ public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $uni
829829
}
830830
}
831831

832+
if ($this->isList()->yes() && $this->getIterableValueType()->isArray()->yes()) {
833+
$result = TypeCombinator::intersect($result, new AccessoryArrayListType());
834+
}
835+
832836
return $result;
833837
}
834838

tests/PHPStan/Analyser/NodeScopeResolverTest.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,7 @@ private static function findTestFiles(): iterable
226226
yield __DIR__ . '/../Rules/Arrays/data/bug-11679.php';
227227
yield __DIR__ . '/../Rules/Methods/data/bug-4801.php';
228228
yield __DIR__ . '/../Rules/Arrays/data/narrow-superglobal.php';
229+
yield __DIR__ . '/../Rules/Methods/data/bug-12927.php';
229230
}
230231

231232
/**

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,8 +56,8 @@ function testKeepNestedListAfterIssetIndex(array $nestedList, int $i, int $j): v
5656
assertType('list<list<int>>', $nestedList);
5757
assertType('list<int>', $nestedList[$i]);
5858
$nestedList[$i][$j] = 21;
59-
assertType('non-empty-list<non-empty-list<int>>', $nestedList);
60-
assertType('non-empty-list<int>', $nestedList[$i]);
59+
assertType('non-empty-list<list<int>>', $nestedList);
60+
assertType('list<int>', $nestedList[$i]);
6161
}
6262
assertType('list<list<int>>', $nestedList);
6363
}

tests/PHPStan/Rules/Methods/ReturnTypeRuleTest.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1224,6 +1224,11 @@ public function testBug1O580(): void
12241224
}
12251225

12261226
#[RequiresPhp('>= 8.0')]
1227+
public function testBug12927(): void
1228+
{
1229+
$this->analyse([__DIR__ . '/data/bug-12927.php'], []);
1230+
}
1231+
12271232
public function testBug4443(): void
12281233
{
12291234
$this->analyse([__DIR__ . '/data/bug-4443.php'], [
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
<?php
2+
3+
namespace Bug12927;
4+
5+
use function PHPStan\Testing\assertType;
6+
7+
class HelloWorld
8+
{
9+
/**
10+
* @param list<array{abc: string}> $list
11+
* @return list<array<string>>
12+
*/
13+
public function sayHello(array $list): array
14+
{
15+
foreach($list as $k => $v) {
16+
unset($list[$k]['abc']);
17+
assertType('non-empty-list<array{}|array{abc: string}>', $list);
18+
assertType('array{}|array{abc: string}', $list[$k]);
19+
}
20+
return $list;
21+
}
22+
23+
/**
24+
* @param list<array<string, string>> $list
25+
*/
26+
public function sayFoo(array $list): void
27+
{
28+
foreach($list as $k => $v) {
29+
unset($list[$k]['abc']);
30+
assertType('non-empty-list<array<string, string>>', $list);
31+
assertType('array<string, string>', $list[$k]);
32+
}
33+
assertType('list<array<string, string>>', $list);
34+
}
35+
36+
/**
37+
* @param list<array<string, string>> $list
38+
*/
39+
public function sayFoo2(array $list): void
40+
{
41+
foreach($list as $k => $v) {
42+
$list[$k]['abc'] = 'world';
43+
assertType("non-empty-list<non-empty-array<string, string>&hasOffsetValue('abc', 'world')>", $list);
44+
assertType("non-empty-array<string, string>&hasOffsetValue('abc', 'world')", $list[$k]);
45+
}
46+
assertType("list<non-empty-array<string, string>&hasOffsetValue('abc', 'world')>", $list);
47+
}
48+
49+
/**
50+
* @param list<array<string, string>> $list
51+
*/
52+
public function sayFooBar(array $list): void
53+
{
54+
foreach($list as $k => $v) {
55+
if (rand(0,1)) {
56+
unset($list[$k]);
57+
}
58+
assertType('array<int<0, max>, array<string, string>>', $list);
59+
assertType('array<string, string>', $list[$k]);
60+
}
61+
assertType('array<string, string>', $list[$k]);
62+
}
63+
}

0 commit comments

Comments
 (0)