diff --git a/.gitignore b/.gitignore index 650c8a0b..92ba894c 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ vendor/ composer.lock phpcs.xml phpunit.xml +/lsp/ diff --git a/.travis.yml b/.travis.yml index 4d44dfd4..c62bf6e2 100644 --- a/.travis.yml +++ b/.travis.yml @@ -18,3 +18,4 @@ install: script: - composer tests - composer coding-style + - composer types diff --git a/composer.json b/composer.json index fd0706b7..c70fa9af 100644 --- a/composer.json +++ b/composer.json @@ -15,7 +15,8 @@ } ], "require": { - "php": "~7" + "php": "~7", + "vimeo/psalm": "dev-master" }, "require-dev": { "phpunit/phpunit": "~7", @@ -124,6 +125,7 @@ }, "scripts": { "tests": "vendor/bin/phpunit", + "types": "vendor/bin/psalm", "coding-style": "vendor/bin/phpcs && vendor/bin/php-cs-fixer fix --dry-run --diff --config=.php_cs.dist", "clear": "rm -rf vendor/" } diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 19bea939..d9c67cab 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -29,11 +29,6 @@ beStrictAboutOutputDuringTests="true" verbose="true" colors="true"> - - - src/Functional - - @@ -42,8 +37,7 @@ - + @@ -53,5 +47,4 @@ - diff --git a/psalm.xml b/psalm.xml new file mode 100644 index 00000000..a21bb25d --- /dev/null +++ b/psalm.xml @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Functional/Average.php b/src/Functional/Average.php index b3029f34..e0569a6c 100644 --- a/src/Functional/Average.php +++ b/src/Functional/Average.php @@ -11,13 +11,13 @@ namespace Functional; use Functional\Exceptions\InvalidArgumentException; -use Traversable; /** * Returns the average of all numeric values in the array or null if no numeric value was found * - * @param Traversable|array $collection - * @return null|float|int + * @param iterable $collection + * @return numeric|null + * @psalm-pure */ function average($collection) { @@ -28,7 +28,7 @@ function average($collection) foreach ($collection as $element) { if (\is_numeric($element)) { - $sum += $element; + $sum = ($sum === null) ? $element : $sum + $element; ++$divisor; } } diff --git a/src/Functional/ButLast.php b/src/Functional/ButLast.php index 1899f604..23381767 100644 --- a/src/Functional/ButLast.php +++ b/src/Functional/ButLast.php @@ -11,13 +11,15 @@ namespace Functional; use Functional\Exceptions\InvalidArgumentException; -use Traversable; /** * Returns an array containing the elements of the list without its last element. * - * @param Traversable|array $collection - * @return array + * @template TKey of array-key + * @template TValue of mixed + * @param iterable $collection + * @return array + * @psalm-pure */ function but_last($collection) { diff --git a/src/Functional/Capture.php b/src/Functional/Capture.php index 341ca970..a324e736 100644 --- a/src/Functional/Capture.php +++ b/src/Functional/Capture.php @@ -13,13 +13,22 @@ /** * Return a new function that captures the return value of $callback in $result and returns the callbacks return value * - * @param callable $callback - * @param mixed $result - * @return callable + * @template TArg of mixed + * @template TResult of mixed + * @param callable(...TArg): TResult $callback + * @param-out TResult $result + * @param TResult $result + * @return callable(...TArg): TResult + * @psalm-pure */ function capture(callable $callback, &$result) { - return function (...$args) use ($callback, &$result) { + return + /** + * @param TArg $args + * @return TResult + */ + static function (...$args) use ($callback, &$result) { $result = $callback(...$args); return $result; diff --git a/src/Functional/CompareObjectHashOn.php b/src/Functional/CompareObjectHashOn.php index 6f59b13f..a48cc660 100644 --- a/src/Functional/CompareObjectHashOn.php +++ b/src/Functional/CompareObjectHashOn.php @@ -13,13 +13,23 @@ /** * Returns a comparison function that can be used with e.g. `usort()` * - * @param callable $comparison A function that compares the two values. Pick e.g. strcmp() or strnatcasecmp() - * @param callable $keyFunction A function that takes an argument and returns the value that should be compared - * @return callable + * @template TCompVal of scalar + * @template TObjIn extends object + * @template TObjOut extends object + * @param callable(TCompVal, TCompVal): int $comparison A function that compares the two values. Pick e.g. `strcmp()` or `strnatcasecmp()` + * @param callable(TObjIn): TObjOut $keyFunction A function that takes an argument and returns the value that should be compared + * @return callable(TObjIn, TObjIn): int + * @psalm-pure */ function compare_object_hash_on(callable $comparison, callable $keyFunction = null) { + /** @var callable(TObjIn): scalar $keyFunction */ $keyFunction = $keyFunction ? compose($keyFunction, 'spl_object_hash') : 'spl_object_hash'; + /** + * @fixme report issue with psalm + * @var callable(scalar, scalar): int $comparison + */ + return compare_on($comparison, $keyFunction); } diff --git a/src/Functional/CompareOn.php b/src/Functional/CompareOn.php index 8f506709..e93817c1 100644 --- a/src/Functional/CompareOn.php +++ b/src/Functional/CompareOn.php @@ -13,19 +13,37 @@ /** * Returns a comparison function that can be used with e.g. `usort()` * - * @param callable $comparison A function that compares the two values. Pick e.g. strcmp() or strnatcasecmp() - * @param callable $reducer A function that takes an argument and returns the value that should be compared - * @return callable + * @template TCompVal of scalar + * @template TVal + * @param callable(TCompVal, TCompVal): int $comparison A function that compares the two values. Pick e.g. `strcmp()` or `strnatcasecmp()` + * @param callable(TVal): TCompVal $reducer A function that takes an argument and returns the value that should be compared + * @return callable(TVal, TVal): int + * @fixme file psalm issue for "Argument 1 expects empty, … provided" (when not passing a reducer) + * @psalm-pure */ -function compare_on(callable $comparison, callable $reducer = null) +function compare_on(callable $comparison, callable $reducer = null): callable { if ($reducer === null) { - return static function ($left, $right) use ($comparison) { + return + /** + * @param TVal $left + * @param TVal $right + */ + static function ($left, $right) use ($comparison): int { + /** + * @var TCompVal $left + * @var TCompVal $right + */ return $comparison($left, $right); }; } - return static function ($left, $right) use ($reducer, $comparison) { + return + /** + * @param TVal $left + * @param TVal $right + */ + static function ($left, $right) use ($reducer, $comparison): int { return $comparison($reducer($left), $reducer($right)); }; } diff --git a/src/Functional/Compose.php b/src/Functional/Compose.php index 99ba32db..4b6edd24 100644 --- a/src/Functional/Compose.php +++ b/src/Functional/Compose.php @@ -15,15 +15,23 @@ * * @param callable ...$functions * @return callable - * @todo Add callable typehint when HHVM supports use of typehints with variadic arguments - * @see https://github.com/facebook/hhvm/issues/6954 + * @psalm-pure */ -function compose(...$functions) +function compose(callable ...$functions): callable { return \array_reduce( $functions, - function ($carry, $item) { - return function ($x) use ($carry, $item) { + /** + * @param mixed $carry + * @param mixed $item + */ + static function ($carry, $item) { + return + /** + * @param mixed $x + * @return mixed + */ + static function ($x) use ($carry, $item) { return $item($carry($x)); }; }, diff --git a/src/Functional/Concat.php b/src/Functional/Concat.php index 64fe8d57..0a19842c 100644 --- a/src/Functional/Concat.php +++ b/src/Functional/Concat.php @@ -13,8 +13,9 @@ /** * Concatenates zero or more strings * - * @param string[] ...$strings + * @param string $strings * @return string + * @psalm-pure */ function concat(string ...$strings) { diff --git a/src/Functional/ConstFunction.php b/src/Functional/ConstFunction.php index ae32d56a..93a349c5 100644 --- a/src/Functional/ConstFunction.php +++ b/src/Functional/ConstFunction.php @@ -13,12 +13,14 @@ /** * Wrap value within a function, which will return it, without any modifications. * - * @param mixed $value - * @return callable + * @template T + * @param T $value + * @return callable(): T + * @psalm-pure */ function const_function($value) { - return function () use ($value) { + return static function () use ($value) { return $value; }; } diff --git a/src/Functional/Contains.php b/src/Functional/Contains.php index f813f504..c41f1f74 100644 --- a/src/Functional/Contains.php +++ b/src/Functional/Contains.php @@ -11,16 +11,17 @@ namespace Functional; use Functional\Exceptions\InvalidArgumentException; -use Traversable; /** * Returns true if the collection contains the given value. If the third parameter is * true values will be compared in strict mode * - * @param Traversable|array $collection - * @param mixed $value + * @template TLookup + * @param iterable $collection + * @param TLookup $value * @param bool $strict * @return bool + * @psalm-pure */ function contains($collection, $value, $strict = true) { diff --git a/src/Functional/Converge.php b/src/Functional/Converge.php index 764526c7..af4f389f 100644 --- a/src/Functional/Converge.php +++ b/src/Functional/Converge.php @@ -16,13 +16,23 @@ * The results of each branching function are passed as arguments * to the converging function to produce the return value. * - * @param callable $convergingFunction Will be invoked with the return values of all branching functions as its arguments - * @param callable[] $branchingFunctions A list of functions - * @return callable A flipped version of the given function + * @template TArg + * @template TBranchReturn + * @template TConvergeReturn + * @template TConvergeFn of callable(...TArg): TBranchReturn + * @param callable(...TBranchReturn): TConvergeReturn $convergingFunction Will be invoked with the return values of all branching functions as its arguments + * @param list $branchingFunctions A list of functions + * @return callable(...TArg): TConvergeReturn + * @psalm-pure */ -function converge($convergingFunction, array $branchingFunctions) +function converge($convergingFunction, array $branchingFunctions): callable { - return function (...$values) use ($convergingFunction, $branchingFunctions) { + return + /** + * @param TArg $values + * @return TConvergeReturn + */ + static function (...$values) use ($convergingFunction, $branchingFunctions) { $result = []; foreach ($branchingFunctions as $branchingFunction) { diff --git a/src/Functional/Curry.php b/src/Functional/Curry.php index 97bb835e..5717c669 100644 --- a/src/Functional/Curry.php +++ b/src/Functional/Curry.php @@ -18,14 +18,20 @@ * Return a curryied version of the given function. You can decide if you also * want to curry optional parameters or not. * - * @param callable $function the function to curry + * @template TArg + * @template TReturn + * @param callable(...TArg): TReturn $function the function you want to curryg * @param bool $required curry optional parameters ? - * @return callable a curryied version of the given function + * @return callable(...TArg): callable a curryied version of the given function + * @return callable + * @psalm-pure */ -function curry(callable $function, $required = true) +function curry(callable $function, bool $required = true): callable { + /** @psalm-suppress ArgumentTypeCoercion */ if (\method_exists('Closure', 'fromCallable')) { // Closure::fromCallable was introduced in PHP 7.1 + /** @psalm-suppress InvalidArgument */ $reflection = new ReflectionFunction(Closure::fromCallable($function)); } else { if (\is_string($function) && \strpos($function, '::', 1) !== false) { @@ -35,6 +41,7 @@ function curry(callable $function, $required = true) } elseif (\is_object($function) && \method_exists($function, '__invoke')) { $reflection = new ReflectionMethod($function, '__invoke'); } else { + /** @psalm-suppress InvalidArgument */ $reflection = new ReflectionFunction($function); } } diff --git a/src/Functional/CurryN.php b/src/Functional/CurryN.php index f018ceca..0f9a2532 100644 --- a/src/Functional/CurryN.php +++ b/src/Functional/CurryN.php @@ -17,18 +17,31 @@ * If you give a smaller number you will have an error when calling the given function. If * you give a higher number, arguments will simply be ignored. * + * @template TArg + * @template TReturn * @param int $count number of arguments you want to curry - * @param callable $function the function you want to curry - * @return callable a curryied version of the given function + * @param callable(...TArg): TReturn $function the function you want to curry + * @return callable(...TArg): callable a curryied version of the given function + * @return callable + * @psalm-pure */ -function curry_n($count, callable $function) +function curry_n($count, callable $function): callable { - $accumulator = function (array $arguments) use ($count, $function, &$accumulator) { - return function (...$newArguments) use ($count, $function, $arguments, $accumulator) { + /** + * @param list $arguments + * @return callable(...TArg): callable + */ + $accumulator = static function (array $arguments) use ($count, $function, &$accumulator): callable { + return + /** + * @psalm-param TArg $newArguments + * @psalm-return callable(...TArg): callable + */ + static function (...$newArguments) use ($count, $function, $arguments, $accumulator) { $arguments = \array_merge($arguments, $newArguments); if ($count <= \count($arguments)) { - return \call_user_func_array($function, $arguments); + return $function(...$arguments); } return $accumulator($arguments); diff --git a/src/Functional/Difference.php b/src/Functional/Difference.php index 0c906f17..76e2b585 100644 --- a/src/Functional/Difference.php +++ b/src/Functional/Difference.php @@ -11,14 +11,14 @@ namespace Functional; use Functional\Exceptions\InvalidArgumentException; -use Traversable; /** * Takes a collection and returns the difference of all elements * - * @param Traversable|array $collection - * @param integer|float $initial - * @return integer|float + * @param iterable $collection + * @param numeric $initial + * @return numeric + * @psalm-pure */ function difference($collection, $initial = 0) { diff --git a/src/Functional/DropFirst.php b/src/Functional/DropFirst.php index 833ac6ea..b4da4f38 100644 --- a/src/Functional/DropFirst.php +++ b/src/Functional/DropFirst.php @@ -11,16 +11,18 @@ namespace Functional; use Functional\Exceptions\InvalidArgumentException; -use Traversable; /** * Drop all elements from a collection until callback returns false * - * @param Traversable|array $collection - * @param callable $callback - * @return array + * @template K of array-key + * @template V + * @param iterable $collection + * @param callable(V, K, iterable): bool $callback + * @return array + * @psalm-pure */ -function drop_first($collection, callable $callback) +function drop_first($collection, callable $callback): array { InvalidArgumentException::assertCollection($collection, __FUNCTION__, 1); diff --git a/src/Functional/DropLast.php b/src/Functional/DropLast.php index e6e7e6a9..fe1d8087 100644 --- a/src/Functional/DropLast.php +++ b/src/Functional/DropLast.php @@ -11,19 +11,20 @@ namespace Functional; use Functional\Exceptions\InvalidArgumentException; -use Traversable; /** * Drop all elements from a collection after callback returns true * - * @param Traversable|array $collection - * @param callable $callback - * @return array + * @template K of array-key + * @template V + * @param iterable $collection + * @param callable(V, K, iterable): bool $callback + * @return array + * @psalm-pure */ -function drop_last($collection, callable $callback) +function drop_last($collection, callable $callback): array { InvalidArgumentException::assertCollection($collection, __FUNCTION__, 1); - InvalidArgumentException::assertCallback($callback, __FUNCTION__, 2); $result = []; diff --git a/src/Functional/Each.php b/src/Functional/Each.php index 9065e44f..311dd1b0 100644 --- a/src/Functional/Each.php +++ b/src/Functional/Each.php @@ -11,15 +11,17 @@ namespace Functional; use Functional\Exceptions\InvalidArgumentException; -use Traversable; /** * Iterates over a collection of elements, yielding each in turn to a callback function. Each invocation of $callback * is called with three arguments: (element, index, collection) * - * @param Traversable|array $collection - * @param callable $callback - * @return null + * @template K of array-key + * @template V + * @param iterable $collection + * @param callable(V, K, iterable): void $callback + * @return void + * @psalm-pure */ function each($collection, callable $callback) { diff --git a/src/Functional/Equal.php b/src/Functional/Equal.php index e95d3e93..4862e097 100644 --- a/src/Functional/Equal.php +++ b/src/Functional/Equal.php @@ -14,12 +14,14 @@ * Performs an equal comparison * * @param mixed $b the value to compare to - * - * @return callable the function to perform the comparison + * @return callable(mixed): bool the function to perform the comparison + * @psalm-pure */ -function equal($b) +function equal($b): callable { - return function ($a) use ($b) { + return + /** @param mixed $a */ + static function ($a) use ($b): bool { return $a == $b; }; } diff --git a/src/Functional/ErrorToException.php b/src/Functional/ErrorToException.php index fb9693a5..c9c5c9af 100644 --- a/src/Functional/ErrorToException.php +++ b/src/Functional/ErrorToException.php @@ -15,16 +15,23 @@ /** * Takes a function and returns a new function that wraps the callback and rethrows PHP errors as exception * - * @param callable $callback - * @throws ErrorException Throws exception if PHP error happened - * @return mixed + * @template TArg + * @template TReturn + * @param callable(...TArg): TReturn $callback + * @return callable(...TArg): TReturn */ -function error_to_exception(callable $callback) +function error_to_exception(callable $callback): callable { - return function (...$arguments) use ($callback) { + return + /** + * @param TArg $arguments + * @throws ErrorException Throws exception if PHP error happened + * @return TReturn + */ + static function (...$arguments) use ($callback) { try { \set_error_handler( - static function ($level, $message, $file, $line) { + static function (int $level, string $message, string $file = '', int $line = 0): bool { throw new ErrorException($message, 0, $level, $file, $line); } ); diff --git a/src/Functional/Every.php b/src/Functional/Every.php index d2fcd845..a46efe6c 100644 --- a/src/Functional/Every.php +++ b/src/Functional/Every.php @@ -11,17 +11,19 @@ namespace Functional; use Functional\Exceptions\InvalidArgumentException; -use Traversable; /** * Returns true if every value in the collection passes the callback truthy test. Opposite of Functional\none(). * Callback arguments will be element, index, collection * - * @param Traversable|array $collection - * @param callable|null $callback + * @template K of array-key + * @template V + * @param iterable $collection + * @param callable(V, K, iterable): bool $callback * @return bool + * @psalm-pure */ -function every($collection, callable $callback = null) +function every($collection, callable $callback = null): bool { InvalidArgumentException::assertCollection($collection, __FUNCTION__, 1); diff --git a/src/Functional/Exceptions/InvalidArgumentException.php b/src/Functional/Exceptions/InvalidArgumentException.php index 5a64a3ac..6dcf6881 100644 --- a/src/Functional/Exceptions/InvalidArgumentException.php +++ b/src/Functional/Exceptions/InvalidArgumentException.php @@ -10,78 +10,55 @@ namespace Functional\Exceptions; +use ArrayAccess; +use Traversable; + class InvalidArgumentException extends \InvalidArgumentException { /** - * @param mixed $callback - * @param string $callee - * @param integer $parameterPosition + * @param mixed $collection + * @param callable-string $callee + * @param int $parameterPosition + * @psalm-assert iterable $collection * @throws InvalidArgumentException + * @return void + * @psalm-pure */ - public static function assertCallback($callback, $callee, $parameterPosition) - { - if (!\is_callable($callback)) { - if (!\is_array($callback) && !\is_string($callback)) { - throw new static( - \sprintf( - '%s() expected parameter %d to be a valid callback, no array, string, closure or functor given', - $callee, - $parameterPosition - ) - ); - } - - $type = \gettype($callback); - switch ($type) { - case 'array': - $type = 'method'; - $callback = \array_values($callback); - - $sep = '::'; - if (\is_object($callback[0])) { - $callback[0] = \get_class($callback[0]); - $sep = '->'; - } - - $callback = \implode($sep, $callback); - break; - - default: - $type = 'function'; - break; - } - - throw new static( - \sprintf( - "%s() expects parameter %d to be a valid callback, %s '%s' not found or invalid %s name", - $callee, - $parameterPosition, - $type, - $callback, - $type - ) - ); - } - } - - public static function assertCollection($collection, $callee, $parameterPosition) + public static function assertCollection($collection, $callee, int $parameterPosition = 0) { - self::assertCollectionAlike($collection, 'Traversable', $callee, $parameterPosition); + self::assertCollectionAlike($collection, Traversable::class, $callee, $parameterPosition); } - public static function assertArrayAccess($collection, $callee, $parameterPosition) + /** + * @param mixed $collection + * @param callable-string $callee + * @param int $parameterPosition + * @psalm-assert array|ArrayAccess $collection + * @throws InvalidArgumentException + * @return void + * @psalm-pure + */ + public static function assertArrayAccess($collection, string $callee, int $parameterPosition = 0) { - self::assertCollectionAlike($collection, 'ArrayAccess', $callee, $parameterPosition); + self::assertCollectionAlike($collection, ArrayAccess::class, $callee, $parameterPosition); } - public static function assertMethodName($methodName, $callee, $parameterPosition) + /** + * @param mixed $methodName + * @param callable-string $callee + * @param int $parameterPosition + * @psalm-assert string $methodName + * @throws InvalidArgumentException + * @return void + * @psalm-pure + */ + public static function assertMethodName($methodName, string $callee, int $parameterPosition = 0) { if (!\is_string($methodName)) { - throw new static( + throw new self( \sprintf( - '%s() expects parameter %d to be string, %s given', - $callee, - $parameterPosition, + '%s to be string, %s given', + self::getErrorMessage($callee, $parameterPosition), self::getType($methodName) ) ); @@ -89,41 +66,24 @@ public static function assertMethodName($methodName, $callee, $parameterPosition } /** - * @param mixed $propertyName - * @param string $callee - * @param integer $parameterPosition + * @param mixed $value + * @param callable-string $callee + * @param int $parameterPosition + * @psalm-assert int $value * @throws InvalidArgumentException + * @return void + * @psalm-pure */ - public static function assertPropertyName($propertyName, $callee, $parameterPosition) - { - if ( - !\is_string($propertyName) && - !\is_int($propertyName) && - !\is_float($propertyName) && - !\is_null($propertyName) - ) { - throw new static( - \sprintf( - '%s() expects parameter %d to be a valid property name or array index, %s given', - $callee, - $parameterPosition, - self::getType($propertyName) - ) - ); - } - } - - public static function assertPositiveInteger($value, $callee, $parameterPosition) + public static function assertPositiveInteger($value, string $callee, int $parameterPosition = 0) { if ((string)(int)$value !== (string)$value || $value < 0) { $type = self::getType($value); $type = $type === 'integer' ? 'negative integer' : $type; - throw new static( + throw new self( \sprintf( - '%s() expects parameter %d to be positive integer, %s given', - $callee, - $parameterPosition, + '%s to be positive integer, %s given', + self::getErrorMessage($callee, $parameterPosition), $type ) ); @@ -132,56 +92,27 @@ public static function assertPositiveInteger($value, $callee, $parameterPosition /** * @param mixed $key - * @param string $callee - * @throws static + * @param callable-string $callee + * @param int $parameterPosition + * @psalm-assert array-key $methodName + * @throws InvalidArgumentException + * @return void + * @psalm-pure */ - public static function assertValidArrayKey($key, $callee) + public static function assertValidArrayKey($key, $callee, int $parameterPosition = 0) { $keyTypes = ['NULL', 'string', 'integer', 'double', 'boolean']; $keyType = \gettype($key); if (!\in_array($keyType, $keyTypes, true)) { - throw new static( + throw new self( \sprintf( - '%s(): callback returned invalid array key of type "%s". Expected %4$s or %3$s', - $callee, - $keyType, + '%s to be a valid array key, "%s" given. Expected "%s" or "%s"', + self::getErrorMessage($callee, $parameterPosition), + self::getType($key), \array_pop($keyTypes), - \implode(', ', $keyTypes) - ) - ); - } - } - - public static function assertArrayKeyExists($collection, $key, $callee) - { - if (!isset($collection[$key])) { - throw new static( - \sprintf( - '%s(): unknown key "%s"', - $callee, - $key - ) - ); - } - } - - /** - * @param boolean $value - * @param string $callee - * @param integer $parameterPosition - * @throws InvalidArgumentException - */ - public static function assertBoolean($value, $callee, $parameterPosition) - { - if (!\is_bool($value)) { - throw new static( - \sprintf( - '%s() expects parameter %d to be boolean, %s given', - $callee, - $parameterPosition, - self::getType($value) + \implode('", "', $keyTypes) ) ); } @@ -189,39 +120,21 @@ public static function assertBoolean($value, $callee, $parameterPosition) /** * @param mixed $value - * @param string $callee - * @param integer $parameterPosition - * @throws InvalidArgumentException - */ - public static function assertInteger($value, $callee, $parameterPosition) - { - if (!\is_int($value)) { - throw new static( - \sprintf( - '%s() expects parameter %d to be integer, %s given', - $callee, - $parameterPosition, - self::getType($value) - ) - ); - } - } - - /** - * @param integer $value * @param integer $limit - * @param string $callee - * @param integer $parameterPosition + * @param callable-string $callee + * @param int $parameterPosition + * @psalm-assert int $value * @throws InvalidArgumentException + * @return void + * @psalm-pure */ - public static function assertIntegerGreaterThanOrEqual($value, $limit, $callee, $parameterPosition) + public static function assertIntegerGreaterThanOrEqual($value, $limit, string $callee, int $parameterPosition = 0) { if (!\is_int($value) || $value < $limit) { - throw new static( + throw new self( \sprintf( - '%s() expects parameter %d to be an integer greater than or equal to %d', - $callee, - $parameterPosition, + '%s to be an integer greater than or equal to %d', + self::getErrorMessage($callee, $parameterPosition), $limit ) ); @@ -229,30 +142,39 @@ public static function assertIntegerGreaterThanOrEqual($value, $limit, $callee, } /** - * @param integer $value + * @param mixed $value * @param integer $limit - * @param string $callee + * @param callable-string $callee * @param integer $parameterPosition + * @psalm-assert int $value * @throws InvalidArgumentException + * @return void + * @psalm-pure */ - public static function assertIntegerLessThanOrEqual($value, $limit, $callee, $parameterPosition) + public static function assertIntegerLessThanOrEqual($value, int $limit, string $callee, int $parameterPosition = 0) { if (!\is_int($value) || $value > $limit) { - throw new static( + throw new self( \sprintf( - '%s() expects parameter %d to be an integer less than or equal to %d', - $callee, - $parameterPosition, + '%s to be an integer less than or equal to %d', + self::getErrorMessage($callee, $parameterPosition), $limit ) ); } } - public static function assertResolvablePlaceholder(array $args, $position) + /** + * @param array $args + * @param int $position + * @throws InvalidArgumentException + * @return void + * @psalm-pure + */ + public static function assertResolvablePlaceholder(array $args, int $position) { if (\count($args) === 0) { - throw new static( + throw new self( \sprintf('Cannot resolve parameter placeholder at position %d. Parameter stack is empty.', $position) ); } @@ -260,19 +182,20 @@ public static function assertResolvablePlaceholder(array $args, $position) /** * @param mixed $collection - * @param string $className - * @param string $callee - * @param integer $parameterPosition + * @param class-string $className + * @param callable-string $callee + * @param int $parameterPosition * @throws InvalidArgumentException + * @return void + * @psalm-pure */ - private static function assertCollectionAlike($collection, $className, $callee, $parameterPosition) + private static function assertCollectionAlike($collection, $className, string $callee, int $parameterPosition = 0) { if (!\is_array($collection) && !$collection instanceof $className) { - throw new static( + throw new self( \sprintf( - '%s() expects parameter %d to be array or instance of %s, %s given', - $callee, - $parameterPosition, + '%s to be array or instance of %s, %s given', + self::getErrorMessage($callee, $parameterPosition), $className, self::getType($collection) ) @@ -280,15 +203,39 @@ private static function assertCollectionAlike($collection, $className, $callee, } } + /** + * @param mixed $value + * @return class-string|string + * @psalm-pure + */ + private static function getType($value): string + { + return \is_object($value) ? \get_class($value) : \gettype($value); + } + + /** + * @param callable-string $callee + * @param int $parameterPosition + * @return string + * @psalm-pure + */ + private static function getErrorMessage(string $callee, int $parameterPosition): string + { + $message = $parameterPosition === 0 + ? '%1$s() expects return value' + : '%1$s() expects parameter %2$d'; + + return \sprintf($message, $callee, $parameterPosition); + } + + /** + * @param mixed $value + * @param callable-string $callee + */ public static function assertNonZeroInteger($value, $callee) { if (!\is_int($value) || $value == 0) { throw new static(\sprintf('%s expected parameter %d to be non-zero', $callee, $value)); } } - - private static function getType($value) - { - return \is_object($value) ? \get_class($value) : \gettype($value); - } } diff --git a/src/Functional/Exceptions/MatchException.php b/src/Functional/Exceptions/MatchException.php index d111ff90..7f55bb10 100644 --- a/src/Functional/Exceptions/MatchException.php +++ b/src/Functional/Exceptions/MatchException.php @@ -12,6 +12,13 @@ class MatchException extends InvalidArgumentException { + /** + * @param array $conditions + * @param callable-string $callee + * @throw MatchException + * @return void + * @psalm-pure + */ public static function assert(array $conditions, $callee) { foreach ($conditions as $key => $condition) { @@ -21,10 +28,19 @@ public static function assert(array $conditions, $callee) } } + /** + * @param array-key $key + * @param mixed $condition + * @param callable-string $callee + * @psalm-assert array $condition + * @throw MatchException + * @return void + * @psalm-pure + */ private static function assertArray($key, $condition, $callee) { if (!\is_array($condition)) { - throw new static( + throw new self( \sprintf( '%s() expects condition at key %d to be array, %s given', $callee, @@ -35,10 +51,18 @@ private static function assertArray($key, $condition, $callee) } } + /** + * @param array-key $key + * @param array $condition + * @param callable-string $callee + * @throw MatchException + * @return void + * @psalm-pure + */ private static function assertLength($key, $condition, $callee) { if (\count($condition) < 2) { - throw new static( + throw new self( \sprintf( '%s() expects size of condition at key %d to be greater than or equals to 2, %d given', $callee, @@ -49,10 +73,18 @@ private static function assertLength($key, $condition, $callee) } } + /** + * @param array-key $key + * @param array $condition + * @param callable-string $callee + * @throw MatchException + * @return void + * @psalm-pure + */ private static function assertCallables($key, $condition, $callee) { if (!\is_callable($condition[0]) || !\is_callable($condition[1])) { - throw new static( + throw new self( \sprintf( '%s() expects first two items of condition at key %d to be callables', $callee, diff --git a/src/Functional/False.php b/src/Functional/False.php index cc0986b3..74928b20 100644 --- a/src/Functional/False.php +++ b/src/Functional/False.php @@ -11,15 +11,16 @@ namespace Functional; use Functional\Exceptions\InvalidArgumentException; -use Traversable; /** * Returns true if all elements of the collection are strictly false * - * @param Traversable|array $collection + * @template V of bool + * @param iterable $collection * @return bool + * @psalm-pure */ -function false($collection) +function false($collection): bool { InvalidArgumentException::assertCollection($collection, __FUNCTION__, 1); diff --git a/src/Functional/Falsy.php b/src/Functional/Falsy.php index 48558e48..42fe48c5 100644 --- a/src/Functional/Falsy.php +++ b/src/Functional/Falsy.php @@ -16,10 +16,11 @@ /** * Returns true if all elements of the collection evaluate to false * - * @param Traversable|array $collection + * @template V of mixed + * @param iterable $collection * @return bool */ -function falsy($collection) +function falsy($collection): bool { InvalidArgumentException::assertCollection($collection, __FUNCTION__, 1); diff --git a/src/Functional/Filter.php b/src/Functional/Filter.php index ec207d7e..908abbb7 100644 --- a/src/Functional/Filter.php +++ b/src/Functional/Filter.php @@ -11,18 +11,24 @@ namespace Functional; use Functional\Exceptions\InvalidArgumentException; -use Traversable; /** * Alias of Functional\select() * - * @param Traversable|array $collection - * @param callable $callback - * @return array + * @template K of array-key + * @template V + * @param iterable $collection + * @param callable(V, K, iterable): bool $callback + * @return array + * @psalm-pure */ -function filter($collection, callable $callback) +function filter($collection, callable $callback): array { InvalidArgumentException::assertCollection($collection, __FUNCTION__, 1); + /** + * @psalm-suppress InvalidScalarArgument + * @fixme report bug + */ return select($collection, $callback); } diff --git a/src/Functional/First.php b/src/Functional/First.php index 7bb0d8e2..a10b90ea 100644 --- a/src/Functional/First.php +++ b/src/Functional/First.php @@ -11,16 +11,18 @@ namespace Functional; use Functional\Exceptions\InvalidArgumentException; -use Traversable; /** * Looks through each element in the collection, returning the first one that passes a truthy test (callback). The * function returns as soon as it finds an acceptable element, and doesn't traverse the entire collection. Callback * arguments will be element, index, collection * - * @param Traversable|array $collection - * @param callable $callback - * @return mixed + * @template K of array-key + * @template V + * @param iterable $collection + * @param callable(V, K, iterable): bool $callback + * @return V|null + * @psalm-pure */ function first($collection, callable $callback = null) { diff --git a/src/Functional/FirstIndexOf.php b/src/Functional/FirstIndexOf.php index be812d5b..e07dffa6 100644 --- a/src/Functional/FirstIndexOf.php +++ b/src/Functional/FirstIndexOf.php @@ -11,14 +11,16 @@ namespace Functional; use Functional\Exceptions\InvalidArgumentException; -use Traversable; /** * Returns the first index holding specified value in the collection. Returns false if value was not found * - * @param Traversable|array $collection - * @param mixed $value - * @return mixed + * @template K of array-key + * @template V + * @param iterable $collection + * @param V|callable(V, K, iterable): bool $value + * @return K|false + * @psalm-pure */ function first_index_of($collection, $value) { diff --git a/src/Functional/FlatMap.php b/src/Functional/FlatMap.php index 8e52085a..302b5017 100644 --- a/src/Functional/FlatMap.php +++ b/src/Functional/FlatMap.php @@ -24,11 +24,16 @@ * then flat_map(collection, callback) will return [1,2,3,[4]] * while flatten(map(collection, callback)) will return [1,2,3,4] * - * @param Traversable|array $collection - * @param callable $callback - * @return array + * @template K of array-key + * @template V + * @template TMapped + * @template TMappedOrIterable of TMapped|iterable + * @param iterable $collection + * @param callable(V, K, iterable): TMappedOrIterable $callback + * @return list + * @psalm-pure */ -function flat_map($collection, callable $callback) +function flat_map($collection, callable $callback): array { InvalidArgumentException::assertCollection($collection, __FUNCTION__, 1); diff --git a/src/Functional/Flatten.php b/src/Functional/Flatten.php index a8568f03..0eca2cec 100644 --- a/src/Functional/Flatten.php +++ b/src/Functional/Flatten.php @@ -17,8 +17,13 @@ * Takes a nested combination of collections and returns their contents as a single, flat array. * Does not preserve indexes. * - * @param Traversable|array $collection - * @return array + * @template K of array-key + * @template V + * @template VFlat of scalar|object + * @fixme express "object but not traversable" + * @param iterable $collection + * @return list + * @psalm-pure */ function flatten($collection) { diff --git a/src/Functional/Flip.php b/src/Functional/Flip.php index ff59399d..8d4dca7b 100644 --- a/src/Functional/Flip.php +++ b/src/Functional/Flip.php @@ -15,12 +15,20 @@ * * If one argument is provided, it is passed to the function without change. * - * @param callable $callback the function you want to flip - * @return callable a flipped version of the given function + * @template TArg + * @template TReturn + * @param callable(...TArg): TReturn $callback the function you want to flip + * @return callable(...TArg): TReturn a flipped version of the given function + * @psalm-pure */ -function flip(callable $callback) +function flip(callable $callback): callable { - return function () use ($callback) { - return $callback(...\array_reverse(\func_get_args())); + return + /** + * @param TArg $args + * @return TReturn + */ + static function (...$args) use ($callback) { + return $callback(...\array_reverse($args)); }; } diff --git a/src/Functional/Functional.php b/src/Functional/Functional.php index 9795044c..9e0114c5 100644 --- a/src/Functional/Functional.php +++ b/src/Functional/Functional.php @@ -10,6 +10,11 @@ namespace Functional; +/** + * @psalm-mutation-free + * @psalm-external-mutation-free + * @psalm-immutable + */ final class Functional { diff --git a/src/Functional/GreaterThan.php b/src/Functional/GreaterThan.php index cd6cc377..ab83d224 100644 --- a/src/Functional/GreaterThan.php +++ b/src/Functional/GreaterThan.php @@ -13,12 +13,18 @@ /** * Returns true if $a is strictly greater than $b. * - * @param mixed $b - * @return \Closure(mixed) + * @template V of numeric + * @param V $b + * @return callable(V): bool + * @psalm-pure */ -function greater_than($b) +function greater_than($b): callable { - return function ($a) use ($b) { + return + /** + * @param V $a + */ + static function ($a) use ($b): bool { return $a > $b; }; } diff --git a/src/Functional/GreaterThanOrEqual.php b/src/Functional/GreaterThanOrEqual.php index baa3da39..d6cd700f 100644 --- a/src/Functional/GreaterThanOrEqual.php +++ b/src/Functional/GreaterThanOrEqual.php @@ -13,12 +13,18 @@ /** * Returns true if $a is greater than or equal to $b. * - * @param mixed $b - * @return \Closure(mixed) + * @template V of numeric + * @param V $b + * @return callable(V): bool + * @psalm-pure */ -function greater_than_or_equal($b) +function greater_than_or_equal($b): callable { - return function ($a) use ($b) { + return + /** + * @param V $a + */ + static function ($a) use ($b): bool { return $a >= $b; }; } diff --git a/src/Functional/Group.php b/src/Functional/Group.php index 88f53011..94b953fe 100644 --- a/src/Functional/Group.php +++ b/src/Functional/Group.php @@ -16,11 +16,15 @@ /** * Groups a collection by index returned by callback. * - * @param Traversable|array $collection - * @param callable $callback - * @return array + * @template K of array-key + * @template TGroup of array-key + * @template V + * @param iterable $collection + * @param callable(V, K, iterable): TGroup $callback + * @return array> + * @psalm-pure */ -function group($collection, callable $callback) +function group($collection, callable $callback): array { InvalidArgumentException::assertCollection($collection, __FUNCTION__, 1); diff --git a/src/Functional/Head.php b/src/Functional/Head.php index c7dd66a2..60e5c8a0 100644 --- a/src/Functional/Head.php +++ b/src/Functional/Head.php @@ -11,18 +11,24 @@ namespace Functional; use Functional\Exceptions\InvalidArgumentException; -use Traversable; /** * Alias for Functional\first * - * @param Traversable|array $collection - * @param callable $callback - * @return mixed + * @template K of array-key + * @template V + * @param iterable $collection + * @param callable(V, K, iterable): bool $callback + * @return V|null + * @psalm-pure */ function head($collection, callable $callback = null) { InvalidArgumentException::assertCollection($collection, __FUNCTION__, 1); + /** + * @psalm-suppress InvalidScalarArgument + * @fixme report bug + */ return first($collection, $callback); } diff --git a/src/Functional/Id.php b/src/Functional/Id.php index b4fef17d..ad5c4539 100644 --- a/src/Functional/Id.php +++ b/src/Functional/Id.php @@ -13,8 +13,10 @@ /** * Return value itself, without any modifications. * - * @param mixed $value - * @return mixed + * @template T + * @param T $value + * @return T + * @psalm-pure */ function id($value) { diff --git a/src/Functional/Identical.php b/src/Functional/Identical.php index 80b1c878..a5a63e23 100644 --- a/src/Functional/Identical.php +++ b/src/Functional/Identical.php @@ -13,12 +13,18 @@ /** * Returns true if $a is equal to $b, and they are of the same type. * - * @param mixed $b - * @return callable + * @template V + * @param V $b + * @return callable(V): bool + * @psalm-pure */ -function identical($b) +function identical($b): callable { - return function ($a) use ($b) { + return + /** + * @param V $a + */ + static function ($a) use ($b): bool { return $a === $b; }; } diff --git a/src/Functional/IfElse.php b/src/Functional/IfElse.php index c099adef..92b9d64c 100644 --- a/src/Functional/IfElse.php +++ b/src/Functional/IfElse.php @@ -13,15 +13,21 @@ /** * Performs an if/else condition over a value using functions as statements * - * @param callable $if the condition function - * @param callable $then function to call if condition is true - * @param callable $else function to call if condition is false - * - * @return mixed the return value of the given $then or $else functions + * @template V + * @template TReturn + * @param callable(V): bool $if the condition function + * @param callable(V): TReturn $then function to call if condition is true + * @param callable(V): TReturn $else function to call if condition is false + * @return callable(V): TReturn the return value of the given $then or $else functions + * @psalm-pure */ -function if_else(callable $if, callable $then, callable $else) +function if_else(callable $if, callable $then, callable $else): callable { - return function ($value) use ($if, $then, $else) { + return + /** + * @param V $value + */ + static function ($value) use ($if, $then, $else) { return $if($value) ? $then($value) : $else($value); }; } diff --git a/src/Functional/IndexesOf.php b/src/Functional/IndexesOf.php index 79135b2d..3fa56937 100644 --- a/src/Functional/IndexesOf.php +++ b/src/Functional/IndexesOf.php @@ -17,11 +17,14 @@ * Returns a list of array indexes, either matching the predicate or strictly equal to the the passed value. Returns an * empty array if no values were found. * - * @param Traversable|array $collection - * @param mixed|callable $value - * @return array + * @template K of array-key + * @template V + * @param iterable $collection + * @param V|callable(V, K, iterable): bool $value + * @return list + * @psalm-pure */ -function indexes_of($collection, $value) +function indexes_of($collection, $value): array { InvalidArgumentException::assertCollection($collection, __FUNCTION__, 1); diff --git a/src/Functional/Intersperse.php b/src/Functional/Intersperse.php index e5fe7ee7..bd3e1cd7 100644 --- a/src/Functional/Intersperse.php +++ b/src/Functional/Intersperse.php @@ -11,15 +11,16 @@ namespace Functional; use Functional\Exceptions\InvalidArgumentException; -use Traversable; /** * Insert a given value between each element of a collection. * Indices are not preserved. * - * @param Traversable|array $collection - * @param mixed $glue - * @return array + * @template V + * @param iterable $collection + * @param V $glue + * @return list + * @psalm-pure */ function intersperse($collection, $glue) { diff --git a/src/Functional/Invoke.php b/src/Functional/Invoke.php index 506fce92..7bd8bf02 100644 --- a/src/Functional/Invoke.php +++ b/src/Functional/Invoke.php @@ -11,16 +11,18 @@ namespace Functional; use Functional\Exceptions\InvalidArgumentException; -use Traversable; /** * Calls the method named by $methodName on each value in the collection. Any extra arguments passed to invoke will be * forwarded on to the method invocation. * - * @param Traversable|array $collection + * @template K of array-key + * @template V + * @param iterable $collection * @param string $methodName * @param array $arguments - * @return array + * @return array + * @psalm-pure */ function invoke($collection, $methodName, array $arguments = []) { diff --git a/src/Functional/InvokeFirst.php b/src/Functional/InvokeFirst.php index a8f9183f..d6eef0d6 100644 --- a/src/Functional/InvokeFirst.php +++ b/src/Functional/InvokeFirst.php @@ -11,17 +11,18 @@ namespace Functional; use Functional\Exceptions\InvalidArgumentException; -use Traversable; /** * Calls the method named by $methodName on first object in the collection containing a callable method named * $methodName. Any extra arguments passed to invoke will be forwarded on to the method invocation. * - * @param Traversable|array $collection + * @template K of array-key + * @template V + * @param iterable $collection * @param string $methodName * @param array $arguments - * * @return mixed + * @psalm-pure */ function invoke_first($collection, $methodName, array $arguments = []) { diff --git a/src/Functional/InvokeIf.php b/src/Functional/InvokeIf.php index 85e3aa81..2f3e2570 100644 --- a/src/Functional/InvokeIf.php +++ b/src/Functional/InvokeIf.php @@ -14,11 +14,12 @@ * Calls the method named by $methodName on $object. Any extra arguments passed to invoke_if will be * forwarded on to the method invocation. If $method is not callable on $object, $defaultValue is returned. * - * @param mixed $object + * @param object $object * @param string $methodName * @param array $methodArguments * @param mixed $defaultValue * @return mixed + * @psalm-pure */ function invoke_if($object, $methodName, array $methodArguments = [], $defaultValue = null) { diff --git a/src/Functional/InvokeLast.php b/src/Functional/InvokeLast.php index 88a06c06..ce16db9a 100644 --- a/src/Functional/InvokeLast.php +++ b/src/Functional/InvokeLast.php @@ -10,17 +10,19 @@ namespace Functional; -use Traversable; use Functional\Exceptions\InvalidArgumentException; /** * Calls the method named by $methodName on last object in the collection containing a callable method named * $methodName. Any extra arguments passed to invoke will be forwarded on to the method invocation. * - * @param Traversable|array $collection + * @template K of array-key + * @template V + * @param iterable $collection * @param string $methodName * @param array $arguments * @return mixed + * @psalm-pure */ function invoke_last($collection, $methodName, array $arguments = []) { diff --git a/src/Functional/Invoker.php b/src/Functional/Invoker.php index d7d80f4d..badb6857 100644 --- a/src/Functional/Invoker.php +++ b/src/Functional/Invoker.php @@ -10,7 +10,6 @@ namespace Functional; -use Traversable; use Functional\Exceptions\InvalidArgumentException; /** @@ -18,13 +17,19 @@ * * @param string $methodName * @param array $arguments - * @return callable + * @return callable(object): mixed + * @psalm-pure */ function invoker($methodName, array $arguments = []) { InvalidArgumentException::assertMethodName($methodName, __FUNCTION__, 1); - return static function ($object) use ($methodName, $arguments) { + return + /** + * @param object $object + * @return mixed + */ + static function ($object) use ($methodName, $arguments) { return $object->{$methodName}(...$arguments); }; } diff --git a/src/Functional/Last.php b/src/Functional/Last.php index c49d660a..416f336f 100644 --- a/src/Functional/Last.php +++ b/src/Functional/Last.php @@ -17,9 +17,12 @@ * Looks through each element in the collection, returning the last one that passes a truthy test (callback). * Callback arguments will be element, index, collection * - * @param Traversable|array $collection - * @param callable $callback - * @return mixed + * @template K of array-key + * @template V + * @param iterable $collection + * @param callable(V, K, iterable): bool $callback + * @return V|null + * @psalm-pure */ function last($collection, callable $callback = null) { diff --git a/src/Functional/LastIndexOf.php b/src/Functional/LastIndexOf.php index cfb4c7e5..3febd090 100644 --- a/src/Functional/LastIndexOf.php +++ b/src/Functional/LastIndexOf.php @@ -11,14 +11,16 @@ namespace Functional; use Functional\Exceptions\InvalidArgumentException; -use Traversable; /** * Returns the last index holding specified value in the collection. Returns false if value was not found * - * @param Traversable|array $collection - * @param mixed $value - * @return mixed + * @template K of array-key + * @template V + * @param iterable $collection + * @param V|callable(V, K, iterable): bool $value + * @return K|false + * @psalm-pure */ function last_index_of($collection, $value) { diff --git a/src/Functional/LessThan.php b/src/Functional/LessThan.php index c5ff4af3..4a4d22b8 100644 --- a/src/Functional/LessThan.php +++ b/src/Functional/LessThan.php @@ -13,12 +13,18 @@ /** * Returns true if $a is strictly less than $b. * - * @param mixed $b - * @return \Closure(mixed) + * @template V of numeric + * @param V $b + * @return callable(V): bool + * @psalm-pure */ -function less_than($b) +function less_than($b): callable { - return function ($a) use ($b) { + return + /** + * @param V $a + */ + static function ($a) use ($b): bool { return $a < $b; }; } diff --git a/src/Functional/LessThanOrEqual.php b/src/Functional/LessThanOrEqual.php index dd2e9fe9..9951d579 100644 --- a/src/Functional/LessThanOrEqual.php +++ b/src/Functional/LessThanOrEqual.php @@ -13,12 +13,18 @@ /** * Returns true if $a is less than or equal to $b. * - * @param mixed $b - * @return \Closure(mixed) + * @template V of numeric + * @param V $b + * @return callable(V): bool + * @psalm-pure */ -function less_than_or_equal($b) +function less_than_or_equal($b): callable { - return function ($a) use ($b) { + return + /** + * @param V $a + */ + static function ($a) use ($b): bool { return $a <= $b; }; } diff --git a/src/Functional/LexicographicCompare.php b/src/Functional/LexicographicCompare.php index 9b056710..2c2bfbd8 100644 --- a/src/Functional/LexicographicCompare.php +++ b/src/Functional/LexicographicCompare.php @@ -14,12 +14,18 @@ * Returns an integer less than, equal to, or greater than zero when * $a is respectively less than, equal to, or greater than $b. * - * @param mixed $b - * @return \Closure(mixed) + * @template V + * @param V $b + * @return callable(V): int + * @psalm-pure */ -function lexicographic_compare($b) +function lexicographic_compare($b): callable { - return function ($a) use ($b) { + return + /** + * @param V $a + */ + static function ($a) use ($b): int { return $a <=> $b; }; } diff --git a/src/Functional/Map.php b/src/Functional/Map.php index a4b1fa8f..15d12d55 100644 --- a/src/Functional/Map.php +++ b/src/Functional/Map.php @@ -11,17 +11,20 @@ namespace Functional; use Functional\Exceptions\InvalidArgumentException; -use Traversable; /** * Produces a new array of elements by mapping each element in collection through a transformation function (callback). * Callback arguments will be element, index, collection * - * @param Traversable|array $collection - * @param callable $callback - * @return array + * @template K of array-key + * @template V + * @template TMapped + * @param iterable $collection + * @param callable(V, K, iterable): TMapped $callback + * @return array + * @psalm-pure */ -function map($collection, callable $callback) +function map($collection, callable $callback): array { InvalidArgumentException::assertCollection($collection, __FUNCTION__, 1); diff --git a/src/Functional/Match.php b/src/Functional/Match.php index 83d3b2a3..ce299d0b 100644 --- a/src/Functional/Match.php +++ b/src/Functional/Match.php @@ -19,15 +19,21 @@ /** * Performs an operation checking for the given conditions * - * @param array $conditions the conditions to check against - * - * @return callable|null the function that calls the callable of the first truthy condition + * @template V + * @template TReturn + * @param array> $conditions the conditions to check against + * @return callable(V): TReturn the function that calls the callable of the first truthy condition + * @psalm-pure */ -function match(array $conditions) +function match(array $conditions): callable { MatchException::assert($conditions, __FUNCTION__); - return function ($value) use ($conditions) { + return + /** + * @param V $value + */ + static function ($value) use ($conditions) { if (empty($conditions)) { return null; } diff --git a/src/Functional/Maximum.php b/src/Functional/Maximum.php index f2c4596f..44dbb2cb 100644 --- a/src/Functional/Maximum.php +++ b/src/Functional/Maximum.php @@ -11,13 +11,13 @@ namespace Functional; use Functional\Exceptions\InvalidArgumentException; -use Traversable; /** * Returns the maximum value of a collection * - * @param Traversable|array $collection - * @return integer|float + * @param iterable $collection + * @return numeric|null + * @psalm-pure */ function maximum($collection) { diff --git a/src/Functional/Memoize.php b/src/Functional/Memoize.php index 4a65bf50..b974b85b 100644 --- a/src/Functional/Memoize.php +++ b/src/Functional/Memoize.php @@ -15,10 +15,12 @@ /** * Memoizes callbacks and returns their value instead of calling them * - * @param callable|null $callback Callable closure or function. Pass null to reset memory - * @param array $arguments Arguments + * @template TArgs + * @template TReturn + * @param callable(...TArgs): TReturn|null $callback Callable closure or function. Pass null to reset memory + * @param TArgs|callable(): string $arguments Arguments * @param array|string $key Optional memoize key to override the auto calculated hash - * @return mixed + * @return TReturn */ function memoize(callable $callback = null, $arguments = [], $key = null) { @@ -39,7 +41,9 @@ function memoize(callable $callback = null, $arguments = [], $key = null) static $keyGenerator = null; if (!$keyGenerator) { - $keyGenerator = function ($value) use (&$keyGenerator) { + + /** @psalm-suppress MissingClosureParamType */ + $keyGenerator = static function ($value) use (&$keyGenerator): string { $type = \gettype($value); if ($type === 'array') { $key = \join(':', map($value, $keyGenerator)); @@ -54,6 +58,7 @@ function memoize(callable $callback = null, $arguments = [], $key = null) } if ($key === null) { + /** @var list $arguments */ $key = $keyGenerator(\array_merge([$callback], $arguments)); } elseif (\is_callable($key)) { $key = $keyGenerator($key()); @@ -62,6 +67,7 @@ function memoize(callable $callback = null, $arguments = [], $key = null) } if (!isset($storage[$key]) && !\array_key_exists($key, $storage)) { + /** @var list $arguments */ $storage[$key] = $callback(...$arguments); } diff --git a/src/Functional/Minimum.php b/src/Functional/Minimum.php index d0d67fb8..78cbda73 100644 --- a/src/Functional/Minimum.php +++ b/src/Functional/Minimum.php @@ -11,13 +11,13 @@ namespace Functional; use Functional\Exceptions\InvalidArgumentException; -use Traversable; /** * Returns the minimum value of a collection * - * @param Traversable|array $collection - * @return integer|float + * @param iterable $collection + * @return numeric|null + * @psalm-pure */ function minimum($collection) { diff --git a/src/Functional/None.php b/src/Functional/None.php index 34704737..4f842b14 100644 --- a/src/Functional/None.php +++ b/src/Functional/None.php @@ -11,17 +11,19 @@ namespace Functional; use Functional\Exceptions\InvalidArgumentException; -use Traversable; /** * Returns true if all of the elements in the collection pass the callback falsy test. Opposite of Functional\all(). * Callback arguments will be element, index, collection. * - * @param Traversable|array $collection - * @param callable|null $callback + * @template K of array-key + * @template V + * @param iterable $collection + * @param callable(V, K, iterable): bool $callback * @return bool + * @psalm-pure */ -function none($collection, callable $callback = null) +function none($collection, callable $callback = null): bool { InvalidArgumentException::assertCollection($collection, __FUNCTION__, 1); diff --git a/src/Functional/Noop.php b/src/Functional/Noop.php index ece00c2a..45cfe682 100644 --- a/src/Functional/Noop.php +++ b/src/Functional/Noop.php @@ -26,6 +26,9 @@ /** * A no-operation function. + * + * @return void|null|mixed + * @psalm-pure */ function noop() { diff --git a/src/Functional/Not.php b/src/Functional/Not.php index 97a5abce..c8e44d10 100644 --- a/src/Functional/Not.php +++ b/src/Functional/Not.php @@ -13,12 +13,18 @@ /** * Logical negation of the given $function * - * @param callable $function The function to run value against - * @return callable A negation version on the given $function + * @template V + * @param callable(V): bool $function The function to run value against + * @return callable(V): bool A negation version on the given $function + * @psalm-pure */ -function not(callable $function) +function not(callable $function): callable { - return function ($value) use ($function) { + return + /** + * @param V $value + */ + static function ($value) use ($function): bool { return !$function($value); }; } diff --git a/src/Functional/OmitKeys.php b/src/Functional/OmitKeys.php index 506cbebe..af3b7e91 100644 --- a/src/Functional/OmitKeys.php +++ b/src/Functional/OmitKeys.php @@ -16,9 +16,12 @@ /** * Returns an array with the specified keys omitted from the array * - * @param Traversable|array $collection - * @param array $keys - * @return array + * @template K of array-key + * @template V + * @param iterable $collection + * @param list $keys + * @return array + * @psalm-pure */ function omit_keys($collection, array $keys) { @@ -30,5 +33,8 @@ function omit_keys($collection, array $keys) $array = $collection; } - return \array_diff_key($array, \array_flip($keys)); + /** @var array $list */ + $list = \array_diff_key($array, \array_flip($keys)); + + return $list; } diff --git a/src/Functional/PartialAny.php b/src/Functional/PartialAny.php index 172adefb..eec9d581 100644 --- a/src/Functional/PartialAny.php +++ b/src/Functional/PartialAny.php @@ -17,13 +17,21 @@ * * Use Functional\…, Functional\…() or Functional\placeholder() as a placeholder * - * @param callable $callback - * @param mixed ...$arguments - * @return callable + * @template TArgs + * @template TArgsOrPlaceholder + * @template TReturn + * @param callable(...TArgsOrPlaceholder, ...TArgs): TReturn $callback + * @param TArgsOrPlaceholder $arguments + * @return callable(...TArgs): TReturn + * @psalm-pure */ -function partial_any(callable $callback, ...$arguments) +function partial_any(callable $callback, ...$arguments): callable { - return function (...$innerArguments) use ($callback, $arguments) { + return + /** + * @param TArgs $innerArguments + */ + static function (...$innerArguments) use ($callback, $arguments) { $placeholder = …(); foreach ($arguments as $position => &$argument) { @@ -37,9 +45,10 @@ function partial_any(callable $callback, ...$arguments) }; } -/** @return resource */ -function …() +/** @psalm-pure */ +function …(): string { + /** @psalm-suppress ImpureStaticVariable */ static $placeholder; if (!$placeholder) { @@ -49,9 +58,8 @@ function …() return $placeholder; } - -/** @return resource */ -function placeholder() +/** @psalm-pure */ +function placeholder(): string { return …(); } diff --git a/src/Functional/PartialLeft.php b/src/Functional/PartialLeft.php index 6ab523c2..771dcaab 100644 --- a/src/Functional/PartialLeft.php +++ b/src/Functional/PartialLeft.php @@ -13,15 +13,22 @@ /** * Return a new function with the arguments partially applied starting from the left side * - * Use Functional\…, Functional\…() or Functional\placeholder() as a placeholder - * - * @param callable $callback - * @param array ...$arguments - * @return callable + * @template TArg1 + * @template TArg2 + * @template TReturn + * @param callable(...TArg1, ...TArg2): TReturn $callback + * @param TArg1 $arguments + * @return callable(...TArg2): TReturn + * @psalm-pure */ -function partial_left(callable $callback, ...$arguments) +function partial_left(callable $callback, ...$arguments): callable { - return function (...$innerArguments) use ($callback, $arguments) { + return + /** + * @param TArg2 $innerArguments + */ + static function (...$innerArguments) use ($callback, $arguments) { + /** @var TArg1 $arguments */ return $callback(...\array_merge($arguments, $innerArguments)); }; } diff --git a/src/Functional/PartialMethod.php b/src/Functional/PartialMethod.php index 9415a59f..5531e635 100644 --- a/src/Functional/PartialMethod.php +++ b/src/Functional/PartialMethod.php @@ -18,13 +18,18 @@ * @param string $methodName * @param array $arguments * @param mixed $defaultValue - * @return callable + * @return callable(object): mixed + * @psalm-pure */ -function partial_method($methodName, array $arguments = [], $defaultValue = null) +function partial_method(string $methodName, array $arguments = [], $defaultValue = null): callable { InvalidArgumentException::assertMethodName($methodName, __FUNCTION__, 1); - return function ($object) use ($methodName, $arguments, $defaultValue) { + return + /** + * @param object $object + */ + static function ($object) use ($methodName, $arguments, $defaultValue) { if (!\is_callable([$object, $methodName])) { return $defaultValue; } diff --git a/src/Functional/PartialRight.php b/src/Functional/PartialRight.php index 6c0ac3fb..c3d181d5 100644 --- a/src/Functional/PartialRight.php +++ b/src/Functional/PartialRight.php @@ -13,13 +13,22 @@ /** * Return a new function with the arguments partially applied starting from the right * - * @param callable $callback - * @param array ...$arguments - * @return callable + * @template TArg1 + * @template TArg2 + * @template TReturn + * @param callable(...TArg1, ...TArg2): TReturn $callback + * @param TArg2 $arguments + * @return callable(...TArg1): TReturn + * @psalm-pure */ -function partial_right(callable $callback, ...$arguments) +function partial_right(callable $callback, ...$arguments): callable { - return function (...$innerArguments) use ($callback, $arguments) { + return + /** + * @param TArg1 $innerArguments + */ + static function (...$innerArguments) use ($callback, $arguments) { + /** @var TArg2 $arguments */ return $callback(...\array_merge($innerArguments, $arguments)); }; } diff --git a/src/Functional/Partition.php b/src/Functional/Partition.php index 83340754..281c241d 100644 --- a/src/Functional/Partition.php +++ b/src/Functional/Partition.php @@ -22,11 +22,14 @@ * Elements are not re-ordered and have the same index they had in the * original array. * - * @param Traversable|array $collection - * @param callable ...$callbacks - * @return array + * @template K of array-key + * @template V + * @param iterable $collection + * @param callable(V, K, iterable): bool $callbacks + * @return array> + * @psalm-pure */ -function partition($collection, callable ...$callbacks) +function partition($collection, callable ...$callbacks): array { InvalidArgumentException::assertCollection($collection, __FUNCTION__, 1); diff --git a/src/Functional/Pick.php b/src/Functional/Pick.php index 8c840aa0..4d0dd497 100644 --- a/src/Functional/Pick.php +++ b/src/Functional/Pick.php @@ -17,11 +17,15 @@ * Pick a single element from a collection of objects or arrays by index. * If no such index exists, return the default value. * - * @param ArrayAccess|array $collection - * @param mixed $index - * @param mixed $default - * @param callable $callback Custom function to check if index exists, default function is "isset" - * @return mixed + * @template TCollection of ArrayAccess|array + * @template TIndex of array-key + * @template TDefault + * @param TCollection $collection + * @param TIndex $index + * @param TDefault $default + * @param callable(TCollection, TIndex): bool $callback Custom function to check if index exists, default function is "isset" + * @return TCollection[TIndex]|TDefault + * @psalm-pure */ function pick($collection, $index, $default = null, callable $callback = null) { diff --git a/src/Functional/Pluck.php b/src/Functional/Pluck.php index b687f01c..ebf750b1 100644 --- a/src/Functional/Pluck.php +++ b/src/Functional/Pluck.php @@ -12,19 +12,19 @@ use ArrayAccess; use Functional\Exceptions\InvalidArgumentException; -use Traversable; /** * Extract a property from a collection of objects. * - * @param Traversable|array $collection - * @param string $propertyName - * @return array + * @param iterable $collection + * @param array-key $propertyName + * @return array + * @psalm-pure */ function pluck($collection, $propertyName) { InvalidArgumentException::assertCollection($collection, __FUNCTION__, 1); - InvalidArgumentException::assertPropertyName($propertyName, __FUNCTION__, 2); + InvalidArgumentException::assertValidArrayKey($propertyName, __FUNCTION__, 2); $aggregation = []; diff --git a/src/Functional/Poll.php b/src/Functional/Poll.php index ca48fcb3..9b4f51e6 100644 --- a/src/Functional/Poll.php +++ b/src/Functional/Poll.php @@ -19,13 +19,13 @@ /** * Retry a callback until it returns a truthy value or the timeout (in microseconds) is reached * - * @param callable $callback + * @param callable(int, int): mixed $callback Callback receives retry count and delay and returns truthy value on success * @param integer $timeout Timeout in microseconds - * @param Traversable|null $delaySequence Default: no delay between calls + * @param iterable $delaySequence Default: no delay between calls * @throws InvalidArgumentException - * @return boolean + * @return mixed|false Truthy value on success, false on timeout */ -function poll(callable $callback, $timeout, Traversable $delaySequence = null) +function poll(callable $callback, int $timeout, iterable $delaySequence = null) { InvalidArgumentException::assertIntegerGreaterThanOrEqual($timeout, 0, __FUNCTION__, 2); @@ -33,7 +33,12 @@ function poll(callable $callback, $timeout, Traversable $delaySequence = null) $delays = new AppendIterator(); if ($delaySequence) { - $delays->append(new InfiniteIterator($delaySequence)); + /** @psalm-suppress ArgumentTypeCoercion */ + $delays->append( + new InfiniteIterator( + $delaySequence instanceof Traversable ? $delaySequence : new ArrayIterator($delaySequence) + ) + ); } $delays->append(new InfiniteIterator(new ArrayIterator([0]))); @@ -56,4 +61,7 @@ function poll(callable $callback, $timeout, Traversable $delaySequence = null) ++$retry; } + + // Never reached because of InfiniteIterator + return false; } diff --git a/src/Functional/Product.php b/src/Functional/Product.php index ccd3a87e..f9ee5830 100644 --- a/src/Functional/Product.php +++ b/src/Functional/Product.php @@ -11,14 +11,14 @@ namespace Functional; use Functional\Exceptions\InvalidArgumentException; -use Traversable; /** * Takes a collection and returns the product of all elements * - * @param Traversable|array $collection - * @param integer|float $initial - * @return integer|float + * @param iterable $collection + * @param numeric $initial + * @return numeric + * @psalm-pure */ function product($collection, $initial = 1) { diff --git a/src/Functional/Ratio.php b/src/Functional/Ratio.php index 85b690c9..3b8e20d4 100644 --- a/src/Functional/Ratio.php +++ b/src/Functional/Ratio.php @@ -11,14 +11,14 @@ namespace Functional; use Functional\Exceptions\InvalidArgumentException; -use Traversable; /** * Takes a collection and returns the quotient of all elements * - * @param Traversable|array $collection - * @param integer|float $initial - * @return integer|float + * @param iterable $collection + * @param numeric $initial + * @return numeric + * @psalm-pure */ function ratio($collection, $initial = 1) { diff --git a/src/Functional/ReduceLeft.php b/src/Functional/ReduceLeft.php index e86d9b6d..25f81c6d 100644 --- a/src/Functional/ReduceLeft.php +++ b/src/Functional/ReduceLeft.php @@ -14,10 +14,14 @@ use Traversable; /** - * @param Traversable|array $collection - * @param callable $callback - * @param mixed $initial - * @return mixed + * @template K of array-key + * @template V + * @template R + * @param iterable $collection + * @param callable(V, K, iterable, R): R $callback + * @param R $initial + * @return R + * @psalm-pure */ function reduce_left($collection, callable $callback, $initial = null) { diff --git a/src/Functional/ReduceRight.php b/src/Functional/ReduceRight.php index 05148322..dac08119 100644 --- a/src/Functional/ReduceRight.php +++ b/src/Functional/ReduceRight.php @@ -11,13 +11,16 @@ namespace Functional; use Functional\Exceptions\InvalidArgumentException; -use Traversable; /** - * @param Traversable|array $collection - * @param callable $callback - * @param mixed $initial - * @return mixed + * @template K of array-key + * @template V + * @template R + * @param iterable $collection + * @param callable(V, K, iterable, R): R $callback + * @param R $initial + * @return R + * @psalm-pure */ function reduce_right($collection, callable $callback, $initial = null) { diff --git a/src/Functional/Reindex.php b/src/Functional/Reindex.php index 7c5dbcc1..31a2789e 100644 --- a/src/Functional/Reindex.php +++ b/src/Functional/Reindex.php @@ -17,11 +17,15 @@ * Produces a new array of elements by assigning the values to keys generated by a transformation function (callback). * Callback arguments will be element, index, collection * - * @param Traversable|array $collection - * @param callable $callback - * @return array + * @template K of array-key + * @template K2 of array-key + * @template V + * @param iterable $collection + * @param callable(V, K, iterable): K2 $callback + * @return array + * @psalm-pure */ -function reindex($collection, callable $callback) +function reindex($collection, callable $callback): array { InvalidArgumentException::assertCollection($collection, __FUNCTION__, 1); diff --git a/src/Functional/Reject.php b/src/Functional/Reject.php index 1a3bbe40..c7eae58d 100644 --- a/src/Functional/Reject.php +++ b/src/Functional/Reject.php @@ -17,11 +17,14 @@ * Returns the elements in list without the elements that the truthy test (callback) passes. The opposite of * Functional\select(). Callback arguments will be element, index, collection * - * @param Traversable|array $collection - * @param callable|null $callback - * @return array + * @template K of array-key + * @template V + * @param iterable $collection + * @param callable(V, K, iterable): bool $callback + * @return array + * @psalm-pure */ -function reject($collection, callable $callback = null) +function reject($collection, callable $callback = null): array { InvalidArgumentException::assertCollection($collection, __FUNCTION__, 1); diff --git a/src/Functional/Repeat.php b/src/Functional/Repeat.php index 4d810271..a47436fe 100644 --- a/src/Functional/Repeat.php +++ b/src/Functional/Repeat.php @@ -16,13 +16,18 @@ /** * Creates a function that can be used to repeat the execution of $callback. * - * @param callable $callback - * - * @return Closure + * @param callable(): void $callback + * @return callable(int): void + * @psalm-pure */ -function repeat(callable $callback) +function repeat(callable $callback): callable { - return function ($times) use ($callback) { + return + /** + * @param int $times + * @return void + */ + static function (int $times) use ($callback) { InvalidArgumentException::assertPositiveInteger($times, __FUNCTION__, 1); for ($i = 0; $i < $times; $i++) { diff --git a/src/Functional/Retry.php b/src/Functional/Retry.php index 02a74bf6..6382ee4c 100644 --- a/src/Functional/Retry.php +++ b/src/Functional/Retry.php @@ -34,6 +34,7 @@ function retry(callable $callback, $retries, Traversable $delaySequence = null) if ($delaySequence) { $delays = new AppendIterator(); + /** @psalm-suppress ArgumentTypeCoercion */ $delays->append(new InfiniteIterator($delaySequence)); $delays->append(new InfiniteIterator(new ArrayIterator([0]))); $delays = new LimitIterator($delays, $retries); diff --git a/src/Functional/Select.php b/src/Functional/Select.php index 1e6b59ec..66c4901e 100644 --- a/src/Functional/Select.php +++ b/src/Functional/Select.php @@ -11,17 +11,19 @@ namespace Functional; use Functional\Exceptions\InvalidArgumentException; -use Traversable; /** * Looks through each element in the list, returning an array of all the elements that pass a truthy test (callback). * Opposite is Functional\reject(). Callback arguments will be element, index, collection * - * @param Traversable|array $collection - * @param callable|null $callback - * @return array + * @template K of array-key + * @template V + * @param iterable $collection + * @param callable(V, K, iterable): bool $callback + * @return array + * @psalm-pure */ -function select($collection, callable $callback = null) +function select($collection, callable $callback = null): array { InvalidArgumentException::assertCollection($collection, __FUNCTION__, 1); diff --git a/src/Functional/SelectKeys.php b/src/Functional/SelectKeys.php index e7eec114..7a84fc88 100644 --- a/src/Functional/SelectKeys.php +++ b/src/Functional/SelectKeys.php @@ -16,11 +16,14 @@ /** * Select the specified keys from the array * - * @param Traversable|array $collection - * @param array $keys - * @return array + * @template K of array-key + * @template V + * @param iterable $collection + * @param list $keys + * @return array + * @psalm-pure */ -function select_keys($collection, array $keys) +function select_keys($collection, array $keys): array { InvalidArgumentException::assertCollection($collection, __FUNCTION__, 1); @@ -30,5 +33,8 @@ function select_keys($collection, array $keys) $array = $collection; } - return \array_intersect_key($array, \array_flip($keys)); + /** @var array $list */ + $list = \array_intersect_key($array, \array_flip($keys)); + + return $list; } diff --git a/src/Functional/SequenceConstant.php b/src/Functional/SequenceConstant.php index f21dc70a..18bdb766 100644 --- a/src/Functional/SequenceConstant.php +++ b/src/Functional/SequenceConstant.php @@ -13,15 +13,15 @@ use ArrayIterator; use Functional\Exceptions\InvalidArgumentException; use InfiniteIterator; -use Traversable; /** * Returns an infinite, traversable sequence of constant values * * @param integer $value - * @return Traversable + * @return iterable + * @psalm-pure */ -function sequence_constant($value) +function sequence_constant(int $value): iterable { InvalidArgumentException::assertIntegerGreaterThanOrEqual($value, 0, __FUNCTION__, 1); diff --git a/src/Functional/SequenceExponential.php b/src/Functional/SequenceExponential.php index 899d7c1b..08137329 100644 --- a/src/Functional/SequenceExponential.php +++ b/src/Functional/SequenceExponential.php @@ -16,12 +16,13 @@ /** * Returns an infinite, traversable sequence that exponentially grows by given percentage * - * @param integer $start - * @param integer $percentage Integer between 1 and 100 - * @return ExponentialSequence + * @param int $start + * @param int $percentage Integer between 1 and 100 + * @return iterable * @throws InvalidArgumentException + * @psalm-pure */ -function sequence_exponential($start, $percentage) +function sequence_exponential(int $start, int $percentage): iterable { InvalidArgumentException::assertIntegerGreaterThanOrEqual($start, 1, __METHOD__, 1); InvalidArgumentException::assertIntegerGreaterThanOrEqual($percentage, 1, __METHOD__, 2); diff --git a/src/Functional/SequenceLinear.php b/src/Functional/SequenceLinear.php index 052ce51b..d66dbc00 100644 --- a/src/Functional/SequenceLinear.php +++ b/src/Functional/SequenceLinear.php @@ -12,19 +12,18 @@ use Functional\Exceptions\InvalidArgumentException; use Functional\Sequences\LinearSequence; -use Traversable; /** * Returns an infinite, traversable sequence that linearly grows by given amount * - * @param integer $start - * @param integer $amount - * @return Traversable + * @param int $start + * @param int $amount + * @return iterable + * @psalm-pure */ -function sequence_linear($start, $amount) +function sequence_linear(int $start, int $amount): iterable { InvalidArgumentException::assertIntegerGreaterThanOrEqual($start, 0, __FUNCTION__, 1); - InvalidArgumentException::assertInteger($amount, __FUNCTION__, 2); return new LinearSequence($start, $amount); } diff --git a/src/Functional/Sequences/ExponentialSequence.php b/src/Functional/Sequences/ExponentialSequence.php index d70b9340..3b53c1c5 100644 --- a/src/Functional/Sequences/ExponentialSequence.php +++ b/src/Functional/Sequences/ExponentialSequence.php @@ -13,21 +13,34 @@ use Functional\Exceptions\InvalidArgumentException; use Iterator; +/** + * @psalm-external-mutation-free + */ class ExponentialSequence implements Iterator { - /** @var integer */ + /** + * @psalm-readonly + * @var integer + */ private $start; - /** @var integer */ + /** + * @var integer + * @psalm-readonly + */ private $percentage; - /** @var integer */ - private $value; + /** + * @var integer + */ + private $value = 0; - /** @var integer */ - private $times; + /** + * @var integer + */ + private $times = 0; - public function __construct($start, $percentage) + public function __construct(int $start, int $percentage) { InvalidArgumentException::assertIntegerGreaterThanOrEqual($start, 1, __METHOD__, 1); InvalidArgumentException::assertIntegerGreaterThanOrEqual($percentage, 1, __METHOD__, 2); @@ -37,23 +50,34 @@ public function __construct($start, $percentage) $this->percentage = $percentage; } - public function current() + /** + * @psalm-mutation-free + */ + public function current(): int { return $this->value; } public function next() { - $this->value = (int) \round(\pow($this->start * (1 + $this->percentage / 100), $this->times)); + $this->value = (int) \round(($this->start * (1 + $this->percentage / 100)) ** $this->times); $this->times++; } + /** + * @psalm-pure + * @psalm-mutation-free + */ public function key() { return null; } - public function valid() + /** + * @psalm-pure + * @psalm-mutation-free + */ + public function valid(): bool { return true; } diff --git a/src/Functional/Sequences/LinearSequence.php b/src/Functional/Sequences/LinearSequence.php index 7ba59d99..53ddc677 100644 --- a/src/Functional/Sequences/LinearSequence.php +++ b/src/Functional/Sequences/LinearSequence.php @@ -13,27 +13,38 @@ use Functional\Exceptions\InvalidArgumentException; use Iterator; +/** + * @psalm-external-mutation-free + */ class LinearSequence implements Iterator { - /** @var integer */ + /** + * @psalm-readonly + * @var integer + */ private $start; - /** @var integer */ + /** + * @psalm-readonly + * @var integer + */ private $amount; /** @var integer */ - private $value; + private $value = 0; - public function __construct($start, $amount) + public function __construct(int $start, int $amount) { InvalidArgumentException::assertIntegerGreaterThanOrEqual($start, 0, __METHOD__, 1); - InvalidArgumentException::assertInteger($amount, __METHOD__, 2); $this->start = $start; $this->amount = $amount; } - public function current() + /** + * @psalm-mutation-free + */ + public function current(): int { return $this->value; } @@ -43,12 +54,20 @@ public function next() $this->value += $this->amount; } + /** + * @psalm-pure + * @psalm-mutation-free + */ public function key() { - return 0; + return null; } - public function valid() + /** + * @psalm-pure + * @psalm-mutation-free + */ + public function valid(): bool { return true; } diff --git a/src/Functional/Some.php b/src/Functional/Some.php index 5583dac4..ad6a2052 100644 --- a/src/Functional/Some.php +++ b/src/Functional/Some.php @@ -17,11 +17,14 @@ * Returns true if some of the elements in the collection pass the callback truthy test. Short-circuits and stops * traversing the collection if a truthy element is found. Callback arguments will be value, index, collection * - * @param Traversable|array $collection - * @param callable|null $callback + * @template K of array-key + * @template V + * @param iterable $collection + * @param callable(V, K, iterable): bool $callback * @return bool + * @psalm-pure */ -function some($collection, callable $callback = null) +function some($collection, callable $callback = null): bool { InvalidArgumentException::assertCollection($collection, __FUNCTION__, 1); diff --git a/src/Functional/Sort.php b/src/Functional/Sort.php index 1706b921..91e64ab5 100644 --- a/src/Functional/Sort.php +++ b/src/Functional/Sort.php @@ -16,15 +16,17 @@ /** * Sorts a collection with a user-defined function, optionally preserving array keys * - * @param Traversable|array $collection - * @param callable $callback + * @template K of array-key + * @template V + * @param iterable $collection + * @param callable(V, V, iterable): int $callback * @param bool $preserveKeys - * @return array + * @return list|array + * @psalm-pure */ -function sort($collection, callable $callback, $preserveKeys = false) +function sort($collection, callable $callback, bool $preserveKeys = false): array { InvalidArgumentException::assertCollection($collection, __FUNCTION__, 1); - InvalidArgumentException::assertBoolean($preserveKeys, __FUNCTION__, 3); if ($collection instanceof Traversable) { $array = \iterator_to_array($collection); @@ -34,9 +36,17 @@ function sort($collection, callable $callback, $preserveKeys = false) $fn = $preserveKeys ? 'uasort' : 'usort'; - $fn($array, function ($left, $right) use ($callback, $collection) { - return $callback($left, $right, $collection); - }); + $fn( + $array, + /** + * @param V $left + * @param V $right + */ + static function ($left, $right) use ($callback, $collection) { + /** @var iterable $collection */ + return $callback($left, $right, $collection); + } + ); return $array; } diff --git a/src/Functional/Sum.php b/src/Functional/Sum.php index f79f5de5..6797ebdb 100644 --- a/src/Functional/Sum.php +++ b/src/Functional/Sum.php @@ -11,14 +11,14 @@ namespace Functional; use Functional\Exceptions\InvalidArgumentException; -use Traversable; /** * Takes a collection and returns the sum of the elements * - * @param Traversable|array $collection - * @param integer|float $initial - * @return integer|float + * @param iterable $collection + * @param numeric $initial + * @return numeric + * @psalm-pure */ function sum($collection, $initial = 0) { diff --git a/src/Functional/SuppressError.php b/src/Functional/SuppressError.php index 06a18ca8..213524f2 100644 --- a/src/Functional/SuppressError.php +++ b/src/Functional/SuppressError.php @@ -12,20 +12,25 @@ use ErrorException; -use function Functional\const_function; - /** * Takes a function and returns a new function that wraps the callback and suppresses the PHP error * - * @param callable $callback - * @throws ErrorException Throws exception if PHP error happened - * @return mixed + * @template TArg + * @template TReturn + * @param callable(...TArg): TReturn $callback + * @return callable(...TArg): TReturn */ -function suppress_error(callable $callback) +function suppress_error(callable $callback): callable { - return function (...$arguments) use ($callback) { + return + /** + * @param TArg $arguments + * @return TReturn + */ + static function (...$arguments) use ($callback) { try { - \set_error_handler(const_function(null)); + \set_error_handler(static function () { + }); return $callback(...$arguments); } finally { diff --git a/src/Functional/Tail.php b/src/Functional/Tail.php index e390d8ea..b3ddc5ee 100644 --- a/src/Functional/Tail.php +++ b/src/Functional/Tail.php @@ -11,17 +11,19 @@ namespace Functional; use Functional\Exceptions\InvalidArgumentException; -use Traversable; /** * Returns all items from $collection except first element (head). Preserves $collection keys. * Takes an optional callback for filtering the collection. * - * @param Traversable|array $collection - * @param callable $callback - * @return array + * @template K of array-key + * @template V + * @param iterable $collection + * @param callable(V, K, iterable): bool $callback + * @return array + * @psalm-pure */ -function tail($collection, callable $callback = null) +function tail($collection, callable $callback = null): array { InvalidArgumentException::assertCollection($collection, __FUNCTION__, 1); diff --git a/src/Functional/TailRecursion.php b/src/Functional/TailRecursion.php index 0ce4f43a..aabac087 100644 --- a/src/Functional/TailRecursion.php +++ b/src/Functional/TailRecursion.php @@ -16,16 +16,24 @@ * I took the solution from here https://gist.github.com/beberlei/4145442 * but reworked it and made without classes. * - * @param callable $fn - * @return callable + * @template TArg + * @template TReturn + * @param callable(...TArg): TReturn $fn + * @return callable(...TArg): TReturn + * @psalm-pure */ function tail_recursion(callable $fn): callable { $underCall = false; $queue = []; - return function (...$args) use (&$fn, &$underCall, &$queue) { + return + /** + * @param TArg $args + */ + static function (...$args) use (&$fn, &$underCall, &$queue) { $result = null; $queue[] = $args; + if (!$underCall) { $underCall = true; while ($head = \array_shift($queue)) { @@ -33,6 +41,7 @@ function tail_recursion(callable $fn): callable } $underCall = false; } + return $result; }; } diff --git a/src/Functional/TakeLeft.php b/src/Functional/TakeLeft.php index 961009f3..f5e3d148 100644 --- a/src/Functional/TakeLeft.php +++ b/src/Functional/TakeLeft.php @@ -17,12 +17,14 @@ * Creates a slice of $collection with $count elements taken from the beginning. If the collection has less than $count * elements, the whole collection will be returned as an array. * - * @param Traversable|array $collection + * @template K of array-key + * @template V + * @param iterable $collection * @param int $count - * - * @return array + * @return array + * @psalm-pure */ -function take_left($collection, $count) +function take_left($collection, int $count): array { InvalidArgumentException::assertCollection($collection, __FUNCTION__, 1); InvalidArgumentException::assertPositiveInteger($count, __FUNCTION__, 2); diff --git a/src/Functional/TakeRight.php b/src/Functional/TakeRight.php index 94a8118a..d34392d4 100644 --- a/src/Functional/TakeRight.php +++ b/src/Functional/TakeRight.php @@ -19,13 +19,14 @@ * This function will reorder and reset the integer array indices by default. This behaviour can be changed by setting * preserveKeys to TRUE. String keys are always preserved, regardless of this parameter. * - * @param Traversable|array $collection + * @template K of array-key + * @template V + * @param iterable $collection * @param int $count - * @param bool $preserveKeys - * - * @return array + * @return array + * @psalm-pure */ -function take_right($collection, $count, $preserveKeys = false) +function take_right($collection, $count, bool $preserveKeys = false) { InvalidArgumentException::assertCollection($collection, __FUNCTION__, 1); InvalidArgumentException::assertPositiveInteger($count, __FUNCTION__, 2); diff --git a/src/Functional/Tap.php b/src/Functional/Tap.php index f0c5d3a2..ac8063d3 100644 --- a/src/Functional/Tap.php +++ b/src/Functional/Tap.php @@ -13,9 +13,11 @@ /** * Call the given Closure with the given value, then return the value. * - * @param mixed $value - * @param callable $callback - * @return mixed + * @template V + * @param V $value + * @param callable(V): void $callback + * @return V + * @psalm-pure */ function tap($value, callable $callback) { diff --git a/src/Functional/True.php b/src/Functional/True.php index 6a7f0d8f..534fb73d 100644 --- a/src/Functional/True.php +++ b/src/Functional/True.php @@ -11,15 +11,15 @@ namespace Functional; use Functional\Exceptions\InvalidArgumentException; -use Traversable; /** * Returns true if all elements of the collection are strictly true * - * @param Traversable|array $collection + * @param iterable $collection * @return bool + * @psalm-pure */ -function true($collection) +function true($collection): bool { InvalidArgumentException::assertCollection($collection, __FUNCTION__, 1); diff --git a/src/Functional/Truthy.php b/src/Functional/Truthy.php index 93c0877a..56256bba 100644 --- a/src/Functional/Truthy.php +++ b/src/Functional/Truthy.php @@ -16,10 +16,12 @@ /** * Returns true if all elements of the collection evaluate to true * - * @param Traversable|array $collection + * @template V of mixed + * @param iterable $collection * @return bool + * @psalm-pure */ -function truthy($collection) +function truthy($collection): bool { InvalidArgumentException::assertCollection($collection, __FUNCTION__, 1); diff --git a/src/Functional/Unique.php b/src/Functional/Unique.php index 238d8264..0df78ecf 100644 --- a/src/Functional/Unique.php +++ b/src/Functional/Unique.php @@ -11,17 +11,20 @@ namespace Functional; use Functional\Exceptions\InvalidArgumentException; -use Traversable; /** * Returns an array of unique elements * - * @param Traversable|array $collection - * @param callable $callback + * @template K of array-key + * @template V + * @template I + * @param iterable $collection + * @param callable(V, K, iterable): I $callback * @param bool $strict - * @return array + * @return array + * @psalm-pure */ -function unique($collection, callable $callback = null, $strict = true) +function unique($collection, callable $callback = null, bool $strict = true): array { InvalidArgumentException::assertCollection($collection, __FUNCTION__, 1); diff --git a/src/Functional/With.php b/src/Functional/With.php index ec858829..bf8bf40d 100644 --- a/src/Functional/With.php +++ b/src/Functional/With.php @@ -23,8 +23,6 @@ */ function with($value, callable $callback, $invokeValue = true, $default = null) { - InvalidArgumentException::assertCallback($callback, __FUNCTION__, 2); - if ($value === null) { return $default; } diff --git a/src/Functional/ZipAll.php b/src/Functional/ZipAll.php index 2c7658b8..80002e0f 100644 --- a/src/Functional/ZipAll.php +++ b/src/Functional/ZipAll.php @@ -27,6 +27,7 @@ function zip_all(...$args) /** @var callable|null $callback */ $callback = null; if (\is_callable(\end($args))) { + /** @var callable $callback */ $callback = \array_pop($args); } diff --git a/tests/Functional/Exceptions/InvalidArgumentExceptionTest.php b/tests/Functional/Exceptions/InvalidArgumentExceptionTest.php index 2de9a7bc..a9d6d190 100644 --- a/tests/Functional/Exceptions/InvalidArgumentExceptionTest.php +++ b/tests/Functional/Exceptions/InvalidArgumentExceptionTest.php @@ -14,46 +14,6 @@ class InvalidArgumentExceptionTest extends TestCase { - public function testCallbackExceptionWithUndefinedStaticMethod() - { - $this->expectException('Functional\Exceptions\InvalidArgumentException'); - $this->expectExceptionMessage("func() expects parameter 1 to be a valid callback, method 'stdClass::method' not found or invalid method name"); - - InvalidArgumentException::assertCallback(['stdClass', 'method'], 'func', 1); - } - - public function testCallbackExceptionWithUndefinedFunction() - { - $this->expectException('Functional\Exceptions\InvalidArgumentException'); - $this->expectExceptionMessage("func() expects parameter 1 to be a valid callback, function 'undefinedFunction' not found or invalid function name"); - - InvalidArgumentException::assertCallback('undefinedFunction', 'func', 1); - } - - public function testCallbackExceptionWithUndefinedMethod() - { - $this->expectException('Functional\Exceptions\InvalidArgumentException'); - $this->expectExceptionMessage("func() expects parameter 2 to be a valid callback, method 'stdClass->method' not found or invalid method name"); - - InvalidArgumentException::assertCallback([new \stdClass(), 'method'], 'func', 2); - } - - public function testCallbackExceptionWithIncorrectArrayIndex() - { - $this->expectException('Functional\Exceptions\InvalidArgumentException'); - $this->expectExceptionMessage("func() expects parameter 1 to be a valid callback, method 'stdClass->method' not found or invalid method name"); - - InvalidArgumentException::assertCallback([1 => new \stdClass(), 2 => 'method'], 'func', 1); - } - - public function testCallbackExceptionWithObject() - { - $this->expectException('Functional\Exceptions\InvalidArgumentException'); - $this->expectExceptionMessage('func() expected parameter 1 to be a valid callback, no array, string, closure or functor given'); - - InvalidArgumentException::assertCallback(new \stdClass(), 'func', 1); - } - public function testExceptionIfStringIsPassedAsList() { $this->expectException('Functional\Exceptions\InvalidArgumentException'); @@ -99,14 +59,14 @@ public function testExceptionIfInvalidMethodName() InvalidArgumentException::assertMethodName(new \stdClass(), "foo", 2); } - public function testExceptionIfInvalidPropertyName() + public function testExceptionIfInvalidArrayKey() { - InvalidArgumentException::assertPropertyName('property', 'func', 2); - InvalidArgumentException::assertPropertyName(0, 'func', 2); - InvalidArgumentException::assertPropertyName(0.2, 'func', 2); + InvalidArgumentException::assertValidArrayKey('property', 'func', 2); + InvalidArgumentException::assertValidArrayKey(0, 'func', 2); + InvalidArgumentException::assertValidArrayKey(0.2, 'func', 2); $this->expectException('Functional\Exceptions\InvalidArgumentException'); - $this->expectExceptionMessage('func() expects parameter 2 to be a valid property name or array index, stdClass given'); - InvalidArgumentException::assertPropertyName(new \stdClass(), "func", 2); + $this->expectExceptionMessage('func() expects parameter 2 to be a valid array key, "stdClass" given. Expected "boolean" or "NULL", "string", "integer", "double"'); + InvalidArgumentException::assertValidArrayKey(new \stdClass(), "func", 2); } public function testNoExceptionThrownWithPositiveInteger() @@ -128,32 +88,4 @@ public function testExceptionIfStringInsteadOfPositiveInteger() $this->expectExceptionMessage('func() expects parameter 2 to be positive integer, string given'); InvalidArgumentException::assertPositiveInteger('str', 'func', 2); } - - public function testAssertIntegerAccessWithString() - { - $this->expectException('Functional\Exceptions\InvalidArgumentException'); - $this->expectExceptionMessage('func() expects parameter 4 to be integer, string given'); - InvalidArgumentException::assertInteger('string', "func", 4); - } - - public function testAssertIntegerAccessWithObject() - { - $this->expectException('Functional\Exceptions\InvalidArgumentException'); - $this->expectExceptionMessage('func() expects parameter 4 to be integer, stdClass given'); - InvalidArgumentException::assertInteger(new \stdClass(), "func", 4); - } - - public function testAssertBooleanAccessWithString() - { - $this->expectException('Functional\Exceptions\InvalidArgumentException'); - $this->expectExceptionMessage('func() expects parameter 4 to be boolean, string given'); - InvalidArgumentException::assertBoolean('string', "func", 4); - } - - public function testAssertBooleanAccessWithObject() - { - $this->expectException('Functional\Exceptions\InvalidArgumentException'); - $this->expectExceptionMessage('func() expects parameter 4 to be boolean, stdClass given'); - InvalidArgumentException::assertBoolean(new \stdClass(), "func", 4); - } } diff --git a/tests/Functional/GroupTest.php b/tests/Functional/GroupTest.php index 41a0198f..acc3921b 100644 --- a/tests/Functional/GroupTest.php +++ b/tests/Functional/GroupTest.php @@ -57,9 +57,9 @@ public function testExceptionIsThrownWhenCallbacksReturnsInvalidKey() $invalidTypes = [ - 'resource' => stream_context_create(), - 'object' => new \stdClass(), - 'array' => [] + 'resource' => stream_context_create(), + 'stdClass' => new \stdClass(), + 'array' => [], ]; foreach ($invalidTypes as $type => $value) { @@ -70,7 +70,7 @@ public function testExceptionIsThrownWhenCallbacksReturnsInvalidKey() } catch (\Exception $e) { $this->assertSame( sprintf( - 'Functional\group(): callback returned invalid array key of type "%s". Expected NULL, string, integer, double or boolean', + 'Functional\group() expects return value to be a valid array key, "%s" given. Expected "boolean" or "NULL", "string", "integer", "double"', $type ), $e->getMessage() diff --git a/tests/Functional/MatchTest.php b/tests/Functional/MatchTest.php index 323fc1bb..e52fb187 100644 --- a/tests/Functional/MatchTest.php +++ b/tests/Functional/MatchTest.php @@ -18,14 +18,19 @@ class MatchTest extends AbstractTestCase { public function testMatch() { - $test = match([ - [equal('foo'), const_function('is foo')], - [equal('bar'), const_function('is bar')], - [equal('baz'), const_function('is baz')], - [const_function(true), function ($x) { - return 'default is ' . $x; - }], - ]); + $test = match( + [ + [equal('foo'), const_function('is foo')], + [equal('bar'), const_function('is bar')], + [equal('baz'), const_function('is baz')], + [ + const_function(true), + function ($x) { + return 'default is ' . $x; + }, + ], + ] + ); $this->assertEquals('is foo', $test('foo')); $this->assertEquals('is bar', $test('bar')); @@ -35,10 +40,12 @@ public function testMatch() public function testNothingMatch() { - $test = match([ - [equal('foo'), const_function('is foo')], - [equal('bar'), const_function('is bar')], - ]); + $test = match( + [ + [equal('foo'), const_function('is foo')], + [equal('bar'), const_function('is bar')], + ] + ); $this->assertNull($test('baz')); } @@ -50,36 +57,46 @@ public function testMatchConditionIsArray() $callable = function () { }; - $test = match([ - [$callable, $callable], - '', - ]); + $test = match( + [ + [$callable, $callable], + '', + ] + ); } public function testMatchConditionLength() { - $this->expectArgumentError('Functional\match() expects size of condition at key 1 to be greater than or equals to 2, 1 given'); + $this->expectArgumentError( + 'Functional\match() expects size of condition at key 1 to be greater than or equals to 2, 1 given' + ); $callable = function () { }; - $test = match([ - [$callable, $callable], - [''], - ]); + $test = match( + [ + [$callable, $callable], + [''], + ] + ); } public function testMatchConditionCallables() { $this->expectException(\Functional\Exceptions\InvalidArgumentException::class); - $this->expectExceptionMessage('Functional\match() expects first two items of condition at key 1 to be callables'); + $this->expectExceptionMessage( + 'Functional\match() expects first two items of condition at key 1 to be callables' + ); $callable = function () { }; - $test = match([ - [$callable, $callable], - [$callable, ''], - ]); + $test = match( + [ + [$callable, $callable], + [$callable, ''], + ] + ); } } diff --git a/tests/Functional/PartialMethodTest.php b/tests/Functional/PartialMethodTest.php index d5cff3c1..e5fdbcc0 100644 --- a/tests/Functional/PartialMethodTest.php +++ b/tests/Functional/PartialMethodTest.php @@ -44,12 +44,6 @@ public function testWithNonObjectAndDefaultValue() $this->assertSame('defaultValue', $method('non-object')); } - public function testWithInvalidMethodName() - { - $this->expectArgumentError('Functional\partial_method() expects parameter 1 to be string, integer given'); - partial_method(1); - } - public function execute($arg1 = null, $arg2 = null) { return $arg1 ? $arg1 . ' ' . $arg2 : 'default'; diff --git a/tests/Functional/PluckTest.php b/tests/Functional/PluckTest.php index a36e1f6f..f204a377 100644 --- a/tests/Functional/PluckTest.php +++ b/tests/Functional/PluckTest.php @@ -212,7 +212,7 @@ public function testPassNoCollection() public function testPassNoPropertyName() { - $this->expectArgumentError('Functional\pluck() expects parameter 2 to be a valid property name or array index, stdClass given'); + $this->expectArgumentError('Functional\pluck() expects parameter 2 to be a valid array key, "stdClass" given. Expected "boolean" or "NULL", "string", "integer", "double"'); pluck($this->propertyExistsSomewhere, new \stdClass()); } diff --git a/tests/Functional/PollTest.php b/tests/Functional/PollTest.php index 1e0dec93..1f0c54d6 100644 --- a/tests/Functional/PollTest.php +++ b/tests/Functional/PollTest.php @@ -11,7 +11,7 @@ namespace Functional\Tests; use ArrayIterator; -use PHPUnit_Framework_MockObject_MockObject as MockObject; +use PHPUnit\Framework\MockObject\MockObject; use function Functional\poll; diff --git a/tests/Functional/SequenceLinearTest.php b/tests/Functional/SequenceLinearTest.php index a3478f65..b71fe011 100644 --- a/tests/Functional/SequenceLinearTest.php +++ b/tests/Functional/SequenceLinearTest.php @@ -39,12 +39,4 @@ public function testArgumentMustBePositiveInteger() ); sequence_linear(-1, 1); } - - public function testAmountArgumentMustBeInteger() - { - $this->expectArgumentError( - 'Functional\sequence_linear() expects parameter 2 to be integer' - ); - sequence_linear(0, 1.1); - } } diff --git a/tests/Functional/TailRecursionTest.php b/tests/Functional/TailRecursionTest.php index ec8b20c7..86daafd6 100644 --- a/tests/Functional/TailRecursionTest.php +++ b/tests/Functional/TailRecursionTest.php @@ -19,12 +19,12 @@ class TailRecursionTest extends TestCase public function testTailRecursion1() { $fact = tail_recursion(function ($n, $acc = 1) use (&$fact) { - if ($n == 0) { + if ($n === 0) { return $acc; } return $fact($n - 1, $acc * $n); }); - $this->assertEquals(3628800, $fact(10)); + $this->assertEquals(INF, $fact(100000)); } public function testTailRecursion2()