* * 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` is shown as `Ds\Seq&iterable`. */ 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 $ints * @param \Closure(int): string $format * @param \Ds\Seq $foos * @param \Ds\Map $map * @param \Ds\Set $set * @param list $list * @param \Generator $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', c\map($format)($ints)); assertType('Ds\Seq&iterable', c\map(fn (int $v): string => $format($v))($ints)); assertType('Ds\Seq&iterable', c\filter(fn (Foo $foo): bool => $foo->id > 0)($foos)); assertType('Ds\Map', c\map(fn (Foo $foo, string $key): string => $key)($map)); assertType('Ds\Set', c\map(fn (Foo $foo): int => $foo->id)($set)); assertType('array', c\reject(fn (Foo $foo): bool => $foo->id > 0)($list)); assertType('Ds\Seq&iterable', 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)|list', c\map(fn (Foo $foo): int => $foo->id)($generator)); assertType('(Ds\Seq&iterable)|list', c\tap(fn (Foo $foo) => null)($generator)); // List transforms: a Map gives a Seq. assertType('Ds\Seq&iterable', c\flatMap(fn (Foo $foo): array => [$foo->name])($map)); assertType('list', c\flatMap(fn (Foo $foo): array => [$foo->name])($list)); // Transforms without a callback keep the element types, but not the container. assertType('iterable', c\values()($foos)); assertType('iterable, Foo>', c\take(2)($list)); assertType('iterable, Foo>>', c\chunk(2)($list)); // Lazy transforms give generators. assertType('Generator', lazy\map(fn (Foo $foo): string => $foo->name)($list)); assertType('Generator', lazy\filter(fn (Foo $foo): bool => $foo->id > 0)($list)); assertType('Generator, Foo>, mixed, mixed>', lazy\chunk(2)($list)); } /** * @param \Ds\Seq $foos * @param list $list */ function collectors(\Ds\Seq $foos, array $list): void { assertType('Ds\Map', c\keyBy(fn (Foo $foo): int => $foo->id)($foos)); assertType('Ds\Map>', c\groupBy(fn (Foo $foo): string => $foo->name)($list)); assertType('Ds\Set', c\toSet()($list)); assertType('array, Foo>', c\toArray()($list)); } /** * @param \Ds\Seq $foos * @param \Ds\Map $map * @param list $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 $list */ function accessors(array $list): void { assertType('array', c\map(t\property('name'))($list)); assertType('array', 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', c\keyBy(t\property('id'))($list)); assertType('array', 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 $foos */ function pipes(\Ds\Seq $foos): void { assertType('Ds\Seq&iterable', $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', $foos |> lazy\filter(fn (Foo $foo): bool => $foo->id > 1) |> c\keyBy(fn (Foo $foo): int => $foo->id)); }