fp4php / functional-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-09-09 20:23:52 UTC
README
安装
支持的安装方法是使用 composer
composer require fp4php/functional-psalm-plugin --dev
用法
要启用插件,使用 psalm-plugin
二进制文件将 Fp\PsalmPlugin\FunctionalPlugin
类添加到您的 psalm 配置中,如下所示
php vendor/bin/psalm-plugin enable fp4php/functional-psalm-plugin
功能
插件为过滤添加类型缩窄。
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
(以及其他具有 filter
方法的集合)
<?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
和 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));
对于上述所有情况,您都可以使用 first-class callable 语法
<?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(...));
使用 psalm 的类型系统制作 fold 函数过于困难。没有插件,Fp\Collection\fold
和集合的 fold 方法有一些边缘情况。例如:[链接](https://psalm.dev/r/b0a99c4912)
插件可以解决这个问题。
PHP 8.1 带来了名为 first-class callable 的功能。但这个功能不能用于类构造函数。`Fp\Callable\ctor` 可以模拟这个功能,但对于类构造函数需要插件进行静态分析。
<?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); }
插件为序列函数带来结构类型推断
<?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)); }
遗憾的是,`@psalm-assert-if-true`/`@psalm-assert-if-false` 对于 Option/Either 断言方法不起作用:[链接](https://psalm.dev/r/408248f46f)
插件实现了对此错误的修复。
Psalm 插件将阻止在非有效情况下调用 *N 拉姆达函数
<?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)); }
对 Fp\Evidence\proveTrue
的实现断言效果(类似于内置的 assert
函数)
<?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 });
对 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(); }
插件从 partitionT
的谓词推断每个 list
类型
<?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); }
插件将所有可空键转换为可能未定义的键
<?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); }