Skip to content

Commit 8ff4eed

Browse files
committed
Merge branch '0.1.x' into 0.2.x
2 parents 9e1fc73 + 4e90318 commit 8ff4eed

44 files changed

Lines changed: 1019 additions & 156 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎bin/NarrowingGenerator.php‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,7 @@ public static function generateSetNarrowing(string $collectionContent, array $bl
203203
': Collection' => ': Set',
204204
'Collection<E>' => 'Set<E>',
205205
'Collection<(E is null ? never : E)>' => 'Set<(E is null ? never : E)>',
206+
'Collection<(E is iterable<mixed> ? value-of<E|array{}> : E)>' => 'Set<(E is iterable<mixed> ? value-of<E|array{}> : E)>',
206207
'Collection<R>' => 'Set<R>',
207208
'Collection<mixed>' => 'Set<mixed>',
208209
'Collection<T>' => 'Set<T>',
@@ -233,6 +234,7 @@ public static function generateImmutableSetNarrowing(string $immutableCollection
233234
': ImmutableCollection' => ': ImmutableSet',
234235
'ImmutableCollection<E>' => 'ImmutableSet<E>',
235236
'ImmutableCollection<(E is null ? never : E)>' => 'ImmutableSet<(E is null ? never : E)>',
237+
'ImmutableCollection<(E is iterable<mixed> ? value-of<E|array{}> : E)>' => 'ImmutableSet<(E is iterable<mixed> ? value-of<E|array{}> : E)>',
236238
'ImmutableCollection<E|NE>' => 'ImmutableSet<E|NE>',
237239
'ImmutableCollection<R>' => 'ImmutableSet<R>',
238240
'ImmutableCollection<mixed>' => 'ImmutableSet<mixed>',
@@ -288,6 +290,7 @@ public static function generateListNarrowing(string $collectionContent, array $b
288290
': Collection' => ': ListInterface',
289291
'Collection<E>' => 'ListInterface<E>',
290292
'Collection<(E is null ? never : E)>' => 'ListInterface<(E is null ? never : E)>',
293+
'Collection<(E is iterable<mixed> ? value-of<E|array{}> : E)>' => 'ListInterface<(E is iterable<mixed> ? value-of<E|array{}> : E)>',
291294
'Collection<R>' => 'ListInterface<R>',
292295
'Collection<mixed>' => 'ListInterface<mixed>',
293296
'Collection<T>' => 'ListInterface<T>',
@@ -318,6 +321,7 @@ public static function generateImmutableCollectionNarrowing(string $collectionCo
318321
': Collection' => ': ImmutableCollection',
319322
'Collection<E>' => 'ImmutableCollection<E>',
320323
'Collection<(E is null ? never : E)>' => 'ImmutableCollection<(E is null ? never : E)>',
324+
'Collection<(E is iterable<mixed> ? value-of<E|array{}> : E)>' => 'ImmutableCollection<(E is iterable<mixed> ? value-of<E|array{}> : E)>',
321325
'Collection<R>' => 'ImmutableCollection<R>',
322326
'Collection<mixed>' => 'ImmutableCollection<mixed>',
323327
'Collection<T>' => 'ImmutableCollection<T>',
@@ -345,6 +349,7 @@ public static function generateWritableCollectionTransformationNarrowing(string
345349
': Collection' => ': ImmutableCollection',
346350
'Collection<E>' => 'ImmutableCollection<E>',
347351
'Collection<(E is null ? never : E)>' => 'ImmutableCollection<(E is null ? never : E)>',
352+
'Collection<(E is iterable<mixed> ? value-of<E|array{}> : E)>' => 'ImmutableCollection<(E is iterable<mixed> ? value-of<E|array{}> : E)>',
348353
'Collection<R>' => 'ImmutableCollection<R>',
349354
'Collection<mixed>' => 'ImmutableCollection<mixed>',
350355
'Collection<T>' => 'ImmutableCollection<T>',
@@ -372,6 +377,7 @@ public static function generateWritableListTransformationNarrowing(string $colle
372377
': Collection' => ': ImmutableList',
373378
'Collection<E>' => 'ImmutableList<E>',
374379
'Collection<(E is null ? never : E)>' => 'ImmutableList<(E is null ? never : E)>',
380+
'Collection<(E is iterable<mixed> ? value-of<E|array{}> : E)>' => 'ImmutableList<(E is iterable<mixed> ? value-of<E|array{}> : E)>',
375381
'Collection<R>' => 'ImmutableList<R>',
376382
'Collection<mixed>' => 'ImmutableList<mixed>',
377383
'Collection<T>' => 'ImmutableList<T>',
@@ -399,6 +405,7 @@ public static function generateWritableSetTransformationNarrowing(string $collec
399405
': Collection' => ': ImmutableSet',
400406
'Collection<E>' => 'ImmutableSet<E>',
401407
'Collection<(E is null ? never : E)>' => 'ImmutableSet<(E is null ? never : E)>',
408+
'Collection<(E is iterable<mixed> ? value-of<E|array{}> : E)>' => 'ImmutableSet<(E is iterable<mixed> ? value-of<E|array{}> : E)>',
402409
'Collection<R>' => 'ImmutableSet<R>',
403410
'Collection<mixed>' => 'ImmutableSet<mixed>',
404411
'Collection<T>' => 'ImmutableSet<T>',
@@ -450,6 +457,7 @@ public static function generateImmutableListNarrowing(string $immutableCollectio
450457
': ImmutableCollection' => ': ImmutableList',
451458
'ImmutableCollection<E>' => 'ImmutableList<E>',
452459
'ImmutableCollection<(E is null ? never : E)>' => 'ImmutableList<(E is null ? never : E)>',
460+
'ImmutableCollection<(E is iterable<mixed> ? value-of<E|array{}> : E)>' => 'ImmutableList<(E is iterable<mixed> ? value-of<E|array{}> : E)>',
453461
'ImmutableCollection<E|NE>' => 'ImmutableList<E|NE>',
454462
'ImmutableCollection<R>' => 'ImmutableList<R>',
455463
'ImmutableCollection<mixed>' => 'ImmutableList<mixed>',

‎docs/collection/api/collection.md‎

Lines changed: 17 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ count(): int
101101
Returns the number of elements in the collection.
102102

103103
```php
104-
countWhere(Closure $predicate): int
104+
countWhere(Closure $predicate): int<0, max>
105105
```
106106
Returns the number of elements matching the predicate `(E, int): bool`.
107107

@@ -135,7 +135,7 @@ Reduce, or `null` if empty.
135135
```php
136136
sum(?Closure $selector = null): int|float
137137
```
138-
Sum of all elements, or values returned by the selector `(E, int): int|float`.
138+
Sum of all elements, or values returned by the selector `(E, int): int|float`. The return type narrows to `int` when every summed value is an `int` — a `Collection<int>`, or a selector declared to return `int` — and stays `int|float` otherwise.
139139

140140
```php
141141
avg(?Closure $selector = null): float
@@ -168,22 +168,22 @@ maxOrNull(?Closure $selector = null): E|null
168168
Maximum element, or `null` if empty. Optional selector `(E, int): mixed`.
169169

170170
```php
171-
minOf(Closure $selector): mixed
171+
minOf(Closure $selector): R
172172
```
173-
Returns the minimum value produced by the selector `(E, int): mixed`. Unlike `min()`, returns the **selector value** itself, not the element. Throws `NoSuchElementException` if empty.
173+
Returns the minimum value produced by the selector `(E, int): R`. Unlike `min()`, returns the **selector value** itself, not the element. The return type follows the selector — one declared `: int` yields `int`. Throws `NoSuchElementException` if empty.
174174

175175
```php
176-
minOfOrNull(Closure $selector): mixed
176+
minOfOrNull(Closure $selector): R|null
177177
```
178178
Returns the minimum selector value, or `null` if empty.
179179

180180
```php
181-
maxOf(Closure $selector): mixed
181+
maxOf(Closure $selector): R
182182
```
183-
Returns the maximum value produced by the selector `(E, int): mixed`. Unlike `max()`, returns the **selector value** itself, not the element. Throws `NoSuchElementException` if empty.
183+
Returns the maximum value produced by the selector `(E, int): R`. Unlike `max()`, returns the **selector value** itself, not the element. The return type follows the selector — one declared `: int` yields `int`. Throws `NoSuchElementException` if empty.
184184

185185
```php
186-
maxOfOrNull(Closure $selector): mixed
186+
maxOfOrNull(Closure $selector): R|null
187187
```
188188
Returns the maximum selector value, or `null` if empty.
189189

@@ -216,7 +216,7 @@ Filter by predicate `(E, int): bool`. Returns `ImmutableList` for lists, `Immuta
216216
```php
217217
filterNotNull(): Collection<E>
218218
```
219-
Filter out `null` elements.
219+
Filter out `null` elements. The element type narrows to exclude `null` — `Collection<string|null>` becomes `Collection<string>`.
220220

221221
```php
222222
filterInstanceOf(string $type): Collection<T>
@@ -239,9 +239,9 @@ flatMap(Closure $transform): Collection<R>
239239
Transform and flatten. Closure: `(E, int): iterable<R>`.
240240

241241
```php
242-
flatten(): Collection<mixed>
242+
flatten(): Collection<V>
243243
```
244-
Flatten a collection of iterables.
244+
Flatten a collection of iterables, one level deep. Iterable elements contribute their own values (`V` is the element type of `E`); non-iterable elements are kept as-is. So `Collection<ImmutableList<int>>` becomes `Collection<int>`, `Collection<array<int>|string>` becomes `Collection<int|string>`, and `Collection<string>` stays `Collection<string>`. Only one level is removed — flattening `Collection<ImmutableList<ImmutableList<int>>>` yields `Collection<ImmutableList<int>>`.
245245

246246
```php
247247
takeFirst(int $n = 1): Collection<E>
@@ -333,19 +333,19 @@ groupBy(Closure $keySelector, ?Closure $valueTransform = null): Map<K, Collectio
333333
Group elements by key selector `(E, int): K`. When a `$valueTransform` `(E, int): V` is provided, each element is transformed before being added to its group — the result is `Map<K, ImmutableList<V>>`.
334334

335335
```php
336-
intersect(iterable $other): ImmutableSet<E>
336+
intersect(iterable<V> $other): ImmutableSet<E&V>
337337
```
338-
Elements present in both this collection and the iterable. Returns a set (duplicates removed).
338+
Elements present in both this collection and the iterable. Returns a set (duplicates removed). The element type narrows to `E&V` — only values that can belong to both sides.
339339

340340
```php
341-
union(iterable $other): ImmutableSet<E>
341+
union(iterable<NE> $other): ImmutableSet<E|NE>
342342
```
343-
All elements from both this collection and the iterable. Returns a set (duplicates removed).
343+
All elements from both this collection and the iterable. Returns a set (duplicates removed). The element type widens to `E|NE`, like `add()`.
344344

345345
```php
346-
subtract(iterable $other): ImmutableSet<E>
346+
subtract(iterable<mixed> $other): ImmutableSet<E>
347347
```
348-
Elements present in this collection but not in the iterable. Returns a set (duplicates removed).
348+
Elements present in this collection but not in the iterable. Returns a set (duplicates removed). The element type is always `E` — any iterable may be subtracted.
349349

350350
## Ordering
351351

‎docs/collection/api/map.md‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ count(): int
9999
Returns the number of entries in the map.
100100

101101
```php
102-
countWhere(Closure $predicate): int
102+
countWhere(Closure $predicate): int<0, max>
103103
```
104104
Returns the number of entries matching the predicate `(V, K): bool`.
105105

@@ -135,7 +135,7 @@ Filter by value predicate `(V): bool`.
135135
```php
136136
filterValuesNotNull(): ImmutableMap<K,V>
137137
```
138-
Exclude entries with `null` values.
138+
Exclude entries with `null` values. The value type narrows to exclude `null` — `Map<string, int|null>` becomes `Map<string, int>`. Keys are preserved.
139139

140140
```php
141141
filterValuesInstanceOf(string $type): ImmutableMap<K,T>
@@ -235,9 +235,9 @@ flatMap(Closure $transform): ImmutableList<R>
235235
Transform each entry with `(V, K): iterable<R>` and flatten all results into a single list.
236236

237237
```php
238-
toArray(KeyCollisionStrategy $onCollision = KeyCollisionStrategy::Throw): array
238+
toArray(KeyCollisionStrategy $onCollision = KeyCollisionStrategy::Throw): array<K,V>
239239
```
240-
Convert to PHP array. Only works with scalar keys. Throws `ConversionException` for object keys or key collisions.
240+
Convert to PHP array. Only works with scalar keys. Throws `ConversionException` for object keys or key collisions. The key type is preserved when `K` is a valid array key — a `Map<string, int>` yields `array<string, int>`; a map with keys PHP cannot use natively falls back to `array<array-key, V>`.
241241

242242
```php
243243
toPairs(): list<array{0:K, 1:V}>

‎phpstan.neon‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,15 @@ parameters:
3838
- src/Set/Set.php
3939
- src/Set/ImmutableSet.php
4040
- src/Set/WritableSet.php
41+
# flatten()'s conditional return type cannot be evaluated in classes with a
42+
# concrete E (the subject is already substituted), so the trait vs ancestor
43+
# comparison sees structurally different but semantically equal conditionals.
44+
-
45+
message: '#::flatten\(\)#'
46+
identifier: method.childReturnType
47+
paths:
48+
- src/List/ListLogic.php
49+
- src/Set/SetLogic.php
4150
# classes which implement ImmutableMap<string,...>. The trait's put()/putFirst()
4251
# accepts string|int|bool|float|object but the store expects string.
4352
-

‎src/Collection.php‎

Lines changed: 23 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,7 @@ public function count(): int;
190190
* Returns the number of elements matching the predicate.
191191
*
192192
* @param Closure(E, int):bool $predicate
193+
* @return int<0, max>
193194
*/
194195
public function countWhere(Closure $predicate): int;
195196

@@ -292,35 +293,39 @@ public function maxOrNull(?Closure $selector = null): mixed;
292293
* Returns the minimum value produced by the selector.
293294
* Throws if the collection is empty.
294295
*
295-
* @param Closure(E, int):mixed $selector
296-
* @return mixed
296+
* @template R of mixed
297+
* @param Closure(E, int):R $selector
298+
* @return R
297299
* @throws NoSuchElementException
298300
*/
299301
public function minOf(Closure $selector): mixed;
300302

301303
/**
302304
* Returns the minimum value produced by the selector, or null if empty.
303305
*
304-
* @param Closure(E, int):mixed $selector
305-
* @return mixed
306+
* @template R of mixed
307+
* @param Closure(E, int):R $selector
308+
* @return R|null
306309
*/
307310
public function minOfOrNull(Closure $selector): mixed;
308311

309312
/**
310313
* Returns the maximum value produced by the selector.
311314
* Throws if the collection is empty.
312315
*
313-
* @param Closure(E, int):mixed $selector
314-
* @return mixed
316+
* @template R of mixed
317+
* @param Closure(E, int):R $selector
318+
* @return R
315319
* @throws NoSuchElementException
316320
*/
317321
public function maxOf(Closure $selector): mixed;
318322

319323
/**
320324
* Returns the maximum value produced by the selector, or null if empty.
321325
*
322-
* @param Closure(E, int):mixed $selector
323-
* @return mixed
326+
* @template R of mixed
327+
* @param Closure(E, int):R $selector
328+
* @return R|null
324329
*/
325330
public function maxOfOrNull(Closure $selector): mixed;
326331

@@ -409,8 +414,10 @@ public function flatMap(Closure $transform): Collection;
409414

410415
/**
411416
* Flatten a collection of iterables into a single collection.
417+
* Iterable elements are flattened one level; non-iterable elements are kept as-is.
418+
* The array{} in value-of keeps the type resolvable when E is never (empty collections).
412419
*
413-
* @return Collection<mixed>
420+
* @return Collection<(E is iterable<mixed> ? value-of<E|array{}> : E)>
414421
*/
415422
#[NoDiscard]
416423
public function flatten(): Collection;
@@ -582,25 +589,27 @@ public function groupBy(Closure $keySelector, ?Closure $valueTransform = null):
582589
/**
583590
* Returns a set containing only elements present in both this collection and the given iterable.
584591
*
585-
* @param iterable<E> $other
586-
* @return Set<E>
592+
* @template V
593+
* @param iterable<V> $other
594+
* @return Set<E&V>
587595
*/
588596
#[NoDiscard]
589597
public function intersect(iterable $other): Set;
590598

591599
/**
592600
* Returns a set containing all elements from both this collection and the given iterable.
593601
*
594-
* @param iterable<E> $other
595-
* @return Set<E>
602+
* @template NE
603+
* @param iterable<NE> $other
604+
* @return Set<E|NE>
596605
*/
597606
#[NoDiscard]
598607
public function union(iterable $other): Set;
599608

600609
/**
601610
* Returns a set containing elements present in this collection but not in the given iterable.
602611
*
603-
* @param iterable<E> $other
612+
* @param iterable<mixed> $other
604613
* @return Set<E>
605614
*/
606615
#[NoDiscard]

‎src/CollectionLogic.php‎

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -957,7 +957,9 @@ public function groupBy(Closure $keySelector, ?Closure $valueTransform = null):
957957
/**
958958
* {@inheritDoc}
959959
*
960-
* @return ImmutableSet<E>
960+
* @template U
961+
* @param iterable<U> $other
962+
* @return ImmutableSet<E&U>
961963
*/
962964
#[NoDiscard]
963965
public function intersect(iterable $other): ImmutableSet
@@ -968,7 +970,9 @@ public function intersect(iterable $other): ImmutableSet
968970
/**
969971
* {@inheritDoc}
970972
*
971-
* @return ImmutableSet<E>
973+
* @template NE
974+
* @param iterable<NE> $other
975+
* @return ImmutableSet<E|NE>
972976
*/
973977
#[NoDiscard]
974978
public function union(iterable $other): ImmutableSet
@@ -979,6 +983,7 @@ public function union(iterable $other): ImmutableSet
979983
/**
980984
* {@inheritDoc}
981985
*
986+
* @param iterable<mixed> $other
982987
* @return ImmutableSet<E>
983988
*/
984989
#[NoDiscard]

‎src/ImmutableCollection.php‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -192,8 +192,10 @@ public function flatMap(Closure $transform): ImmutableCollection;
192192

193193
/**
194194
* Flatten a collection of iterables into a single collection.
195+
* Iterable elements are flattened one level; non-iterable elements are kept as-is.
196+
* The array{} in value-of keeps the type resolvable when E is never (empty collections).
195197
*
196-
* @return ImmutableCollection<mixed>
198+
* @return ImmutableCollection<(E is iterable<mixed> ? value-of<E|array{}> : E)>
197199
*/
198200
#[NoDiscard]
199201
public function flatten(): ImmutableCollection;

‎src/List/ImmutableList.php‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -252,8 +252,10 @@ public function flatMap(Closure $transform): ImmutableList;
252252

253253
/**
254254
* Flatten a collection of iterables into a single collection.
255+
* Iterable elements are flattened one level; non-iterable elements are kept as-is.
256+
* The array{} in value-of keeps the type resolvable when E is never (empty collections).
255257
*
256-
* @return ImmutableList<mixed>
258+
* @return ImmutableList<(E is iterable<mixed> ? value-of<E|array{}> : E)>
257259
*/
258260
#[NoDiscard]
259261
public function flatten(): ImmutableList;

‎src/List/ListInterface.php‎

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,16 @@ public function get(int $index);
4242
*/
4343
public function getOrNull(int $index): mixed;
4444

45+
/**
46+
* Returns the element at the specified index, or throws if out of bounds.
47+
* Alias of get() for array access syntax `$list[0]`.
48+
*
49+
* @param int $offset
50+
* @return E
51+
* @throws IndexOutOfBoundsException
52+
*/
53+
public function offsetGet(mixed $offset): mixed;
54+
4555
/**
4656
* Returns the element at the specified index, or the default value if out of bounds.
4757
*
@@ -170,8 +180,10 @@ public function flatMap(Closure $transform): ListInterface;
170180

171181
/**
172182
* Flatten a collection of iterables into a single collection.
183+
* Iterable elements are flattened one level; non-iterable elements are kept as-is.
184+
* The array{} in value-of keeps the type resolvable when E is never (empty collections).
173185
*
174-
* @return ListInterface<mixed>
186+
* @return ListInterface<(E is iterable<mixed> ? value-of<E|array{}> : E)>
175187
*/
176188
#[NoDiscard]
177189
public function flatten(): ListInterface;

‎src/List/ListLogic.php‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -239,9 +239,9 @@ public function flatMap(Closure $transform): ImmutableList
239239
/**
240240
* {@inheritDoc}
241241
*
242-
* @return ImmutableList<mixed>
242+
* @return ImmutableList<(E is iterable<mixed> ? value-of<E|array{}> : E)>
243243
*/
244-
#[NoDiscard]
244+
#[NoDiscard] // @phpstan-ignore conditionalType.subjectNotFound, return.unresolvableType (in classes with a concrete E the conditional subject is already substituted and stays unevaluated)
245245
public function flatten(): ImmutableList
246246
{
247247
return $this->newCollectionOf(new FlattenOperation($this->store)->items());

0 commit comments

Comments
 (0)