fp4php/functional-psalm-plugin

v1.0.0 2023-05-21 17:46 UTC

This package is not auto-updated.

Last update: 2024-09-09 20:23:52 UTC


README

psalm level psalm type coverage

安装

支持的安装方法是使用 composer

composer require fp4php/functional-psalm-plugin --dev

用法

要启用插件,使用 psalm-plugin 二进制文件将 Fp\PsalmPlugin\FunctionalPlugin 类添加到您的 psalm 配置中,如下所示

php vendor/bin/psalm-plugin enable fp4php/functional-psalm-plugin

功能

  • filter

插件为过滤添加类型缩窄。

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\firstFp\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(...));
  • fold

使用 psalm 的类型系统制作 fold 函数过于困难。没有插件,Fp\Collection\fold 和集合的 fold 方法有一些边缘情况。例如:[链接](https://psalm.dev/r/b0a99c4912)

插件可以解决这个问题。

  • ctor

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);
}
  • sequence

插件为序列函数带来结构类型推断

<?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));
}
  • assertion

遗憾的是,`@psalm-assert-if-true`/`@psalm-assert-if-false` 对于 Option/Either 断言方法不起作用:[链接](https://psalm.dev/r/408248f46f)

插件实现了对此错误的修复。

  • N-combinators

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));
}
  • proveTrue

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
});
  • toEither

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

插件从 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);
}
  • filterNotNull

插件将所有可空键转换为可能未定义的键

<?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);
}