Repository navigation
fix(weaving): handle #[\Override] on intercepted trait methods - #763
Open
lisachenko wants to merge 4 commits into
Open
lisachenko wants to merge 4 commits into
lisachenko wants to merge 4 commits into
Conversation
…verride]
Intercepting a method that a class imports from a trait and that carries
#[\Override] produced a proxy that could not be loaded: PHP copies the
attribute to the private "<method>OriginalAlias" alias of the proxy, which
overrides nothing ("has #[\Override] attribute, but no matching parent
method exists"). The attribute lives in the shared trait file, so it can
not be stripped per class like it is for methods of the class body.
WeavingTransformer::processSingleClass() now checks the intercepted
dynamic and static methods right after indexing them, before any
generator runs (native reflection reports the using class as declaring
class, so the generators can not detect it). A method whose `class` is
the woven class, whose declaring class is a trait and whose node carries
#[\Override] fails with a WeavingException naming the class, the method
and the trait, with the hint to exclude it with "&& !matchInherited()".
This covers trait aliases, static methods, enums and woven traits using
another trait. Trait methods inherited through a parent class are called
through parent::m(...) without an alias and keep working. The #[\Override]
name check is shared with the class-body stripping (isOverrideAttribute()).
Documented the limitation and its workarounds (`!matchInherited()`, a
narrower `!execution(...)`, or dropping #[\Override] from the trait) in
src/Instrument/AGENTS.md, docs/advices.md and the CHANGELOG; docs/pointcuts.md
now says that matchInherited() also matches members imported from traits.
Transformation performance group, base vs head interleaved, 6 runs each,
median ratios: SyntaxTreeRewriter default rules 0.096 -> 0.098 (+2.8%,
path not touched), all rules 1.337 -> 1.339 (+0.2%), WeavingTransformer
0.240 -> 0.240 (-0.2%).
Closes #761
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KXjD75egSqDNujmNS5kr5c
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## master #763 +/- ##
============================================
+ Coverage 97.63% 97.67% +0.03%
- Complexity 1829 1839 +10
============================================
Files 104 104
Lines 4829 4857 +28
============================================
+ Hits 4715 4744 +29
+ Misses 114 113 -1 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KXjD75egSqDNujmNS5kr5c
lisachenko
marked this pull request as ready for review
October 9, 2026 22:32
…aits Weaving a trait directly (a pointcut on the trait name) whose own intercepted method carries #[\Override] produced a fatal error in every class using it: the renamed original trait kept the attribute, the proxy trait aliases the method (`hello as private helloOriginalAlias`) and PHP copies #[\Override] to the alias, which overrides nothing. Unlike a method imported from another trait, the attribute lives in the file being woven, so the trait branch of processSingleClass() now strips it with stripOverrideAttributeFromInterceptedMethods(), the mechanism already used for classes and enums (lines are kept in place). The proxy trait method keeps #[\Override], copied from the AST by MethodGenerator, so PHP still checks the override in the using class. The rejection of trait-imported methods with #[\Override] is unchanged. Docs (src/Instrument/AGENTS.md, docs/advices.md, CHANGELOG) describe both cases and mention advising the trait itself as a way to intercept such a method. Transformation performance group, previous branch head vs this commit, interleaved, 6 runs each, median ratios: SyntaxTreeRewriter default rules 0.097 -> 0.098 (+1.0%), all rules 1.341 -> 1.325 (-1.2%), WeavingTransformer 0.243 -> 0.245 (+1.2%). Refs #761 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KXjD75egSqDNujmNS5kr5c
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KXjD75egSqDNujmNS5kr5c
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What and why
Fixes #761 the way decided in the issue: detect the trait-imported case, fail with a clear message, and document it. The related case of a woven trait's own
#[\Override]method is fixed properly (see the last section).The bug: on PHP 8.3+, intercepting a method that a class imports from a trait and that carries
#[\Override]produced an unloadable proxy. PHP copies the attribute to the private…OriginalAliasalias in the proxy's trait-use block. That alias overrides nothing, so loading the class failed withFatal error: …::helloOriginalAlias() has #[\Override] attribute, but no matching parent method exists. Class-body methods get the attribute stripped from the woven source, but a trait's attribute lives in the shared trait file and cannot be edited per class.The change:
WeavingTransformer::assertNoTraitImportedOverride()runs inprocessSingleClass()before the proxy generators, which cannot see the case because native reflection hides the trait. It throws aWeavingExceptionwhen all of these hold:#[\Override], compared by resolved name, souse Override as Ois handled.Method Bug3\Child::hello() is imported from trait Bug3\HelloTrait and has #[\Override], so it can not be intercepted: PHP copies the attribute to the private "helloOriginalAlias" alias of the proxy, which overrides nothing. Exclude it from the pointcut with "&& !matchInherited()".use T { foo as bar; }), static methods, enums, and woven traits that use another trait.parent::m(...)and creates no alias.#[\Override]handling is unchanged.#[\Override]check is extracted intoisOverrideAttribute(), which the existing stripping code now shares.src/Instrument/AGENTS.md: the limitation and its workarounds.docs/advices.md, Limitations: workarounds are&& !matchInherited(), the narrower&& !execution(public Foo->hello(*)), removing#[\Override]from the trait, or advising the trait itself.docs/pointcuts.md:matchInherited()also covers members imported from a trait, which was always true but undocumented.Verification
Repro from #761 (
Child extends Base { use HelloTrait; }, where the trait'shello()has#[\Override]):execution(public Bug3\Child->hello(*))Fatal error: …helloOriginalAlias() has #[\Override] attribute, but no matching parent method existsWeavingExceptionwith the message above&& !matchInherited()hello()returnstrait, and an own method ofChildis still advisedexecution(public OwnOverrideTrait->hello(*))(trait's own method)[advice] own-override-traitExtra cases run on master and on this branch:
Tests
WeavingTransformerTest:testWeaverRejectsTraitImportedMethodWithOverride, plus an…InEnumvariant: the message names the class, the method, the trait, the alias and!matchInherited().testWeaverInterceptsTraitImportedMethodWithOtherAttribute: a trait-imported method with a non-Override attribute is aliased and intercepted as usual.testWeaverInterceptsTraitMethodWithOverrideInheritedFromParent: no exception, and the proxy callsparent::count(...)with no alias.testWeaverDoesNotStripAttributesOfTraitImportedMethodsnow advises only the own method, because it used to intercept exactly the rejected case.PointcutSemanticsTest:!matchInherited()excludes trait-imported methods on a parser-reflection class.TraitCompositionTest: fixturesIssue761Base,Issue761OverrideTraitandIssue761Child, with the pointcut…Issue761Child->*(*) && !matchInherited(). The class is woven and loads in a subprocess, its own method is advised, and the trait method is not.Woven trait with its own
#[\Override]method — fixed hereWeaving a trait whose own intercepted method has
#[\Override]used to fail the same way (OwnOverrideUser::helloOriginalAlias() has #[\Override]…):adjustOriginalTrait()kept the attribute whileTraitProxyGeneratoraliased the method. Here the attribute lives in the file being woven, so it is fixed properly instead of rejected:processSingleClass()now callsstripOverrideAttributeFromInterceptedMethods()too (same line-preserving mechanism as classes and enums). The proxy trait method keeps#[\Override](copied from the AST), so PHP still checks the override in the using class.WeavingTransformerTest::testWeaverStripsOverrideAttributeFromInterceptedMethodsOfTrait(dynamic, static viause Override as OverrideAlias, attribute group; a non-intercepted method keeps the attribute; lines preserved; proxy methods keep#[\Override]) and functionalTraitCompositionTest::testWovenTraitMethodWithOverrideIsIntercepted(subprocess printsadvised:woven-trait). Both fail without the fix.PHP Fatal error: Scen\OwnOverrideUser::helloOriginalAlias() has #[\Override] attribute…, after[advice] own-override-trait.Checklist
type(scope): summary)composer checkpasses🤖 Generated with Claude Code
https://claude.ai/code/session_01KXjD75egSqDNujmNS5kr5c