diff --git a/docs/pages/api.rst b/docs/pages/api.rst index dfd43f05a..35b66d100 100644 --- a/docs/pages/api.rst +++ b/docs/pages/api.rst @@ -287,32 +287,8 @@ Interface: `Associateable`_ Signature: ``Collection::associate(?callable $callbackForKeys = null, ?callable $callbackForValues = null): Collection;`` -.. code-block:: php - - $input = range(1, 10); - - Collection::fromIterable($input) - ->associate( - static function ($key, $value) { - return $key * 2; - }, - static function ($key, $value) { - return $value * 2; - } - ); - - // [ - // 0 => 2, - // 2 => 4, - // 4 => 6, - // 6 => 8, - // 8 => 10, - // 10 => 12, - // 12 => 14, - // 14 => 16, - // 16 => 18, - // 18 => 20, - // ] +.. literalinclude:: code/operations/associate.php + :language: php asyncMap ~~~~~~~~ diff --git a/docs/pages/code/operations/associate.php b/docs/pages/code/operations/associate.php new file mode 100644 index 000000000..1ba88fcd5 --- /dev/null +++ b/docs/pages/code/operations/associate.php @@ -0,0 +1,72 @@ +associate( + static function ($key, $value) { + return $key * 2; + }, + static function ($value, $key) { + return $value * 3; + } + ); + +// [ +// 0 => 3, +// 2 => 6, +// 4 => 9, +// 6 => 12, +// 8 => 15, +// ] + +// Example 2: Only the callback for keys is provided +$input = range(1, 5); + +Collection::fromIterable($input) + ->associate( + static function ($key, $value) { + return $key * 2; + } + ); + +// [ +// 0 => 1, +// 2 => 2, +// 4 => 3, +// 6 => 4, +// 8 => 5, +// ] + +// Example 3: Only the callback for values is provided +$input = range(1, 5); + +Collection::fromIterable($input) + ->associate( + null, + static function ($value, $key) { + return $value * 3; + } + ); + +// [ +// 0 => 3, +// 1 => 6, +// 2 => 9, +// 3 => 12, +// 4 => 15, +// ] diff --git a/phpstan-unsupported-baseline.neon b/phpstan-unsupported-baseline.neon index 78136fca4..3311886b0 100644 --- a/phpstan-unsupported-baseline.neon +++ b/phpstan-unsupported-baseline.neon @@ -1,12 +1,7 @@ parameters: ignoreErrors: - - message: "#^Parameter \\#1 \\$callable of class loophp\\\\collection\\\\Collection constructor expects callable\\(\\.\\.\\.mixed\\)\\: iterable\\, Closure\\(string, string\\)\\: loophp\\\\collection\\\\Iterator\\\\StringIterator\\ given\\.$#" - count: 1 - path: src/Collection.php - - - - message: "#^Generic type loophp\\\\collection\\\\Contract\\\\Operation\\\\Unpackable\\ in PHPDoc tag @extends does not specify all template types of interface loophp\\\\collection\\\\Contract\\\\Operation\\\\Unpackable\\: TKey, T, NewTKey, NewT$#" + message: "#^Generic type loophp\\\\collection\\\\Contract\\\\Operation\\\\Unpackable\\\\> in PHPDoc tag @extends does not specify all template types of interface loophp\\\\collection\\\\Contract\\\\Operation\\\\Unpackable\\: TKey, T, NewTKey, NewT$#" count: 1 path: src/Contract/Collection.php diff --git a/spec/loophp/collection/CollectionSpec.php b/spec/loophp/collection/CollectionSpec.php index 5aeffa3e8..515823a68 100644 --- a/spec/loophp/collection/CollectionSpec.php +++ b/spec/loophp/collection/CollectionSpec.php @@ -225,10 +225,10 @@ public function it_can_associate(): void $this::fromIterable($input) ->associate( - static function (int $carry, int $key, int $value): int { + static function (int $key, int $value): int { return $key * 2; }, - static function (int $carry, int $key, int $value): int { + static function (int $value, int $key): int { return $value * 2; } ) diff --git a/src/Collection.php b/src/Collection.php index 5acce6bc2..f85aca767 100644 --- a/src/Collection.php +++ b/src/Collection.php @@ -154,7 +154,7 @@ final class Collection implements CollectionInterface { /** - * @var iterable + * @var iterable */ private iterable $parameters; @@ -167,7 +167,7 @@ final class Collection implements CollectionInterface * @psalm-external-mutation-free * * @param callable(mixed ...$parameters): iterable $callable - * @param iterable $parameters + * @param iterable $parameters */ private function __construct(callable $callable, iterable $parameters = []) { @@ -196,9 +196,9 @@ public function associate( ): CollectionInterface { $defaultCallback = /** - * @param T|TKey $carry + * @param mixed $carry * - * @return T|TKey + * @return mixed */ static fn ($carry) => $carry; @@ -449,7 +449,7 @@ public function frequency(): CollectionInterface * @template NewT * * @param callable(mixed ...$parameters): iterable $callable - * @param iterable $parameters + * @param iterable $parameters * * @return self */ @@ -466,8 +466,7 @@ public static function fromCallable(callable $callable, iterable $parameters = [ public static function fromFile(string $filepath): self { return new self( - static fn (string $filepath): Iterator => new ResourceIterator(fopen($filepath, 'rb'), true), - [$filepath] + static fn (): Iterator => new ResourceIterator(fopen($filepath, 'rb'), true), ); } @@ -508,10 +507,7 @@ static function (Generator $generator): Generator { */ public static function fromIterable(iterable $iterable): self { - return new self( - static fn (iterable $iterable): Iterator => new IterableIterator($iterable), - [$iterable] - ); + return new self(static fn (): Iterator => new IterableIterator($iterable)); } /** @@ -523,13 +519,7 @@ public static function fromIterable(iterable $iterable): self */ public static function fromResource($resource): self { - return new self( - /** - * @param resource $resource - */ - static fn ($resource): Iterator => new ResourceIterator($resource), - [$resource] - ); + return new self(static fn (): Iterator => new ResourceIterator($resource)); } /** @@ -539,10 +529,7 @@ public static function fromResource($resource): self */ public static function fromString(string $string, string $delimiter = ''): self { - return new self( - static fn (string $string, string $delimiter): Iterator => new StringIterator($string, $delimiter), - [$string, $delimiter] - ); + return new self(static fn (): Iterator => new StringIterator($string, $delimiter)); } /** diff --git a/src/Contract/Collection.php b/src/Contract/Collection.php index a592affce..c77ea7d47 100644 --- a/src/Contract/Collection.php +++ b/src/Contract/Collection.php @@ -235,7 +235,7 @@ * @template-extends Transposeable * @template-extends Truthyable * @template-extends Unlinesable - * @template-extends Unpackable + * @template-extends Unpackable * @template-extends Unpairable * @template-extends Untilable * @template-extends Unwindowable diff --git a/src/Contract/Operation/Associateable.php b/src/Contract/Operation/Associateable.php index 3c4d28927..d54a4a3e3 100644 --- a/src/Contract/Operation/Associateable.php +++ b/src/Contract/Operation/Associateable.php @@ -23,10 +23,13 @@ interface Associateable * * @see https://loophp-collection.readthedocs.io/en/stable/pages/api.html#associate * - * @param null|callable(TKey, TKey, T, Iterator):(T|TKey) $callbackForKeys - * @param null|callable(T, TKey, T, Iterator):(T|TKey) $callbackForValues + * @template NewT + * @template NewTKey * - * @return Collection + * @param callable(TKey=, T=, Iterator=): NewTKey $callbackForKeys + * @param callable(T=, TKey=, Iterator=): NewT $callbackForValues + * + * @return Collection */ public function associate(?callable $callbackForKeys = null, ?callable $callbackForValues = null): Collection; } diff --git a/src/Contract/Operation/Zipable.php b/src/Contract/Operation/Zipable.php index 4ff46d003..ec401ca38 100644 --- a/src/Contract/Operation/Zipable.php +++ b/src/Contract/Operation/Zipable.php @@ -22,9 +22,12 @@ interface Zipable * * @see https://loophp-collection.readthedocs.io/en/stable/pages/api.html#zip * - * @param iterable ...$iterables + * @template U + * @template UKey * - * @return Collection, list> + * @param iterable ...$iterables + * + * @return Collection, list> */ public function zip(iterable ...$iterables): Collection; } diff --git a/src/Iterator/ClosureIterator.php b/src/Iterator/ClosureIterator.php index 0857e98e1..79d86662f 100644 --- a/src/Iterator/ClosureIterator.php +++ b/src/Iterator/ClosureIterator.php @@ -33,7 +33,7 @@ final class ClosureIterator extends ProxyIterator /** * @param callable(mixed ...$parameters): iterable $callable - * @param iterable $parameters + * @param iterable $parameters */ public function __construct(callable $callable, iterable $parameters) { @@ -48,10 +48,10 @@ public function rewind(): void } /** - * @return Generator + * @return Generator */ private function getGenerator(): Generator { - return yield from ($this->callable)(...$this->parameters); + yield from ($this->callable)(...$this->parameters); } } diff --git a/src/Iterator/ResourceIterator.php b/src/Iterator/ResourceIterator.php index 24bfe800a..adfc4ec94 100644 --- a/src/Iterator/ResourceIterator.php +++ b/src/Iterator/ResourceIterator.php @@ -11,7 +11,6 @@ use Generator; use InvalidArgumentException; -use IteratorIterator; use function is_resource; @@ -38,7 +37,7 @@ public function __construct($resource, bool $closeResource = false) /** * @param resource $resource * - * @return Generator + * @return Generator */ static function ($resource) use ($closeResource): Generator { try { @@ -52,6 +51,6 @@ static function ($resource) use ($closeResource): Generator { } }; - $this->iterator = new IteratorIterator($callback($resource)); + $this->iterator = new ClosureIterator($callback, [$resource]); } } diff --git a/src/Iterator/TypedIterator.php b/src/Iterator/TypedIterator.php index 29ebfbb0c..da6ae1363 100644 --- a/src/Iterator/TypedIterator.php +++ b/src/Iterator/TypedIterator.php @@ -28,7 +28,7 @@ final class TypedIterator extends ProxyIterator { /** * @param Iterator $iterator - * @param null|callable(mixed): string $getType + * @param null|callable(T): string $getType */ public function __construct(Iterator $iterator, ?callable $getType = null) { @@ -55,9 +55,18 @@ static function ($variable): string { }; $this->iterator = new ClosureIterator( + /** + * @param Iterator $iterator + * + * @return Generator + */ static function (Iterator $iterator) use ($getType): Generator { $previousType = null; + /** + * @var TKey $key + * @var T $value + */ foreach ($iterator as $key => $value) { if (null === $value) { yield $key => $value; diff --git a/src/Operation/Associate.php b/src/Operation/Associate.php index 7f3d07a50..6be83aba1 100644 --- a/src/Operation/Associate.php +++ b/src/Operation/Associate.php @@ -9,7 +9,6 @@ namespace loophp\collection\Operation; -use ArrayIterator; use Closure; use Generator; use Iterator; @@ -27,61 +26,34 @@ final class Associate extends AbstractOperation /** * @pure * - * @return Closure(callable(TKey, TKey, T, Iterator): (T|TKey) ...): Closure((callable(T, TKey, T, Iterator): (T|TKey))...): Closure(Iterator): Generator + * @template NewTKey + * @template NewT + * + * @return Closure(callable(TKey=, T=, Iterator=): NewTKey): Closure(callable(T=, TKey=, Iterator=): NewT): Closure(Iterator): Generator */ public function __invoke(): Closure { return /** - * @param callable(TKey, TKey, T, Iterator): (T|TKey) ...$callbackForKeys + * @param callable(TKey=, T=, Iterator=): NewTKey $callbackForKeys * - * @return Closure((callable(T, TKey, T, Iterator): (T|TKey))...): Closure(Iterator): Generator + * @return Closure((callable(T=, TKey=, Iterator=): NewT)): Closure(Iterator): Generator */ - static fn (callable ...$callbackForKeys): Closure => + static fn (callable $callbackForKeys): Closure => /** - * @param callable(T, TKey, T, Iterator): (T|TKey) ...$callbackForValues + * @param callable(T=, TKey=, Iterator=): NewT $callbackForValues * - * @return Closure(Iterator): Generator + * @return Closure(Iterator): Generator */ - static fn (callable ...$callbackForValues): Closure => + static fn (callable $callbackForValues): Closure => /** * @param Iterator $iterator * - * @return Generator + * @return Generator */ static function (Iterator $iterator) use ($callbackForKeys, $callbackForValues): Generator { - $callbackFactory = - /** - * @param TKey $key - * - * @return Closure(T): Closure(T|TKey, callable(T|TKey, TKey, T, Iterator): (T|TKey), int, Iterator): (T|TKey) - */ - static fn ($key): Closure => - /** - * @param T $value - * - * @return Closure(T|TKey, callable(T|TKey, TKey, T, Iterator): (T|TKey), int, Iterator): (T|TKey) - */ - static fn ($value): Closure => - /** - * @param T|TKey $accumulator - * @param callable(T|TKey, TKey, T, Iterator): (T|TKey) $callback - * @param Iterator $iterator - * - * @return T|TKey - */ - static fn ($accumulator, callable $callback, int $callbackId, Iterator $iterator) => $callback($accumulator, $key, $value, $iterator); - foreach ($iterator as $key => $value) { - $reduceCallback = $callbackFactory($key)($value); - - /** @var Generator $k */ - $k = Reduce::of()($reduceCallback)($key)(new ArrayIterator($callbackForKeys)); - - /** @var Generator $c */ - $c = Reduce::of()($reduceCallback)($value)(new ArrayIterator($callbackForValues)); - - yield $k->current() => $c->current(); + yield $callbackForKeys($key, $value, $iterator) => $callbackForValues($value, $key, $iterator); } }; } diff --git a/src/Operation/Flip.php b/src/Operation/Flip.php index 036810016..098fcb9be 100644 --- a/src/Operation/Flip.php +++ b/src/Operation/Flip.php @@ -30,22 +30,21 @@ public function __invoke(): Closure { $callbackForKeys = /** - * @param mixed $carry * @param TKey $key * @param T $value * * @return T */ - static fn ($carry, $key, $value) => $value; + static fn ($key, $value) => $value; $callbackForValues = /** - * @param mixed $carry + * @param T $value * @param TKey $key * * @return TKey */ - static fn ($carry, $key) => $key; + static fn ($value, $key) => $key; /** @var Closure(Iterator): Generator $associate */ $associate = Associate::of()($callbackForKeys)($callbackForValues); diff --git a/src/Operation/Normalize.php b/src/Operation/Normalize.php index 2c0f994ec..98e85fde3 100644 --- a/src/Operation/Normalize.php +++ b/src/Operation/Normalize.php @@ -24,7 +24,7 @@ final class Normalize extends AbstractOperation /** * @pure * - * @return Closure(Iterator): Generator + * @return Closure(Iterator): Generator */ public function __invoke(): Closure { @@ -32,7 +32,7 @@ public function __invoke(): Closure /** * @param Iterator $iterator * - * @return Generator + * @return Generator */ static function (Iterator $iterator): Generator { foreach ($iterator as $value) { diff --git a/src/Operation/Pair.php b/src/Operation/Pair.php index a17527e90..d664f943d 100644 --- a/src/Operation/Pair.php +++ b/src/Operation/Pair.php @@ -30,23 +30,20 @@ public function __invoke(): Closure { $callbackForKeys = /** - * @param T $initial * @param TKey $key * @param array{0: TKey, 1: T} $value * * @return TKey|null */ - static fn ($initial, $key, array $value) => $value[0] ?? null; + static fn ($key, array $value) => $value[0] ?? null; $callbackForValues = /** - * @param T $initial - * @param TKey $key * @param array{0: TKey, 1: T} $value * * @return T|null */ - static fn ($initial, $key, array $value) => $value[1] ?? null; + static fn (array $value) => $value[1] ?? null; /** @var Closure(Iterator): Generator $pipe */ $pipe = Pipe::of()( diff --git a/src/Operation/Reverse.php b/src/Operation/Reverse.php index 1d2dcd2af..cb895bf29 100644 --- a/src/Operation/Reverse.php +++ b/src/Operation/Reverse.php @@ -37,7 +37,7 @@ public function __invoke(): Closure */ static fn (array $carry, array $value): array => [...$value, ...$carry]; - /** @var Closure(Iterator): Generator $pipe */ + /** @var Closure(Iterator): Generator $pipe */ $pipe = Pipe::of()( (new Pack())(), Reduce::of()($callback)([]), diff --git a/src/Operation/Transpose.php b/src/Operation/Transpose.php index 4605d21b5..efa2930a2 100644 --- a/src/Operation/Transpose.php +++ b/src/Operation/Transpose.php @@ -34,22 +34,19 @@ public function __invoke(): Closure { $callbackForKeys = /** - * @param array $carry * @param non-empty-array $key * * @return TKey */ - static fn (array $carry, array $key) => reset($key); + static fn (array $key) => reset($key); $callbackForValues = /** - * @param array $carry - * @param array $key * @param array $value * * @return array */ - static fn (array $carry, array $key, array $value): array => $value; + static fn (array $value): array => $value; /** @var Closure(Iterator): Generator> $pipe */ $pipe = Pipe::of()( diff --git a/src/Operation/Unpack.php b/src/Operation/Unpack.php index fc757f80f..dc7f6be79 100644 --- a/src/Operation/Unpack.php +++ b/src/Operation/Unpack.php @@ -41,16 +41,15 @@ public function __invoke(): Closure * * @return NewTKey */ - static fn ($initial, int $key, array $value) => $value[0]; + static fn (int $key, array $value) => $value[0]; $callbackForValues = /** - * @param NewT $initial * @param T $value * * @return NewT */ - static fn ($initial, int $key, array $value) => $value[1]; + static fn (array $value) => $value[1]; /** @var Closure(Iterator): Generator $pipe */ $pipe = Pipe::of()( diff --git a/src/Operation/Unpair.php b/src/Operation/Unpair.php index 1ae45e183..b0f71ed56 100644 --- a/src/Operation/Unpair.php +++ b/src/Operation/Unpair.php @@ -24,7 +24,7 @@ final class Unpair extends AbstractOperation /** * @pure * - * @return Closure(Iterator): Generator + * @return Closure(Iterator): Generator */ public function __invoke(): Closure { @@ -32,7 +32,7 @@ public function __invoke(): Closure /** * @param Iterator $iterator * - * @return Generator + * @return Generator */ static function (Iterator $iterator): Generator { foreach ($iterator as $key => $value) { diff --git a/src/Operation/Unwords.php b/src/Operation/Unwords.php index 80575b566..41a056960 100644 --- a/src/Operation/Unwords.php +++ b/src/Operation/Unwords.php @@ -28,7 +28,7 @@ final class Unwords extends AbstractOperation */ public function __invoke(): Closure { - /** @var Closure(Iterator): Generator $implode */ + /** @var Closure(Iterator): Generator $implode */ $implode = Implode::of()(' '); // Point free style. diff --git a/src/Operation/Zip.php b/src/Operation/Zip.php index 735c19b14..76b08a62e 100644 --- a/src/Operation/Zip.php +++ b/src/Operation/Zip.php @@ -29,34 +29,37 @@ final class Zip extends AbstractOperation /** * @pure * - * @return Closure(iterable...): Closure(Iterator): Iterator, list> + * @template UKey + * @template U + * + * @return Closure(iterable...): Closure(Iterator): Iterator, list> */ public function __invoke(): Closure { return /** - * @param iterable ...$iterables + * @param iterable ...$iterables * - * @return Closure(Iterator): Iterator, list> + * @return Closure(Iterator): Iterator, list> */ static function (iterable ...$iterables): Closure { $buildArrayIterator = /** - * @param list> $iterables + * @param list> $iterables */ static fn (array $iterables): Closure => /** * @param Iterator $iterator * - * @return ArrayIterator|IterableIterator)> + * @return ArrayIterator|IterableIterator)> */ static fn (Iterator $iterator): Iterator => new ArrayIterator([ $iterator, ...array_map( /** - * @param iterable $iterable + * @param iterable $iterable * - * @return IterableIterator + * @return IterableIterator */ static fn (iterable $iterable): IterableIterator => new IterableIterator($iterable), $iterables @@ -65,7 +68,7 @@ static function (iterable ...$iterables): Closure { $buildMultipleIterator = /** - * @return Closure(ArrayIterator|IterableIterator)>): MultipleIterator + * @return Closure(ArrayIterator|IterableIterator)>): MultipleIterator */ Reduce::of()( /** @@ -78,7 +81,7 @@ static function (MultipleIterator $acc, Iterator $iterator): MultipleIterator { } )(new MultipleIterator(MultipleIterator::MIT_NEED_ANY)); - /** @var Closure(Iterator): Generator, list> $pipe */ + /** @var Closure(Iterator): Generator, list> $pipe */ $pipe = Pipe::of()( $buildArrayIterator($iterables), $buildMultipleIterator, diff --git a/tests/static-analysis/associate.php b/tests/static-analysis/associate.php new file mode 100644 index 000000000..a8051c4fe --- /dev/null +++ b/tests/static-analysis/associate.php @@ -0,0 +1,40 @@ + $collection + */ +function associate_checkIntInt(CollectionInterface $collection): void +{ +} +/** + * @param CollectionInterface $collection + */ +function associate_checkStringString(CollectionInterface $collection): void +{ +} +/** + * @param CollectionInterface $collection + */ +function associate_checkStringBool(CollectionInterface $collection): void +{ +} + +$square = static fn (int $val): int => $val ** 2; +$toBoldString = static fn (int $val): string => sprintf('*%s*', $val); +$isEven = static fn (int $val): bool => 0 === $val % 2; + +associate_checkIntInt(Collection::fromIterable([1, 2, 3])->associate($square, $square)); +associate_checkStringString(Collection::fromIterable([1, 2, 3])->associate($toBoldString, $toBoldString)); +associate_checkStringBool(Collection::fromIterable([1, 2, 3])->associate($toBoldString, $isEven)); diff --git a/tests/static-analysis/unpack.php b/tests/static-analysis/unpack.php new file mode 100644 index 000000000..1c52eda66 --- /dev/null +++ b/tests/static-analysis/unpack.php @@ -0,0 +1,56 @@ + $collection + */ +function unpack_checkListInt(CollectionInterface $collection): void +{ +} + +/** + * @param CollectionInterface $collection + */ +function unpack_checkListString(CollectionInterface $collection): void +{ +} + +/** + * @param CollectionInterface $collection + */ +function unpack_checkListStringWithString(CollectionInterface $collection): void +{ +} + +$infiniteIntIntGenerator = static function (): Generator { + while (true) { + yield [random_int(-10, 10), random_int(-10, 10)]; + } +}; + +$infiniteIntStringGenerator = static function (): Generator { + while (true) { + yield [random_int(-10, 10), chr(random_int(0, 255))]; + } +}; + +$infiniteStringStringGenerator = static function (): Generator { + while (true) { + yield [chr(random_int(0, 255)), chr(random_int(0, 255))]; + } +}; + +unpack_checkListInt(Collection::fromIterable($infiniteIntIntGenerator())->unpack()); +unpack_checkListString(Collection::fromIterable($infiniteIntStringGenerator())->unpack()); +unpack_checkListStringWithString(Collection::fromIterable($infiniteStringStringGenerator())->unpack()); diff --git a/tests/static-analysis/zip.php b/tests/static-analysis/zip.php new file mode 100644 index 000000000..f6df67748 --- /dev/null +++ b/tests/static-analysis/zip.php @@ -0,0 +1,66 @@ +, list> $collection + */ +function zip_checkBoolBool(CollectionInterface $collection): void +{ +} + +/** + * @param CollectionInterface, list> $collection + */ +function zip_checkBoolString(CollectionInterface $collection): void +{ +} + +/** + * @param CollectionInterface, list> $collection + */ +function zip_checkIntString(CollectionInterface $collection): void +{ +} + +/** + * @param CollectionInterface, list> $collection + */ +function zip_checkBoolStringInt(CollectionInterface $collection): void +{ +} + +$generator = + /** + * @return Generator + */ + static function (): Generator { + yield true => true; + + yield false => false; + + yield true => true; + }; + +// With one single parameter +zip_checkBoolBool(Collection::fromIterable($generator())->zip($generator())); +zip_checkBoolString(Collection::fromIterable($generator())->zip(range('a', 'c'))); +zip_checkIntString(Collection::fromIterable(range(1, 3))->zip(range('a', 'c'))); + +// With two parameters of the same types +zip_checkBoolBool(Collection::fromIterable($generator())->zip($generator(), $generator())); + +// With two parameters of different types +// Fails with PHPStan, not in PSalm. +/** @phpstan-ignore-next-line */ +zip_checkBoolStringInt(Collection::fromIterable($generator())->zip(range('a', 'c'), range(1, 3)));