diff --git a/src/PhpDoc/TypeNodeResolver.php b/src/PhpDoc/TypeNodeResolver.php index cf3fd8cf75..85563630d1 100644 --- a/src/PhpDoc/TypeNodeResolver.php +++ b/src/PhpDoc/TypeNodeResolver.php @@ -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; @@ -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()); @@ -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) { @@ -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 - return new ArrayType(new MixedType(true), $genericTypes[0]); - + $arrayType = new ArrayType(new MixedType(true), $genericTypes[0]); + } elseif (count($genericTypes) === 2) { // array + $arrayType = new ArrayType($genericTypes[0], $genericTypes[1]); + } else { + return new ErrorType(); } - if (count($genericTypes) === 2) { // array - 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 - 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(); diff --git a/src/Rules/RuleLevelHelper.php b/src/Rules/RuleLevelHelper.php index 8d2c0d2686..4cd5421378 100644 --- a/src/Rules/RuleLevelHelper.php +++ b/src/Rules/RuleLevelHelper.php @@ -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 ) { diff --git a/src/Type/Accessory/NonEmptyArrayType.php b/src/Type/Accessory/NonEmptyArrayType.php index dd74ae2419..bff07c0787 100644 --- a/src/Type/Accessory/NonEmptyArrayType.php +++ b/src/Type/Accessory/NonEmptyArrayType.php @@ -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; @@ -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()); } @@ -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()); } @@ -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()); } diff --git a/src/Type/IntersectionType.php b/src/Type/IntersectionType.php index e1af0ea9bd..f0467de8a2 100644 --- a/src/Type/IntersectionType.php +++ b/src/Type/IntersectionType.php @@ -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; @@ -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); diff --git a/src/Type/VerbosityLevel.php b/src/Type/VerbosityLevel.php index c43908acb1..e6b02d0e09 100644 --- a/src/Type/VerbosityLevel.php +++ b/src/Type/VerbosityLevel.php @@ -3,6 +3,7 @@ namespace PHPStan\Type; use PHPStan\Type\Accessory\AccessoryNumericStringType; +use PHPStan\Type\Accessory\NonEmptyArrayType; class VerbosityLevel { @@ -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); }); diff --git a/tests/PHPStan/Analyser/NodeScopeResolverTest.php b/tests/PHPStan/Analyser/NodeScopeResolverTest.php index c3c22e8f5a..a6cbfb599d 100644 --- a/tests/PHPStan/Analyser/NodeScopeResolverTest.php +++ b/tests/PHPStan/Analyser/NodeScopeResolverTest.php @@ -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 @@ -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 diff --git a/tests/PHPStan/Analyser/data/non-empty-array.php b/tests/PHPStan/Analyser/data/non-empty-array.php new file mode 100644 index 0000000000..584ce1290e --- /dev/null +++ b/tests/PHPStan/Analyser/data/non-empty-array.php @@ -0,0 +1,37 @@ + $arrayOfStrings + * @param non-empty-list<\stdClass> $listOfStd + * @param non-empty-list<\stdClass> $listOfStd2 + * @param non-empty-list $invalidList + */ + public function doFoo( + array $array, + array $list, + array $arrayOfStrings, + array $listOfStd, + $listOfStd2, + array $invalidList, + $invalidList2 + ): void + { + assertType('array&nonEmpty', $array); + assertType('array&nonEmpty', $list); + assertType('array&nonEmpty', $arrayOfStrings); + assertType('array&nonEmpty', $listOfStd); + assertType('array&nonEmpty', $listOfStd2); + assertType('array', $invalidList); + assertType('mixed', $invalidList2); + } + +} diff --git a/tests/PHPStan/Levels/data/acceptTypes-5.json b/tests/PHPStan/Levels/data/acceptTypes-5.json index 516d2cf03b..1321636907 100644 --- a/tests/PHPStan/Levels/data/acceptTypes-5.json +++ b/tests/PHPStan/Levels/data/acceptTypes-5.json @@ -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 } ] \ No newline at end of file diff --git a/tests/PHPStan/Levels/data/acceptTypes.php b/tests/PHPStan/Levels/data/acceptTypes.php index 33445b7fb4..b5766bad8f 100644 --- a/tests/PHPStan/Levels/data/acceptTypes.php +++ b/tests/PHPStan/Levels/data/acceptTypes.php @@ -717,3 +717,33 @@ public function doBar(string $numericString): void } } + +class AcceptNonEmpty +{ + + /** + * @param array $array + * @param non-empty-array $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 $nonEmpty + */ + public function doBar( + array $nonEmpty + ): void + { + + } + +} diff --git a/tests/PHPStan/Rules/Comparison/StrictComparisonOfDifferentTypesRuleTest.php b/tests/PHPStan/Rules/Comparison/StrictComparisonOfDifferentTypesRuleTest.php index 63605836b6..87cea09280 100644 --- a/tests/PHPStan/Rules/Comparison/StrictComparisonOfDifferentTypesRuleTest.php +++ b/tests/PHPStan/Rules/Comparison/StrictComparisonOfDifferentTypesRuleTest.php @@ -83,7 +83,7 @@ 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, ], [ @@ -91,7 +91,7 @@ public function testStrictComparison(): void 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, ], [ @@ -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, ], [