Skip to content

Commit d67dae3

Browse files
committed
General array after offset assign is non empty
1 parent 6012c89 commit d67dae3

15 files changed

+144
-42
lines changed

src/Analyser/MutatingScope.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3622,7 +3622,7 @@ private static function generalizeType(Type $a, Type $b): Type
36223622
$constantArrays[$key][] = $type;
36233623
continue;
36243624
}
3625-
if ($type instanceof ArrayType) {
3625+
if ($type->isArray()->yes()) {
36263626
$generalArrays[$key][] = $type;
36273627
continue;
36283628
}

src/Rules/Missing/MissingClosureNativeReturnTypehintRule.php

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
use PHPStan\Node\ClosureReturnStatementsNode;
88
use PHPStan\Rules\Rule;
99
use PHPStan\Rules\RuleErrorBuilder;
10-
use PHPStan\Type\ArrayType;
1110
use PHPStan\Type\IntersectionType;
1211
use PHPStan\Type\MixedType;
1312
use PHPStan\Type\NeverType;
@@ -115,7 +114,7 @@ public function processNode(Node $node, Scope $scope): array
115114

116115
$returnType = TypeUtils::generalizeType($returnType);
117116
$description = $returnType->describe(VerbosityLevel::typeOnly());
118-
if ($returnType instanceof ArrayType) {
117+
if ($returnType->isArray()->yes()) {
119118
$description = 'array';
120119
}
121120
if ($hasNull) {

src/Type/ArrayType.php

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
use PHPStan\Reflection\ClassMemberAccessAnswerer;
66
use PHPStan\Reflection\TrivialParametersAcceptor;
77
use PHPStan\TrinaryLogic;
8+
use PHPStan\Type\Accessory\NonEmptyArrayType;
89
use PHPStan\Type\Constant\ConstantArrayType;
910
use PHPStan\Type\Constant\ConstantIntegerType;
1011
use PHPStan\Type\Constant\ConstantStringType;
@@ -225,10 +226,10 @@ public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $uni
225226
$offsetType = new IntegerType();
226227
}
227228

228-
return new self(
229+
return TypeCombinator::intersect(new self(
229230
TypeCombinator::union($this->keyType, self::castToArrayKeyType($offsetType)),
230231
$unionValues ? TypeCombinator::union($this->itemType, $valueType) : $valueType
231-
);
232+
), new NonEmptyArrayType());
232233
}
233234

234235
public function isCallable(): TrinaryLogic

src/Type/BenevolentUnionType.php

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,4 +41,24 @@ public function isAcceptedBy(Type $acceptingType, bool $strictTypes): TrinaryLog
4141
return TrinaryLogic::createNo()->or(...$results);
4242
}
4343

44+
public function traverse(callable $cb): Type
45+
{
46+
$types = [];
47+
$changed = false;
48+
49+
foreach ($this->getTypes() as $type) {
50+
$newType = $cb($type);
51+
if ($type !== $newType) {
52+
$changed = true;
53+
}
54+
$types[] = $newType;
55+
}
56+
57+
if ($changed) {
58+
return TypeUtils::toBenevolentUnion(TypeCombinator::union(...$types));
59+
}
60+
61+
return $this;
62+
}
63+
4464
}

src/Type/Constant/ConstantArrayType.php

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
use PHPStan\Reflection\InaccessibleMethod;
88
use PHPStan\Reflection\TrivialParametersAcceptor;
99
use PHPStan\TrinaryLogic;
10+
use PHPStan\Type\Accessory\NonEmptyArrayType;
1011
use PHPStan\Type\ArrayType;
1112
use PHPStan\Type\BooleanType;
1213
use PHPStan\Type\CompoundType;
@@ -477,12 +478,31 @@ public function unsetOffset(Type $offsetType): Type
477478
}
478479
}
479480

480-
return $this->generalize();
481+
$arrays = [];
482+
foreach ($this->getAllArrays() as $tmp) {
483+
$arrays[] = new self($tmp->keyTypes, $tmp->valueTypes, $tmp->nextAutoIndex, array_keys($tmp->keyTypes));
484+
}
485+
486+
return TypeUtils::generalizeType(TypeCombinator::union(...$arrays));
481487
}
482488

