Skip to content

fix(attribute): process Attribute nodes in leaveNode for cross-file enum resolution - #13

Closed
yuan-dian wants to merge 1 commit into
swoole:masterfrom
yuan-dian:fix/enum-attr-crossfile-leave-node
Closed

fix(attribute): process Attribute nodes in leaveNode for cross-file enum resolution#13
yuan-dian wants to merge 1 commit into
swoole:masterfrom
yuan-dian:fix/enum-attr-crossfile-leave-node

Conversation

@yuan-dian

Copy link
Copy Markdown
Contributor

Bug

When an enum from a different file is used as an attribute argument (e.g. #[TableId(IdType::AUTO)]), the RuntimeAttributeFactoryLowering pass fails to detect the enum case and does not mark the factory as lazy. The PHPX bridge then stores the raw string backing value instead of calling the factory function, causing a TypeError at runtime.

TypeError: The parameter 'object' must be 'object', got 'string'

Root Cause

RuntimeAttributeFactoryLowering processed Attribute nodes in enterNode(). At that point, NodeTraverser has not yet descended into the Attribute's argument expressions, so NameResolver hasn't resolved Name('IdType')Name\FullyQualified('App\Enums\IdType').

The fallback in resolveClassConstFetchClass() uses $this->namespace . '\\' . $name, producing the wrong namespace (e.g. App\BenchModels\IdType), so isDeclaredEnumCase() returns false and the factory is not marked as lazy.

Traversal Order Issue

NodeTraverser::traverse()
  ├─ enterNode(Attribute)          ← RAF runs here, sees Name('IdType') UNRESOLVED
  │   └─ foreach args
  │       └─ requiresFactory() → true
  │       └─ isEnumCaseFetch() → false (wrong namespace)
  │       └─ factory created but NOT marked lazy
  └─ traverse children
      └─ enterNode(Name('IdType')) ← NameResolver resolves here, TOO LATE
          └─ replaced with FullyQualified('App\Enums\IdType')

Fix

Move Attribute argument processing from enterNode() to leaveNode(). In leaveNode(), all child nodes have been traversed and NameResolver has replaced Name nodes with Name\FullyQualified nodes containing correct resolvedName attributes, allowing isEnumCaseFetch() to correctly identify enum cases across files.

Files Changed

src/Transform/RuntimeAttributeFactoryLowering.php — 29 lines added, 27 removed (net +2)

Before (broken)

public function enterNode(Node $node) {
    // ...
    } elseif ($node instanceof Node\Attribute) {
        // $argument->value may contain Name('IdType') — UNRESOLVED
        // → resolveClassConstFetchClass() falls back to wrong namespace
        // → isEnumCaseFetch() returns false → factory NOT marked lazy
    }
}

After (fixed)

public function leaveNode(Node $node) {
    // ...
    } elseif ($node instanceof Node\Attribute) {
        // $argument->value now contains Name\FullyQualified('App\Enums\IdType')
        // → isEnumCaseFetch() correctly resolves cross-file enums
        // → factory marked as lazy → PHPX bridge calls factory function
    }
}

Reproduction

// Status.php — enum in separate file
namespace App;
enum Status: string { case Active = 'active'; }

// Label.php — attribute with enum default
#[Attribute(Attribute::TARGET_CLASS)]
class Label {
    public function __construct(public Status $status = Status::Active) {}
}

// User.php — uses enum attribute from different file
#[Label(Status::Active)]
class User {}
Mode Result
PHP interpreter ✅ works
Compiled (before fix) TypeError: must be 'object', got 'string'
Compiled (after fix) ✅ works

Move RuntimeAttributeFactoryLowering's Attribute argument processing
from enterNode to leaveNode. In enterNode, NameResolver has not yet
resolved Name nodes in attribute argument expressions, causing
resolveClassConstFetchClass() to fall back to incorrect namespace
resolution (e.g. 'App\BenchModels\IdType' instead of
'App\Enums\IdType').

This breaks enum case detection for cross-file enum attributes,
resulting in the factory being created but not marked as lazy,
which causes the PHPX bridge to store raw string values instead
of calling the factory function.

In leaveNode, all child nodes have been traversed and NameResolver
has replaced Name nodes with Name\FullyQualified nodes containing
correct resolvedName attributes, allowing isEnumCaseFetch() to
correctly identify enum cases across files.
@matyhtf matyhtf closed this Aug 28, 2026
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.

2 participants