Skip to content

Load an ext-ds v2 stub overlay with list return types when ext-ds 2 is selected - #6939

Open
phpstan-bot wants to merge 1 commit into
phpstan:2.3.xfrom
phpstan-bot:create-pull-request/patch-24shpyt
Open

phpstan-bot wants to merge 1 commit into
phpstan:2.3.xfrom
phpstan-bot:create-pull-request/patch-24shpyt

Conversation

@phpstan-bot

Copy link
Copy Markdown
Collaborator

Summary

Since 2.2.15 PHPStan picks the ext-ds stubs by major version. For ext-ds 2 it loaded no PHPStan overlay at all. Types came only from the PhpStorm ds_v2 stubs, so Ds\Set::toArray() and similar methods became array<int, TValue> instead of list<TValue>. This PR adds an overlay for ext-ds 2 and loads it when version 2 is detected.

Changes

  • stubs/ext-ds-v2.stub (new): overlay for the ext-ds 2 classes Seq, Map, Set and Heap:
    • list<TValue> for toArray() and jsonSerialize() on Seq, Set and Heap, and for __serialize() on Seq and Heap
    • the same generic precision the v1 overlay already gave:
      • merge(), union() and xor() widen the type parameters
      • intersect() and diff() on Map and Set
      • Seq::merge()
      • Map::get() and Map::remove() use a TDefault template, so the result is TValue (not TValue|null) when no default is passed
      • reduce() is typed on Seq, Map and Set
  • src/PhpDoc/ExtDsStubFilesExtension.php: return ext-ds-v2.stub for ext-ds 2. ext-ds 1 and unknown versions behave as before.
  • I left out @throws tags from the v1 overlay, because the v2 exception classes aren't confirmed. Without them the methods stay implicitly throwing, so catch blocks are not reported as dead.

Root cause

Commit 677df31 ("Select versioned extension stubs") made ext-ds.stub load only for ext-ds 1. It added no v2 counterpart. So everything the overlay used to refine (list returns, widening generics, TDefault on get/remove) was lost for v2 users, not just toArray().

Test

tests/PHPStan/Analyser/ExtDsV2Test.php with data/ext-ds-v2.php and an ext-ds ^2 composer.json fixture (data/ext-ds-v2-platform). It covers:

  • the reproducer from the issue
  • toArray(), jsonSerialize() and __serialize() on all v2 collections
  • Map::get() and Map::remove(), with and without a default
  • merge(), union() and xor() on Map, Set and Seq
  • reduce()

The test fails without the change in ExtDsStubFilesExtension (10 failures) and passes with it.

Fixes phpstan/phpstan#15645

🤖 Generated with Claude Code

… 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<int, TValue>` for toArray() etc.).
- Add stubs/ext-ds-v2.stub and load it when ext-ds 2 is detected:
  - `list<TValue>` 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.

@VincentLanglet VincentLanglet left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@phpstan-bot

Copy link
Copy Markdown
Collaborator Author

I didn't change any code. With the PhpStorm stubs alone, users get exactly the regression reported in the issue.

Why the PhpStorm stubs aren't enough: the stub you linked (ds_v2/ds.php at f5ebeeaf) is the same as the one we pin in composer.json (f690ff36) and as current upstream master. That stub declares:

  • Seq::toArray(), jsonSerialize() and __serialize() as @return array<int, TValue>, and the same for Set and Heap. That's why Ds\Set<string>::toArray() comes out as array<int, string> instead of list<string>, even though ext-ds 2 returns lists at runtime.
  • Map::get() and Map::remove() as TValue|null even when no default is passed. The v1 overlay used TDefault so the result is just TValue.
  • merge(), union() and xor() as Map<TKey, TValue>, Set<TValue> and Seq<TValue>, so they don't widen to the argument's types.
  • reduce() as TReturn|null, where the v1 overlay gives TCarry.
  • Map::intersect() and Map::diff() with no generics on the parameter. Set::intersect() returns Set<TValue>, not the intersection type.

So the stubs aren't missing anything ext-ds 2 has. They are just less precise than PHPStan's own stubs/ext-ds.stub. The v1 overlay exists for the same reason. Until 2.2.15 it was applied whatever the ext-ds version, so v2 users got all of these refinements.

What the v2 overlay does: it only overrides the methods whose PhpStorm types are imprecise. Everything else, including the class shapes, Key, Pair and the method list, still comes from the PhpStorm ds_v2 stubs. I left out the v1 @throws tags, so throw behaviour also follows the PhpStorm stubs.

Fixing it upstream instead: we could also send a PR to JetBrains/phpstorm-stubs. That would only cover the list part, since TDefault and the widening generics are PHPStan-specific precision. We'd still have to wait for that PR to merge and then bump the pinned commit, so the overlay is needed to fix the issue now. If you'd like, I can open the upstream PR as a follow-up. I haven't done it, because it would be posted to an external repo.

Checks: ExtDsV2Test passes (22/22). With the ExtDsStubFilesExtension change reverted, so only the PhpStorm stubs apply, 10 assertions fail. I ran only this test, not make tests or make phpstan, because no code changed. There are no new commits to push.

@VincentLanglet VincentLanglet self-assigned this Nov 4, 2026
@VincentLanglet
VincentLanglet requested a review from staabm November 4, 2026 09:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ext-ds v2 stub was lost since v2.2.15

2 participants