Skip to content

Commit

Permalink
Support non-empty-array and non-empty-list
Browse files Browse the repository at this point in the history
  • Loading branch information
ondrejmirtes committed Oct 15, 2020
1 parent f9a6538 commit a4038b2
Show file tree
Hide file tree
Showing 10 changed files with 126 additions and 19 deletions.
35 changes: 28 additions & 7 deletions src/PhpDoc/TypeNodeResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
use PHPStan\Reflection\PassedByReference;
use PHPStan\Reflection\ReflectionProvider;
use PHPStan\Type\Accessory\AccessoryNumericStringType;
use PHPStan\Type\Accessory\NonEmptyArrayType;
use PHPStan\Type\ArrayType;
use PHPStan\Type\BenevolentUnionType;
use PHPStan\Type\BooleanType;
Expand Down Expand Up @@ -183,6 +184,12 @@ private function resolveIdentifierTypeNode(IdentifierTypeNode $typeNode, NameSco
case 'associative-array':
return new ArrayType(new MixedType(), new MixedType());

case 'non-empty-array':
return TypeCombinator::intersect(
new ArrayType(new MixedType(), new MixedType()),
new NonEmptyArrayType()
);

case 'iterable':
return new IterableType(new MixedType(), new MixedType());

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

case 'list':
return new ArrayType(new IntegerType(), new MixedType());
case 'non-empty-list':
return TypeCombinator::intersect(
new ArrayType(new IntegerType(), new MixedType()),
new NonEmptyArrayType()
);
}

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

if ($mainTypeName === 'array') {
if ($mainTypeName === 'array' || $mainTypeName === 'non-empty-array') {
if (count($genericTypes) === 1) { // array<ValueType>
return new ArrayType(new MixedType(true), $genericTypes[0]);

$arrayType = new ArrayType(new MixedType(true), $genericTypes[0]);
} elseif (count($genericTypes) === 2) { // array<KeyType, ValueType>
$arrayType = new ArrayType($genericTypes[0], $genericTypes[1]);
} else {
return new ErrorType();
}

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

} elseif ($mainTypeName === 'list') {
return $arrayType;
} elseif ($mainTypeName === 'list' || $mainTypeName === 'non-empty-list') {
if (count($genericTypes) === 1) { // list<ValueType>
return new ArrayType(new IntegerType(), $genericTypes[0]);
$listType = new ArrayType(new IntegerType(), $genericTypes[0]);
if ($mainTypeName === 'non-empty-list') {
return TypeCombinator::intersect($listType, new NonEmptyArrayType());
}

return $listType;
}

return new ErrorType();
Expand Down
1 change: 1 addition & 0 deletions src/Rules/RuleLevelHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ public function accepts(Type $acceptingType, Type $acceptedType, bool $strictTyp
if (
$acceptedType->isArray()->yes()
&& $acceptingType->isArray()->yes()
&& !$acceptingType->isIterableAtLeastOnce()->yes()
&& count(TypeUtils::getConstantArrays($acceptedType)) === 0
&& count(TypeUtils::getConstantArrays($acceptingType)) === 0
) {
Expand Down
10 changes: 3 additions & 7 deletions src/Type/Accessory/NonEmptyArrayType.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
namespace PHPStan\Type\Accessory;

use PHPStan\TrinaryLogic;
use PHPStan\Type\ArrayType;
use PHPStan\Type\CompoundType;
use PHPStan\Type\CompoundTypeHelper;
use PHPStan\Type\ErrorType;
Expand Down Expand Up @@ -35,8 +34,7 @@ public function accepts(Type $type, bool $strictTypes): TrinaryLogic
return CompoundTypeHelper::accepts($type, $this, $strictTypes);
}

return (new ArrayType(new MixedType(), new MixedType()))
->isSuperTypeOf($type)
return $type->isArray()
->and($type->isIterableAtLeastOnce());
}

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

return (new ArrayType(new MixedType(), new MixedType()))
->isSuperTypeOf($type)
return $type->isArray()
->and($type->isIterableAtLeastOnce());
}

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

return (new ArrayType(new MixedType(), new MixedType()))
->isSuperTypeOf($otherType)
return $otherType->isArray()
->and($otherType->isIterableAtLeastOnce())
->and($otherType instanceof self ? TrinaryLogic::createYes() : TrinaryLogic::createMaybe());
}
Expand Down
3 changes: 2 additions & 1 deletion src/Type/IntersectionType.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
use PHPStan\TrinaryLogic;
use PHPStan\Type\Accessory\AccessoryNumericStringType;
use PHPStan\Type\Accessory\AccessoryType;
use PHPStan\Type\Accessory\NonEmptyArrayType;
use PHPStan\Type\Generic\TemplateTypeMap;
use PHPStan\Type\Generic\TemplateTypeVariance;

