functional-php/tests/types/collections.php
2026-10-27 22:44:36 +01:00

138 lines
5.4 KiB
PHP

<?php
/**
* Copyright 2026 Kacper Donat
*
* @author Kacper "Kadet" Donat <kacper@kadet.net>
*
* Full license available in separate LICENSE file
*/
/**
* PHPStan type tests for the collection functions.
*
* This file is never executed: `composer phpstan` analyses it, and every `assertType()` that doesn't match the
* inferred type is reported as an error. `Ds\Seq` is not generic in PHPStan's view (it comes from the php-ds 2.0
* polyfill), so `Ds\Seq<T>` is shown as `Ds\Seq&iterable<T>`.
*/
use Kadet\Functional\Collections as c;
use Kadet\Functional\Collections\Lazy as lazy;
use Kadet\Functional\Predicates as p;
use Kadet\Functional\Transforms as t;
use function PHPStan\Testing\assertType;
final class Foo
{
public function __construct(public int $id, public string $name)
{
}
}
/**
* @param \Ds\Seq<int> $ints
* @param \Closure(int): string $format
* @param \Ds\Seq<Foo> $foos
* @param \Ds\Map<string, Foo> $map
* @param \Ds\Set<Foo> $set
* @param list<Foo> $list
* @param \Generator<int, Foo> $generator
*/
function transforms(
\Ds\Seq $ints,
\Closure $format,
\Ds\Seq $foos,
\Ds\Map $map,
\Ds\Set $set,
array $list,
\Generator $generator,
): void {
// The container is preserved, the element type comes from the callback.
assertType('Ds\Seq&iterable<string>', c\map($format)($ints));
assertType('Ds\Seq&iterable<string>', c\map(fn (int $v): string => $format($v))($ints));
assertType('Ds\Seq&iterable<Foo>', c\filter(fn (Foo $foo): bool => $foo->id > 0)($foos));
assertType('Ds\Map<string, string>', c\map(fn (Foo $foo, string $key): string => $key)($map));
assertType('Ds\Set<int>', c\map(fn (Foo $foo): int => $foo->id)($set));
assertType('array<int|string, Foo>', c\reject(fn (Foo $foo): bool => $foo->id > 0)($list));
assertType('Ds\Seq&iterable<Foo>', c\sortBy(fn (Foo $foo): string => $foo->name)($foos));
// A generator can't be returned half-consumed, so eager transforms give a Seq (or a list without php-ds).
assertType('(Ds\Seq&iterable<int>)|list<int>', c\map(fn (Foo $foo): int => $foo->id)($generator));
assertType('(Ds\Seq&iterable<Foo>)|list<Foo>', c\tap(fn (Foo $foo) => null)($generator));
// List transforms: a Map gives a Seq.
assertType('Ds\Seq&iterable<string>', c\flatMap(fn (Foo $foo): array => [$foo->name])($map));
assertType('list<string>', c\flatMap(fn (Foo $foo): array => [$foo->name])($list));
// Transforms without a callback keep the element types, but not the container.
assertType('iterable<int, Foo>', c\values()($foos));
assertType('iterable<int<0, max>, Foo>', c\take(2)($list));
// Lazy transforms give generators.
assertType('Generator<mixed, string, mixed, mixed>', lazy\map(fn (Foo $foo): string => $foo->name)($list));
assertType('Generator<mixed, Foo, mixed, mixed>', lazy\filter(fn (Foo $foo): bool => $foo->id > 0)($list));
}
/**
* @param \Ds\Seq<Foo> $foos
* @param list<Foo> $list
*/
function collectors(\Ds\Seq $foos, array $list): void
{
assertType('Ds\Map<int, Foo>', c\keyBy(fn (Foo $foo): int => $foo->id)($foos));
assertType('Ds\Map<string, Ds\Seq&iterable<Foo>>', c\groupBy(fn (Foo $foo): string => $foo->name)($list));
assertType('Ds\Set<Foo>', c\toSet()($list));
assertType('array<int<0, max>, Foo>', c\toArray()($list));
}
/**
* @param \Ds\Seq<Foo> $foos
* @param \Ds\Map<string, Foo> $map
* @param list<int> $ints
*/
function queries(\Ds\Seq $foos, \Ds\Map $map, array $ints): void
{
assertType('Foo|null', c\first()($foos));
assertType('Foo|null', c\first(fn (Foo $foo): bool => $foo->id > 1)($foos));
assertType('Foo|null', c\last()($foos));
assertType('Foo|null', c\find(fn (Foo $foo): bool => $foo->id > 1)($map));
assertType('int|null', c\min()($ints));
assertType('string|null', c\max(fn (Foo $foo): string => $foo->name)($foos));
assertType('bool', c\any(fn (Foo $foo): bool => $foo->id > 1)($foos));
assertType('int', c\count()($foos));
assertType('int', c\reduce(fn (int $carry, Foo $foo): int => $carry + $foo->id, 0)($foos));
}
/**
* Accessors (`t\`) and predicates (`p\`) are untyped about the object they read, so the element type is `object` or
* `mixed`; what matters is that these idioms analyse without errors.
*
* @param list<Foo> $list
*/
function accessors(array $list): void
{
assertType('array<int|string, mixed>', c\map(t\property('name'))($list));
assertType('array<int|string, object>', c\sortBy(t\property('id'))($list));
assertType('object|null', $list |> c\sortBy(t\property('id'), descending: true) |> c\first());
assertType('float|int', c\sum(t\property('id'))($list));
assertType('mixed', c\max(t\property('id'))($list));
assertType('Ds\\Map<mixed, object>', c\keyBy(t\property('id'))($list));
assertType('array<int|string, mixed>', c\filter(p\property('id', p\gt(1)))($list));
assertType('mixed', $list |> c\first(p\property('name', p\same('a'))));
}
/**
* The pipe operator: PHPStan infers through `|>` like through a call.
*
* @param \Ds\Seq<Foo> $foos
*/
function pipes(\Ds\Seq $foos): void
{
assertType('Ds\Seq&iterable<string>', $foos |> c\map(fn (Foo $foo): string => $foo->name));
assertType(
'Foo|null',
$foos |> c\filter(fn (Foo $foo): bool => $foo->id > 1) |> c\first(),
);
assertType('Ds\Map<int, Foo>', $foos |> lazy\filter(fn (Foo $foo): bool => $foo->id > 1) |> c\keyBy(fn (Foo $foo): int => $foo->id));
}