feat(generator): 添加匿名类生成器支持命名空间类型解析

- 新增 AnonClassGenerator trait 处理匿名类类型名称解析
- 实现匿名类内部类型引用转为全限定名称的功能
- 添加处理命名空间 use 导入的类型匹配逻辑
- 重构嵌入代码生成方法到独立 trait 中
- 添加匿名类继承和接口实现的全限定名称转换
- 新增测试用例验证匿名类在命名空间中的行为
pull/3/head
韩天峰 2 months ago
parent 601b985600
commit cd37d26e64
  1. 129
      src/Php/CompilerBase.php
  2. 199
      src/Php/Generator/AnonClassGenerator.php
  3. 40
      tests/aot/anon_class/004.phpt
  4. 28
      tests/aot/namespace/new-ns-class.phpt
  5. 2
      version.txt

@ -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<Node\Stmt\Class_> */
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();

@ -0,0 +1,199 @@
<?php
/**
* This file is part of Swoole-Compiler(AOT).
*
* @link https://www.swoole.com/
* @contact service@swoole.com
*/
namespace PhpAot\Php\Generator;
use PhpParser\Node;
use PhpParser\NodeAbstract;
use PhpParser\Node\Identifier;
use PhpParser\Node\IntersectionType;
use PhpParser\Node\Name;
use PhpParser\Node\NullableType;
use PhpParser\Node\UnionType;
use PhpParser\Node\Stmt\Class_;
use PhpParser\Node\Stmt\ClassConst;
use PhpParser\Node\Stmt\ClassMethod;
use PhpParser\Node\Stmt\Property;
use PhpParser\NodeTraverser;
use PhpParser\NodeVisitorAbstract;
use PhpAot\Php\Reflection;
trait AnonClassGenerator
{
public function genAnonClassName(): string
{
return self::ANON_CLASS . $this->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<Class_> */
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;
}
}

@ -0,0 +1,40 @@
--TEST--
Anonymous class inside namespace
--FILE--
<?php
namespace Foo\App {
class Node {
public string $name;
}
abstract class VisitorAbstract {
abstract public function test(Node $node): string;
}
}
namespace Bar\App {
use Foo\App\Node;
use Foo\App\VisitorAbstract;
function test() {
$node = new Node();
$node->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

@ -0,0 +1,28 @@
--TEST--
Namespace with constants defined via const keyword
--FILE--
<?php
namespace Foo\App {
class Printer {
public function print() {
echo "Hello World!\n";
}
}
}
namespace Bar\App {
use Foo\App;
function test(){
$o = new App\Printer;
$o->print();
}
}
namespace {
function main() {
Bar\App\test();
}
}
?>
--EXPECT--
Hello World!

@ -1 +1 @@
1069
1070

Loading…
Cancel
Save