From 13df538b789ed45e4db6e11f792c4cade6874307 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=8E=9F=E7=82=B9?= <467490186@qq.com> Date: Fri, 28 Aug 2026 16:49:41 +0800 Subject: [PATCH] fix(attribute): process Attribute nodes in leaveNode for name resolution 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. --- .../RuntimeAttributeFactoryLowering.php | 56 ++++++++++--------- 1 file changed, 29 insertions(+), 27 deletions(-) diff --git a/src/Transform/RuntimeAttributeFactoryLowering.php b/src/Transform/RuntimeAttributeFactoryLowering.php index ae8b15a4..0f799eba 100644 --- a/src/Transform/RuntimeAttributeFactoryLowering.php +++ b/src/Transform/RuntimeAttributeFactoryLowering.php @@ -78,30 +78,6 @@ public function enterNode(Node $node): null return null; } - if (!$node instanceof Node\Attribute) { - return null; - } - if (CompileTimeAttributeRegistry::get($node->name->toString()) !== null) { - return null; - } - - foreach ($node->args as $argument) { - if (!$this->requiresFactory($argument->value)) { - continue; - } - - $factory = $this->createFactory($argument->value); - $argument->value->setAttribute(self::FACTORY_NAME_ATTRIBUTE, $factory['fullName']); - if ($this->requiresLazyValue($argument->value)) { - $argument->value->setAttribute(self::FACTORY_LAZY_VALUE_ATTRIBUTE, true); - } - if ($this->namespaceFactories !== []) { - $index = array_key_last($this->namespaceFactories); - $this->namespaceFactories[$index][] = $factory['node']; - } else { - $this->globalFactories[] = $factory['node']; - } - } return null; } @@ -115,6 +91,33 @@ public function leaveNode(Node $node): null array_push($node->stmts, ...$factories); } $this->namespace = ''; + } elseif ($node instanceof Node\Attribute) { + // Process attribute args in leaveNode so that NameResolver has + // already resolved all Name nodes in the argument expressions. + // Using enterNode would see unresolved Name('IdType') instead of + // Name\FullyQualified('App\\Enums\\IdType'), causing enum case + // detection to fail for cross-file enums. + if (CompileTimeAttributeRegistry::get($node->name->toString()) !== null) { + return null; + } + + foreach ($node->args as $argument) { + if (!$this->requiresFactory($argument->value)) { + continue; + } + + $factory = $this->createFactory($argument->value); + $argument->value->setAttribute(self::FACTORY_NAME_ATTRIBUTE, $factory['fullName']); + if ($this->requiresLazyValue($argument->value)) { + $argument->value->setAttribute(self::FACTORY_LAZY_VALUE_ATTRIBUTE, true); + } + if ($this->namespaceFactories !== []) { + $index = array_key_last($this->namespaceFactories); + $this->namespaceFactories[$index][] = $factory['node']; + } else { + $this->globalFactories[] = $factory['node']; + } + } } return null; } @@ -271,9 +274,8 @@ public function enterNode(Node $node): ?Node } } if ($node instanceof Node\Name) { - // Attribute factories are created while the outer - // traverser is entering the Attribute node, before its - // argument names have been visited by NameResolver. + // Attribute factories are created in leaveNode after + // NameResolver has resolved all Name nodes. if ($node instanceof Node\Name\Relative) { $name = ltrim($this->namespace . '\\' . $node->toString(), '\\'); return new Node\Name\FullyQualified($name, $node->getAttributes());