From cd37d26e642e9eeb4d565fa74086bf541dbc2f21 Mon Sep 17 00:00:00 2001 From: tianfenghan Date: Tue, 23 Jun 2026 20:02:23 +0800 Subject: [PATCH] =?UTF-8?q?feat(generator):=20=E6=B7=BB=E5=8A=A0=E5=8C=BF?= =?UTF-8?q?=E5=90=8D=E7=B1=BB=E7=94=9F=E6=88=90=E5=99=A8=E6=94=AF=E6=8C=81?= =?UTF-8?q?=E5=91=BD=E5=90=8D=E7=A9=BA=E9=97=B4=E7=B1=BB=E5=9E=8B=E8=A7=A3?= =?UTF-8?q?=E6=9E=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 AnonClassGenerator trait 处理匿名类类型名称解析 - 实现匿名类内部类型引用转为全限定名称的功能 - 添加处理命名空间 use 导入的类型匹配逻辑 - 重构嵌入代码生成方法到独立 trait 中 - 添加匿名类继承和接口实现的全限定名称转换 - 新增测试用例验证匿名类在命名空间中的行为 --- src/Php/CompilerBase.php | 129 ++------------- src/Php/Generator/AnonClassGenerator.php | 199 +++++++++++++++++++++++ tests/aot/anon_class/004.phpt | 40 +++++ tests/aot/namespace/new-ns-class.phpt | 28 ++++ version.txt | 2 +- 5 files changed, 283 insertions(+), 115 deletions(-) create mode 100644 src/Php/Generator/AnonClassGenerator.php create mode 100644 tests/aot/anon_class/004.phpt create mode 100644 tests/aot/namespace/new-ns-class.phpt diff --git a/src/Php/CompilerBase.php b/src/Php/CompilerBase.php index 79c93bdd..915b723d 100644 --- a/src/Php/CompilerBase.php +++ b/src/Php/CompilerBase.php @@ -23,6 +23,7 @@ use PhpAot\Php\Exception\PlaceHolder; use PhpAot\Php\Exception\Redo; use PhpAot\Php\Exception\Skip; use PhpAot\Php\Exception\TestError; +use PhpAot\Php\Generator\AnonClassGenerator; use PhpAot\Php\Generator\ClosureGenerator; use PhpAot\Php\Generator\PlaceHolderGenerator; use PhpAot\Php\Generator\PropertyPromotion; @@ -64,6 +65,7 @@ class CompilerBase extends \PhpAot\Core\Translator { use AstNodeType; use FuncCallOptimizer; + use AnonClassGenerator; use ClosureGenerator; use PlaceHolderGenerator; use PropertyPromotion; @@ -734,11 +736,6 @@ class CompilerBase extends \PhpAot\Core\Translator return 'tmp_var_' . $this->context->tmpVarIndex++; } - public function genAnonClassName(): string - { - return self::ANON_CLASS . $this->anonClassIndex++; - } - public function writeFile(string $file, string $content): void { $dir = dirname($file); @@ -916,6 +913,17 @@ class CompilerBase extends \PhpAot\Core\Translator } } + // Handle qualified names that exactly match a use import (e.g. the extends + // of an anonymous class may already be a qualified name like "A\B\C" when the + // use import is also "A\B\C"). + if (count($ns2) > 1) { + foreach ($this->useNamespaces as $useNamespace) { + if (strcasecmp(trim($useNamespace, '\\'), $class) === 0) { + return $class; + } + } + } + if (!$currentNamespace) { $currentNamespace = $this->namespace; } @@ -3447,7 +3455,6 @@ class CompilerBase extends \PhpAot\Core\Translator $className = $this->genAnonClassName(); $classDef->name = new Node\Identifier($className); // 继承父类和接口可能是 use 的名称,需要转换成全限定名称 - // TODO 匿名类的属性、常量、方法参数中都可能会用相对类名,都需要转为全限定名称 if ($classDef->extends !== null) { $parentClass = $this->getNamespacedClassName($classDef->extends->toString()); $classDef->extends = new Node\Name\FullyQualified($parentClass); @@ -3458,6 +3465,8 @@ class CompilerBase extends \PhpAot\Core\Translator $classDef->implements[$i] = new Node\Name\FullyQualified($ifaceName); } } + // 将匿名类内部的类型引用(方法参数、返回值、属性等)转为全限定名称 + $this->resolveAnonClassTypeNames($classDef); $this->context->beforeStmtLines[] = 'static THREAD_LOCAL bool ' . $className . '_defined = false;'; $classCode = $this->genEmbeddedCode($classDef); $this->addConstData($className . '_code', $classCode); @@ -5741,114 +5750,6 @@ class CompilerBase extends \PhpAot\Core\Translator } } - protected function genEmbeddedCode(NodeAbstract $stmt): string - { - if ($stmt instanceof Node\Stmt\Class_) { - $stmt = clone $stmt; - $shouldAddMixedReturn = fn (Node\Stmt\Class_ $class, Node\Stmt\ClassMethod $method): bool => - $this->shouldAddMixedReturnToEmbeddedClassMethod($class, $method); - $traverser = new \PhpParser\NodeTraverser(); - $traverser->addVisitor(new class($shouldAddMixedReturn) extends \PhpParser\NodeVisitorAbstract { - /** @var list */ - private array $classStack = []; - - public function __construct(private \Closure $shouldAddMixedReturn) - { - } - - public function enterNode(Node $node) - { - if ($node instanceof Node\Stmt\Class_) { - $this->classStack[] = $node; - return null; - } - - if ($node instanceof Node\Stmt\ClassMethod && $node->returnType === null) { - $class = $this->classStack[count($this->classStack) - 1] ?? null; - if ($class !== null && ($this->shouldAddMixedReturn)($class, $node)) { - $node->returnType = new Node\Identifier('mixed'); - } - } - return null; - } - - public function leaveNode(Node $node) - { - if ($node instanceof Node\Stmt\Class_) { - array_pop($this->classStack); - } - return null; - } - }); - $stmt = $traverser->traverse([$stmt])[0]; - } - return $this->printer->prettyPrint([$stmt]); - } - - protected function shouldAddMixedReturnToEmbeddedClassMethod(Node\Stmt\Class_ $class, Node\Stmt\ClassMethod $method): bool - { - $methodName = strtolower($method->name->toString()); - if ($this->isEmbeddedMagicMethodReturnSensitive($methodName)) { - return false; - } - - if (!empty($class->implements)) { - return true; - } - - return $class->extends !== null - && $this->ancestorMethodMayRequireMixedReturn($class->extends, $methodName); - } - - protected function isEmbeddedMagicMethodReturnSensitive(string $methodName): bool - { - return in_array($methodName, [ - '__construct', - '__destruct', - '__clone', - '__debuginfo', - '__isset', - '__serialize', - '__set', - '__set_state', - '__sleep', - '__tostring', - '__unserialize', - '__unset', - '__wakeup', - ], true); - } - - protected function ancestorMethodMayRequireMixedReturn(Node\Name $extends, string $methodName): bool - { - $className = ltrim($extends->toString(), '\\'); - - while ($className !== '') { - if ($this->hasClass($className)) { - $classDef = $this->getClass($className); - if ($classDef->hasMethod($methodName)) { - $functionDef = $classDef->getMethod($methodName)->functionDef; - return $functionDef !== null - && ($functionDef->returnTypeUndeclared || $functionDef->returnType === self::TYPE_VAR); - } - $className = $classDef->extends; - continue; - } - - if ($this->isInternalClass($className) || $this->isInternalInterface($className)) { - if (!Reflection::hasMethod($className, $methodName)) { - return false; - } - $returnType = Reflection::getMethodReturnType($className, $methodName); - return $returnType === null || strtolower($returnType) === 'mixed'; - } - - return true; - } - - return false; - } - protected function parseArrowFunction(Expr\ArrowFunction $expr): string { $nodeFinder = new NodeFinder(); diff --git a/src/Php/Generator/AnonClassGenerator.php b/src/Php/Generator/AnonClassGenerator.php new file mode 100644 index 00000000..43ebcfea --- /dev/null +++ b/src/Php/Generator/AnonClassGenerator.php @@ -0,0 +1,199 @@ +anonClassIndex++; + } + + /** + * Resolve all relative type names in an anonymous class to fully qualified names. + * The generated eval code runs without use imports, so all type references must be FQN. + */ + protected function resolveAnonClassTypeNames(Class_ $classDef): void + { + foreach ($classDef->stmts as $stmt) { + if ($stmt instanceof ClassMethod) { + foreach ($stmt->params as $param) { + if ($param->type !== null) { + $param->type = $this->resolveTypeNode($param->type); + } + } + if ($stmt->returnType !== null) { + $stmt->returnType = $this->resolveTypeNode($stmt->returnType); + } + } elseif ($stmt instanceof Property) { + if ($stmt->type !== null) { + $stmt->type = $this->resolveTypeNode($stmt->type); + } + } elseif ($stmt instanceof ClassConst) { + if ($stmt->type !== null) { + $stmt->type = $this->resolveTypeNode($stmt->type); + } + } + } + } + + /** + * Resolve a single type node, converting relative Name to FullyQualified. + */ + protected function resolveTypeNode(Node $type): Node + { + if ($type instanceof Name) { + if ($type->isFullyQualified()) { + return $type; + } + $resolved = $this->getNamespacedClassName($type->toString()); + return new Name\FullyQualified($resolved); + } + if ($type instanceof NullableType) { + $type->type = $this->resolveTypeNode($type->type); + return $type; + } + if ($type instanceof UnionType) { + foreach ($type->types as $i => $subType) { + $type->types[$i] = $this->resolveTypeNode($subType); + } + return $type; + } + if ($type instanceof IntersectionType) { + foreach ($type->types as $i => $subType) { + $type->types[$i] = $this->resolveTypeNode($subType); + } + return $type; + } + return $type; + } + + protected function genEmbeddedCode(NodeAbstract $stmt): string + { + if ($stmt instanceof Class_) { + $stmt = clone $stmt; + $shouldAddMixedReturn = fn (Class_ $class, ClassMethod $method): bool => + $this->shouldAddMixedReturnToEmbeddedClassMethod($class, $method); + $traverser = new NodeTraverser(); + $traverser->addVisitor(new class($shouldAddMixedReturn) extends NodeVisitorAbstract { + /** @var list */ + private array $classStack = []; + + public function __construct(private \Closure $shouldAddMixedReturn) + { + } + + public function enterNode(Node $node) + { + if ($node instanceof Class_) { + $this->classStack[] = $node; + return null; + } + + if ($node instanceof ClassMethod && $node->returnType === null) { + $class = $this->classStack[count($this->classStack) - 1] ?? null; + if ($class !== null && ($this->shouldAddMixedReturn)($class, $node)) { + $node->returnType = new Identifier('mixed'); + } + } + return null; + } + + public function leaveNode(Node $node) + { + if ($node instanceof Class_) { + array_pop($this->classStack); + } + return null; + } + }); + $stmt = $traverser->traverse([$stmt])[0]; + } + return $this->printer->prettyPrint([$stmt]); + } + + protected function shouldAddMixedReturnToEmbeddedClassMethod(Class_ $class, ClassMethod $method): bool + { + $methodName = strtolower($method->name->toString()); + if ($this->isEmbeddedMagicMethodReturnSensitive($methodName)) { + return false; + } + + if (!empty($class->implements)) { + return true; + } + + return $class->extends !== null + && $this->ancestorMethodMayRequireMixedReturn($class->extends, $methodName); + } + + protected function isEmbeddedMagicMethodReturnSensitive(string $methodName): bool + { + return in_array($methodName, [ + '__construct', + '__destruct', + '__clone', + '__debuginfo', + '__isset', + '__serialize', + '__set', + '__set_state', + '__sleep', + '__tostring', + '__unserialize', + '__unset', + '__wakeup', + ], true); + } + + protected function ancestorMethodMayRequireMixedReturn(Name $extends, string $methodName): bool + { + $className = ltrim($extends->toString(), '\\'); + + while ($className !== '') { + if ($this->hasClass($className)) { + $classDef = $this->getClass($className); + if ($classDef->hasMethod($methodName)) { + $functionDef = $classDef->getMethod($methodName)->functionDef; + return $functionDef !== null + && ($functionDef->returnTypeUndeclared || $functionDef->returnType === self::TYPE_VAR); + } + $className = $classDef->extends; + continue; + } + + if ($this->isInternalClass($className) || $this->isInternalInterface($className)) { + if (!Reflection::hasMethod($className, $methodName)) { + return false; + } + $returnType = Reflection::getMethodReturnType($className, $methodName); + return $returnType === null || strtolower($returnType) === 'mixed'; + } + + return true; + } + + return false; + } +} diff --git a/tests/aot/anon_class/004.phpt b/tests/aot/anon_class/004.phpt new file mode 100644 index 00000000..2ce827a2 --- /dev/null +++ b/tests/aot/anon_class/004.phpt @@ -0,0 +1,40 @@ +--TEST-- +Anonymous class inside namespace +--FILE-- +name = "World"; + $obj = new class() extends VisitorAbstract { + public function test(Node $node): string { + return 'Hello ' . $node->name . " !"; + } + }; + var_dump($obj->test($node)); + } +} + +namespace { + function main() { + Bar\App\test(); + echo "done\n"; + } +} +?> +--EXPECT-- +string(13) "Hello World !" +done diff --git a/tests/aot/namespace/new-ns-class.phpt b/tests/aot/namespace/new-ns-class.phpt new file mode 100644 index 00000000..aa264e15 --- /dev/null +++ b/tests/aot/namespace/new-ns-class.phpt @@ -0,0 +1,28 @@ +--TEST-- +Namespace with constants defined via const keyword +--FILE-- +print(); + } +} + +namespace { + function main() { + Bar\App\test(); + } +} +?> +--EXPECT-- +Hello World! \ No newline at end of file diff --git a/version.txt b/version.txt index 48c6edb2..7d3a042f 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -1069 +1070