Expand Down Expand Up @@ -133,7 +134,7 @@ function () use ($level): string {
function () use ($level): string {
$typeNames = [];
foreach ($this->types as $type) {
if ($type instanceof AccessoryType && !$type instanceof AccessoryNumericStringType) {
if ($type instanceof AccessoryType && !$type instanceof AccessoryNumericStringType && !$type instanceof NonEmptyArrayType) {
continue;
}
$typeNames[] = $type->describe($level);
Expand Down
5 changes: 5 additions & 0 deletions src/Type/VerbosityLevel.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace PHPStan\Type;

use PHPStan\Type\Accessory\AccessoryNumericStringType;
use PHPStan\Type\Accessory\NonEmptyArrayType;

class VerbosityLevel
{
Expand Down Expand Up @@ -64,6 +65,10 @@ public static function getRecommendedLevelByType(Type $type): self
$moreVerbose = true;
return $type;
}
if ($type instanceof NonEmptyArrayType) {
$moreVerbose = true;
return $type;
}
return $traverse($type);
});

Expand Down
6 changes: 6 additions & 0 deletions tests/PHPStan/Analyser/NodeScopeResolverTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -10171,6 +10171,11 @@ public function dataThrowExpression(): array
return $this->gatherAssertTypes(__DIR__ . '/data/throw-expr.php');
}

public function dataNotEmptyArray(): array
{
return $this->gatherAssertTypes(__DIR__ . '/data/non-empty-array.php');
}

/**
* @dataProvider dataBug2574
* @dataProvider dataBug2577
Expand Down Expand Up @@ -10249,6 +10254,7 @@ public function dataThrowExpression(): array
* @dataProvider dataBugFromPr339
* @dataProvider dataPow
* @dataProvider dataThrowExpression
* @dataProvider dataNotEmptyArray
* @param string $assertType
* @param string $file
* @param mixed ...$args
Expand Down
37 changes: 37 additions & 0 deletions tests/PHPStan/Analyser/data/non-empty-array.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<?php

namespace NonEmptyArray;

use function PHPStan\Analyser\assertType;

class Foo
{

/**
* @param non-empty-array $array
* @param non-empty-list $list
* @param non-empty-array<int, string> $arrayOfStrings
* @param non-empty-list<\stdClass> $listOfStd
* @param non-empty-list<\stdClass> $listOfStd2
* @param non-empty-list<string, \stdClass> $invalidList
*/
public function doFoo(
array $array,
array $list,
array $arrayOfStrings,
array $listOfStd,
$listOfStd2,
array $invalidList,
$invalidList2
): void
{
assertType('array&nonEmpty', $array);
assertType('array<int, mixed>&nonEmpty', $list);
assertType('array<int, string>&nonEmpty', $arrayOfStrings);
assertType('array<int, stdClass>&nonEmpty', $listOfStd);
assertType('array<int, stdClass>&nonEmpty', $listOfStd2);
assertType('array', $invalidList);
assertType('mixed', $invalidList2);
}

}
10 changes: 10 additions & 0 deletions tests/PHPStan/Levels/data/acceptTypes-5.json
Original file line number Diff line number Diff line change
Expand Up @@ -208,5 +208,15 @@
"message": "Parameter #1 $numericString of method Levels\\AcceptTypes\\NumericStrings::doBar() expects string&numeric, string given.",
"line": 708,
"ignorable": true
},
{
"message": "Parameter #1 $nonEmpty of method Levels\\AcceptTypes\\AcceptNonEmpty::doBar() expects array&nonEmpty, array() given.",
"line": 733,
"ignorable": true
},
{
"message": "Parameter #1 $nonEmpty of method Levels\\AcceptTypes\\AcceptNonEmpty::doBar() expects array&nonEmpty, array given.",
"line": 735,
"ignorable": true
}
]
30 changes: 30 additions & 0 deletions tests/PHPStan/Levels/data/acceptTypes.php
Original file line number Diff line number Diff line change
Expand Up @@ -717,3 +717,33 @@ public function doBar(string $numericString): void

}
}

class AcceptNonEmpty
{

/**
* @param array<mixed> $array
* @param non-empty-array<mixed> $nonEmpty
*/
public function doFoo(
array $array,
array $nonEmpty
): void
{
$this->doBar([]);
$this->doBar([1, 2, 3]);
$this->doBar($array);
$this->doBar($nonEmpty);
}

/**
* @param non-empty-array<mixed> $nonEmpty
*/
public function doBar(
array $nonEmpty
): void
{

}

}
Original file line number Diff line number Diff line change
Expand Up @@ -83,15 +83,15 @@ public function testStrictComparison(): void
130,
],
[
'Strict comparison using === between array and null will always evaluate to false.',
'Strict comparison using === between array&nonEmpty and null will always evaluate to false.',
140,
],
[
'Strict comparison using !== between StrictComparison\Foo|null and 1 will always evaluate to true.',
154,
],
[
'Strict comparison using === between array and null will always evaluate to false.',
'Strict comparison using === between array&nonEmpty and null will always evaluate to false.',
164,
],
[
Expand Down Expand Up @@ -277,11 +277,11 @@ public function testStrictComparisonWithoutAlwaysTrue(): void
98,
],
[
'Strict comparison using === between array and null will always evaluate to false.',
'Strict comparison using === between array&nonEmpty and null will always evaluate to false.',
140,
],
[
'Strict comparison using === between array and null will always evaluate to false.',
'Strict comparison using === between array&nonEmpty and null will always evaluate to false.',
164,
],
[
Expand Down

0 comments on commit a4038b2

Please sign in to comment.