From 0e969f52cebee2d993749d955fa8dfff069ddbc8 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 9 Oct 2026 22:25:02 +0000 Subject: [PATCH 1/4] fix(weaving): reject interception of trait-imported methods with #[\Override] 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 "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 Claude-Session: https://claude.ai/code/session_01KXjD75egSqDNujmNS5kr5c --- CHANGELOG.md | 1 + docs/advices.md | 5 ++ docs/pointcuts.md | 2 +- src/Instrument/AGENTS.md | 4 + .../Transformer/WeavingTransformer.php | 74 ++++++++++++++++- tests/Aop/Pointcut/PointcutSemanticsTest.php | 16 ++++ .../project/src/Application/Issue761Base.php | 15 ++++ .../project/src/Application/Issue761Child.php | 18 ++++ .../src/Application/Issue761OverrideTrait.php | 16 ++++ .../src/Aspect/TraitCompositionAspect.php | 7 ++ tests/Functional/TraitCompositionTest.php | 37 +++++++++ .../Stubs/ClassExtendingOverrideTraitUser.php | 25 ++++++ .../Stubs/ClassUsingOverrideTrait.php | 4 +- .../Stubs/EnumUsingOverrideTrait.php | 24 ++++++ .../Transformer/WeavingTransformerTest.php | 82 ++++++++++++++++++- 15 files changed, 322 insertions(+), 8 deletions(-) create mode 100644 tests/Fixtures/project/src/Application/Issue761Base.php create mode 100644 tests/Fixtures/project/src/Application/Issue761Child.php create mode 100644 tests/Fixtures/project/src/Application/Issue761OverrideTrait.php create mode 100644 tests/Instrument/Transformer/Stubs/ClassExtendingOverrideTraitUser.php create mode 100644 tests/Instrument/Transformer/Stubs/EnumUsingOverrideTrait.php diff --git a/CHANGELOG.md b/CHANGELOG.md index c8bdff5b2..14f123121 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -38,6 +38,7 @@ See [UPGRADE-4.0.md](UPGRADE-4.0.md) for the migration guide. * [BC BREAK] **Removed unused API** — the `#[Aspect]` attribute, `Pointcut::KIND_TRAIT` (`KIND_ALL` is `119`), `InterceptedConstructorGenerator`, `FunctionParameterList` and a few unused methods; see UPGRADE-4.0. * [BC BREAK] **Pointcut semantics** — space-separated modifiers must all match and `|` alternatives bind tighter, so `final public|protected` no longer matches every non-final public method. `Foo+` also matches `Foo` itself and classes using the trait `Foo`. `@execution`/`@within`/`@access` match subclasses of the attribute. Return-type patterns accept the `?Foo` nullable marker, and regular-expression failures raise a `PointcutSyntaxException` instead of silently not matching. * [BC BREAK] **Methods returning by reference** can not be intercepted: weaving one fails with a `WeavingException` instead of silently returning a copy. The new `matchReturningByReference()` pointcut excludes them (`&& !matchReturningByReference()`). +* **Trait methods with `#[\Override]`** can not be intercepted in the class importing them: weaving one fails with a `WeavingException` instead of a fatal error at load time; exclude them with `&& !matchInherited()` (#761). * [BC BREAK] **Renames** — `TransformerResultEnum::RESULT_*` becomes `TransformerResult::{Aborted, Abstain, Transformed}`; enum cases use PascalCase everywhere (`FieldAccessType::Read`/`Write`, and the `Proxy\Generator` enums `Visibility`, `PropertyModifier`, `ClassModifier`); `AspectContainer::AOP_PROXIED_SUFFIX` becomes `ORIGINAL_TRAIT_SUFFIX` plus `ORIGINAL_TRAIT_FILE_SUFFIX`. **Performance** diff --git a/docs/advices.md b/docs/advices.md index bb6ea1279..f7af53e53 100644 --- a/docs/advices.md +++ b/docs/advices.md @@ -59,6 +59,11 @@ The joinpoints declare `@template` types, so PHPStan and IDEs know the types of - **Returning by reference.** A method or function declared `function &name()` can not be intercepted: joinpoints and advices return values, so the caller would get a copy instead of the reference. Weaving one fails with a `WeavingException`; exclude such methods from a broad pointcut with `&& !matchReturningByReference()`. +- **Trait methods with `#[\Override]`.** A method that a class imports from a trait and that carries `#[\Override]` + can not be intercepted in that class: PHP copies the attribute to the private alias the proxy calls, which + overrides nothing. Weaving one fails with a `WeavingException`; exclude trait methods from the pointcut with + `&& !matchInherited()`, exclude only that method (`&& !execution(public Foo->hello(*))`), or remove + `#[\Override]` from the trait. Trait methods that a class inherits from its parent class are not affected. - **Generators.** For a method that contains `yield`, the advice runs when the method is called and `proceed()` returns the `Generator` object. The advice does not see the yielded values, which are produced later, while the caller iterates. diff --git a/docs/pointcuts.md b/docs/pointcuts.md index 3e2a56522..633e05cb3 100644 --- a/docs/pointcuts.md +++ b/docs/pointcuts.md @@ -17,7 +17,7 @@ creation. It is the argument of an advice attribute, e.g. `#[Before('execution(p | `@execution(Attribute)` | Methods that carry the attribute | | `@access(Attribute)` | Properties that carry the attribute | | `@within(Attribute)` | Every member of classes that carry the attribute | -| `matchInherited()` | Only members inherited from a parent class; `!matchInherited()` excludes them | +| `matchInherited()` | Only members inherited from a parent class or imported from a trait; `!matchInherited()` excludes them | | `matchReturningByReference()` | Methods and functions declared `function &name()`; `!matchReturningByReference()` excludes them | | `$this->pointcutName` | A named pointcut declared with `#[Pointcut]` on a method of the same aspect | | `Aspect\Class->pointcutName` | A named pointcut of another aspect | diff --git a/src/Instrument/AGENTS.md b/src/Instrument/AGENTS.md index 79d4e6d92..23d9a65fd 100644 --- a/src/Instrument/AGENTS.md +++ b/src/Instrument/AGENTS.md @@ -79,6 +79,10 @@ Woven trait MUST preserve original source line numbers for XDebug breakpoints. ## PHP compat: #[\Override] on intercepted methods (8.3+) When intercepted method has #[\Override], PHP copies attribute to trait alias — fatal error (alias doesn't override anything). WeavingTransformer::convertClassToTrait() strips #[\Override] from trait for every intercepted method. Attribute preserved on proxy's override method (proxy extends same parent). +- Limitation (#761): a method imported from a trait with #[\Override] can not be intercepted — the attribute lives in the shared trait file, can't be stripped per class. assertNoTraitImportedOverride() (processSingleClass, before the generators; native reflection hides the trait) throws WeavingException naming class, method, trait. Covers aliases, static methods, enums, woven traits using a trait. +- Workaround: `&& !matchInherited()` (excludes trait-imported methods too), narrower `&& !execution(public Foo->hello(*))`, or drop #[\Override] from the trait method. +- Trait methods inherited via a parent class are fine: proxy calls `parent::m(...)`, no alias. +- Not handled: weaving a trait whose own method has #[\Override] (adjustOriginalTrait keeps it, TraitProxyGenerator aliases it → same fatal error in the using class). ## Aspects themselves Classes implementing \Go\Aop\Aspect: unconditionally skipped by WeavingTransformer. Aspects cannot weave themselves. diff --git a/src/Instrument/Transformer/WeavingTransformer.php b/src/Instrument/Transformer/WeavingTransformer.php index 20f025fa3..b46f271b7 100644 --- a/src/Instrument/Transformer/WeavingTransformer.php +++ b/src/Instrument/Transformer/WeavingTransformer.php @@ -175,6 +175,7 @@ private function processSingleClass( $originalImports = $namespace->getNamespaceAliases(); // Intercepted methods are looked up by name several times per method, here and in the proxy generators $methods = ClassProxyGenerator::indexMethods($class); + $this->assertNoTraitImportedOverride($class, $advices, $methods); // For traits: rename the trait (legacy approach, TraitProxyGenerator generates a child trait). // For enums: convert the enum body to a trait (cases extracted to proxy enum by EnumProxyGenerator). @@ -223,6 +224,61 @@ private function isAspectSafe(ReflectionClass $class): bool } } + /** + * Rejects the interception of a method imported from a trait that carries #[\Override] (issue #761) + * + * Every intercepted own method is aliased in the trait-use block of the proxy (`m as private mOriginalAlias`) + * and PHP copies the attributes of the method to that alias, which overrides nothing: #[\Override] on it is a + * fatal error at load time. For a method declared in the class body the attribute is stripped from the woven + * source (see stripOverrideAttributeFromInterceptedMethods()), but the attribute of a trait method lives in the + * trait file, which is shared by every class using it and can not be edited per class. The proxy generators can + * not detect the case: native reflection reports the using class as the declaring class of a trait method. + * + * Covered: trait aliases (`use T { foo as bar; }`), static methods, enums (EnumProxyGenerator aliases every + * intercepted method) and woven traits using another trait. Trait methods arriving through a parent class keep + * the parent in their `class` property and are called through `parent::m(...)` without an alias, so they pass. + * Methods of a trait used by an imported trait (nested traits) are not collected by parser-reflection 5.0.1, so + * they can not get here. + * + * @param array>> $advices + * @param array $methods Methods of the class by name + */ + private function assertNoTraitImportedOverride(ReflectionClass $class, array $advices, array $methods): void + { + $interceptedNames = array_merge( + array_keys($advices[AspectContainer::METHOD_PREFIX] ?? []), + array_keys($advices[AspectContainer::STATIC_METHOD_PREFIX] ?? []), + ); + + foreach ($interceptedNames as $methodName) { + $method = $methods[$methodName] ?? null; + // Methods inherited from a parent class (including the ones it imports from traits) are not aliased + if (!$method instanceof ReflectionMethod || $method->class !== $class->name) { + continue; + } + $declaringClass = $method->getDeclaringClass(); + if ($declaringClass->name === $class->name || !$declaringClass->isTrait()) { + continue; + } + foreach ($method->getNode()->attrGroups as $attrGroup) { + foreach ($attrGroup->attrs as $attribute) { + if (!self::isOverrideAttribute($attribute)) { + continue; + } + throw new WeavingException(sprintf( + 'Method %s::%s() is imported from trait %s and has #[\Override], so it can not be ' + . 'intercepted: PHP copies the attribute to the private "%sOriginalAlias" alias of the ' + . 'proxy, which overrides nothing. Exclude it from the pointcut with "&& !matchInherited()".', + $class->name, + $methodName, + $declaringClass->name, + $methodName, + )); + } + } + } + } + /** * Adjust definition of original trait source to enable extending */ @@ -629,21 +685,31 @@ private function stripOverrideAttributeFromInterceptedMethods( foreach ($interceptedNames as $methodName) { // The declaring class, not the `class` property: a method imported from a trait reports the using - // class there, while its attributes live in the trait source, not in this token stream + // class there, while its attributes live in the trait source, not in this token stream. Interception + // of such a method with #[\Override] is rejected earlier, by assertNoTraitImportedOverride() $method = $methods[$methodName] ?? null; if (!$method instanceof ReflectionMethod || $method->getDeclaringClass()->name !== $class->name) { continue; } - // Attribute names are compared as resolved by the parser, so aliases (`use Override as O; #[O]`), - // qualified names and attributes that merely end with "Override" are told apart correctly $this->stripAttributes( $method->getNode()->attrGroups, - static fn(Attribute $attribute): bool => self::resolveAttributeName($attribute) === 'Override', + self::isOverrideAttribute(...), $streamMetaData, ); } } + /** + * Checks whether the attribute is #[\Override], compared by the name resolved by the parser + * + * Aliases (`use Override as O; #[O]`), qualified names and attributes that merely end with "Override" are told + * apart correctly. + */ + private static function isOverrideAttribute(Attribute $attribute): bool + { + return self::resolveAttributeName($attribute) === 'Override'; + } + /** * Removes intercepted property declarations from the woven trait body. * diff --git a/tests/Aop/Pointcut/PointcutSemanticsTest.php b/tests/Aop/Pointcut/PointcutSemanticsTest.php index a1e97822e..ec7c7d45d 100644 --- a/tests/Aop/Pointcut/PointcutSemanticsTest.php +++ b/tests/Aop/Pointcut/PointcutSemanticsTest.php @@ -15,6 +15,8 @@ use Attribute; use Go\Aop\Exception\PointcutSyntaxException; use Go\Aop\Pointcut; +use Go\Instrument\Transformer\Stubs\ClassUsingOverrideTrait; +use Go\ParserReflection\ReflectionClass as ParserReflectionClass; use Go\Stubs\ByReferenceStub; use PhpParser\PrettyPrinter\Standard; use PHPUnit\Framework\Attributes\DataProvider; @@ -125,6 +127,20 @@ public function testMethodsReturningByReferenceCanBeFilteredOut(): void ); } + /** + * `!matchInherited()` also excludes the methods imported from a trait: the workaround for trait methods with + * #[\Override], which can not be intercepted (issue #761). Parser reflection is used, as the weaver does: + * native reflection reports the using class as the declaring class of a trait method. + */ + public function testNotMatchInheritedExcludesTraitImportedMethods(): void + { + $class = new ParserReflectionClass(ClassUsingOverrideTrait::class); + $pointcut = self::parse('execution(public ' . ClassUsingOverrideTrait::class . '->*(*)) && !matchInherited()'); + + $this->assertTrue($pointcut->matches($class, $class->getMethod('ownMethod'))); + $this->assertFalse($pointcut->matches($class, $class->getMethod('count'))); + } + public function testNullableReturnTypeMarker(): void { $class = new ReflectionClass(PointcutSemanticsTarget::class); diff --git a/tests/Fixtures/project/src/Application/Issue761Base.php b/tests/Fixtures/project/src/Application/Issue761Base.php new file mode 100644 index 000000000..2071693a3 --- /dev/null +++ b/tests/Fixtures/project/src/Application/Issue761Base.php @@ -0,0 +1,15 @@ +*(*))")] public function afterNonPublicMethod(): void {} + + /** + * Intercepts the own methods of Issue761Child only: its trait method with #[\Override] can not be + * intercepted (issue #761), `!matchInherited()` excludes the methods imported from traits. + */ + #[Pointcut\After("execution(public Go\Tests\TestProject\Application\Issue761Child->*(*)) && !matchInherited()")] + public function afterIssue761OwnMethod(): void {} } diff --git a/tests/Functional/TraitCompositionTest.php b/tests/Functional/TraitCompositionTest.php index 8b83e3d2a..87993e590 100644 --- a/tests/Functional/TraitCompositionTest.php +++ b/tests/Functional/TraitCompositionTest.php @@ -14,6 +14,9 @@ use Go\Tests\TestProject\Application\ClassUsingTrait; use Go\Tests\TestProject\Application\ClassWithPrivateMethods; +use Go\Tests\TestProject\Application\Issue761Child; +use Symfony\Component\Process\PhpExecutableFinder; +use Symfony\Component\Process\Process; /** * Functional tests for trait composition weaving and private/protected method interception. @@ -80,6 +83,40 @@ public function testOwnMethodOfClassUsingTraitIsWoven(): void ); } + /** + * A trait method with #[\Override] can not be intercepted (issue #761), the documented workaround is to exclude + * it with `!matchInherited()`: the class is woven for its own method, loads, and the trait method keeps its + * behaviour without an advice. + */ + public function testTraitMethodWithOverrideExcludedByNotMatchInherited(): void + { + $advisor = 'Go\\Tests\\TestProject\\Aspect\\TraitCompositionAspect->afterIssue761OwnMethod'; + $this->assertClassIsWoven(Issue761Child::class); + $this->assertMethodWoven(Issue761Child::class, 'own', $advisor); + $this->assertMethodNotWoven(Issue761Child::class, 'hello', $advisor); + + $phpExecutable = (new PhpExecutableFinder())->find(); + assert($phpExecutable !== false); + $script = sprintf( + 'include %s; $instance = new %s(); echo $instance->hello(), ",", $instance->own(), ",", ' + . '$instance instanceof \Go\Aop\Proxy ? "proxy" : "plain";', + var_export($this->configuration['frontController'], true), + '\\' . Issue761Child::class, + ); + $process = new Process( + [$phpExecutable, '-r', $script], + null, + ['GO_AOP_CONFIGURATION' => $this->getConfigurationName()], + ); + $process->run(); + + $this->assertTrue( + $process->isSuccessful(), + 'Loading the woven class failed: ' . $process->getOutput() . $process->getErrorOutput(), + ); + $this->assertSame('trait,own,proxy', trim($process->getOutput())); + } + // ------------------------------------------------------------------------- // Private / protected method interception // ------------------------------------------------------------------------- diff --git a/tests/Instrument/Transformer/Stubs/ClassExtendingOverrideTraitUser.php b/tests/Instrument/Transformer/Stubs/ClassExtendingOverrideTraitUser.php new file mode 100644 index 000000000..952988e02 --- /dev/null +++ b/tests/Instrument/Transformer/Stubs/ClassExtendingOverrideTraitUser.php @@ -0,0 +1,25 @@ + + * + * This source file is subject to the license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Go\Instrument\Transformer\Stubs; + +/** + * Weaving input for a trait method with #[\Override] that arrives through the parent class: the proxy calls it + * through `parent::count(...)` without a trait alias, so it can be intercepted (issue #761) + */ +class ClassExtendingOverrideTraitUser extends ClassUsingOverrideTrait +{ + public function childMethod(): string + { + return 'child'; + } +} diff --git a/tests/Instrument/Transformer/Stubs/ClassUsingOverrideTrait.php b/tests/Instrument/Transformer/Stubs/ClassUsingOverrideTrait.php index a0b0505c7..d5130b720 100644 --- a/tests/Instrument/Transformer/Stubs/ClassUsingOverrideTrait.php +++ b/tests/Instrument/Transformer/Stubs/ClassUsingOverrideTrait.php @@ -13,8 +13,8 @@ namespace Go\Instrument\Transformer\Stubs; /** - * Weaving input for an advice that matches a method imported from a trait with #[\Override]: the attribute is - * declared in the trait file, the token stream of this file must not be touched for it. + * Weaving input for a class importing a trait method with #[\Override]: the attribute is declared in the trait file, + * the token stream of this file must not be touched for it, and the interception of `count` is rejected (#761). */ class ClassUsingOverrideTrait implements \Countable { diff --git a/tests/Instrument/Transformer/Stubs/EnumUsingOverrideTrait.php b/tests/Instrument/Transformer/Stubs/EnumUsingOverrideTrait.php new file mode 100644 index 000000000..1c4d574e1 --- /dev/null +++ b/tests/Instrument/Transformer/Stubs/EnumUsingOverrideTrait.php @@ -0,0 +1,24 @@ + + * + * This source file is subject to the license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Go\Instrument\Transformer\Stubs; + +/** + * Weaving input for an enum importing a trait method with #[\Override]: the enum proxy aliases every intercepted + * method, so the interception of `count` is rejected (issue #761) + */ +enum EnumUsingOverrideTrait implements \Countable +{ + use OverrideTrait; + + case First; +} diff --git a/tests/Instrument/Transformer/WeavingTransformerTest.php b/tests/Instrument/Transformer/WeavingTransformerTest.php index 97abddac6..10ad34a9e 100644 --- a/tests/Instrument/Transformer/WeavingTransformerTest.php +++ b/tests/Instrument/Transformer/WeavingTransformerTest.php @@ -1220,7 +1220,6 @@ public function testWeaverDoesNotStripAttributesOfTraitImportedMethods(): void $classFqn = Stubs\ClassUsingOverrideTrait::class; $transformer = $this->createTransformerWithAdvices([ AspectContainer::METHOD_PREFIX => [ - 'count' => ["advisor.{$classFqn}->count" => new BeforeInterceptor(static function (): void {})], 'ownMethod' => ["advisor.{$classFqn}->ownMethod" => new BeforeInterceptor(static function (): void {})], ], ]); @@ -1236,6 +1235,87 @@ public function testWeaverDoesNotStripAttributesOfTraitImportedMethods(): void $this->assertStringContainsString(rtrim($originalBody), $woven); } + /** + * A method imported from a trait with #[\Override] can not be intercepted: PHP copies the attribute to the + * private alias of the proxy, which overrides nothing, and the attribute lives in the shared trait file, so it + * can not be stripped per class (issue #761). Weaving fails with a hint to exclude the method. + */ + public function testWeaverRejectsTraitImportedMethodWithOverride(): void + { + $classFqn = Stubs\ClassUsingOverrideTrait::class; + $transformer = $this->createTransformerWithAdvices([ + AspectContainer::METHOD_PREFIX => [ + 'count' => ["advisor.{$classFqn}->count" => new BeforeInterceptor(static function (): void {})], + 'ownMethod' => ["advisor.{$classFqn}->ownMethod" => new BeforeInterceptor(static function (): void {})], + ], + ]); + + $metadata = $this->loadStubMetadata('ClassUsingOverrideTrait'); + + try { + $transformer->transform($metadata); + $this->fail('Interception of a trait-imported method with #[\Override] must be rejected'); + } catch (WeavingException $exception) { + $message = $exception->getMessage(); + $this->assertStringContainsString('ClassUsingOverrideTrait::count()', $message); + $this->assertStringContainsString('imported from trait ' . Stubs\OverrideTrait::class, $message); + $this->assertStringContainsString('"countOriginalAlias"', $message); + $this->assertStringContainsString('!matchInherited()', $message); + } + } + + /** + * Enum proxies alias every intercepted method, so a trait-imported method with #[\Override] is rejected there too + */ + public function testWeaverRejectsTraitImportedMethodWithOverrideInEnum(): void + { + $enumFqn = Stubs\EnumUsingOverrideTrait::class; + $transformer = $this->createTransformerWithAdvices([ + AspectContainer::METHOD_PREFIX => [ + 'count' => ["advisor.{$enumFqn}->count" => new BeforeInterceptor(static function (): void {})], + ], + ]); + + $metadata = $this->loadStubMetadata('EnumUsingOverrideTrait'); + + try { + $transformer->transform($metadata); + $this->fail('Interception of a trait-imported method with #[\Override] in an enum must be rejected'); + } catch (WeavingException $exception) { + $this->assertStringContainsString( + 'EnumUsingOverrideTrait::count() is imported from trait ' . Stubs\OverrideTrait::class, + $exception->getMessage(), + ); + } + } + + /** + * A trait method with #[\Override] that arrives through the parent class is not aliased: the proxy calls it + * through `parent::count(...)`, the attribute of the parent's method is never copied, so it is intercepted. + */ + public function testWeaverInterceptsTraitMethodWithOverrideInheritedFromParent(): void + { + $classFqn = Stubs\ClassExtendingOverrideTraitUser::class; + $transformer = $this->createTransformerWithAdvices([ + AspectContainer::METHOD_PREFIX => [ + 'count' => ["advisor.{$classFqn}->count" => new BeforeInterceptor(static function (): void {})], + ], + ]); + + $metadata = $this->loadStubMetadata('ClassExtendingOverrideTraitUser'); + $transformer->transform($metadata); + + $actual = $this->normalizeWhitespaces($metadata->source); + $this->assertStringContainsString('trait ClassExtendingOverrideTraitUserOriginalTrait', $actual); + + $matches = []; + $this->assertSame(1, preg_match("/AOP_CACHE_DIR . '(.+)';$/m", $actual, $matches)); + $proxyContent = $this->normalizeWhitespaces((string) file_get_contents('vfs://' . $matches[1])); + + $this->assertStringContainsString('parent::count(...)', $proxyContent); + $this->assertStringNotContainsString('countOriginalAlias', $proxyContent); + } + /** * Global functions called from a namespace are woven into a per-namespace proxy file placed * in the `_functions/` cache sub-directory, and the original file receives an include_once From b321bc89ca80759d7c853521de049f1692a0b30a Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 9 Oct 2026 22:30:21 +0000 Subject: [PATCH 2/4] test(weaving): cover trait-imported method with non-Override attribute Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01KXjD75egSqDNujmNS5kr5c --- .../Transformer/Stubs/OverrideTrait.php | 9 +++++++- .../Transformer/WeavingTransformerTest.php | 23 +++++++++++++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/tests/Instrument/Transformer/Stubs/OverrideTrait.php b/tests/Instrument/Transformer/Stubs/OverrideTrait.php index f71a2ecd7..5dcb15c3e 100644 --- a/tests/Instrument/Transformer/Stubs/OverrideTrait.php +++ b/tests/Instrument/Transformer/Stubs/OverrideTrait.php @@ -13,7 +13,8 @@ namespace Go\Instrument\Transformer\Stubs; /** - * Trait method with #[\Override], imported by {@see ClassUsingOverrideTrait}: its attribute lives in this file + * Trait method with #[\Override], imported by {@see ClassUsingOverrideTrait}: its attribute lives in this file. + * The `label` method carries another attribute only, so it can be intercepted. */ trait OverrideTrait { @@ -22,4 +23,10 @@ public function count(): int { return 0; } + + #[\ReturnTypeWillChange] + public function label(): string + { + return 'label'; + } } diff --git a/tests/Instrument/Transformer/WeavingTransformerTest.php b/tests/Instrument/Transformer/WeavingTransformerTest.php index 10ad34a9e..260675f1b 100644 --- a/tests/Instrument/Transformer/WeavingTransformerTest.php +++ b/tests/Instrument/Transformer/WeavingTransformerTest.php @@ -1264,6 +1264,29 @@ public function testWeaverRejectsTraitImportedMethodWithOverride(): void } } + /** + * A trait-imported method with attributes other than #[\Override] is aliased and intercepted as usual + */ + public function testWeaverInterceptsTraitImportedMethodWithOtherAttribute(): void + { + $classFqn = Stubs\ClassUsingOverrideTrait::class; + $transformer = $this->createTransformerWithAdvices([ + AspectContainer::METHOD_PREFIX => [ + 'label' => ["advisor.{$classFqn}->label" => new BeforeInterceptor(static function (): void {})], + ], + ]); + + $metadata = $this->loadStubMetadata('ClassUsingOverrideTrait'); + $transformer->transform($metadata); + + $actual = $this->normalizeWhitespaces($metadata->source); + $matches = []; + $this->assertSame(1, preg_match("/AOP_CACHE_DIR . '(.+)';$/m", $actual, $matches)); + $proxyContent = $this->normalizeWhitespaces((string) file_get_contents('vfs://' . $matches[1])); + + $this->assertStringContainsString('as private labelOriginalAlias;', $proxyContent); + } + /** * Enum proxies alias every intercepted method, so a trait-imported method with #[\Override] is rejected there too */ From 1cd8ed339ecaeee76e7cd1bda1f5976f0a7991cb Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 9 Oct 2026 22:43:00 +0000 Subject: [PATCH 3/4] fix(weaving): strip #[\Override] from intercepted methods of woven traits 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 Claude-Session: https://claude.ai/code/session_01KXjD75egSqDNujmNS5kr5c --- CHANGELOG.md | 2 +- docs/advices.md | 5 +- src/Instrument/AGENTS.md | 7 +-- .../Transformer/WeavingTransformer.php | 5 +- .../Issue761WovenOverrideTrait.php | 17 ++++++ .../Issue761WovenOverrideTraitUser.php | 12 ++++ .../src/Aspect/TraitCompositionAspect.php | 11 ++++ tests/Functional/TraitCompositionTest.php | 41 +++++++++++--- .../Transformer/Stubs/WovenOverrideTrait.php | 46 ++++++++++++++++ .../Transformer/WeavingTransformerTest.php | 55 +++++++++++++++++++ 10 files changed, 187 insertions(+), 14 deletions(-) create mode 100644 tests/Fixtures/project/src/Application/Issue761WovenOverrideTrait.php create mode 100644 tests/Fixtures/project/src/Application/Issue761WovenOverrideTraitUser.php create mode 100644 tests/Instrument/Transformer/Stubs/WovenOverrideTrait.php diff --git a/CHANGELOG.md b/CHANGELOG.md index 14f123121..987309bb0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -38,7 +38,7 @@ See [UPGRADE-4.0.md](UPGRADE-4.0.md) for the migration guide. * [BC BREAK] **Removed unused API** — the `#[Aspect]` attribute, `Pointcut::KIND_TRAIT` (`KIND_ALL` is `119`), `InterceptedConstructorGenerator`, `FunctionParameterList` and a few unused methods; see UPGRADE-4.0. * [BC BREAK] **Pointcut semantics** — space-separated modifiers must all match and `|` alternatives bind tighter, so `final public|protected` no longer matches every non-final public method. `Foo+` also matches `Foo` itself and classes using the trait `Foo`. `@execution`/`@within`/`@access` match subclasses of the attribute. Return-type patterns accept the `?Foo` nullable marker, and regular-expression failures raise a `PointcutSyntaxException` instead of silently not matching. * [BC BREAK] **Methods returning by reference** can not be intercepted: weaving one fails with a `WeavingException` instead of silently returning a copy. The new `matchReturningByReference()` pointcut excludes them (`&& !matchReturningByReference()`). -* **Trait methods with `#[\Override]`** can not be intercepted in the class importing them: weaving one fails with a `WeavingException` instead of a fatal error at load time; exclude them with `&& !matchInherited()` (#761). +* **Trait methods with `#[\Override]`** can not be intercepted in the class importing them: weaving one fails with a `WeavingException` instead of a fatal error at load time; exclude them with `&& !matchInherited()`, or advise the trait itself: a woven trait strips `#[\Override]` from its intercepted methods like a woven class does, instead of failing to load (#761). * [BC BREAK] **Renames** — `TransformerResultEnum::RESULT_*` becomes `TransformerResult::{Aborted, Abstain, Transformed}`; enum cases use PascalCase everywhere (`FieldAccessType::Read`/`Write`, and the `Proxy\Generator` enums `Visibility`, `PropertyModifier`, `ClassModifier`); `AspectContainer::AOP_PROXIED_SUFFIX` becomes `ORIGINAL_TRAIT_SUFFIX` plus `ORIGINAL_TRAIT_FILE_SUFFIX`. **Performance** diff --git a/docs/advices.md b/docs/advices.md index f7af53e53..8aa18564d 100644 --- a/docs/advices.md +++ b/docs/advices.md @@ -63,7 +63,10 @@ The joinpoints declare `@template` types, so PHPStan and IDEs know the types of can not be intercepted in that class: PHP copies the attribute to the private alias the proxy calls, which overrides nothing. Weaving one fails with a `WeavingException`; exclude trait methods from the pointcut with `&& !matchInherited()`, exclude only that method (`&& !execution(public Foo->hello(*))`), or remove - `#[\Override]` from the trait. Trait methods that a class inherits from its parent class are not affected. + `#[\Override]` from the trait. To intercept the method anyway, advise the trait itself + (`execution(public HelloTrait->hello(*))`): the trait is woven, which works with `#[\Override]`, and the advice + applies in every class using the trait. Trait methods that a class inherits from its parent class are not + affected. - **Generators.** For a method that contains `yield`, the advice runs when the method is called and `proceed()` returns the `Generator` object. The advice does not see the yielded values, which are produced later, while the caller iterates. diff --git a/src/Instrument/AGENTS.md b/src/Instrument/AGENTS.md index 23d9a65fd..384a84902 100644 --- a/src/Instrument/AGENTS.md +++ b/src/Instrument/AGENTS.md @@ -78,11 +78,10 @@ Woven trait MUST preserve original source line numbers for XDebug breakpoints. ## PHP compat: #[\Override] on intercepted methods (8.3+) When intercepted method has #[\Override], PHP copies attribute to trait alias — fatal error (alias doesn't override anything). -WeavingTransformer::convertClassToTrait() strips #[\Override] from trait for every intercepted method. Attribute preserved on proxy's override method (proxy extends same parent). -- Limitation (#761): a method imported from a trait with #[\Override] can not be intercepted — the attribute lives in the shared trait file, can't be stripped per class. assertNoTraitImportedOverride() (processSingleClass, before the generators; native reflection hides the trait) throws WeavingException naming class, method, trait. Covers aliases, static methods, enums, woven traits using a trait. -- Workaround: `&& !matchInherited()` (excludes trait-imported methods too), narrower `&& !execution(public Foo->hello(*))`, or drop #[\Override] from the trait method. +WeavingTransformer::stripOverrideAttributeFromInterceptedMethods() strips #[\Override] from the woven source for every intercepted own method: classes (convertClassToTrait), enums (convertEnumToTrait) and woven traits (renamed original trait, its methods aliased by TraitProxyGenerator). Attribute preserved on the proxy method (MethodGenerator copies it from the AST), so PHP still checks the override. +- Limitation (#761): a method a class/enum/trait imports from ANOTHER trait with #[\Override] can not be intercepted — the attribute lives in the shared trait file, can't be stripped per class. assertNoTraitImportedOverride() (processSingleClass, before the generators; native reflection hides the trait) throws WeavingException naming class, method, trait. Covers aliases, static methods, enums, woven traits using a trait. +- Workaround: `&& !matchInherited()` (excludes trait-imported methods too), narrower `&& !execution(public Foo->hello(*))`, drop #[\Override] from the trait method, or advise the trait itself (`execution(public HelloTrait->hello(*))`, intercepts it in every class using the trait). - Trait methods inherited via a parent class are fine: proxy calls `parent::m(...)`, no alias. -- Not handled: weaving a trait whose own method has #[\Override] (adjustOriginalTrait keeps it, TraitProxyGenerator aliases it → same fatal error in the using class). ## Aspects themselves Classes implementing \Go\Aop\Aspect: unconditionally skipped by WeavingTransformer. Aspects cannot weave themselves. diff --git a/src/Instrument/Transformer/WeavingTransformer.php b/src/Instrument/Transformer/WeavingTransformer.php index b46f271b7..03e58ede8 100644 --- a/src/Instrument/Transformer/WeavingTransformer.php +++ b/src/Instrument/Transformer/WeavingTransformer.php @@ -183,6 +183,8 @@ private function processSingleClass( if ($class->isTrait()) { $this->removeInterceptedPropertiesFromTraitBody($class, $advices, $metadata); $this->adjustOriginalTrait($class, $metadata, $newClassName); + // The proxy trait aliases every intercepted method of the renamed trait, PHP would copy #[\Override] there + $this->stripOverrideAttributeFromInterceptedMethods($class, $advices, $methods, $metadata); $childProxyGenerator = new TraitProxyGenerator($class, $newFqcn, $advices, $originalImports, $methods); } elseif ($class->isEnum()) { $this->convertEnumToTrait($class, $advices, $methods, $metadata, $newClassName); @@ -667,7 +669,8 @@ private function convertEnumToTrait( * `SomeTrait::method as private methodOriginalAlias`), PHP copies the method's attributes to * the alias. If the original method had `#[\Override]`, the alias name has no matching * parent method → fatal error. We strip the attribute only from methods that will be aliased - * (those with dynamic or static method advices). + * (those with dynamic or static method advices). Applies to woven classes, enums and traits: the proxy trait + * of a woven trait aliases its intercepted methods the same way, and the proxy methods keep #[\Override]. * * @param array>> $advices * @param array $methods Methods of the class by name diff --git a/tests/Fixtures/project/src/Application/Issue761WovenOverrideTrait.php b/tests/Fixtures/project/src/Application/Issue761WovenOverrideTrait.php new file mode 100644 index 000000000..3dc667c42 --- /dev/null +++ b/tests/Fixtures/project/src/Application/Issue761WovenOverrideTrait.php @@ -0,0 +1,17 @@ +*(*)) && !matchInherited()")] public function afterIssue761OwnMethod(): void {} + + /** + * Intercepts the own #[\Override] method of a woven trait (issue #761): the result is prefixed, so a functional + * test sees that the advice runs in the class using the trait. + */ + #[Pointcut\Around("execution(public Go\Tests\TestProject\Application\Issue761WovenOverrideTrait->hello(*))")] + public function aroundIssue761WovenTraitMethod(MethodInvocation $invocation): mixed + { + return 'advised:' . $invocation->proceed(); + } } diff --git a/tests/Functional/TraitCompositionTest.php b/tests/Functional/TraitCompositionTest.php index 87993e590..ef984817f 100644 --- a/tests/Functional/TraitCompositionTest.php +++ b/tests/Functional/TraitCompositionTest.php @@ -15,6 +15,8 @@ use Go\Tests\TestProject\Application\ClassUsingTrait; use Go\Tests\TestProject\Application\ClassWithPrivateMethods; use Go\Tests\TestProject\Application\Issue761Child; +use Go\Tests\TestProject\Application\Issue761WovenOverrideTrait; +use Go\Tests\TestProject\Application\Issue761WovenOverrideTraitUser; use Symfony\Component\Process\PhpExecutableFinder; use Symfony\Component\Process\Process; @@ -95,14 +97,38 @@ public function testTraitMethodWithOverrideExcludedByNotMatchInherited(): void $this->assertMethodWoven(Issue761Child::class, 'own', $advisor); $this->assertMethodNotWoven(Issue761Child::class, 'hello', $advisor); + $output = $this->runInProject( + sprintf( + '$instance = new %s(); echo $instance->hello(), ",", $instance->own(), ",", ' + . '$instance instanceof \Go\Aop\Proxy ? "proxy" : "plain";', + '\\' . Issue761Child::class, + ), + ); + $this->assertSame('trait,own,proxy', $output); + } + + /** + * A woven trait whose own intercepted method has #[\Override] (issue #761): the attribute is stripped from the + * renamed original trait, so the class using the trait loads, and the advice runs. + */ + public function testWovenTraitMethodWithOverrideIsIntercepted(): void + { + $this->assertClassIsWoven(Issue761WovenOverrideTrait::class); + + $output = $this->runInProject( + sprintf('echo (new %s())->hello();', '\\' . Issue761WovenOverrideTraitUser::class), + ); + $this->assertSame('advised:woven-trait', $output); + } + + /** + * Runs PHP code in a subprocess after the front controller of the fixture project, returns its trimmed output + */ + private function runInProject(string $code): string + { $phpExecutable = (new PhpExecutableFinder())->find(); assert($phpExecutable !== false); - $script = sprintf( - 'include %s; $instance = new %s(); echo $instance->hello(), ",", $instance->own(), ",", ' - . '$instance instanceof \Go\Aop\Proxy ? "proxy" : "plain";', - var_export($this->configuration['frontController'], true), - '\\' . Issue761Child::class, - ); + $script = sprintf('include %s; %s', var_export($this->configuration['frontController'], true), $code); $process = new Process( [$phpExecutable, '-r', $script], null, @@ -114,7 +140,8 @@ public function testTraitMethodWithOverrideExcludedByNotMatchInherited(): void $process->isSuccessful(), 'Loading the woven class failed: ' . $process->getOutput() . $process->getErrorOutput(), ); - $this->assertSame('trait,own,proxy', trim($process->getOutput())); + + return trim($process->getOutput()); } // ------------------------------------------------------------------------- diff --git a/tests/Instrument/Transformer/Stubs/WovenOverrideTrait.php b/tests/Instrument/Transformer/Stubs/WovenOverrideTrait.php new file mode 100644 index 000000000..80e1dfebc --- /dev/null +++ b/tests/Instrument/Transformer/Stubs/WovenOverrideTrait.php @@ -0,0 +1,46 @@ + + * + * This source file is subject to the license that is bundled + * with this source code in the file LICENSE. + */ + +namespace Go\Instrument\Transformer\Stubs; + +use Override as OverrideAlias; + +/** + * Trait weaving input with #[\Override] on its own methods: the attribute must be stripped from the intercepted + * methods of the renamed original trait, as the proxy trait aliases them (issue #761) + */ +trait WovenOverrideTrait +{ + #[\Override] + public function hello(): string + { + return 'hello'; + } + + #[OverrideAlias] + public static function make(): string + { + return 'make'; + } + + #[\ReturnTypeWillChange, \Override] + public function count(): int + { + return 0; + } + + #[\Override] + public function notIntercepted(): string + { + return 'kept'; + } +} diff --git a/tests/Instrument/Transformer/WeavingTransformerTest.php b/tests/Instrument/Transformer/WeavingTransformerTest.php index 260675f1b..4ba596bb3 100644 --- a/tests/Instrument/Transformer/WeavingTransformerTest.php +++ b/tests/Instrument/Transformer/WeavingTransformerTest.php @@ -1061,6 +1061,61 @@ public function testWeaverForTraitRenamesOriginalTraitAndMovesInterceptedPropert $this->assertStringContainsString('$interceptedProperty', $proxyContent); } + /** + * A woven trait aliases every intercepted method in the proxy trait (`m as private mOriginalAlias`), and PHP + * copies #[\Override] to the alias of the class using it, which overrides nothing (issue #761). The attribute + * lives in the file being woven, so it is stripped from the intercepted methods of the renamed original trait, + * keeping every line in place, and stays on the methods of the proxy trait. + */ + public function testWeaverStripsOverrideAttributeFromInterceptedMethodsOfTrait(): void + { + $traitFqn = Stubs\WovenOverrideTrait::class; + $transformer = $this->createTransformerWithAdvices([ + AspectContainer::METHOD_PREFIX => [ + 'hello' => ["advisor.{$traitFqn}->hello" => new BeforeInterceptor(static function (): void {})], + 'count' => ["advisor.{$traitFqn}->count" => new BeforeInterceptor(static function (): void {})], + ], + AspectContainer::STATIC_METHOD_PREFIX => [ + 'make' => ["advisor.{$traitFqn}->make" => new BeforeInterceptor(static function (): void {})], + ], + ]); + + $metadata = $this->loadStubMetadata('WovenOverrideTrait'); + $original = $metadata->source; + $transformer->transform($metadata); + + $woven = $metadata->source; + $this->assertStringContainsString('trait WovenOverrideTraitOriginalTrait', $woven); + // Only the method that is not intercepted keeps #[\Override], an aliased one is recognised too + $this->assertSame(1, preg_match_all('/#\[\\\\Override\]\s+public/', $woven)); + $this->assertMatchesRegularExpression('/#\[\\\\Override\]\s+public function notIntercepted\(\)/', $woven); + $this->assertStringNotContainsString('#[OverrideAlias]', $woven); + $this->assertStringNotContainsString(', \Override', $woven); + $this->assertStringContainsString('#[\ReturnTypeWillChange', $woven); + // Every method stays at its original line + foreach (['hello', 'make', 'count', 'notIntercepted'] as $methodName) { + $pattern = '/^.*function ' . $methodName . '\(/m'; + $this->assertSame(1, preg_match($pattern, $original, $originalMatch, PREG_OFFSET_CAPTURE)); + $this->assertSame(1, preg_match($pattern, $woven, $wovenMatch, PREG_OFFSET_CAPTURE)); + $this->assertSame( + substr_count(substr($original, 0, $originalMatch[0][1]), "\n"), + substr_count(substr($woven, 0, $wovenMatch[0][1]), "\n"), + "Method {$methodName}() must stay at its original line", + ); + } + + $matches = []; + $this->assertSame(1, preg_match("/AOP_CACHE_DIR . '(.+)';$/m", $woven, $matches)); + $proxyContent = $this->normalizeWhitespaces((string) file_get_contents('vfs://' . $matches[1])); + + // The proxy trait methods keep the Override semantics of the original methods + $this->assertStringContainsString('as private helloOriginalAlias;', $proxyContent); + $this->assertStringContainsString('as private makeOriginalAlias;', $proxyContent); + $this->assertMatchesRegularExpression('/#\[\\\\Override\]\s+public function hello\(\)/', $proxyContent); + $this->assertMatchesRegularExpression('/#\[\\\\Override\]\s+public static function make\(\)/', $proxyContent); + $this->assertMatchesRegularExpression('/\\\\Override\]\s+public function count\(\)/', $proxyContent); + } + /** * `abstract` is a class-only modifier — convertClassToTrait() must drop it, otherwise the * woven file would contain the invalid `abstract trait ...` declaration. Abstract *methods* From a5260379db82ffb4cf04d692050819c075a80829 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 9 Oct 2026 22:44:00 +0000 Subject: [PATCH 4/4] docs(changelog): clarify the #[\Override] trait entry Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01KXjD75egSqDNujmNS5kr5c --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 987309bb0..b038c9b21 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -38,7 +38,7 @@ See [UPGRADE-4.0.md](UPGRADE-4.0.md) for the migration guide. * [BC BREAK] **Removed unused API** — the `#[Aspect]` attribute, `Pointcut::KIND_TRAIT` (`KIND_ALL` is `119`), `InterceptedConstructorGenerator`, `FunctionParameterList` and a few unused methods; see UPGRADE-4.0. * [BC BREAK] **Pointcut semantics** — space-separated modifiers must all match and `|` alternatives bind tighter, so `final public|protected` no longer matches every non-final public method. `Foo+` also matches `Foo` itself and classes using the trait `Foo`. `@execution`/`@within`/`@access` match subclasses of the attribute. Return-type patterns accept the `?Foo` nullable marker, and regular-expression failures raise a `PointcutSyntaxException` instead of silently not matching. * [BC BREAK] **Methods returning by reference** can not be intercepted: weaving one fails with a `WeavingException` instead of silently returning a copy. The new `matchReturningByReference()` pointcut excludes them (`&& !matchReturningByReference()`). -* **Trait methods with `#[\Override]`** can not be intercepted in the class importing them: weaving one fails with a `WeavingException` instead of a fatal error at load time; exclude them with `&& !matchInherited()`, or advise the trait itself: a woven trait strips `#[\Override]` from its intercepted methods like a woven class does, instead of failing to load (#761). +* **Trait methods with `#[\Override]`** — a woven trait strips `#[\Override]` from its intercepted methods like a woven class does, instead of failing to load. A class importing such a method can not intercept it: weaving fails with a `WeavingException` instead of a fatal error at load time; exclude it with `&& !matchInherited()` or advise the trait itself (#761). * [BC BREAK] **Renames** — `TransformerResultEnum::RESULT_*` becomes `TransformerResult::{Aborted, Abstain, Transformed}`; enum cases use PascalCase everywhere (`FieldAccessType::Read`/`Write`, and the `Proxy\Generator` enums `Visibility`, `PropertyModifier`, `ClassModifier`); `AspectContainer::AOP_PROXIED_SUFFIX` becomes `ORIGINAL_TRAIT_SUFFIX` plus `ORIGINAL_TRAIT_FILE_SUFFIX`. **Performance**