fix(attribute): preserve enum case arguments

master
韩天峰 20 hours ago
parent fb79fb832c
commit 7f0a8181f5
  1. 8
      src/Context/CompilationStateTrait.php
  2. 5
      src/Preprocessor.php
  3. 74
      src/Transform/RuntimeAttributeFactoryLowering.php
  4. 5
      src/Translator.php
  5. 5
      src/gen_stub.php
  6. 70
      tests/compiler/attribute/enum-default-argument.phpt

@ -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, '\\')));

@ -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

@ -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<string, true> */
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
{

@ -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);

@ -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());

@ -0,0 +1,70 @@
--TEST--
Attribute constructor defaults and arguments preserve backed enum cases
--FILE--
<?php
enum Status: string
{
case Active = 'active';
}
#[Attribute(Attribute::TARGET_CLASS)]
class ValidateStatus
{
public function __construct(public Status $status = Status::Active)
{
}
}
#[ValidateStatus]
class ActiveResource
{
}
#[ValidateStatus(Status::Active)]
class ExplicitActiveResource
{
}
#[ValidateStatus(true ? Status::Active : Status::Active)]
class ConditionalActiveResource
{
}
function main(): void
{
foreach ([
ActiveResource::class,
ExplicitActiveResource::class,
ConditionalActiveResource::class,
] as $class) {
$attribute = (new ReflectionClass($class))->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"
Loading…
Cancel
Save