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 1435e5d9..6f3402a5 100644 --- a/.travis.yml +++ b/.travis.yml @@ -15,3 +15,4 @@ install: script: - composer tests - composer coding-style + - composer types diff --git a/composer.json b/composer.json index fe4fad50..4c7cbbcc 100644 --- a/composer.json +++ b/composer.json @@ -15,7 +15,8 @@ } ], "require": { - "php": "~7" + "php": "~7", + "vimeo/psalm": "^3.7" }, "require-dev": { "phpunit/phpunit": "~7", @@ -123,6 +124,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/psalm.xml b/psalm.xml new file mode 100644 index 00000000..42f355b2 --- /dev/null +++ b/psalm.xml @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Functional/Average.php b/src/Functional/Average.php index b3029f34..d0e4adbf 100644 --- a/src/Functional/Average.php +++ b/src/Functional/Average.php @@ -17,7 +17,7 @@ * 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 + * @return numeric|null */ 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/Concat.php b/src/Functional/Concat.php index 64fe8d57..c607e08f 100644 --- a/src/Functional/Concat.php +++ b/src/Functional/Concat.php @@ -13,7 +13,7 @@ /** * Concatenates zero or more strings * - * @param string[] ...$strings + * @param array $strings * @return string */ function concat(string ...$strings) diff --git a/src/Functional/Curry.php b/src/Functional/Curry.php index 97bb835e..e473a5a8 100644 --- a/src/Functional/Curry.php +++ b/src/Functional/Curry.php @@ -24,8 +24,10 @@ */ function curry(callable $function, $required = true) { + /** @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 +37,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/Difference.php b/src/Functional/Difference.php index 0c906f17..469ae0c1 100644 --- a/src/Functional/Difference.php +++ b/src/Functional/Difference.php @@ -17,8 +17,8 @@ * Takes a collection and returns the difference of all elements * * @param Traversable|array $collection - * @param integer|float $initial - * @return integer|float + * @param numeric $initial + * @return numeric */ function difference($collection, $initial = 0) { diff --git a/src/Functional/Exceptions/InvalidArgumentException.php b/src/Functional/Exceptions/InvalidArgumentException.php index 5ab8e800..86bfbc0f 100644 --- a/src/Functional/Exceptions/InvalidArgumentException.php +++ b/src/Functional/Exceptions/InvalidArgumentException.php @@ -13,6 +13,8 @@ class InvalidArgumentException extends \InvalidArgumentException { /** + * @psalm-pure + * * @param mixed $callback * @param string $callee * @param integer $parameterPosition @@ -22,7 +24,7 @@ public static function assertCallback($callback, $callee, $parameterPosition) { if (!\is_callable($callback)) { if (!\is_array($callback) && !\is_string($callback)) { - throw new static( + throw new self( \sprintf( '%s() expected parameter %d to be a valid callback, no array, string, closure or functor given', $callee, @@ -43,7 +45,7 @@ public static function assertCallback($callback, $callee, $parameterPosition) $sep = '->'; } - $callback = \implode($callback, $sep); + $callback = \implode($sep, $callback); break; default: @@ -51,7 +53,7 @@ public static function assertCallback($callback, $callee, $parameterPosition) break; } - throw new static( + throw new self( \sprintf( "%s() expects parameter %d to be a valid callback, %s '%s' not found or invalid %s name", $callee, @@ -64,20 +66,29 @@ public static function assertCallback($callback, $callee, $parameterPosition) } } + /** + * @psalm-pure + */ public static function assertCollection($collection, $callee, $parameterPosition) { self::assertCollectionAlike($collection, 'Traversable', $callee, $parameterPosition); } + /** + * @psalm-pure + */ public static function assertArrayAccess($collection, $callee, $parameterPosition) { self::assertCollectionAlike($collection, 'ArrayAccess', $callee, $parameterPosition); } + /** + * @psalm-pure + */ public static function assertMethodName($methodName, $callee, $parameterPosition) { if (!\is_string($methodName)) { - throw new static( + throw new self( \sprintf( '%s() expects parameter %d to be string, %s given', $callee, @@ -89,6 +100,8 @@ public static function assertMethodName($methodName, $callee, $parameterPosition } /** + * @psalm-pure + * * @param mixed $propertyName * @param string $callee * @param integer $parameterPosition @@ -102,7 +115,7 @@ public static function assertPropertyName($propertyName, $callee, $parameterPosi !\is_float($propertyName) && !\is_null($propertyName) ) { - throw new static( + throw new self( \sprintf( '%s() expects parameter %d to be a valid property name or array index, %s given', $callee, @@ -113,13 +126,16 @@ public static function assertPropertyName($propertyName, $callee, $parameterPosi } } + /** + * @psalm-pure + */ public static function assertPositiveInteger($value, $callee, $parameterPosition) { 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, @@ -131,9 +147,11 @@ public static function assertPositiveInteger($value, $callee, $parameterPosition } /** + * @psalm-pure + * * @param mixed $key * @param string $callee - * @throws static + * @throws InvalidArgumentException */ public static function assertValidArrayKey($key, $callee) { @@ -142,7 +160,7 @@ public static function assertValidArrayKey($key, $callee) $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, @@ -154,10 +172,13 @@ public static function assertValidArrayKey($key, $callee) } } + /** + * @psalm-pure + */ public static function assertArrayKeyExists($collection, $key, $callee) { if (!isset($collection[$key])) { - throw new static( + throw new self( \sprintf( '%s(): unknown key "%s"', $callee, @@ -168,6 +189,8 @@ public static function assertArrayKeyExists($collection, $key, $callee) } /** + * @psalm-pure + * * @param boolean $value * @param string $callee * @param integer $parameterPosition @@ -176,7 +199,7 @@ public static function assertArrayKeyExists($collection, $key, $callee) public static function assertBoolean($value, $callee, $parameterPosition) { if (!\is_bool($value)) { - throw new static( + throw new self( \sprintf( '%s() expects parameter %d to be boolean, %s given', $callee, @@ -188,6 +211,8 @@ public static function assertBoolean($value, $callee, $parameterPosition) } /** + * @psalm-pure + * * @param mixed $value * @param string $callee * @param integer $parameterPosition @@ -196,7 +221,7 @@ public static function assertBoolean($value, $callee, $parameterPosition) public static function assertInteger($value, $callee, $parameterPosition) { if (!\is_int($value)) { - throw new static( + throw new self( \sprintf( '%s() expects parameter %d to be integer, %s given', $callee, @@ -208,6 +233,8 @@ public static function assertInteger($value, $callee, $parameterPosition) } /** + * @psalm-pure + * * @param integer $value * @param integer $limit * @param string $callee @@ -217,7 +244,7 @@ public static function assertInteger($value, $callee, $parameterPosition) public static function assertIntegerGreaterThanOrEqual($value, $limit, $callee, $parameterPosition) { 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, @@ -229,6 +256,8 @@ public static function assertIntegerGreaterThanOrEqual($value, $limit, $callee, } /** + * @psalm-pure + * * @param integer $value * @param integer $limit * @param string $callee @@ -238,7 +267,7 @@ public static function assertIntegerGreaterThanOrEqual($value, $limit, $callee, public static function assertIntegerLessThanOrEqual($value, $limit, $callee, $parameterPosition) { 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, @@ -249,16 +278,21 @@ public static function assertIntegerLessThanOrEqual($value, $limit, $callee, $pa } } + /** + * @psalm-pure + */ public static function assertResolvablePlaceholder(array $args, $position) { if (\count($args) === 0) { - throw new static( + throw new self( \sprintf('Cannot resolve parameter placeholder at position %d. Parameter stack is empty.', $position) ); } } /** + * @psalm-pure + * * @param mixed $collection * @param string $className * @param string $callee @@ -268,7 +302,7 @@ public static function assertResolvablePlaceholder(array $args, $position) private static function assertCollectionAlike($collection, $className, $callee, $parameterPosition) { 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, @@ -280,6 +314,9 @@ private static function assertCollectionAlike($collection, $className, $callee, } } + /** + * @psalm-pure + */ private static function getType($value) { return \is_object($value) ? \get_class($value) : \gettype($value); diff --git a/src/Functional/Match.php b/src/Functional/Match.php index 83d3b2a3..3370954a 100644 --- a/src/Functional/Match.php +++ b/src/Functional/Match.php @@ -21,7 +21,7 @@ * * @param array $conditions the conditions to check against * - * @return callable|null the function that calls the callable of the first truthy condition + * @return callable the function that calls the callable of the first truthy condition */ function match(array $conditions) { diff --git a/src/Functional/Maximum.php b/src/Functional/Maximum.php index f2c4596f..15097581 100644 --- a/src/Functional/Maximum.php +++ b/src/Functional/Maximum.php @@ -17,7 +17,7 @@ * Returns the maximum value of a collection * * @param Traversable|array $collection - * @return integer|float + * @return numeric|null */ function maximum($collection) { diff --git a/src/Functional/Minimum.php b/src/Functional/Minimum.php index d0d67fb8..3898a348 100644 --- a/src/Functional/Minimum.php +++ b/src/Functional/Minimum.php @@ -17,7 +17,7 @@ * Returns the minimum value of a collection * * @param Traversable|array $collection - * @return integer|float + * @return numeric|null */ function minimum($collection) { diff --git a/src/Functional/Poll.php b/src/Functional/Poll.php index ca48fcb3..667d4a17 100644 --- a/src/Functional/Poll.php +++ b/src/Functional/Poll.php @@ -33,6 +33,7 @@ function poll(callable $callback, $timeout, Traversable $delaySequence = null) $delays = new AppendIterator(); if ($delaySequence) { + /** @psalm-suppress ArgumentTypeCoercion */ $delays->append(new InfiniteIterator($delaySequence)); } $delays->append(new InfiniteIterator(new ArrayIterator([0]))); diff --git a/src/Functional/Product.php b/src/Functional/Product.php index ccd3a87e..697f204d 100644 --- a/src/Functional/Product.php +++ b/src/Functional/Product.php @@ -17,8 +17,8 @@ * Takes a collection and returns the product of all elements * * @param Traversable|array $collection - * @param integer|float $initial - * @return integer|float + * @param numeric $initial + * @return numeric */ function product($collection, $initial = 1) { 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/Sum.php b/src/Functional/Sum.php index f79f5de5..642daceb 100644 --- a/src/Functional/Sum.php +++ b/src/Functional/Sum.php @@ -17,8 +17,8 @@ * Takes a collection and returns the sum of the elements * * @param Traversable|array $collection - * @param integer|float $initial - * @return integer|float + * @param numeric $initial + * @return numeric */ function sum($collection, $initial = 0) { diff --git a/src/Functional/Unique.php b/src/Functional/Unique.php index 238d8264..6b9fda08 100644 --- a/src/Functional/Unique.php +++ b/src/Functional/Unique.php @@ -16,8 +16,17 @@ /** * Returns an array of unique elements * + * @psalm-pure + * + * @template TKey as array-key + * @template TValue + * + * @psalm-param iterable $collection + * @psalm-param null|callable(TValue, TKey, iterable):mixed $callback + * @psalm-return array + * * @param Traversable|array $collection - * @param callable $callback + * @param null|callable $callback * @param bool $strict * @return array */ diff --git a/src/Functional/With.php b/src/Functional/With.php index ec858829..b488c107 100644 --- a/src/Functional/With.php +++ b/src/Functional/With.php @@ -15,6 +15,18 @@ /** * Invoke a callback on a value if the value is not null * + * @psalm-pure + * + * @template I as null|mixed + * @template D + * @template R + * + * @psalm-param I $value + * @psalm-param callable(I):R $callback + * @psalm-param bool $invokeValue + * @psalm-param null|D $default + * @psalm-return R|D|null + * * @param mixed $value * @param callable $callback * @param bool $invokeValue Set to false to not invoke $value if it is a callable. Will be removed in 2.0 @@ -30,6 +42,7 @@ function with($value, callable $callback, $invokeValue = true, $default = null) } if ($invokeValue && \is_callable($value)) { + /** @psalm-suppress ImpureFunctionCall */ \trigger_error('Invoking the value is deprecated and will be removed in 2.0', E_USER_DEPRECATED); $value = $value(); diff --git a/src/Functional/Zip.php b/src/Functional/Zip.php index 2e28b475..4ca81aa8 100644 --- a/src/Functional/Zip.php +++ b/src/Functional/Zip.php @@ -16,6 +16,8 @@ /** * Recombines arrays by index and applies a callback optionally * + * @psalm-pure + * * @param array|Traversable ...$args One or more callbacks * @return array */ diff --git a/src/Functional/ZipAll.php b/src/Functional/ZipAll.php index 2c7658b8..a36d0d14 100644 --- a/src/Functional/ZipAll.php +++ b/src/Functional/ZipAll.php @@ -19,6 +19,8 @@ * When the input collections are different lengths the resulting collections * will all have the length which is required to fit all the keys * + * @psalm-pure + * * @param array|Traversable ...$args One or more callbacks * @return array */ @@ -27,6 +29,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/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, ''], + ] + ); } }