Skip to content

Sort classes before interfaces when describing an IntersectionType#5873

Closed
phpstan-bot wants to merge 1 commit into
phpstan:2.2.xfrom
phpstan-bot:create-pull-request/patch-zuyxtbo
Closed

Sort classes before interfaces when describing an IntersectionType#5873
phpstan-bot wants to merge 1 commit into
phpstan:2.2.xfrom
phpstan-bot:create-pull-request/patch-zuyxtbo

Conversation

@phpstan-bot

Copy link
Copy Markdown
Collaborator

Summary

In an IntersectionType consisting of a class and one or more interfaces, the members were ordered purely by their textual description. Because static/$this/self and interface names sort inconsistently against each other ($ sorts before letters, but static/self sort by their first letter), PHPStan produced inconsistent output: I&static(HelloWorld) (interface first) yet $this(HelloWorld)&I2 (class first). This made error messages and dumped types harder to read.

The fix sorts the concrete class before the interfaces, so the leading type of an intersection is always the class, regardless of how the names happen to compare alphabetically.

Changes

  • src/Type/UnionTypeHelper.php: sortTypes() gains an opt-in bool $sortClassesBeforeInterfaces parameter (default false). When enabled and both compared types resolve to exactly one class reflection, a non-interface class sorts before an interface.
  • src/Type/IntersectionType.php: getSortedTypes() calls sortTypes(..., true) so the new ordering applies to intersection descriptions only. Union ordering is intentionally left untouched.
  • Updated test expectations that encoded the old interface-first ordering:
    • tests/PHPStan/Analyser/nsrt/bug-9341.php, bug-4557.php, instanceof.php, array-destructuring.php, closure-types.php
    • tests/PHPStan/Analyser/data/bug-6649.php, bug-7344.php
    • tests/PHPStan/Rules/Arrays/NonexistentOffsetInArrayDimFetchRuleTest.php

Root cause

IntersectionType describes its members in the order produced by UnionTypeHelper::sortTypes(), whose final fallback compares describe() strings. For object members that comparison is unstable with respect to the class/interface distinction: static/$this/self describe with prefixes (static(...), $this(...), or the bare class name) that sort differently against interface names. The result was that the class sometimes landed before and sometimes after the interfaces. Sorting by the class-vs-interface property — using each type's getObjectClassReflections() + ClassReflection::isInterface() rather than its description — removes the dependency on alphabetical accidents.

Test

  • Added tests/PHPStan/Analyser/nsrt/bug-7343.php covering I&static, static&I, $this&I2, I2&$this, self&I, I&self, a class with multiple interfaces (self&I&I2), and a class intersected with a generic interface (self&ArrayAccess<int,int>). All now describe the class first. Verified the test fails on the static/I cases before the fix and passes after.
  • Probed analogous constructs (multiple interfaces, generic object interfaces, intersections appearing inside unions and class-string<...>): they all benefit from the same fix, and their existing test expectations were updated accordingly. Union ordering was deliberately not changed.

Fixes phpstan/phpstan#7343

- Add an opt-in `$sortClassesBeforeInterfaces` flag to `UnionTypeHelper::sortTypes()`; when set, object types backed by a single non-interface class reflection sort before object types backed by an interface.
- `IntersectionType::getSortedTypes()` passes the flag, so `self`/`static`/`$this` (and any concrete class) are now always placed before interfaces in the intersection, e.g. `static(HelloWorld)&I` and `$this(HelloWorld)&I2` instead of the previous inconsistent `I&static(HelloWorld)` vs `$this(HelloWorld)&I2`.
- The flag is left off for `UnionType` ordering, so union descriptions are unchanged.
- Updated test expectations that asserted the old interface-first ordering.
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.

self, static, $this should be sorted consistently

2 participants