document collections and add type tests
This commit is contained in:
parent
ed427dd6cd
commit
b20ef22cfd
437
README.md
Normal file
437
README.md
Normal file
@ -0,0 +1,437 @@
|
||||
# kadet/functional
|
||||
|
||||
Small functional building blocks for PHP 8.2+: predicates, property and method accessors, partial application and
|
||||
currying, and a set of collection functions made for the pipe operator. They work on arrays, generators and
|
||||
[php-ds](https://github.com/php-ds/ext-ds) collections.
|
||||
|
||||
```php
|
||||
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;
|
||||
|
||||
$delayed = $departures
|
||||
|> lazy\filter(p\property('delay', p\gt(60)))
|
||||
|> c\sortBy(t\property('delay'), descending: true)
|
||||
|> c\keyBy(t\property('id'))
|
||||
|> c\map(fn (Departure $departure) => "{$departure->line} to {$departure->destination}");
|
||||
```
|
||||
|
||||
- [Installation](#installation)
|
||||
- [Predicates](#predicates)
|
||||
- [Transforms](#transforms)
|
||||
- [Partial application, currying and composition](#partial-application-currying-and-composition)
|
||||
- [Compiling decorators](#compiling-decorators)
|
||||
- [Collections](#collections)
|
||||
- [Static analysis](#static-analysis)
|
||||
- [Contributing](#contributing)
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
composer require kadet/functional
|
||||
```
|
||||
|
||||
The library has no required dependencies. For the collection functions, php-ds is recommended: either the
|
||||
extension (`pie install php-ds/ext-ds:^2.0`) or the polyfill (`composer require php-ds/php-ds`). Without it, everything
|
||||
that works on arrays and generators still does; see [Without php-ds](#without-php-ds).
|
||||
|
||||
The examples below use this class, and `$departures` is a list of them:
|
||||
|
||||
```php
|
||||
final class Departure
|
||||
{
|
||||
public function __construct(
|
||||
public readonly string $id,
|
||||
public readonly string $line,
|
||||
public readonly string $destination,
|
||||
public readonly int $delay = 0, // in seconds
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
||||
$departures = [
|
||||
new Departure('d1', '8', 'Stogi'),
|
||||
new Departure('d2', '8', 'Stogi', delay: 120),
|
||||
new Departure('d3', '12', 'Oliwa', delay: 30),
|
||||
new Departure('d4', '12', 'Oliwa', delay: 300),
|
||||
new Departure('d5', '6', 'Łostowice'),
|
||||
];
|
||||
```
|
||||
|
||||
## Predicates
|
||||
|
||||
A predicate is a callable object returning a bool. The functions in `Kadet\Functional\Predicates` build the common
|
||||
ones, and predicates combine with `and()`, `or()` and `not()`.
|
||||
|
||||
```php
|
||||
use Kadet\Functional as f;
|
||||
use Kadet\Functional\Predicates as p;
|
||||
|
||||
$late = p\property('delay', p\gt(60));
|
||||
$toOliwa = p\property('destination', p\same('Oliwa'));
|
||||
|
||||
$late($departures[3]); // true
|
||||
$late->and($toOliwa)($departures[1]); // false: late, but going to Stogi
|
||||
$late->or($toOliwa)->not()($departures[0]); // true
|
||||
f\not($late)($departures[0]); // true
|
||||
```
|
||||
|
||||
Comparisons: `equals` (`==`), `same` (`===`), `gt`, `ge`, `lt`, `le`. Accessors: `property` (uses a `getX()`
|
||||
getter when there is one), `field`, `method`. Plus `instance($class)`. `f\all(...)` and `f\any(...)` combine many
|
||||
predicates at once, and `f\always()` / `f\never()` are constants.
|
||||
|
||||
## Transforms
|
||||
|
||||
`Kadet\Functional\Transforms` builds accessor functions, handy as selectors:
|
||||
|
||||
```php
|
||||
use Kadet\Functional\Transforms as t;
|
||||
|
||||
t\property('line')($departures[0]); // '8', via getLine() when the object has one
|
||||
t\field('line')($departures[0]); // '8', always the field itself
|
||||
t\method('format', 'H:i')(new DateTimeImmutable('12:30')); // '12:30'
|
||||
```
|
||||
|
||||
## Partial application, currying and composition
|
||||
|
||||
```php
|
||||
use Kadet\Functional as f;
|
||||
use const Kadet\Functional\_;
|
||||
|
||||
// Fix some arguments; `_` leaves a hole to fill later.
|
||||
$replaceA = f\partial(str_replace(...), 'a', _, 'banana');
|
||||
$replaceA('o'); // 'bonono'
|
||||
|
||||
// A curried function waits until it has all required arguments.
|
||||
$between = f\curry(fn (int $min, int $max, int $value) => $value >= $min && $value <= $max);
|
||||
$isHour = $between(0, 23);
|
||||
$isHour(12); // true
|
||||
$between(0)(59)(60); // false
|
||||
|
||||
// Left-to-right composition.
|
||||
$slug = f\pipe(trim(...), strtolower(...), f\partial(str_replace(...), ' ', '-', _));
|
||||
$slug(' Main Station '); // 'main-station'
|
||||
|
||||
// Anonymous recursion: the function receives itself as the first argument.
|
||||
$factorial = f\fix(fn ($self, int $n) => $n < 2 ? 1 : $n * $self($n - 1));
|
||||
$factorial(5); // 120
|
||||
|
||||
// Remember results by arguments.
|
||||
$slow = f\memoize(fn (string $stop) => strtoupper($stop)); // imagine an expensive lookup
|
||||
$slow('stogi'); // computed
|
||||
$slow('stogi'); // remembered
|
||||
```
|
||||
|
||||
## Compiling decorators
|
||||
|
||||
`partial()`, `curry()` and `fix()` return decorator objects. Calling one goes through its `__invoke()`, which
|
||||
inspects the arguments every time. That is fine for a single call, but in a loop over thousands of elements it
|
||||
adds up. `f\compile()` flattens such a callable into a plain `\Closure` once, so the loop pays only for the
|
||||
function itself:
|
||||
|
||||
```php
|
||||
use Kadet\Functional as f;
|
||||
use const Kadet\Functional\_;
|
||||
|
||||
$pad = f\compile(f\partial(str_pad(...), _, 3, '0', STR_PAD_LEFT));
|
||||
|
||||
$pad('7'); // '007'
|
||||
```
|
||||
|
||||
Objects implementing `Kadet\Functional\Compilable` are compiled with their own `compile()`; any other callable is
|
||||
converted to a closure. Implement `Compilable` on your own decorators to get the same benefit.
|
||||
You rarely need to call `compile()` yourself: the collection functions compile every callback once per step.
|
||||
|
||||
## Collections
|
||||
|
||||
The collection functions take their configuration and return a function that takes the data, so a pipeline is a
|
||||
chain of `|>`:
|
||||
|
||||
```php
|
||||
use Kadet\Functional\Collections as c; // eager: returns collections
|
||||
use Kadet\Functional\Collections\Lazy as lazy; // lazy: returns generators
|
||||
|
||||
$lines = $departures
|
||||
|> c\filter(fn (Departure $departure) => $departure->delay > 0)
|
||||
|> c\map(t\property('line'))
|
||||
|> c\unique()
|
||||
|> c\values();
|
||||
// ['8', '12']
|
||||
```
|
||||
|
||||
The namespace says which flavour you get. `c\` is eager and gives a finished collection after every step. `lazy\`
|
||||
holds the streaming transforms, which give a generator and do no work until something reads it. Sorting, grouping,
|
||||
collectors and queries need the whole input, so they exist only in `c\`. They take any iterable, generators
|
||||
included, and are how a lazy chain ends.
|
||||
|
||||
### Pipes, and calling without them
|
||||
|
||||
Every function returns a closure, and there is no "data first" form. One shape for everything means every function
|
||||
composes the same way, and it avoids an ambiguity: a callable array like `[$repository, 'find']` is also an
|
||||
iterable, so `c\first($something)` could not tell data from a predicate.
|
||||
|
||||
The pipe operator needs PHP 8.5. On earlier versions, or for a single step, call the closure directly or compose
|
||||
steps with `f\pipe()`:
|
||||
|
||||
```php
|
||||
c\first()($departures); // the first departure
|
||||
|
||||
$firstLate = f\pipe(c\filter(p\property('delay', p\gt(60))), c\first());
|
||||
$firstLate($departures); // d2
|
||||
```
|
||||
|
||||
Where a php-ds method reads better for a one-off (`$seq->first()`), use it.
|
||||
|
||||
### Callbacks get the value, then the key
|
||||
|
||||
Every callback, predicate and selector is called as `$fn($value, $key)`, for every kind of input, `Ds\Map`
|
||||
included:
|
||||
|
||||
```php
|
||||
$codes = new Ds\Map(['STG' => 'Stogi', 'OLW' => 'Oliwa']);
|
||||
|
||||
$codes |> c\map(fn (string $name, string $code) => "$name ($code)");
|
||||
// Ds\Map ['STG' => 'Stogi (STG)', 'OLW' => 'Oliwa (OLW)']
|
||||
```
|
||||
|
||||
Value first, because most callbacks only need the value. It is also the order of `array_filter()` with
|
||||
`ARRAY_FILTER_USE_BOTH`. `Ds\Map::map()` itself passes `($key, $value)`: when you switch between the two, the
|
||||
library's order is the one that stays the same across arrays, generators and all php-ds types.
|
||||
|
||||
### Builtins get only the value
|
||||
|
||||
Passing the key along is a trap for functions with optional parameters. `round()` takes a precision as its second
|
||||
argument, so a naive `($value, $key)` call would round the first element to 0 places, the second to 1 and the
|
||||
third to 2. To prevent that, two kinds of callables are called with the value only:
|
||||
|
||||
- **PHP's internal functions**, written as first-class callables: `trim(...)`, `round(...)`, `strtoupper(...)`.
|
||||
This also holds when they are wrapped, e.g. `f\partial(str_pad(...), _, 3, '0')`.
|
||||
- **Closures declaring exactly one parameter**, like `fn ($value) => ...`.
|
||||
|
||||
```php
|
||||
[1.25, 2.5, 3.14159] |> c\map(round(...)); // [1.0, 3.0, 3.0], not [1.0, 2.5, 3.14]
|
||||
[' a ', ' b'] |> c\map(trim(...)); // ['a', 'b']; the key never becomes trim's character list
|
||||
```
|
||||
|
||||
Any other callable gets the key, **even into an optional parameter**. A method like
|
||||
`format(Departure $departure, string $style = 'short')` would receive the key as `$style`. Wrap it in a
|
||||
one-parameter closure: `c\map(fn ($departure) => $formatter->format($departure))`.
|
||||
|
||||
Builtins are written as first-class callables (`trim(...)`) throughout. String callables (`'trim'`) work too, but
|
||||
`trim(...)` fails right where it is written if the function doesn't exist, is understood by IDEs and static
|
||||
analysis, and can't be mistaken for data.
|
||||
|
||||
### What you get back
|
||||
|
||||
Eager transforms return the same kind of container they were given, so a `Seq` stays a `Seq` and an array stays an
|
||||
array. The details:
|
||||
|
||||
| Input | Keyed transforms¹ | List transforms² |
|
||||
|---|---|---|
|
||||
| array | array, keys preserved | list array |
|
||||
| `Ds\Map` | `Ds\Map`, keys preserved | `Ds\Seq` |
|
||||
| `Ds\Seq` | `Ds\Seq`, reindexed | `Ds\Seq` |
|
||||
| `Ds\Set` | `Ds\Set` | `Ds\Set` (but `keys()` gives a `Ds\Seq`) |
|
||||
| generator, any other iterable | `Ds\Seq` (a list array without php-ds) | `Ds\Seq` (a list array without php-ds) |
|
||||
|
||||
¹ `map`, `filter`, `reject`, `unique`, `uniqueBy`, `sort`, `sortBy`, `slice`, `take`, `skip`, `merge`, `each`,
|
||||
`tap`. ² `flatMap`, `values`, `keys`, `prepend`, `append`, `concat`: these produce a list, and a `Ds\Map` can't hold
|
||||
a list, so it becomes a `Ds\Seq`.
|
||||
|
||||
A few consequences worth knowing:
|
||||
|
||||
- **A generator given to an eager transform comes back as a `Ds\Seq`** (or a list array without php-ds). The
|
||||
callbacks still see the generator's keys, but the result is a list. A generator can be read only once, so
|
||||
returning a half-read generator, or a generator at all, from an eager function would be a trap.
|
||||
- **`each()`/`tap()` return their input** unchanged for arrays and php-ds collections, which makes them safe to put
|
||||
in the middle of a chain for logging. A generator is consumed by them, so they return its values as a
|
||||
`Ds\Seq`, not the spent generator.
|
||||
- `filter()` on an array keeps the keys, like `array_filter()`. Add `c\values()` when you need a list.
|
||||
- `map()` on a `Ds\Set` follows `Ds\Set::map()`, so results that are equal collapse into one.
|
||||
- `merge()` merges arrays and `Ds\Map` by key (integer keys of arrays are kept, unlike `array_merge()`); for other
|
||||
inputs it appends.
|
||||
- `sort()`/`sortBy()` are stable and never modify their input, unlike `Ds\Seq::sort()`.
|
||||
|
||||
### Finding things: `first`, `find` and `required`
|
||||
|
||||
`first()` and `last()` return `null` when the collection is empty or nothing matches. A missing value is usually
|
||||
an expected outcome (no departure from this stop right now), so it should not need a `try`. When the value must be
|
||||
there, say so with `required: true`, and get an exception that names the function and what went wrong:
|
||||
|
||||
```php
|
||||
$departures |> c\first(p\property('line', p\same('12'))); // d3
|
||||
$departures |> c\first(p\property('line', p\same('2'))); // null
|
||||
$departures |> c\first(p\property('line', p\same('2')), required: true);
|
||||
// EmptyCollectionException: first(): no element matched
|
||||
|
||||
[] |> c\last(required: true);
|
||||
// EmptyCollectionException: last(): collection is empty
|
||||
```
|
||||
|
||||
`required` also makes a found `null` distinguishable: `[null] |> c\first(required: true)` returns `null` without
|
||||
throwing, because something was found.
|
||||
|
||||
`contains($value)` and `any($predicate)` are separate functions on purpose. A string like `'date'` is both a
|
||||
value and a valid callable, so one function taking either could not know which you meant. `contains()` compares
|
||||
strictly (`===`; a `Ds\Key` with `equals()`), `any()` calls the predicate.
|
||||
|
||||
### Aliases
|
||||
|
||||
Some functions have a second name, so the code can say what it means. Each is a one-line delegation and behaves
|
||||
exactly like its target:
|
||||
|
||||
| Alias | Same as | Use it when |
|
||||
|---|---|---|
|
||||
| `find($predicate)` | `first($predicate)`, with the predicate required | you are searching, not taking the head |
|
||||
| `some($predicate)` | `any($predicate)` | it reads better in a condition: `if ($stops \|> c\some(...))` |
|
||||
| `tap($fn)` | `each($fn)` | the call is for a side effect in the middle of a chain, like logging |
|
||||
|
||||
### Transforms
|
||||
|
||||
`map`, `filter` (keeps truthy values without a callback), `reject`, `flatMap`, `values`, `keys`, `unique`,
|
||||
`uniqueBy`, `sort` (comparator or `<=>`), `sortBy` (selector, optionally `descending: true`), `slice`, `take`,
|
||||
`skip`, `prepend`, `append`, `concat`, `merge`, `each`, `tap`.
|
||||
|
||||
```php
|
||||
$delays = $departures
|
||||
|> c\reject(fn (Departure $departure) => $departure->delay === 0)
|
||||
|> c\sortBy(t\property('delay'))
|
||||
|> c\map(fn (Departure $departure) => sprintf('%s: +%d min', $departure->id, $departure->delay / 60))
|
||||
|> c\values();
|
||||
// ['d3: +0 min', 'd2: +2 min', 'd4: +5 min']
|
||||
```
|
||||
|
||||
### Collectors
|
||||
|
||||
`keyBy`, `groupBy` (a `Ds\Map` of `Ds\Seq`, in encounter order), `toSeq`, `toMap`, `toSet` and `toArray`. All but
|
||||
`toArray` produce php-ds collections.
|
||||
|
||||
```php
|
||||
$byLine = $departures |> c\groupBy(t\property('line'));
|
||||
// Ds\Map ['8' => Seq [d1, d2], '12' => Seq [d3, d4], '6' => Seq [d5]]
|
||||
|
||||
$byId = $departures |> c\keyBy(t\property('id'));
|
||||
$byId->get('d4')->destination; // 'Oliwa'
|
||||
```
|
||||
|
||||
`toArray()` throws an `InvalidArgumentException` when a key can't be an array key, like an object key of a
|
||||
`Ds\Map`, instead of PHP's "Illegal offset type".
|
||||
|
||||
### Queries
|
||||
|
||||
`first`, `last`, `find`, `contains`, `any`, `some`, `every`, `count`, `isEmpty`, `min`, `max`, `reduce`, `sum` and
|
||||
`join` turn a collection into a single value. They stop reading as soon as the answer is known, which matters for
|
||||
generators.
|
||||
|
||||
```php
|
||||
$departures |> c\every(fn (Departure $departure) => $departure->delay < 600); // true
|
||||
$departures |> c\sum(t\property('delay')); // 450
|
||||
$departures |> c\max(t\property('delay')); // 300
|
||||
$departures |> c\map(t\property('line')) |> c\unique() |> c\join(', '); // '8, 12, 6'
|
||||
$departures |> c\reduce(fn (int $carry, Departure $departure) => $carry + 1, 0); // 5
|
||||
```
|
||||
|
||||
**`min()` and `max()` with a selector return the selected value, not the element.** Above, `c\max()` gives `300`,
|
||||
the largest delay, not the departure that has it. To get the element, sort and take the first:
|
||||
|
||||
```php
|
||||
$departures |> c\sortBy(t\property('delay'), descending: true) |> c\first(); // d4
|
||||
```
|
||||
|
||||
`reduce()` callbacks take the carry first: `fn ($carry, $value, $key)`.
|
||||
|
||||
### Lazy pipelines
|
||||
|
||||
`lazy\map`, `filter`, `reject`, `flatMap`, `values`, `keys`, `unique`, `uniqueBy`, `slice`, `take`, `skip`,
|
||||
`prepend`, `append`, `concat`, `each` and `tap` return a generator. Each element travels through the whole chain
|
||||
before the next one is read, and nothing happens until the chain is read by a collector or a query:
|
||||
|
||||
```php
|
||||
function readDepartures(string $path): Generator
|
||||
{
|
||||
foreach (new SplFileObject($path) as $line) {
|
||||
if ($line !== '') {
|
||||
yield json_decode($line, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$firstTram = readDepartures('departures.jsonl')
|
||||
|> lazy\filter(fn (array $row) => $row['vehicle'] === 'tram')
|
||||
|> lazy\map(fn (array $row) => new Departure($row['id'], $row['line'], $row['destination']))
|
||||
|> c\first(); // reads the file only up to the first tram
|
||||
```
|
||||
|
||||
Choose lazy when:
|
||||
|
||||
- **the input is large or streamed** (an import, a file, a database cursor): no step builds an intermediate
|
||||
collection, so memory stays flat;
|
||||
- **you stop early**: `first`, `find`, `any` or `take` on a big input read only as far as needed;
|
||||
- **the input is infinite**, like a generator counting up.
|
||||
|
||||
Stay eager when the result is stored, returned from a function or read more than once. A generator can be read
|
||||
only once, and a second `foreach` over it silently sees nothing (or throws). So don't let a lazy value leave the
|
||||
function that builds it: end the chain with a collector (`c\toSeq()`, `c\keyBy()`, ...) or a query. For small
|
||||
collections, eager is also easier to debug, since every step has a value you can look at.
|
||||
|
||||
### Without php-ds
|
||||
|
||||
php-ds is optional. Without it:
|
||||
|
||||
- every transform, lazy or eager, works on arrays and generators; eager transforms given a generator return a list
|
||||
array instead of a `Ds\Seq`;
|
||||
- `toArray()` and all queries work;
|
||||
- `keyBy`, `groupBy`, `toSeq`, `toMap` and `toSet` throw `Kadet\Functional\Exception\DsUnavailableException`
|
||||
("keyBy() needs php-ds: install ext-ds or require php-ds/php-ds"). They throw when the collector is built, not
|
||||
when data arrives, so `$fn = c\keyBy(...)` fails at once, at the line that needs php-ds, even if no data ever
|
||||
reaches it.
|
||||
|
||||
With php-ds you get `Ds\Map` keys of any type (objects included), collections that are objects (passed by handle,
|
||||
not copied), and results whose type says whether they are a list (`Seq`), a mapping (`Map`) or a set (`Set`).
|
||||
|
||||
## Static analysis
|
||||
|
||||
Every function has `@template` generics, so PHPStan (level 8 in this repository) follows element types through a
|
||||
chain, `|>` included:
|
||||
|
||||
```php
|
||||
/** @param Ds\Seq<Departure> $departures */
|
||||
function delayedLines(Ds\Seq $departures): ?string
|
||||
{
|
||||
return $departures
|
||||
|> c\filter(fn (Departure $departure): bool => $departure->delay > 0) // Ds\Seq<Departure>
|
||||
|> c\map(fn (Departure $departure): string => $departure->line) // Ds\Seq<string>
|
||||
|> c\first(); // string|null
|
||||
}
|
||||
```
|
||||
|
||||
Eager transforms with a callback keep the container in the type as well (a `Ds\Map` in, a `Ds\Map` out; a
|
||||
generator in, `Ds\Seq|list` out). The limits of what can be inferred:
|
||||
|
||||
- **Element types come from the callback's parameter and return types.** `fn ($departure) => ...` without types
|
||||
gives `mixed`.
|
||||
- **Keys are inferred only when the callback declares a `$key` parameter.** Otherwise the keys of the result are
|
||||
`mixed` (`int|string` for arrays).
|
||||
- **Transforms without a callback** (`values`, `keys`, `unique`, `sort()`, `slice`, `take`, `skip`, `prepend`,
|
||||
`append`, `concat`, `merge`) keep the element types but are typed as returning `iterable`: the container type is
|
||||
lost. Follow them with a collector such as `c\toSeq()` if you need it.
|
||||
- With the php-ds **polyfill**, PHPStan does not treat `Ds\Seq` as generic: `Ds\Seq<T>` shows as
|
||||
`Ds\Seq&iterable<T>`, and the values of a `Ds\Map` read as `mixed`, so queries over a map (`c\first()`, `c\last()`)
|
||||
give `mixed`.
|
||||
|
||||
## Contributing
|
||||
|
||||
```bash
|
||||
composer test # both PHPUnit suites
|
||||
composer phpstan # PHPStan, level 8, on src/ and the type tests in tests/types/
|
||||
```
|
||||
|
||||
- `composer test` runs the main suite and the no-php-ds suite (`phpunit.no-ds.xml`), which hides the polyfill to
|
||||
check the paths without php-ds. **The no-ds suite is skipped when ext-ds is loaded**, because an extension can't
|
||||
be hidden: it prints a notice and passes. Run it on a PHP without ext-ds to cover those paths.
|
||||
- `tests/PipeSyntaxTest.php` is the only test that uses `|>`. It is loaded only on PHP 8.5 and newer (see
|
||||
`phpunit.xml`), so the rest of the suite still runs on PHP 8.2. Keep `|>` out of other tests.
|
||||
- `tests/types/` holds PHPStan type assertions (`assertType()`). They are analysed, never executed. Add one when
|
||||
you change a signature.
|
||||
- `phpstan-baseline.neon` lists errors in code that predates PHPStan in this project. New code must not add to it.
|
||||
@ -40,6 +40,7 @@
|
||||
"test": [
|
||||
"phpunit",
|
||||
"phpunit -c phpunit.no-ds.xml"
|
||||
]
|
||||
],
|
||||
"phpstan": "phpstan analyse"
|
||||
}
|
||||
}
|
||||
|
||||
745
phpstan-baseline.neon
Normal file
745
phpstan-baseline.neon
Normal file
@ -0,0 +1,745 @@
|
||||
parameters:
|
||||
ignoreErrors:
|
||||
-
|
||||
message: '#^Method Kadet\\Functional\\Decorator\:\:getDecorated\(\) has no return type specified\.$#'
|
||||
identifier: missingType.return
|
||||
count: 1
|
||||
path: src/Decorator.php
|
||||
|
||||
-
|
||||
message: '#^Method Kadet\\Functional\\Predicate\:\:__invoke\(\) has parameter \$args with no type specified\.$#'
|
||||
identifier: missingType.parameter
|
||||
count: 1
|
||||
path: src/Predicate.php
|
||||
|
||||
-
|
||||
message: '#^Method Kadet\\Functional\\Predicate\:\:and\(\) has parameter \$predicate with no type specified\.$#'
|
||||
identifier: missingType.parameter
|
||||
count: 1
|
||||
path: src/Predicate.php
|
||||
|
||||
-
|
||||
message: '#^Method Kadet\\Functional\\Predicate\:\:or\(\) has parameter \$predicate with no type specified\.$#'
|
||||
identifier: missingType.parameter
|
||||
count: 1
|
||||
path: src/Predicate.php
|
||||
|
||||
-
|
||||
message: '#^Method Kadet\\Functional\\Predicate\\AbstractPredicate\:\:and\(\) has parameter \$predicate with no type specified\.$#'
|
||||
identifier: missingType.parameter
|
||||
count: 1
|
||||
path: src/Predicate/AbstractPredicate.php
|
||||
|
||||
-
|
||||
message: '#^Method Kadet\\Functional\\Predicate\\AbstractPredicate\:\:or\(\) has parameter \$predicate with no type specified\.$#'
|
||||
identifier: missingType.parameter
|
||||
count: 1
|
||||
path: src/Predicate/AbstractPredicate.php
|
||||
|
||||
-
|
||||
message: '#^Method Kadet\\Functional\\Predicate\\AllOfPredicate\:\:__invoke\(\) has parameter \$args with no type specified\.$#'
|
||||
identifier: missingType.parameter
|
||||
count: 1
|
||||
path: src/Predicate/AllOfPredicate.php
|
||||
|
||||
-
|
||||
message: '#^Method Kadet\\Functional\\Predicate\\AllOfPredicate\:\:and\(\) has parameter \$predicate with no type specified\.$#'
|
||||
identifier: missingType.parameter
|
||||
count: 1
|
||||
path: src/Predicate/AllOfPredicate.php
|
||||
|
||||
-
|
||||
message: '#^Method Kadet\\Functional\\Predicate\\AnyOfPredicate\:\:__invoke\(\) has parameter \$args with no type specified\.$#'
|
||||
identifier: missingType.parameter
|
||||
count: 1
|
||||
path: src/Predicate/AnyOfPredicate.php
|
||||
|
||||
-
|
||||
message: '#^Method Kadet\\Functional\\Predicate\\AnyOfPredicate\:\:or\(\) has parameter \$predicate with no type specified\.$#'
|
||||
identifier: missingType.parameter
|
||||
count: 1
|
||||
path: src/Predicate/AnyOfPredicate.php
|
||||
|
||||
-
|
||||
message: '#^Method Kadet\\Functional\\Predicate\\ClosurePredicate\:\:__invoke\(\) has parameter \$args with no type specified\.$#'
|
||||
identifier: missingType.parameter
|
||||
count: 1
|
||||
path: src/Predicate/ClosurePredicate.php
|
||||
|
||||
-
|
||||
message: '#^Method Kadet\\Functional\\Predicate\\ClosurePredicate\:\:getDecorated\(\) has no return type specified\.$#'
|
||||
identifier: missingType.return
|
||||
count: 1
|
||||
path: src/Predicate/ClosurePredicate.php
|
||||
|
||||
-
|
||||
message: '#^Method Kadet\\Functional\\Predicate\\ConstantPredicate\:\:__invoke\(\) has parameter \$args with no type specified\.$#'
|
||||
identifier: missingType.parameter
|
||||
count: 1
|
||||
path: src/Predicate/ConstantPredicate.php
|
||||
|
||||
-
|
||||
message: '#^Method Kadet\\Functional\\Predicate\\ConstantPredicate\:\:always\(\) has no return type specified\.$#'
|
||||
identifier: missingType.return
|
||||
count: 1
|
||||
path: src/Predicate/ConstantPredicate.php
|
||||
|
||||
-
|
||||
message: '#^Method Kadet\\Functional\\Predicate\\ConstantPredicate\:\:and\(\) has parameter \$predicate with no type specified\.$#'
|
||||
identifier: missingType.parameter
|
||||
count: 1
|
||||
path: src/Predicate/ConstantPredicate.php
|
||||
|
||||
-
|
||||
message: '#^Method Kadet\\Functional\\Predicate\\ConstantPredicate\:\:never\(\) has no return type specified\.$#'
|
||||
identifier: missingType.return
|
||||
count: 1
|
||||
path: src/Predicate/ConstantPredicate.php
|
||||
|
||||
-
|
||||
message: '#^Method Kadet\\Functional\\Predicate\\ConstantPredicate\:\:or\(\) has parameter \$predicate with no type specified\.$#'
|
||||
identifier: missingType.parameter
|
||||
count: 1
|
||||
path: src/Predicate/ConstantPredicate.php
|
||||
|
||||
-
|
||||
message: '#^Unsafe usage of new static\(\)\.$#'
|
||||
identifier: new.static
|
||||
count: 2
|
||||
path: src/Predicate/ConstantPredicate.php
|
||||
|
||||
-
|
||||
message: '#^Expression "\$this\-\>negated" on a separate line does not do anything\.$#'
|
||||
identifier: expr.resultUnused
|
||||
count: 1
|
||||
path: src/Predicate/NegatedPredicate.php
|
||||
|
||||
-
|
||||
message: '#^Method Kadet\\Functional\\Predicate\\NegatedPredicate\:\:__invoke\(\) has parameter \$args with no type specified\.$#'
|
||||
identifier: missingType.parameter
|
||||
count: 1
|
||||
path: src/Predicate/NegatedPredicate.php
|
||||
|
||||
-
|
||||
message: '#^Method Kadet\\Functional\\Predicate\\NegatedPredicate\:\:getDecorated\(\) has no return type specified\.$#'
|
||||
identifier: missingType.return
|
||||
count: 1
|
||||
path: src/Predicate/NegatedPredicate.php
|
||||
|
||||
-
|
||||
message: '#^Function Kadet\\Functional\\Predicates\\equals\(\) has parameter \$expected with no type specified\.$#'
|
||||
identifier: missingType.parameter
|
||||
count: 1
|
||||
path: src/Predicates/comparisons.php
|
||||
|
||||
-
|
||||
message: '#^Function Kadet\\Functional\\Predicates\\ge\(\) has parameter \$expected with no type specified\.$#'
|
||||
identifier: missingType.parameter
|
||||
count: 1
|
||||
path: src/Predicates/comparisons.php
|
||||
|
||||
-
|
||||
message: '#^Function Kadet\\Functional\\Predicates\\gt\(\) has parameter \$expected with no type specified\.$#'
|
||||
identifier: missingType.parameter
|
||||
count: 1
|
||||
path: src/Predicates/comparisons.php
|
||||
|
||||
-
|
||||
message: '#^Function Kadet\\Functional\\Predicates\\le\(\) has parameter \$expected with no type specified\.$#'
|
||||
identifier: missingType.parameter
|
||||
count: 1
|
||||
path: src/Predicates/comparisons.php
|
||||
|
||||
-
|
||||
message: '#^Function Kadet\\Functional\\Predicates\\lt\(\) has parameter \$expected with no type specified\.$#'
|
||||
identifier: missingType.parameter
|
||||
count: 1
|
||||
path: src/Predicates/comparisons.php
|
||||
|
||||
-
|
||||
message: '#^Function Kadet\\Functional\\Predicates\\same\(\) has parameter \$expected with no type specified\.$#'
|
||||
identifier: missingType.parameter
|
||||
count: 1
|
||||
path: src/Predicates/comparisons.php
|
||||
|
||||
-
|
||||
message: '#^Function Kadet\\Functional\\Predicates\\field\(\) has parameter \$predicate with no type specified\.$#'
|
||||
identifier: missingType.parameter
|
||||
count: 1
|
||||
path: src/Predicates/objects.php
|
||||
|
||||
-
|
||||
message: '#^Function Kadet\\Functional\\Predicates\\instance\(\) has no return type specified\.$#'
|
||||
identifier: missingType.return
|
||||
count: 1
|
||||
path: src/Predicates/objects.php
|
||||
|
||||
-
|
||||
message: '#^Function Kadet\\Functional\\Predicates\\method\(\) has parameter \$args with no value type specified in iterable type array\.$#'
|
||||
identifier: missingType.iterableValue
|
||||
count: 1
|
||||
path: src/Predicates/objects.php
|
||||
|
||||
-
|
||||
message: '#^Function Kadet\\Functional\\Predicates\\method\(\) has parameter \$predicate with no type specified\.$#'
|
||||
identifier: missingType.parameter
|
||||
count: 1
|
||||
path: src/Predicates/objects.php
|
||||
|
||||
-
|
||||
message: '#^Function Kadet\\Functional\\Predicates\\property\(\) has parameter \$predicate with no type specified\.$#'
|
||||
identifier: missingType.parameter
|
||||
count: 1
|
||||
path: src/Predicates/objects.php
|
||||
|
||||
-
|
||||
message: '#^Method Kadet\\Functional\\Reflection\\ReflectionAppliedParameter\:\:__construct\(\) has parameter \$function with no type specified\.$#'
|
||||
identifier: missingType.parameter
|
||||
count: 1
|
||||
path: src/Reflection/ReflectionAppliedParameter.php
|
||||
|
||||
-
|
||||
message: '#^Method Kadet\\Functional\\Reflection\\ReflectionAppliedParameter\:\:__construct\(\) has parameter \$value with no type specified\.$#'
|
||||
identifier: missingType.parameter
|
||||
count: 1
|
||||
path: src/Reflection/ReflectionAppliedParameter.php
|
||||
|
||||
-
|
||||
message: '#^Method Kadet\\Functional\\Reflection\\ReflectionAppliedParameter\:\:getValue\(\) has no return type specified\.$#'
|
||||
identifier: missingType.return
|
||||
count: 1
|
||||
path: src/Reflection/ReflectionAppliedParameter.php
|
||||
|
||||
-
|
||||
message: '#^Property Kadet\\Functional\\Reflection\\ReflectionAppliedParameter\:\:\$value has no type specified\.$#'
|
||||
identifier: missingType.property
|
||||
count: 1
|
||||
path: src/Reflection/ReflectionAppliedParameter.php
|
||||
|
||||
-
|
||||
message: '#^Method Kadet\\Functional\\Reflection\\ReflectionDecoratedFunction\:\:getClosureScopeClass\(\) return type with generic class ReflectionClass does not specify its types\: T$#'
|
||||
identifier: missingType.generics
|
||||
count: 1
|
||||
path: src/Reflection/ReflectionDecoratedFunction.php
|
||||
|
||||
-
|
||||
message: '#^Method Kadet\\Functional\\Reflection\\ReflectionDecoratedFunction\:\:getStaticVariables\(\) return type has no value type specified in iterable type array\.$#'
|
||||
identifier: missingType.iterableValue
|
||||
count: 1
|
||||
path: src/Reflection/ReflectionDecoratedFunction.php
|
||||
|
||||
-
|
||||
message: '#^Method Kadet\\Functional\\Reflection\\ReflectionDecoratedFunction\:\:hasReturnType\(\) has no return type specified\.$#'
|
||||
identifier: missingType.return
|
||||
count: 1
|
||||
path: src/Reflection/ReflectionDecoratedFunction.php
|
||||
|
||||
-
|
||||
message: '#^Property Kadet\\Functional\\Reflection\\ReflectionDecoratedFunction\:\:\$decorated has no type specified\.$#'
|
||||
identifier: missingType.property
|
||||
count: 1
|
||||
path: src/Reflection/ReflectionDecoratedFunction.php
|
||||
|
||||
-
|
||||
message: '#^Property Kadet\\Functional\\Reflection\\ReflectionDecoratedFunction\:\:\$reflection has no type specified\.$#'
|
||||
identifier: missingType.property
|
||||
count: 1
|
||||
path: src/Reflection/ReflectionDecoratedFunction.php
|
||||
|
||||
-
|
||||
message: '#^Method Kadet\\Functional\\Reflection\\ReflectionPartiallyAppliedFunction\:\:getAppliedParameters\(\) has no return type specified\.$#'
|
||||
identifier: missingType.return
|
||||
count: 1
|
||||
path: src/Reflection/ReflectionPartiallyAppliedFunction.php
|
||||
|
||||
-
|
||||
message: '#^Method Kadet\\Functional\\Reflection\\ReflectionPartiallyAppliedFunction\:\:getNumberOfAppliedParameters\(\) has no return type specified\.$#'
|
||||
identifier: missingType.return
|
||||
count: 1
|
||||
path: src/Reflection/ReflectionPartiallyAppliedFunction.php
|
||||
|
||||
-
|
||||
message: '#^Method Kadet\\Functional\\Reflection\\ReflectionPartiallyAppliedFunction\:\:isCurried\(\) has no return type specified\.$#'
|
||||
identifier: missingType.return
|
||||
count: 1
|
||||
path: src/Reflection/ReflectionPartiallyAppliedFunction.php
|
||||
|
||||
-
|
||||
message: '#^Property Kadet\\Functional\\Reflection\\ReflectionPartiallyAppliedFunction\:\:\$applied has no type specified\.$#'
|
||||
identifier: missingType.property
|
||||
count: 1
|
||||
path: src/Reflection/ReflectionPartiallyAppliedFunction.php
|
||||
|
||||
-
|
||||
message: '#^Function Kadet\\Functional\\Transforms\\field\(\) has no return type specified\.$#'
|
||||
identifier: missingType.return
|
||||
count: 1
|
||||
path: src/Transforms/objects.php
|
||||
|
||||
-
|
||||
message: '#^Function Kadet\\Functional\\Transforms\\field\(\) has parameter \$field with no type specified\.$#'
|
||||
identifier: missingType.parameter
|
||||
count: 1
|
||||
path: src/Transforms/objects.php
|
||||
|
||||
-
|
||||
message: '#^Function Kadet\\Functional\\Transforms\\getter\(\) has no return type specified\.$#'
|
||||
identifier: missingType.return
|
||||
count: 1
|
||||
path: src/Transforms/objects.php
|
||||
|
||||
-
|
||||
message: '#^Function Kadet\\Functional\\Transforms\\getter\(\) has parameter \$property with no type specified\.$#'
|
||||
identifier: missingType.parameter
|
||||
count: 1
|
||||
path: src/Transforms/objects.php
|
||||
|
||||
-
|
||||
message: '#^Function Kadet\\Functional\\Transforms\\method\(\) has no return type specified\.$#'
|
||||
identifier: missingType.return
|
||||
count: 1
|
||||
path: src/Transforms/objects.php
|
||||
|
||||
-
|
||||
message: '#^Function Kadet\\Functional\\Transforms\\method\(\) has parameter \$args with no type specified\.$#'
|
||||
identifier: missingType.parameter
|
||||
count: 1
|
||||
path: src/Transforms/objects.php
|
||||
|
||||
-
|
||||
message: '#^Function Kadet\\Functional\\Transforms\\method\(\) has parameter \$method with no type specified\.$#'
|
||||
identifier: missingType.parameter
|
||||
count: 1
|
||||
path: src/Transforms/objects.php
|
||||
|
||||
-
|
||||
message: '#^Function Kadet\\Functional\\Transforms\\property\(\) has no return type specified\.$#'
|
||||
identifier: missingType.return
|
||||
count: 1
|
||||
path: src/Transforms/objects.php
|
||||
|
||||
-
|
||||
message: '#^Function Kadet\\Functional\\Transforms\\property\(\) has parameter \$property with no type specified\.$#'
|
||||
identifier: missingType.parameter
|
||||
count: 1
|
||||
path: src/Transforms/objects.php
|
||||
|
||||
-
|
||||
message: '#^Method Kadet\\Functional\\Utils\\CurriedFunction\:\:__construct\(\) has parameter \$args with no value type specified in iterable type array\.$#'
|
||||
identifier: missingType.iterableValue
|
||||
count: 1
|
||||
path: src/Utils/CurriedFunction.php
|
||||
|
||||
-
|
||||
message: '#^Method Kadet\\Functional\\Utils\\CurriedFunction\:\:apply\(\) has no return type specified\.$#'
|
||||
identifier: missingType.return
|
||||
count: 1
|
||||
path: src/Utils/CurriedFunction.php
|
||||
|
||||
-
|
||||
message: '#^Method Kadet\\Functional\\Utils\\CurriedFunction\:\:apply\(\) has parameter \$args with no type specified\.$#'
|
||||
identifier: missingType.parameter
|
||||
count: 1
|
||||
path: src/Utils/CurriedFunction.php
|
||||
|
||||
-
|
||||
message: '#^Method Kadet\\Functional\\Utils\\CurriedFunction\:\:call\(\) has no return type specified\.$#'
|
||||
identifier: missingType.return
|
||||
count: 1
|
||||
path: src/Utils/CurriedFunction.php
|
||||
|
||||
-
|
||||
message: '#^Method Kadet\\Functional\\Utils\\CurriedFunction\:\:call\(\) has parameter \$args with no type specified\.$#'
|
||||
identifier: missingType.parameter
|
||||
count: 1
|
||||
path: src/Utils/CurriedFunction.php
|
||||
|
||||
-
|
||||
message: '#^Method Kadet\\Functional\\Utils\\CurriedFunction\:\:uncurry\(\) has no return type specified\.$#'
|
||||
identifier: missingType.return
|
||||
count: 1
|
||||
path: src/Utils/CurriedFunction.php
|
||||
|
||||
-
|
||||
message: '#^Method Kadet\\Functional\\Utils\\CurriedFunction\:\:withThreshold\(\) has no return type specified\.$#'
|
||||
identifier: missingType.return
|
||||
count: 1
|
||||
path: src/Utils/CurriedFunction.php
|
||||
|
||||
-
|
||||
message: '#^Property Kadet\\Functional\\Utils\\CurriedFunction\:\:\$threshold has no type specified\.$#'
|
||||
identifier: missingType.property
|
||||
count: 1
|
||||
path: src/Utils/CurriedFunction.php
|
||||
|
||||
-
|
||||
message: '#^Unsafe usage of new static\(\)\.$#'
|
||||
identifier: new.static
|
||||
count: 2
|
||||
path: src/Utils/CurriedFunction.php
|
||||
|
||||
-
|
||||
message: '#^Method Kadet\\Functional\\Utils\\FixedFunction\:\:__invoke\(\) has no return type specified\.$#'
|
||||
identifier: missingType.return
|
||||
count: 1
|
||||
path: src/Utils/FixedFunction.php
|
||||
|
||||
-
|
||||
message: '#^Method Kadet\\Functional\\Utils\\FixedFunction\:\:__invoke\(\) has parameter \$args with no type specified\.$#'
|
||||
identifier: missingType.parameter
|
||||
count: 1
|
||||
path: src/Utils/FixedFunction.php
|
||||
|
||||
-
|
||||
message: '#^Method Kadet\\Functional\\Utils\\FixedFunction\:\:getDecorated\(\) has no return type specified\.$#'
|
||||
identifier: missingType.return
|
||||
count: 1
|
||||
path: src/Utils/FixedFunction.php
|
||||
|
||||
-
|
||||
message: '#^Method Kadet\\Functional\\Utils\\FunctionPipeline\:\:__invoke\(\) has no return type specified\.$#'
|
||||
identifier: missingType.return
|
||||
count: 1
|
||||
path: src/Utils/FunctionPipeline.php
|
||||
|
||||
-
|
||||
message: '#^Method Kadet\\Functional\\Utils\\FunctionPipeline\:\:__invoke\(\) has parameter \$args with no type specified\.$#'
|
||||
identifier: missingType.parameter
|
||||
count: 1
|
||||
path: src/Utils/FunctionPipeline.php
|
||||
|
||||
-
|
||||
message: '#^Method Kadet\\Functional\\Utils\\FunctionPipeline\:\:append\(\) has no return type specified\.$#'
|
||||
identifier: missingType.return
|
||||
count: 1
|
||||
path: src/Utils/FunctionPipeline.php
|
||||
|
||||
-
|
||||
message: '#^Method Kadet\\Functional\\Utils\\FunctionPipeline\:\:prepend\(\) has no return type specified\.$#'
|
||||
identifier: missingType.return
|
||||
count: 1
|
||||
path: src/Utils/FunctionPipeline.php
|
||||
|
||||
-
|
||||
message: '#^PHPDoc tag @param for parameter \$functions with type array\<callable\> is not subtype of native type callable\.$#'
|
||||
identifier: parameter.phpDocType
|
||||
count: 1
|
||||
path: src/Utils/FunctionPipeline.php
|
||||
|
||||
-
|
||||
message: '#^Unsafe usage of new static\(\)\.$#'
|
||||
identifier: new.static
|
||||
count: 2
|
||||
path: src/Utils/FunctionPipeline.php
|
||||
|
||||
-
|
||||
message: '#^Method Kadet\\Functional\\Utils\\MemoizedFunction\:\:__invoke\(\) has no return type specified\.$#'
|
||||
identifier: missingType.return
|
||||
count: 1
|
||||
path: src/Utils/MemoizedFunction.php
|
||||
|
||||
-
|
||||
message: '#^Method Kadet\\Functional\\Utils\\MemoizedFunction\:\:__invoke\(\) has parameter \$args with no type specified\.$#'
|
||||
identifier: missingType.parameter
|
||||
count: 1
|
||||
path: src/Utils/MemoizedFunction.php
|
||||
|
||||
-
|
||||
message: '#^Property Kadet\\Functional\\Utils\\MemoizedFunction\:\:\$decorated has no type specified\.$#'
|
||||
identifier: missingType.property
|
||||
count: 1
|
||||
path: src/Utils/MemoizedFunction.php
|
||||
|
||||
-
|
||||
message: '#^Property Kadet\\Functional\\Utils\\MemoizedFunction\:\:\$memoized has no type specified\.$#'
|
||||
identifier: missingType.property
|
||||
count: 1
|
||||
path: src/Utils/MemoizedFunction.php
|
||||
|
||||
-
|
||||
message: '#^Method Kadet\\Functional\\Utils\\PartiallyAppliedFunction\:\:__construct\(\) has parameter \$args with no type specified\.$#'
|
||||
identifier: missingType.parameter
|
||||
count: 1
|
||||
path: src/Utils/PartiallyAppliedFunction.php
|
||||
|
||||
-
|
||||
message: '#^Method Kadet\\Functional\\Utils\\PartiallyAppliedFunction\:\:__invoke\(\) has no return type specified\.$#'
|
||||
identifier: missingType.return
|
||||
count: 1
|
||||
path: src/Utils/PartiallyAppliedFunction.php
|
||||
|
||||
-
|
||||
message: '#^Method Kadet\\Functional\\Utils\\PartiallyAppliedFunction\:\:__invoke\(\) has parameter \$args with no type specified\.$#'
|
||||
identifier: missingType.parameter
|
||||
count: 1
|
||||
path: src/Utils/PartiallyAppliedFunction.php
|
||||
|
||||
-
|
||||
message: '#^Method Kadet\\Functional\\Utils\\PartiallyAppliedFunction\:\:fix\(\) has no return type specified\.$#'
|
||||
identifier: missingType.return
|
||||
count: 1
|
||||
path: src/Utils/PartiallyAppliedFunction.php
|
||||
|
||||
-
|
||||
message: '#^Method Kadet\\Functional\\Utils\\PartiallyAppliedFunction\:\:fix\(\) has parameter \$args with no type specified\.$#'
|
||||
identifier: missingType.parameter
|
||||
count: 1
|
||||
path: src/Utils/PartiallyAppliedFunction.php
|
||||
|
||||
-
|
||||
message: '#^Method Kadet\\Functional\\Utils\\PartiallyAppliedFunction\:\:getArguments\(\) has no return type specified\.$#'
|
||||
identifier: missingType.return
|
||||
count: 1
|
||||
path: src/Utils/PartiallyAppliedFunction.php
|
||||
|
||||
-
|
||||
message: '#^Method Kadet\\Functional\\Utils\\PartiallyAppliedFunction\:\:getDecorated\(\) has no return type specified\.$#'
|
||||
identifier: missingType.return
|
||||
count: 1
|
||||
path: src/Utils/PartiallyAppliedFunction.php
|
||||
|
||||
-
|
||||
message: '#^Method Kadet\\Functional\\Utils\\PartiallyAppliedFunction\:\:prepareArguments\(\) has no return type specified\.$#'
|
||||
identifier: missingType.return
|
||||
count: 1
|
||||
path: src/Utils/PartiallyAppliedFunction.php
|
||||
|
||||
-
|
||||
message: '#^Method Kadet\\Functional\\Utils\\PartiallyAppliedFunction\:\:prepareArguments\(\) has parameter \$args with no type specified\.$#'
|
||||
identifier: missingType.parameter
|
||||
count: 1
|
||||
path: src/Utils/PartiallyAppliedFunction.php
|
||||
|
||||
-
|
||||
message: '#^Parameter \#1 \$array \(list\) of array_values is already a list, call has no effect\.$#'
|
||||
identifier: arrayValues.list
|
||||
count: 1
|
||||
path: src/Utils/PartiallyAppliedFunction.php
|
||||
|
||||
-
|
||||
message: '#^Property Kadet\\Functional\\Utils\\PartiallyAppliedFunction\:\:\$arguments has no type specified\.$#'
|
||||
identifier: missingType.property
|
||||
count: 1
|
||||
path: src/Utils/PartiallyAppliedFunction.php
|
||||
|
||||
-
|
||||
message: '#^Property Kadet\\Functional\\Utils\\PartiallyAppliedFunction\:\:\$callable has no type specified\.$#'
|
||||
identifier: missingType.property
|
||||
count: 1
|
||||
path: src/Utils/PartiallyAppliedFunction.php
|
||||
|
||||
-
|
||||
message: '#^Class Kadet\\Functional\\Utils\\UnpackedArgs extends generic class ArrayObject but does not specify its types\: TKey, TValue$#'
|
||||
identifier: missingType.generics
|
||||
count: 1
|
||||
path: src/Utils/UnpackedArgs.php
|
||||
|
||||
-
|
||||
message: '#^Function Kadet\\Functional\\apply\(\) has no return type specified\.$#'
|
||||
identifier: missingType.return
|
||||
count: 1
|
||||
path: src/Utils/index.php
|
||||
|
||||
-
|
||||
message: '#^Function Kadet\\Functional\\apply\(\) has parameter \$args with no type specified\.$#'
|
||||
identifier: missingType.parameter
|
||||
count: 1
|
||||
path: src/Utils/index.php
|
||||
|
||||
-
|
||||
message: '#^Function Kadet\\Functional\\applyWithArity\(\) has no return type specified\.$#'
|
||||
identifier: missingType.return
|
||||
count: 1
|
||||
path: src/Utils/index.php
|
||||
|
||||
-
|
||||
message: '#^Function Kadet\\Functional\\applyWithArity\(\) has parameter \$args with no value type specified in iterable type array\.$#'
|
||||
identifier: missingType.iterableValue
|
||||
count: 1
|
||||
path: src/Utils/index.php
|
||||
|
||||
-
|
||||
message: '#^Function Kadet\\Functional\\curry\(\) has parameter \$args with no type specified\.$#'
|
||||
identifier: missingType.parameter
|
||||
count: 1
|
||||
path: src/Utils/index.php
|
||||
|
||||
-
|
||||
message: '#^Function Kadet\\Functional\\fix\(\) has no return type specified\.$#'
|
||||
identifier: missingType.return
|
||||
count: 1
|
||||
path: src/Utils/index.php
|
||||
|
||||
-
|
||||
message: '#^Function Kadet\\Functional\\fix\(\) has parameter \$args with no type specified\.$#'
|
||||
identifier: missingType.parameter
|
||||
count: 1
|
||||
path: src/Utils/index.php
|
||||
|
||||
-
|
||||
message: '#^Function Kadet\\Functional\\memoize\(\) has no return type specified\.$#'
|
||||
identifier: missingType.return
|
||||
count: 1
|
||||
path: src/Utils/index.php
|
||||
|
||||
-
|
||||
message: '#^Function Kadet\\Functional\\partial\(\) has parameter \$args with no type specified\.$#'
|
||||
identifier: missingType.parameter
|
||||
count: 1
|
||||
path: src/Utils/index.php
|
||||
|
||||
-
|
||||
message: '#^Function Kadet\\Functional\\pipe\(\) has no return type specified\.$#'
|
||||
identifier: missingType.return
|
||||
count: 1
|
||||
path: src/Utils/index.php
|
||||
|
||||
-
|
||||
message: '#^Function Kadet\\Functional\\pipe\(\) has parameter \$functions with no type specified\.$#'
|
||||
identifier: missingType.parameter
|
||||
count: 1
|
||||
path: src/Utils/index.php
|
||||
|
||||
-
|
||||
message: '#^Function Kadet\\Functional\\all\(\) has no return type specified\.$#'
|
||||
identifier: missingType.return
|
||||
count: 1
|
||||
path: src/functions.php
|
||||
|
||||
-
|
||||
message: '#^Function Kadet\\Functional\\all\(\) has parameter \$predicates with no type specified\.$#'
|
||||
identifier: missingType.parameter
|
||||
count: 1
|
||||
path: src/functions.php
|
||||
|
||||
-
|
||||
message: '#^Function Kadet\\Functional\\always\(\) has no return type specified\.$#'
|
||||
identifier: missingType.return
|
||||
count: 1
|
||||
path: src/functions.php
|
||||
|
||||
-
|
||||
message: '#^Function Kadet\\Functional\\any\(\) has no return type specified\.$#'
|
||||
identifier: missingType.return
|
||||
count: 1
|
||||
path: src/functions.php
|
||||
|
||||
-
|
||||
message: '#^Function Kadet\\Functional\\any\(\) has parameter \$predicates with no type specified\.$#'
|
||||
identifier: missingType.parameter
|
||||
count: 1
|
||||
path: src/functions.php
|
||||
|
||||
-
|
||||
message: '#^Function Kadet\\Functional\\arify\(\) has no return type specified\.$#'
|
||||
identifier: missingType.return
|
||||
count: 1
|
||||
path: src/functions.php
|
||||
|
||||
-
|
||||
message: '#^Function Kadet\\Functional\\arify\(\) has parameter \$callable with no type specified\.$#'
|
||||
identifier: missingType.parameter
|
||||
count: 1
|
||||
path: src/functions.php
|
||||
|
||||
-
|
||||
message: '#^Function Kadet\\Functional\\closure\(\) has no return type specified\.$#'
|
||||
identifier: missingType.return
|
||||
count: 1
|
||||
path: src/functions.php
|
||||
|
||||
-
|
||||
message: '#^Function Kadet\\Functional\\closure\(\) has parameter \$callable with no type specified\.$#'
|
||||
identifier: missingType.parameter
|
||||
count: 1
|
||||
path: src/functions.php
|
||||
|
||||
-
|
||||
message: '#^Function Kadet\\Functional\\identity\(\) has no return type specified\.$#'
|
||||
identifier: missingType.return
|
||||
count: 1
|
||||
path: src/functions.php
|
||||
|
||||
-
|
||||
message: '#^Function Kadet\\Functional\\identity\(\) has parameter \$x with no type specified\.$#'
|
||||
identifier: missingType.parameter
|
||||
count: 1
|
||||
path: src/functions.php
|
||||
|
||||
-
|
||||
message: '#^Function Kadet\\Functional\\never\(\) has no return type specified\.$#'
|
||||
identifier: missingType.return
|
||||
count: 1
|
||||
path: src/functions.php
|
||||
|
||||
-
|
||||
message: '#^Function Kadet\\Functional\\not\(\) has no return type specified\.$#'
|
||||
identifier: missingType.return
|
||||
count: 1
|
||||
path: src/functions.php
|
||||
|
||||
-
|
||||
message: '#^Function Kadet\\Functional\\not\(\) has parameter \$predicate with no type specified\.$#'
|
||||
identifier: missingType.parameter
|
||||
count: 1
|
||||
path: src/functions.php
|
||||
|
||||
-
|
||||
message: '#^Function Kadet\\Functional\\predicate\(\) has parameter \$predicate with no type specified\.$#'
|
||||
identifier: missingType.parameter
|
||||
count: 1
|
||||
path: src/functions.php
|
||||
|
||||
-
|
||||
message: '#^Function Kadet\\Functional\\ref\(\) has no return type specified\.$#'
|
||||
identifier: missingType.return
|
||||
count: 1
|
||||
path: src/functions.php
|
||||
|
||||
-
|
||||
message: '#^Function Kadet\\Functional\\ref\(\) has parameter \$callable with no type specified\.$#'
|
||||
identifier: missingType.parameter
|
||||
count: 1
|
||||
path: src/functions.php
|
||||
|
||||
-
|
||||
message: '#^Function Kadet\\Functional\\repeat\(\) has no return type specified\.$#'
|
||||
identifier: missingType.return
|
||||
count: 1
|
||||
path: src/functions.php
|
||||
|
||||
-
|
||||
message: '#^Function Kadet\\Functional\\repeat\(\) has parameter \$times with no type specified\.$#'
|
||||
identifier: missingType.parameter
|
||||
count: 1
|
||||
path: src/functions.php
|
||||
|
||||
-
|
||||
message: '#^Function Kadet\\Functional\\symbol\(\) has no return type specified\.$#'
|
||||
identifier: missingType.return
|
||||
count: 1
|
||||
path: src/functions.php
|
||||
|
||||
-
|
||||
message: '#^Function Kadet\\Functional\\symbol\(\) has parameter \$name with no type specified\.$#'
|
||||
identifier: missingType.parameter
|
||||
count: 1
|
||||
path: src/functions.php
|
||||
|
||||
-
|
||||
message: '#^Function Kadet\\Functional\\unpack\(\) has no return type specified\.$#'
|
||||
identifier: missingType.return
|
||||
count: 1
|
||||
path: src/functions.php
|
||||
|
||||
-
|
||||
message: '#^Function Kadet\\Functional\\unpack\(\) has parameter \$arguments with no type specified\.$#'
|
||||
identifier: missingType.parameter
|
||||
count: 1
|
||||
path: src/functions.php
|
||||
|
||||
-
|
||||
message: '#^Interface Kadet\\Functional\\Predicate referenced with incorrect case\: Kadet\\Functional\\predicate\.$#'
|
||||
identifier: interface.nameCase
|
||||
count: 2
|
||||
path: src/functions.php
|
||||
27
phpstan.neon
Normal file
27
phpstan.neon
Normal file
@ -0,0 +1,27 @@
|
||||
parameters:
|
||||
level: 8
|
||||
# tests/types checks inference through the pipe operator, which needs PHP 8.5 to parse. PHPStan parses with the
|
||||
# lowest configured version, so this can't be a range starting at the library's PHP 8.2 floor; that floor is
|
||||
# covered by running the test suite on PHP 8.2.
|
||||
phpVersion: 80500
|
||||
paths:
|
||||
- src
|
||||
- tests/types
|
||||
ignoreErrors:
|
||||
-
|
||||
# Eager transforms are typed with a conditional closure type that maps the input container to the result
|
||||
# container (see keyedLike()/listLike(), which implement it). PHPStan can't check a closure literal
|
||||
# against such a type, so only that mismatch is ignored.
|
||||
identifier: return.type
|
||||
message: '#^Function Kadet\\Functional\\Collections\\\w+\(\) should return Closure<TIn of iterable>\(TIn\): \(TIn is array \?#'
|
||||
path: src/Collections/transforms.php
|
||||
-
|
||||
# toArray() keeps PHP's own coercion of bool, float and null keys, as an array literal would.
|
||||
identifier: offsetAccess.invalidOffset
|
||||
message: '#^Possibly invalid array key type bool\|float\|int\|string\|null\.$#'
|
||||
path: src/Collections/collectors.php
|
||||
|
||||
includes:
|
||||
# Errors in the code that predates PHPStan in this project (untyped pre-1.0 functions and classes). New code
|
||||
# must pass without entries here.
|
||||
- phpstan-baseline.neon
|
||||
@ -4,6 +4,9 @@
|
||||
<testsuite name="all">
|
||||
<directory>tests</directory>
|
||||
<exclude>tests/NoDs</exclude>
|
||||
<!-- Uses the pipe operator, so it only parses on PHP 8.5+: loaded by the <file> entry below. -->
|
||||
<exclude>tests/PipeSyntaxTest.php</exclude>
|
||||
<file phpVersion="8.5.0" phpVersionOperator=">=">tests/PipeSyntaxTest.php</file>
|
||||
</testsuite>
|
||||
</testsuites>
|
||||
<source>
|
||||
|
||||
@ -37,7 +37,9 @@ use function Kadet\Functional\compile;
|
||||
*
|
||||
* @param (callable(TValue, TKey): mixed)|null $predicate
|
||||
*
|
||||
* @return \Closure(iterable<TKey, TValue>): ?TValue
|
||||
* @return ($predicate is null
|
||||
* ? \Closure<V>(iterable<mixed, V>): ?V
|
||||
* : \Closure(iterable<TKey, TValue>): ?TValue)
|
||||
*
|
||||
* @throws EmptyCollectionException when `$required` is set and nothing matched
|
||||
*/
|
||||
@ -57,7 +59,9 @@ function first(?callable $predicate = null, bool $required = false): \Closure
|
||||
*
|
||||
* @param (callable(TValue, TKey): mixed)|null $predicate
|
||||
*
|
||||
* @return \Closure(iterable<TKey, TValue>): ?TValue
|
||||
* @return ($predicate is null
|
||||
* ? \Closure<V>(iterable<mixed, V>): ?V
|
||||
* : \Closure(iterable<TKey, TValue>): ?TValue)
|
||||
*
|
||||
* @throws EmptyCollectionException when `$required` is set and nothing matched
|
||||
*/
|
||||
@ -118,10 +122,7 @@ function find(callable $predicate, bool $required = false): \Closure
|
||||
/**
|
||||
* Whether the collection holds `$value`, compared strictly (`Ds\Key::equals` when `$value` implements `Ds\Key`).
|
||||
*
|
||||
* @template TKey
|
||||
* @template TValue
|
||||
*
|
||||
* @return \Closure(iterable<TKey, TValue>): bool
|
||||
* @return \Closure(iterable<mixed, mixed>): bool
|
||||
*/
|
||||
function contains(mixed $value): \Closure
|
||||
{
|
||||
@ -247,12 +248,17 @@ function isEmpty(): \Closure
|
||||
/**
|
||||
* The smallest value (compared with `<=>`), or the smallest of `$selector($value, $key)` when given. Null when empty.
|
||||
*
|
||||
* With a selector this returns the selected value, not the element it was selected from.
|
||||
*
|
||||
* @template TKey
|
||||
* @template TValue
|
||||
* @template TSelected
|
||||
*
|
||||
* @param (callable(TValue, TKey): mixed)|null $selector
|
||||
* @param (callable(TValue, TKey): TSelected)|null $selector
|
||||
*
|
||||
* @return \Closure(iterable<TKey, TValue>): mixed
|
||||
* @return ($selector is null
|
||||
* ? \Closure<V>(iterable<mixed, V>): ?V
|
||||
* : \Closure(iterable<TKey, TValue>): ?TSelected)
|
||||
*/
|
||||
function min(?callable $selector = null): \Closure
|
||||
{
|
||||
@ -262,12 +268,17 @@ function min(?callable $selector = null): \Closure
|
||||
/**
|
||||
* The largest value (compared with `<=>`), or the largest of `$selector($value, $key)` when given. Null when empty.
|
||||
*
|
||||
* With a selector this returns the selected value, not the element it was selected from.
|
||||
*
|
||||
* @template TKey
|
||||
* @template TValue
|
||||
* @template TSelected
|
||||
*
|
||||
* @param (callable(TValue, TKey): mixed)|null $selector
|
||||
* @param (callable(TValue, TKey): TSelected)|null $selector
|
||||
*
|
||||
* @return \Closure(iterable<TKey, TValue>): mixed
|
||||
* @return ($selector is null
|
||||
* ? \Closure<V>(iterable<mixed, V>): ?V
|
||||
* : \Closure(iterable<TKey, TValue>): ?TSelected)
|
||||
*/
|
||||
function max(?callable $selector = null): \Closure
|
||||
{
|
||||
@ -307,9 +318,11 @@ function reduce(callable $fn, mixed $initial = null): \Closure
|
||||
* @template TKey
|
||||
* @template TValue
|
||||
*
|
||||
* @param (callable(TValue, TKey): int|float)|null $selector
|
||||
* @param (callable(TValue, TKey): (int|float))|null $selector
|
||||
*
|
||||
* @return \Closure(iterable<TKey, TValue>): (int|float)
|
||||
* @return ($selector is null
|
||||
* ? \Closure(iterable<mixed, int|float>): (int|float)
|
||||
* : \Closure(iterable<TKey, TValue>): (int|float))
|
||||
*/
|
||||
function sum(?callable $selector = null): \Closure
|
||||
{
|
||||
|
||||
@ -40,7 +40,13 @@ use function Kadet\Functional\compile;
|
||||
*
|
||||
* @param callable(TValue, TKey): TResult $fn
|
||||
*
|
||||
* @return \Closure(iterable<TKey, TValue>): iterable<TKey, TResult>
|
||||
* @return \Closure<TIn of iterable<mixed, mixed>>(TIn): (TIn is array
|
||||
* ? array<TKey, TResult>
|
||||
* : (TIn is \Ds\Map<mixed, mixed>
|
||||
* ? \Ds\Map<TKey, TResult>
|
||||
* : (TIn is \Ds\Set<mixed>
|
||||
* ? \Ds\Set<TResult>
|
||||
* : (TIn is \Ds\Seq<mixed> ? \Ds\Seq<TResult> : \Ds\Seq<TResult>|list<TResult>))))
|
||||
*/
|
||||
function map(callable $fn): \Closure
|
||||
{
|
||||
@ -59,7 +65,13 @@ function map(callable $fn): \Closure
|
||||
*
|
||||
* @param (callable(TValue, TKey): mixed)|null $fn
|
||||
*
|
||||
* @return \Closure(iterable<TKey, TValue>): iterable<TKey, TValue>
|
||||
* @return \Closure<TIn of iterable<mixed, mixed>>(TIn): (TIn is array
|
||||
* ? array<TKey, TValue>
|
||||
* : (TIn is \Ds\Map<mixed, mixed>
|
||||
* ? \Ds\Map<TKey, TValue>
|
||||
* : (TIn is \Ds\Set<mixed>
|
||||
* ? \Ds\Set<TValue>
|
||||
* : (TIn is \Ds\Seq<mixed> ? \Ds\Seq<TValue> : \Ds\Seq<TValue>|list<TValue>))))
|
||||
*/
|
||||
function filter(?callable $fn = null): \Closure
|
||||
{
|
||||
@ -76,7 +88,13 @@ function filter(?callable $fn = null): \Closure
|
||||
*
|
||||
* @param callable(TValue, TKey): mixed $fn
|
||||
*
|
||||
* @return \Closure(iterable<TKey, TValue>): iterable<TKey, TValue>
|
||||
* @return \Closure<TIn of iterable<mixed, mixed>>(TIn): (TIn is array
|
||||
* ? array<TKey, TValue>
|
||||
* : (TIn is \Ds\Map<mixed, mixed>
|
||||
* ? \Ds\Map<TKey, TValue>
|
||||
* : (TIn is \Ds\Set<mixed>
|
||||
* ? \Ds\Set<TValue>
|
||||
* : (TIn is \Ds\Seq<mixed> ? \Ds\Seq<TValue> : \Ds\Seq<TValue>|list<TValue>))))
|
||||
*/
|
||||
function reject(callable $fn): \Closure
|
||||
{
|
||||
@ -96,7 +114,11 @@ function reject(callable $fn): \Closure
|
||||
*
|
||||
* @param callable(TValue, TKey): iterable<mixed, TResult> $fn
|
||||
*
|
||||
* @return \Closure(iterable<TKey, TValue>): iterable<int, TResult>
|
||||
* @return \Closure<TIn of iterable<mixed, mixed>>(TIn): (TIn is array
|
||||
* ? list<TResult>
|
||||
* : (TIn is \Ds\Set<mixed>
|
||||
* ? \Ds\Set<TResult>
|
||||
* : (TIn is \Ds\Seq<mixed>|\Ds\Map<mixed, mixed> ? \Ds\Seq<TResult> : \Ds\Seq<TResult>|list<TResult>)))
|
||||
*/
|
||||
function flatMap(callable $fn): \Closure
|
||||
{
|
||||
@ -152,7 +174,13 @@ function unique(): \Closure
|
||||
*
|
||||
* @param callable(TValue, TKey): mixed $selector
|
||||
*
|
||||
* @return \Closure(iterable<TKey, TValue>): iterable<TKey, TValue>
|
||||
* @return \Closure<TIn of iterable<mixed, mixed>>(TIn): (TIn is array
|
||||
* ? array<TKey, TValue>
|
||||
* : (TIn is \Ds\Map<mixed, mixed>
|
||||
* ? \Ds\Map<TKey, TValue>
|
||||
* : (TIn is \Ds\Set<mixed>
|
||||
* ? \Ds\Set<TValue>
|
||||
* : (TIn is \Ds\Seq<mixed> ? \Ds\Seq<TValue> : \Ds\Seq<TValue>|list<TValue>))))
|
||||
*/
|
||||
function uniqueBy(callable $selector): \Closure
|
||||
{
|
||||
@ -196,7 +224,13 @@ function sort(?callable $comparator = null): \Closure
|
||||
*
|
||||
* @param callable(TValue, TKey): mixed $selector
|
||||
*
|
||||
* @return \Closure(iterable<TKey, TValue>): iterable<TKey, TValue>
|
||||
* @return \Closure<TIn of iterable<mixed, mixed>>(TIn): (TIn is array
|
||||
* ? array<TKey, TValue>
|
||||
* : (TIn is \Ds\Map<mixed, mixed>
|
||||
* ? \Ds\Map<TKey, TValue>
|
||||
* : (TIn is \Ds\Set<mixed>
|
||||
* ? \Ds\Set<TValue>
|
||||
* : (TIn is \Ds\Seq<mixed> ? \Ds\Seq<TValue> : \Ds\Seq<TValue>|list<TValue>))))
|
||||
*/
|
||||
function sortBy(callable $selector, bool $descending = false): \Closure
|
||||
{
|
||||
@ -331,7 +365,13 @@ function merge(iterable $other): \Closure
|
||||
*
|
||||
* @param callable(TValue, TKey): mixed $fn
|
||||
*
|
||||
* @return \Closure(iterable<TKey, TValue>): iterable<TKey, TValue>
|
||||
* @return \Closure<TIn of iterable<mixed, mixed>>(TIn): (TIn is array
|
||||
* ? array<TKey, TValue>
|
||||
* : (TIn is \Ds\Map<mixed, mixed>
|
||||
* ? \Ds\Map<TKey, TValue>
|
||||
* : (TIn is \Ds\Set<mixed>
|
||||
* ? \Ds\Set<TValue>
|
||||
* : (TIn is \Ds\Seq<mixed> ? \Ds\Seq<TValue> : \Ds\Seq<TValue>|list<TValue>))))
|
||||
*/
|
||||
function each(callable $fn): \Closure
|
||||
{
|
||||
@ -358,7 +398,13 @@ function each(callable $fn): \Closure
|
||||
*
|
||||
* @param callable(TValue, TKey): mixed $fn
|
||||
*
|
||||
* @return \Closure(iterable<TKey, TValue>): iterable<TKey, TValue>
|
||||
* @return \Closure<TIn of iterable<mixed, mixed>>(TIn): (TIn is array
|
||||
* ? array<TKey, TValue>
|
||||
* : (TIn is \Ds\Map<mixed, mixed>
|
||||
* ? \Ds\Map<TKey, TValue>
|
||||
* : (TIn is \Ds\Set<mixed>
|
||||
* ? \Ds\Set<TValue>
|
||||
* : (TIn is \Ds\Seq<mixed> ? \Ds\Seq<TValue> : \Ds\Seq<TValue>|list<TValue>))))
|
||||
*/
|
||||
function tap(callable $fn): \Closure
|
||||
{
|
||||
|
||||
89
tests/PipeSyntaxTest.php
Normal file
89
tests/PipeSyntaxTest.php
Normal file
@ -0,0 +1,89 @@
|
||||
<?php
|
||||
/**
|
||||
* Copyright 2026 Kacper Donat
|
||||
*
|
||||
* @author Kacper "Kadet" Donat <kacper@kadet.net>
|
||||
*
|
||||
* Full license available in separate LICENSE file
|
||||
*/
|
||||
|
||||
/*
|
||||
* The only test written with the pipe operator, which needs PHP 8.5 to parse. phpunit.xml loads this file only on
|
||||
* PHP 8.5 and newer, so the rest of the suite still runs on PHP 8.2-8.4.
|
||||
*/
|
||||
|
||||
use Ds\Map;
|
||||
use Kadet\Functional\Collections as c;
|
||||
use Kadet\Functional\Collections\Lazy as lazy;
|
||||
use Kadet\Functional\Exception\EmptyCollectionException;
|
||||
use Kadet\Functional\Predicates as p;
|
||||
use Kadet\Functional\Transforms as t;
|
||||
use PHPUnit\Framework\Attributes\RequiresPhp;
|
||||
|
||||
final class PipeDeparture
|
||||
{
|
||||
public function __construct(
|
||||
public readonly string $id,
|
||||
public readonly string $line,
|
||||
public readonly string $destination,
|
||||
public readonly int $delay,
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
||||
#[RequiresPhp('>= 8.5')]
|
||||
class PipeSyntaxTest extends \PHPUnit\Framework\TestCase
|
||||
{
|
||||
/** @return list<PipeDeparture> */
|
||||
private static function departures(): array
|
||||
{
|
||||
return [
|
||||
new PipeDeparture('d1', '8', 'Stogi', 0),
|
||||
new PipeDeparture('d2', '8', 'Stogi', 120),
|
||||
new PipeDeparture('d3', '12', 'Oliwa', 30),
|
||||
new PipeDeparture('d4', '12', 'Oliwa', 300),
|
||||
new PipeDeparture('d5', '6', 'Łostowice', 0),
|
||||
];
|
||||
}
|
||||
|
||||
public function testRealisticPipeline()
|
||||
{
|
||||
$late = self::departures()
|
||||
|> lazy\filter(p\property('delay', p\gt(60)))
|
||||
|> c\keyBy(t\property('id'))
|
||||
|> c\map(fn (PipeDeparture $departure) => "{$departure->line} → {$departure->destination}");
|
||||
|
||||
$this->assertInstanceOf(Map::class, $late);
|
||||
$this->assertSame(['d2' => '8 → Stogi', 'd4' => '12 → Oliwa'], $late->toArray());
|
||||
}
|
||||
|
||||
public function testFirstRequired()
|
||||
{
|
||||
$mostDelayed = self::departures()
|
||||
|> c\sortBy(t\property('delay'), descending: true)
|
||||
|> c\first(required: true);
|
||||
|
||||
$this->assertSame('d4', $mostDelayed->id);
|
||||
}
|
||||
|
||||
public function testFirstRequiredThrowsWhenNothingMatches()
|
||||
{
|
||||
$this->expectException(EmptyCollectionException::class);
|
||||
$this->expectExceptionMessage('first(): no element matched');
|
||||
|
||||
self::departures() |> c\first(p\property('line', p\same('2')), required: true);
|
||||
}
|
||||
|
||||
public function testLazyChainReadsOnlyWhatIsNeeded()
|
||||
{
|
||||
$read = [];
|
||||
|
||||
$line = self::departures()
|
||||
|> lazy\tap(function (PipeDeparture $departure) use (&$read) { $read[] = $departure->id; })
|
||||
|> lazy\map(t\property('line'))
|
||||
|> c\find(fn (string $line) => $line === '12');
|
||||
|
||||
$this->assertSame('12', $line);
|
||||
$this->assertSame(['d1', 'd2', 'd3'], $read);
|
||||
}
|
||||
}
|
||||
117
tests/types/collections.php
Normal file
117
tests/types/collections.php
Normal file
@ -0,0 +1,117 @@
|
||||
<?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 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));
|
||||
}
|
||||
|
||||
/**
|
||||
* 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));
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user