Skip to content

Work out class members the way PHP links traits and inheritance - #890

Merged
Firehed merged 14 commits into
mainfrom
732/trait-composition
Oct 29, 2026
Merged

Firehed merged 14 commits into
mainfrom
732/trait-composition

Conversation

@Firehed

@Firehed Firehed commented Oct 29, 2026 •

Copy link
Copy Markdown
Owner

Closes #732. Closes #889. Closes #887.

When a class uses traits and also extends a parent, the server often reported different members than PHP does. For example:

class Base { public function save(): void {} }
trait NeedsSave { abstract public function save(): void; }
class Model extends Base { use NeedsSave; }

PHP gives Model::save() the concrete method from Base. The server showed the trait's abstract declaration instead, in hover, go to definition and completion. The same family of mistakes affected trait aliases (as), insteadof, and traits that use other traits.

Cause

The server worked out a class's members by collecting every declaration of a name it could reach (the class, its traits, its parents, its interfaces) and taking the nearest one, then applied as aliases in a separate step for the class being looked up only. PHP instead builds each class's member list once, when it loads the class, in a fixed order:

  1. The class's own members.
  2. Members from its traits, using each trait's already finished member list, with the class's as and insteadof rules applied. Abstract trait methods wait.
  3. Members inherited from the parent, for names the class does not already have.
  4. The traits' abstract methods, only for names still missing.
  5. Interface members, for names still missing.

Change

MemberResolver now builds each class's member list in that same order and answers every lookup from it. The old search and the separate alias step are gone. Rules that differ by kind of member (method, property, constant, enum case) live in MemberKind.

Visible results, each matching PHP:

  • An abstract trait method no longer hides a method the class has or inherits.
  • An as alias declared in a parent class, or inside a used trait, reaches the class (Trait member rules must match PHP #732 case 2).
  • A class's own method wins over an alias with the same name (Trait member rules must match PHP #732 case 3).
  • insteadof matches method names regardless of case (Trait member rules must match PHP #732 case 4), and also excludes what the excluded trait got from traits it uses.
  • A trait that uses itself together with an alias no longer crashes the server (Trait cycles through aliases must not crash the server #889). Its known-defect test now passes and moves into tests/EndToEnd/scripts/.
  • A parent's private constant is not reachable through a subclass.
  • A protected constructor that overrides another is checked against its own class.
  • Two static-property cases follow PHP: a trait's static property replaces the class's own, and a class's private static property does not shadow a subclass's instance property.

PHP 8.4 vs 8.5

PHP 8.5 changed when trait members are added relative to inheritance. For methods, 8.4 and 8.5 end up with the same members. For a constant or property declared identically in both a trait and a parent, they differ:

class P { protected const X = 1; public $p = 1; }
trait T { protected const X = 1; public $p = 1; }
class C extends P { use T; }
class Q extends P { public function read(): int { return C::X; } }
8.2 – 8.4 8.5
C::X declared by P C
C::$p declared by P C
(new Q)->read() 1 Error: Cannot access protected constant C::X

This follows 8.5, which reads as a bug fix, regardless of which PHP runs the server. Test fixtures avoid that case, since CI also runs on 8.4.

Tests

  • MemberResolverTypeGraphTest now checks, for every public method of every fixture class, that the server picks the same declaration PHP's reflection reports (same file, same line, same abstractness). Three new classes in tests/Fixtures/src/Hierarchy/, built on the existing ConflictingTraitA/B, TraitAdaptationUser and TraitAliasCollidingParent, cover the cases above; each fails against the previous resolver.
  • MemberResolverTest covers the constant, constructor, property, trait-cycle and visibility-only-alias cases. One existing test, testListedMethodsIncludeANamedAlias, now looks members up by name instead of by list position, since PHP adds an alias before the method it renames.
  • MemberKindTest and the new MemberEntryTest cover the new per-kind rules.

Known limits

Note for reviewers

The move of #889's test script landed in the commit titled "Wrap two long assertions" by mistake; the squash merge makes that moot.

🤖 Generated with Claude Code

Firehed and others added 8 commits October 29, 2026 16:04
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@codecov

codecov Bot commented Oct 29, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 99.70%. Comparing base (ee753a6) to head (fec0b3f).
⚠️ Report is 1 commits behind head on main.
✅ All tests successful. No failed tests found.

Additional details and impacted files
@@            Coverage Diff            @@
##               main     #890   +/-   ##
=========================================
  Coverage     99.70%   99.70%           
- Complexity     1954     1969   +15     
=========================================
  Files           146      147    +1     
  Lines          4464     4479   +15     
=========================================
+ Hits           4451     4466   +15     
  Misses           13       13           

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

@Firehed Firehed changed the title Answer members from the table PHP links Work out class members the way PHP links traits and inheritance Oct 29, 2026
Firehed and others added 6 commits October 29, 2026 16:29
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Firehed
Firehed marked this pull request as ready for review October 29, 2026 23:49
@Firehed
Firehed merged commit 2e54eaa into main Oct 29, 2026
8 checks passed
@Firehed
Firehed deleted the 732/trait-composition branch October 29, 2026 23:51
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.

Trait cycles through aliases must not crash the server Trait member rules must match PHP

1 participant