483489
public function isIterableAtLeastOnce(): TrinaryLogic
484490
{
485-
return TrinaryLogic::createFromBoolean(count($this->keyTypes) > 0);
491+
$keysCount = count($this->keyTypes);
492+
if ($keysCount === 0) {
493+
return TrinaryLogic::createNo();
494+
}
495+
496+
$optionalKeysCount = count($this->optionalKeys);
497+
if ($optionalKeysCount === 0) {
498+
return TrinaryLogic::createYes();
499+
}
500+
501+
if ($optionalKeysCount < $keysCount) {
502+
return TrinaryLogic::createYes();
503+
}
504+
505+
return TrinaryLogic::createMaybe();
486506
}
487507

488508
public function removeLast(): self
@@ -583,10 +603,16 @@ public function generalize(): Type
583603
return $this;
584604
}
585605

586-
return new ArrayType(
606+
$arrayType = new ArrayType(
587607
TypeUtils::generalizeType($this->getKeyType()),
588608
$this->getItemType()
589609
);
610+
611+
if (count($this->keyTypes) > count($this->optionalKeys)) {
612+
return TypeCombinator::intersect($arrayType, new NonEmptyArrayType());
613+
}
614+
615+
return $arrayType;
590616
}
591617

592618
/**

src/Type/Php/ArrayFilterFunctionReturnTypeReturnTypeExtension.php

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
use PHPStan\Type\ArrayType;
1313
use PHPStan\Type\BenevolentUnionType;
1414
use PHPStan\Type\Constant\ConstantArrayType;
15+
use PHPStan\Type\Constant\ConstantArrayTypeBuilder;
1516
use PHPStan\Type\MixedType;
1617
use PHPStan\Type\NeverType;
1718
use PHPStan\Type\NullType;
@@ -84,22 +85,19 @@ public function removeFalsey(Type $type): Type
8485
$keys = $type->getKeyTypes();
8586
$values = $type->getValueTypes();
8687

87-
$generalize = false;
88+
$builder = ConstantArrayTypeBuilder::createEmpty();
8889

8990
foreach ($values as $offset => $value) {
9091
$isFalsey = $falseyTypes->isSuperTypeOf($value);
9192

92-
if ($isFalsey->yes()) {
93-
unset($keys[$offset], $values[$offset]);
94-
} elseif ($isFalsey->maybe()) {
95-
$values[$offset] = TypeCombinator::remove($values[$offset], $falseyTypes);
96-
$generalize = true;
93+
if ($isFalsey->maybe()) {
94+
$builder->setOffsetValueType($keys[$offset], TypeCombinator::remove($value, $falseyTypes), true);
95+
} elseif ($isFalsey->no()) {
96+
$builder->setOffsetValueType($keys[$offset], $value);
9797
}
9898
}
9999

100-
$filteredArray = new ConstantArrayType(array_values($keys), array_values($values));
101-
102-
return $generalize ? $filteredArray->generalize() : $filteredArray;
100+
return $builder->getArray();
103101
}
104102

105103
$keyType = $type->getIterableKeyType();

src/Type/TypeUtils.php

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -120,15 +120,13 @@ public static function getAnyArrays(Type $type): array
120120

121121
public static function generalizeType(Type $type): Type
122122
{
123-
if ($type instanceof ConstantType) {
124-
return $type->generalize();
125-
} elseif ($type instanceof UnionType) {
126-
return TypeCombinator::union(...array_map(static function (Type $innerType): Type {
127-
return self::generalizeType($innerType);
128-
}, $type->getTypes()));
129-
}
123+
return TypeTraverser::map($type, static function (Type $type, callable $traverse): Type {
124+
if ($type instanceof ConstantType) {
125+
return $type->generalize();
126+
}
130127

131-
return $type;
128+
return $traverse($type);
129+
});
132130
}
133131

134132
/**

tests/PHPStan/Analyser/NodeScopeResolverTest.php

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3194,6 +3194,10 @@ public function dataBinaryOperations(): array
31943194
'array<int, 1|2|3>',
31953195
'$arrayToBeUnset',
31963196
],
3197+
[
3198+
'array<int, 1|2|3>',
3199+
'$arrayToBeUnset2',
3200+
],
31973201
[
31983202
'array',
31993203
'$shiftedNonEmptyArray',
@@ -5036,7 +5040,7 @@ public function dataArrayFunctions(): array
50365040
'array_intersect_key(...[$integers, [4, 5, 6]])',
50375041
],
50385042
[
5039-
'array<int|string, int>',
5043+
'array<int>',
50405044
'array_intersect_key(...$generalIntegersInAnotherArray, [])',
50415045
],
50425046
[
@@ -5128,7 +5132,7 @@ public function dataArrayFunctions(): array
51285132
'array_merge(...[$generalStringKeys, $generalDateTimeValues])',
51295133
],
51305134
[
5131-
'array<int|string, int>',
5135+
'array<int>',
51325136
'$mergedInts',
51335137
],
51345138
[
@@ -5244,7 +5248,7 @@ public function dataArrayFunctions(): array
52445248
'array_filter($union)',
52455249
],
52465250
[
5247-
'array<int, int<min, -1>|int<1, max>|true>',
5251+
'array(?0 => true, ?1 => int<min, -1>|int<1, max>)',
52485252
'array_filter($withPossiblyFalsey)',
52495253
],
52505254
[
@@ -7545,7 +7549,7 @@ public function dataForeachLoopVariables(): array
75457549
"'end'",
75467550
],
75477551
[
7548-
'array<int, 1|2|3>',
7552+
'array<int, 1|2|3>&nonEmpty',
75497553
'$integers',
75507554
"'end'",
75517555
],
@@ -7560,7 +7564,7 @@ public function dataForeachLoopVariables(): array
75607564
"'begin'",
75617565
],
75627566
[
7563-
'array<string, 1|2|3>',
7567+
'array<string, 1|2|3>&nonEmpty',
75647568
'$this->property',
75657569
"'end'",
75667570
],
@@ -9642,7 +9646,7 @@ public function dataGeneralizeScope(): array
96429646
{
96439647
return [
96449648
[
9645-
"array<int|string, array<int|string, array('hitCount' => int, 'loadCount' => int, 'removeCount' => int, 'saveCount' => int)>>",
9649+
"array<array<int|string, array('hitCount' => int, 'loadCount' => int, 'removeCount' => int, 'saveCount' => int)>>",
96469650
'$statistics',
96479651
],
96489652
];
@@ -9669,7 +9673,7 @@ public function dataGeneralizeScopeRecursiveType(): array
96699673
{
96709674
return [
96719675
[
9672-
'array()|array(\'foo\' => array<int|string, array>)',
9676+
'array()|array(\'foo\' => array<array>)',
96739677
'$data',
96749678
],
96759679
];
@@ -10250,6 +10254,11 @@ public function dataBug3997(): array
1025010254
return $this->gatherAssertTypes(__DIR__ . '/data/bug-3997.php');
1025110255
}
1025210256

10257+
public function dataBug4016(): array
10258+
{
10259+
return $this->gatherAssertTypes(__DIR__ . '/data/bug-4016.php');
10260+
}
10261+
1025310262
/**
1025410263
* @dataProvider dataBug2574
1025510264
* @dataProvider dataBug2577
@@ -10341,6 +10350,7 @@ public function dataBug3997(): array
1034110350
* @dataProvider dataBug3990
1034210351
* @dataProvider dataBug3991
1034310352
* @dataProvider dataBug3993
10353+
* @dataProvider dataBug4016
1034410354
* @param string $assertType
1034510355
* @param string $file
1034610356
* @param mixed ...$args

tests/PHPStan/Analyser/data/assign-nested-arrays.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ public function doFoo(int $i)
1414
$array[$i]['bar'] = 1;
1515
$array[$i]['baz'] = 2;
1616

17-
assertType('array<int, array(\'bar\' => 1, \'baz\' => 2)>', $array);
17+
assertType('array<int, array(\'bar\' => 1, \'baz\' => 2)>&nonEmpty', $array);
1818
}
1919

2020
public function doBar(int $i, int $j)
@@ -27,7 +27,7 @@ public function doBar(int $i, int $j)
2727
echo $array[$i][$j]['bar'];
2828
echo $array[$i][$j]['baz'];
2929

30-
assertType('array<int, array<int, array(\'bar\' => 1, \'baz\' => 2)>>', $array);
30+
assertType('array<int, array<int, array(\'bar\' => 1, \'baz\' => 2)>&nonEmpty>&nonEmpty', $array);
3131
}
3232

3333
}

tests/PHPStan/Analyser/data/binary.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,9 @@ public function doFoo(array $generalArray)
134134
$arrayToBeUnset = $array;
135135
unset($arrayToBeUnset[$string]);
136136

137+
$arrayToBeUnset2 = $arrayToBeUnset;
138+
unset($arrayToBeUnset2[$string]);
139+
137140
/** @var array $shiftedNonEmptyArray */
138141
$shiftedNonEmptyArray = doFoo();
139142

0 commit comments

Comments
 (0)