From a042b96da4fda2b3edcc0db105c340e99606d0cf Mon Sep 17 00:00:00 2001 From: aymericcucherousset Date: Sun, 19 Jul 2026 21:29:33 +0200 Subject: [PATCH] Add isListOf and isNonEmptyListOf assertions --- CHANGELOG.md | 7 + README.md | 2 + src/Assert.php | 47 ++++++ src/HasAssert.php | 8 ++ src/Mixin.php | 136 ++++++++++++++++++ tests/AssertTest.php | 21 +++ tests/static-analysis/assert-isListOf.php | 44 ++++++ .../assert-isNonEmptyListOf.php | 44 ++++++ .../non-return/assert-isListOf.php | 52 +++++++ .../non-return/assert-isNonEmptyListOf.php | 52 +++++++ 10 files changed, 413 insertions(+) create mode 100644 tests/static-analysis/assert-isListOf.php create mode 100644 tests/static-analysis/assert-isNonEmptyListOf.php create mode 100644 tests/static-analysis/non-return/assert-isListOf.php create mode 100644 tests/static-analysis/non-return/assert-isNonEmptyListOf.php diff --git a/CHANGELOG.md b/CHANGELOG.md index d62c758f..d2e52ec5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,13 @@ Changelog ========= +## Unreleased + +### Added + +- Added `isListOf` assertion. +- Added `isNonEmptyListOf` assertion. + ## 2.4.1 ### Fixed diff --git a/README.md b/README.md index fef4eda5..b865bb61 100644 --- a/README.md +++ b/README.md @@ -208,6 +208,8 @@ Method | Description `countBetween($array, $min, $max, $message = '')` | Check that an array has a count in the given range `isList($array, $message = '')` | Check that an array is a non-associative list `isNonEmptyList($array, $message = '')` | Check that an array is a non-associative list, and not empty +`isListOf($array, $class, $message = '')` | Check that an array is a non-associative list of instances of a given class +`isNonEmptyListOf($array, $class, $message = '')` | Check that an array is a non-associative list of instances of a given class, and not empty `isMap($array, $message = '')` | Check that an array is associative and has strings as keys `isNonEmptyMap($array, $message = '')` | Check that an array is associative and has strings as keys, and is not empty diff --git a/src/Assert.php b/src/Assert.php index 31c861b9..0568225e 100644 --- a/src/Assert.php +++ b/src/Assert.php @@ -2328,6 +2328,53 @@ public static function isNonEmptyList(mixed $array, string|callable $message = ' return $array; } + /** + * @psalm-pure + * + * @template T of object + * + * @psalm-assert list $array + * + * @param class-string $class + * @param string|callable():string $message + * + * @return list + * + * @throws InvalidArgumentException + */ + public static function isListOf(mixed $array, mixed $class, string|callable $message = ''): array + { + static::isList($array, $message); + + foreach ($array as $entry) { + static::isInstanceOf($entry, $class, $message); + } + + return $array; + } + + /** + * @psalm-pure + * + * @template T of object + * + * @psalm-assert non-empty-list $array + * + * @param class-string $class + * @param string|callable():string $message + * + * @return non-empty-list + * + * @throws InvalidArgumentException + */ + public static function isNonEmptyListOf(mixed $array, mixed $class, string|callable $message = ''): array + { + static::isListOf($array, $class, $message); + static::notEmpty($array, $message); + + return $array; + } + /** * @psalm-pure * diff --git a/src/HasAssert.php b/src/HasAssert.php index 11d144af..da42f0d3 100644 --- a/src/HasAssert.php +++ b/src/HasAssert.php @@ -169,6 +169,14 @@ final class HasAssert 'nullorisnonemptylist' => true, 'allisnonemptylist' => true, 'allnullorisnonemptylist' => true, + 'islistof' => true, + 'nullorislistof' => true, + 'allislistof' => true, + 'allnullorislistof' => true, + 'isnonemptylistof' => true, + 'nullorisnonemptylistof' => true, + 'allisnonemptylistof' => true, + 'allnullorisnonemptylistof' => true, 'ismap' => true, 'nullorismap' => true, 'allismap' => true, diff --git a/src/Mixin.php b/src/Mixin.php index a714f16c..8f2a4409 100644 --- a/src/Mixin.php +++ b/src/Mixin.php @@ -5466,6 +5466,142 @@ public static function allNullOrIsNonEmptyList(mixed $array, callable|string $me return $array; } + /** + * @psalm-pure + * + * @template T of object + * @psalm-assert list|null $array + * + * @param class-string $class + * @param string|callable():string $message + * + * @return list|null + * + * @throws InvalidArgumentException + */ + public static function nullOrIsListOf(mixed $array, mixed $class, callable|string $message = ''): ?array + { + null === $array || static::isListOf($array, $class, $message); + + return $array; + } + + /** + * @psalm-pure + * + * @template T of object + * @psalm-assert iterable> $array + * + * @param class-string $class + * @param string|callable():string $message + * + * @return iterable> + * + * @throws InvalidArgumentException + */ + public static function allIsListOf(mixed $array, mixed $class, callable|string $message = ''): iterable + { + static::isIterable($array); + + foreach ($array as $entry) { + static::isListOf($entry, $class, $message); + } + + return $array; + } + + /** + * @psalm-pure + * + * @template T of object|null + * @psalm-assert iterable|null> $array + * + * @param class-string $class + * @param string|callable():string $message + * + * @return iterable|null> + * + * @throws InvalidArgumentException + */ + public static function allNullOrIsListOf(mixed $array, mixed $class, callable|string $message = ''): iterable + { + static::isIterable($array); + + foreach ($array as $entry) { + null === $entry || static::isListOf($entry, $class, $message); + } + + return $array; + } + + /** + * @psalm-pure + * + * @template T of object + * @psalm-assert non-empty-list|null $array + * + * @param class-string $class + * @param string|callable():string $message + * + * @return non-empty-list|null + * + * @throws InvalidArgumentException + */ + public static function nullOrIsNonEmptyListOf(mixed $array, mixed $class, callable|string $message = ''): ?array + { + null === $array || static::isNonEmptyListOf($array, $class, $message); + + return $array; + } + + /** + * @psalm-pure + * + * @template T of object + * @psalm-assert iterable> $array + * + * @param class-string $class + * @param string|callable():string $message + * + * @return iterable> + * + * @throws InvalidArgumentException + */ + public static function allIsNonEmptyListOf(mixed $array, mixed $class, callable|string $message = ''): iterable + { + static::isIterable($array); + + foreach ($array as $entry) { + static::isNonEmptyListOf($entry, $class, $message); + } + + return $array; + } + + /** + * @psalm-pure + * + * @template T of object|null + * @psalm-assert iterable|null> $array + * + * @param class-string $class + * @param string|callable():string $message + * + * @return iterable|null> + * + * @throws InvalidArgumentException + */ + public static function allNullOrIsNonEmptyListOf(mixed $array, mixed $class, callable|string $message = ''): iterable + { + static::isIterable($array); + + foreach ($array as $entry) { + null === $entry || static::isNonEmptyListOf($entry, $class, $message); + } + + return $array; + } + /** * @psalm-pure * diff --git a/tests/AssertTest.php b/tests/AssertTest.php index f0ec064d..18ee2c5a 100644 --- a/tests/AssertTest.php +++ b/tests/AssertTest.php @@ -578,6 +578,17 @@ public static function getTests(): array ['isNonEmptyList', [[false]], true], ['isNonEmptyList', [[[1], [2]]], true], ['isNonEmptyList', [[['foo' => 'bar'], ['baz' => 'tab']]], true], + ['isListOf', [[new stdClass(), new stdClass()], 'stdClass'], true], + ['isListOf', [[], 'stdClass'], true], + ['isListOf', [[new stdClass(), new Exception()], 'stdClass'], false], + ['isListOf', [['foo' => new stdClass()], 'stdClass'], false], + ['isListOf', [[0 => new stdClass(), 2 => new stdClass()], 'stdClass'], false], + ['isListOf', [true, 'stdClass'], false], + ['isNonEmptyListOf', [[new stdClass(), new stdClass()], 'stdClass'], true], + ['isNonEmptyListOf', [[], 'stdClass'], false], + ['isNonEmptyListOf', [[new stdClass(), new Exception()], 'stdClass'], false], + ['isNonEmptyListOf', [['foo' => new stdClass()], 'stdClass'], false], + ['isNonEmptyListOf', [true, 'stdClass'], false], ['isMap', [['key' => 1, 'foo' => 2]], true], ['isMap', [[0 => 1, 2 => 3]], true], ['isMap', [[]], true], @@ -1083,6 +1094,16 @@ public static function getMethodsThatUseOtherMethods(): array 'args' => [111, 'stdClass', 'Value must be an instance of stdClass. Got: %s'], 'exceptionMessage' => 'Value must be an instance of stdClass. Got: integer', ], + [ + 'method' => 'isListOf', + 'args' => [[111], 'stdClass', 'Value must be an instance of stdClass. Got: %s'], + 'exceptionMessage' => 'Value must be an instance of stdClass. Got: integer', + ], + [ + 'method' => 'isNonEmptyListOf', + 'args' => [[111], 'stdClass', 'Value must be an instance of stdClass. Got: %s'], + 'exceptionMessage' => 'Value must be an instance of stdClass. Got: integer', + ], [ 'method' => 'notInstanceOf', 'args' => [111, 'stdClass', 'Value must be an instance of stdClass. Got: %s'], diff --git a/tests/static-analysis/assert-isListOf.php b/tests/static-analysis/assert-isListOf.php new file mode 100644 index 00000000..b16ccc0c --- /dev/null +++ b/tests/static-analysis/assert-isListOf.php @@ -0,0 +1,44 @@ + + */ +function isListOf(mixed $value): array +{ + return Assert::isListOf($value, stdClass::class); +} + +/** + * @psalm-pure + * + * @return null|list + */ +function nullOrIsListOf(mixed $value): ?array +{ + return Assert::nullOrIsListOf($value, stdClass::class); +} + +/** + * @psalm-pure + */ +function allIsListOf(mixed $value): iterable +{ + return Assert::allIsListOf($value, stdClass::class); +} + +/** + * @psalm-pure + */ +function allNullOrIsListOf(mixed $value): iterable +{ + return Assert::allNullOrIsListOf($value, stdClass::class); +} diff --git a/tests/static-analysis/assert-isNonEmptyListOf.php b/tests/static-analysis/assert-isNonEmptyListOf.php new file mode 100644 index 00000000..53c1d70e --- /dev/null +++ b/tests/static-analysis/assert-isNonEmptyListOf.php @@ -0,0 +1,44 @@ + + */ +function isNonEmptyListOf(mixed $value): array +{ + return Assert::isNonEmptyListOf($value, stdClass::class); +} + +/** + * @psalm-pure + * + * @return null|non-empty-list + */ +function nullOrIsNonEmptyListOf(mixed $value): ?array +{ + return Assert::nullOrIsNonEmptyListOf($value, stdClass::class); +} + +/** + * @psalm-pure + */ +function allIsNonEmptyListOf(mixed $value): iterable +{ + return Assert::allIsNonEmptyListOf($value, stdClass::class); +} + +/** + * @psalm-pure + */ +function allNullOrIsNonEmptyListOf(mixed $value): iterable +{ + return Assert::allNullOrIsNonEmptyListOf($value, stdClass::class); +} diff --git a/tests/static-analysis/non-return/assert-isListOf.php b/tests/static-analysis/non-return/assert-isListOf.php new file mode 100644 index 00000000..bfc2ec4a --- /dev/null +++ b/tests/static-analysis/non-return/assert-isListOf.php @@ -0,0 +1,52 @@ + + */ +function isListOf(mixed $value): array +{ + Assert::isListOf($value, stdClass::class); + + return $value; +} + +/** + * @psalm-pure + * + * @return null|list + */ +function nullOrIsListOf(mixed $value): ?array +{ + Assert::nullOrIsListOf($value, stdClass::class); + + return $value; +} + +/** + * @psalm-pure + */ +function allIsListOf(mixed $value): iterable +{ + Assert::allIsListOf($value, stdClass::class); + + return $value; +} + +/** + * @psalm-pure + */ +function allNullOrIsListOf(mixed $value): iterable +{ + Assert::allNullOrIsListOf($value, stdClass::class); + + return $value; +} diff --git a/tests/static-analysis/non-return/assert-isNonEmptyListOf.php b/tests/static-analysis/non-return/assert-isNonEmptyListOf.php new file mode 100644 index 00000000..8999b264 --- /dev/null +++ b/tests/static-analysis/non-return/assert-isNonEmptyListOf.php @@ -0,0 +1,52 @@ + + */ +function isNonEmptyListOf(mixed $value): array +{ + Assert::isNonEmptyListOf($value, stdClass::class); + + return $value; +} + +/** + * @psalm-pure + * + * @return null|non-empty-list + */ +function nullOrIsNonEmptyListOf(mixed $value): ?array +{ + Assert::nullOrIsNonEmptyListOf($value, stdClass::class); + + return $value; +} + +/** + * @psalm-pure + */ +function allIsNonEmptyListOf(mixed $value): iterable +{ + Assert::allIsNonEmptyListOf($value, stdClass::class); + + return $value; +} + +/** + * @psalm-pure + */ +function allNullOrIsNonEmptyListOf(mixed $value): iterable +{ + Assert::allNullOrIsNonEmptyListOf($value, stdClass::class); + + return $value; +}