Skip to content

Commit a4038b2

Browse files
committed
Support non-empty-array and non-empty-list
1 parent f9a6538 commit a4038b2

10 files changed

+126
-19
lines changed

src/PhpDoc/TypeNodeResolver.php

+28-7
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
use PHPStan\Reflection\PassedByReference;
3030
use PHPStan\Reflection\ReflectionProvider;
3131
use PHPStan\Type\Accessory\AccessoryNumericStringType;
32+
use PHPStan\Type\Accessory\NonEmptyArrayType;
3233
use PHPStan\Type\ArrayType;
3334
use PHPStan\Type\BenevolentUnionType;
3435
use PHPStan\Type\BooleanType;
@@ -183,6 +184,12 @@ private function resolveIdentifierTypeNode(IdentifierTypeNode $typeNode, NameSco
183184
case 'associative-array':
184185
return new ArrayType(new MixedType(), new MixedType());
185186

187+
case 'non-empty-array':
188+
return TypeCombinator::intersect(
189+
new ArrayType(new MixedType(), new MixedType()),
190+
new NonEmptyArrayType()
191+
);
192+
186193
case 'iterable':
187194
return new IterableType(new MixedType(), new MixedType());
188195

@@ -207,6 +214,11 @@ private function resolveIdentifierTypeNode(IdentifierTypeNode $typeNode, NameSco
207214

208215
case 'list':
209216
return new ArrayType(new IntegerType(), new MixedType());
217+
case 'non-empty-list':
218+
return TypeCombinator::intersect(
219+
new ArrayType(new IntegerType(), new MixedType()),
220+
new NonEmptyArrayType()
221+
);
210222
}
211223

212224
if ($nameScope->getClassName() !== null) {
@@ -321,19 +333,28 @@ private function resolveGenericTypeNode(GenericTypeNode $typeNode, NameScope $na
321333
$mainTypeName = strtolower($typeNode->type->name);
322334
$genericTypes = $this->resolveMultiple($typeNode->genericTypes, $nameScope);
323335

324-
if ($mainTypeName === 'array') {
336+
if ($mainTypeName === 'array' || $mainTypeName === 'non-empty-array') {
325337
if (count($genericTypes) === 1) { // array<ValueType>
326-
return new ArrayType(new MixedType(true), $genericTypes[0]);
327-
338+
$arrayType = new ArrayType(new MixedType(true), $genericTypes[0]);
339+
} elseif (count($genericTypes) === 2) { // array<KeyType, ValueType>
340+
$arrayType = new ArrayType($genericTypes[0], $genericTypes[1]);
341+
} else {
342+
return new ErrorType();
328343
}
329344

330-
if (count($genericTypes) === 2) { // array<KeyType, ValueType>
331-
return new ArrayType($genericTypes[0], $genericTypes[1]);
345+
if ($mainTypeName === 'non-empty-array') {
346+
return TypeCombinator::intersect($arrayType, new NonEmptyArrayType());
332347
}
333348

334-
} elseif ($mainTypeName === 'list') {
349+
return $arrayType;
350+
} elseif ($mainTypeName === 'list' || $mainTypeName === 'non-empty-list') {
335351
if (count($genericTypes) === 1) { // list<ValueType>
336-
return new ArrayType(new IntegerType(), $genericTypes[0]);
352+
$listType = new ArrayType(new IntegerType(), $genericTypes[0]);
353+
if ($mainTypeName === 'non-empty-list') {
354+
return TypeCombinator::intersect($listType, new NonEmptyArrayType());
355+
}
356+
357+
return $listType;
337358
}
338359

339360
return new ErrorType();

src/Rules/RuleLevelHelper.php

+1
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ public function accepts(Type $acceptingType, Type $acceptedType, bool $strictTyp
8585
if (
8686
$acceptedType->isArray()->yes()
8787
&& $acceptingType->isArray()->yes()
88+
&& !$acceptingType->isIterableAtLeastOnce()->yes()
8889
&& count(TypeUtils::getConstantArrays($acceptedType)) === 0
8990
&& count(TypeUtils::getConstantArrays($acceptingType)) === 0
9091
) {

src/Type/Accessory/NonEmptyArrayType.php

+3-7
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
namespace PHPStan\Type\Accessory;
44

55
use PHPStan\TrinaryLogic;
6-
use PHPStan\Type\ArrayType;
76
use PHPStan\Type\CompoundType;
87
use PHPStan\Type\CompoundTypeHelper;
98
use PHPStan\Type\ErrorType;
@@ -35,8 +34,7 @@ public function accepts(Type $type, bool $strictTypes): TrinaryLogic
3534
return CompoundTypeHelper::accepts($type, $this, $strictTypes);
3635
}
3736

38-
return (new ArrayType(new MixedType(), new MixedType()))
39-
->isSuperTypeOf($type)
37+
return $type->isArray()
4038
->and($type->isIterableAtLeastOnce());
4139
}
4240

@@ -50,8 +48,7 @@ public function isSuperTypeOf(Type $type): TrinaryLogic
5048
return $type->isSubTypeOf($this);
5149
}
5250

53-
return (new ArrayType(new MixedType(), new MixedType()))
54-
->isSuperTypeOf($type)
51+
return $type->isArray()
5552
->and($type->isIterableAtLeastOnce());
5653
}
5754

@@ -61,8 +58,7 @@ public function isSubTypeOf(Type $otherType): TrinaryLogic
6158
return $otherType->isSuperTypeOf($this);
6259
}
6360

64-
return (new ArrayType(new MixedType(), new MixedType()))
65-
->isSuperTypeOf($otherType)
61+
return $otherType->isArray()
6662
->and($otherType->isIterableAtLeastOnce())
6763
->and($otherType instanceof self ? TrinaryLogic::createYes() : TrinaryLogic::createMaybe());
6864
}

src/Type/IntersectionType.php

+2-1
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
use PHPStan\TrinaryLogic;
1212
use PHPStan\Type\Accessory\AccessoryNumericStringType;
1313
use PHPStan\Type\Accessory\AccessoryType;
14+
use PHPStan\Type\Accessory\NonEmptyArrayType;
1415
use PHPStan\Type\Generic\TemplateTypeMap;
1516
use PHPStan\Type\Generic\TemplateTypeVariance;
1617

@@ -133,7 +134,7 @@ function () use ($level): string {
133134
function () use ($level): string {
134135
$typeNames = [];
135136
foreach ($this->types as $type) {
136-
if ($type instanceof AccessoryType && !$type instanceof AccessoryNumericStringType) {
137+
if ($type instanceof AccessoryType && !$type instanceof AccessoryNumericStringType && !$type instanceof NonEmptyArrayType) {
137138
continue;
138139
}
139140
$typeNames[] = $type->describe($level);

src/Type/VerbosityLevel.php

+5
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
namespace PHPStan\Type;
44

55
use PHPStan\Type\Accessory\AccessoryNumericStringType;
6+
use PHPStan\Type\Accessory\NonEmptyArrayType;
67

78
class VerbosityLevel
89
{
@@ -64,6 +65,10 @@ public static function getRecommendedLevelByType(Type $type): self
6465
$moreVerbose = true;
6566
return $type;
6667
}
68+
if ($type instanceof NonEmptyArrayType) {
69+
$moreVerbose = true;
70+
return $type;
71+
}
6772
return $traverse($type);
6873
});
6974

tests/PHPStan/Analyser/NodeScopeResolverTest.php

+6
Original file line numberDiff line numberDiff line change
@@ -10171,6 +10171,11 @@ public function dataThrowExpression(): array
1017110171
return $this->gatherAssertTypes(__DIR__ . '/data/throw-expr.php');
1017210172
}
1017310173

10174+
public function dataNotEmptyArray(): array
10175+
{
10176+
return $this->gatherAssertTypes(__DIR__ . '/data/non-empty-array.php');
10177+
}
10178+
1017410179
/**
1017510180
* @dataProvider dataBug2574
1017610181
* @dataProvider dataBug2577
@@ -10249,6 +10254,7 @@ public function dataThrowExpression(): array
1024910254
* @dataProvider dataBugFromPr339
1025010255
* @dataProvider dataPow
1025110256
* @dataProvider dataThrowExpression
10257+
* @dataProvider dataNotEmptyArray
1025210258
* @param string $assertType
1025310259
* @param string $file
1025410260
* @param mixed ...$args
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
<?php
2+
3+
namespace NonEmptyArray;
4+
5+
use function PHPStan\Analyser\assertType;
6+
7+
class Foo
8+
{
9+
10+
/**
11+
* @param non-empty-array $array
12+
* @param non-empty-list $list
13+
* @param non-empty-array<int, string> $arrayOfStrings
14+
* @param non-empty-list<\stdClass> $listOfStd
15+
* @param non-empty-list<\stdClass> $listOfStd2
16+
* @param non-empty-list<string, \stdClass> $invalidList
17+
*/
18+
public function doFoo(
19+
array $array,
20+
array $list,
21+
array $arrayOfStrings,
22+
array $listOfStd,
23+
$listOfStd2,
24+
array $invalidList,
25+
$invalidList2
26+
): void
27+
{
28+
assertType('array&nonEmpty', $array);
29+
assertType('array<int, mixed>&nonEmpty', $list);
30+
assertType('array<int, string>&nonEmpty', $arrayOfStrings);
31+
assertType('array<int, stdClass>&nonEmpty', $listOfStd);
32+
assertType('array<int, stdClass>&nonEmpty', $listOfStd2);
33+
assertType('array', $invalidList);
34+
assertType('mixed', $invalidList2);
35+
}
36+
37+
}

tests/PHPStan/Levels/data/acceptTypes-5.json

+10
Original file line numberDiff line numberDiff line change
@@ -208,5 +208,15 @@
208208
"message": "Parameter #1 $numericString of method Levels\\AcceptTypes\\NumericStrings::doBar() expects string&numeric, string given.",
209209
"line": 708,
210210
"ignorable": true
211+
},
212+
{
213+
"message": "Parameter #1 $nonEmpty of method Levels\\AcceptTypes\\AcceptNonEmpty::doBar() expects array&nonEmpty, array() given.",
214+
"line": 733,
215+
"ignorable": true
216+
},
217+
{
218+
"message": "Parameter #1 $nonEmpty of method Levels\\AcceptTypes\\AcceptNonEmpty::doBar() expects array&nonEmpty, array given.",
219+
"line": 735,
220+
"ignorable": true
211221
}
212222
]

