diff --git a/README.md b/README.md index bbfac0a..7c10111 100644 --- a/README.md +++ b/README.md @@ -212,9 +212,11 @@ Method | Description ### Function Assertions -Method | Description -------------------------------------------- | ----------------------------------------------------------------------------------------------------- -`throws($closure, $class, $message = '')` | Check that a function throws a certain exception. Subclasses of the exception class will be accepted. +Method | Description +------------------------------------------| ----------------------------------------------------------------------------------------------------- +`throws($closure, $class, $message = '')` | Check that a function throws a certain exception. Subclasses of the exception class will be accepted. +`isStatic($closure, $message = '')` | Check that a function is static. +`notStatic($closure, $message = '')` | Check that a function is not static. ### Collection Assertions diff --git a/src/Assert.php b/src/Assert.php index d585aa4..6a7e345 100644 --- a/src/Assert.php +++ b/src/Assert.php @@ -14,9 +14,11 @@ namespace Webmozart\Assert; use ArrayAccess; +use Closure; use Countable; use DateTime; use DateTimeImmutable; +use ReflectionFunction; use ReflectionProperty; use Throwable; use Traversable; @@ -2042,6 +2044,56 @@ public static function isMap(mixed $array, string $message = ''): array return $array; } + /** + * @param Closure $closure + * + * @psalm-pure + * + * @psalm-assert static Closure $closure + * + * @psalm-return static Closure + * + * @throws InvalidArgumentException + */ + public static function isStatic(mixed $closure, string $message = ''): Closure + { + static::isCallable($closure, $message); + $reflection = new ReflectionFunction($closure); + + if (!$reflection->isStatic()) { + static::reportInvalidArgument( + $message ?: 'Closure is not static.' + ); + } + + return $closure; + } + + /** + * @param Closure $closure + * + * @psalm-pure + * + * @psalm-assert Closure $closure + * + * @psalm-return Closure + * + * @throws InvalidArgumentException + */ + public static function notStatic(mixed $closure, string $message = ''): Closure + { + static::isCallable($closure, $message); + $reflection = new ReflectionFunction($closure); + + if ($reflection->isStatic()) { + static::reportInvalidArgument( + $message ?: 'Closure is not static.' + ); + } + + return $closure; + } + /** * @psalm-pure * diff --git a/src/Mixin.php b/src/Mixin.php index c076c4e..0780cc8 100644 --- a/src/Mixin.php +++ b/src/Mixin.php @@ -4955,6 +4955,130 @@ public static function allNullOrIsMap(mixed $array, string $message = ''): mixed return $array; } + /** + * @param Closure|null $closure + * + * @psalm-pure + * + * @psalm-assert static Closure|null $closure + * + * @return static Closure|null + * + * @throws InvalidArgumentException + */ + public static function nullOrIsStatic(mixed $closure, string $message = ''): mixed + { + null === $closure || static::isStatic($closure, $message); + + return $closure; + } + + /** + * @param iterable $closure + * + * @psalm-pure + * + * @psalm-assert iterable $closure + * + * @return iterable + * + * @throws InvalidArgumentException + */ + public static function allIsStatic(mixed $closure, string $message = ''): mixed + { + static::isIterable($closure); + + foreach ($closure as $entry) { + static::isStatic($entry, $message); + } + + return $closure; + } + + /** + * @param iterable $closure + * + * @psalm-pure + * + * @psalm-assert iterable $closure + * + * @return iterable + * + * @throws InvalidArgumentException + */ + public static function allNullOrIsStatic(mixed $closure, string $message = ''): mixed + { + static::isIterable($closure); + + foreach ($closure as $entry) { + null === $entry || static::isStatic($entry, $message); + } + + return $closure; + } + + /** + * @param Closure|null $closure + * + * @psalm-pure + * + * @psalm-assert Closure|null $closure + * + * @return Closure|null + * + * @throws InvalidArgumentException + */ + public static function nullOrNotStatic(mixed $closure, string $message = ''): mixed + { + null === $closure || static::notStatic($closure, $message); + + return $closure; + } + + /** + * @param iterable $closure + * + * @psalm-pure + * + * @psalm-assert iterable $closure + * + * @return iterable + * + * @throws InvalidArgumentException + */ + public static function allNotStatic(mixed $closure, string $message = ''): mixed + { + static::isIterable($closure); + + foreach ($closure as $entry) { + static::notStatic($entry, $message); + } + + return $closure; + } + + /** + * @param iterable $closure + * + * @psalm-pure + * + * @psalm-assert iterable $closure + * + * @return iterable + * + * @throws InvalidArgumentException + */ + public static function allNullOrNotStatic(mixed $closure, string $message = ''): mixed + { + static::isIterable($closure); + + foreach ($closure as $entry) { + null === $entry || static::notStatic($entry, $message); + } + + return $closure; + } + /** * @psalm-pure * diff --git a/tests/AssertTest.php b/tests/AssertTest.php index 4e801eb..7ec8c6f 100644 --- a/tests/AssertTest.php +++ b/tests/AssertTest.php @@ -621,6 +621,10 @@ public static function getTests(): array ['uniqueValues', [['qwerty', 'qwerty']], false], ['uniqueValues', [['asdfg', 'qwerty']], true], ['uniqueValues', [[123, '123']], false], + ['isStatic', [static function () {}], true], + ['isStatic', [function () {}], false], + ['notStatic', [static function () {}], false], + ['notStatic', [function () {}], true], ]; } diff --git a/tests/static-analysis/assert-isStatic.php b/tests/static-analysis/assert-isStatic.php new file mode 100644 index 0000000..d24cf02 --- /dev/null +++ b/tests/static-analysis/assert-isStatic.php @@ -0,0 +1,13 @@ +