From 247a880960b601599dd877fb536bda677425de27 Mon Sep 17 00:00:00 2001 From: VincentLanglet <9052536+VincentLanglet@users.noreply.github.com> Date: Wed, 4 Nov 2026 09:18:17 +0000 Subject: [PATCH] Load an ext-ds v2 stub overlay with `list` return types when ext-ds 2 is selected - ExtDsStubFilesExtension only loaded stubs/ext-ds.stub for ext-ds 1 and nothing for ext-ds 2, so v2 classes fell back to the plain PhpStorm ds_v2 stubs (`array` for toArray() etc.). - Add stubs/ext-ds-v2.stub and load it when ext-ds 2 is detected: - `list` for Seq/Set/Heap toArray() and jsonSerialize(), and Seq/Heap __serialize() - the same generic precision the v1 overlay has: widening merge()/union()/xor(), intersect()/diff() on Map and Set, Seq::merge(), Map::get()/remove() with a TDefault template (no `|null` without a default), and typed reduce() on Seq/Map/Set - Add ExtDsV2Test with an ext-ds ^2 composer.json fixture. --- src/PhpDoc/ExtDsStubFilesExtension.php | 4 + stubs/ext-ds-v2.stub | 249 ++++++++++++++++++ tests/PHPStan/Analyser/ExtDsV2Test.php | 41 +++ .../data/ext-ds-v2-platform/composer.json | 5 + tests/PHPStan/Analyser/data/ext-ds-v2.php | 91 +++++++ 5 files changed, 390 insertions(+) create mode 100644 stubs/ext-ds-v2.stub create mode 100644 tests/PHPStan/Analyser/ExtDsV2Test.php create mode 100644 tests/PHPStan/Analyser/data/ext-ds-v2-platform/composer.json create mode 100644 tests/PHPStan/Analyser/data/ext-ds-v2.php diff --git a/src/PhpDoc/ExtDsStubFilesExtension.php b/src/PhpDoc/ExtDsStubFilesExtension.php index 78ad7f67951..10b0919d3a8 100644 --- a/src/PhpDoc/ExtDsStubFilesExtension.php +++ b/src/PhpDoc/ExtDsStubFilesExtension.php @@ -16,6 +16,10 @@ public function __construct(private ExtensionVersionProvider $extensionVersionPr public function getFiles(): array { $version = $this->extensionVersionProvider->getExtensionVersions()['ds'] ?? null; + if ($version === 2) { + return [__DIR__ . '/../../stubs/ext-ds-v2.stub']; + } + if ($version !== null && $version !== 1) { return []; } diff --git a/stubs/ext-ds-v2.stub b/stubs/ext-ds-v2.stub new file mode 100644 index 00000000000..bdc4effd64b --- /dev/null +++ b/stubs/ext-ds-v2.stub @@ -0,0 +1,249 @@ + + * @implements ArrayAccess + */ +final class Seq implements Countable, IteratorAggregate, JsonSerializable, ArrayAccess +{ + /** + * @template TValue2 + * @param iterable $values + * @return Seq + */ + public function merge($values): Seq + { + } + + /** + * @template TCarry + * @param callable(TCarry, TValue): TCarry $callback + * @param TCarry $initial + * @return TCarry + */ + public function reduce(callable $callback, $initial = null) + { + } + + /** + * @return list + */ + public function toArray(): array + { + } + + /** + * @return list + */ + public function jsonSerialize(): array + { + } + + /** + * @return list + */ + public function __serialize(): array + { + } +} + +/** + * @template TKey + * @template TValue + * @implements IteratorAggregate + * @implements ArrayAccess + */ +final class Map implements Countable, IteratorAggregate, JsonSerializable, ArrayAccess +{ + /** + * @template TValue2 + * @param Map $map + * @return Map + */ + public function diff(Map $map): Map + { + } + + /** + * @template TDefault + * @param TKey $key + * @param TDefault $default + * @return TValue|TDefault + */ + public function get($key, $default = null) + { + } + + /** + * @template TKey2 + * @template TValue2 + * @param Map $map + * @return Map + */ + public function intersect(Map $map): Map + { + } + + /** + * @template TKey2 + * @template TValue2 + * @param iterable $values + * @return Map + */ + public function merge($values): Map + { + } + + /** + * @template TCarry + * @param callable(TCarry, TKey, TValue): TCarry $callback + * @param TCarry $initial + * @return TCarry + */ + public function reduce(callable $callback, $initial = null) + { + } + + /** + * @template TDefault + * @param TKey $key + * @param TDefault $default + * @return TValue|TDefault + */ + public function remove($key, $default = null) + { + } + + /** + * @template TKey2 + * @template TValue2 + * @param Map $map + * @return Map + */ + public function union(Map $map): Map + { + } + + /** + * @template TKey2 + * @template TValue2 + * @param Map $map + * @return Map + */ + public function xor(Map $map): Map + { + } +} + +/** + * @template TValue + * @implements IteratorAggregate + * @implements ArrayAccess + */ +final class Set implements Countable, IteratorAggregate, JsonSerializable, ArrayAccess +{ + /** + * @template TValue2 + * @param Set $set + * @return Set + */ + public function diff(Set $set): Set + { + } + + /** + * @template TValue2 + * @param Set $set + * @return Set + */ + public function intersect(Set $set): Set + { + } + + /** + * @template TValue2 + * @param iterable $values + * @return Set + */ + public function merge($values): Set + { + } + + /** + * @template TCarry + * @param callable(TCarry, TValue): TCarry $callback + * @param TCarry $initial + * @return TCarry + */ + public function reduce(callable $callback, $initial = null) + { + } + + /** + * @return list + */ + public function toArray(): array + { + } + + /** + * @return list + */ + public function jsonSerialize(): array + { + } + + /** + * @template TValue2 + * @param Set $set + * @return Set + */ + public function union(Set $set): Set + { + } + + /** + * @template TValue2 + * @param Set $set + * @return Set + */ + public function xor(Set $set): Set + { + } +} + +/** + * @template TValue + * @implements IteratorAggregate + */ +final class Heap implements Countable, IteratorAggregate, JsonSerializable +{ + /** + * @return list + */ + public function toArray(): array + { + } + + /** + * @return list + */ + public function jsonSerialize(): array + { + } + + /** + * @return list + */ + public function __serialize(): array + { + } +} diff --git a/tests/PHPStan/Analyser/ExtDsV2Test.php b/tests/PHPStan/Analyser/ExtDsV2Test.php new file mode 100644 index 00000000000..5171dfeb1da --- /dev/null +++ b/tests/PHPStan/Analyser/ExtDsV2Test.php @@ -0,0 +1,41 @@ +assertFileAsserts($assertType, $file, ...$args); + } + + public static function getAdditionalConfigFiles(): array + { + return [ + __DIR__ . '/../../../conf/bleedingEdge.neon', + ]; + } + + public static function getComposerAutoloaderProjectPaths(): array + { + return [__DIR__ . '/data/ext-ds-v2-platform']; + } + +} diff --git a/tests/PHPStan/Analyser/data/ext-ds-v2-platform/composer.json b/tests/PHPStan/Analyser/data/ext-ds-v2-platform/composer.json new file mode 100644 index 00000000000..04af4057fd1 --- /dev/null +++ b/tests/PHPStan/Analyser/data/ext-ds-v2-platform/composer.json @@ -0,0 +1,5 @@ +{ + "require": { + "ext-ds": "^2" + } +} diff --git a/tests/PHPStan/Analyser/data/ext-ds-v2.php b/tests/PHPStan/Analyser/data/ext-ds-v2.php new file mode 100644 index 00000000000..8081241311b --- /dev/null +++ b/tests/PHPStan/Analyser/data/ext-ds-v2.php @@ -0,0 +1,91 @@ + $s */ +function f(\Ds\Set $s): void +{ + assertType('list', $s->toArray()); +} + +class Foo +{ + + /** + * @param Seq $seq + * @param Set $set + * @param Heap $heap + * @param Map $map + */ + public function toArray(Seq $seq, Set $set, Heap $heap, Map $map): void + { + assertType('list', $seq->toArray()); + assertType('list', $set->toArray()); + assertType('list', $heap->toArray()); + assertType('array', $map->toArray()); + + assertType('list', $seq->jsonSerialize()); + assertType('list', $set->jsonSerialize()); + assertType('list', $heap->jsonSerialize()); + + assertType('list', $seq->__serialize()); + assertType('list', $heap->__serialize()); + } + + /** @param Map $a */ + public function mapGetRemove(Map $a): void + { + assertType('int|string', $a->get(1)); + assertType('int|string|null', $a->get(1, null)); + assertType('int|string', $a->remove(1)); + assertType('int|string|null', $a->remove(1, null)); + } + + public function mapMergeUnionXor(): void + { + $a = new Map([1 => new A()]); + $b = new Map(['a' => new B()]); + + assertType('Ds\Map<1|\'a\', ExtDsV2\A|ExtDsV2\B>', $a->merge(['a' => new B()])); + assertType('Ds\Map<1|\'a\', ExtDsV2\A|ExtDsV2\B>', $a->union($b)); + assertType('Ds\Map<1|\'a\', ExtDsV2\A|ExtDsV2\B>', $a->xor($b)); + } + + public function setMergeUnionXor(): void + { + $a = new Set([new A()]); + $b = new Set([new B()]); + + assertType('Ds\Set', $a->merge([new B()])); + assertType('Ds\Set', $a->union($b)); + assertType('Ds\Set', $a->xor($b)); + } + + public function seqMerge(): void + { + $a = new Seq([new A()]); + + assertType('Ds\Seq', $a->merge([new B()])); + } + + /** @param Set $set */ + public function reduce(Set $set): void + { + assertType('int', $set->reduce(static fn (int $carry, int $value): int => $carry + $value, 0)); + } + +}