Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions bin/NarrowingGenerator.php
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,7 @@ public static function generateSetNarrowing(string $collectionContent, array $bl
': Collection' => ': Set',
'Collection<E>' => 'Set<E>',
'Collection<(E is null ? never : E)>' => 'Set<(E is null ? never : E)>',
'Collection<(E is iterable<mixed> ? value-of<E|array{}> : E)>' => 'Set<(E is iterable<mixed> ? value-of<E|array{}> : E)>',
'Collection<R>' => 'Set<R>',
'Collection<mixed>' => 'Set<mixed>',
'Collection<T>' => 'Set<T>',
Expand Down Expand Up @@ -233,6 +234,7 @@ public static function generateImmutableSetNarrowing(string $immutableCollection
': ImmutableCollection' => ': ImmutableSet',
'ImmutableCollection<E>' => 'ImmutableSet<E>',
'ImmutableCollection<(E is null ? never : E)>' => 'ImmutableSet<(E is null ? never : E)>',
'ImmutableCollection<(E is iterable<mixed> ? value-of<E|array{}> : E)>' => 'ImmutableSet<(E is iterable<mixed> ? value-of<E|array{}> : E)>',
'ImmutableCollection<E|NE>' => 'ImmutableSet<E|NE>',
'ImmutableCollection<R>' => 'ImmutableSet<R>',
'ImmutableCollection<mixed>' => 'ImmutableSet<mixed>',
Expand Down Expand Up @@ -288,6 +290,7 @@ public static function generateListNarrowing(string $collectionContent, array $b
': Collection' => ': ListInterface',
'Collection<E>' => 'ListInterface<E>',
'Collection<(E is null ? never : E)>' => 'ListInterface<(E is null ? never : E)>',
'Collection<(E is iterable<mixed> ? value-of<E|array{}> : E)>' => 'ListInterface<(E is iterable<mixed> ? value-of<E|array{}> : E)>',
'Collection<R>' => 'ListInterface<R>',
'Collection<mixed>' => 'ListInterface<mixed>',
'Collection<T>' => 'ListInterface<T>',
Expand Down Expand Up @@ -318,6 +321,7 @@ public static function generateImmutableCollectionNarrowing(string $collectionCo
': Collection' => ': ImmutableCollection',
'Collection<E>' => 'ImmutableCollection<E>',
'Collection<(E is null ? never : E)>' => 'ImmutableCollection<(E is null ? never : E)>',
'Collection<(E is iterable<mixed> ? value-of<E|array{}> : E)>' => 'ImmutableCollection<(E is iterable<mixed> ? value-of<E|array{}> : E)>',
'Collection<R>' => 'ImmutableCollection<R>',
'Collection<mixed>' => 'ImmutableCollection<mixed>',
'Collection<T>' => 'ImmutableCollection<T>',
Expand Down Expand Up @@ -345,6 +349,7 @@ public static function generateWritableCollectionTransformationNarrowing(string
': Collection' => ': ImmutableCollection',
'Collection<E>' => 'ImmutableCollection<E>',
'Collection<(E is null ? never : E)>' => 'ImmutableCollection<(E is null ? never : E)>',
'Collection<(E is iterable<mixed> ? value-of<E|array{}> : E)>' => 'ImmutableCollection<(E is iterable<mixed> ? value-of<E|array{}> : E)>',
'Collection<R>' => 'ImmutableCollection<R>',
'Collection<mixed>' => 'ImmutableCollection<mixed>',
'Collection<T>' => 'ImmutableCollection<T>',
Expand Down Expand Up @@ -372,6 +377,7 @@ public static function generateWritableListTransformationNarrowing(string $colle
': Collection' => ': ImmutableList',
'Collection<E>' => 'ImmutableList<E>',
'Collection<(E is null ? never : E)>' => 'ImmutableList<(E is null ? never : E)>',
'Collection<(E is iterable<mixed> ? value-of<E|array{}> : E)>' => 'ImmutableList<(E is iterable<mixed> ? value-of<E|array{}> : E)>',
'Collection<R>' => 'ImmutableList<R>',
'Collection<mixed>' => 'ImmutableList<mixed>',
'Collection<T>' => 'ImmutableList<T>',
Expand Down Expand Up @@ -399,6 +405,7 @@ public static function generateWritableSetTransformationNarrowing(string $collec
': Collection' => ': ImmutableSet',
'Collection<E>' => 'ImmutableSet<E>',
'Collection<(E is null ? never : E)>' => 'ImmutableSet<(E is null ? never : E)>',
'Collection<(E is iterable<mixed> ? value-of<E|array{}> : E)>' => 'ImmutableSet<(E is iterable<mixed> ? value-of<E|array{}> : E)>',
'Collection<R>' => 'ImmutableSet<R>',
'Collection<mixed>' => 'ImmutableSet<mixed>',
'Collection<T>' => 'ImmutableSet<T>',
Expand Down Expand Up @@ -450,6 +457,7 @@ public static function generateImmutableListNarrowing(string $immutableCollectio
': ImmutableCollection' => ': ImmutableList',
'ImmutableCollection<E>' => 'ImmutableList<E>',
'ImmutableCollection<(E is null ? never : E)>' => 'ImmutableList<(E is null ? never : E)>',
'ImmutableCollection<(E is iterable<mixed> ? value-of<E|array{}> : E)>' => 'ImmutableList<(E is iterable<mixed> ? value-of<E|array{}> : E)>',
'ImmutableCollection<E|NE>' => 'ImmutableList<E|NE>',
'ImmutableCollection<R>' => 'ImmutableList<R>',
'ImmutableCollection<mixed>' => 'ImmutableList<mixed>',
Expand Down
9 changes: 9 additions & 0 deletions phpstan.neon
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,15 @@ parameters:
- src/Set/Set.php
- src/Set/ImmutableSet.php
- src/Set/WritableSet.php
# flatten()'s conditional return type cannot be evaluated in classes with a
# concrete E (the subject is already substituted), so the trait vs ancestor
# comparison sees structurally different but semantically equal conditionals.
-
message: '#::flatten\(\)#'
identifier: method.childReturnType
paths:
- src/List/ListLogic.php
- src/Set/SetLogic.php
# classes which implement ImmutableMap<string,...>. The trait's put()/putFirst()
# accepts string|int|bool|float|object but the store expects string.
-
Expand Down
4 changes: 3 additions & 1 deletion src/Collection.php
Original file line number Diff line number Diff line change
Expand Up @@ -409,8 +409,10 @@ public function flatMap(Closure $transform): Collection;

/**
* Flatten a collection of iterables into a single collection.
* Iterable elements are flattened one level; non-iterable elements are kept as-is.
* The array{} in value-of keeps the type resolvable when E is never (empty collections).
*
* @return Collection<mixed>
* @return Collection<(E is iterable<mixed> ? value-of<E|array{}> : E)>
*/
#[NoDiscard]
public function flatten(): Collection;
Expand Down
4 changes: 3 additions & 1 deletion src/ImmutableCollection.php
Original file line number Diff line number Diff line change
Expand Up @@ -192,8 +192,10 @@ public function flatMap(Closure $transform): ImmutableCollection;

/**
* Flatten a collection of iterables into a single collection.
* Iterable elements are flattened one level; non-iterable elements are kept as-is.
* The array{} in value-of keeps the type resolvable when E is never (empty collections).
*
* @return ImmutableCollection<mixed>
* @return ImmutableCollection<(E is iterable<mixed> ? value-of<E|array{}> : E)>
*/
#[NoDiscard]
public function flatten(): ImmutableCollection;
Expand Down
4 changes: 3 additions & 1 deletion src/List/ImmutableList.php
Original file line number Diff line number Diff line change
Expand Up @@ -252,8 +252,10 @@ public function flatMap(Closure $transform): ImmutableList;

/**
* Flatten a collection of iterables into a single collection.
* Iterable elements are flattened one level; non-iterable elements are kept as-is.
* The array{} in value-of keeps the type resolvable when E is never (empty collections).
*
* @return ImmutableList<mixed>
* @return ImmutableList<(E is iterable<mixed> ? value-of<E|array{}> : E)>
*/
#[NoDiscard]
public function flatten(): ImmutableList;
Expand Down
4 changes: 3 additions & 1 deletion src/List/ListInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -170,8 +170,10 @@ public function flatMap(Closure $transform): ListInterface;

/**
* Flatten a collection of iterables into a single collection.
* Iterable elements are flattened one level; non-iterable elements are kept as-is.
* The array{} in value-of keeps the type resolvable when E is never (empty collections).
*
* @return ListInterface<mixed>
* @return ListInterface<(E is iterable<mixed> ? value-of<E|array{}> : E)>
*/
#[NoDiscard]
public function flatten(): ListInterface;
Expand Down
4 changes: 2 additions & 2 deletions src/List/ListLogic.php
Original file line number Diff line number Diff line change
Expand Up @@ -239,9 +239,9 @@ public function flatMap(Closure $transform): ImmutableList
/**
* {@inheritDoc}
*
* @return ImmutableList<mixed>
* @return ImmutableList<(E is iterable<mixed> ? value-of<E|array{}> : E)>
*/
#[NoDiscard]
#[NoDiscard] // @phpstan-ignore conditionalType.subjectNotFound, return.unresolvableType (in classes with a concrete E the conditional subject is already substituted and stays unevaluated)
public function flatten(): ImmutableList
{
return $this->newCollectionOf(new FlattenOperation($this->store)->items());
Expand Down
4 changes: 3 additions & 1 deletion src/List/WritableList.php
Original file line number Diff line number Diff line change
Expand Up @@ -227,8 +227,10 @@ public function flatMap(Closure $transform): ImmutableList;

/**
* Flatten a collection of iterables into a single collection.
* Iterable elements are flattened one level; non-iterable elements are kept as-is.
* The array{} in value-of keeps the type resolvable when E is never (empty collections).
*
* @return ImmutableList<mixed>
* @return ImmutableList<(E is iterable<mixed> ? value-of<E|array{}> : E)>
*/
#[NoDiscard]
public function flatten(): ImmutableList;
Expand Down
4 changes: 3 additions & 1 deletion src/Set/ImmutableSet.php
Original file line number Diff line number Diff line change
Expand Up @@ -202,8 +202,10 @@ public function flatMap(Closure $transform): ImmutableSet;

/**
* Flatten a collection of iterables into a single collection.
* Iterable elements are flattened one level; non-iterable elements are kept as-is.
* The array{} in value-of keeps the type resolvable when E is never (empty collections).
*
* @return ImmutableSet<mixed>
* @return ImmutableSet<(E is iterable<mixed> ? value-of<E|array{}> : E)>
*/
#[NoDiscard]
public function flatten(): ImmutableSet;
Expand Down
4 changes: 3 additions & 1 deletion src/Set/Set.php
Original file line number Diff line number Diff line change
Expand Up @@ -86,8 +86,10 @@ public function flatMap(Closure $transform): Set;

/**
* Flatten a collection of iterables into a single collection.
* Iterable elements are flattened one level; non-iterable elements are kept as-is.
* The array{} in value-of keeps the type resolvable when E is never (empty collections).
*
* @return Set<mixed>
* @return Set<(E is iterable<mixed> ? value-of<E|array{}> : E)>
*/
#[NoDiscard]
public function flatten(): Set;
Expand Down
4 changes: 2 additions & 2 deletions src/Set/SetLogic.php
Original file line number Diff line number Diff line change
Expand Up @@ -118,9 +118,9 @@ public function flatMap(Closure $transform): ImmutableSet
/**
* {@inheritDoc}
*
* @return ImmutableSet<mixed>
* @return ImmutableSet<(E is iterable<mixed> ? value-of<E|array{}> : E)>
*/
#[NoDiscard]
#[NoDiscard] // @phpstan-ignore conditionalType.subjectNotFound, return.unresolvableType (in classes with a concrete E the conditional subject is already substituted and stays unevaluated)
public function flatten(): ImmutableSet
{
return $this->newCollectionOf(new FlattenOperation($this->store)->items());
Expand Down
4 changes: 3 additions & 1 deletion src/Set/WritableSet.php
Original file line number Diff line number Diff line change
Expand Up @@ -185,8 +185,10 @@ public function flatMap(Closure $transform): ImmutableSet;

/**
* Flatten a collection of iterables into a single collection.
* Iterable elements are flattened one level; non-iterable elements are kept as-is.
* The array{} in value-of keeps the type resolvable when E is never (empty collections).
*
* @return ImmutableSet<mixed>
* @return ImmutableSet<(E is iterable<mixed> ? value-of<E|array{}> : E)>
*/
#[NoDiscard]
public function flatten(): ImmutableSet;
Expand Down
4 changes: 3 additions & 1 deletion src/WritableCollection.php
Original file line number Diff line number Diff line change
Expand Up @@ -185,8 +185,10 @@ public function flatMap(Closure $transform): ImmutableCollection;

/**
* Flatten a collection of iterables into a single collection.
* Iterable elements are flattened one level; non-iterable elements are kept as-is.
* The array{} in value-of keeps the type resolvable when E is never (empty collections).
*
* @return ImmutableCollection<mixed>
* @return ImmutableCollection<(E is iterable<mixed> ? value-of<E|array{}> : E)>
*/
#[NoDiscard]
public function flatten(): ImmutableCollection;
Expand Down
44 changes: 44 additions & 0 deletions tests/Type/FlattenTypeTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
<?php

/**
* This file is part of the Noctud Collection.
* Copyright (c) Noctud.dev
*/

declare(strict_types=1);

namespace Noctud\Collection\Tests\Type;

use Noctud\Collection\Collection;

use function Noctud\Collection\listOf;
use function Noctud\Collection\mutableListOf;
use function Noctud\Collection\setOf;
use function PHPStan\Testing\assertType;

// flatten() extracts the element type of iterable elements (one level).
assertType('Noctud\Collection\List\ImmutableList<int>', listOf([listOf([1, 2]), listOf([3])])->flatten());
assertType('Noctud\Collection\Set\ImmutableSet<int>', setOf([listOf([1, 2]), listOf([3])])->flatten());
assertType('Noctud\Collection\List\ImmutableList<int>', mutableListOf([listOf([1, 2])])->flatten());

// Only one level is flattened.
assertType(
'Noctud\Collection\List\ImmutableList<Noctud\Collection\List\ImmutableList<int>>',
listOf([listOf([listOf([1])])])->flatten(),
);

// Plain arrays are iterables too.
/** @var Collection<array<int>> $arrays */
$arrays = listOf([[1, 2], [3]]);
assertType('Noctud\Collection\Collection<int>', $arrays->flatten());

// Non-iterable elements are kept as-is...
assertType('Noctud\Collection\List\ImmutableList<string>', listOf(['a', 'b'])->flatten());

// ...and the conditional distributes over union element types.
/** @var Collection<array<int>|string> $mixed */
$mixed = listOf([[1], 'a']);
assertType('Noctud\Collection\Collection<int|string>', $mixed->flatten());

// flatten() on an empty collection stays typed (E = never).
assertType('Noctud\Collection\List\ImmutableList<*NEVER*>', listOf([])->flatten());
Loading