fp4php / functional-psalm-plugin
Installs: 1 199
Dependents: 2
Suggesters: 0
Security: 0
Stars: 0
Watchers: 1
Forks: 0
Open Issues: 0
Type:psalm-plugin
Requires
- php: ^8.1
- ext-simplexml: *
Requires (Dev)
- fp4php/functional: dev-psalm-v5
- vimeo/psalm: ^5.7
This package is not auto-updated.
Last update: 2024-12-16 21:30:16 UTC
README
Installation
Supported installation method is via composer:
composer require fp4php/functional-psalm-plugin --dev
Usage
To enable the plugin, add the Fp\PsalmPlugin\FunctionalPlugin
class to your psalm configuration using psalm-plugin
binary as follows:
php vendor/bin/psalm-plugin enable fp4php/functional-psalm-plugin
Features
Plugin add type narrowing for filtering.
Fp\Functional\Option\Option::filter
:
<?php declare(strict_types=1); use Fp\Functional\Option\Option; /** * @return Option<int|string> */ function getOption(): Option { // ... } // Narrowed to Option<string> /** @psalm-trace $result */ $result = getOption()->filter(fn($value) => is_string($value));
Fp\Collections\ArrayList::filter
(and other collections with
filter
method):
<?php declare(strict_types=1); use Fp\Collections\ArrayList; /** * @return ArrayList<int|string> */ function getArrayList(): ArrayList { // ... } // Narrowed to ArrayList<string> /** @psalm-trace $result */ $result = getArrayList()->filter(fn($value) => is_string($value));
Fp\Functional\Either\Either::filterOrElse
:
<?php declare(strict_types=1); use TypeError; use ValueError; use Fp\Functional\Either\Either; /** * @return Either<ValueError, int|string> */ function getEither(): Either { // ... } // Narrowed to Either<TypeError|ValueError, string> getEither()->filterOrElse( fn($value) => is_string($value), fn() => new TypeError('Is not string'), );
Fp\Collection\filter
:
<?php declare(strict_types=1); use function Fp\Collection\filter; /** * @return list<int|string> */ function getList(): array { // ... } // Narrowed to list<string> filter(getList(), fn($value) => is_string($value));
Fp\Collection\first
and Fp\Collection\last
:
<?php declare(strict_types=1); use function Fp\Collection\first; use function Fp\Collection\last; /** * @return list<int|string> */ function getList(): array { // ... } // Narrowed to Option<string> first(getList(), fn($value) => is_string($value)); // Narrowed to Option<int> last(getList(), fn($value) => is_int($value));
For all cases above you can use first-class callable syntax:
<?php declare(strict_types=1); use function Fp\Collection\filter; /** * @return list<int|string> */ function getList(): array { // ... } // Narrowed to list<string> filter(getList(), is_string(...));
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.
PHP 8.1 brings feature called first-class
callable. 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 use Tests\Mock\Foo; use function Fp\Callable\ctor; // Psalm knows that ctor(Foo::class) is Closure(int, bool, bool): Foo test(ctor(Foo::class)); /** * @param Closure(int, bool, bool): Foo $makeFoo */ function test(Closure $makeFoo): void { print_r($makeFoo(42, true, false)); print_r(PHP_EOL); }
Plugin brings structural type inference for sequence functions:
<?php use Fp\Functional\Option\Option; use function Fp\Collection\sequenceOption; use function Fp\Collection\sequenceOptionT; function getFoo(int $id): Option { // ... } function getBar(int $id): Option { // ... } /** * @return Option<array{foo: Foo, bar: Bar}> */ function sequenceOptionShapeExample(int $id): Option { // Inferred type is: Option<array{foo: Foo, bar: Bar}> not Option<array<'foo'|'bar', Foo|Bar>> return sequenceOption([ 'foo' => getFoo($id), 'bar' => getBar($id), ]); } /** * @return Option<array{Foo, Bar}> */ function sequenceOptionTupleExample(int $id): Option { // Inferred type is: Option<array{Foo, Bar}> not Option<list<Foo|Bar>> return sequenceOptionT(getFoo($id), getBar($id)); }
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.
Psalm plugin will prevent calling *N combinator in non-valid cases:
<?php declare(strict_types=1); use Fp\Functional\Option\Option; use Tests\Mock\Foo; /** * @param Option<array{int, bool}> $maybeData * @return Option<Foo> */ function test(Option $maybeData): Option { /* * ERROR: IfThisIsMismatch * Object must be type of Option<array{int, bool, bool}>, actual type Option<array{int, bool}> */ return $maybeData->mapN(fn(int $a, bool $b, bool $c) => new Foo($a, $b, $c)); }
Implementation assertion effect for Fp\Evidence\proveTrue
(like
for builtin assert
function):
<?php use Fp\Functional\Option\Option; function getIntOrString(): int|string { // ... } Option::do(function() { $value = getIntOrString(); yield proveTrue(is_int($value)); // here $value narrowed to int from int|string });
Inference for Fp\Functional\Separated\Separated::toEither
:
<?php use Fp\Collections\HashSet; use Fp\Collections\ArrayList; use Fp\Functional\Either\Either; use Fp\Functional\Separated\Separated; /** * @param Separated<ArrayList<int>, ArrayList<string>> $separated * @return Either<ArrayList<int>, ArrayList<string>> */ function separatedArrayListToEither(Separated $separated): Either { return $separated->toEither(); } /** * @param Separated<HashSet<int>, HashSet<string>> $separated * @return Either<HashSet<int>, HashSet<string>> */ function separatedHashSetToEither(Separated $separated): Either { return $separated->toEither(); }
Plugin infers each list
type from predicates of partitionT
:
<?php declare(strict_types=1); use Tests\Mock\Foo; use Tests\Mock\Bar; use Tests\Mock\Baz; use function Fp\Collection\partitionT; /** * @param list<Foo|Bar|Baz> $list * @return array{list<Foo>, list<Bar>, list<Baz>} */ function testExhaustiveInference(array $list): array { return partitionT($list, fn($i) => $i instanceof Foo, fn($i) => $i instanceof Bar); }
Plugin turns all nullable keys to possibly undefined keys:
<?php declare(strict_types=1); use function Fp\Collection\filterNotNull; /** * @param array{name: string, age: int|null} $shape * @return array{name: string, age?: int} */ function example(array $shape): array { return filterNotNull($shape); }