From 7f0a8181f5037fb5d8f33ac0e50cbee93a6049ba Mon Sep 17 00:00:00 2001 From: tianfenghan Date: Thu, 27 Aug 2026 19:56:05 +0800 Subject: [PATCH] fix(attribute): preserve enum case arguments --- src/Context/CompilationStateTrait.php | 8 ++ src/Preprocessor.php | 5 +- .../RuntimeAttributeFactoryLowering.php | 74 ++++++++++++++++++- src/Translator.php | 5 +- src/gen_stub.php | 5 +- .../attribute/enum-default-argument.phpt | 70 ++++++++++++++++++ 6 files changed, 160 insertions(+), 7 deletions(-) create mode 100644 tests/compiler/attribute/enum-default-argument.phpt diff --git a/src/Context/CompilationStateTrait.php b/src/Context/CompilationStateTrait.php index 144316d6..dcf83c2f 100644 --- a/src/Context/CompilationStateTrait.php +++ b/src/Context/CompilationStateTrait.php @@ -192,6 +192,14 @@ trait CompilationStateTrait return $this->symbols->findClass($this->escapeClass($name)); } + public function isDeclaredEnumCase(string $class, string $case): bool + { + $classDef = $this->getClassDef(ltrim($class, '\\')); + return $classDef !== null + && $classDef->enum + && array_key_exists($case, $classDef->enumCases); + } + public function getParentClass(string $class): string { return $this->symbols->parent(strtolower(ltrim($class, '\\'))); diff --git a/src/Preprocessor.php b/src/Preprocessor.php index a178df48..a108febe 100644 --- a/src/Preprocessor.php +++ b/src/Preprocessor.php @@ -303,7 +303,10 @@ class Preprocessor extends CompilerBase $this->file, )); $traverser->addVisitor(new ConstantExpressionValidationVisitor($this->phpVersion)); - $traverser->addVisitor(new RuntimeAttributeFactoryLowering($this->file)); + $traverser->addVisitor(new RuntimeAttributeFactoryLowering( + $this->file, + fn (string $class, string $case): bool => $this->isDeclaredEnumCase($class, $case), + )); $stmts = $this->requireStatementList($traverser->traverse($ast)); // Keep the resolved declaration AST until convert. Defaults and // constants are validated here, but their C++ expressions are not diff --git a/src/Transform/RuntimeAttributeFactoryLowering.php b/src/Transform/RuntimeAttributeFactoryLowering.php index 017d25de..ae8b15a4 100644 --- a/src/Transform/RuntimeAttributeFactoryLowering.php +++ b/src/Transform/RuntimeAttributeFactoryLowering.php @@ -8,6 +8,7 @@ namespace TypePhp\Transform; +use Closure; use PhpParser\Node; use PhpParser\Node\Expr; use PhpParser\Node\Stmt; @@ -37,9 +38,14 @@ final class RuntimeAttributeFactoryLowering extends NodeVisitorAbstract private array $namespaceFactories = []; private string $namespace = ''; private int $sequence = 0; + /** @var array */ + private array $declaredEnumCases = []; - public function __construct(private readonly string $sourceFile = '') - { + /** @param null|Closure(string, string): bool $enumCaseResolver */ + public function __construct( + private readonly string $sourceFile = '', + private readonly ?Closure $enumCaseResolver = null, + ) { } public function enterNode(Node $node): null @@ -53,12 +59,22 @@ final class RuntimeAttributeFactoryLowering extends NodeVisitorAbstract if ($node instanceof Stmt\ClassLike) { $class = $node->getAttribute('namespacedName'); $parent = $node instanceof Stmt\Class_ ? $node->extends : null; - $this->classStack[] = [ + $context = [ 'namespace' => $class instanceof Node\Name ? $class->toString() : ltrim($this->namespace . '\\' . ($node->name?->toString() ?? ''), '\\'), 'parent' => $parent?->toString() ?? '', ]; + $this->classStack[] = $context; + if ($node instanceof Stmt\Enum_) { + foreach ($node->stmts as $statement) { + if ($statement instanceof Stmt\EnumCase) { + $this->declaredEnumCases[ + strtolower($context['namespace']) . '::' . $statement->name->toString() + ] = true; + } + } + } return null; } @@ -123,19 +139,69 @@ final class RuntimeAttributeFactoryLowering extends NodeVisitorAbstract if ($value instanceof Expr\Array_ && $value->items !== []) { return true; } + if ($value instanceof Expr\ClassConstFetch && $this->isEnumCaseFetch($value)) { + return true; + } - return (new NodeFinder())->findFirst($value, static function (Node $node): bool { + return (new NodeFinder())->findFirst($value, function (Node $node): bool { return $node instanceof Expr\New_ || $node instanceof Expr\Closure // A PHP 8.5 array cast may produce a non-empty array even // though it is not represented by an Array_ AST node. || $node instanceof Expr\Cast\Array_ || $node instanceof Expr\Cast\Object_ + || ($node instanceof Expr\ClassConstFetch && $this->isEnumCaseFetch($node)) || (($node instanceof Expr\FuncCall || $node instanceof Expr\StaticCall) && $node->isFirstClassCallable()); }) !== null; } + private function isEnumCaseFetch(Expr\ClassConstFetch $fetch): bool + { + if (!$fetch->name instanceof Node\Identifier) { + return false; + } + $class = $this->resolveClassConstFetchClass($fetch); + if ($class === null) { + return false; + } + $case = $fetch->name->toString(); + if (isset($this->declaredEnumCases[strtolower($class) . '::' . $case])) { + return true; + } + return $this->enumCaseResolver !== null + && ($this->enumCaseResolver)($class, $case); + } + + private function resolveClassConstFetchClass(Expr\ClassConstFetch $fetch): ?string + { + if (!$fetch->class instanceof Node\Name) { + return null; + } + + $name = $fetch->class->toString(); + $lower = strtolower($name); + if (($lower === 'self' || $lower === 'static') && $this->classStack !== []) { + return $this->classStack[array_key_last($this->classStack)]['namespace']; + } + if ($lower === 'parent' && $this->classStack !== []) { + $parent = $this->classStack[array_key_last($this->classStack)]['parent']; + return $parent === '' ? null : ltrim($parent, '\\'); + } + + $resolved = $fetch->class->getAttribute('resolvedName'); + if ($resolved instanceof Node\Name) { + return ltrim($resolved->toString(), '\\'); + } + if ($fetch->class instanceof Node\Name\FullyQualified) { + return ltrim($name, '\\'); + } + if ($fetch->class instanceof Node\Name\Relative) { + return ltrim($this->namespace . '\\' . $name, '\\'); + } + return ltrim($this->namespace . '\\' . $name, '\\'); + } + /** @return array{fullName: string, node: Stmt\Function_} */ private function createFactory(Expr $value): array { diff --git a/src/Translator.php b/src/Translator.php index 2e70a129..17f830b6 100644 --- a/src/Translator.php +++ b/src/Translator.php @@ -2781,7 +2781,10 @@ CODE; $this->phpVersion, fn (Node $node, string $message) => $this->fatalError($node, $message), )); - $traverser->addVisitor(new RuntimeAttributeFactoryLowering($this->file)); + $traverser->addVisitor(new RuntimeAttributeFactoryLowering( + $this->file, + fn (string $class, string $case): bool => $this->isDeclaredEnumCase($class, $case), + )); $stmts = $traverser->traverse($ast); diff --git a/src/gen_stub.php b/src/gen_stub.php index 167f797b..d559806c 100755 --- a/src/gen_stub.php +++ b/src/gen_stub.php @@ -4842,7 +4842,10 @@ class FileInfo { )); $nodeTraverser->addVisitor(new TypePhp\Transform\Visitor(sourceFile: $sourceFile)); $nodeTraverser->addVisitor(new TypePhp\Transform\ConstantExpressionValidationVisitor($phpVersion)); - $nodeTraverser->addVisitor(new TypePhp\Transform\RuntimeAttributeFactoryLowering($sourceFile)); + $nodeTraverser->addVisitor(new TypePhp\Transform\RuntimeAttributeFactoryLowering( + $sourceFile, + static fn (string $class, string $case): bool => getTranslator()->isDeclaredEnumCase($class, $case), + )); $prettyPrinter = new class extends Standard { protected function pName_FullyQualified(PhpParser\Node\Name\FullyQualified $node): string { return implode('\\', $node->getParts()); diff --git a/tests/compiler/attribute/enum-default-argument.phpt b/tests/compiler/attribute/enum-default-argument.phpt new file mode 100644 index 00000000..78364a21 --- /dev/null +++ b/tests/compiler/attribute/enum-default-argument.phpt @@ -0,0 +1,70 @@ +--TEST-- +Attribute constructor defaults and arguments preserve backed enum cases +--FILE-- +getAttributes(ValidateStatus::class)[0]; + $validation = $attribute->newInstance(); + + var_dump($attribute->getArguments()); + var_dump($validation->status === Status::Active); + var_dump($validation->status->name); + var_dump($validation->status->value); + } +} +?> +--EXPECT-- +array(0) { +} +bool(true) +string(6) "Active" +string(6) "active" +array(1) { + [0]=> + enum(Status::Active) +} +bool(true) +string(6) "Active" +string(6) "active" +array(1) { + [0]=> + enum(Status::Active) +} +bool(true) +string(6) "Active" +string(6) "active"