Skip to content

Resolve nested PHPDoc unions in linear time - #334

Merged
ondrejmirtes merged 1 commit into
phpstan:2.0.xfrom
SanderMuller:mock-union-resolve-linear
Sep 26, 2026
Merged

ondrejmirtes merged 1 commit into
phpstan:2.0.xfrom
SanderMuller:mock-union-resolve-linear

Conversation

@SanderMuller

Copy link
Copy Markdown
Contributor

MockObjectTypeNodeResolverExtension resolved every member of a union before it found that the union holds no mock class. TypeNodeResolver then resolved the same union again, so each nesting level of a PHPDoc union doubled the work.

Now the extension resolves only the direct identifier members first. It resolves the whole union only when one of them is a mock class. Identifiers have no children, so resolving them twice costs a constant amount per member.

Measured with PHPStan 2.2.16 on a file with one nested union, /** @var ((((int|string)|string)|string)|string) $x */ at increasing depth, level 9, single process, one run each:

depth before after without the extension
18 1.6s 1.0s 1.0s
20 5.0s 1.0s 1.3s
21 6.2s 1.0s 1.1s
22 11.3s 1.0s 1.2s
30 1.0s

The inferred types do not change. Before the fix I dumped the types of six mock unions and one union without a mock, and the new test pins those types. The mock unions are MockObject|Foo, the same name with a leading backslash, a use ... as alias, Stub|Foo, a local @phpstan-type alias, and a mock union nested in (...)|null. A type alias from the global typeAliases config also still gives the intersection. The check runs on the resolved identifier, not on its name.

The test also has a depth-30 union without a mock, and a depth-30 union with a mock at the centre. Before the fix it was still running after 90 seconds. With the fix it runs in 0.1s.

🤖 Generated with Claude Code

MockObjectTypeNodeResolverExtension resolved every member of every union
before it found that the union holds no mock class. TypeNodeResolver then
resolved the same union again, so each nesting level doubled the work.

The extension now resolves the direct identifier members first. It
resolves the whole union only when one of them is a mock class.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@ondrejmirtes
ondrejmirtes merged commit 33a190a into phpstan:2.0.x Sep 26, 2026
96 of 97 checks passed
@ondrejmirtes

Copy link
Copy Markdown
Member

Thank you!

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.

2 participants