tests/PHPStan/Levels/data/acceptTypes.php

+30
Original file line numberDiff line numberDiff line change
@@ -717,3 +717,33 @@ public function doBar(string $numericString): void
717717

718718
}
719719
}
720+
721+
class AcceptNonEmpty
722+
{
723+
724+
/**
725+
* @param array<mixed> $array
726+
* @param non-empty-array<mixed> $nonEmpty
727+
*/
728+
public function doFoo(
729+
array $array,
730+
array $nonEmpty
731+
): void
732+
{
733+
$this->doBar([]);
734+
$this->doBar([1, 2, 3]);
735+
$this->doBar($array);
736+
$this->doBar($nonEmpty);
737+
}
738+
739+
/**
740+
* @param non-empty-array<mixed> $nonEmpty
741+
*/
742+
public function doBar(
743+
array $nonEmpty
744+
): void
745+
{
746+
747+
}
748+
749+
}

tests/PHPStan/Rules/Comparison/StrictComparisonOfDifferentTypesRuleTest.php

+4-4
Original file line numberDiff line numberDiff line change
@@ -83,15 +83,15 @@ public function testStrictComparison(): void
8383
130,
8484
],
8585
[
86-
'Strict comparison using === between array and null will always evaluate to false.',
86+
'Strict comparison using === between array&nonEmpty and null will always evaluate to false.',
8787
140,
8888
],
8989
[
9090
'Strict comparison using !== between StrictComparison\Foo|null and 1 will always evaluate to true.',
9191
154,
9292
],
9393
[
94-
'Strict comparison using === between array and null will always evaluate to false.',
94+
'Strict comparison using === between array&nonEmpty and null will always evaluate to false.',
9595
164,
9696
],
9797
[
@@ -277,11 +277,11 @@ public function testStrictComparisonWithoutAlwaysTrue(): void
277277
98,
278278
],
279279
[
280-
'Strict comparison using === between array and null will always evaluate to false.',
280+
'Strict comparison using === between array&nonEmpty and null will always evaluate to false.',
281281
140,
282282
],
283283
[
284-
'Strict comparison using === between array and null will always evaluate to false.',
284+
'Strict comparison using === between array&nonEmpty and null will always evaluate to false.',
285285
164,
286286
],
287287
[

0 commit comments

Comments
 (0)