Summary
Weaving any class that carries a class-level attribute produces a corrupted trait: WeavingTransformer::convertClassToTrait() mangles the attribute tokens and drops the class keyword entirely, so the woven file no longer parses.
Found during the PHP 8.5 support audit (PR #597). Reproduces identically on PHP 8.4, 8.5.10 and 8.6.0beta2.
Reproduction
#[ExprAttr]
class Php80ClassAttrPlain
{
public function run(): int
{
return 42;
}
}
Woven output (method advice on run):
#[Php80ClassAttrPlain__AopProxied
{
public function run(): int
...
PHP Parse error: syntax error, unexpected token "{", expecting "]"
With attribute arguments it gets worse — #[\Attribute(\Attribute::TARGET_ALL)] becomes #[\Attribute(\Attribute::Php80ClassAttrPlain__AopProxied (the first T_STRING inside the attribute group is renamed to the trait name).
Fixture: tests/Fixtures/audit/src/Php80ClassAttrPlain.php, harness tests/Instrument/Transformer/Php85AuditScratchTest.php on the audit branch.
Root cause
src/Instrument/Transformer/WeavingTransformer.php — convertClassToTrait() (~line 237) starts its token scan from the class node's start position. In nikic/php-parser, a ClassLike node's startTokenPos includes its attribute groups, so the scan that expects final|abstract|readonly modifiers followed by T_CLASS and the class name instead lands inside the #[...] group: the first T_STRING there gets renamed to <Class>__AopProxied, and the delete-until-{ step then eats the rest of the attribute plus the real class <Name> header.
Note the enum path (convertEnumToTrait()) is not affected — the existing #[Loggable] enum BackedEnum functional fixture weaves fine — which is why this was never caught: no existing woven-class fixture has a class-level attribute.
Proposed fix
In convertClassToTrait(), begin the token scan after the class node's attribute groups — e.g. use the position after the last AttributeGroup sub-node (or scan forward for the actual T_CLASS token at depth 0, skipping T_ATTRIBUTE…] ranges the way stripOverrideAttributeFromInterceptedMethods() already tracks attribute-group extents). Class-level attributes should remain on the woven trait untouched (attributes are legal on traits) — or be dropped there and preserved on the proxy, mirroring the readonly handling; preserving them on the trait is the smaller change.
Add golden fixtures: attribute-bearing class with and without arguments.
Environment
- goaop/framework master (4.0-dev), nikic/php-parser 5.8.0, goaop/parser-reflection 4.0.0
- PHP 8.4 / 8.5.10 / 8.6.0beta2 — identical failure
Summary
Weaving any class that carries a class-level attribute produces a corrupted trait:
WeavingTransformer::convertClassToTrait()mangles the attribute tokens and drops theclasskeyword entirely, so the woven file no longer parses.Found during the PHP 8.5 support audit (PR #597). Reproduces identically on PHP 8.4, 8.5.10 and 8.6.0beta2.
Reproduction
Woven output (method advice on
run):#[Php80ClassAttrPlain__AopProxied { public function run(): int ...With attribute arguments it gets worse —
#[\Attribute(\Attribute::TARGET_ALL)]becomes#[\Attribute(\Attribute::Php80ClassAttrPlain__AopProxied(the firstT_STRINGinside the attribute group is renamed to the trait name).Fixture:
tests/Fixtures/audit/src/Php80ClassAttrPlain.php, harnesstests/Instrument/Transformer/Php85AuditScratchTest.phpon the audit branch.Root cause
src/Instrument/Transformer/WeavingTransformer.php—convertClassToTrait()(~line 237) starts its token scan from the class node's start position. In nikic/php-parser, aClassLikenode'sstartTokenPosincludes its attribute groups, so the scan that expectsfinal|abstract|readonlymodifiers followed byT_CLASSand the class name instead lands inside the#[...]group: the firstT_STRINGthere gets renamed to<Class>__AopProxied, and the delete-until-{step then eats the rest of the attribute plus the realclass <Name>header.Note the enum path (
convertEnumToTrait()) is not affected — the existing#[Loggable] enum BackedEnumfunctional fixture weaves fine — which is why this was never caught: no existing woven-class fixture has a class-level attribute.Proposed fix
In
convertClassToTrait(), begin the token scan after the class node's attribute groups — e.g. use the position after the lastAttributeGroupsub-node (or scan forward for the actualT_CLASStoken at depth 0, skippingT_ATTRIBUTE…]ranges the waystripOverrideAttributeFromInterceptedMethods()already tracks attribute-group extents). Class-level attributes should remain on the woven trait untouched (attributes are legal on traits) — or be dropped there and preserved on the proxy, mirroring thereadonlyhandling; preserving them on the trait is the smaller change.Add golden fixtures: attribute-bearing class with and without arguments.
Environment