- 新增 AnonClassGenerator trait 处理匿名类类型名称解析 - 实现匿名类内部类型引用转为全限定名称的功能 - 添加处理命名空间 use 导入的类型匹配逻辑 - 重构嵌入代码生成方法到独立 trait 中 - 添加匿名类继承和接口实现的全限定名称转换 - 新增测试用例验证匿名类在命名空间中的行为pull/3/head
parent
601b985600
commit
cd37d26e64
5 changed files with 283 additions and 115 deletions
@ -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…
Reference in new issue