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, ''],
+ ]
+ );
}
}