diff --git a/README.md b/README.md index 54da7949..b8b7a261 100644 --- a/README.md +++ b/README.md @@ -16,19 +16,15 @@ PHP Functional Programming library. Monads and common use functions. ### Composer +Supported installation method is via [composer](https://getcomposer.org): + ```console $ composer require fp4php/functional ``` -### Enable psalm plugin (optional) - -Read more about [plugin](doc/Psalm.md). +### Psalm integration -```console -$ composer require --dev fp4php/psalm-toolkit -$ vendor/bin/psalm-plugin enable fp4php/psalm-toolkit -$ vendor/bin/psalm-plugin enable fp4php/functional -``` +Please refer to the [fp4php/functional-psalm-plugin](https://github.com/fp4php/functional-psalm-plugin) repository. ## Overview Typesafe and concise. diff --git a/UPGRADING-v6.md b/UPGRADING-v6.md new file mode 100644 index 00000000..f35120c2 --- /dev/null +++ b/UPGRADING-v6.md @@ -0,0 +1,32 @@ +## Update psalm to v5 + +Since fp4php/functional v6 Psalm integration ships as separate package. +Including the Psalm integration in the main fp4php/functional repository was a mistake. + +For migration: + +1. Remove old toolkit: + +```shell +composer remove fp4php/psalm-toolkit +``` + +2. Install new psalm plugin: + +```shell +composer require --dev fp4php/functional-psalm-plugin +``` + +3. Update psalm.xml: + +```diff + + + ... + +- +- ++ + + +``` diff --git a/composer.json b/composer.json index 739006a4..539a0659 100644 --- a/composer.json +++ b/composer.json @@ -22,25 +22,26 @@ "ext-simplexml": "*" }, "require-dev": { - "vimeo/psalm": "^4.23", + "vimeo/psalm": "^5.7", "phpunit/phpunit": "^9", "symfony/process": "^5.2", "rregeer/phpunit-coverage-check": "^0.3.1", "php-coveralls/php-coveralls": "~2.4.0", - "fp4php/psalm-toolkit": "dev-main" + "fp4php/psalm-toolkit": "dev-psalm-v5", + "fp4php/functional-psalm-plugin": "^1.0" }, "autoload": { "psr-4": { "Fp\\Operations\\": "src/Fp/Operations", "Fp\\Collections\\": "src/Fp/Collections", "Fp\\Streams\\": "src/Fp/Streams", - "Fp\\Functional\\": "src/Fp/Functional", - "Fp\\Psalm\\": "src/Fp/Psalm" + "Fp\\Functional\\": "src/Fp/Functional" }, "files": [ "src/Fp/Functions/Callable/Ctor.php", "src/Fp/Functions/Callable/ToSafeClosure.php", "src/Fp/Functions/Callable/DropFirstArg.php", + "src/Fp/Functions/Callable/Pipe.php", "src/Fp/Functions/Callable/Compose.php", "src/Fp/Functions/Callable/Partial.php", "src/Fp/Functions/Callable/PartialLeft.php", @@ -98,6 +99,7 @@ "src/Fp/Functions/Evidence/ProveObject.php", "src/Fp/Functions/Evidence/ProveOf.php", "src/Fp/Functions/Evidence/ProveString.php", + "src/Fp/Functions/Util/ToString.php", "src/Fp/Functions/Util/JsonDecode.php", "src/Fp/Functions/Util/RegExp.php", "src/Fp/Functions/Util/Writeln.php", diff --git a/psalm.xml b/psalm.xml index e7ee65a1..441cbc99 100644 --- a/psalm.xml +++ b/psalm.xml @@ -1,11 +1,11 @@ - + @@ -13,57 +13,15 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + diff --git a/src/Doc/Md/Psalm/1_PsalmSupport.md b/src/Doc/Md/Psalm/1_PsalmSupport.md deleted file mode 100644 index 42c90d54..00000000 --- a/src/Doc/Md/Psalm/1_PsalmSupport.md +++ /dev/null @@ -1,368 +0,0 @@ -# Static analysis - -Highly recommended to use this library in tandem with [Psalm](https://github.com/vimeo/psalm). - -Psalm is awesome library for static analysis of PHP code. -It opens the road to typed functional programming. - -# Psalm plugin - -Psalm cannot check everything. But the [plugin system](https://psalm.dev/docs/running_psalm/plugins/authoring_plugins/) allows to improve type inference and implement other custom diagnostics. - -To enable plugin shipped with library: - -```console -$ composer require --dev fp4php/psalm-toolkit -$ vendor/bin/psalm-plugin enable fp4php/functional -``` - -# Features - -- #### filter - - Plugin add type narrowing for filtering. - - `Fp\Functional\Option\Option::filter`: - - ```php - - */ - function getOption(): Option - { - // ... - } - - // Narrowed to Option - - /** @psalm-trace $result */ - $result = getOption()->filter(fn($value) => is_string($value)); - ``` - - `Fp\Collections\ArrayList::filter` (and other collections with `filter` method): - - ```php - - */ - function getArrayList(): ArrayList - { - // ... - } - - // Narrowed to ArrayList - - /** @psalm-trace $result */ - $result = getArrayList()->filter(fn($value) => is_string($value)); - ``` - - `Fp\Functional\Either\Either::filterOrElse`: - - ```php - - */ - function getEither(): Either - { - // ... - } - - // Narrowed to Either - getEither()->filterOrElse( - fn($value) => is_string($value), - fn() => new TypeError('Is not string'), - ); - ``` - - `Fp\Collection\filter`: - - ```php - - */ - function getList(): array - { - // ... - } - - // Narrowed to list - filter(getList(), fn($value) => is_string($value)); - ``` - - `Fp\Collection\first` and `Fp\Collection\last`: - - ```php - - */ - function getList(): array - { - // ... - } - - // Narrowed to Option - first(getList(), fn($value) => is_string($value)); - - // Narrowed to Option - last(getList(), fn($value) => is_int($value)); - ``` - - For all cases above you can use [first-class callable](https://wiki.php.net/rfc/first_class_callable_syntax) syntax: - - ```php - - */ - function getList(): array - { - // ... - } - - // Narrowed to list - filter(getList(), is_string(...)); - ``` - -- #### fold - - Is too difficult to make the fold function using type system of psalm. - Without plugin `Fp\Collection\fold` and collections fold method has some edge cases. For example: https://psalm.dev/r/b0a99c4912 - - Plugin can fix that problem. - -- #### ctor - - PHP 8.1 brings feature called [first-class callable](https://wiki.php.net/rfc/first_class_callable_syntax). - But that feature cannot be used for class constructor. `Fp\Callable\ctor` can simulate this feature for class constructors, but requires plugin for static analysis. - - ```php - - */ - function sequenceOptionShapeExample(int $id): Option - { - // Inferred type is: Option not Option> - return sequenceOption([ - 'foo' => getFoo($id), - 'bar' => getBar($id), - ]); - } - - /** - * @return Option - */ - function sequenceOptionTupleExample(int $id): Option - { - // Inferred type is: Option not Option> - return sequenceOptionT(getFoo($id), getBar($id)); - } - ``` - -- #### assertion - - Unfortunately `@psalm-assert-if-true`/`@psalm-assert-if-false` works incorrectly for Option/Either assertion methods: https://psalm.dev/r/408248f46f - - Plugin implements workaround for this bug. - -- #### N-combinators - - Psalm plugin will prevent calling *N combinator in non-valid cases: - - ```php - $maybeData - * @return Option - */ - function test(Option $maybeData): Option - { - /* - * ERROR: IfThisIsMismatch - * Object must be type of Option, actual type Option - */ - return $maybeData->mapN(fn(int $a, bool $b, bool $c) => new Foo($a, $b, $c)); - } - ``` - -- #### proveTrue - - Implementation assertion effect for `Fp\Evidence\proveTrue` (like for builtin `assert` function): - - ```php - , ArrayList> $separated - * @return Either, ArrayList> - */ - function separatedArrayListToEither(Separated $separated): Either - { - return $separated->toEither(); - } - - /** - * @param Separated, HashSet> $separated - * @return Either, HashSet> - */ - function separatedHashSetToEither(Separated $separated): Either - { - return $separated->toEither(); - } - ``` - -- #### partitionT - - Plugin infers each `list` type from predicates of `partitionT`: - - ```php - $list - * @return array{list, list, list} - */ - function testExhaustiveInference(array $list): array - { - return partitionT($list, fn($i) => $i instanceof Foo, fn($i) => $i instanceof Bar); - } - ``` - -- #### filterNotNull - - Plugin turns all nullable keys to possibly undefined keys: - ```php - , TVO> $callback + * @return Either, ArrayList> + */ + public function traverseEitherMerged(callable $callback): Either + { + return Ops\TraverseEitherMergedOperation::of($this)(dropFirstArg($callback))->map(ArrayList::collect(...)); + } + + /** + * {@inheritDoc} + * + * @template E + * @template TVO + * + * @param callable(mixed...): Either, TVO> $callback + * @return Either, ArrayList> + * + * @see MapTapNMethodReturnTypeProvider + */ + public function traverseEitherMergedN(callable $callback): Either + { + return $this->traverseEitherMerged(function($tuple) use ($callback) { + /** @var array $tuple */; + return toSafeClosure($callback)(...$tuple); + }); + } + /** * {@inheritDoc} * @@ -810,6 +843,22 @@ public function sequenceEither(): Either return Ops\TraverseEitherOperation::id($this)->map(ArrayList::collect(...)); } + /** + * {@inheritDoc} + * + * Same as {@see Seq::sequenceEither()} but merge all left errors into non-empty-list. + * + * @template E + * @template TVO + * @psalm-if-this-is ArrayList, TVO>> + * + * @return Either, ArrayList> + */ + public function sequenceEitherMerged(): Either + { + return Ops\TraverseEitherMergedOperation::id($this)->map(ArrayList::collect(...)); + } + /** * {@inheritDoc} * diff --git a/src/Fp/Collections/Collection.php b/src/Fp/Collections/Collection.php index 4e9a590e..c17abcec 100644 --- a/src/Fp/Collections/Collection.php +++ b/src/Fp/Collections/Collection.php @@ -5,7 +5,6 @@ namespace Fp\Collections; use Countable; -use Iterator; use IteratorAggregate; /** diff --git a/src/Fp/Collections/HashMap.php b/src/Fp/Collections/HashMap.php index 6f2fd80a..3f785619 100644 --- a/src/Fp/Collections/HashMap.php +++ b/src/Fp/Collections/HashMap.php @@ -839,6 +839,51 @@ public function traverseEitherKV(callable $callback): Either return Ops\TraverseEitherOperation::of($this)($callback)->map(fn($gen) => HashMap::collect($gen)); } + /** + * {@inheritDoc} + * + * @template E + * @template TVO + * + * @param callable(TV): Either, TVO> $callback + * @return Either, HashMap> + */ + public function traverseEitherMerged(callable $callback): Either + { + return $this->traverseEitherMergedKV(dropFirstArg($callback)); + } + + /** + * {@inheritDoc} + * + * @template E + * @template TVO + * + * @param callable(TK, TV): Either, TVO> $callback + * @return Either, HashMap> + */ + public function traverseEitherMergedKV(callable $callback): Either + { + return Ops\TraverseEitherMergedOperation::of($this)($callback)->map(HashMap::collect(...)); + } + + /** + * {@inheritDoc} + * + * @template E + * @template TVO + * + * @param callable(mixed...): Either, TVO> $callback + * @return Either, HashMap> + */ + public function traverseEitherMergedN(callable $callback): Either + { + return $this->traverseEitherMerged(function($tuple) use ($callback) { + /** @var array $tuple */; + return toSafeClosure($callback)(...$tuple); + }); + } + /** * {@inheritDoc} * @@ -853,6 +898,20 @@ public function sequenceEither(): Either return Ops\TraverseEitherOperation::id($this)->map(fn($gen) => HashMap::collect($gen)); } + /** + * {@inheritDoc} + * + * @template E + * @template TVO + * @psalm-if-this-is HashMap, TVO>> + * + * @return Either, HashMap> + */ + public function sequenceEitherMerged(): Either + { + return Ops\TraverseEitherMergedOperation::id($this)->map(HashMap::collect(...)); + } + /** * {@inheritDoc} * diff --git a/src/Fp/Collections/HashSet.php b/src/Fp/Collections/HashSet.php index efa38b0e..9f4db172 100644 --- a/src/Fp/Collections/HashSet.php +++ b/src/Fp/Collections/HashSet.php @@ -607,6 +607,37 @@ public function traverseEitherN(callable $callback): Either }); } + /** + * {@inheritDoc} + * + * @template E + * @template TVO + * + * @param callable(TV): Either, TVO> $callback + * @return Either, HashSet> + */ + public function traverseEitherMerged(callable $callback): Either + { + return Ops\TraverseEitherMergedOperation::of($this)(dropFirstArg($callback))->map(HashSet::collect(...)); + } + + /** + * {@inheritDoc} + * + * @template E + * @template TVO + * + * @param callable(mixed...): Either, TVO> $callback + * @return Either, HashSet> + */ + public function traverseEitherMergedN(callable $callback): Either + { + return $this->traverseEitherMerged(function($tuple) use ($callback) { + /** @var array $tuple */; + return toSafeClosure($callback)(...$tuple); + }); + } + /** * {@inheritDoc} * @@ -618,8 +649,21 @@ public function traverseEitherN(callable $callback): Either */ public function sequenceEither(): Either { - return Ops\TraverseEitherOperation::id($this->getIterator()) - ->map(fn($gen) => HashSet::collect($gen)); + return Ops\TraverseEitherOperation::id($this->getIterator())->map(HashSet::collect(...)); + } + + /** + * {@inheritDoc} + * + * @template E + * @template TVO + * @psalm-if-this-is HashSet, TVO>> + * + * @return Either, HashSet> + */ + public function sequenceEitherMerged(): Either + { + return Ops\TraverseEitherMergedOperation::id($this)->map(HashSet::collect(...)); } /** diff --git a/src/Fp/Collections/LinkedList.php b/src/Fp/Collections/LinkedList.php index ece93fad..635cb107 100644 --- a/src/Fp/Collections/LinkedList.php +++ b/src/Fp/Collections/LinkedList.php @@ -659,6 +659,7 @@ public function uniqueBy(callable $callback): LinkedList */ public function reverse(): LinkedList { + /** @var LinkedList */ $list = Nil::getInstance(); foreach ($this as $elem) { @@ -788,6 +789,39 @@ public function traverseEitherN(callable $callback): Either }); } + /** + * {@inheritDoc} + * + * @template E + * @template TVO + * + * @param callable(TV): Either, TVO> $callback + * @return Either, LinkedList> + */ + public function traverseEitherMerged(callable $callback): Either + { + return Ops\TraverseEitherMergedOperation::of($this)(dropFirstArg($callback))->map(LinkedList::collect(...)); + } + + /** + * {@inheritDoc} + * + * @template E + * @template TVO + * + * @param callable(mixed...): Either, TVO> $callback + * @return Either, LinkedList> + * + * @see MapTapNMethodReturnTypeProvider + */ + public function traverseEitherMergedN(callable $callback): Either + { + return $this->traverseEitherMerged(function($tuple) use ($callback) { + /** @var array $tuple */; + return toSafeClosure($callback)(...$tuple); + }); + } + /** * {@inheritDoc} * @@ -799,8 +833,23 @@ public function traverseEitherN(callable $callback): Either */ public function sequenceEither(): Either { - return Ops\TraverseEitherOperation::id($this->getIterator()) - ->map(fn($gen) => LinkedList::collect($gen)); + return Ops\TraverseEitherOperation::id($this->getIterator())->map(LinkedList::collect(...)); + } + + /** + * {@inheritDoc} + * + * Same as {@see Seq::sequenceEither()} but merge all left errors into non-empty-list. + * + * @template E + * @template TVO + * @psalm-if-this-is LinkedList, TVO>> + * + * @return Either, LinkedList> + */ + public function sequenceEitherMerged(): Either + { + return Ops\TraverseEitherMergedOperation::id($this->getIterator())->map(LinkedList::collect(...)); } /** diff --git a/src/Fp/Collections/LinkedListBuffer.php b/src/Fp/Collections/LinkedListBuffer.php index d5d1410d..14961d43 100644 --- a/src/Fp/Collections/LinkedListBuffer.php +++ b/src/Fp/Collections/LinkedListBuffer.php @@ -7,7 +7,6 @@ /** * Provides constant time append to list * - * @internal * @template TV */ final class LinkedListBuffer diff --git a/src/Fp/Collections/Map.php b/src/Fp/Collections/Map.php index 5d45ef67..9d5b64ad 100644 --- a/src/Fp/Collections/Map.php +++ b/src/Fp/Collections/Map.php @@ -4,8 +4,6 @@ namespace Fp\Collections; -use Iterator; - /** * @template-covariant TK * @template-covariant TV diff --git a/src/Fp/Collections/MapCastableOps.php b/src/Fp/Collections/MapCastableOps.php index 21cb30b8..6bb798b1 100644 --- a/src/Fp/Collections/MapCastableOps.php +++ b/src/Fp/Collections/MapCastableOps.php @@ -8,7 +8,7 @@ use Fp\Streams\Stream; /** - * @template TK + * @template-covariant TK * @template-covariant TV */ interface MapCastableOps diff --git a/src/Fp/Collections/MapChainableOps.php b/src/Fp/Collections/MapChainableOps.php index 19d42348..6d4f9ff5 100644 --- a/src/Fp/Collections/MapChainableOps.php +++ b/src/Fp/Collections/MapChainableOps.php @@ -9,7 +9,7 @@ use Fp\Psalm\Hook\MethodReturnTypeProvider\MapTapNMethodReturnTypeProvider; /** - * @template TK + * @template-covariant TK * @template-covariant TV * * @psalm-suppress InvalidTemplateParam diff --git a/src/Fp/Collections/MapCollector.php b/src/Fp/Collections/MapCollector.php index 098462c3..756ef4c9 100644 --- a/src/Fp/Collections/MapCollector.php +++ b/src/Fp/Collections/MapCollector.php @@ -5,7 +5,7 @@ namespace Fp\Collections; /** - * @template TK + * @template-covariant TK * @template-covariant TV */ interface MapCollector diff --git a/src/Fp/Collections/MapOps.php b/src/Fp/Collections/MapOps.php index 5e1b7402..65173b3c 100644 --- a/src/Fp/Collections/MapOps.php +++ b/src/Fp/Collections/MapOps.php @@ -5,7 +5,7 @@ namespace Fp\Collections; /** - * @template TK + * @template-covariant TK * @template-covariant TV * @extends MapChainableOps * @extends MapTerminalOps diff --git a/src/Fp/Collections/MapTerminalOps.php b/src/Fp/Collections/MapTerminalOps.php index 70e9176e..703cd02a 100644 --- a/src/Fp/Collections/MapTerminalOps.php +++ b/src/Fp/Collections/MapTerminalOps.php @@ -11,7 +11,7 @@ use Fp\Psalm\Hook\MethodReturnTypeProvider\FoldMethodReturnTypeProvider; /** - * @template TK + * @template-covariant TK * @template-covariant TV * * @psalm-suppress InvalidTemplateParam @@ -218,6 +218,41 @@ public function traverseEitherN(callable $callback): Either; */ public function traverseEitherKV(callable $callback): Either; + /** + * Same as {@see MapTerminalOps::traverseEither()}, but collects all errors to non-empty-list. + * + * @template E + * @template TVO + * + * @param callable(TV): Either, TVO> $callback + * @return Either, Map> + */ + public function traverseEitherMerged(callable $callback): Either; + + /** + * Same as {@see MapTerminalOps::traverseEitherMerged()}, but passing also the key to the $callback function. + * + * @template E + * @template TVO + * + * @param callable(TK, TV): Either, TVO> $callback + * @return Either, Map> + */ + public function traverseEitherMergedKV(callable $callback): Either; + + /** + * Same as {@see MapTerminalOps::traverseEitherMerged()}, but deconstruct input tuple and pass it to the $callback function. + * + * @template E + * @template TVO + * + * @param callable(mixed...): Either, TVO> $callback + * @return Either, Map> + * + * @see MapTapNMethodReturnTypeProvider + */ + public function traverseEitherMergedN(callable $callback): Either; + /** * Same as {@see MapTerminalOps::traverseEither()} but use {@see id()} implicitly for $callback. * @@ -245,6 +280,17 @@ public function traverseEitherKV(callable $callback): Either; */ public function sequenceEither(): Either; + /** + * Same as {@see Set::sequenceEither()} but merge all left errors into non-empty-list. + * + * @template E + * @template TVO + * @psalm-if-this-is Map, TVO>> + * + * @return Either, Map> + */ + public function sequenceEitherMerged(): Either; + /** * Split collection to two parts by predicate function. * If $predicate returns true then item gonna to right. diff --git a/src/Fp/Collections/NonEmptyMapCastableOps.php b/src/Fp/Collections/NonEmptyMapCastableOps.php index 741ed5a7..87282daa 100644 --- a/src/Fp/Collections/NonEmptyMapCastableOps.php +++ b/src/Fp/Collections/NonEmptyMapCastableOps.php @@ -7,7 +7,7 @@ use Fp\Streams\Stream; /** - * @template TK + * @template-covariant TK * @template-covariant TV */ interface NonEmptyMapCastableOps diff --git a/src/Fp/Collections/NonEmptyMapChainableOps.php b/src/Fp/Collections/NonEmptyMapChainableOps.php index f7cc5254..cfeaefbd 100644 --- a/src/Fp/Collections/NonEmptyMapChainableOps.php +++ b/src/Fp/Collections/NonEmptyMapChainableOps.php @@ -7,7 +7,7 @@ use Fp\Psalm\Hook\MethodReturnTypeProvider\MapTapNMethodReturnTypeProvider; /** - * @template TK + * @template-covariant TK * @template-covariant TV * * @psalm-suppress InvalidTemplateParam diff --git a/src/Fp/Collections/NonEmptyMapCollector.php b/src/Fp/Collections/NonEmptyMapCollector.php index 8bf5f08c..97deef9b 100644 --- a/src/Fp/Collections/NonEmptyMapCollector.php +++ b/src/Fp/Collections/NonEmptyMapCollector.php @@ -7,7 +7,7 @@ use Fp\Functional\Option\Option; /** - * @template TK + * @template-covariant TK * @template-covariant TV */ interface NonEmptyMapCollector diff --git a/src/Fp/Collections/NonEmptyMapOps.php b/src/Fp/Collections/NonEmptyMapOps.php index a542a19b..b895e8e2 100644 --- a/src/Fp/Collections/NonEmptyMapOps.php +++ b/src/Fp/Collections/NonEmptyMapOps.php @@ -5,7 +5,7 @@ namespace Fp\Collections; /** - * @template TK + * @template-covariant TK * @template-covariant TV * @extends NonEmptyMapChainableOps * @extends NonEmptyMapTerminalOps diff --git a/src/Fp/Collections/NonEmptyMapTerminalOps.php b/src/Fp/Collections/NonEmptyMapTerminalOps.php index 1327d29d..b3cbfd87 100644 --- a/src/Fp/Collections/NonEmptyMapTerminalOps.php +++ b/src/Fp/Collections/NonEmptyMapTerminalOps.php @@ -11,7 +11,7 @@ use Fp\Psalm\Hook\MethodReturnTypeProvider\FoldMethodReturnTypeProvider; /** - * @template TK + * @template-covariant TK * @template-covariant TV * * @psalm-suppress InvalidTemplateParam diff --git a/src/Fp/Collections/NonEmptySetChainableOps.php b/src/Fp/Collections/NonEmptySetChainableOps.php index f8a93888..c31f1616 100644 --- a/src/Fp/Collections/NonEmptySetChainableOps.php +++ b/src/Fp/Collections/NonEmptySetChainableOps.php @@ -4,8 +4,6 @@ namespace Fp\Collections; -use Fp\Functional\Option\Option; -use Fp\Psalm\Hook\MethodReturnTypeProvider\CollectionFilterMethodReturnTypeProvider; use Fp\Psalm\Hook\MethodReturnTypeProvider\MapTapNMethodReturnTypeProvider; /** diff --git a/src/Fp/Collections/SeqTerminalOps.php b/src/Fp/Collections/SeqTerminalOps.php index 6eb52e31..e42ca66e 100644 --- a/src/Fp/Collections/SeqTerminalOps.php +++ b/src/Fp/Collections/SeqTerminalOps.php @@ -156,6 +156,30 @@ public function traverseEither(callable $callback): Either; */ public function traverseEitherN(callable $callback): Either; + /** + * Same as {@see SeqTerminalOps::traverseEither()}, but collects all errors to non-empty-list. + * + * @template E + * @template TVO + * + * @param callable(TV): Either, TVO> $callback + * @return Either, Seq> + */ + public function traverseEitherMerged(callable $callback): Either; + + /** + * Same as {@see SeqTerminalOps::traverseEitherMerged()}, but deconstruct input tuple and pass it to the $callback function. + * + * @template E + * @template TVO + * + * @param callable(mixed...): Either, TVO> $callback + * @return Either, Seq> + * + * @see MapTapNMethodReturnTypeProvider + */ + public function traverseEitherMergedN(callable $callback): Either; + /** * Same as {@see SeqTerminalOps::traverseEither()} but use {@see id()} implicitly for $callback. * @@ -175,10 +199,21 @@ public function traverseEitherN(callable $callback): Either; */ public function sequenceEither(): Either; + /** + * Same as {@see Seq::sequenceEither()} but merge all left errors into non-empty-list. + * + * @template E + * @template TVO + * @psalm-if-this-is Seq, TVO>> + * + * @return Either, Seq> + */ + public function sequenceEitherMerged(): Either; + /** * Split collection to two parts by predicate function. * If $predicate returns true then item gonna to right. - * Otherwise to left. + * Otherwise, to left. * * ```php * >>> ArrayList::collect([0, 1, 2, 3, 4, 5])->partition(fn($i) => $i < 3); diff --git a/src/Fp/Collections/SetTerminalOps.php b/src/Fp/Collections/SetTerminalOps.php index d58027d4..ceade6e8 100644 --- a/src/Fp/Collections/SetTerminalOps.php +++ b/src/Fp/Collections/SetTerminalOps.php @@ -118,7 +118,7 @@ public function traverseOptionN(callable $callback): Option; public function sequenceOption(): Option; /** - * Suppose you have an Set and you want to format each element with a function that returns an Either. + * Suppose you have a Set and you want to format each element with a function that returns an Either. * Using traverseEither you can apply $callback to all elements and directly obtain as a result an Either> * i.e. an Right> if all the results are Right, or a Left if at least one result is Left. * @@ -147,6 +147,30 @@ public function traverseEither(callable $callback): Either; */ public function traverseEitherN(callable $callback): Either; + /** + * Same as {@see SetTerminalOps::traverseEither()}, but collects all errors to non-empty-list. + * + * @template E + * @template TVO + * + * @param callable(TV): Either, TVO> $callback + * @return Either, Set> + */ + public function traverseEitherMerged(callable $callback): Either; + + /** + * Same as {@see SetTerminalOps::traverseEitherMerged()}, but deconstruct input tuple and pass it to the $callback function. + * + * @template E + * @template TVO + * + * @param callable(mixed...): Either, TVO> $callback + * @return Either, Set> + * + * @see MapTapNMethodReturnTypeProvider + */ + public function traverseEitherMergedN(callable $callback): Either; + /** * Same as {@see SetTerminalOps::traverseEither()} but use {@see id()} implicitly for $callback. * @@ -166,10 +190,21 @@ public function traverseEitherN(callable $callback): Either; */ public function sequenceEither(): Either; + /** + * Same as {@see Set::sequenceEither()} but merge all left errors into non-empty-list. + * + * @template E + * @template TVO + * @psalm-if-this-is Set, TVO>> + * + * @return Either, Set> + */ + public function sequenceEitherMerged(): Either; + /** * Split collection to two parts by predicate function. * If $predicate returns true then item gonna to right. - * Otherwise to left. + * Otherwise, to left. * * ```php * >>> HashSet::collect([0, 1, 2, 3, 4, 5])->partition(fn($i) => $i < 3); @@ -191,7 +226,7 @@ public function partitionN(callable $predicate): Separated; * Similar to {@see SetTerminalOps::partition()} but uses {@see Either} instead of bool. * So the output types LO/RO can be different from the input type TV. * If $callback returns Right then item gonna to right. - * Otherwise to left. + * Otherwise, to left. * * ```php * >>> HashSet::collect([0, 1, 2, 3, 4, 5]) diff --git a/src/Fp/Functional/Assertion.php b/src/Fp/Functional/Assertion.php index 520ca41c..181601c3 100644 --- a/src/Fp/Functional/Assertion.php +++ b/src/Fp/Functional/Assertion.php @@ -5,9 +5,8 @@ namespace Fp\Functional; /** - * @template T of string + * @template T */ interface Assertion { - } diff --git a/src/Fp/Functional/Either/Either.php b/src/Fp/Functional/Either/Either.php index 69ec9506..47646a9b 100644 --- a/src/Fp/Functional/Either/Either.php +++ b/src/Fp/Functional/Either/Either.php @@ -276,13 +276,11 @@ public static function firstMergedT(Either|Closure $first, Either|Closure ...$ta * => Left('error!') * ``` * - * @todo Replace Either with Either and drop suppress @see https://github.com/vimeo/psalm/issues/6288 - * * @template TL * @template TR * @template TO * - * @param callable(): Generator, TR, TO> $computation + * @param callable(): Generator, TR, TO> $computation * @return Either */ public static function do(callable $computation): Either { @@ -291,14 +289,11 @@ public static function do(callable $computation): Either { while ($generator->valid()) { $currentStep = $generator->current(); - if ($currentStep->isRight()) { - /** @psalm-suppress MixedArgument */ - $generator->send($currentStep->get()); - } else { - /** @var Either $currentStep */ + if ($currentStep->isLeft()) { return $currentStep; } + $generator->send($currentStep->get()); } return Either::right($generator->getReturn()); @@ -442,7 +437,8 @@ public function getOrCall(callable $fallback): mixed * => true * ``` * - * @psalm-assert-if-true Left&\Fp\Functional\Assertion<"must-be-left"> $this + * @psalm-assert-if-true Left&\Fp\Functional\Assertion $this + * @psalm-assert-if-false Right&\Fp\Functional\Assertion $this */ public function isLeft(): bool { @@ -460,7 +456,8 @@ public function isLeft(): bool * => false * ``` * - * @psalm-assert-if-true Right&\Fp\Functional\Assertion<"must-be-right"> $this + * @psalm-assert-if-true Right&\Fp\Functional\Assertion $this + * @psalm-assert-if-false Left&\Fp\Functional\Assertion $this */ public function isRight(): bool { @@ -728,7 +725,10 @@ public function flatMapN(callable $callback): Either public function flatTap(callable $callback): Either { return $this->flatMap( - /** @param R $r */ + /** + * @param R $r + * @psalm-suppress InvalidArgument + */ fn(mixed $r) => $callback($r)->fold( fn($l) => Either::left($l), fn() => Either::right($r), diff --git a/src/Fp/Functional/Option/Option.php b/src/Fp/Functional/Option/Option.php index f585065b..c9fec1b2 100644 --- a/src/Fp/Functional/Option/Option.php +++ b/src/Fp/Functional/Option/Option.php @@ -237,12 +237,10 @@ public static function firstT(Option|Closure $head, Option|Closure ...$tail): Op * => None * ``` * - * @todo Replace Option with Option and drop suppress @see https://github.com/vimeo/psalm/issues/6288 - * * @template TS * @template TO * - * @param callable(): Generator, TS, TO> $computation + * @param callable(): Generator, TS, TO> $computation * @return Option */ public static function do(callable $computation): Option { @@ -251,14 +249,11 @@ public static function do(callable $computation): Option { while ($generator->valid()) { $currentStep = $generator->current(); - if ($currentStep->isSome()) { - /** @psalm-suppress MixedArgument */ - $generator->send($currentStep->get()); - } else { - /** @var Option $currentStep */ + if ($currentStep->isNone()) { return $currentStep; } + $generator->send($currentStep->get()); } return Option::some($generator->getReturn()); @@ -398,7 +393,8 @@ public function getOrCall(callable $fallback): mixed * => false * ``` * - * @psalm-assert-if-true Some&\Fp\Functional\Assertion<"must-be-some"> $this + * @psalm-assert-if-true Some&\Fp\Functional\Assertion $this + * @psalm-assert-if-false None&\Fp\Functional\Assertion $this */ public function isSome(): bool { @@ -413,7 +409,8 @@ public function isSome(): bool * => true * ``` * - * @psalm-assert-if-true None&\Fp\Functional\Assertion<"must-be-none"> $this + * @psalm-assert-if-true None&\Fp\Functional\Assertion $this + * @psalm-assert-if-false Some&\Fp\Functional\Assertion $this */ public function isNone(): bool { @@ -712,7 +709,10 @@ public function flatMapN(callable $callback): Option public function flatTap(callable $callback): Option { return $this->flatMap( - /** @param A $value */ + /** + * @param A $value + * @psalm-suppress InvalidArgument + */ fn(mixed $value) => $callback($value)->fold( fn() => Option::none(), fn() => Option::some($value), diff --git a/src/Fp/Functions/Callable/Pipe.php b/src/Fp/Functions/Callable/Pipe.php new file mode 100644 index 00000000..4824ff7c --- /dev/null +++ b/src/Fp/Functions/Callable/Pipe.php @@ -0,0 +1,33 @@ +>> pipe( + * >>> 0, + * >>> fn($i) => $i + 11, + * >>> fn($i) => $i + 20, + * >>> fn($i) => $i + 11, + * >>> ); + * => 42 + * ``` + * + * @see PipeFunctionStorageProvider + * @no-named-arguments + */ +function pipe(mixed $a, callable $head, callable ...$tail): mixed +{ + foreach ([$head, ...$tail] as $function) { + /** @psalm-suppress MixedAssignment */ + $a = $function($a); + } + + return $a; +} diff --git a/src/Fp/Functions/Cast/AsGenerator.php b/src/Fp/Functions/Cast/AsGenerator.php index 978475ee..dcdc01ab 100644 --- a/src/Fp/Functions/Cast/AsGenerator.php +++ b/src/Fp/Functions/Cast/AsGenerator.php @@ -8,14 +8,14 @@ /** * ```php - * >>> asGenerator(function { yield 1; yield 2; }); + * >>> asGenerator(function() { yield 1; yield 2; }); * => Generator(1, 2) * ``` * * @template TK * @template TV * - * @param callable(): iterable $callback + * @param callable(): (iterable | Generator) $callback * @return Generator */ function asGenerator(callable $callback): Generator @@ -34,4 +34,3 @@ function asGenerator(callable $callback): Generator return $generator(); } - diff --git a/src/Fp/Functions/Cast/AsNonEmptyArray.php b/src/Fp/Functions/Cast/AsNonEmptyArray.php index 31c61554..6ce67f5b 100644 --- a/src/Fp/Functions/Cast/AsNonEmptyArray.php +++ b/src/Fp/Functions/Cast/AsNonEmptyArray.php @@ -6,9 +6,6 @@ use Fp\Functional\Option\Option; -use function Fp\Collection\head; -use function Fp\Collection\map; - /** * Try copy and cast collection to non-empty-array * Returns None if there is no first collection element diff --git a/src/Fp/Functions/Collection/Drop.php b/src/Fp/Functions/Collection/Drop.php index 1ec421ee..541bfca5 100644 --- a/src/Fp/Functions/Collection/Drop.php +++ b/src/Fp/Functions/Collection/Drop.php @@ -28,11 +28,10 @@ */ function drop(iterable $collection, int $length): array { + $isList = is_array($collection) && array_is_list($collection); $gen = DropOperation::of($collection)($length); - return is_array($collection) && array_is_list($collection) - ? asList($gen) - : asArray($gen); + return $isList ? asList($gen) : asArray($gen); } /** @@ -73,9 +72,8 @@ function dropRight(iterable $collection, int $length): array */ function dropWhile(iterable $collection, callable $predicate): array { + $isList = is_array($collection) && array_is_list($collection); $gen = DropWhileOperation::of($collection)(dropFirstArg($predicate)); - return is_array($collection) && array_is_list($collection) - ? asList($gen) - : asArray($gen); + return $isList ? asList($gen) : asArray($gen); } diff --git a/src/Fp/Functions/Collection/Filter.php b/src/Fp/Functions/Collection/Filter.php index b607c234..26f001e8 100644 --- a/src/Fp/Functions/Collection/Filter.php +++ b/src/Fp/Functions/Collection/Filter.php @@ -56,10 +56,10 @@ function filter(iterable $collection, callable $predicate): array */ function filterKV(iterable $collection, callable $predicate): array { + $isList = is_array($collection) && array_is_list($collection); $gen = FilterOperation::of($collection)($predicate); - return is_array($collection) && array_is_list($collection) - ? asList($gen) - : asArray($gen); + + return $isList ? asList($gen) : asArray($gen); } /** @@ -81,11 +81,10 @@ function filterKV(iterable $collection, callable $predicate): array */ function filterNotNull(iterable $collection): array { + $isList = is_array($collection) && array_is_list($collection); $gen = FilterNotNullOperation::of($collection)(); - return is_array($collection) && array_is_list($collection) - ? asList($gen) - : asArray($gen); + return $isList ? asList($gen) : asArray($gen); } /** @@ -130,9 +129,8 @@ function filterMap(iterable $collection, callable $predicate): array */ function filterMapKV(iterable $collection, callable $predicate): array { + $isList = is_array($collection) && array_is_list($collection); $gen = FilterMapOperation::of($collection)($predicate); - return is_array($collection) && array_is_list($collection) - ? asList($gen) - : asArray($gen); + return $isList ? asList($gen) : asArray($gen); } diff --git a/src/Fp/Functions/Collection/GroupMap.php b/src/Fp/Functions/Collection/GroupMap.php index 6a7fd6c7..c17f4574 100644 --- a/src/Fp/Functions/Collection/GroupMap.php +++ b/src/Fp/Functions/Collection/GroupMap.php @@ -5,8 +5,8 @@ namespace Fp\Collection; use Fp\Collections\NonEmptyHashMap; -use Fp\Collections\NonEmptyLinkedList; use Fp\Operations\GroupMapOperation; + use function Fp\Callable\dropFirstArg; /** diff --git a/src/Fp/Functions/Collection/Sequence.php b/src/Fp/Functions/Collection/Sequence.php index 02553f09..82e1392e 100644 --- a/src/Fp/Functions/Collection/Sequence.php +++ b/src/Fp/Functions/Collection/Sequence.php @@ -7,7 +7,6 @@ use Closure; use Fp\Functional\Either\Either; use Fp\Functional\Option\Option; -use Fp\Operations\TraverseEitherAccOperation; use Fp\Operations\TraverseEitherMergedOperation; use Fp\Operations\TraverseEitherOperation; use Fp\Operations\TraverseOptionOperation; @@ -71,6 +70,20 @@ function sequenceEither(iterable $collection): Either return TraverseEitherOperation::id($collection)->map(asArray(...)); } +/** + * Varargs version of {@see sequenceEither()}. + * + * @template E + * @template TVI + * + * @param Either | Closure(): Either ...$items + * @return Either> + */ +function sequenceEitherT(Either|Closure ...$items): Either +{ + return TraverseEitherOperation::id($items)->map(asList(...)); +} + /** * Same as {@see traverseEither()} but merge all left errors into non-empty-list. * @@ -106,45 +119,3 @@ function sequenceEitherMergedT(Either|Closure ...$items): Either { return TraverseEitherMergedOperation::id($items)->map(asList(...)); } - -/** - * Varargs version of {@see sequenceEither()}. - * - * @template E - * @template TVI - * - * @param Either | Closure(): Either ...$items - * @return Either> - */ -function sequenceEitherT(Either|Closure ...$items): Either -{ - return TraverseEitherOperation::id($items)->map(asList(...)); -} - -/** - * Same as {@see sequenceEither()} but accumulates all left errors. - * - * @template E - * @template TK of array-key - * @template TVI - * - * @param iterable | Closure(): Either> $collection - * @return Either, array> - * @psalm-return ( - * $collection is non-empty-list ? Either, non-empty-list> : - * $collection is list ? Either, list> : - * $collection is non-empty-array ? Either, non-empty-array> : - * Either, array> - * ) - */ -function sequenceEitherAcc(iterable $collection): Either -{ - return TraverseEitherAccOperation::id($collection) - ->mapLeft(function($gen) use ($collection) { - /** @var non-empty-array */ - return is_array($collection) && array_is_list($collection) - ? asList($gen) - : asArray($gen); - }) - ->map(asArray(...)); -} diff --git a/src/Fp/Functions/Collection/Traverse.php b/src/Fp/Functions/Collection/Traverse.php index 811108ee..2bee57ce 100644 --- a/src/Fp/Functions/Collection/Traverse.php +++ b/src/Fp/Functions/Collection/Traverse.php @@ -6,14 +6,12 @@ use Fp\Functional\Either\Either; use Fp\Functional\Option\Option; -use Fp\Operations\TraverseEitherAccOperation; use Fp\Operations\TraverseEitherMergedOperation; use Fp\Operations\TraverseEitherOperation; use Fp\Operations\TraverseOptionOperation; use function Fp\Callable\dropFirstArg; use function Fp\Cast\asArray; -use function Fp\Cast\asList; /** * Suppose you have a list and you want to format each element with a function that returns an Option. @@ -160,57 +158,3 @@ function traverseEitherMergedKV(iterable $collection, callable $callback): Eithe { return TraverseEitherMergedOperation::of($collection)($callback)->map(asArray(...)); } - -/** - * Same as {@see traverseEither()} but accumulates all left errors. - * - * @template E - * @template TK of array-key - * @template TV - * @template TVO - * - * @param iterable $collection - * @param callable(TV): Either $callback - * @return Either> - * @psalm-return ( - * $collection is non-empty-list ? Either, non-empty-list> : - * $collection is list ? Either, list> : - * $collection is non-empty-array ? Either, non-empty-array> : - * Either, array> - * ) - */ -function traverseEitherAcc(iterable $collection, callable $callback): Either -{ - return traverseEitherKVAcc($collection, dropFirstArg($callback)); -} - -/** - * Same as {@see traverseEitherKV()} but accumulates all left errors. - * - * @template E - * @template TK of array-key - * @template TV - * @template TVO - * - * @param iterable $collection - * @param callable(TK, TV): Either $callback - * @return Either> - * - * @psalm-return ( - * $collection is non-empty-list ? Either, non-empty-list> : - * $collection is list ? Either, list> : - * $collection is non-empty-array ? Either, non-empty-array> : - * Either, array> - * ) - */ -function traverseEitherKVAcc(iterable $collection, callable $callback): Either -{ - return TraverseEitherAccOperation::of($collection)($callback) - ->mapLeft(function($gen) use ($collection) { - /** @var non-empty-array */ - return is_array($collection) && array_is_list($collection) - ? asList($gen) - : asArray($gen); - }) - ->map(asArray(...)); -} diff --git a/src/Fp/Functions/Collection/Unique.php b/src/Fp/Functions/Collection/Unique.php index f0406ba1..88bbfe5b 100644 --- a/src/Fp/Functions/Collection/Unique.php +++ b/src/Fp/Functions/Collection/Unique.php @@ -9,7 +9,6 @@ use function Fp\Cast\asArray; use function Fp\Cast\asList; - /** * Returns collection unique elements * @@ -25,11 +24,9 @@ */ function unique(iterable $collection): array { - $gen = UniqueByOperation::of($collection)(fn(mixed $i): mixed => $i); - return is_array($collection) && array_is_list($collection) - ? asList($gen) - : asArray($gen); + ? asList(UniqueByOperation::of($collection)(fn(mixed $i): mixed => $i)) + : asArray(UniqueByOperation::of($collection)(fn(mixed $i): mixed => $i)); } /** @@ -51,9 +48,7 @@ function unique(iterable $collection): array */ function uniqueBy(iterable $collection, callable $callback): array { - $gen = UniqueByOperation::of($collection)($callback); - return is_array($collection) && array_is_list($collection) - ? asList($gen) - : asArray($gen); + ? asList(UniqueByOperation::of($collection)($callback)) + : asArray(UniqueByOperation::of($collection)($callback)); } diff --git a/src/Fp/Functions/Evidence/ProveArray.php b/src/Fp/Functions/Evidence/ProveArray.php index 98c3f255..1ff246b9 100644 --- a/src/Fp/Functions/Evidence/ProveArray.php +++ b/src/Fp/Functions/Evidence/ProveArray.php @@ -53,8 +53,6 @@ * ? ($vType is null ? Option> : Option>) * : ($vType is null ? Option> : Option>) * ) - * - * @psalm-suppress MixedAssignment, PossiblyNullArrayOffset, InvalidReturnStatement, InvalidReturnType */ function proveArray(mixed $value, null|callable $kType = null, null|callable $vType = null): Option { diff --git a/src/Fp/Functions/Evidence/ProveString.php b/src/Fp/Functions/Evidence/ProveString.php index 71fb3b09..169acd25 100644 --- a/src/Fp/Functions/Evidence/ProveString.php +++ b/src/Fp/Functions/Evidence/ProveString.php @@ -88,7 +88,10 @@ function proveClassStringOf(mixed $potential, string|array $fqcn, bool $invarian */ function classStringOf(string|array $fqcn, bool $invariant = false): Closure { - return fn(mixed $potential) => proveClassStringOf($potential, $fqcn, $invariant); + return function(mixed $potential) use ($fqcn, $invariant) { + /** @var Option> */ + return proveClassStringOf($potential, $fqcn, $invariant); + }; } /** diff --git a/src/Fp/Functions/Evidence/ProveUnion.php b/src/Fp/Functions/Evidence/ProveUnion.php index 9bf1dc63..c892c73c 100644 --- a/src/Fp/Functions/Evidence/ProveUnion.php +++ b/src/Fp/Functions/Evidence/ProveUnion.php @@ -70,6 +70,8 @@ function union(array $evidences): Closure * @param Closure(mixed): Option $evidence * @param Closure(mixed): Option ...$evidences * @return Closure(mixed): Option + * + * @no-named-arguments */ function unionT(Closure $evidence, Closure ...$evidences): Closure { diff --git a/src/Fp/Functions/Util/JsonDecode.php b/src/Fp/Functions/Util/JsonDecode.php index 669213d7..4c5d191c 100644 --- a/src/Fp/Functions/Util/JsonDecode.php +++ b/src/Fp/Functions/Util/JsonDecode.php @@ -25,6 +25,7 @@ * => Left(JsonException('Syntax error')) * ``` * + * @param int<1, 2147483647> $depth * @return Either */ function jsonDecode(string $json, int $depth = 512, int $flags = 0): Either @@ -68,6 +69,8 @@ function jsonDecodeArray(string $json): Either * >>> jsonEncode([1, 2, 3]); * => '[1, 2, 3]' * ``` + * + * @param int<1, 2147483647> $depth */ function jsonEncode(mixed $value, int $flags = JSON_THROW_ON_ERROR, int $depth = 512): string { diff --git a/src/Fp/Functions/Util/ToString.php b/src/Fp/Functions/Util/ToString.php new file mode 100644 index 00000000..c1f84aec --- /dev/null +++ b/src/Fp/Functions/Util/ToString.php @@ -0,0 +1,12 @@ +gen as $prefixElem) { - yield $prefixElem; - } + foreach ($this->gen as $prefixElem) { + yield $prefixElem; + } - foreach ($suffix as $suffixElem) { - yield $suffixElem; - } - }); + foreach ($suffix as $suffixElem) { + yield $suffixElem; + } } } diff --git a/src/Fp/Operations/AppendedOperation.php b/src/Fp/Operations/AppendedOperation.php index 2d1d13d2..b7adf650 100644 --- a/src/Fp/Operations/AppendedOperation.php +++ b/src/Fp/Operations/AppendedOperation.php @@ -6,8 +6,6 @@ use Generator; -use function Fp\Cast\asGenerator; - /** * @template TK * @template TV @@ -24,12 +22,10 @@ final class AppendedOperation extends AbstractOperation */ public function __invoke(mixed $elem): Generator { - return asGenerator(function () use ($elem) { - foreach ($this->gen as $prefixElem) { - yield $prefixElem; - } + foreach ($this->gen as $prefixElem) { + yield $prefixElem; + } - yield $elem; - }); + yield $elem; } } diff --git a/src/Fp/Operations/ChunksOperation.php b/src/Fp/Operations/ChunksOperation.php index b23da3ff..165f7307 100644 --- a/src/Fp/Operations/ChunksOperation.php +++ b/src/Fp/Operations/ChunksOperation.php @@ -6,8 +6,6 @@ use Generator; -use function Fp\Cast\asGenerator; - /** * @template TK * @template TV @@ -22,24 +20,22 @@ final class ChunksOperation extends AbstractOperation */ public function __invoke(int $size): Generator { - return asGenerator(function () use ($size) { - $chunk = []; - $i = 0; - - foreach ($this->gen as $value) { - $i++; + $chunk = []; + $i = 0; - $chunk[] = $value; + foreach ($this->gen as $value) { + $i++; - if (0 === $i % $size) { - yield $chunk; - $chunk = []; - } - } + $chunk[] = $value; - if (!empty($chunk)) { + if (0 === $i % $size) { yield $chunk; + $chunk = []; } - }); + } + + if (!empty($chunk)) { + yield $chunk; + } } } diff --git a/src/Fp/Operations/DropOperation.php b/src/Fp/Operations/DropOperation.php index 96e7b0b3..f04c9ae4 100644 --- a/src/Fp/Operations/DropOperation.php +++ b/src/Fp/Operations/DropOperation.php @@ -6,8 +6,6 @@ use Generator; -use function Fp\Cast\asGenerator; - /** * @template TK * @template TV @@ -21,17 +19,15 @@ final class DropOperation extends AbstractOperation */ public function __invoke(int $length): Generator { - return asGenerator(function () use ($length) { - $i = 0; - - foreach ($this->gen as $key => $value) { - if ($i < $length) { - $i++; - continue; - } + $i = 0; - yield $key => $value; + foreach ($this->gen as $key => $value) { + if ($i < $length) { + $i++; + continue; } - }); + + yield $key => $value; + } } } diff --git a/src/Fp/Operations/DropWhileOperation.php b/src/Fp/Operations/DropWhileOperation.php index 8b85be9a..b3ccf4fd 100644 --- a/src/Fp/Operations/DropWhileOperation.php +++ b/src/Fp/Operations/DropWhileOperation.php @@ -6,8 +6,6 @@ use Generator; -use function Fp\Cast\asGenerator; - /** * @template TK * @template TV @@ -24,14 +22,12 @@ final class DropWhileOperation extends AbstractOperation */ public function __invoke(callable $f): Generator { - return asGenerator(function () use ($f) { - $toggle = true; + $toggle = true; - foreach ($this->gen as $key => $value) { - if (!($toggle = $toggle && $f($key, $value))) { - yield $key => $value; - } + foreach ($this->gen as $key => $value) { + if (!($toggle = $toggle && $f($key, $value))) { + yield $key => $value; } - }); + } } } diff --git a/src/Fp/Operations/FilterMapOperation.php b/src/Fp/Operations/FilterMapOperation.php index 98ce4e16..cc3bd42f 100644 --- a/src/Fp/Operations/FilterMapOperation.php +++ b/src/Fp/Operations/FilterMapOperation.php @@ -7,8 +7,6 @@ use Fp\Functional\Option\Option; use Generator; -use function Fp\Cast\asGenerator; - /** * @template TK * @template TV @@ -25,14 +23,12 @@ final class FilterMapOperation extends AbstractOperation */ public function __invoke(callable $f): Generator { - return asGenerator(function () use ($f) { - foreach ($this->gen as $key => $value) { - $res = $f($key, $value); + foreach ($this->gen as $key => $value) { + $res = $f($key, $value); - if ($res->isSome()) { - yield $key => $res->get(); - } + if ($res->isSome()) { + yield $key => $res->get(); } - }); + } } } diff --git a/src/Fp/Operations/FilterNotNullOperation.php b/src/Fp/Operations/FilterNotNullOperation.php index 09b5718e..4898399f 100644 --- a/src/Fp/Operations/FilterNotNullOperation.php +++ b/src/Fp/Operations/FilterNotNullOperation.php @@ -6,8 +6,6 @@ use Generator; -use function Fp\Cast\asGenerator; - /** * @template TK * @template TV @@ -21,12 +19,10 @@ final class FilterNotNullOperation extends AbstractOperation */ public function __invoke(): Generator { - return asGenerator(function () { - foreach ($this->gen as $key => $value) { - if (null !== $value) { - yield $key => $value; - } + foreach ($this->gen as $key => $value) { + if (null !== $value) { + yield $key => $value; } - }); + } } } diff --git a/src/Fp/Operations/FlatMapOperation.php b/src/Fp/Operations/FlatMapOperation.php index 02f39488..2a3666df 100644 --- a/src/Fp/Operations/FlatMapOperation.php +++ b/src/Fp/Operations/FlatMapOperation.php @@ -7,8 +7,6 @@ use Generator; use Fp\Collections\Collection; -use function Fp\Cast\asGenerator; - /** * @template TK * @template TV @@ -26,14 +24,12 @@ final class FlatMapOperation extends AbstractOperation */ public function __invoke(callable $f): Generator { - return asGenerator(function () use ($f) { - foreach ($this->gen as $key => $value) { - $xs = $f($key, $value); + foreach ($this->gen as $key => $value) { + $xs = $f($key, $value); - foreach ($xs as $k => $x) { - yield $k => $x; - } + foreach ($xs as $k => $x) { + yield $k => $x; } - }); + } } } diff --git a/src/Fp/Operations/GroupAdjacentByOperation.php b/src/Fp/Operations/GroupAdjacentByOperation.php index 2504fa81..a1266d36 100644 --- a/src/Fp/Operations/GroupAdjacentByOperation.php +++ b/src/Fp/Operations/GroupAdjacentByOperation.php @@ -6,8 +6,6 @@ use Generator; -use function Fp\Cast\asGenerator; - /** * @template TK * @template TV @@ -24,31 +22,29 @@ final class GroupAdjacentByOperation extends AbstractOperation */ public function __invoke(callable $discriminator): Generator { - return asGenerator(function () use ($discriminator) { - $buffer = []; - $prevDisc = null; - $isHead = true; - - foreach ($this->gen as $elem) { - if ($isHead) { - $isHead = false; - $prevDisc = $discriminator($elem); - } - - $curDisc = $discriminator($elem); - - if ($prevDisc !== $curDisc && !empty($buffer)) { - yield [$prevDisc, $buffer]; - $buffer = []; - } - - $buffer[] = $elem; - $prevDisc = $curDisc; + $buffer = []; + $prevDisc = null; + $isHead = true; + + foreach ($this->gen as $elem) { + if ($isHead) { + $isHead = false; + $prevDisc = $discriminator($elem); } - if (!empty($buffer)) { + $curDisc = $discriminator($elem); + + if ($prevDisc !== $curDisc && !empty($buffer)) { yield [$prevDisc, $buffer]; + $buffer = []; } - }); + + $buffer[] = $elem; + $prevDisc = $curDisc; + } + + if (!empty($buffer)) { + yield [$prevDisc, $buffer]; + } } } diff --git a/src/Fp/Operations/IntersperseOperation.php b/src/Fp/Operations/IntersperseOperation.php index c2feea66..c1d2312e 100644 --- a/src/Fp/Operations/IntersperseOperation.php +++ b/src/Fp/Operations/IntersperseOperation.php @@ -6,8 +6,6 @@ use Generator; -use function Fp\Cast\asGenerator; - /** * @template TK * @template TV @@ -24,18 +22,16 @@ final class IntersperseOperation extends AbstractOperation */ public function __invoke(mixed $separator): Generator { - return asGenerator(function () use ($separator) { - $isFirst = true; - - foreach ($this->gen as $elem) { - if ($isFirst) { - $isFirst = false; - } else { - yield $separator; - } + $isFirst = true; - yield $elem; + foreach ($this->gen as $elem) { + if ($isFirst) { + $isFirst = false; + } else { + yield $separator; } - }); + + yield $elem; + } } } diff --git a/src/Fp/Operations/KeysOperation.php b/src/Fp/Operations/KeysOperation.php index ddbf8ce1..431fd6da 100644 --- a/src/Fp/Operations/KeysOperation.php +++ b/src/Fp/Operations/KeysOperation.php @@ -6,8 +6,6 @@ use Generator; -use function Fp\Cast\asGenerator; - /** * @template TK * @template TV @@ -21,10 +19,8 @@ final class KeysOperation extends AbstractOperation */ public function __invoke(): Generator { - return asGenerator(function () { - foreach ($this->gen as $key => $value) { - yield $key; - } - }); + foreach ($this->gen as $key => $value) { + yield $key; + } } } diff --git a/src/Fp/Operations/MapOperation.php b/src/Fp/Operations/MapOperation.php index be1b7c5b..f51e42e7 100644 --- a/src/Fp/Operations/MapOperation.php +++ b/src/Fp/Operations/MapOperation.php @@ -6,8 +6,6 @@ use Generator; -use function Fp\Cast\asGenerator; - /** * @template TK * @template TV @@ -24,10 +22,8 @@ final class MapOperation extends AbstractOperation */ public function __invoke(callable $f): Generator { - return asGenerator(function () use ($f) { - foreach ($this->gen as $key => $value) { - yield $key => $f($key, $value); - } - }); + foreach ($this->gen as $key => $value) { + yield $key => $f($key, $value); + } } } diff --git a/src/Fp/Operations/PrependedAllOperation.php b/src/Fp/Operations/PrependedAllOperation.php index 3807e25e..7549b3ce 100644 --- a/src/Fp/Operations/PrependedAllOperation.php +++ b/src/Fp/Operations/PrependedAllOperation.php @@ -6,8 +6,6 @@ use Generator; -use function Fp\Cast\asGenerator; - /** * @template TK * @template TV @@ -24,14 +22,12 @@ final class PrependedAllOperation extends AbstractOperation */ public function __invoke(iterable $prefix): Generator { - return asGenerator(function () use ($prefix) { - foreach ($prefix as $prefixElem) { - yield $prefixElem; - } + foreach ($prefix as $prefixElem) { + yield $prefixElem; + } - foreach ($this->gen as $suffixElem) { - yield $suffixElem; - } - }); + foreach ($this->gen as $suffixElem) { + yield $suffixElem; + } } } diff --git a/src/Fp/Operations/PrependedOperation.php b/src/Fp/Operations/PrependedOperation.php index 9dcfa7c0..2ae60280 100644 --- a/src/Fp/Operations/PrependedOperation.php +++ b/src/Fp/Operations/PrependedOperation.php @@ -6,8 +6,6 @@ use Generator; -use function Fp\Cast\asGenerator; - /** * @template TK * @template TV @@ -24,12 +22,10 @@ final class PrependedOperation extends AbstractOperation */ public function __invoke(mixed $elem): Generator { - return asGenerator(function () use ($elem) { - yield $elem; + yield $elem; - foreach ($this->gen as $suffixElem) { - yield $suffixElem; - } - }); + foreach ($this->gen as $suffixElem) { + yield $suffixElem; + } } } diff --git a/src/Fp/Operations/RepeatNOperation.php b/src/Fp/Operations/RepeatNOperation.php index de4c5efc..a56efe91 100644 --- a/src/Fp/Operations/RepeatNOperation.php +++ b/src/Fp/Operations/RepeatNOperation.php @@ -7,8 +7,6 @@ use Fp\Collections\ArrayList; use Generator; -use function Fp\Cast\asGenerator; - /** * @template TK * @template TV @@ -22,18 +20,16 @@ final class RepeatNOperation extends AbstractOperation */ public function __invoke(int $times): Generator { - return asGenerator(function () use ($times) { - $buffer = ArrayList::collect($this->gen); + $buffer = ArrayList::collect($this->gen); + + foreach ($buffer as $elem) { + yield $elem; + } + for ($i = 0; $i < $times - 1; $i++) { foreach ($buffer as $elem) { yield $elem; } - - for($i = 0; $i < $times - 1; $i++) { - foreach ($buffer as $elem) { - yield $elem; - } - } - }); + } } } diff --git a/src/Fp/Operations/RepeatOperation.php b/src/Fp/Operations/RepeatOperation.php index c68bc856..61744e68 100644 --- a/src/Fp/Operations/RepeatOperation.php +++ b/src/Fp/Operations/RepeatOperation.php @@ -7,8 +7,6 @@ use Fp\Collections\ArrayList; use Generator; -use function Fp\Cast\asGenerator; - /** * @template TK * @template TV @@ -22,18 +20,16 @@ final class RepeatOperation extends AbstractOperation */ public function __invoke(): Generator { - return asGenerator(function () { - $buffer = ArrayList::collect($this->gen); + $buffer = ArrayList::collect($this->gen); + + foreach ($buffer as $elem) { + yield $elem; + } + while (true) { foreach ($buffer as $elem) { yield $elem; } - - while(true) { - foreach ($buffer as $elem) { - yield $elem; - } - } - }); + } } } diff --git a/src/Fp/Operations/TailOperation.php b/src/Fp/Operations/TailOperation.php index 6334c436..c91b2ab1 100644 --- a/src/Fp/Operations/TailOperation.php +++ b/src/Fp/Operations/TailOperation.php @@ -6,8 +6,6 @@ use Generator; -use function Fp\Cast\asGenerator; - /** * @template TK * @template TV @@ -21,17 +19,15 @@ final class TailOperation extends AbstractOperation */ public function __invoke(): Generator { - return asGenerator(function () { - $isFirst = true; - - foreach ($this->gen as $key => $value) { - if ($isFirst) { - $isFirst = false; - continue; - } + $isFirst = true; - yield $key => $value; + foreach ($this->gen as $key => $value) { + if ($isFirst) { + $isFirst = false; + continue; } - }); + + yield $key => $value; + } } } diff --git a/src/Fp/Operations/TakeOperation.php b/src/Fp/Operations/TakeOperation.php index 18fb1498..d2a4da71 100644 --- a/src/Fp/Operations/TakeOperation.php +++ b/src/Fp/Operations/TakeOperation.php @@ -6,8 +6,6 @@ use Generator; -use function Fp\Cast\asGenerator; - /** * @template TK * @template TV @@ -21,17 +19,15 @@ final class TakeOperation extends AbstractOperation */ public function __invoke(int $length): Generator { - return asGenerator(function () use ($length) { - $i = 0; - - foreach ($this->gen as $key => $value) { - if ($i === $length) { - break; - } + $i = 0; - yield $key => $value; - $i++; + foreach ($this->gen as $key => $value) { + if ($i === $length) { + break; } - }); + + yield $key => $value; + $i++; + } } } diff --git a/src/Fp/Operations/TakeWhileOperation.php b/src/Fp/Operations/TakeWhileOperation.php index 65ba3c70..764de2a3 100644 --- a/src/Fp/Operations/TakeWhileOperation.php +++ b/src/Fp/Operations/TakeWhileOperation.php @@ -6,8 +6,6 @@ use Generator; -use function Fp\Cast\asGenerator; - /** * @template TK * @template TV @@ -24,14 +22,12 @@ final class TakeWhileOperation extends AbstractOperation */ public function __invoke(callable $f): Generator { - return asGenerator(function () use ($f) { - foreach ($this->gen as $key => $value) { - if (!$f($key, $value)) { - break; - } - - yield $key => $value; + foreach ($this->gen as $key => $value) { + if (!$f($key, $value)) { + break; } - }); + + yield $key => $value; + } } } diff --git a/src/Fp/Operations/TapOperation.php b/src/Fp/Operations/TapOperation.php index 69e3f986..bda69cdf 100644 --- a/src/Fp/Operations/TapOperation.php +++ b/src/Fp/Operations/TapOperation.php @@ -6,8 +6,6 @@ use Generator; -use function Fp\Cast\asGenerator; - /** * @template TK * @template TV @@ -22,11 +20,9 @@ final class TapOperation extends AbstractOperation */ public function __invoke(callable $f): Generator { - return asGenerator(function () use ($f) { - foreach ($this->gen as $key => $value) { - $f($key, $value); - yield $key => $value; - } - }); + foreach ($this->gen as $key => $value) { + $f($key, $value); + yield $key => $value; + } } } diff --git a/src/Fp/Operations/ToStringOperation.php b/src/Fp/Operations/ToStringOperation.php index 190e5d15..ac72cbd7 100644 --- a/src/Fp/Operations/ToStringOperation.php +++ b/src/Fp/Operations/ToStringOperation.php @@ -6,18 +6,22 @@ use Stringable; use Throwable; + use function Fp\Collection\map; use function Fp\Collection\mapKV; +use function Fp\Util\jsonEncode; final class ToStringOperation { public static function of(mixed $value): string { if ($value instanceof Throwable) { - $message = $value->getMessage(); + $message = self::of($value->getMessage()); $exClass = $value::class; - return empty($message) ? "{$exClass}()" : "{$exClass}('{$message}')"; + return $message !== '""' + ? "{$exClass}({$message})" + : "{$exClass}()"; } if ($value instanceof Stringable) { @@ -25,10 +29,8 @@ public static function of(mixed $value): string } return match (get_debug_type($value)) { - 'null' => 'null', - 'int', 'float' => "{$value}", - 'bool' => $value ? 'true' : 'false', - 'string' => "'" . str_replace("'", "\'", $value) . "'", + 'null', 'int', 'bool', 'string', => jsonEncode($value), + 'float' => $value - ((int) $value) === 0.0 ? "{$value}.00" : "{$value}", 'array' => self::arrayToStr($value), default => get_debug_type($value), }; diff --git a/src/Fp/Operations/TraverseEitherAccOperation.php b/src/Fp/Operations/TraverseEitherAccOperation.php deleted file mode 100644 index 6f9281a1..00000000 --- a/src/Fp/Operations/TraverseEitherAccOperation.php +++ /dev/null @@ -1,64 +0,0 @@ - - */ -final class TraverseEitherAccOperation extends AbstractOperation -{ - /** - * @template E - * @template TVO - * - * @param callable(TK, TV): Either $f - * @return Either, Generator> - */ - public function __invoke(callable $f): Either - { - /** @psalm-var HashTable $rights */ - $rights = new HashTable(); - - /** @psalm-var HashTable $lefts */ - $lefts = new HashTable(); - - foreach ($this->gen as $key => $value) { - $mapped = $f($key, $value); - - if ($mapped->isLeft()) { - $lefts->update($key, $mapped->get()); - } else { - $rights->update($key, $mapped->get()); - } - } - - return !$lefts->isEmpty() - ? Either::left($lefts->getKeyValueIterator()) - : Either::right($rights->getKeyValueIterator()); - } - - /** - * @template E - * @template TKI - * @template TVI - * - * @param iterable | Closure(): Either> $collection - * @return Either, Generator> - */ - public static function id(iterable $collection): Either - { - return self::of($collection)( - fn(mixed $_key, Either|Closure $i): Either => $i instanceof Closure ? $i() : $i - ); - } -} diff --git a/src/Fp/Operations/ValuesOperation.php b/src/Fp/Operations/ValuesOperation.php index 912cc683..692f926e 100644 --- a/src/Fp/Operations/ValuesOperation.php +++ b/src/Fp/Operations/ValuesOperation.php @@ -6,8 +6,6 @@ use Generator; -use function Fp\Cast\asGenerator; - /** * @template TK * @template TV @@ -21,10 +19,8 @@ final class ValuesOperation extends AbstractOperation */ public function __invoke(): Generator { - return asGenerator(function () { - foreach ($this->gen as $value) { - yield $value; - } - }); + foreach ($this->gen as $value) { + yield $value; + } } } diff --git a/src/Fp/Operations/ZipOperation.php b/src/Fp/Operations/ZipOperation.php index 624ae4ac..f359c93c 100644 --- a/src/Fp/Operations/ZipOperation.php +++ b/src/Fp/Operations/ZipOperation.php @@ -24,22 +24,20 @@ final class ZipOperation extends AbstractOperation */ public function __invoke(iterable $that): Generator { - return asGenerator(function () use ($that) { - $thisIter = $this->gen; - $thatIter = asGenerator(fn() => $that); + $thisIter = $this->gen; + $thatIter = asGenerator(fn() => $that); - $thisIter->rewind(); - $thatIter->rewind(); + $thisIter->rewind(); + $thatIter->rewind(); - while ($thisIter->valid() && $thatIter->valid()) { - $thisElem = $thisIter->current(); - $thatElem = $thatIter->current(); + while ($thisIter->valid() && $thatIter->valid()) { + $thisElem = $thisIter->current(); + $thatElem = $thatIter->current(); - yield [$thisElem, $thatElem]; + yield [$thisElem, $thatElem]; - $thisIter->next(); - $thatIter->next(); - } - }); + $thisIter->next(); + $thatIter->next(); + } } } diff --git a/src/Fp/Psalm/FunctionalPlugin.php b/src/Fp/Psalm/FunctionalPlugin.php deleted file mode 100644 index f31f9be5..00000000 --- a/src/Fp/Psalm/FunctionalPlugin.php +++ /dev/null @@ -1,69 +0,0 @@ -registerHooksFromClass($hook); - } - }; - - $register(PartialFunctionReturnTypeProvider::class); - $register(PartitionFunctionReturnTypeProvider::class); - $register(PluckFunctionReturnTypeProvider::class); - - $register(FilterFunctionReturnTypeProvider::class); - $register(CollectionFilterMethodReturnTypeProvider::class); - $register(OptionFilterMethodReturnTypeProvider::class); - $register(EitherFilterOrElseMethodReturnTypeProvider::class); - - $register(ProveTrueExpressionAnalyzer::class); - $register(EitherGetReturnTypeProvider::class); - $register(OptionGetReturnTypeProvider::class); - - $register(SequenceOptionFunctionReturnTypeProvider::class); - $register(SequenceEitherFunctionReturnTypeProvider::class); - $register(SequenceEitherAccFunctionReturnTypeProvider::class); - - $register(FilterNotNullFunctionReturnTypeProvider::class); - $register(FoldFunctionReturnTypeProvider::class); - $register(FoldMethodReturnTypeProvider::class); - $register(MapTapNMethodReturnTypeProvider::class); - $register(PluckMethodReturnTypeProvider::class); - $register(CtorFunctionReturnTypeProvider::class); - $register(SeparatedToEitherMethodReturnTypeProvider::class); - } -} diff --git a/src/Fp/Psalm/Hook/AfterExpressionAnalysis/ProveTrueExpressionAnalyzer.php b/src/Fp/Psalm/Hook/AfterExpressionAnalysis/ProveTrueExpressionAnalyzer.php deleted file mode 100644 index 6304f323..00000000 --- a/src/Fp/Psalm/Hook/AfterExpressionAnalysis/ProveTrueExpressionAnalyzer.php +++ /dev/null @@ -1,95 +0,0 @@ - Option::some($event->getExpr()) - ->flatMap(of(Yield_::class)) - ->flatMap(self::getProveTrueArgsFromYield(...)), - fn() => Option::some($event->getStatementsSource()) - ->flatMap(of(StatementsAnalyzer::class)) - ->filter(fn(StatementsAnalyzer $source) => $source->getSource() instanceof ClosureAnalyzer), - fn() => Option::some($event), - )->tapN(self::assert(...)); - - return null; - } - - /** - * @param Node\Arg[] $prove_true_args - */ - private static function assert( - array $prove_true_args, - StatementsAnalyzer $analyzer, - AfterExpressionAnalysisEvent $event, - ): void { - FunctionCallAnalyzer::analyze( - $analyzer, - new VirtualFuncCall( - new VirtualFullyQualified('assert'), - $prove_true_args, - ), - $event->getContext(), - ); - } - - /** - * @psalm-return Option - */ - private static function getProveTrueArgsFromYield(Yield_ $expr): Option - { - $visitor = new class extends NodeVisitorAbstract { - /** @var Node\Arg[] */ - public array $proveTrueArgs = []; - - public function leaveNode(Node $node): ?int - { - $this->proveTrueArgs = Option::some($node) - ->flatMap(of(FuncCall::class)) - ->filter(fn(FuncCall $n) => 'Fp\Evidence\proveTrue' === $n->name->getAttribute('resolvedName')) - ->filter(fn(FuncCall $n) => !$n->isFirstClassCallable()) - ->map(fn(FuncCall $n) => $n->getArgs()) - ->getOrElse([]); - - return !empty($this->proveTrueArgs) - ? NodeTraverser::STOP_TRAVERSAL - : null; - } - }; - - $traverser = new NodeTraverser(); - $traverser->addVisitor($visitor); - $traverser->traverse([$expr]); - - return proveNonEmptyArray($visitor->proveTrueArgs); - } -} diff --git a/src/Fp/Psalm/Hook/FunctionReturnTypeProvider/CtorFunctionReturnTypeProvider.php b/src/Fp/Psalm/Hook/FunctionReturnTypeProvider/CtorFunctionReturnTypeProvider.php deleted file mode 100644 index 76271e25..00000000 --- a/src/Fp/Psalm/Hook/FunctionReturnTypeProvider/CtorFunctionReturnTypeProvider.php +++ /dev/null @@ -1,51 +0,0 @@ -getCallArgs($event) - ->flatMap(fn(ArrayList $args) => $args->head()) - ->pluck('type') - ->flatMap(PsalmApi::$types->asSingleAtomic(...)) - ->flatMap(of(TLiteralClassString::class)) - ->pluck('value') - ->map(ctor(TNamedObject::class)) - ->map(fn(TNamedObject $class) => [ - new TClosure( - value: 'Closure', - params: PsalmApi::$classlikes->getStorage($class) - ->flatMap(fn(ClassLikeStorage $storage) => at($storage->methods, '__construct')) - ->map(fn(MethodStorage $method) => $method->params) - ->getOrElse([]), - return_type: new Union([$class]), - ) - ]) - ->map(ctor(Union::class)) - ->get(); - } -} diff --git a/src/Fp/Psalm/Hook/FunctionReturnTypeProvider/FilterFunctionReturnTypeProvider.php b/src/Fp/Psalm/Hook/FunctionReturnTypeProvider/FilterFunctionReturnTypeProvider.php deleted file mode 100644 index be1d20e0..00000000 --- a/src/Fp/Psalm/Hook/FunctionReturnTypeProvider/FilterFunctionReturnTypeProvider.php +++ /dev/null @@ -1,102 +0,0 @@ -getCallArgs($event) - ->flatMap(fn(ArrayList $args) => sequenceOptionT( - fn() => Option::some($event->getFunctionId() === 'fp\collection\filterkv' - ? RefineForEnum::KeyValue - : RefineForEnum::Value), - fn() => PredicateExtractor::extract($event), - fn() => Option::some($event->getContext()), - fn() => proveOf($event->getStatementsSource(), StatementsAnalyzer::class), - fn() => $args->firstMap(fn(CallArg $arg) => GetCollectionTypeParams::keyValue($arg->type)), - )) - ->mapN(ctor(RefinementContext::class)) - ->map(RefineByPredicate::for(...)) - ->map(fn(CollectionTypeParams $result) => self::getReturnType($event, $result)) - ->get(); - } - - private static function getReturnType(FunctionReturnTypeProviderEvent $event, CollectionTypeParams $result): Union - { - if (self::isAccessorFunction($event->getFunctionId())) { - return self::optionType($result); - } - - return first($event->getCallArgs()) - ->flatMap(fn(Arg $preserve_keys) => PsalmApi::$args->getArgType($event, $preserve_keys)) - ->flatMap(PsalmApi::$types->asSingleAtomic(...)) - ->map(fn($atomic) => $atomic::class === TList::class || $atomic::class === TNonEmptyList::class - ? self::listType($result) - : self::arrayType($result)) - ->getOrCall(fn() => self::listType($result)); - } - - private static function isAccessorFunction(string $id): bool - { - return $id === strtolower('Fp\Collection\last') || $id === strtolower('Fp\Collection\first'); - } - - private static function arrayType(CollectionTypeParams $result): Union - { - return new Union([ - new TArray([$result->key_type, $result->val_type]), - ]); - } - - private static function listType(CollectionTypeParams $result): Union - { - return new Union([ - new TList($result->val_type), - ]); - } - - private static function optionType(CollectionTypeParams $result): Union - { - return new Union([ - new TGenericObject(Option::class, [$result->val_type]), - ]); - } -} diff --git a/src/Fp/Psalm/Hook/FunctionReturnTypeProvider/FilterNotNullFunctionReturnTypeProvider.php b/src/Fp/Psalm/Hook/FunctionReturnTypeProvider/FilterNotNullFunctionReturnTypeProvider.php deleted file mode 100644 index 50abc652..00000000 --- a/src/Fp/Psalm/Hook/FunctionReturnTypeProvider/FilterNotNullFunctionReturnTypeProvider.php +++ /dev/null @@ -1,49 +0,0 @@ -getFirstCallArgType($event) - ->flatMap(PsalmApi::$types->asSingleAtomic(...)) - ->flatMap(of(TKeyedArray::class)) - ->filter(fn(TKeyedArray $keyed) => !$keyed->is_list) - ->map(fn(TKeyedArray $keyed) => NonEmptyHashMap::collectNonEmpty($keyed->properties) - ->map(function(Union $property) { - if (!$property->isNullable()) { - return $property; - } - - $possibly_undefined = clone $property; - $possibly_undefined->removeType('null'); - $possibly_undefined->possibly_undefined = true; - - return $possibly_undefined; - }) - ->toNonEmptyArray()) - ->map(ctor(TKeyedArray::class)) - ->map(fn($keyed) => new Union([$keyed])) - ->get(); - } -} diff --git a/src/Fp/Psalm/Hook/FunctionReturnTypeProvider/FoldFunctionReturnTypeProvider.php b/src/Fp/Psalm/Hook/FunctionReturnTypeProvider/FoldFunctionReturnTypeProvider.php deleted file mode 100644 index 0f58a1dc..00000000 --- a/src/Fp/Psalm/Hook/FunctionReturnTypeProvider/FoldFunctionReturnTypeProvider.php +++ /dev/null @@ -1,46 +0,0 @@ -flatMap(fn() => sequenceOptionT( - fn() => PsalmApi::$args->getCallArgs($event) - ->flatMap(fn(ArrayList $args) => $args->lastElement()) - ->pluck('type') - ->flatMap(GetCollectionTypeParams::value(...)), - fn() => PsalmApi::$args->getCallArgs($event) - ->flatMap(fn(ArrayList $args) => $args->firstElement()) - ->pluck('type') - ->map(PsalmApi::$types->asNonLiteralType(...)), - )) - ->mapN(fn(Union $A, Union $TInit) => [ - new TGenericObject(FoldOperation::class, [$A, $TInit]), - ]) - ->map(ctor(Union::class)) - ->get(); - } -} diff --git a/src/Fp/Psalm/Hook/FunctionReturnTypeProvider/PartialFunctionReturnTypeProvider.php b/src/Fp/Psalm/Hook/FunctionReturnTypeProvider/PartialFunctionReturnTypeProvider.php deleted file mode 100644 index f9d0354a..00000000 --- a/src/Fp/Psalm/Hook/FunctionReturnTypeProvider/PartialFunctionReturnTypeProvider.php +++ /dev/null @@ -1,141 +0,0 @@ -getCallArgs()) - ->flatMap(fn(Arg $head_arg) => PsalmApi::$args->getArgType($event, $head_arg)) - ->flatMap(fn(Union $head_arg_type) => head(array_merge( - $head_arg_type->getClosureTypes(), - $head_arg_type->getCallableTypes(), - filterNotNull(map( - collection: asList($head_arg_type->getAtomicTypes()), - callback: fn(Atomic $atomic) => CallableTypeComparator::getCallableFromAtomic( - codebase: $event->getStatementsSource()->getCodebase(), - input_type_part: $atomic - ) - )) - ))) - ->map(function (TClosure|TCallable $closure_type) use ($event) { - $is_partial_right = str_ends_with($event->getFunctionId(), 'right'); - $closure_type_copy = clone $closure_type; - $closure_params = $closure_type_copy->params ?? []; - $tail_args = tail($event->getCallArgs()); - - self::assertValidArgs( - event: $event, - callable: $closure_type_copy, - args: $tail_args, - is_partial_right: $is_partial_right - ); - - $args_tail_size = count($tail_args); - - if (0 === $args_tail_size) { - return new Union([$closure_type_copy]); - } - - $free_params = $is_partial_right - ? array_slice($closure_params, 0, -$args_tail_size) - : array_slice($closure_params, $args_tail_size); - - $closure_type_copy->params = $free_params; - - return new Union([$closure_type_copy]); - }) - ->get(); - } - - /** - * @psalm-param array $args - */ - private static function assertValidArgs( - FunctionReturnTypeProviderEvent $event, - TClosure|TCallable $callable, - array $args, - bool $is_partial_right - ): void - { - $source = $event->getStatementsSource(); - $codebase = $source->getCodebase(); - - $args_list = array_values($args); - $params_list = $is_partial_right - ? array_reverse($callable->params ?? []) - : $callable->params ?? []; - - for ($i = 0; $i < count($args); $i++) { - $arg = $args_list[$i] ?? null; - $param = $params_list[$i] ?? null; - - if (!isset($arg, $param)) { - continue; - } - - $param_type = $param->type ?? Type::getMixed(); - $arg_type = PsalmApi::$args->getArgType($event, $arg); - - if ($arg_type->isNone()) { - continue; - } - - $is_subtype_of = $codebase->isTypeContainedByType($arg_type->get(), $param_type); - - if ($is_subtype_of) { - continue; - } - - self::issueInvalidArgument( - function_id: $event->getFunctionId(), - code_location: $event->getCodeLocation(), - expected_type: (string) $param_type - ); - } - } - - - private static function issueInvalidArgument(string $function_id, CodeLocation $code_location, string $expected_type): void - { - $issue = new InvalidArgument( - message: sprintf('argument should be of type %s', $expected_type), - code_location: $code_location, - function_id: $function_id - ); - - IssueBuffer::accepts($issue); - } -} diff --git a/src/Fp/Psalm/Hook/FunctionReturnTypeProvider/PartitionFunctionReturnTypeProvider.php b/src/Fp/Psalm/Hook/FunctionReturnTypeProvider/PartitionFunctionReturnTypeProvider.php deleted file mode 100644 index b891bbe7..00000000 --- a/src/Fp/Psalm/Hook/FunctionReturnTypeProvider/PartitionFunctionReturnTypeProvider.php +++ /dev/null @@ -1,92 +0,0 @@ -getCallArgs(); - $partition_count = count(tail($args)); - - return head($args) - ->flatMap(fn(Arg $head_arg) => PsalmApi::$args->getArgType($event, $head_arg)) - ->flatMap(GetCollectionTypeParams::keyValue(...)) - ->flatMap( - fn(CollectionTypeParams $type_params) => ArrayList::range(1, $partition_count + 1) - ->traverseOption(fn(int $offset) => sequenceOptionT( - fn() => Option::some(RefineForEnum::Value), - fn() => at($args, $offset)->pluck('value')->flatMap(of(FunctionLike::class)), - fn() => Option::some($event->getContext()), - fn() => proveOf($event->getStatementsSource(), StatementsAnalyzer::class), - fn() => Option::some($type_params), - )) - ->map(function(ArrayList $args) use ($type_params) { - $init_types = $args - ->mapN(ctor(RefinementContext::class)) - ->map(RefineByPredicate::for(...)) - ->map(fn(CollectionTypeParams $params) => $params->val_type); - - $last_type = $init_types - ->fold($type_params->val_type)( - function(Union $last, Union $current) { - $cloned = clone $last; - $cloned->removeType($current->getId()); - - return $cloned; - }, - ); - - return $init_types - ->appended($last_type) - ->map(fn(Union $type) => [new TList($type)]) - ->map(ctor(Union::class)) - ->toList(); - }) - ) - ->flatMap(fn(array $partitions) => asNonEmptyArray($partitions)) - ->map(function(array $non_empty_partitions) { - $tuple = new TKeyedArray($non_empty_partitions); - $tuple->is_list = true; - $tuple->sealed = true; - - return new Union([$tuple]); - }) - ->get(); - } -} diff --git a/src/Fp/Psalm/Hook/FunctionReturnTypeProvider/PluckFunctionReturnTypeProvider.php b/src/Fp/Psalm/Hook/FunctionReturnTypeProvider/PluckFunctionReturnTypeProvider.php deleted file mode 100644 index 7e133b63..00000000 --- a/src/Fp/Psalm/Hook/FunctionReturnTypeProvider/PluckFunctionReturnTypeProvider.php +++ /dev/null @@ -1,85 +0,0 @@ -getCallArgs($event) - ->flatMap(fn(ArrayList $args) => sequenceOptionT( - fn() => $args->lastElement() - ->pluck('type') - ->flatMap(PsalmApi::$types->asSingleAtomic(...)) - ->flatMap(of(TLiteralString::class)), - fn() => $args->firstElement() - ->pluck('type') - ->flatMap(GetCollectionTypeParams::value(...)) - ->flatMap(PsalmApi::$types->asSingleAtomic(...)) - ->flatMap(fn($atomic) => proveOf($atomic, [TNamedObject::class, TKeyedArray::class])), - fn() => Option::some($event->getStatementsSource()), - fn() => Option::some($event->getCodeLocation()), - )) - ->mapN(ctor(PluckResolveContext::class)) - ->flatMap(PluckPropertyTypeResolver::resolve(...)) - ->map(fn(Union $result) => [ - match (true) { - self::itWas(TNonEmptyList::class, $event) => new TNonEmptyList($result), - self::itWas(TList::class, $event) => new TList($result), - self::itWas(TNonEmptyArray::class, $event) => new TNonEmptyArray([self::getArrayKey($event), $result]), - default => new TArray([self::getArrayKey($event), $result]), - }, - ]) - ->map(ctor(Union::class)) - ->get(); - } - - private static function getArrayKey(FunctionReturnTypeProviderEvent $event): Union - { - return PsalmApi::$args->getCallArgs($event) - ->flatMap(fn(ArrayList $args) => $args->head()) - ->pluck('type') - ->flatMap(GetCollectionTypeParams::key(...)) - ->getOrCall(fn() => Type::getArrayKey()); - } - - private static function itWas(string $class, FunctionReturnTypeProviderEvent $event): bool - { - return PsalmApi::$args->getCallArgs($event) - ->flatMap(fn(ArrayList $args) => $args->head()) - ->pluck('type') - ->flatMap(PsalmApi::$types->asSingleAtomic(...)) - ->map(fn(Type\Atomic $atomic) => $atomic instanceof $class) - ->getOrElse(false); - } -} diff --git a/src/Fp/Psalm/Hook/FunctionReturnTypeProvider/SequenceEitherAccFunctionReturnTypeProvider.php b/src/Fp/Psalm/Hook/FunctionReturnTypeProvider/SequenceEitherAccFunctionReturnTypeProvider.php deleted file mode 100644 index 84fd6ba3..00000000 --- a/src/Fp/Psalm/Hook/FunctionReturnTypeProvider/SequenceEitherAccFunctionReturnTypeProvider.php +++ /dev/null @@ -1,70 +0,0 @@ -getFirstCallArgType($event) - ->flatMap(PsalmApi::$types->asSingleAtomic(...)) - ->flatMap(of(TKeyedArray::class)) - ->flatMap(fn(TKeyedArray $either_shape) => sequenceOptionT( - self::mapEither($either_shape, GetEitherTypeParam::GET_LEFT), - self::mapEither($either_shape, GetEitherTypeParam::GET_RIGHT), - )) - ->map(fn(array $type_params) => [ - new TGenericObject(Either::class, $type_params), - ]) - ->map(ctor(Union::class)) - ->get(); - } - - /** - * @param GetEitherTypeParam::GET_* $idx - * @return Option - */ - private static function mapEither(TKeyedArray $either_shape, int $idx): Option - { - return NonEmptyHashMap::collectNonEmpty($either_shape->properties) - ->traverseOption(fn($property_type) => GetEitherTypeParam::from($property_type, $idx)) - ->map(fn(NonEmptyHashMap $properties) => $properties->map( - fn(Union $property_type) => $idx === GetEitherTypeParam::GET_LEFT - ? PsalmApi::$types->asPossiblyUndefined($property_type) - : $property_type - )) - ->map(function(NonEmptyHashMap $properties) use ($idx) { - $is_list = $properties->keys()->every(is_int(...)); - - $keyed = new TKeyedArray($properties->toNonEmptyArray()); - $keyed->is_list = $is_list; - $keyed->sealed = $is_list; - - return new Union([$keyed]); - }); - } -} diff --git a/src/Fp/Psalm/Hook/FunctionReturnTypeProvider/SequenceEitherFunctionReturnTypeProvider.php b/src/Fp/Psalm/Hook/FunctionReturnTypeProvider/SequenceEitherFunctionReturnTypeProvider.php deleted file mode 100644 index f3026c03..00000000 --- a/src/Fp/Psalm/Hook/FunctionReturnTypeProvider/SequenceEitherFunctionReturnTypeProvider.php +++ /dev/null @@ -1,102 +0,0 @@ -orElse(fn() => self::getInputTypeFromSequenceEitherT($event)) - ->flatMap(fn(TKeyedArray $types) => sequenceOptionT( - fn() => NonEmptyHashMap::collectNonEmpty($types->properties) - ->traverseOption(GetEitherTypeParam::left(...)) - ->map(fn(NonEmptyHashMap $props) => $props->values()->toNonEmptyList()) - ->map(fn(array $left_cases) => Type::combineUnionTypeArray($left_cases, PsalmApi::$codebase)), - fn() => NonEmptyHashMap::collectNonEmpty($types->properties) - ->traverseOption(GetEitherTypeParam::right(...)) - ->map(function(NonEmptyHashMap $props) { - $is_list = $props->keys()->every(is_int(...)); - - $keyed = new TKeyedArray($props->toNonEmptyArray()); - $keyed->is_list = $is_list; - $keyed->sealed = $is_list; - - return new Union([$keyed]); - }), - )) - ->map(fn(array $type_params) => [ - new TGenericObject(Either::class, $type_params), - ]) - ->map(ctor(Union::class)) - ->get(); - } - - /** - * @return Option - */ - private static function getInputTypeFromSequenceEither(FunctionReturnTypeProviderEvent $event): Option - { - $isSequenceEither = contains($event->getFunctionId(), [ - strtolower('Fp\Collection\sequenceEither'), - strtolower('Fp\Collection\sequenceEitherMerged'), - ]); - - return proveTrue($isSequenceEither) - ->flatMap(fn() => PsalmApi::$args->getCallArgs($event)) - ->flatMap(fn($args) => $args->head()) - ->flatMap(fn(CallArg $arg) => PsalmApi::$types->asSingleAtomic($arg->type)) - ->flatMap(of(TKeyedArray::class)); - } - - /** - * @return Option - */ - private static function getInputTypeFromSequenceEitherT(FunctionReturnTypeProviderEvent $event): Option - { - $isSequenceEitherT = contains($event->getFunctionId(), [ - strtolower('Fp\Collection\sequenceEitherT'), - strtolower('Fp\Collection\sequenceEitherMergedT'), - ]); - - return proveTrue($isSequenceEitherT) - ->flatMap(fn() => PsalmApi::$args->getCallArgs($event)) - ->flatMap(fn(ArrayList $args) => $args->toNonEmptyArrayList()) - ->map(fn(NonEmptyArrayList $args) => new TKeyedArray( - $args->map(fn(CallArg $arg) => $arg->type)->toNonEmptyList(), - )); - } -} diff --git a/src/Fp/Psalm/Hook/FunctionReturnTypeProvider/SequenceOptionFunctionReturnTypeProvider.php b/src/Fp/Psalm/Hook/FunctionReturnTypeProvider/SequenceOptionFunctionReturnTypeProvider.php deleted file mode 100644 index 9ecc5ccc..00000000 --- a/src/Fp/Psalm/Hook/FunctionReturnTypeProvider/SequenceOptionFunctionReturnTypeProvider.php +++ /dev/null @@ -1,97 +0,0 @@ -orElse(fn() => self::getInputTypeFromSequenceOptionT($event)) - ->flatMap(fn(TKeyedArray $types) => traverseOption($types->properties, self::getOptionTypeParam(...))) - ->map(function(array $mapped) { - $is_list = array_is_list($mapped); - - $keyed = new TKeyedArray($mapped); - $keyed->is_list = $is_list; - $keyed->sealed = $is_list; - - return new Union([ - new TGenericObject(Option::class, [ - new Union([$keyed]), - ]), - ]); - }) - ->get(); - } - - /** - * @return Option - */ - private static function getInputTypeFromSequenceOption(FunctionReturnTypeProviderEvent $event): Option - { - return proveTrue(strtolower('Fp\Collection\sequenceOption') === $event->getFunctionId()) - ->flatMap(fn() => PsalmApi::$args->getCallArgs($event)) - ->flatMap(fn(ArrayList $args) => $args->head()) - ->map(fn(CallArg $arg) => $arg->type) - ->flatMap(PsalmApi::$types->asSingleAtomic(...)) - ->flatMap(of(TKeyedArray::class)); - } - - /** - * @return Option - */ - private static function getInputTypeFromSequenceOptionT(FunctionReturnTypeProviderEvent $event): Option - { - return proveTrue(strtolower('Fp\Collection\sequenceOptionT') === $event->getFunctionId()) - ->flatMap(fn() => PsalmApi::$args->getCallArgs($event)) - ->flatMap(fn(ArrayList $args) => $args->toNonEmptyArrayList()) - ->map(fn(NonEmptyArrayList $args) => new TKeyedArray( - $args->map(fn(CallArg $arg) => $arg->type)->toNonEmptyList(), - )); - } - - /** - * @return Option - */ - private static function getOptionTypeParam(Union $type): Option - { - return PsalmApi::$types->asSingleAtomic($type) - ->flatMap(fn(Atomic $atomic) => match (true) { - $atomic instanceof TGenericObject => Option::some($atomic) - ->filter(fn(TGenericObject $generic) => $generic->value === Option::class) - ->flatMap(fn(TGenericObject $option) => first($option->type_params)), - $atomic instanceof TClosure => Option::fromNullable($atomic->return_type) - ->flatMap(self::getOptionTypeParam(...)), - default => Option::none(), - }); - } -} diff --git a/src/Fp/Psalm/Hook/MethodReturnTypeProvider/CollectionFilterMethodReturnTypeProvider.php b/src/Fp/Psalm/Hook/MethodReturnTypeProvider/CollectionFilterMethodReturnTypeProvider.php deleted file mode 100644 index 1e5ba834..00000000 --- a/src/Fp/Psalm/Hook/MethodReturnTypeProvider/CollectionFilterMethodReturnTypeProvider.php +++ /dev/null @@ -1,105 +0,0 @@ -getMethodNameLowercase(), self::getMethodNames())) - ->flatMap(fn() => sequenceOptionT( - fn() => Option::some($event->getMethodNameLowercase() === strtolower('filterKV') - ? RefineForEnum::KeyValue - : RefineForEnum::Value), - fn() => PredicateExtractor::extract($event), - fn() => Option::some($event->getContext()), - fn() => proveOf($event->getSource(), StatementsAnalyzer::class), - fn() => Option::fromNullable($event->getTemplateTypeParameters()) - ->map(fn($template_params) => 2 === count($template_params) - ? new CollectionTypeParams($template_params[0], $template_params[1]) - : new CollectionTypeParams(Type::getArrayKey(), $template_params[0])), - )) - ->mapN(ctor(RefinementContext::class)) - ->map(RefineByPredicate::for(...)) - ->map(fn(CollectionTypeParams $result) => self::getReturnType($event, $result)) - ->get(); - } - - private static function getReturnType(MethodReturnTypeProviderEvent $event, CollectionTypeParams $result): Union - { - $class_name = str_replace('NonEmpty', '', $event->getFqClasslikeName()); - - $template_params = 2 === count($event->getTemplateTypeParameters() ?? []) - ? [$result->key_type, $result->val_type] - : [$result->val_type]; - - return new Union([ - new TGenericObject($class_name, $template_params), - ]); - } -} diff --git a/src/Fp/Psalm/Hook/MethodReturnTypeProvider/EitherFilterOrElseMethodReturnTypeProvider.php b/src/Fp/Psalm/Hook/MethodReturnTypeProvider/EitherFilterOrElseMethodReturnTypeProvider.php deleted file mode 100644 index 82f506b5..00000000 --- a/src/Fp/Psalm/Hook/MethodReturnTypeProvider/EitherFilterOrElseMethodReturnTypeProvider.php +++ /dev/null @@ -1,75 +0,0 @@ -getMethodNameLowercase()) - ->flatMap(fn() => sequenceOptionT( - fn() => Option::some(RefineForEnum::Value), - fn() => PredicateExtractor::extract($event), - fn() => Option::some($event->getContext()), - fn() => proveOf($event->getSource(), StatementsAnalyzer::class), - fn() => second($event->getTemplateTypeParameters() ?? []) - ->map(fn($value_type) => [Type::getArrayKey(), $value_type]) - ->mapN(ctor(CollectionTypeParams::class)), - )) - ->mapN(ctor(RefinementContext::class)) - ->map(RefineByPredicate::for(...)) - ->map(fn(CollectionTypeParams $result) => new TGenericObject(Either::class, [ - self::getOutLeft($event), - $result->val_type, - ])) - ->map(fn(TGenericObject $result) => new Union([$result])) - ->get(); - } - - private static function getOutLeft(MethodReturnTypeProviderEvent $event): Union - { - return Type::combineUnionTypes( - first($event->getTemplateTypeParameters() ?? []) - ->getOrCall(fn() => Type::getNever()), - second($event->getCallArgs()) - ->flatMap(fn(Arg $arg) => PsalmApi::$args->getArgType($event, $arg)) - ->flatMap(PsalmApi::$types->asSingleAtomic(...)) - ->flatMap(of([TClosure::class, TCallable::class])) - ->flatMap(fn(TClosure|TCallable $func) => Option::fromNullable($func->return_type)) - ->getOrCall(fn() => Type::getNever()), - ); - } -} diff --git a/src/Fp/Psalm/Hook/MethodReturnTypeProvider/EitherGetReturnTypeProvider.php b/src/Fp/Psalm/Hook/MethodReturnTypeProvider/EitherGetReturnTypeProvider.php deleted file mode 100644 index eca17587..00000000 --- a/src/Fp/Psalm/Hook/MethodReturnTypeProvider/EitherGetReturnTypeProvider.php +++ /dev/null @@ -1,45 +0,0 @@ - match (true) { - str_starts_with($possibility, '!' . Right::class) => new Union([ - new TGenericObject(Left::class, [$either->type_params[0]]), - ]), - str_starts_with($possibility, '!' . Left::class) => new Union([ - new TGenericObject(Right::class, [$either->type_params[1]]), - ]), - default => new Union([$either]), - }, - to_return_type: fn(TGenericObject $generic) => match ($generic->value) { - Left::class, Right::class => first($generic->type_params), - default => Option::none(), - }, - ); - } -} diff --git a/src/Fp/Psalm/Hook/MethodReturnTypeProvider/FoldMethodReturnTypeProvider.php b/src/Fp/Psalm/Hook/MethodReturnTypeProvider/FoldMethodReturnTypeProvider.php deleted file mode 100644 index 683248f3..00000000 --- a/src/Fp/Psalm/Hook/MethodReturnTypeProvider/FoldMethodReturnTypeProvider.php +++ /dev/null @@ -1,211 +0,0 @@ -orElse(fn() => self::foldReturnType($event)) - ->get(); - } - - /** - * @return Option - */ - private static function foldReturnType(MethodReturnTypeProviderEvent $event): Option - { - // $list->fold(0)(fn($acc, $cur) => $acc + $cur) - // ^ ^ - // | | - // | | - // TInit TFold (return type of function) - return proveTrue(FoldOperation::class === $event->getFqClasslikeName()) - - // Get TInit and TFold - ->flatMap(fn() => sequenceOptionT( - fn() => second($event->getTemplateTypeParameters() ?? []), - fn() => proveOf($event->getStmt(), MethodCall::class) - ->map(fn(MethodCall $call) => $call->getArgs()) - ->flatMap(fn(array $args) => first($args)) - ->flatMap(fn(Arg $arg) => PsalmApi::$args->getArgType($event, $arg)) - ->flatMap(fn(Union $type) => PsalmApi::$types->asSingleAtomicOf(TClosure::class, $type)) - ->flatMap(fn(TClosure $closure) => Option::fromNullable($closure->return_type)) - ->map(fn(Union $type) => PsalmApi::$types->asNonLiteralType($type)) - ->flatMap(fn(Union $type) => PsalmApi::$types->asSingleAtomic($type)) - ->map(fn(Atomic $atomic) => new Union([$atomic])) - )) - - // TFold must be assignable to TInit - ->tapN(function(Union $TInit, Union $TFold) use ($event) { - // $fold = $integers->fold(ArrayList::empty()) === FoldingOperation> (second param is IInit) - // $fold(fn($list, $num) => $list->appended($num + 1)); - // ^ - // | - // | - // Type will be ArrayList (It is TFold) - // - // We must check that the TFold is assignable to the TInit. - // But ArrayList cannot be assigned to ArrayList because the never is supertype of the int. - // The neverToMixed swaps never type to mixed (Hope this won't be a problem) - if (PsalmApi::$types->isTypeContainedByType($TFold, self::neverToMixed($TInit))) { - return; - } - - $source = $event->getSource(); - - IssueBuffer::accepts( - e: new InvalidReturnStatement( - message: "The inferred type '{$TFold->getId()}' does not match the declared return type '{$TInit->getId()}'", - code_location: $event->getCodeLocation(), - ), - suppressed_issues: $source->getSuppressedIssues(), - ); - }) - - // Fold return type will be subsume of the TFold and the TInit - // when TInit = ArrayList - // and TFold = ArrayList - // then ArrayList | ArrayList = ArrayList - ->map(fn($types) => Type::combineUnionTypeArray($types, PsalmApi::$codebase)); - } - - /** - * @return Option - */ - private static function removeLiteralsFromTInit(MethodReturnTypeProviderEvent $event): Option - { - // $fold = $integers->fold(0) === FoldingOperation - // $fold(fn($sum, $num) => $sum + $num); - // ^ - // | - // | - // Type will be the int - // - // In the example above TInit is the int type, but TInit is the 0 (literal type) - // The int is not assignable to 0. - // It also extends to other literal types. - // Next code maps any literal types to non-literal analog. - // Then $integers->fold(0) will be FoldingOperation - return proveTrue('fold' === $event->getMethodNameLowercase()) - ->flatMap(fn() => sequenceOptionT( - fn() => last($event->getTemplateTypeParameters() ?? []), - fn() => proveOf($event->getStmt(), MethodCall::class) - ->flatMap(fn(MethodCall $call) => first($call->getArgs())) - ->flatMap(fn(Arg $arg) => PsalmApi::$args->getArgType($event, $arg)) - ->map(fn(Union $type) => PsalmApi::$types->asNonLiteralType($type)) - )) - ->mapN(fn(Union $A, Union $TInit) => [ - new TGenericObject(FoldOperation::class, [$A, $TInit]), - ]) - ->map(ctor(Union::class)); - } - - private static function neverToMixed(Union $type): Union - { - return new Union( - map(asList($type->getAtomicTypes()), fn(Atomic $a) => match (true) { - $a instanceof TKeyedArray => $a->is_list - ? new TNonEmptyList( - self::neverToMixed($a->getGenericValueType()), - ) - : new TNonEmptyArray([ - self::neverToMixed($a->getGenericKeyType()), - self::neverToMixed($a->getGenericValueType()), - ]), - $a instanceof TNonEmptyList => new TNonEmptyList( - self::neverToMixed($a->type_param), - ), - $a instanceof TList => new TList( - self::neverToMixed($a->type_param), - ), - $a instanceof TNonEmptyArray => new TNonEmptyArray([ - self::neverToMixed($a->type_params[0]), - self::neverToMixed($a->type_params[1]), - ]), - $a instanceof TArray => new TArray([ - self::neverToMixed($a->type_params[0]), - self::neverToMixed($a->type_params[1]), - ]), - $a instanceof TGenericObject => new TGenericObject( - $a->value, - map($a->type_params, fn(Union $t) => self::neverToMixed($t)), - ), - default => $a instanceof TNever || $a instanceof TEmpty ? new TMixed() : $a, - }), - ); - } -} diff --git a/src/Fp/Psalm/Hook/MethodReturnTypeProvider/MapTapNMethodReturnTypeProvider.php b/src/Fp/Psalm/Hook/MethodReturnTypeProvider/MapTapNMethodReturnTypeProvider.php deleted file mode 100644 index 801d28be..00000000 --- a/src/Fp/Psalm/Hook/MethodReturnTypeProvider/MapTapNMethodReturnTypeProvider.php +++ /dev/null @@ -1,245 +0,0 @@ -flatMap(fn() => proveNonEmptyList($event->getTemplateTypeParameters() ?? [])); - - // Allows call *N combinators in the origin class without type-check. - yield proveFalse(in_array($event->getContext()->self, self::getClassLikeNames())); - - // Take the most right template: - // Option -> A - // Either -> A - // Map -> A - $current_args = yield Option::firstT( - fn() => last($templates) - ->flatMap(PsalmApi::$types->asSingleAtomic(...)) - ->flatMap(of(TKeyedArray::class)) - ->filter(fn(TKeyedArray $keyed) => self::isTuple($keyed) || self::isAssoc($keyed)), - fn() => self::valueTypeIsNotValidKeyedArrayIssue($event), - ); - - $current_args_kind = self::isTuple($current_args) - ? MapTapNContextEnum::Tuple - : MapTapNContextEnum::Shape; - - // $callback mapN/tapN argument - $map_callback = yield PsalmApi::$args->getCallArgs($event) - ->flatMap(fn(ArrayList $args) => $args->firstElement()) - ->pluck('type') - ->flatMap(PsalmApi::$types->asSingleAtomic(...)) - ->flatMap(of([TCallable::class, TClosure::class])); - - // Input tuple type inferred by $callback argument - $func_args = yield Option::some($map_callback) - ->flatMap(fn(TCallable|TClosure $func) => HashMap::collect($func->params ?? []) - ->reindexKV(fn(int $idx, FunctionLikeParameter $param) => $current_args_kind === MapTapNContextEnum::Shape ? $param->name : $idx) - ->map(function(FunctionLikeParameter $param) use ($current_args_kind) { - $param_type = $param->type ?? Type::getMixed(); - - return $current_args_kind === MapTapNContextEnum::Shape && $param->is_optional - ? PsalmApi::$types->asPossiblyUndefined($param_type) - : $param_type; - }) - ->toNonEmptyArray()) - ->map(ctor(TKeyedArray::class)); - - $ctx = new MapTapNContext( - event: $event, - func_args: $func_args, - current_args: $current_args, - kind: $current_args_kind, - is_variadic: last($map_callback->params ?? []) - ->pluck('is_variadic') - ->getOrElse(false), - optional_count: ArrayList::collect($map_callback->params ?? []) - ->filter(fn(FunctionLikeParameter $p) => $p->is_optional) - ->count(), - required_count: ArrayList::collect($map_callback->params ?? []) - ->filter(fn(FunctionLikeParameter $p) => !$p->is_optional) - ->count(), - ); - - Option::firstT( - fn() => proveFalse($ctx->is_variadic && self::isAssoc($current_args)), - fn() => self::cannotSafelyCallShapeWithVariadicArg($ctx), - ); - - // Assert that $func_args is assignable to $current_args - Option::firstT( - fn() => proveTrue(self::isTypeContainedByType($ctx)), - fn() => self::typesAreNotCompatibleIssue($ctx), - ); - }); - - return null; - } - - private static function isSupportedMethod(MethodReturnTypeProviderEvent $event): bool - { - $methods = [ - 'mapN', - 'tapN', - 'flatMapN', - 'flatTapN', - 'reindexN', - 'filterN', - 'filterMapN', - 'everyN', - 'existsN', - 'traverseOptionN', - 'traverseEitherN', - 'partitionN', - 'partitionMapN', - 'firstN', - 'lastN', - ]; - - return exists($methods, fn($method) => $event->getMethodNameLowercase() === strtolower($method)); - } - - private static function isTuple(TKeyedArray $keyed): bool - { - return array_is_list($keyed->properties); - } - - private static function isAssoc(TKeyedArray $keyed): bool - { - return every(keys($keyed->properties), is_string(...)); - } - - /** - * @return Option - */ - private static function valueTypeIsNotValidKeyedArrayIssue(MethodReturnTypeProviderEvent $event): Option - { - $mappable_class = $event->getFqClasslikeName(); - $source = $event->getSource(); - - $issue = new IfThisIsMismatch( - message: "Value template of class {$mappable_class} must be tuple (all keys int from 0 to N) or shape (all keys is string)", - code_location: $event->getCodeLocation(), - ); - - IssueBuffer::accepts($issue, $source->getSuppressedIssues()); - return Option::none(); - } - - /** - * @return Option - */ - private static function cannotSafelyCallShapeWithVariadicArg(MapTapNContext $ctx): Option - { - $source = $ctx->event->getSource(); - - $issue = new IfThisIsMismatch( - message: 'Shape cannot safely passed to function with variadic parameter.', - code_location: $ctx->event->getCodeLocation(), - ); - - IssueBuffer::accepts($issue, $source->getSuppressedIssues()); - return Option::none(); - } - - /** - * @return Option - */ - private static function typesAreNotCompatibleIssue(MapTapNContext $ctx): Option - { - $mappable_class = $ctx->event->getFqClasslikeName(); - $source = $ctx->event->getSource(); - - $tuned_func_args = $ctx->kind->tuneForOptionalAndVariadicParams($ctx); - - $issue = new IfThisIsMismatch( - message: implode(', ', [ - "Object must be type of {$mappable_class}<{$tuned_func_args->getId()}>", - "actual type {$mappable_class}<{$ctx->current_args->getId()}>", - ]), - code_location: $ctx->event->getCodeLocation(), - ); - - IssueBuffer::accepts($issue, $source->getSuppressedIssues()); - return Option::none(); - } - - private static function isTypeContainedByType(MapTapNContext $context): bool - { - return PsalmApi::$types->isTypeContainedByType( - new Union([$context->current_args]), - new Union([ - $context->kind->tuneForOptionalAndVariadicParams($context), - ]), - ); - } -} diff --git a/src/Fp/Psalm/Hook/MethodReturnTypeProvider/OptionFilterMethodReturnTypeProvider.php b/src/Fp/Psalm/Hook/MethodReturnTypeProvider/OptionFilterMethodReturnTypeProvider.php deleted file mode 100644 index 82de7f94..00000000 --- a/src/Fp/Psalm/Hook/MethodReturnTypeProvider/OptionFilterMethodReturnTypeProvider.php +++ /dev/null @@ -1,52 +0,0 @@ -getMethodNameLowercase()) - ->flatMap(fn() => sequenceOptionT( - fn() => Option::some(RefineForEnum::Value), - fn() => PredicateExtractor::extract($event), - fn() => Option::some($event->getContext()), - fn() => proveOf($event->getSource(), StatementsAnalyzer::class), - fn() => first($event->getTemplateTypeParameters() ?? []) - ->map(fn($value_type) => [Type::getArrayKey(), $value_type]) - ->mapN(ctor(CollectionTypeParams::class)), - )) - ->mapN(ctor(RefinementContext::class)) - ->map(RefineByPredicate::for(...)) - ->map(fn(CollectionTypeParams $result) => new TGenericObject(Option::class, [$result->val_type])) - ->map(fn(TGenericObject $result) => new Union([$result])) - ->get(); - } -} diff --git a/src/Fp/Psalm/Hook/MethodReturnTypeProvider/OptionGetReturnTypeProvider.php b/src/Fp/Psalm/Hook/MethodReturnTypeProvider/OptionGetReturnTypeProvider.php deleted file mode 100644 index 2dc72a67..00000000 --- a/src/Fp/Psalm/Hook/MethodReturnTypeProvider/OptionGetReturnTypeProvider.php +++ /dev/null @@ -1,52 +0,0 @@ - match (true) { - str_starts_with($possibility, '!' . Some::class) => new Union([ - new TNamedObject(None::class), - ]), - str_starts_with($possibility, '!' . None::class) => new Union([ - new TGenericObject(Some::class, [$option->type_params[0]]), - ]), - default => new Union([$option]), - }, - to_return_type: fn(TGenericObject $generic) => match ($generic->value) { - Some::class => first($generic->type_params), - Option::class => first($generic->type_params)->map(function(Union $union) { - $nullable = clone $union; - $nullable->addType(new Type\Atomic\TNull()); - - return $nullable; - }), - default => Option::some(Type::getNull()) - }, - ); - } -} diff --git a/src/Fp/Psalm/Hook/MethodReturnTypeProvider/PluckMethodReturnTypeProvider.php b/src/Fp/Psalm/Hook/MethodReturnTypeProvider/PluckMethodReturnTypeProvider.php deleted file mode 100644 index e8b6f3df..00000000 --- a/src/Fp/Psalm/Hook/MethodReturnTypeProvider/PluckMethodReturnTypeProvider.php +++ /dev/null @@ -1,59 +0,0 @@ -getMethodNameLowercase()) - ->flatMap(fn() => sequenceOptionT( - fn() => PsalmApi::$args->getCallArgs($event) - ->flatMap(fn(ArrayList $args) => $args->lastElement() - ->pluck('type') - ->flatMap(PsalmApi::$types->asSingleAtomic(...)) - ->flatMap(of(TLiteralString::class))), - fn() => Option::fromNullable($event->getTemplateTypeParameters()) - ->flatMap(fn(array $templates) => last($templates)) - ->flatMap(PsalmApi::$types->asSingleAtomic(...)) - ->flatMap(fn(Atomic $atomic) => proveOf($atomic, [TNamedObject::class, TKeyedArray::class])), - fn() => Option::some($event->getSource()), - fn() => Option::some($event->getCodeLocation()), - )) - ->mapN(ctor(PluckResolveContext::class)) - ->flatMap(PluckPropertyTypeResolver::resolve(...)) - ->map(fn(Union $result) => [ - new TGenericObject(Option::class, [$result]), - ]) - ->map(ctor(Union::class)) - ->get(); - } -} diff --git a/src/Fp/Psalm/Hook/MethodReturnTypeProvider/SeparatedToEitherMethodReturnTypeProvider.php b/src/Fp/Psalm/Hook/MethodReturnTypeProvider/SeparatedToEitherMethodReturnTypeProvider.php deleted file mode 100644 index 6f356587..00000000 --- a/src/Fp/Psalm/Hook/MethodReturnTypeProvider/SeparatedToEitherMethodReturnTypeProvider.php +++ /dev/null @@ -1,33 +0,0 @@ -getMethodNameLowercase()) - ->filter(fn(string $method) => strtolower('toEither') === $method) - ->flatMap(fn() => proveNonEmptyList($event->getTemplateTypeParameters() ?? [])) - ->map(fn(array $templates) => new TGenericObject(Either::class, $templates)) - ->map(fn(TGenericObject $object) => new Union([$object])) - ->get(); - } -} diff --git a/src/Fp/Psalm/Util/GetCollectionTypeParams.php b/src/Fp/Psalm/Util/GetCollectionTypeParams.php deleted file mode 100644 index b2479252..00000000 --- a/src/Fp/Psalm/Util/GetCollectionTypeParams.php +++ /dev/null @@ -1,197 +0,0 @@ - - */ - public static function keyValue(Type\Union $union): Option - { - return GetCollectionTypeParams::value($union) - ->map(fn($val_type) => new CollectionTypeParams( - key_type: GetCollectionTypeParams::key($union)->getOrCall(fn() => Type::getArrayKey()), - val_type: $val_type - )); - } - - /** - * @return Option - */ - public static function key(Type\Union $union): Option - { - return PsalmApi::$types - ->asSingleAtomic($union) - ->flatMap(fn($atomic) => Option::firstT( - fn() => self::keyFromIterable($atomic), - fn() => self::keyFromGenerator($atomic), - fn() => self::keyFromArray($atomic), - fn() => self::keyFromGenericObject($atomic), - fn() => self::keyFromKeyedArray($atomic), - )); - } - - /** - * @return Option - */ - private static function keyFromIterable(Type\Atomic $atomic): Option - { - return Option::some($atomic) - ->flatMap(of(Type\Atomic\TIterable::class)) - ->flatMap(fn(Type\Atomic\TIterable $a) => first($a->type_params)); - } - - /** - * @return Option - */ - private static function keyFromGenerator(Type\Atomic $atomic): Option - { - return Option::some($atomic) - ->flatMap(of(Type\Atomic\TGenericObject::class)) - ->filter(fn(Type\Atomic\TGenericObject $generic) => $generic->value === Generator::class) - ->flatMap(fn(Type\Atomic\TGenericObject $generic) => first($generic->type_params)); - } - - /** - * @return Option - */ - private static function keyFromArray(Type\Atomic $atomic): Option - { - return Option::some($atomic) - ->flatMap(of(Type\Atomic\TArray::class)) - ->flatMap(fn(Type\Atomic\TArray $a) => first($a->type_params)); - } - - /** - * @return Option - */ - private static function keyFromKeyedArray(Type\Atomic $atomic): Option - { - return Option::some($atomic) - ->flatMap(of(Type\Atomic\TKeyedArray::class)) - ->map(fn(Type\Atomic\TKeyedArray $a) => $a->getGenericKeyType()); - } - - /** - * @return Option - */ - private static function keyFromGenericObject(Type\Atomic $atomic): Option - { - return Option::some($atomic) - ->flatMap(of(Type\Atomic\TGenericObject::class)) - ->flatMap( - fn(Type\Atomic\TGenericObject $a) => Option::some($a->value) - ->flatMap(classStringOf([Map::class, NonEmptyMap::class])) - ->flatMap(fn() => second($a->type_params)), - ); - } - - /** - * @return Option - */ - public static function value(Type\Union $union): Option - { - return PsalmApi::$types - ->asSingleAtomic($union) - ->flatMap(fn($atomic) => Option::firstT( - fn() => self::valueFromIterable($atomic), - fn() => self::valueFromGenerator($atomic), - fn() => self::valueFromArray($atomic), - fn() => self::valueFromList($atomic), - fn() => self::valueFromGenericObject($atomic), - fn() => self::valueFromKeyedArray($atomic), - )); - } - - /** - * @return Option - */ - private static function valueFromIterable(Type\Atomic $atomic): Option - { - return Option::some($atomic) - ->flatMap(of(Type\Atomic\TIterable::class)) - ->flatMap(fn(Type\Atomic\TIterable $a) => second($a->type_params)); - } - - /** - * @return Option - */ - private static function valueFromGenerator(Type\Atomic $atomic): Option - { - return Option::some($atomic) - ->flatMap(of(Type\Atomic\TGenericObject::class)) - ->filter(fn(Type\Atomic\TGenericObject $generic) => $generic->value === Generator::class) - ->flatMap(fn(Type\Atomic\TGenericObject $generic) => second($generic->type_params)); - } - - /** - * @return Option - */ - private static function valueFromArray(Type\Atomic $atomic): Option - { - return Option::some($atomic) - ->flatMap(of(Type\Atomic\TArray::class)) - ->flatMap(fn(Type\Atomic\TArray $a) => second($a->type_params)); - } - - /** - * @return Option - */ - private static function valueFromList(Type\Atomic $atomic): Option - { - return Option::some($atomic) - ->flatMap(of(Type\Atomic\TList::class)) - ->map(fn(Type\Atomic\TList $a) => $a->type_param); - } - - /** - * @return Option - */ - private static function valueFromKeyedArray(Type\Atomic $atomic): Option - { - return Option::some($atomic) - ->flatMap(of(Type\Atomic\TKeyedArray::class)) - ->map(fn(Type\Atomic\TKeyedArray $a) => $a->getGenericValueType()); - } - - /** - * @return Option - */ - private static function valueFromGenericObject(Type\Atomic $atomic): Option - { - return Option::some($atomic) - ->flatMap(of(Type\Atomic\TGenericObject::class)) - ->flatMap(fn(Type\Atomic\TGenericObject $a) => match (true) { - is_subclass_of($a->value, Seq::class), - is_subclass_of($a->value, Set::class), - is_subclass_of($a->value, NonEmptySet::class), - is_subclass_of($a->value, NonEmptySeq::class), - is_subclass_of($a->value, Option::class) => first($a->type_params), - is_subclass_of($a->value, Map::class), - is_subclass_of($a->value, NonEmptyMap::class), - is_subclass_of($a->value, Either::class) => second($a->type_params), - default => Option::none(), - }); - } -} diff --git a/src/Fp/Psalm/Util/MapTapNContext.php b/src/Fp/Psalm/Util/MapTapNContext.php deleted file mode 100644 index 76b38bd7..00000000 --- a/src/Fp/Psalm/Util/MapTapNContext.php +++ /dev/null @@ -1,21 +0,0 @@ -func_args; - } - - return $this->tuneForOptional( - func_args: $ctx->is_variadic - ? $this->tuneForVariadic($ctx->func_args, $ctx->current_args) - : $ctx->func_args, - drop_length: $ctx->optional_count > 0 - ? $ctx->required_count + $ctx->optional_count - count($ctx->current_args->properties) - : 0, - ); - } - - private function tuneForOptional(TKeyedArray $func_args, int $drop_length): TKeyedArray - { - $propsOption = proveNonEmptyList(dropRight($func_args->properties, $drop_length)); - - $cloned = clone $func_args; - $cloned->properties = $propsOption->getOrElse($cloned->properties); - - return $cloned; - } - - private function tuneForVariadic(TKeyedArray $func_args, TKeyedArray $current_args): TKeyedArray - { - $func_args_count = count($func_args->properties); - $current_args_count = count($current_args->properties); - - $propsOption = match (true) { - // There are variadic args: extend type with variadic param type - $current_args_count > $func_args_count => last($func_args->properties) - ->map(fn($last) => [ - ...$func_args->properties, - ...array_fill(0, $current_args_count - $func_args_count, $last), - ]), - - // No variadic args: remove variadic param from type - $current_args_count < $func_args_count => proveNonEmptyArray(init($func_args->properties)), - - // Exactly one variadic arg: leave as is - default => Option::none(), - }; - - $cloned = clone $func_args; - $cloned->properties = $propsOption->getOrElse($cloned->properties); - - return $cloned; - } -} diff --git a/src/Fp/Psalm/Util/Pluck/PluckPropertyTypeResolver.php b/src/Fp/Psalm/Util/Pluck/PluckPropertyTypeResolver.php deleted file mode 100644 index 294b4597..00000000 --- a/src/Fp/Psalm/Util/Pluck/PluckPropertyTypeResolver.php +++ /dev/null @@ -1,95 +0,0 @@ - - */ - public static function resolve(PluckResolveContext $context): Option - { - return self::getTypesForObject($context) - ->orElse(fn() => self::getTypesForObjectLikeArray($context)); - } - - /** - * @return Option - * @todo: Templated properties are not supported - */ - private static function getTypesForObject(PluckResolveContext $context): Option - { - return proveOf($context->object, TNamedObject::class) - ->flatMap(PsalmApi::$classlikes->getStorage(...)) - ->flatMap(fn(ClassLikeStorage $storage) => at($storage->properties, $context->key->value) - ->orElse(fn() => self::getPropertyFromParentClass($storage, $context->key->value)) - ->map(fn(PropertyStorage $property) => $property->type ?? Type::getMixed()) - ->orElse(fn() => self::undefinedPropertyIssue($storage->name, $context)) - ); - } - - /** - * @return Option - */ - private static function getPropertyFromParentClass(ClassLikeStorage $storage, string $property): Option - { - return at($storage->declaring_property_ids, $property) - ->flatMap(PsalmApi::$classlikes->getStorage(...)) - ->flatMap(fn(ClassLikeStorage $parent) => at($parent->properties, $property)); - } - - /** - * @return Option - */ - private static function undefinedPropertyIssue(string $class, PluckResolveContext $context): Option - { - $issue = new UndefinedPropertyFetch( - message: "Property '{$context->key->value}' is undefined", - code_location: $context->location, - property_id: "{$class}::\${$context->key->value}", - ); - - IssueBuffer::accepts($issue, $context->source->getSuppressedIssues()); - return Option::none(); - } - - /** - * @return Option - */ - private static function getTypesForObjectLikeArray(PluckResolveContext $context): Option - { - return proveOf($context->object, TKeyedArray::class) - ->flatMap(fn(TKeyedArray $array) => at($array->properties, $context->key->value) - ->orElse(fn() => self::undefinedArrayKeyIssue($context))); - } - - /** - * @return Option - */ - private static function undefinedArrayKeyIssue(PluckResolveContext $context): Option - { - $issue = new PossiblyUndefinedArrayOffset( - message: "Array key '{$context->key->value}' is undefined", - code_location: $context->location, - ); - - IssueBuffer::accepts($issue, $context->source->getSuppressedIssues()); - return Option::none(); - } -} diff --git a/src/Fp/Psalm/Util/Pluck/PluckResolveContext.php b/src/Fp/Psalm/Util/Pluck/PluckResolveContext.php deleted file mode 100644 index c3ff5e7e..00000000 --- a/src/Fp/Psalm/Util/Pluck/PluckResolveContext.php +++ /dev/null @@ -1,21 +0,0 @@ - - */ - public static function left(Union $type): Option - { - return self::from($type, self::GET_LEFT); - } - - /** - * @return Option - */ - public static function right(Union $type): Option - { - return self::from($type, self::GET_RIGHT); - } - - /** - * @param self::GET_* $idx - * @return Option - */ - public static function from(Union $type, int $idx): Option - { - return PsalmApi::$types->asSingleAtomic($type) - ->flatMap(fn(Atomic $atomic) => match (true) { - $atomic instanceof TGenericObject => Option::some($atomic) - ->filter(fn(TGenericObject $generic) => $generic->value === Either::class) - ->flatMap(fn(TGenericObject $option) => at($option->type_params, $idx)), - $atomic instanceof TClosure => Option::fromNullable($atomic->return_type) - ->flatMap(fn(Union $t) => self::from($t, $idx)), - default => Option::none(), - }); - } -} diff --git a/src/Fp/Psalm/Util/TypeFromPsalmAssertionResolver.php b/src/Fp/Psalm/Util/TypeFromPsalmAssertionResolver.php deleted file mode 100644 index 418115bb..00000000 --- a/src/Fp/Psalm/Util/TypeFromPsalmAssertionResolver.php +++ /dev/null @@ -1,87 +0,0 @@ - $to_return_type - */ - public static function getMethodReturnType( - MethodReturnTypeProviderEvent $event, - string $for_class, - Closure $to_negated, - Closure $to_return_type, - string $get_method_name = 'get', - ): null|Union { - $stmt = $event->getStmt(); - $ctx = $event->getContext(); - - return proveTrue($get_method_name === $event->getMethodNameLowercase()) - ->flatMap(fn() => self::getMethodVar($stmt)) - ->tap(fn() => self::negateAssertion($ctx, $for_class, $to_negated)) - ->flatMap(fn($called_variable) => at($ctx->vars_in_scope, $called_variable) - ->flatMap(fn(Union $union) => PsalmApi::$types->asSingleAtomicOf(TGenericObject::class, $union)) - ->flatMap($to_return_type)) - ->get(); - } - - /** - * @param Closure(TGenericObject, string): Union $to_negated - * @param class-string $for_class - */ - private static function negateAssertion(Context $context, string $for_class, Closure $to_negated): void - { - foreach ($context->clauses as $clause) { - if (count($clause->possibilities) > 1) { - continue; - } - - foreach ($clause->possibilities as $variable => [$possibility]) { - $reconciled = at($context->vars_in_scope, $variable) - ->flatMap(fn(Union $from_scope) => PsalmApi::$types->asSingleAtomicOf(TGenericObject::class, $from_scope)) - ->filter(fn(TGenericObject $generic) => $generic->value === $for_class) - ->map(fn(TGenericObject $generic) => $to_negated($generic, $possibility)); - - if ($reconciled->isSome()) { - $context->vars_in_scope[$variable] = $reconciled->get(); - } - } - } - } - - /** - * @return Option - */ - private static function getMethodVar(Expr $expr): Option - { - return Option::do(function() use ($expr) { - $method_call = yield proveOf($expr, MethodCall::class); - - return yield proveOf($method_call->var, MethodCall::class) - ->flatMap(fn(MethodCall $call) => self::getMethodVar($call)) - ->orElse(fn() => proveOf($method_call->var, Variable::class) - ->flatMap(fn(Variable $variable) => proveString($variable->name)) - ->map(fn($name) => "\${$name}")); - }); - } -} diff --git a/src/Fp/Psalm/Util/TypeRefinement/CollectionTypeParams.php b/src/Fp/Psalm/Util/TypeRefinement/CollectionTypeParams.php deleted file mode 100644 index 3115b241..00000000 --- a/src/Fp/Psalm/Util/TypeRefinement/CollectionTypeParams.php +++ /dev/null @@ -1,18 +0,0 @@ - - */ - public static function extract(MethodReturnTypeProviderEvent|FunctionReturnTypeProviderEvent $event): Option - { - return Option::firstT( - fn() => self::getPredicateCallback($event), - fn() => self::mockNotNullPredicateArg($event), - fn() => self::mockFirstClassCallable($event), - ); - } - - /** - * @return Option - */ - private static function getPredicateCallback(MethodReturnTypeProviderEvent|FunctionReturnTypeProviderEvent $event): Option - { - $predicate_arg = $event instanceof MethodReturnTypeProviderEvent - ? first($event->getCallArgs()) - : second($event->getCallArgs()); - - return $predicate_arg - ->map(fn(Arg $arg) => $arg->value) - ->flatMap(of([Closure::class, ArrowFunction::class])); - } - - /** - * @psalm-return Option - */ - private static function mockNotNullPredicateArg(MethodReturnTypeProviderEvent|FunctionReturnTypeProviderEvent $event): Option - { - return Option::some($event) - ->flatMap(of(MethodReturnTypeProviderEvent::class)) - ->filter(fn($e) => $e->getMethodNameLowercase() === strtolower('filterNotNull')) - ->map(fn() => new VirtualVariable('elem')) - ->map(fn(VirtualVariable $var) => new VirtualArrowFunction([ - 'expr' => new Isset_([$var]), - 'params' => [new Param($var)], - ])); - } - - /** - * @psalm-return Option - */ - private static function mockFirstClassCallable(MethodReturnTypeProviderEvent|FunctionReturnTypeProviderEvent $e): Option - { - $calling_id = $e instanceof MethodReturnTypeProviderEvent - ? $e->getMethodNameLowercase() - : $e->getFunctionId(); - - $variables = strtolower('filterKV') === $calling_id || strtolower('Fp\Collection\filterKV') === $calling_id - ? [new VirtualVariable('key'), new VirtualVariable('val')] - : [new VirtualVariable('val')]; - - $predicate_arg = $e instanceof MethodReturnTypeProviderEvent - ? first($e->getCallArgs()) - : second($e->getCallArgs()); - - return $predicate_arg - ->map(fn(Arg $arg) => $arg->value) - ->flatMap(of([FuncCall::class, MethodCall::class, StaticCall::class])) - ->filter(fn(CallLike $call_like) => $call_like->isFirstClassCallable()) - ->flatMap(fn(CallLike $call_like) => self::createVirtualCall( - statements_source: $e instanceof MethodReturnTypeProviderEvent ? $e->getSource() : $e->getStatementsSource(), - self: $e->getContext()->self, - original_call: $call_like, - fake_variables: $variables, - )) - ->map(fn(CallLike $call_like) => new VirtualArrowFunction([ - 'expr' => $call_like, - 'params' => map($variables, ctor(Param::class)), - ])); - } - - /** - * @param FuncCall|MethodCall|StaticCall $original_call - * @param non-empty-list $fake_variables - * @return Option - */ - private static function createVirtualCall( - StatementsSource $statements_source, - null|string $self, - CallLike $original_call, - array $fake_variables, - ): Option { - $function_id = match (true) { - $original_call instanceof FuncCall => Option::fromNullable($original_call->name->getAttribute('resolvedName')) - ->orElse(fn() => Option::some($original_call->name) - ->flatMap(of(Name::class)) - ->map(fn(Name $name) => (string) $name)) - ->flatMap(proveNonEmptyString(...)), - $original_call instanceof MethodCall => sequenceOptionT( - PsalmApi::$types->getType($statements_source, $original_call->var) - ->flatMap(PsalmApi::$types->asSingleAtomic(...)) - ->flatMap(of(TNamedObject::class)) - ->map(fn(TNamedObject $object) => $object->value), - Option::some($original_call->name) - ->flatMap(of(Identifier::class)) - ->map(fn(Identifier $id) => (string) $id), - )->mapN(self::toMethodId(...)), - $original_call instanceof StaticCall => sequenceOptionT( - Option::some($original_call->class) - ->flatMap(of(Name::class)) - ->map(fn(Name $id) => (string) $id) - ->map(fn($name) => in_array($name, ['self', 'static', 'parent']) ? $self : $name) - ->flatMap(proveNonEmptyString(...)), - Option::some($original_call->name) - ->flatMap(of(Identifier::class)) - ->map(fn(Identifier $id) => (string) $id), - )->mapN(self::toMethodId(...)), - }; - - $args = map($fake_variables, ctor(VirtualArg::class)); - - return $function_id->flatMap( - fn($id) => self::withCustomAssertions($id, $statements_source, match (true) { - $original_call instanceof FuncCall => new VirtualFuncCall($original_call->name, $args), - $original_call instanceof MethodCall => new VirtualMethodCall($original_call->var, $original_call->name, $args), - $original_call instanceof StaticCall => new VirtualStaticCall($original_call->class, $original_call->name, $args), - }) - ); - } - - /** - * @return non-empty-string - */ - private static function toMethodId(string $class_name, string $method_name): string - { - return "{$class_name}::{$method_name}"; - } - - /** - * @param non-empty-string $function_id - * @param VirtualStaticCall|VirtualFuncCall|VirtualMethodCall $expr - * @return Option - */ - private static function withCustomAssertions(string $function_id, StatementsSource $source, CallLike $expr): Option - { - return Option::some($source) - ->flatMap(of(StatementsAnalyzer::class)) - ->flatMap(fn(StatementsAnalyzer $analyzer) => Option - ::when( - cond: PsalmApi::$codebase->functions->functionExists($analyzer, strtolower($function_id)), - some: fn() => PsalmApi::$codebase->functions->getStorage($analyzer, strtolower($function_id)), - ) - ->orElse(fn() => Option::when( - cond: PsalmApi::$codebase->methods->hasStorage(MethodIdentifier::wrap($function_id)), - some: fn() => PsalmApi::$codebase->methods->getStorage(MethodIdentifier::wrap($function_id)), - )) - ->tap(fn(FunctionLikeStorage $storage) => $analyzer->node_data->setIfTrueAssertions($expr, $storage->if_true_assertions)) - ->tap(fn(FunctionLikeStorage $storage) => $analyzer->node_data->setIfFalseAssertions($expr, $storage->if_false_assertions)) - ->map(fn() => $expr)); - } -} diff --git a/src/Fp/Psalm/Util/TypeRefinement/RefineByPredicate.php b/src/Fp/Psalm/Util/TypeRefinement/RefineByPredicate.php deleted file mode 100644 index 7a43fe20..00000000 --- a/src/Fp/Psalm/Util/TypeRefinement/RefineByPredicate.php +++ /dev/null @@ -1,174 +0,0 @@ ->> - */ -final class RefineByPredicate -{ - /** - * Refine collection type-parameters - * By predicate expression - */ - public static function for(RefinementContext $context): CollectionTypeParams - { - return new CollectionTypeParams( - self::forKey($context)->getOrElse($context->type_params->key_type), - self::forValue($context)->getOrElse($context->type_params->val_type), - ); - } - - /** - * Try to refine collection key type-parameter - * - * @psalm-return Option - */ - private static function forKey(RefinementContext $context): Option - { - return sequenceOptionT( - fn() => self::getPredicateKeyArgumentName($context), - fn() => self::getPredicateSingleReturn($context), - fn() => Option::some($context), - fn() => Option::some($context->type_params->key_type), - )->flatMapN(self::refine(...)); - } - - /** - * Try to refine collection value type-parameter - * - * @psalm-return Option - */ - private static function forValue(RefinementContext $context): Option - { - return sequenceOptionT( - fn() => self::getPredicateValueArgumentName($context), - fn() => self::getPredicateSingleReturn($context), - fn() => Option::some($context), - fn() => Option::some($context->type_params->val_type), - )->flatMapN(self::refine(...)); - } - - /** - * Returns key argument name of $predicate that going to be refined. - * - * @psalm-return Option - */ - private static function getPredicateKeyArgumentName(RefinementContext $context): Option - { - return Option::some($context->refine_for) - ->filter(fn($refine_for) => RefineForEnum::KeyValue === $context->refine_for) - ->flatMap(fn() => first($context->predicate->getParams())) - ->flatMap(fn($key_param) => proveOf($key_param->var, Variable::class)) - ->flatMap(fn($variable) => proveString($variable->name)) - ->map(fn($name) => '$' . $name); - } - - /** - * Returns value argument name of $predicate that going to be refined. - * - * @psalm-return Option - */ - private static function getPredicateValueArgumentName(RefinementContext $context): Option - { - return Option::some($context->refine_for) - ->filter(fn($refine_for) => RefineForEnum::Value === $context->refine_for) - ->flatMap(fn() => first($context->predicate->getParams())) - ->orElse(fn() => second($context->predicate->getParams())) - ->flatMap(fn($value_param) => proveOf($value_param->var, Node\Expr\Variable::class)) - ->flatMap(fn($variable) => proveString($variable->name)) - ->map(fn($name) => '$' . $name); - } - - /** - * Returns single return expression of $predicate if present. - * Collection type parameter can be refined only for function with single return. - * - * @psalm-return Option - */ - private static function getPredicateSingleReturn(RefinementContext $context): Option - { - return Option::fromNullable($context->predicate->getStmts()) - ->filter(fn($stmts) => 1 === count($stmts)) - ->flatMap(fn($stmts) => firstMap($stmts, of(Return_::class))) - ->flatMap(fn($return) => Option::fromNullable($return->expr)); - } - - /** - * Collects assertion for $predicate_arg_name from $return_expr. - * - * @return Option - */ - private static function collectAssertions(RefinementContext $context, Expr $return_expr): Option - { - $cond_object_id = spl_object_id($return_expr); - - $truths = Algebra::getTruthsFromFormula( - clauses: FormulaGenerator::getFormula( - conditional_object_id: $cond_object_id, - creating_object_id: $cond_object_id, - conditional: $return_expr, - this_class_name: $context->execution_context->self, - source: $context->source, - codebase: PsalmApi::$codebase, - ), - creating_conditional_id: $cond_object_id, - ); - - return proveNonEmptyArray($truths); - } - - /** - * Reconciles $collection_type_param with $assertions using internal Psalm api. - * - * @psalm-param PsalmAssertions $assertions - * @psalm-return Option - */ - private static function refine( - string $arg_name, - Expr $return_expr, - RefinementContext $context, - Union $type, - ): Option - { - // reconcileKeyedTypes takes it by ref - $changed_var_ids = []; - - return self::collectAssertions($context, $return_expr) - ->map(fn($assertions) => Reconciler::reconcileKeyedTypes( - new_types: $assertions, - active_new_types: $assertions, - existing_types: [$arg_name => $type], - changed_var_ids: $changed_var_ids, - referenced_var_ids: [$arg_name => true], - statements_analyzer: $context->source, - template_type_map: $context->source->getTemplateTypeMap() ?: [], - code_location: new CodeLocation($context->source, $return_expr) - )) - ->flatMap(fn($reconciled_types) => at($reconciled_types, $arg_name)); - } -} diff --git a/src/Fp/Psalm/Util/TypeRefinement/RefineForEnum.php b/src/Fp/Psalm/Util/TypeRefinement/RefineForEnum.php deleted file mode 100644 index 473a5cfa..00000000 --- a/src/Fp/Psalm/Util/TypeRefinement/RefineForEnum.php +++ /dev/null @@ -1,9 +0,0 @@ - - * @implements StreamTerminalOps - * @implements StreamCastableOps + * @extends StreamChainableOps + * @extends StreamTerminalOps + * @extends StreamCastableOps */ interface StreamOps extends StreamChainableOps, StreamTerminalOps, StreamCastableOps { diff --git a/tests/Mock/FooIterable.php b/tests/Mock/FooIterable.php index bd817189..29958567 100644 --- a/tests/Mock/FooIterable.php +++ b/tests/Mock/FooIterable.php @@ -12,16 +12,18 @@ */ class FooIterable implements Iterator { - public function current() + public function current(): int { + return 0; } - public function next() + public function next(): void { } - public function key() + public function key(): int { + return 0; } public function valid(): bool @@ -29,7 +31,7 @@ public function valid(): bool return false; } - public function rewind() + public function rewind(): void { } } diff --git a/tests/Runtime/Classes/Either/EitherDoNotationTest.php b/tests/Runtime/Classes/Either/EitherDoNotationTest.php index dd5bb08d..c6b18bb2 100644 --- a/tests/Runtime/Classes/Either/EitherDoNotationTest.php +++ b/tests/Runtime/Classes/Either/EitherDoNotationTest.php @@ -50,6 +50,7 @@ public function testShortCircuit(): void $a = 1; $b = yield Either::right(2); $c = yield new Right(3); + /** @psalm-suppress NoValue */ $d = yield Either::left('error!'); $e = 5; diff --git a/tests/Runtime/Classes/Either/EitherTest.php b/tests/Runtime/Classes/Either/EitherTest.php index 334d951e..abcf36df 100644 --- a/tests/Runtime/Classes/Either/EitherTest.php +++ b/tests/Runtime/Classes/Either/EitherTest.php @@ -26,11 +26,11 @@ public function testToString(): void $this->assertEquals('Left(42)', Either::left(42)->toString()); $this->assertEquals('Right(42)', Either::right(42)->toString()); $this->assertEquals('Right([])', Either::right([])); - $this->assertEquals("Right([1, 2, 3])", Either::right([1, 2, 3])); - $this->assertEquals("Right(['t' => 1])", Either::right(['t' => 1])); + $this->assertEquals('Right([1, 2, 3])', Either::right([1, 2, 3])); + $this->assertEquals('Right(["t" => 1])', Either::right(['t' => 1])); $this->assertEquals('Left(InvalidArgumentException())', Either::left(new InvalidArgumentException())); $this->assertEquals( - "Left(InvalidArgumentException('Invalid string given'))", + 'Left(InvalidArgumentException("Invalid string given"))', Either::left(new InvalidArgumentException('Invalid string given')), ); } diff --git a/tests/Runtime/Classes/ExtensionTest.php b/tests/Runtime/Classes/ExtensionTest.php index 7f5ff623..5a796999 100644 --- a/tests/Runtime/Classes/ExtensionTest.php +++ b/tests/Runtime/Classes/ExtensionTest.php @@ -137,7 +137,7 @@ public function testCallStaticExtensionMethod(object $instance, string $extClass public function testAddInstanceMethodTwice(): void { - $this->expectErrorMessage("Instance extension method '{$this->testMethodName}' is already defined!"); + $this->expectExceptionMessage("Instance extension method '{$this->testMethodName}' is already defined!"); ArrayListExtensions::addInstanceExtension($this->testMethodName, function(ArrayList $list): int { /** @var ArrayList $list */; @@ -147,7 +147,7 @@ public function testAddInstanceMethodTwice(): void public function testAddStaticMethodTwice(): void { - $this->expectErrorMessage("Static extension method '{$this->testStaticMethodName}' is already defined!"); + $this->expectExceptionMessage("Static extension method '{$this->testStaticMethodName}' is already defined!"); ArrayListExtensions::addStaticExtension($this->testStaticMethodName, function(string $string): ArrayList { return ArrayList::collect(str_split($string)); diff --git a/tests/Runtime/Classes/Option/OptionDoNotationTest.php b/tests/Runtime/Classes/Option/OptionDoNotationTest.php index 80fba849..66347553 100644 --- a/tests/Runtime/Classes/Option/OptionDoNotationTest.php +++ b/tests/Runtime/Classes/Option/OptionDoNotationTest.php @@ -51,6 +51,7 @@ public function testShortCircuit(): void $a = 1; $b = yield Option::fromNullable(2); $c = yield Option::some(3); + /** @psalm-suppress NoValue */ $d = yield Option::none(); $e = 5; diff --git a/tests/Runtime/Functions/Callable/CallableTest.php b/tests/Runtime/Functions/Callable/CallableTest.php index 1aa07456..45ebebff 100644 --- a/tests/Runtime/Functions/Callable/CallableTest.php +++ b/tests/Runtime/Functions/Callable/CallableTest.php @@ -15,6 +15,7 @@ use function Fp\Callable\partial; use function Fp\Callable\partialLeft; use function Fp\Callable\partialRight; +use function Fp\Callable\pipe; final class CallableTest extends TestCase { @@ -51,4 +52,14 @@ public function testPartial(): void $this->assertEquals('abc', partialLeft($c, 'a', 'b')('c')); $this->assertEquals('cba', partialRight($c, 'a', 'b')('c')); } + + public function testPipe(): void + { + $this->assertEquals(42, pipe( + 0, + fn($i) => $i + 11, + fn($i) => $i + 20, + fn($i) => $i + 11, + )); + } } diff --git a/tests/Runtime/Functions/Collection/EitherTraverseTest.php b/tests/Runtime/Functions/Collection/EitherTraverseTest.php index bcb3ea08..4d089a66 100644 --- a/tests/Runtime/Functions/Collection/EitherTraverseTest.php +++ b/tests/Runtime/Functions/Collection/EitherTraverseTest.php @@ -8,12 +8,10 @@ use PHPUnit\Framework\TestCase; use function Fp\Collection\sequenceEither; -use function Fp\Collection\sequenceEitherAcc; use function Fp\Collection\sequenceEitherMerged; use function Fp\Collection\sequenceEitherMergedT; use function Fp\Collection\sequenceEitherT; use function Fp\Collection\traverseEither; -use function Fp\Collection\traverseEitherAcc; use function Fp\Collection\traverseEitherKV; use function Fp\Collection\traverseEitherMerged; @@ -59,26 +57,6 @@ public function testTraverseEitherMerged(): void ); } - public function testTraverseAcc(): void - { - /** @psalm-var list $c */ - $c = [1, 2, 3]; - - $this->assertEquals( - Either::right($c), - traverseEitherAcc($c, fn(int $v) => $v <= 3 - ? Either::right($v) - : Either::left('Is too high')) - ); - - $this->assertEquals( - Either::left(['2 is too high', '3 is too high']), - traverseEitherAcc($c, fn(int $v) => $v < 2 - ? Either::right($v) - : Either::left("{$v} is too high")) - ); - } - public function testTraverseKV(): void { $c1 = [ @@ -237,58 +215,4 @@ public function testLazySequence(): void ]) ); } - - public function testSequenceAcc(): void - { - $this->assertEquals( - Either::left([ - 'err1', - 'err2', - ]), - sequenceEitherAcc([ - Either::left('err1'), - Either::right('val'), - Either::left('err2'), - ]), - ); - - $this->assertEquals( - Either::left([ - 'k1' => 'err1', - 'k3' => 'err2', - ]), - sequenceEitherAcc([ - 'k1' => Either::left('err1'), - 'k2' => Either::right('val'), - 'k3' => Either::left('err2'), - ]), - ); - } - - public function testLazySequenceAcc(): void - { - $this->assertEquals( - Either::left([ - 'err1', - 'err2', - ]), - sequenceEitherAcc([ - fn() => Either::left('err1'), - fn() => Either::right('val'), - fn() => Either::left('err2'), - ]), - ); - - $this->assertEquals( - Either::left([ - 'k1' => 'err1', - 'k3' => 'err2', - ]), - sequenceEitherAcc([ - 'k1' => fn() => Either::left('err1'), - 'k2' => fn() => Either::right('val'), - 'k3' => fn() => Either::left('err2'), - ]), - ); - } } diff --git a/tests/Runtime/Functions/Collection/GroupMapReduceTest.php b/tests/Runtime/Functions/Collection/GroupMapReduceTest.php index 2c63ddb0..52fa8d4f 100644 --- a/tests/Runtime/Functions/Collection/GroupMapReduceTest.php +++ b/tests/Runtime/Functions/Collection/GroupMapReduceTest.php @@ -5,6 +5,7 @@ namespace Tests\Runtime\Functions\Collection; use PHPUnit\Framework\TestCase; + use function Fp\Collection\groupMapReduce; use function Fp\Collection\groupMapReduceKV; @@ -29,6 +30,10 @@ public function testGroup(): void ], fn(array $a) => $a['id'], fn(array $a) => [$a['sum']], + /** + * @param non-empty-list $old + * @param non-empty-list $new + */ fn(array $old, array $new) => array_merge($old, $new), ) ); diff --git a/tests/Runtime/Functions/Util/ToStringTest.php b/tests/Runtime/Functions/Util/ToStringTest.php new file mode 100644 index 00000000..1ba59924 --- /dev/null +++ b/tests/Runtime/Functions/Util/ToStringTest.php @@ -0,0 +1,45 @@ +assertEquals($expected, toString($value)); + } + + /** + * @return list + */ + public function provideCases(): array + { + return [ + [42, '42'], + [42.00, '42.00'], + [42.42, '42.42'], + [true, 'true'], + [false, 'false'], + ['string', '"string"'], + ['type: "string"', '"type: \"string\""'], + [new Baz, 'Baz()'], + [new Bar(a: 42), 'Tests\Mock\Bar'], + [new RuntimeException(), 'RuntimeException()'], + [new RuntimeException('Error occurred'), 'RuntimeException("Error occurred")'], + [[1, 2, 3], '[1, 2, 3]'], + [['fst' => 1, 'snd' => 2, 'thr' => 3], '["fst" => 1, "snd" => 2, "thr" => 3]'], + ]; + } +} diff --git a/tests/Runtime/Interfaces/Map/MapOpsTest.php b/tests/Runtime/Interfaces/Map/MapOpsTest.php index 83faadf9..8543f306 100644 --- a/tests/Runtime/Interfaces/Map/MapOpsTest.php +++ b/tests/Runtime/Interfaces/Map/MapOpsTest.php @@ -12,7 +12,6 @@ use Fp\Functional\Separated\Separated; use Generator; use PHPUnit\Framework\TestCase; -use Tests\Mock\Bar; use Tests\Mock\Foo; final class MapOpsTest extends TestCase @@ -199,6 +198,62 @@ public function testTraverseEitherN(): void ); } + public function testTraverseEitherMerged(): void + { + /** @var HashMap $seq1 */ + $seq1 = HashMap::collect(['fst' => 1, 'snd' => 2, 'thr' => 3]); + + $this->assertEquals( + Either::right($seq1), + $seq1->traverseEitherMerged(fn($x) => $x >= 1 ? Either::right($x) : Either::left(['err'])), + ); + $this->assertEquals( + Either::right($seq1), + $seq1->map(fn($x) => $x >= 1 ? Either::right($x) : Either::left(['err']))->sequenceEitherMerged(), + ); + + /** @var HashMap $seq2 */ + $seq2 = HashMap::collect([ + 'neg-snd' => -2, + 'neg-fst' => -1, + 'zero' => 0, + 'fst' => 1, + 'snd' => 2, + ]); + + $this->assertEquals( + Either::left(['wrong: -2', 'wrong: -1', 'wrong: 0']), + $seq2->traverseEitherMerged(fn($x) => $x >= 1 ? Either::right($x) : Either::left(["wrong: {$x}"])), + ); + $this->assertEquals( + Either::left(['wrong: -2', 'wrong: -1', 'wrong: 0']), + $seq2->map(fn($x) => $x >= 1 ? Either::right($x) : Either::left(["wrong: {$x}"]))->sequenceEitherMerged(), + ); + } + + public function testTraverseEitherMergedN(): void + { + $collection = HashMap::collect([ + 'fst' => [1, 1], + 'snd' => [2, 2], + 'thr' => [3, 3], + ]); + + $this->assertEquals( + Either::right(HashMap::collect(['fst' => 2, 'snd' => 4, 'thr' => 6])), + $collection->traverseEitherMergedN(fn(int $a, int $b) => $a + $b <= 6 + ? Either::right($a + $b) + : Either::left(["{$a} + {$b} is invalid"])), + ); + + $this->assertEquals( + Either::left(['3 + 3 is invalid', '4 + 4 is invalid']), + $collection->appended('fth', [4, 4])->traverseEitherMergedN(fn(int $a, int $b) => $a + $b < 6 + ? Either::right($a + $b) + : Either::left(["{$a} + {$b} is invalid"])), + ); + } + public function testPartition(): void { $expected = Separated::create( @@ -571,14 +626,16 @@ public function testGroupMap(): void public function testGroupMapReduce(): void { - $actual = HashMap::collect([ + /** @var array */ + $source = [ '10-1' => ['id' => 10, 'sum' => 10], '10-2' => ['id' => 10, 'sum' => 15], '10-3' => ['id' => 10, 'sum' => 20], '20-1' => ['id' => 20, 'sum' => 10], '20-2' => ['id' => 20, 'sum' => 15], '30-1' => ['id' => 30, 'sum' => 20], - ])->groupMapReduce( + ]; + $actual = HashMap::collect($source)->groupMapReduce( fn(array $a) => $a['id'], fn(array $a) => $a['sum'], fn(int $old, int $new) => $old + $new, diff --git a/tests/Runtime/Interfaces/Map/MapTest.php b/tests/Runtime/Interfaces/Map/MapTest.php index 40db6d39..4ce73f91 100644 --- a/tests/Runtime/Interfaces/Map/MapTest.php +++ b/tests/Runtime/Interfaces/Map/MapTest.php @@ -25,7 +25,7 @@ final class MapTest extends TestCase public function testToString(): void { $this->assertEquals( - "HashMap('k1' => 1, 'k2' => 2, 'k3' => 3)", + 'HashMap("k1" => 1, "k2" => 2, "k3" => 3)', (string) HashMap::collectPairs([ ['k1', 1], ['k2', 2], @@ -33,7 +33,7 @@ public function testToString(): void ]), ); $this->assertEquals( - "HashMap('k1' => Some(1), 'k2' => Some(2), 'k3' => None)", + 'HashMap("k1" => Some(1), "k2" => Some(2), "k3" => None)', (string) HashMap::collectPairs([ ['k1', Option::some(1)], ['k2', Option::some(2)], @@ -41,7 +41,7 @@ public function testToString(): void ]), ); $this->assertEquals( - "HashMap('k1' => 1, 'k2' => 2, 'k3' => 3)", + 'HashMap("k1" => 1, "k2" => 2, "k3" => 3)', HashMap::collectPairs([ ['k1', 1], ['k2', 2], @@ -49,7 +49,7 @@ public function testToString(): void ])->toString(), ); $this->assertEquals( - "HashMap('k1' => Some(1), 'k2' => Some(2), 'k3' => None)", + 'HashMap("k1" => Some(1), "k2" => Some(2), "k3" => None)', HashMap::collectPairs([ ['k1', Option::some(1)], ['k2', Option::some(2)], diff --git a/tests/Runtime/Interfaces/NonEmptyMap/NonEmptyMapOpsTest.php b/tests/Runtime/Interfaces/NonEmptyMap/NonEmptyMapOpsTest.php index c835ab45..68066c82 100644 --- a/tests/Runtime/Interfaces/NonEmptyMap/NonEmptyMapOpsTest.php +++ b/tests/Runtime/Interfaces/NonEmptyMap/NonEmptyMapOpsTest.php @@ -12,7 +12,6 @@ use Fp\Functional\Separated\Separated; use Generator; use PHPUnit\Framework\TestCase; -use Tests\Mock\Bar; use Tests\Mock\Foo; final class NonEmptyMapOpsTest extends TestCase @@ -511,12 +510,15 @@ public function testGroupMap(): void public function testGroupMapReduce(): void { + /** @var non-empty-array */ + $source = ['fst' => 1, 'snd' => 2, 'trd' => 3]; + $this->assertEquals( NonEmptyHashMap::collectNonEmpty([ 'oddDoubledSum' => 8, 'evenDoubledSum' => 4, ]), - NonEmptyHashMap::collectNonEmpty(['fst' => 1, 'snd' => 2, 'trd' => 3]) + NonEmptyHashMap::collectNonEmpty($source) ->groupMapReduce( fn(int $i) => 0 === $i % 2 ? 'evenDoubledSum' : 'oddDoubledSum', fn(int $i) => $i * 2, diff --git a/tests/Runtime/Interfaces/NonEmptyMap/NonEmptyMapTest.php b/tests/Runtime/Interfaces/NonEmptyMap/NonEmptyMapTest.php index fe9e03c7..3ea07c85 100644 --- a/tests/Runtime/Interfaces/NonEmptyMap/NonEmptyMapTest.php +++ b/tests/Runtime/Interfaces/NonEmptyMap/NonEmptyMapTest.php @@ -21,7 +21,7 @@ final class NonEmptyMapTest extends TestCase public function testToString(): void { $this->assertEquals( - "NonEmptyHashMap('k1' => 1, 'k2' => 2, 'k3' => 3)", + 'NonEmptyHashMap("k1" => 1, "k2" => 2, "k3" => 3)', (string) NonEmptyHashMap::collectPairsNonEmpty([ ['k1', 1], ['k2', 2], @@ -29,7 +29,7 @@ public function testToString(): void ]), ); $this->assertEquals( - "NonEmptyHashMap('k1' => Some(1), 'k2' => Some(2), 'k3' => None)", + 'NonEmptyHashMap("k1" => Some(1), "k2" => Some(2), "k3" => None)', (string) NonEmptyHashMap::collectPairsNonEmpty([ ['k1', Option::some(1)], ['k2', Option::some(2)], @@ -37,7 +37,7 @@ public function testToString(): void ]), ); $this->assertEquals( - "NonEmptyHashMap('k1' => 1, 'k2' => 2, 'k3' => 3)", + 'NonEmptyHashMap("k1" => 1, "k2" => 2, "k3" => 3)', NonEmptyHashMap::collectPairsNonEmpty([ ['k1', 1], ['k2', 2], @@ -45,7 +45,7 @@ public function testToString(): void ])->toString(), ); $this->assertEquals( - "NonEmptyHashMap('k1' => Some(1), 'k2' => Some(2), 'k3' => None)", + 'NonEmptyHashMap("k1" => Some(1), "k2" => Some(2), "k3" => None)', NonEmptyHashMap::collectPairsNonEmpty([ ['k1', Option::some(1)], ['k2', Option::some(2)], diff --git a/tests/Runtime/Interfaces/NonEmptySeq/NonEmptySeqOpsTest.php b/tests/Runtime/Interfaces/NonEmptySeq/NonEmptySeqOpsTest.php index 53da6e7d..a8654f9d 100644 --- a/tests/Runtime/Interfaces/NonEmptySeq/NonEmptySeqOpsTest.php +++ b/tests/Runtime/Interfaces/NonEmptySeq/NonEmptySeqOpsTest.php @@ -20,7 +20,7 @@ final class NonEmptySeqOpsTest extends TestCase { /** - * @return list}> + * @return list, class-string}> */ public function seqClassDataProvider(): array { @@ -421,22 +421,24 @@ public function testGroupBy(string $seq): void */ public function testGroupMapReduce(string $seq): void { + /** @var non-empty-list */ + $source = [ + ['id' => 10, 'sum' => 10], + ['id' => 10, 'sum' => 15], + ['id' => 10, 'sum' => 20], + ['id' => 20, 'sum' => 10], + ['id' => 20, 'sum' => 15], + ['id' => 30, 'sum' => 20], + ]; $this->assertEquals( NonEmptyHashMap::collectNonEmpty([ 10 => [10, 15, 20], 20 => [10, 15], 30 => [20], ]), - $seq::collectNonEmpty([ - ['id' => 10, 'sum' => 10], - ['id' => 10, 'sum' => 15], - ['id' => 10, 'sum' => 20], - ['id' => 20, 'sum' => 10], - ['id' => 20, 'sum' => 15], - ['id' => 30, 'sum' => 20], - ])->groupMapReduce( + $seq::collectNonEmpty($source)->groupMapReduce( fn(array $a) => $a['id'], - fn(array $a) => [$a['sum']], + fn(array $a) => /** @var non-empty-list */[$a['sum']], fn(array $old, array $new) => [...$old, ...$new], ) ); diff --git a/tests/Runtime/Interfaces/NonEmptySeq/NonEmptySeqTest.php b/tests/Runtime/Interfaces/NonEmptySeq/NonEmptySeqTest.php index cf1aa47a..c934832e 100644 --- a/tests/Runtime/Interfaces/NonEmptySeq/NonEmptySeqTest.php +++ b/tests/Runtime/Interfaces/NonEmptySeq/NonEmptySeqTest.php @@ -43,7 +43,7 @@ public function provideToStringData(): Generator Either::right(2), Either::left('err'), ]), - 'NonEmptyArrayList(Right(1), Right(2), Left(\'err\'))', + 'NonEmptyArrayList(Right(1), Right(2), Left("err"))', ]; yield 'NonEmptyLinkedList' => [ NonEmptyLinkedList::collectNonEmpty([1, 2, 3]), @@ -63,7 +63,7 @@ public function provideToStringData(): Generator Either::right(2), Either::left('err'), ]), - 'NonEmptyLinkedList(Right(1), Right(2), Left(\'err\'))', + 'NonEmptyLinkedList(Right(1), Right(2), Left("err"))', ]; } diff --git a/tests/Runtime/Interfaces/NonEmptySet/NonEmptySetOpsTest.php b/tests/Runtime/Interfaces/NonEmptySet/NonEmptySetOpsTest.php index bf6c9899..5bed77b3 100644 --- a/tests/Runtime/Interfaces/NonEmptySet/NonEmptySetOpsTest.php +++ b/tests/Runtime/Interfaces/NonEmptySet/NonEmptySetOpsTest.php @@ -291,22 +291,24 @@ public function testGroupMap(): void public function testGroupMapReduce(): void { + /** @var non-empty-list */ + $source = [ + ['id' => 10, 'sum' => 10], + ['id' => 10, 'sum' => 15], + ['id' => 10, 'sum' => 20], + ['id' => 20, 'sum' => 10], + ['id' => 20, 'sum' => 15], + ['id' => 30, 'sum' => 20], + ]; $this->assertEquals( NonEmptyHashMap::collectNonEmpty([ 10 => [10, 15, 20], 20 => [10, 15], 30 => [20], ]), - NonEmptyHashSet::collectNonEmpty([ - ['id' => 10, 'sum' => 10], - ['id' => 10, 'sum' => 15], - ['id' => 10, 'sum' => 20], - ['id' => 20, 'sum' => 10], - ['id' => 20, 'sum' => 15], - ['id' => 30, 'sum' => 20], - ])->groupMapReduce( + NonEmptyHashSet::collectNonEmpty($source)->groupMapReduce( fn(array $a) => $a['id'], - fn(array $a) => [$a['sum']], + fn(array $a) => /** @var non-empty-list */[$a['sum']], fn(array $old, array $new) => array_merge($old, $new), ) ); diff --git a/tests/Runtime/Interfaces/Seq/SeqOpsTest.php b/tests/Runtime/Interfaces/Seq/SeqOpsTest.php index d82ec246..13163205 100644 --- a/tests/Runtime/Interfaces/Seq/SeqOpsTest.php +++ b/tests/Runtime/Interfaces/Seq/SeqOpsTest.php @@ -31,7 +31,7 @@ public function seqClassDataProvider(): array } /** - * @return list}> + * @return list, class-string}> */ public function seqWithNonEmptySeqClassDataProvider(): array { @@ -162,6 +162,37 @@ public function testTraverseEither(string $seq): void ); } + /** + * @param class-string $seq + * @dataProvider seqClassDataProvider + */ + public function testTraverseEitherMerged(string $seq): void + { + /** @var Seq $seq1 */ + $seq1 = $seq::collect([1, 2, 3]); + + $this->assertEquals( + Either::right($seq1), + $seq1->traverseEitherMerged(fn($x) => $x >= 1 ? Either::right($x) : Either::left(['err'])), + ); + $this->assertEquals( + Either::right($seq1), + $seq1->map(fn($x) => $x >= 1 ? Either::right($x) : Either::left(['err']))->sequenceEitherMerged(), + ); + + /** @var Seq $seq2 */ + $seq2 = $seq::collect([-2, -1, 0, 1, 2]); + + $this->assertEquals( + Either::left(['wrong: -2', 'wrong: -1', 'wrong: 0']), + $seq2->traverseEitherMerged(fn($x) => $x >= 1 ? Either::right($x) : Either::left(["wrong: {$x}"])), + ); + $this->assertEquals( + Either::left(['wrong: -2', 'wrong: -1', 'wrong: 0']), + $seq2->map(fn($x) => $x >= 1 ? Either::right($x) : Either::left(["wrong: {$x}"]))->sequenceEitherMerged(), + ); + } + /** * @param class-string $seq * @dataProvider seqClassDataProvider @@ -432,22 +463,24 @@ public function testGroupBy(string $seq, string $nonEmptySeq): void */ public function testGroupMapReduce(string $seq): void { + /** @var list */ + $source = [ + ['id' => 10, 'sum' => 10], + ['id' => 10, 'sum' => 15], + ['id' => 10, 'sum' => 20], + ['id' => 20, 'sum' => 10], + ['id' => 20, 'sum' => 15], + ['id' => 30, 'sum' => 20], + ]; $this->assertEquals( HashMap::collect([ 10 => [10, 15, 20], 20 => [10, 15], 30 => [20], ]), - $seq::collect([ - ['id' => 10, 'sum' => 10], - ['id' => 10, 'sum' => 15], - ['id' => 10, 'sum' => 20], - ['id' => 20, 'sum' => 10], - ['id' => 20, 'sum' => 15], - ['id' => 30, 'sum' => 20], - ])->groupMapReduce( + $seq::collect($source)->groupMapReduce( fn(array $a) => $a['id'], - fn(array $a) => [$a['sum']], + fn(array $a) => /** @var list */[$a['sum']], fn(array $old, array $new) => [...$old, ...$new], ) ); @@ -923,6 +956,33 @@ public function testTraverseEitherN(string $seq): void ); } + /** + * @param class-string $seq + * @dataProvider seqClassDataProvider + */ + public function testTraverseEitherMergedN(string $seq): void + { + $collection = $seq::collect([ + [1, 1], + [2, 2], + [3, 3], + [4, 4], + ]); + + $this->assertEquals( + Either::right($seq::collect([2, 4, 6, 8])), + $collection->traverseEitherMergedN( + fn(int $a, int $b) => $a + $b <= 8 ? Either::right($a + $b) : Either::left(['invalid']), + ), + ); + $this->assertEquals( + Either::left(['invalid: 3 + 3', 'invalid: 4 + 4']), + $collection->traverseEitherMergedN( + fn(int $a, int $b) => $a + $b < 6 ? Either::right($a + $b) : Either::left(["invalid: {$a} + {$b}"]), + ), + ); + } + /** * @param class-string $seq * @dataProvider seqClassDataProvider diff --git a/tests/Runtime/Interfaces/Seq/SeqTest.php b/tests/Runtime/Interfaces/Seq/SeqTest.php index 9cd14141..146c43cf 100644 --- a/tests/Runtime/Interfaces/Seq/SeqTest.php +++ b/tests/Runtime/Interfaces/Seq/SeqTest.php @@ -54,7 +54,7 @@ public function provideToStringData(): Generator Either::right(2), Either::left('err'), ]), - 'ArrayList(Right(1), Right(2), Left(\'err\'))', + 'ArrayList(Right(1), Right(2), Left("err"))', ]; yield 'LinkedList' => [ LinkedList::collect([1, 2, 3]), @@ -74,7 +74,7 @@ public function provideToStringData(): Generator Either::right(2), Either::left('err'), ]), - 'LinkedList(Right(1), Right(2), Left(\'err\'))', + 'LinkedList(Right(1), Right(2), Left("err"))', ]; } diff --git a/tests/Runtime/Interfaces/Set/SetOpsTest.php b/tests/Runtime/Interfaces/Set/SetOpsTest.php index 4bbbbc7f..79bb642c 100644 --- a/tests/Runtime/Interfaces/Set/SetOpsTest.php +++ b/tests/Runtime/Interfaces/Set/SetOpsTest.php @@ -165,6 +165,56 @@ public function testTraverseEitherN(): void ); } + public function testTraverseEitherMerged(): void + { + /** @var HashSet $seq1 */ + $seq1 = HashSet::collect([1, 2, 3]); + + $this->assertEquals( + Either::right($seq1), + $seq1->traverseEitherMerged(fn($x) => $x >= 1 ? Either::right($x) : Either::left(['err'])), + ); + $this->assertEquals( + Either::right($seq1), + $seq1->map(fn($x) => $x >= 1 ? Either::right($x) : Either::left(['err']))->sequenceEitherMerged(), + ); + + /** @var HashSet $seq2 */ + $seq2 = HashSet::collect([-2, -1, 0, 1, 2]); + + $this->assertEquals( + Either::left(['wrong: -2', 'wrong: -1', 'wrong: 0']), + $seq2->traverseEitherMerged(fn($x) => $x >= 1 ? Either::right($x) : Either::left(["wrong: {$x}"])), + ); + $this->assertEquals( + Either::left(['wrong: -2', 'wrong: -1', 'wrong: 0']), + $seq2->map(fn($x) => $x >= 1 ? Either::right($x) : Either::left(["wrong: {$x}"]))->sequenceEitherMerged(), + ); + } + + public function testTraverseEitherMergedN(): void + { + $collection = HashSet::collect([ + [1, 1], + [2, 2], + [3, 3], + [4, 4], + ]); + + $this->assertEquals( + Either::right(HashSet::collect([2, 4, 6, 8])), + $collection->traverseEitherMergedN( + fn(int $a, int $b) => $a + $b <= 8 ? Either::right($a + $b) : Either::left(['invalid']), + ), + ); + $this->assertEquals( + Either::left(['invalid: 3 + 3', 'invalid: 4 + 4']), + $collection->traverseEitherMergedN( + fn(int $a, int $b) => $a + $b < 6 ? Either::right($a + $b) : Either::left(["invalid: {$a} + {$b}"]), + ), + ); + } + public function testPartition(): void { $this->assertEquals( @@ -298,22 +348,24 @@ public function testGroupMap(): void public function testGroupMapReduce(): void { + /** @var list */ + $source = [ + ['id' => 10, 'sum' => 10], + ['id' => 10, 'sum' => 15], + ['id' => 10, 'sum' => 20], + ['id' => 20, 'sum' => 10], + ['id' => 20, 'sum' => 15], + ['id' => 30, 'sum' => 20], + ]; $this->assertEquals( HashMap::collect([ 10 => [10, 15, 20], 20 => [10, 15], 30 => [20], ]), - HashSet::collect([ - ['id' => 10, 'sum' => 10], - ['id' => 10, 'sum' => 15], - ['id' => 10, 'sum' => 20], - ['id' => 20, 'sum' => 10], - ['id' => 20, 'sum' => 15], - ['id' => 30, 'sum' => 20], - ])->groupMapReduce( + HashSet::collect($source)->groupMapReduce( fn(array $a) => $a['id'], - fn(array $a) => [$a['sum']], + fn(array $a) => /** @var list */[$a['sum']], fn(array $old, array $new) => array_merge($old, $new), ) ); diff --git a/tests/Static/Classes/Either/EitherAssertionStaticTest.php b/tests/Static/Classes/Either/EitherAssertionStaticTest.php deleted file mode 100644 index 5e9a645c..00000000 --- a/tests/Static/Classes/Either/EitherAssertionStaticTest.php +++ /dev/null @@ -1,76 +0,0 @@ - $either - */ - public function testIsRightWithIfTrueBranch(Either $either): int - { - if ($either->isRight()) { - return $either->get(); - } else { - throw new Error(); - } - } - - /** - * @param Either $either - */ - public function testIsRightWithIfFalseBranch(Either $either): string - { - if ($either->isRight()) { - throw new Error(); - } else { - return $either->get(); - } - } - - /** - * @param Either $either - */ - public function testIsRightWithTernaryTrueBranch(Either $either): int - { - return $either->isRight() - ? $either->get() - : throw new Error(); - } - - /** - * @param Either $either - * @return string - */ - public function testIsRightWithTernaryFalseBranch(Either $either): string - { - return $either->isRight() - ? throw new Error() - : $either->get(); - } - - /** - * @param Either $either - */ - public function testIsLeftWithTernaryTrueBranch(Either $either): string - { - return $either->isLeft() - ? $either->get() - : throw new Error(); - } - - /** - * @param Either $either - */ - public function testIsLeftWithTernaryFalseBranch(Either $either): int - { - return $either->isLeft() - ? throw new Error() - : $either->get(); - } -} diff --git a/tests/Static/Classes/Either/EitherDoNotationStaticTest.php b/tests/Static/Classes/Either/EitherDoNotationStaticTest.php deleted file mode 100644 index b286246b..00000000 --- a/tests/Static/Classes/Either/EitherDoNotationStaticTest.php +++ /dev/null @@ -1,29 +0,0 @@ - - */ - public function testWithFilter(): Either - { - return Either::do(function() { - /** @var int $num1 */ - $num1 = yield Either::right(10); - /** @var int $num2 */ - $num2 = yield Either::right(20); - - if ($num1 < 10) { - return yield Either::left("num1 less than 10"); - } - - return $num1 + $num2; - }); - } -} diff --git a/tests/Static/Classes/Either/EitherStaticTest.php b/tests/Static/Classes/Either/EitherStaticTest.php deleted file mode 100644 index d0aa3730..00000000 --- a/tests/Static/Classes/Either/EitherStaticTest.php +++ /dev/null @@ -1,110 +0,0 @@ - - */ - public function testRightFabricMethod(): Either - { - return Either::right(1); - } - - /** - * @return Either<1, empty> - */ - public function testLeftFabricMethod(): Either - { - return Either::left(1); - } - - /** - * @psalm-return int|string - */ - public function testGet(): int|string - { - $getEither = fn (): Either => rand(0, 1) - ? Either::right(1) - : Either::left("error!"); - - return $getEither()->get(); - } - - /** - * @psalm-return 1|string - */ - public function testMap(): int|string - { - return Either::left(1) - ->map(fn(mixed $v) => (string) $v) - ->get(); - } - - /** - * @psalm-return '1' - */ - public function testFlatMap(): string - { - return Either::right(1) - ->flatMap(fn(int $v) => Either::right((string) $v)) - ->get(); - } - - /** - * @return Either - */ - public function testSwap(): Either - { - $getEither = fn (): Either => rand(0, 1) - ? Either::right(1) - : Either::left("error!"); - - return $getEither() - ->flatMap(fn(int $v) => Either::right((bool) $v)) - ->swap(); - } - - /** - * @return Either<0|1,float> - */ - public function testMapLeft(): Either - { - $getEither = fn (): Either => rand(0, 1) - ? Either::right(1) - : Either::left("error!"); - - return $getEither() - ->flatMap(fn(int $v) => Either::right((float) $v)) - ->mapLeft(fn(string $e) => (bool) $e) - ->mapLeft(fn(bool $e) => (int) $e); - } - - /** - * @param Either $in - * @return Either - */ - public function testFilterOrElse(Either $in): Either - { - return $in->filterOrElse( - fn($i) => is_string($i), - fn() => new RuntimeException('Unable to filter'), - ); - } - - /** - * @param Either $in - * @return Either - */ - public function testFilterOrElseWithFirstClassCallable(Either $in): Either - { - return $in->filterOrElse(is_string(...), fn() => new RuntimeException('Unable to filter')); - } -} diff --git a/tests/Static/Classes/Option/OptionAssertionStaticTest.php b/tests/Static/Classes/Option/OptionAssertionStaticTest.php deleted file mode 100644 index a32eafb8..00000000 --- a/tests/Static/Classes/Option/OptionAssertionStaticTest.php +++ /dev/null @@ -1,103 +0,0 @@ - $option - */ - public function testIsSomeWithIfTrueBranch(Option $option): int - { - if ($option->isSome()) { - return $option->get(); - } else { - throw new Error(); - } - } - - /** - * @param Option $option - */ - public function testIsSomeWithIfFalseBranch(Option $option): int|null - { - if ($option->isSome()) { - throw new Error(); - } else { - return $option->get(); - } - } - - /** - * @param Option $option - */ - public function testIsNoneWithTrueBranch(Option $option): int|null - { - if ($option->isNone()) { - return $option->get(); - } else { - throw new Error(); - } - } - - /** - * @param Option $option - */ - public function testIsNoneWithFalseBranch(Option $option): int - { - if ($option->isNone()) { - throw new Error(); - } else { - return $option->get(); - } - } - - /** - * @param Option $option - */ - public function testIsSomeWithTernaryTrueBranch(Option $option): int - { - return $option->isSome() - ? $option->get() - : throw new Error(); - } - - /** - * @param Option $option - */ - public function testIsSomeWithTernaryFalseBranch(Option $option): int|null - { - return $option->isSome() - ? throw new Error() - : $option->get(); - } - - /** - * @param Option $option - * @return None - */ - public function testIsNoneWithTernaryTrueBranch(Option $option): None - { - return $option->isNone() - ? call_user_func(function() use ($option) { - return $option; - }) - : throw new Error(); - } - - /** - * @param Option $option - */ - public function testIsNoneWithTernaryFalseBranch(Option $option): int - { - return $option->isNone() - ? throw new Error() - : $option->get(); - } -} diff --git a/tests/Static/Classes/Option/OptionDoNotationStaticTest.php b/tests/Static/Classes/Option/OptionDoNotationStaticTest.php deleted file mode 100644 index ad84561d..00000000 --- a/tests/Static/Classes/Option/OptionDoNotationStaticTest.php +++ /dev/null @@ -1,57 +0,0 @@ - - */ - public function testUnitReturn(): Option - { - return Option::do(function () { - yield Option::fromNullable(false); - return unit(); - }); - } - - /** - * @return Option<1|Unit> - */ - public function testUnitReturnConditionally(): Option - { - return Option::do(function () { - yield Option::fromNullable(false); - - if (rand(0, 1) === 1) { - return 1; - } - - return unit(); - }); - } - - /** - * @return Option - */ - public function testWithFilter(): Option - { - return Option::do(function() { - /** @var int $num */ - $num = yield Option::some(10); - - if ($num < 10) { - return yield Option::none(); - } - - return $num + 32; - }); - } -} diff --git a/tests/Static/Classes/Option/OptionFilterStaticTest.php b/tests/Static/Classes/Option/OptionFilterStaticTest.php deleted file mode 100644 index dce2c116..00000000 --- a/tests/Static/Classes/Option/OptionFilterStaticTest.php +++ /dev/null @@ -1,76 +0,0 @@ - - */ - public function testPreviousTypeRemainUnchanged(array $in): Option - { - return Option::fromNullable($in) - ->filter(fn($arr) => array_key_exists('a', $arr)) - ->filter(fn($arr) => array_key_exists('b', $arr)); - } - - /** - * @param Option $in - * @return Option - */ - public function testRefineNotNull(Option $in): Option - { - return $in->filter(fn(null|int $v) => null !== $v); - } - - /** - * @param Option $in - * @return Option - */ - public function testRefineShapeType(Option $in): Option - { - return $in->filter( - fn(array $v) => - array_key_exists('name', $v) && - array_key_exists('postcode', $v) && - is_int($v['postcode']) - ); - } - - /** - * @param Option $in - * @return Option - */ - public function testRefineShapeWithPsalmAssert(Option $in): Option - { - return $in->filter(fn(array $v) => $this->isValidShape($v)); - } - - /** - * @param Option $in - * @return Option - */ - public function testRefineWithFirstClassCallable(Option $in): Option - { - return $in->filter(is_int(...)); - } - - /** - * @psalm-assert-if-true Shape $shape - */ - public function isValidShape(array $shape): bool - { - return array_key_exists('name', $shape) && - array_key_exists('postcode', $shape) && - is_int($shape['postcode']); - } -} diff --git a/tests/Static/Classes/Option/OptionPluckTest.php b/tests/Static/Classes/Option/OptionPluckTest.php deleted file mode 100644 index 988e5844..00000000 --- a/tests/Static/Classes/Option/OptionPluckTest.php +++ /dev/null @@ -1,47 +0,0 @@ - $option - * @return Option - */ - public function pluckObjectProperty(Option $option): Option - { - return $option->pluck('a'); - } - - /** - * @param Option $option - */ - public function pluckUndefinedObjectProperty(Option $option): Option - { - /** @psalm-suppress UndefinedPropertyFetch */ - return $option->pluck('undefined'); - } - - /** - * @param Option $option - * @return Option - */ - public function pluckArrayProperty(Option $option): Option - { - return $option->pluck('a'); - } - - /** - * @param Option $option - */ - public function pluckUndefinedArrayProperty(Option $option): Option - { - /** @psalm-suppress PossiblyUndefinedArrayOffset */ - return $option->pluck('undefined'); - } -} diff --git a/tests/Static/Classes/Option/OptionStaticTest.php b/tests/Static/Classes/Option/OptionStaticTest.php deleted file mode 100644 index 3fa8f1e4..00000000 --- a/tests/Static/Classes/Option/OptionStaticTest.php +++ /dev/null @@ -1,74 +0,0 @@ - - */ - public function testCreation(?int $in): Option - { - return Option::fromNullable($in); - } - - /** - * @param Option $in - * @return int|null - */ - public function testGet(Option $in): ?int - { - return $in->get(); - } - - /** - * @psalm-return '1'|null - */ - public function testMap(): ?string - { - return Option::fromNullable(1) - ->map(fn(int $v) => (string) $v) - ->get(); - } - - /** - * @psalm-return '1'|null - */ - public function testFlatMap(): ?string - { - return Option::fromNullable(1) - ->flatMap(fn(int $v) => Option::fromNullable((string) $v)) - ->get(); - } - - /** - * @return ArrayList<1> - */ - public function testToArrayList(): ArrayList - { - return Option::fromNullable(1)->toArrayList(); - } - - /** - * @param Option $in - * @return Option - */ - public function testFilter(Option $in): Option - { - return $in->filter(fn($i) => is_string($i)); - } - - /** - * @param Option $in - * @return Option - */ - public function testFilterWithFirstClassCallable(Option $in): Option - { - return $in->filter(is_string(...)); - } -} diff --git a/tests/Static/Functions/Callable/ComposeStaticTest.php b/tests/Static/Functions/Callable/ComposeStaticTest.php deleted file mode 100644 index 62149fcf..00000000 --- a/tests/Static/Functions/Callable/ComposeStaticTest.php +++ /dev/null @@ -1,31 +0,0 @@ - true; - $bToC = fn(bool $b): string => (string) $b; - return compose($aToB, $bToC); - } - - /** - * @return callable(int): float - */ - public function testCompose3(): callable - { - $aToB = fn(int $a): bool => true; - $bToC = fn(bool $b): string => (string) $b; - $cTod = fn(string $c): float => (float) $c; - return compose($aToB, $bToC, $cTod); - } -} diff --git a/tests/Static/Functions/Callable/CtorStaticTest.php b/tests/Static/Functions/Callable/CtorStaticTest.php deleted file mode 100644 index bce1f294..00000000 --- a/tests/Static/Functions/Callable/CtorStaticTest.php +++ /dev/null @@ -1,55 +0,0 @@ - true, 1); - } - - /** - * @psalm-return (pure-Closure(string): true) - */ - public function testPartialLeftForClosure2(): Closure - { - return partialLeft(fn(int $a, string $b): bool => true, 1); - } - - /** - * @psalm-return (pure-Closure(): true) - */ - public function testPartialLeftForClosure1(): Closure - { - return partialLeft(fn(int $a): bool => true, 1); - } - - /** - * @psalm-return (pure-Closure(int, string): true) - */ - public function testPartialRightForClosure3(): Closure - { - return partialRight(fn(int $a, string $b, bool $c): bool => true, true); - } - - /** - * @psalm-return (pure-Closure(int): true) - */ - public function testPartialRightForClosure2(): Closure - { - return partialRight(fn(int $a, string $b) => true, ""); - } - - /** - * @psalm-return (pure-Closure(): true) - */ - public function testPartialRightForClosure1(): Closure - { - return partialRight(fn(int $a): bool => true, 1); - } -} diff --git a/tests/Static/Functions/Cast/AsArrayStaticTest.php b/tests/Static/Functions/Cast/AsArrayStaticTest.php deleted file mode 100644 index 8b459901..00000000 --- a/tests/Static/Functions/Cast/AsArrayStaticTest.php +++ /dev/null @@ -1,19 +0,0 @@ - $coll - * @return array - */ - public function testWithIterable(iterable $coll): array - { - return asArray($coll); - } -} diff --git a/tests/Static/Functions/Cast/AsListStaticTest.php b/tests/Static/Functions/Cast/AsListStaticTest.php deleted file mode 100644 index 44614e8d..00000000 --- a/tests/Static/Functions/Cast/AsListStaticTest.php +++ /dev/null @@ -1,37 +0,0 @@ - $coll - * @return list - */ - public function testWithIterable(iterable $coll): array - { - return asList($coll); - } - - /** - * @param non-empty-array $coll - * @return non-empty-list - */ - public function testWithNonEmptyArray(iterable $coll): array - { - return asList($coll); - } - - /** - * @param non-empty-list $coll - * @return non-empty-list - */ - public function testWithNonEmptyList(iterable $coll): array - { - return asList($coll); - } -} diff --git a/tests/Static/Functions/Cast/AsNonEmptyArrayStaticTest.php b/tests/Static/Functions/Cast/AsNonEmptyArrayStaticTest.php deleted file mode 100644 index 63e4176b..00000000 --- a/tests/Static/Functions/Cast/AsNonEmptyArrayStaticTest.php +++ /dev/null @@ -1,21 +0,0 @@ - $coll - * @return Option> - */ - public function testWithIterable(iterable $coll): Option - { - return asNonEmptyArray($coll); - } -} diff --git a/tests/Static/Functions/Cast/AsNonEmptyListStaticTest.php b/tests/Static/Functions/Cast/AsNonEmptyListStaticTest.php deleted file mode 100644 index e945ab87..00000000 --- a/tests/Static/Functions/Cast/AsNonEmptyListStaticTest.php +++ /dev/null @@ -1,21 +0,0 @@ - $coll - * @return Option> - */ - public function testWithIterable(iterable $coll): Option - { - return asNonEmptyList($coll); - } -} diff --git a/tests/Static/Functions/Collection/AtStaticTest.php b/tests/Static/Functions/Collection/AtStaticTest.php deleted file mode 100644 index 7d17caf0..00000000 --- a/tests/Static/Functions/Collection/AtStaticTest.php +++ /dev/null @@ -1,21 +0,0 @@ - $coll - * @return Option - */ - public function testWithArray(array $coll): Option - { - return at($coll, "key"); - } -} diff --git a/tests/Static/Functions/Collection/ChunksStaticTest.php b/tests/Static/Functions/Collection/ChunksStaticTest.php deleted file mode 100644 index abbe7037..00000000 --- a/tests/Static/Functions/Collection/ChunksStaticTest.php +++ /dev/null @@ -1,21 +0,0 @@ - $coll - * @return Generator> - */ - public function testChunksWithArray(array $coll): Generator - { - return chunks($coll, 2); - } -} diff --git a/tests/Static/Functions/Collection/FilterNotNullStaticTest.php b/tests/Static/Functions/Collection/FilterNotNullStaticTest.php deleted file mode 100644 index 8a557158..00000000 --- a/tests/Static/Functions/Collection/FilterNotNullStaticTest.php +++ /dev/null @@ -1,46 +0,0 @@ - $list - * @return list - */ - public function withList(array $list): array - { - return filterNotNull($list); - } - - /** - * @param array $array - * @return array - */ - public function withArray(array $array): array - { - return filterNotNull($array); - } - - /** - * @param array{1, 2, 3, null} $list - * @return list<1|2|3> - */ - public function withListLiterals(array $list): array - { - return filterNotNull($list); - } - - /** - * @param array{fst: int, snd: int, trd: int|null} $shape - * @return array{fst: int, snd: int, trd?: int} - */ - public function withShape(array $shape): array - { - return filterNotNull($shape); - } -} diff --git a/tests/Static/Functions/Collection/FilterStaticTest.php b/tests/Static/Functions/Collection/FilterStaticTest.php deleted file mode 100644 index 3fcb284f..00000000 --- a/tests/Static/Functions/Collection/FilterStaticTest.php +++ /dev/null @@ -1,137 +0,0 @@ - $list - * @return list - */ - public function testPreserveListType(array $list): array - { - return filter($list, fn($i) => $i > 42); - } - - /** - * @param non-empty-list $list - * @return list - */ - public function testPreserveListTypeFromNonEmptyList(array $list): array - { - return filter($list, fn($i) => $i > 42); - } - - /** - * @param array $coll - * @return array - */ - public function testRefineNotNull(array $coll): array - { - return filter( - $coll, - fn(null|int $v) => null !== $v - ); - } - - /** - * @param array $coll - * @return array - */ - public function testRefineShapeType(array $coll): array - { - return filter( - $coll, - fn(array $v) => - array_key_exists('name', $v) && - array_key_exists('postcode', $v) && - is_int($v['postcode']) - ); - } - - /** - * @param array $coll - * @return array - */ - public function testRefineShapeWithPsalmAssert(array $coll): array - { - return filter( - $coll, - fn(array $v) => $this->isValidShape($v) - ); - } - - /** - * @psalm-assert-if-true Shape $shape - */ - public function isValidShape(array $shape): bool - { - return array_key_exists('name', $shape) && - array_key_exists('postcode', $shape) && - is_int($shape['postcode']); - } - - /** - * @psalm-assert-if-true Shape $shape - */ - public function isValidShapeStatic(array $shape): bool - { - return array_key_exists('name', $shape) && - array_key_exists('postcode', $shape) && - is_int($shape['postcode']); - } - - /** - * @param array $coll - * @return array - */ - public function testWithFirstClassCallableMethod(array $coll): array - { - return filter($coll, $this->isValidShape(...)); - } - - /** - * @param array $coll - * @return array - */ - public function testWithFirstClassCallableStaticMethod(array $coll): array - { - return filter($coll, self::isValidShape(...)); - } - - /** - * @param array $coll - * @return array - */ - public function testWithFirstClassCallableFunction(array $coll): array - { - return filter($coll, is_int(...)); - } - - /** - * @psalm-assert-if-true int $key - * @psalm-assert-if-true string $val - */ - public function assertKV(mixed $key, mixed $val): bool - { - return is_int($key) && is_string($val); - } - - /** - * @param array $coll - * @return array - */ - public function testFilterKVWithFirstClassCallable(array $coll): array - { - return filterKV($coll, $this->assertKV(...)); - } -} diff --git a/tests/Static/Functions/Collection/FirstStaticTest.php b/tests/Static/Functions/Collection/FirstStaticTest.php deleted file mode 100644 index 0adda0f5..00000000 --- a/tests/Static/Functions/Collection/FirstStaticTest.php +++ /dev/null @@ -1,21 +0,0 @@ - $coll - * @return Option - */ - public function testWithArray(array $coll): Option - { - return first($coll, fn(int $v) => true); - } -} diff --git a/tests/Static/Functions/Collection/FlatMapStaticTest.php b/tests/Static/Functions/Collection/FlatMapStaticTest.php deleted file mode 100644 index 81e2aee1..00000000 --- a/tests/Static/Functions/Collection/FlatMapStaticTest.php +++ /dev/null @@ -1,22 +0,0 @@ - $coll - * @return list - */ - public function testWithArray(array $coll): array - { - return flatMap( - $coll, - fn(int $v) => [$v - 1, $v + 1] - ); - } -} diff --git a/tests/Static/Functions/Collection/FoldStaticTest.php b/tests/Static/Functions/Collection/FoldStaticTest.php deleted file mode 100644 index 01b9aa06..00000000 --- a/tests/Static/Functions/Collection/FoldStaticTest.php +++ /dev/null @@ -1,19 +0,0 @@ - $coll - * @return int - */ - public function testWithArray(array $coll): int - { - return fold(0, $coll)(fn($acc, $v) => $acc + $v); - } -} diff --git a/tests/Static/Functions/Collection/GroupStaticTest.php b/tests/Static/Functions/Collection/GroupStaticTest.php deleted file mode 100644 index 1e931bc7..00000000 --- a/tests/Static/Functions/Collection/GroupStaticTest.php +++ /dev/null @@ -1,96 +0,0 @@ - $coll - * @return array> - */ - public function testWithArray(array $coll): array - { - return groupBy( - $coll, - fn(int $v) => "{$v}-10" - ); - } - - /** - * @param ArrayList $coll - * @return array> - */ - public function testWithArrayList(ArrayList $coll): array - { - return groupBy( - $coll, - fn(int $v) => "{$v}-10" - ); - } - - /** - * @param non-empty-array $coll - * @return non-empty-array> - */ - public function testWithNonEmptyArray(array $coll): array - { - return groupBy( - $coll, - fn(int $v) => "{$v}-10" - ); - } - - /** - * @param non-empty-list $coll - * @return non-empty-array> - */ - public function testWithNonEmptyList(array $coll): array - { - return groupBy( - $coll, - fn(int $v) => "{$v}-10" - ); - } - - /** - * @param list $coll - * @return array> - */ - public function testWithListInferGroupKey(array $coll): array - { - return groupBy( - $coll, - fn(string $value) => $value . "10" - ); - } - - /** - * @param array $coll - * @return array> - */ - public function testWithArrayInferGroupKey(array $coll): array - { - return groupBy( - $coll, - fn(string $value) => $value . "10" - ); - } - - /** - * @psalm-type Alias = string - * @param array $coll - * @return array> - */ - public function testWithArrayAndGroupKeyAsTypeAlias(array $coll): array - { - return groupBy( - $coll, - fn(int $value) => $value - ); - } -} diff --git a/tests/Static/Functions/Collection/HeadStaticTest.php b/tests/Static/Functions/Collection/HeadStaticTest.php deleted file mode 100644 index a2484f7b..00000000 --- a/tests/Static/Functions/Collection/HeadStaticTest.php +++ /dev/null @@ -1,21 +0,0 @@ - $coll - * @return Option - */ - public function testWithArray(array $coll): Option - { - return head($coll); - } -} diff --git a/tests/Static/Functions/Collection/KeysStaticTest.php b/tests/Static/Functions/Collection/KeysStaticTest.php deleted file mode 100644 index f4256d82..00000000 --- a/tests/Static/Functions/Collection/KeysStaticTest.php +++ /dev/null @@ -1,40 +0,0 @@ -, - * non-empty-array, - * array, - * non-empty-array, - * array, - * non-empty-array - * } $in - * @psalm-return array{ - * list, - * non-empty-list, - * list, - * non-empty-list, - * list, - * non-empty-list, - * } - */ - public function testArrayToKeys(array $in): array - { - return [ - keys($in[0]), - keys($in[1]), - keys($in[2]), - keys($in[3]), - keys($in[4]), - keys($in[5]), - ]; - } -} diff --git a/tests/Static/Functions/Collection/LastStaticTest.php b/tests/Static/Functions/Collection/LastStaticTest.php deleted file mode 100644 index 5354c1d2..00000000 --- a/tests/Static/Functions/Collection/LastStaticTest.php +++ /dev/null @@ -1,20 +0,0 @@ - $coll - * @return Option - */ - public function testWithArray(array $coll): Option - { - return last($coll); - } -} diff --git a/tests/Static/Functions/Collection/MapStaticTest.php b/tests/Static/Functions/Collection/MapStaticTest.php deleted file mode 100644 index 6a4b3f03..00000000 --- a/tests/Static/Functions/Collection/MapStaticTest.php +++ /dev/null @@ -1,83 +0,0 @@ - $coll - * @return list - */ - public function testMapListToList(array $coll): array - { - return map($coll, fn(int $value) => (string) $value); - } - - /** - * @param non-empty-list $coll - * @return non-empty-list - */ - public function testMapNonEmptyListToNonEmptyList(array $coll): array - { - return map($coll, fn(int $value) => (string) $value); - } - - /** - * @param array $coll - * @return array - */ - public function testMapArrayToArray(array $coll): array - { - return map($coll, fn(int $value) => (string) $value); - } - - /** - * @param non-empty-array $coll - * @return non-empty-array - */ - public function testMapNonEmptyArrayToNonEmptyToArray(array $coll): array - { - return map($coll, fn(int $value) => (string) $value); - } - - /** - * @param list $coll - * @return list - */ - public function testMapWithKeyListToList(array $coll): array - { - return mapKV($coll, fn(int $key, int $value) => "{$key}-{$value}"); - } - - /** - * @param non-empty-list $coll - * @return non-empty-list - */ - public function testMapWithKeyNonEmptyListToNonEmptyList(array $coll): array - { - return mapKV($coll, fn(int $key, int $value) => "{$key}-{$value}"); - } - - /** - * @param array $coll - * @return array - */ - public function testMapWithKeyArrayToArray(array $coll): array - { - return mapKV($coll, fn(string $key, int $value) => "{$key}-{$value}"); - } - - /** - * @param non-empty-array $coll - * @return non-empty-array - */ - public function testMapWithKeyNonEmptyArrayToNonEmptyToArray(array $coll): array - { - return mapKV($coll, fn(string $key, int $value) => "{$key}-{$value}"); - } -} diff --git a/tests/Static/Functions/Collection/PartitionStaticTest.php b/tests/Static/Functions/Collection/PartitionStaticTest.php deleted file mode 100644 index d3de233f..00000000 --- a/tests/Static/Functions/Collection/PartitionStaticTest.php +++ /dev/null @@ -1,45 +0,0 @@ - $list - * @return array{list, list} - */ - public function testPartitionWithOnePredicate(array $list): array - { - return partitionT($list, fn(int $v) => $v % 2 === 0); - } - - /** - * @param list $nums - * @return array{list, list, list} - */ - public function testPartitionWithTwoPredicates(array $nums): array - { - return partitionT( - $nums, - fn(int $v) => $v % 2 === 0, - fn(int $v) => $v % 2 === 1, - ); - } - - /** - * @param list $list - * @return array{list, list, list} - */ - public function testExhaustiveInference(array $list): array - { - return partitionT($list, fn($i) => $i instanceof Foo, fn($i) => $i instanceof Bar); - } -} diff --git a/tests/Static/Functions/Collection/PluckStaticTest.php b/tests/Static/Functions/Collection/PluckStaticTest.php deleted file mode 100644 index 2ebec9f0..00000000 --- a/tests/Static/Functions/Collection/PluckStaticTest.php +++ /dev/null @@ -1,75 +0,0 @@ - $list - * @return list - */ - public function testListShape(array $list): array - { - return pluck($list, 'name'); - } - - /** - * @param non-empty-list $list - * @return non-empty-list - */ - public function testNonEmptyListShape(array $list): array - { - return pluck($list, 'name'); - } - - /** - * @param array $list - * @return array - */ - public function testArrayShape(array $list): array - { - return pluck($list, 'name'); - } - - /** - * @param non-empty-array $array - * @return non-empty-array - */ - public function testNonEmptyArrayShape(array $array): array - { - return pluck($array, 'name'); - } - - /** - * @param list $list - * @return list - */ - public function testObjectList(array $list): array - { - return pluck($list, 'a'); - } - - /** - * @param list $list - */ - public function testObjectListUndefinedPropertyFetch(array $list): array - { - /** @psalm-suppress UndefinedPropertyFetch */ - return pluck($list, 'undefined'); - } - - /** - * @param list $list - */ - public function testShapeListUndefinedArrayKey(array $list): array - { - /** @psalm-suppress PossiblyUndefinedArrayOffset */ - return pluck($list, 'undefined'); - } -} diff --git a/tests/Static/Functions/Collection/PopStaticTest.php b/tests/Static/Functions/Collection/PopStaticTest.php deleted file mode 100644 index 779ea7ad..00000000 --- a/tests/Static/Functions/Collection/PopStaticTest.php +++ /dev/null @@ -1,21 +0,0 @@ - $coll - * @return Option}> - */ - public function testWithArray(array $coll): Option - { - return pop($coll); - } -} diff --git a/tests/Static/Functions/Collection/ReindexStaticTest.php b/tests/Static/Functions/Collection/ReindexStaticTest.php deleted file mode 100644 index 8bc2a95c..00000000 --- a/tests/Static/Functions/Collection/ReindexStaticTest.php +++ /dev/null @@ -1,65 +0,0 @@ - $coll - * @return array - */ - public function testReindexArray(array $coll): array - { - return reindex($coll, fn(int $v) => "key-{$v}"); - } - - /** - * @param non-empty-array $coll - * @return non-empty-array - */ - public function testReindexNonEmptyArray(array $coll): array - { - return reindex($coll, fn(int $v) => "key-{$v}"); - } - - /** - * @param non-empty-list $coll - * @return non-empty-array - */ - public function testReindexNonEmptyList(array $coll): array - { - return reindex($coll, fn(int $v) => "key-{$v}"); - } - - /** - * @param array $coll - * @return array - */ - public function testReindexKVArray(array $coll): array - { - return reindexKV($coll, fn(string $k, int $v) => "key-{$k}-{$v}"); - } - - /** - * @param non-empty-array $coll - * @return non-empty-array - */ - public function testReindexKVNonEmptyArray(array $coll): array - { - return reindexKV($coll, fn(string $k, int $v) => "key-{$k}-{$v}"); - } - - /** - * @param non-empty-list $coll - * @return non-empty-array - */ - public function testReindexKVNonEmptyList(array $coll): array - { - return reindexKV($coll, fn(int $k, int $v) => "key-{$k}-{$v}"); - } -} diff --git a/tests/Static/Functions/Collection/ReverseStaticTest.php b/tests/Static/Functions/Collection/ReverseStaticTest.php deleted file mode 100644 index 8813e798..00000000 --- a/tests/Static/Functions/Collection/ReverseStaticTest.php +++ /dev/null @@ -1,46 +0,0 @@ - $coll - * @return array - */ - public function testWithArray(array $coll): array - { - return reverse($coll); - } - - /** - * @param non-empty-array $coll - * @return non-empty-array - */ - public function testWithNonEmptyArray(array $coll): array - { - return reverse($coll); - } - - /** - * @param list $coll - * @return list - */ - public function testWithList(array $coll): array - { - return reverse($coll); - } - - /** - * @param non-empty-list $coll - * @return non-empty-list - */ - public function testWithNonEmptyList(array $coll): array - { - return reverse($coll); - } -} diff --git a/tests/Static/Functions/Collection/SecondStaticTest.php b/tests/Static/Functions/Collection/SecondStaticTest.php deleted file mode 100644 index 98af1adf..00000000 --- a/tests/Static/Functions/Collection/SecondStaticTest.php +++ /dev/null @@ -1,21 +0,0 @@ - $coll - * @return Option - */ - public function testWithArray(array $coll): Option - { - return second($coll); - } -} diff --git a/tests/Static/Functions/Collection/ShiftStaticTest.php b/tests/Static/Functions/Collection/ShiftStaticTest.php deleted file mode 100644 index 2dd2f46f..00000000 --- a/tests/Static/Functions/Collection/ShiftStaticTest.php +++ /dev/null @@ -1,21 +0,0 @@ - $coll - * @return Option}> - */ - public function testWithArray(array $coll): Option - { - return shift($coll); - } -} diff --git a/tests/Static/Functions/Collection/TailStaticTest.php b/tests/Static/Functions/Collection/TailStaticTest.php deleted file mode 100644 index e8f558ae..00000000 --- a/tests/Static/Functions/Collection/TailStaticTest.php +++ /dev/null @@ -1,19 +0,0 @@ - $coll - * @return list - */ - public function testWithArray(array $coll): array - { - return tail($coll); - } -} diff --git a/tests/Static/Functions/Collection/ZipStaticTest.php b/tests/Static/Functions/Collection/ZipStaticTest.php deleted file mode 100644 index 7e207be7..00000000 --- a/tests/Static/Functions/Collection/ZipStaticTest.php +++ /dev/null @@ -1,20 +0,0 @@ - $coll1 - * @param iterable $coll2 - * @return list - */ - public function testWithIterable(array $coll1, iterable $coll2): array - { - return zip($coll1, $coll2); - } -} diff --git a/tests/Static/Functions/Evidence/ProveCollectionStaticTest.php b/tests/Static/Functions/Evidence/ProveCollectionStaticTest.php deleted file mode 100644 index ea530dca..00000000 --- a/tests/Static/Functions/Evidence/ProveCollectionStaticTest.php +++ /dev/null @@ -1,129 +0,0 @@ -> - */ - public function proveListFromMixed(mixed $value): Option - { - return proveList($value); - } - - /** - * @param array $value - * @return Option> - */ - public function proveListFromArrayWithUnknownKey(array $value): Option - { - return proveList($value); - } - - /** - * @return Option> - */ - public function proveListFromMixedByCallback(mixed $value): Option - { - return proveList($value, proveInt(...)); - } - - /** - * @return Option> - */ - public function proveNonEmptyListFromMixed(mixed $value): Option - { - return proveNonEmptyList($value); - } - - /** - * @param iterable $value - * @return Option> - */ - public function proveNonEmptyListFromIterableWithUnknownKey(iterable $value): Option - { - return proveNonEmptyList($value); - } - - /** - * @return Option> - */ - public function proveNonEmptyListFromMixedByCallback(mixed $value): Option - { - return proveNonEmptyList($value, proveInt(...)); - } - - /** - * @return Option> - */ - public function proveArrayFromMixed(mixed $value): Option - { - return proveArray($value); - } - - /** - * @param iterable $value - * @return Option> - */ - public function proveArrayFromIterableWithUnknownKey(mixed $value): Option - { - return proveArray($value); - } - - /** - * @param iterable $value - * @return Option> - */ - public function proveArrayFromIterableWithUnknownValue(iterable $value): Option - { - return proveArray($value); - } - - /** - * @param iterable $value - * @return Option> - */ - public function proveArrayFromIterable(iterable $value): Option - { - return proveArray($value); - } - - /** - * @param iterable $value - * @return Option> - */ - public function proveArrayFromIterableWithUnknownKeyByCallback(iterable $value): Option - { - return proveArray($value, kType: proveString(...)); - } - - /** - * @param iterable $value - * @return Option> - */ - public function proveArrayFromIterableWithUnknownValueByCallback(iterable $value): Option - { - return proveArray($value, vType: proveInt(...)); - } - - /** - * @return Option> - */ - public function proveArrayFromMixedByCallback(mixed $value): Option - { - return proveArray($value, - kType: proveString(...), - vType: proveInt(...)); - } -} diff --git a/tests/Static/Functions/Evidence/ProveStaticTest.php b/tests/Static/Functions/Evidence/ProveStaticTest.php deleted file mode 100644 index 64633e50..00000000 --- a/tests/Static/Functions/Evidence/ProveStaticTest.php +++ /dev/null @@ -1,30 +0,0 @@ - - */ - public function testProveOf(): Option - { - return proveOf(new Foo(1), Foo::class); - } - - /** - * @return Option - */ - public function testProveOfWithMultipleClasses(): Option - { - return proveOf(new Foo(1), [Foo::class, Bar::class]); - } -} diff --git a/tests/Static/Functions/Evidence/ProveStringStaticTest.php b/tests/Static/Functions/Evidence/ProveStringStaticTest.php deleted file mode 100644 index c81bd728..00000000 --- a/tests/Static/Functions/Evidence/ProveStringStaticTest.php +++ /dev/null @@ -1,18 +0,0 @@ -get(); - } -} diff --git a/tests/Static/Functions/Evidence/ProveTrueStaticTest.php b/tests/Static/Functions/Evidence/ProveTrueStaticTest.php deleted file mode 100644 index 8e226f41..00000000 --- a/tests/Static/Functions/Evidence/ProveTrueStaticTest.php +++ /dev/null @@ -1,65 +0,0 @@ - - */ - public function testAssertNotNullWithOption(?int $number): Option - { - return Option::do(function () use ($number) { - yield proveTrue(null !== $number); - - return $number; - }); - } - - /** - * @param array{name?: string} $hasName - * @return Option - */ - public function testAssertKeyExistsWithOption(array $hasName): Option - { - return Option::do(function() use ($hasName) { - yield proveTrue(array_key_exists("name", $hasName)); - - return $hasName["name"]; - }); - } - - /** - * @param int|null $number - * @return Either<"not_number", int> - */ - public function testAssertNotNullWithEither(?int $number): Either - { - return Either::do(function() use ($number) { - yield proveTrue(null !== $number)->toRight(fn() => "not_number"); - - return $number; - }); - } - - /** - * @param array{name?: string} $hasName - * @return Either<"no_prop", string> - */ - public function testAssertKeyExistsWithEither(array $hasName): Either - { - return Either::do(function() use ($hasName) { - yield proveTrue(array_key_exists("name", $hasName))->toRight(fn() => "no_prop"); - - return $hasName["name"]; - }); - } -} diff --git a/tests/Static/Functions/Evidence/ProveUnionStaticTest.php b/tests/Static/Functions/Evidence/ProveUnionStaticTest.php deleted file mode 100644 index a0f72882..00000000 --- a/tests/Static/Functions/Evidence/ProveUnionStaticTest.php +++ /dev/null @@ -1,46 +0,0 @@ - - */ - public function proveUnion(mixed $value): Option - { - return proveUnion($value, [ - proveInt(...), - proveString(...), - ]); - } - - /** - * @return Option - */ - public function union(mixed $value): Option - { - return union([ - proveInt(...), - proveString(...), - ])($value); - } - - /** - * @return Option - */ - public function unionT(mixed $value): Option - { - return unionT(proveInt(...), proveString(...))($value); - } -} diff --git a/tests/Static/Functions/PanicStaticTest.php b/tests/Static/Functions/PanicStaticTest.php deleted file mode 100644 index cea07d53..00000000 --- a/tests/Static/Functions/PanicStaticTest.php +++ /dev/null @@ -1,29 +0,0 @@ - $maybeInt - */ - public function withOption(Option $maybeInt): int - { - return $maybeInt->getOrCall(fn() => panic('is not int')); - } - - /** - * @param Either $maybeInt - */ - public function withEither(Either $maybeInt): int - { - return $maybeInt->getOrCall(fn() => panic('is not int')); - } -} diff --git a/tests/Static/Interfaces/Map/MapStaticTest.php b/tests/Static/Interfaces/Map/MapStaticTest.php deleted file mode 100644 index a1a584c0..00000000 --- a/tests/Static/Interfaces/Map/MapStaticTest.php +++ /dev/null @@ -1,32 +0,0 @@ - $coll - * @return array - */ - public function testToAssocArrayWithValidInput(Map $coll): array - { - return $coll->toArray(); - } - - /** - * @param Seq $coll - * @return array - */ - public function testToAssocArrayFromSeq(Seq $coll): array - { - return $coll - ->toHashMap() - ->toArray(); - } -} diff --git a/tests/Static/Interfaces/Seq/ToMergedArrayTest.php b/tests/Static/Interfaces/Seq/ToMergedArrayTest.php deleted file mode 100644 index 6be4e6d2..00000000 --- a/tests/Static/Interfaces/Seq/ToMergedArrayTest.php +++ /dev/null @@ -1,65 +0,0 @@ -> $list - * @return list - */ - public function asList(ArrayList $list): array - { - return $list->toMergedArray(); - } - - /** - * @param ArrayList> $list - * @return list - */ - public function asListFromNonEmptyList(ArrayList $list): array - { - return $list->toMergedArray(); - } - - /** - * @param ArrayList> $list - * @return array - */ - public function asArray(ArrayList $list): array - { - return $list->toMergedArray(); - } - - /** - * @param ArrayList> $list - * @return array - */ - public function asArrayFromNonEmptyArray(ArrayList $list): array - { - return $list->toMergedArray(); - } - - /** - * @param NonEmptyArrayList> $list - * @return non-empty-list - */ - public function asNonEmptyList(NonEmptyArrayList $list): array - { - return $list->toNonEmptyMergedArray(); - } - - /** - * @param NonEmptyArrayList> $list - * @return non-empty-array - */ - public function asNonEmptyArray(NonEmptyArrayList $list): array - { - return $list->toNonEmptyMergedArray(); - } -} diff --git a/tests/Static/Plugin/CollectionFilterPluginStaticTest.php b/tests/Static/Plugin/CollectionFilterPluginStaticTest.php deleted file mode 100644 index 3b2d25bc..00000000 --- a/tests/Static/Plugin/CollectionFilterPluginStaticTest.php +++ /dev/null @@ -1,198 +0,0 @@ -, - * LinkedList: LinkedList<1|null|2>, - * HashSet: HashSet<1|null|2>, - * NonEmptyArrayList: NonEmptyArrayList<1|null|2>, - * NonEmptyLinkedList: NonEmptyLinkedList<1|null|2>, - * NonEmptyHashSet: NonEmptyHashSet<1|null|2>, - * Seq: Seq<1|null|2>, - * Set: Set<1|null|2>, - * NonEmptySeq: NonEmptySeq<1|null|2>, - * NonEmptySet: NonEmptySet<1|null|2>, - * Map: Map, - * HashMap: HashMap, - * Stream: Stream<1|null|2>, - * } $in - * - * @psalm-return array{ - * ArrayList: ArrayList<1|2>, - * LinkedList: LinkedList<1|2>, - * HashSet: HashSet<1|2>, - * NonEmptyArrayList: ArrayList<1|2>, - * NonEmptyLinkedList: LinkedList<1|2>, - * NonEmptyHashSet: HashSet<1|2>, - * Seq: Seq<1|2>, - * Set: Set<1|2>, - * NonEmptySeq: Seq<1|2>, - * NonEmptySet: Set<1|2>, - * Map: Map, - * HashMap: HashMap, - * Stream: Stream<1|2>, - * } - */ - public function testFilter(array $in): array - { - return [ - 'ArrayList' => $in['ArrayList']->filter(fn($e) => null !== $e), - 'LinkedList' => $in['LinkedList']->filter(fn($e) => null !== $e), - 'HashSet' => $in['HashSet']->filter(fn($e) => null !== $e), - 'NonEmptyArrayList' => $in['NonEmptyArrayList']->filter(fn($e) => null !== $e), - 'NonEmptyLinkedList' => $in['NonEmptyLinkedList']->filter(fn($e) => null !== $e), - 'NonEmptyHashSet' => $in['NonEmptyHashSet']->filter(fn($e) => null !== $e), - 'Seq' => $in['Seq']->filter(fn($e) => null !== $e), - 'Set' => $in['Set']->filter(fn($e) => null !== $e), - 'NonEmptySeq' => $in['NonEmptySeq']->filter(fn($e) => null !== $e), - 'NonEmptySet' => $in['NonEmptySet']->filter(fn($e) => null !== $e), - 'Map' => $in['Map']->filter(fn($e) => null !== $e), - 'HashMap' => $in['HashMap']->filter(fn($e) => null !== $e), - 'Stream' => $in['Stream']->filter(fn($e) => null !== $e), - ]; - } - - /** - * @psalm-param array{ - * ArrayList: ArrayList<1|null|2>, - * LinkedList: LinkedList<1|null|2>, - * HashSet: HashSet<1|null|2>, - * NonEmptyArrayList: NonEmptyArrayList<1|null|2>, - * NonEmptyLinkedList: NonEmptyLinkedList<1|null|2>, - * NonEmptyHashSet: NonEmptyHashSet<1|null|2>, - * Seq: Seq<1|null|2>, - * Set: Set<1|null|2>, - * NonEmptySeq: NonEmptySeq<1|null|2>, - * NonEmptySet: NonEmptySet<1|null|2>, - * Map: Map, - * HashMap: HashMap, - * Stream: Stream<1|null|2>, - * } $in - * - * @psalm-return array{ - * ArrayList<1|2>, - * LinkedList<1|2>, - * HashSet<1|2>, - * ArrayList<1|2>, - * LinkedList<1|2>, - * HashSet<1|2>, - * Seq<1|2>, - * Set<1|2>, - * Seq<1|2>, - * Set<1|2>, - * Map, - * HashMap, - * Stream<1|2>, - * } - */ - public function testFilterFirstClassCallable(array $in): array - { - return [ - $in['ArrayList']->filter(is_int(...)), - $in['LinkedList']->filter(is_int(...)), - $in['HashSet']->filter(is_int(...)), - $in['NonEmptyArrayList']->filter(is_int(...)), - $in['NonEmptyLinkedList']->filter(is_int(...)), - $in['NonEmptyHashSet']->filter(is_int(...)), - $in['Seq']->filter(is_int(...)), - $in['Set']->filter(is_int(...)), - $in['NonEmptySeq']->filter(is_int(...)), - $in['NonEmptySet']->filter(is_int(...)), - $in['Map']->filter(is_int(...)), - $in['HashMap']->filter(is_int(...)), - $in['Stream']->filter(is_int(...)), - ]; - } - - /** - * @psalm-param array{ - * ArrayList: ArrayList, - * LinkedList: LinkedList, - * NonEmptyArrayList: NonEmptyArrayList, - * NonEmptyLinkedList: NonEmptyLinkedList, - * Seq: Seq, - * NonEmptySeq: NonEmptySeq, - * Set: Set, - * NonEmptySet: NonEmptySet, - * Map: Map, - * HashMap: HashMap - * } $in - * - * @psalm-return array{ - * Map: Map, - * HashMap: HashMap, - * } - */ - public function testFilterKV(array $in): array - { - return [ - 'Map' => $in['Map']->filterKV(fn($k, $v) => is_string($k) && null !== $v), - 'HashMap' => $in['HashMap']->filterKV(fn($k, $v) => is_string($k) && null !== $v), - ]; - } - - /** - * @psalm-param array{ - * ArrayList: ArrayList<1|null|2>, - * LinkedList: LinkedList<1|null|2>, - * HashSet: HashSet<1|null|2>, - * NonEmptyArrayList: NonEmptyArrayList<1|null|2>, - * NonEmptyLinkedList: NonEmptyLinkedList<1|null|2>, - * NonEmptyHashSet: NonEmptyHashSet<1|null|2>, - * Seq: Seq<1|null|2>, - * Set: Set<1|null|2>, - * NonEmptySeq: NonEmptySeq<1|null|2>, - * NonEmptySet: NonEmptySet<1|null|2>, - * Stream: Stream<1|null|2>, - * } $in - * @psalm-return array{ - * ArrayList: ArrayList<1|2>, - * LinkedList: LinkedList<1|2>, - * HashSet: HashSet<1|2>, - * NonEmptyArrayList: ArrayList<1|2>, - * NonEmptyLinkedList: LinkedList<1|2>, - * NonEmptyHashSet: HashSet<1|2>, - * Seq: Seq<1|2>, - * Set: Set<1|2>, - * NonEmptySeq: Seq<1|2>, - * NonEmptySet: Set<1|2>, - * Stream: Stream<1|2>, - * } - */ - public function testFilterNotNull(array $in): array - { - return [ - 'ArrayList' => $in['ArrayList']->filterNotNull(), - 'LinkedList' => $in['LinkedList']->filterNotNull(), - 'HashSet' => $in['HashSet']->filterNotNull(), - 'NonEmptyArrayList' => $in['NonEmptyArrayList']->filterNotNull(), - 'NonEmptyLinkedList' => $in['NonEmptyLinkedList']->filterNotNull(), - 'NonEmptyHashSet' => $in['NonEmptyHashSet']->filterNotNull(), - 'Seq' => $in['Seq']->filterNotNull(), - 'Set' => $in['Set']->filterNotNull(), - 'NonEmptySeq' => $in['NonEmptySeq']->filterNotNull(), - 'NonEmptySet' => $in['NonEmptySet']->filterNotNull(), - 'Stream' => $in['Stream']->filterNotNull(), - ]; - } -} diff --git a/tests/Static/Plugin/CollectionFoldStaticTest.php b/tests/Static/Plugin/CollectionFoldStaticTest.php deleted file mode 100644 index e52546f7..00000000 --- a/tests/Static/Plugin/CollectionFoldStaticTest.php +++ /dev/null @@ -1,102 +0,0 @@ -fold(0)(fn($sum, $num) => $sum + $num); - } - - public function concatAllStrings(): string - { - return ArrayList::collect(['fst', 'snd', 'thr']) - ->fold('')(fn($concatenated, $string) => $concatenated . $string); - } - - public function countingElements(): int - { - return ArrayList::collect([1, 2, 3]) - ->fold(0)(fn($count) => $count + 1); - } - - public function findingMaxValue(): int - { - return ArrayList::collect([1, 2, 3]) - ->fold(PHP_INT_MIN)(fn($min, $num) => max($min, $num)); - } - - public function findingMinValue(): int - { - return ArrayList::collect([1, 2, 3]) - ->fold(PHP_INT_MAX)(fn($min, $num) => min($min, $num)); - } - - /** - * @return ArrayList - */ - public function mapEachValueToString(): ArrayList - { - return ArrayList::collect([1, 2, 3]) - ->fold(ArrayList::empty())(fn($list, $num) => $list->appended((string) $num)); - } - - /** - * @return ArrayList - */ - public function mergeAllChunksIntoOneArrayList(): ArrayList - { - return ArrayList::collect([[1, 2, 3], [4, 5, 6], [7, 8, 9]]) - ->fold(ArrayList::empty())(fn($merged, $chunk) => $merged->appendedAll($chunk)); - } - - /** - * @return list - */ - public function mergeAllChunksIntoOneList(): array - { - return ArrayList::collect([[1, 2, 3], [4, 5, 6], [7, 8, 9]]) - ->fold([])(fn($merged, $chunk) => [...$merged, ...$chunk]); - } - - /** - * @return list - */ - public function mergeAllChunksIntoOneNativeArray(): array - { - return ArrayList::collect([[1, 2, 3], [4, 5, 6], [7, 8, 9]]) - ->fold([])(fn($merged, $chunk) => [...$merged, ...$chunk]); - } - - public function calculatingFactorial(): int - { - return ArrayList::collect([1, 2, 3, 4, 5]) - ->fold(1)(fn($x, $y) => $x * $y); - } - - /** - * @param callable(int): bool $predicate - */ - public function everyViaFold(callable $predicate): bool - { - return ArrayList::collect([1, 2, 3, 4, 5]) - ->fold(true)(fn($cond, $num) => $cond && $predicate($num)); - } - - /** - * @return ArrayList - */ - public function reverse(): ArrayList - { - return ArrayList::collect([1, 2, 3, 4, 5]) - ->fold(ArrayList::empty())(fn($reversed, $current) => $reversed->prepended($current)); - } -} diff --git a/tests/Static/Plugin/MapTapNPluginStaticTest.php b/tests/Static/Plugin/MapTapNPluginStaticTest.php deleted file mode 100644 index ee81852f..00000000 --- a/tests/Static/Plugin/MapTapNPluginStaticTest.php +++ /dev/null @@ -1,303 +0,0 @@ - - */ - public static function methodWithVariadicParam(int $a, int $b, int $c, int ...$rest): array - { - return [$a, $b, $c, ...$rest]; - } - - /** - * @param Option $args - * @return Option - */ - public static function testPutAllRequiredArgumentsToRegularMethod(Option $args): Option - { - return $args->mapN(self::methodWithRegularParams(...)); - } - - /** - * @param Option $args - * @return Option - */ - public static function testPutMoreThanRequiredArgumentToRegularMethod(Option $args): Option - { - return $args->mapN(self::methodWithRegularParams(...)); - } - - /** - * @param Option $args - * @return Option - */ - public static function testPutInvalidArgumentToRegularMethod(Option $args): Option - { - /** @psalm-suppress IfThisIsMismatch */ - return $args->mapN(self::methodWithRegularParams(...)); - } - - /** - * @param Option $args - * @return Option - */ - public static function testPutOneArgumentInsteadThreeToRegularMethod(Option $args): Option - { - /** @psalm-suppress IfThisIsMismatch */ - return $args->mapN(self::methodWithRegularParams(...)); - } - - /** - * @param Option $args - * @return Option - */ - public static function testPutTwoArgumentInsteadThreeToRegularMethod(Option $args): Option - { - /** @psalm-suppress IfThisIsMismatch */ - return $args->mapN(self::methodWithRegularParams(...)); - } - - /** - * @param Option $args - * @return Option - */ - public static function testPutOneRequiredAndTwoOptionalArgumentsToMethodWithOptionalParams(Option $args): Option - { - return $args->mapN(self::methodWithOptionalParams(...)); - } - - /** - * @param Option $args - * @return Option - */ - public static function testPutOneRequiredAndOneOptionalArgumentsToMethodWithOptionalParams(Option $args): Option - { - return $args->mapN(self::methodWithOptionalParams(...)); - } - - /** - * @param Option $args - * @return Option - */ - public static function testPutOneRequiredAndNoOptionalArgumentsToMethodWithOptionalParams(Option $args): Option - { - return $args->mapN(self::methodWithOptionalParams(...)); - } - - /** - * @param Option $args - * @return Option - */ - public static function testPutAllArgumentsAndOneUnnecessaryArgumentToMethodWithOptionalParams(Option $args): Option - { - return $args->mapN(self::methodWithOptionalParams(...)); - } - /** - * @param Option $args - * @return Option - */ - public static function testPutAllArgumentsAndTwoUnnecessaryArgumentToMethodWithOptionalParams(Option $args): Option - { - return $args->mapN(self::methodWithOptionalParams(...)); - } - - /** - * @param Option $args - * @return Option - */ - public static function testPutOneInvalidArgumentToMethodWithOptionalParams(Option $args): Option - { - /** @psalm-suppress IfThisIsMismatch */ - return $args->mapN(self::methodWithOptionalParams(...)); - } - - /** - * @param Option $args - * @return Option - */ - public static function testPutTwoInvalidArgumentToMethodWithOptionalParams(Option $args): Option - { - /** @psalm-suppress IfThisIsMismatch */ - return $args->mapN(self::methodWithOptionalParams(...)); - } - - /** - * @param Option $args - * @return Option - */ - public static function testTemplateIsNotTuple(Option $args): Option - { - /** @psalm-suppress IfThisIsMismatch */ - return $args->mapN(self::methodWithRegularParams(...)); - } - - /** - * @param Option $args - * @return Option> - */ - public static function testForgetOneRequiredArgumentForMethodWithVariadicParam(Option $args): Option - { - /** @psalm-suppress IfThisIsMismatch */ - return $args->mapN(self::methodWithVariadicParam(...)); - } - - /** - * @param Option $args - * @return Option> - */ - public static function testForgetTwoRequiredArgumentForMethodWithVariadicParam(Option $args): Option - { - /** @psalm-suppress IfThisIsMismatch */ - return $args->mapN(self::methodWithVariadicParam(...)); - } - - /** - * @param Option $args - * @return Option> - */ - public static function testPutOnlyRequiredArgumentsForMethodWithVariadicParam(Option $args): Option - { - return $args->mapN(self::methodWithVariadicParam(...)); - } - - /** - * @param Option $args - * @return Option> - */ - public static function testPutRequiredArgumentsAndOneVariadicToMethodWithVariadicParam(Option $args): Option - { - return $args->mapN(self::methodWithVariadicParam(...)); - } - - /** - * @param Option $args - * @return Option> - */ - public static function testPutRequiredArgumentsAndManyVariadicToMethodWithVariadicParam(Option $args): Option - { - return $args->mapN(self::methodWithVariadicParam(...)); - } - - /** - * @param Option $args - * @return Option> - */ - public static function testPutRequiredArgumentsAndManyVariadicWithInvalidArgumentToMethodWithVariadicParam(Option $args): Option - { - /** @psalm-suppress IfThisIsMismatch */ - return $args->mapN(self::methodWithVariadicParam(...)); - } - - /** - * @param Option $args - * @return Option - */ - public static function testPutAllRequiredArgumentsToRegularMethodUsingShape(Option $args): Option - { - return $args->mapN(self::methodWithRegularParams(...)); - } - - /** - * @param Option $args - * @return Option - */ - public static function testForgetToPutOneRequiredArgumentsToRegularMethodUsingShape(Option $args): Option - { - /** @psalm-suppress IfThisIsMismatch */ - return $args->mapN(self::methodWithRegularParams(...)); - } - - /** - * @param Option $args - * @return Option - */ - public static function testForgetToPutTwoRequiredArgumentsToRegularMethodUsingShape(Option $args): Option - { - /** @psalm-suppress IfThisIsMismatch */ - return $args->mapN(self::methodWithRegularParams(...)); - } - - /** - * @param Option $args - * @return Option - */ - public static function testPutAllRequiredArgumentsWithOneInvalidArgumentToRegularMethodUsingShape(Option $args): Option - { - /** @psalm-suppress IfThisIsMismatch */ - return $args->mapN(self::methodWithRegularParams(...)); - } - - /** - * @param Option $args - * @return Option - */ - public static function testPutAllRequiredArgumentsWithTwoInvalidArgumentToRegularMethodUsingShape(Option $args): Option - { - /** @psalm-suppress IfThisIsMismatch */ - return $args->mapN(self::methodWithRegularParams(...)); - } - - /** - * @param Option $args - * @return Option - */ - public static function testPutOnlyRequiredArgumentsToMethodWithOptionalParamsUsingShape(Option $args): Option - { - return $args->mapN(self::methodWithOptionalParams(...)); - } - - /** - * @param Option $args - * @return Option - */ - public static function testPutOneRequiredArgumentAndOneOptionalArgumentToMethodWithOptionalParamsUsingShape(Option $args): Option - { - return $args->mapN(self::methodWithOptionalParams(...)); - } - - /** - * @param Option $args - * @return Option - */ - public static function testPutOneRequiredArgumentAndAllOptionalArgumentsToMethodWithOptionalParamsUsingShape(Option $args): Option - { - return $args->mapN(self::methodWithOptionalParams(...)); - } - - /** - * @param Option $args - * @return Option - */ - public static function testPutAllArgumentsToMethodWithOptionalParamsAndOneUnknownUsingShape(Option $args): Option - { - return $args->mapN(self::methodWithOptionalParams(...)); - } - - /** - * @param Option $args - * @return Option - */ - public static function testPutAllArgumentsToMethodWithOptionalParamsAndOneInvalidUsingShape(Option $args): Option - { - /** @psalm-suppress IfThisIsMismatch */ - return $args->mapN(self::methodWithOptionalParams(...)); - } -} diff --git a/tests/Static/Plugin/Psalm8124WorkaroundTest.php b/tests/Static/Plugin/Psalm8124WorkaroundTest.php deleted file mode 100644 index 06c23c97..00000000 --- a/tests/Static/Plugin/Psalm8124WorkaroundTest.php +++ /dev/null @@ -1,99 +0,0 @@ - $list - * @param HashSet $set - * @return array{ - * ArrayList, - * ArrayList, - * ArrayList, - * ArrayList, - * ArrayList, - * ArrayList, - * ArrayList, - * ArrayList, - * ArrayList, - * ArrayList, - * LinkedList, - * LinkedList, - * LinkedList, - * LinkedList, - * LinkedList, - * LinkedList, - * LinkedList, - * LinkedList, - * LinkedList, - * LinkedList, - * } - */ - public function seqTest(ArrayList $list, HashSet $set): array - { - return [ - ArrayList::collect($list), - ArrayList::collect([])->flatMap(fn() => $list), - ArrayList::collect($set), - ArrayList::collect([])->flatMap(fn() => $set), - ArrayList::collect([1, 2, 3])->zip($list), - ArrayList::collect([1, 2, 3])->zip($set), - ArrayList::collect([1, 2, 3])->prependedAll($list), - ArrayList::collect([1, 2, 3])->prependedAll($set), - ArrayList::collect([1, 2, 3])->appendedAll($list), - ArrayList::collect([1, 2, 3])->appendedAll($set), - LinkedList::collect($list), - LinkedList::collect([])->flatMap(fn() => $list), - LinkedList::collect($set), - LinkedList::collect([])->flatMap(fn() => $set), - LinkedList::collect([1, 2, 3])->zip($list), - LinkedList::collect([1, 2, 3])->zip($set), - LinkedList::collect([1, 2, 3])->prependedAll($list), - LinkedList::collect([1, 2, 3])->prependedAll($set), - LinkedList::collect([1, 2, 3])->appendedAll($list), - LinkedList::collect([1, 2, 3])->appendedAll($set), - ]; - } - - /** - * @param NonEmptyArrayList $neList - * @param NonEmptyArrayList $otherNeList - * @param ArrayList $list - * @param HashSet $set - * @return array{ - * Option>, - * Option>, - * NonEmptyArrayList, - * NonEmptyArrayList, - * NonEmptyArrayList, - * NonEmptyArrayList, - * NonEmptyArrayList, - * NonEmptyArrayList - * } - */ - public function nonEmptySeqTest(NonEmptyArrayList $neList, NonEmptyArrayList $otherNeList, ArrayList $list, HashSet $set): array - { - return [ - NonEmptyArrayList::collect($list), - NonEmptyArrayList::collect($set), - $neList->zip($otherNeList), - $neList->prependedAll($list), - $neList->prependedAll($set), - $neList->appendedAll($list), - $neList->appendedAll($set), - $neList->appendedAll($otherNeList), - ]; - } -} diff --git a/tests/Static/Plugin/SeparatedToEitherTest.php b/tests/Static/Plugin/SeparatedToEitherTest.php deleted file mode 100644 index 8adde750..00000000 --- a/tests/Static/Plugin/SeparatedToEitherTest.php +++ /dev/null @@ -1,31 +0,0 @@ -, ArrayList> $separated - * @return Either, ArrayList> - */ - public function separatedArrayListToEither(Separated $separated): Either - { - return $separated->toEither(); - } - - /** - * @param Separated, HashMap> $separated - * @return Either, HashMap> - */ - public function separatedHashMapToEither(Separated $separated): Either - { - return $separated->toEither(); - } -} diff --git a/tests/Static/Plugin/SequenceEitherPluginStaticTest.php b/tests/Static/Plugin/SequenceEitherPluginStaticTest.php deleted file mode 100644 index 6e39570c..00000000 --- a/tests/Static/Plugin/SequenceEitherPluginStaticTest.php +++ /dev/null @@ -1,213 +0,0 @@ -> $values - * @return Either> - */ - public function sequenceArray(array $values): Either - { - return sequenceEither($values); - } - - /** - * @param non-empty-array> $values - * @return Either> - */ - public function sequenceNonEmptyArray(array $values): Either - { - return sequenceEither($values); - } - - /** - * @param list> $values - * @return Either> - */ - public function sequenceList(array $values): Either - { - return sequenceEither($values); - } - - /** - * @param non-empty-list> $values - * @return Either> - */ - public function sequenceNonEmptyList(array $values): Either - { - return sequenceEither($values); - } - - /** - * @return Either - */ - public function sequenceShape(mixed $name, mixed $age): Either - { - return sequenceEither([ - 'name' => proveString($name)->toRight(fn() => new InvalidArgumentException('Invalid name')), - 'age' => proveInt($age)->toRight(fn() => new RuntimeException('Invalid age')), - ]); - } - - /** - * @return Either - */ - public function sequenceTuple(mixed $name, mixed $age): Either - { - return sequenceEither([ - proveString($name)->toRight(fn() => new InvalidArgumentException('Invalid name')), - proveInt($age)->toRight(fn() => new RuntimeException('Invalid age')), - ]); - } - - /** - * @return Either - */ - public function sequenceLazyShape(mixed $name, mixed $age): Either - { - return sequenceEither([ - 'name' => fn() => proveString($name)->toRight(fn() => new InvalidArgumentException('Invalid name')), - 'age' => fn() => proveInt($age)->toRight(fn() => new RuntimeException('Invalid age')), - ]); - } - - /** - * @return Either - */ - public function sequenceLazyTuple(mixed $name, mixed $age): Either - { - return sequenceEither([ - fn() => proveString($name)->toRight(fn() => new InvalidArgumentException('Invalid name')), - fn() => proveInt($age)->toRight(fn() => new RuntimeException('Invalid age')), - ]); - } - - /** - * @param list> $list - * @return Either, list> - */ - public function sequenceAccWithList(array $list): Either - { - return sequenceEitherAcc($list); - } - - /** - * @param non-empty-list> $list - * @return Either, non-empty-list> - */ - public function sequenceAccWithNonEmptyList(array $list): Either - { - return sequenceEitherAcc($list); - } - - /** - * @param array> $list - * @return Either, array> - */ - public function sequenceAccWithArray(array $list): Either - { - return sequenceEitherAcc($list); - } - - /** - * @param non-empty-array> $list - * @return Either, non-empty-array> - */ - public function sequenceAccWithNonEmptyArray(array $list): Either - { - return sequenceEitherAcc($list); - } - - /** - * @param array $data - * @return Either - */ - public function sequenceEitherT(array $data): Either - { - return sequenceEitherT( - at($data, 'name') - ->flatMap(proveNonEmptyString(...)) - ->toRight(fn() => new InvalidArgumentException()), - at($data, 'age') - ->flatMap(proveInt(...)) - ->toRight(fn() => new InvalidArgumentException()), - ); - } - - /** - * @param array $data - * @return Either, array{non-empty-string, int}> - */ - public function sequenceEitherMergedT(array $data): Either - { - return sequenceEitherMergedT( - at($data, 'name')->flatMap(proveNonEmptyString(...))->toRight(fn() => [-1]), - at($data, 'age')->flatMap(proveInt(...))->toRight(fn() => [-2]), - ); - } - - /** - * @param array $data - * @return Either, array{ - * n: non-empty-string, - * a: int, - * }> - */ - public function sequenceEitherMerged(array $data): Either - { - return sequenceEitherMerged([ - 'n' => at($data, 'name')->flatMap(proveNonEmptyString(...))->toRight(fn() => [-1]), - 'a' => at($data, 'age')->flatMap(proveInt(...))->toRight(fn() => [-2]), - ]); - } - - /** - * @return Either< - * array{ - * name?: "Is not non-empty-string", - * age?: "Is not int", - * address?: array{ - * postcode?: "Is not int", - * city?: "Is not string" - * } - * }, - * array{ - * name: non-empty-string, - * age: int, - * address: array{ - * postcode: int, - * city: non-empty-string - * } - * } - * > - */ - public function sequenceAccShape(array $data): Either - { - return sequenceEitherAcc([ - 'name' => at($data, 'name')->flatMap(proveNonEmptyString(...))->toRight(fn() => 'Is not non-empty-string'), - 'age' => at($data, 'age')->flatMap(proveInt(...))->toRight(fn() => 'Is not int'), - 'address' => sequenceEitherAcc([ - 'postcode' => at($data, 'postcode')->flatMap(proveInt(...))->toRight(fn() => 'Is not int'), - 'city' => at($data, 'city')->flatMap(proveNonEmptyString(...))->toRight(fn() => 'Is not string'), - ]), - ]); - } -} diff --git a/tests/Static/Plugin/SequenceOptionPluginStaticTest.php b/tests/Static/Plugin/SequenceOptionPluginStaticTest.php deleted file mode 100644 index a4f9f680..00000000 --- a/tests/Static/Plugin/SequenceOptionPluginStaticTest.php +++ /dev/null @@ -1,108 +0,0 @@ -> $values - * @return Option> - */ - public function sequenceArray(array $values): Option - { - return sequenceOption($values); - } - - /** - * @param non-empty-array> $values - * @return Option> - */ - public function sequenceNonEmptyArray(array $values): Option - { - return sequenceOption($values); - } - - /** - * @param list> $values - * @return Option> - */ - public function sequenceList(array $values): Option - { - return sequenceOption($values); - } - - /** - * @param non-empty-list> $values - * @return Option> - */ - public function sequenceNonEmptyList(array $values): Option - { - return sequenceOption($values); - } - - /** - * @return Option - */ - public function sequenceShape(mixed $name, mixed $age): Option - { - return sequenceOption([ - 'name' => proveString($name), - 'age' => proveInt($age), - ]); - } - - /** - * @return Option - */ - public function sequenceTuple(mixed $name, mixed $age): Option - { - return sequenceOption([ - proveString($name), - proveInt($age), - ]); - } - - /** - * @return Option - */ - public function sequenceLazyShape(mixed $name, mixed $age): Option - { - return sequenceOption([ - 'name' => fn() => proveString($name), - 'age' => fn() => proveInt($age), - ]); - } - - /** - * @return Option - */ - public function sequenceLazyTuple(mixed $name, mixed $age): Option - { - return sequenceOption([ - fn() => proveString($name), - fn() => proveInt($age), - ]); - } - - /** - * @param array $data - * @return Option - */ - public function sequenceT(array $data): Option - { - return sequenceOptionT( - at($data, 'name')->flatMap(proveString(...)), - at($data, 'age')->flatMap(proveInt(...)), - ); - } -}