diff --git a/docs/collection/api/collection.md b/docs/collection/api/collection.md index 018391f..00bed89 100644 --- a/docs/collection/api/collection.md +++ b/docs/collection/api/collection.md @@ -333,19 +333,19 @@ groupBy(Closure $keySelector, ?Closure $valueTransform = null): Map>`. ```php -intersect(iterable $other): ImmutableSet +intersect(iterable $other): ImmutableSet ``` -Elements present in both this collection and the iterable. Returns a set (duplicates removed). +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. ```php -union(iterable $other): ImmutableSet +union(iterable $other): ImmutableSet ``` -All elements from both this collection and the iterable. Returns a set (duplicates removed). +All elements from both this collection and the iterable. Returns a set (duplicates removed). The element type widens to `E|NE`, like `add()`. ```php -subtract(iterable $other): ImmutableSet +subtract(iterable $other): ImmutableSet ``` -Elements present in this collection but not in the iterable. Returns a set (duplicates removed). +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. ## Ordering diff --git a/src/Collection.php b/src/Collection.php index 216bb3f..4f2f0b3 100644 --- a/src/Collection.php +++ b/src/Collection.php @@ -582,8 +582,9 @@ public function groupBy(Closure $keySelector, ?Closure $valueTransform = null): /** * Returns a set containing only elements present in both this collection and the given iterable. * - * @param iterable $other - * @return Set + * @template V + * @param iterable $other + * @return Set */ #[NoDiscard] public function intersect(iterable $other): Set; @@ -591,8 +592,9 @@ public function intersect(iterable $other): Set; /** * Returns a set containing all elements from both this collection and the given iterable. * - * @param iterable $other - * @return Set + * @template NE + * @param iterable $other + * @return Set */ #[NoDiscard] public function union(iterable $other): Set; @@ -600,7 +602,7 @@ public function union(iterable $other): Set; /** * Returns a set containing elements present in this collection but not in the given iterable. * - * @param iterable $other + * @param iterable $other * @return Set */ #[NoDiscard] diff --git a/src/CollectionLogic.php b/src/CollectionLogic.php index 1368fbd..b83ffe3 100644 --- a/src/CollectionLogic.php +++ b/src/CollectionLogic.php @@ -957,7 +957,9 @@ public function groupBy(Closure $keySelector, ?Closure $valueTransform = null): /** * {@inheritDoc} * - * @return ImmutableSet + * @template U + * @param iterable $other + * @return ImmutableSet */ #[NoDiscard] public function intersect(iterable $other): ImmutableSet @@ -968,7 +970,9 @@ public function intersect(iterable $other): ImmutableSet /** * {@inheritDoc} * - * @return ImmutableSet + * @template NE + * @param iterable $other + * @return ImmutableSet */ #[NoDiscard] public function union(iterable $other): ImmutableSet @@ -979,6 +983,7 @@ public function union(iterable $other): ImmutableSet /** * {@inheritDoc} * + * @param iterable $other * @return ImmutableSet */ #[NoDiscard] diff --git a/src/Operation/SetOperation.php b/src/Operation/SetOperation.php index 664c605..059d979 100644 --- a/src/Operation/SetOperation.php +++ b/src/Operation/SetOperation.php @@ -20,8 +20,9 @@ final class SetOperation extends AbstractOperation { /** - * @param iterable $other - * @return Generator + * @template U + * @param iterable $other + * @return Generator */ public function intersect(iterable $other): Generator { @@ -35,14 +36,16 @@ public function intersect(iterable $other): Generator $hash = KeyHasher::hashSetKey($v); if (isset($otherSet[$hash]) && !isset($seen[$hash])) { $seen[$hash] = true; - yield $v; + // Hash membership in $otherSet means the value also occurs in $other. + yield $v; // @phpstan-ignore generator.valueType } } } /** - * @param iterable $other - * @return Generator + * @template U + * @param iterable $other + * @return Generator */ public function union(iterable $other): Generator { @@ -65,7 +68,7 @@ public function union(iterable $other): Generator } /** - * @param iterable $other + * @param iterable $other * @return Generator */ public function subtract(iterable $other): Generator diff --git a/src/Set/SelfPreservingImmutableSetLogic.php b/src/Set/SelfPreservingImmutableSetLogic.php index 81da310..7bf0dcd 100644 --- a/src/Set/SelfPreservingImmutableSetLogic.php +++ b/src/Set/SelfPreservingImmutableSetLogic.php @@ -36,9 +36,11 @@ * `newCollectionOf()` builds derived collections with `new static(...)`, so there is * no factory method to override. * - * Two consequences of the `static` promise, both intentional: + * Three consequences of the `static` promise, all intentional: * - Mutations are **strict**: unlike the widening base `add(NE): ImmutableSet`, * here `add(E): static`. A fixed-type collection cannot widen its element type. + * - `union()` is **strict** for the same reason: unlike the widening base + * `union(iterable): Set`, here `union(iterable): static`. * - Type-changing methods (map, flatMap, flatten, filterInstanceOf, groupBy, the * to* conversions) are **not** narrowed — they still return the base type, because * their result is no longer a collection of `E`. (`groupBy` additionally cannot be @@ -424,7 +426,8 @@ public function shuffled(): static /** * {@inheritDoc} * - * @param iterable $other + * @template V + * @param iterable $other * @return static */ #[NoDiscard] @@ -448,7 +451,7 @@ public function union(iterable $other): static /** * {@inheritDoc} * - * @param iterable $other + * @param iterable $other * @return static */ #[NoDiscard] diff --git a/src/Set/SetLogic.php b/src/Set/SetLogic.php index 041409e..c964bbc 100644 --- a/src/Set/SetLogic.php +++ b/src/Set/SetLogic.php @@ -376,7 +376,9 @@ public function groupBy(Closure $keySelector, ?Closure $valueTransform = null): * Routed through newCollectionOf (rather than the free setOf) so that subtypes * which override the factory preserve their own type. Defaults to ImmutableSet. * - * @return ImmutableSet + * @template U + * @param iterable $other + * @return ImmutableSet */ #[NoDiscard] public function intersect(iterable $other): ImmutableSet @@ -387,7 +389,9 @@ public function intersect(iterable $other): ImmutableSet /** * {@inheritDoc} * - * @return ImmutableSet + * @template NE + * @param iterable $other + * @return ImmutableSet */ #[NoDiscard] public function union(iterable $other): ImmutableSet @@ -398,6 +402,7 @@ public function union(iterable $other): ImmutableSet /** * {@inheritDoc} * + * @param iterable $other * @return ImmutableSet */ #[NoDiscard] diff --git a/tests/Type/SetOperationsTypeTest.php b/tests/Type/SetOperationsTypeTest.php new file mode 100644 index 0000000..d98ae18 --- /dev/null +++ b/tests/Type/SetOperationsTypeTest.php @@ -0,0 +1,51 @@ + $c */ +$c = listOf(['a', 'b', 'c']); +/** @var Collection $scalars */ +$scalars = listOf([1, 'a']); +/** @var iterable $ints */ +$ints = [1, 2]; +/** @var iterable $anything */ +$anything = [1, 'a']; + +// With a same-typed iterable, the element type is unchanged. +assertType('Noctud\Collection\Set\Set', $c->intersect(['a', 'b'])); +assertType('Noctud\Collection\Set\Set', $c->union(['a', 'b'])); +assertType('Noctud\Collection\Set\Set', $c->subtract(['a', 'b'])); + +// intersect narrows to E&V: only values that can belong to both sides. +assertType('Noctud\Collection\Set\Set', $scalars->intersect($ints)); +assertType('Noctud\Collection\Set\Set', $c->intersect($anything)); +// Intersecting disjoint types is reported as an unresolvable return type. +assertType('Noctud\Collection\Set\Set<*NEVER*>', $c->intersect($ints)); // @phpstan-ignore method.unresolvableReturnType + +// union widens to E|V: elements of both sides end up in the result. +assertType('Noctud\Collection\Set\Set', $c->union($ints)); +assertType('Noctud\Collection\Set\Set', $scalars->union($ints)); + +// subtract always keeps E: any iterable may be subtracted. +assertType('Noctud\Collection\Set\Set', $scalars->subtract($ints)); +assertType('Noctud\Collection\Set\Set', $c->subtract($anything)); + +// Same behavior on a Set receiver. +$imm = setOf([1, 2, 3]); +/** @var iterable $strings */ +$strings = ['a', 'b']; +assertType('Noctud\Collection\Set\Set<*NEVER*>', $imm->intersect($strings)); // @phpstan-ignore method.unresolvableReturnType +assertType('Noctud\Collection\Set\Set', $imm->union($strings)); +assertType('Noctud\Collection\Set\Set', $imm->subtract($strings));