Skip to content

Class-level attributes break class→trait conversion (woven trait has parse error) #598

Description

@lisachenko

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.phpconvertClassToTrait() (~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

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions