feat(compiler): 完善类型声明和对象赋值检查功能

- 将 getNamespacedClassName 方法设为公共方法以便外部调用
- 添加对 self、parent、static 关键字的类型解析支持
- 优化 Name_FullyQualified 节点的解析逻辑
- 实现 detectClassOfExpr 方法用于检测表达式的类类型
- 增强对象赋值时的类型检查和错误提示
- 完善返回值类型的验证机制
- 重构类型检测相关方法的命名和实现
- 添加对 trait 语句的解析支持
- 优化构建模式设置功能
- 增加类型声明相关的测试用例
pull/1/head
韩天峰 4 months ago
parent af0e5df4ae
commit e62c953c71
  1. 200
      src/Php/CompilerBase.php
  2. 4
      src/Php/Entity/FunctionDef.php
  3. 6
      src/Php/Preprocessor.php
  4. 14
      src/Php/Translator.php
  5. 4
      src/gen_stub.php
  6. 37
      tests/aot/type_decl/006.phpt

@ -649,7 +649,7 @@ class CompilerBase extends \PhpAot\Core\Translator
return strtolower($fullClassName . '::' . $method);
}
protected function getNamespacedClassName(string $class): string
public function getNamespacedClassName(string $class): string
{
if ($class === '') {
$this->error('Class name can not be empty');
@ -773,6 +773,9 @@ class CompilerBase extends \PhpAot\Core\Translator
return 'php_get_prop(' . $funcId . ', ' . $this->getLiteralString($prop) . ', ' . $classId . ', ' . $this->getLiteralString($class) . ')';
}
/**
* @param string $class 一定是带有命名空间的完整类名
*/
protected function parseTypeDecl(?NodeAbstract $type, int $what, string &$class): string
{
// 未定义类型视为 var (mixed, any)
@ -790,7 +793,16 @@ class CompilerBase extends \PhpAot\Core\Translator
} elseif (isset($this->zendTypeMap[$typeName])) {
return $this->getTypeFromZendType($typeName);
} else {
$class = $this->getNamespacedClassName($typeName);
if ($typeName === 'self') {
$class = $this->getFullClassName();
} elseif ($typeName === 'parent') {
$class = $this->classDef->extends;
} elseif ($typeName === 'static') {
// static 类无法在编译期获取
$class = '';
} else {
$class = $this->getNamespacedClassName($typeName);
}
return self::TYPE_OBJECT;
}
}
@ -950,8 +962,9 @@ class CompilerBase extends \PhpAot\Core\Translator
switch ($type) {
case self::EXPR_VARIABLE:
return $this->parseVariable($expr);
case 'Name':
case 'Name_FullyQualified':
return '\\' . $expr->name;
case 'Name':
case 'VarLikeIdentifier':
case 'Identifier':
return $expr->name;
@ -1252,74 +1265,71 @@ class CompilerBase extends \PhpAot\Core\Translator
}
$expr = $this->parseExpr($right);
$type = $this->detectExprType($right);
$type = $this->detectTypeOfExpr($right);
if ($this->isVarExpr($left)) {
// 类型推断,获取对象的类名
if ($this->isNewExpr($right) and $this->isNameExpr($right->class)) {
$class = $this->parseIdentifier($right->class);
$fullClass = $this->getNamespacedClassName($class);
if ($this->isTypedObject($var)) {
// 类型推断,获取对象的类名,如果不是对象则返回空字符串
$rightClass = $this->detectClassOfExpr($right);
// 右值是一个对象,已获得类的名称,左值必须与右值的类一致
if ($rightClass) {
if (!$this->hasVar($var)) {
$this->addLocalVar($var, self::TYPE_OBJECT);
$this->addObject($var, $rightClass);
} elseif ($this->isTypedObject($var)) {
$leftClass = $this->getObjectType($var);
if ($leftClass !== $fullClass) {
$this->fatalError($left, "Cannot re-assign typed object `\${$var}` from `{$leftClass}` to `{$fullClass}`");
// 对象的类不一致,不能互相赋值
if ($leftClass !== $rightClass) {
$this->fatalError($left, "Cannot re-assign typed object `\${$var}` from `{$leftClass}` to `{$rightClass}`");
}
}
$this->addObject($var, $fullClass);
$type = self::TYPE_OBJECT;
} elseif ($this->isFuncCallExpr($right) and $this->isNameExpr($right->name)) {
$fn = $this->parseIdentifier($right->name);
if (count($right->args) === 2 and $fn === 'objval' and $this->isScalarString($right->args[1]->value)) {
$fullClass = $this->getNamespacedClassName($this->parseIdentifier($right->args[1]->value));
if ($this->isTypedObject($var)) {
$leftClass = $this->getObjectType($var);
$this->fatalError($left, "Cannot re-assign typed object `\${$var}` from `{$leftClass}` to `{$fullClass}`");
}
$this->addObject($var, $fullClass);
$type = self::TYPE_OBJECT;
} elseif (count($right->args) === 1 and $fn === 'any') {
$type = self::TYPE_VAR;
if (!$this->hasVar($var)) {
$this->addLocalVar($var, $type);
}
return $var . ' = ' . $this->parseIdentifier($right->args[0]->value);
} else {
$type = $type === self::TYPE_VOID ? self::TYPE_VAR : $type;
$this->fatalError($left, "Cannot re-assign `\${$var}` to object of `{$rightClass}`");
}
} elseif ($this->isStaticCall($right) and $this->isIdExpr($right->name)) {
$class = $this->parseIdentifier($right->class);
if ($class === 'std') {
$func = $this->parseIdentifier($right->name);
$type = match ($func) {
'int' => self::TYPE_INT,
'float' => self::TYPE_FLOAT,
'bool' => self::TYPE_BOOL,
default => '',
};
// Native 类型
if ($type) {
} else {
if ($this->isFuncCallExpr($right) and $this->isNameExpr($right->name)) {
$fn = $this->parseIdentifier($right->name);
if (count($right->args) === 1 and $fn === 'any') {
$type = self::TYPE_VAR;
if (!$this->hasVar($var)) {
$this->addLocalVar($var, $type);
}
$expr = $this->parseExpr($right->args[0]->value);
return $var . ' = ' . $this->convertExprFromType($type, $expr);
return $var . ' = ' . $this->parseIdentifier($right->args[0]->value);
} else {
$type = $type === self::TYPE_VOID ? self::TYPE_VAR : $type;
}
} elseif ($this->isStaticCall($right) and $this->isIdExpr($right->name)) {
$class = $this->parseIdentifier($right->class);
if ($class === 'std') {
$func = $this->parseIdentifier($right->name);
$type = match ($func) {
'int' => self::TYPE_INT,
'float' => self::TYPE_FLOAT,
'bool' => self::TYPE_BOOL,
default => '',
};
// Native 类型
if ($type) {
if (!$this->hasVar($var)) {
$this->addLocalVar($var, $type);
}
$expr = $this->parseExpr($right->args[0]->value);
return $var . ' = ' . $this->convertExprFromType($type, $expr);
}
}
} elseif ($this->isVarExpr($right)) {
$rightVar = $this->parseIdentifier($right);
$type = $this->getVarType($rightVar);
if ($this->isTypedObject($rightVar) and $this->isTypedObject($var)) {
$leftClass = $this->getObjectType($var);
$rightClass = $this->getObjectType($rightVar);
$this->fatalError($left, "Cannot re-assign typed object `\${$var}` from `{$leftClass}` to `{$rightClass}`");
}
}
} elseif ($this->isVarExpr($right)) {
$rightVar = $this->parseIdentifier($right);
$type = $this->getVarType($rightVar);
if ($this->isTypedObject($rightVar) and $this->isTypedObject($var)) {
$leftClass = $this->getObjectType($var);
$rightClass = $this->getObjectType($rightVar);
$this->fatalError($left, "Cannot re-assign typed object `\${$var}` from `{$leftClass}` to `{$rightClass}`");
if (!$this->hasVar($var)) {
$this->addLocalVar($var, $type);
} elseif ($this->getVarType($var) !== self::TYPE_VAR and $this->isTypedObject($var) and $type !== self::TYPE_OBJECT) {
$this->fatalError($left, "Cannot re-assign variable `\${$var}` from " . $this->getVarType($var) . ' to ' . $type);
}
}
if (!$this->hasVar($var)) {
$this->addLocalVar($var, $type);
} elseif ($this->getVarType($var) !== self::TYPE_VAR and $this->isTypedObject($var) and $type !== self::TYPE_OBJECT) {
$this->fatalError($left, "Cannot re-assign variable `\${$var}` from " . $this->getVarType($var) . ' to ' . $type);
}
} elseif ($this->isPropertyFetch($left) and !$left->getAttribute('nativeProperty')) {
return $this->parseAssignPropertyFetch($left, $right);
} elseif ($this->isArrayDimFetch($left) and $this->isVarExpr($left->var)) {
@ -1330,7 +1340,7 @@ class CompilerBase extends \PhpAot\Core\Translator
return $this->parseAssignArrayDim($left, $right);
}
return $var . ' = ' . $this->convertExprType($expr, $this->detectExprType($left), $this->detectExprType($right));
return $var . ' = ' . $this->convertExprType($expr, $this->detectTypeOfExpr($left), $this->detectTypeOfExpr($right));
}
protected function parseEcho(mixed $v): string
@ -1431,8 +1441,8 @@ class CompilerBase extends \PhpAot\Core\Translator
$this->checkVarMustExist($left, $leftExpr);
$this->checkVarMustExist($right, $rightExpr);
$leftType = $this->detectExprType($left);
$rightType = $this->detectExprType($right);
$leftType = $this->detectTypeOfExpr($left);
$rightType = $this->detectTypeOfExpr($right);
if ($leftType === self::TYPE_FLOAT) {
$rightExpr = $this->convertExprType($rightExpr, self::TYPE_FLOAT, $rightType);
@ -1456,6 +1466,38 @@ class CompilerBase extends \PhpAot\Core\Translator
return $this->parseBinaryOp($expr->left, $expr->right, '+');
}
protected function detectClassOfExpr(NodeAbstract $expr): string
{
if ($this->isNewExpr($expr) and $this->isNameExpr($expr->class)) {
$class = $this->parseIdentifier($expr->class);
if ($class === 'self') {
return $this->getFullClassName();
}
if ($class === 'static') {
// 无法在编译期获得 static 类的准确类名
return '';
} else {
return $this->getNamespacedClassName($class);
}
}
if ($this->isVarExpr($expr)) {
$object = $this->parseVariable($expr);
if ($object === 'this_') {
return $this->getFullClassName();
}
if ($this->isTypedObject($object)) {
return $this->getObjectType($object);
}
}
if ($this->isFuncCallExpr($expr) and $this->isNameExpr($expr->name)) {
$fn = $this->parseIdentifier($expr->name);
if (count($expr->args) === 2 and $fn === 'objval' and $this->isScalarString($expr->args[1]->value)) {
return $this->getNamespacedClassName($this->parseIdentifier($expr->args[1]->value));
}
}
return '';
}
protected function parseReturn(Node\Stmt\Return_ $v): string
{
if ($v->expr === null) {
@ -1466,7 +1508,7 @@ class CompilerBase extends \PhpAot\Core\Translator
}
}
// 实际函数的返回值
$type = $this->detectExprType($v->expr);
$type = $this->detectTypeOfExpr($v->expr);
$expr = $this->parseExpr($v->expr);
$returnType = $this->getReturnType();
@ -1479,6 +1521,17 @@ class CompilerBase extends \PhpAot\Core\Translator
$returnType = self::TYPE_VAR;
}
// 返回值的表达式是一个类的对象
$objectClass = $this->detectClassOfExpr($v->expr);
$returnClass = $this->getReturnClass();
if ($returnClass) {
if (!$objectClass) {
$this->fatalError($v, 'The return value must be an instance of the `' . $returnClass . '` class');
} elseif (!$this->isInheritedFrom($objectClass, $returnClass)) {
$this->fatalError($v, 'The return type is `' . $returnClass . '`, cannot return an instance of `' . $objectClass . '`');
}
}
$exprCode = $this->convertExprType($expr, $returnType, $type);
// return 如果使用了 Indirect 语句,可能会导致变量提前析构,出现悬空指针
// 将 Indirect 赋值给临时变量后,使用 Ctor::Copy 解除了 Indirect,保证内存安全
@ -1721,7 +1774,7 @@ class CompilerBase extends \PhpAot\Core\Translator
return $this->getVarType($name);
}
protected function detectExprType($expr): string
protected function detectTypeOfExpr($expr): string
{
$exprType = $expr->getType();
switch ($exprType) {
@ -1750,8 +1803,8 @@ class CompilerBase extends \PhpAot\Core\Translator
case 'Expr_BinaryOp_BitwiseOr':
case 'Expr_BinaryOp_BitwiseXor':
case 'Expr_BinaryOp_BooleanAnd':
$leftType = $this->detectExprType($expr->left);
$rightType = $this->detectExprType($expr->right);
$leftType = $this->detectTypeOfExpr($expr->left);
$rightType = $this->detectTypeOfExpr($expr->right);
if ($leftType === self::TYPE_FLOAT || $rightType === self::TYPE_FLOAT) {
return self::TYPE_FLOAT;
}
@ -2043,7 +2096,7 @@ class CompilerBase extends \PhpAot\Core\Translator
$this->fatalError($node->var, 'Cannot assign to undefined variable');
}
$type = $this->detectVarType($node->var);
$rightExprStr = $this->convertExprType($expr, $type, $this->detectExprType($node->expr));
$rightExprStr = $this->convertExprType($expr, $type, $this->detectTypeOfExpr($node->expr));
if ($this->isAssignOpConcat($op)) {
if ($this->isArrayVar($node->var)) {
$this->fatalError($node->var, 'Cannot concat string to array');
@ -2065,7 +2118,7 @@ class CompilerBase extends \PhpAot\Core\Translator
* $count[$r] = $tmp_var;.
*/
$type = $this->detectVarType($node->var);
$rightType = $this->detectExprType($node->expr);
$rightType = $this->detectTypeOfExpr($node->expr);
$tmpVar = $this->genTmpVarName();
$this->addLocalVar($tmpVar, $rightType);
$dim = $this->parseIdentifier($node->var->dim);
@ -3148,6 +3201,11 @@ class CompilerBase extends \PhpAot\Core\Translator
return $this->functionDef->returnType;
}
protected function getReturnClass(): string
{
return $this->functionDef->returnClass;
}
protected function isInheritedFrom(string $class, string $expected): bool
{
$internal = ($this->isInternalClass($expected) or $this->isInternalInterface($expected));
@ -3175,7 +3233,7 @@ class CompilerBase extends \PhpAot\Core\Translator
protected function getTypeConvertedArg(Node\Arg $arg, ArgInfo $argInfo): string
{
$expr = $this->parseArg($arg);
$type = $this->detectExprType($arg->value);
$type = $this->detectTypeOfExpr($arg->value);
if ($argInfo->byRef) {
return $this->convertToRef($arg->value);
@ -3425,7 +3483,7 @@ class CompilerBase extends \PhpAot\Core\Translator
{
$cond = $v->cond;
$tmp_var = $this->genTmpVarName();
$type = $this->detectExprType($cond);
$type = $this->detectTypeOfExpr($cond);
if ($this->isVarExpr($cond)) {
$this->requireVar($v, $this->parseIdentifier($cond));
}
@ -3525,7 +3583,7 @@ class CompilerBase extends \PhpAot\Core\Translator
$list = [];
foreach ($v->vars as $var) {
$varName = $this->escapeVarName($var->var->name);
$type = $var->default ? $this->detectExprType($var->default) : self::TYPE_VAR;
$type = $var->default ? $this->detectTypeOfExpr($var->default) : self::TYPE_VAR;
$globalVar = $this->addStaticVar($var->var, $varName, $type);
$list[] = self::TYPE_VAR . ' &' . $varName . ' = ' . $this->escapeGlobalVar($globalVar) . ';';
@ -4667,7 +4725,7 @@ class CompilerBase extends \PhpAot\Core\Translator
$this->errorUndefinedVariable($expr->expr);
}
if ($this->isVarExpr($expr->var) and !$this->hasVar($var)) {
$this->addLocalVar($var, $this->detectExprType($expr->expr));
$this->addLocalVar($var, $this->detectTypeOfExpr($expr->expr));
}
return '(' . $isset . '?' . $var . ':(' . $var . ' = ' . $right . '))';
}

@ -24,6 +24,10 @@ class FunctionDef
public string $namespace;
public bool $method = false;
public bool $stub = false;
/**
* @var string 必须是带有命名空间的完整类名
*/
public string $returnClass = '';
public function __construct(string $name, string $returnType, string $namespace)

@ -376,11 +376,7 @@ class Preprocessor extends CompilerBase
protected function getParentClass(NodeAbstract $extends): string
{
if ($extends instanceof Node\Name\FullyQualified) {
return $this->parseIdentifier($extends);
} else {
return $this->getNamespacedClassName($this->parseIdentifier($extends));
}
return $this->getNamespacedClassName($this->parseIdentifier($extends));
}
protected function prepareClass(Node\Stmt\Class_|Node\Stmt\Trait_|Node\Stmt\Enum_ $class): string

@ -128,6 +128,11 @@ class Translator extends Preprocessor
return implode(', ', $this->getRegisterClassFunctionCeList($classDef));
}
public function setBuildMode(string $mode): void
{
$this->buildMode = $mode;
}
public function setTargetName(string $name): void
{
if ($this->climate->arguments->defined('output')) {
@ -688,6 +693,9 @@ class Translator extends Preprocessor
if (!empty($cfg['name'])) {
$this->setTargetName($cfg['name']);
}
if (!empty($cfg['type'])) {
$this->setBuildMode($cfg['type']);
}
return $list;
}
@ -870,6 +878,7 @@ class Translator extends Preprocessor
$type2 = $v2->getType();
switch ($type2) {
case 'Stmt_Class':
case 'Stmt_Trait':
$code .= $this->parseClass($v2);
break;
case 'Stmt_Const':
@ -884,9 +893,6 @@ class Translator extends Preprocessor
case 'Stmt_Interface':
$code .= $this->parseInterface($v2) . PHP_EOL;
break;
case 'Stmt_Trait':
$code .= $this->parseTrait($v2) . PHP_EOL;
break;
default:
abort($v2);
}
@ -902,9 +908,9 @@ class Translator extends Preprocessor
$stubFilenameWithoutExtension = str_replace(['.stub.php', '.php'], '', $file);
$headerFile = $this->getArgInfoHeaderFile($stubFilenameWithoutExtension, true);
$this->climate->info('generate stub file: ' . $this->getRelativePath($file));
generateStubFile($file, $this->getIncludeDir() . '/' . $headerFile, true);
$this->climate->info('generate stub file: ' . $this->getRelativePath($file));
if ($this->useRegisterSymbolsFn) {
preg_match('/php_(.*)_arginfo.h/', $headerFile, $matches);
$registerSymbolFn = 'register_' . $matches[1] . '_symbols';

@ -228,8 +228,8 @@ class SimpleType {
return new SimpleType(ClassInfo::$currentClassName, false);
}
assert($node->isFullyQualified());
return new SimpleType($node->toString(), false);
$class = $node->isFullyQualified() ? $node->toString() : getTranslator()->getNamespacedClassName($node->toString());
return new SimpleType($class, false);
}
if ($node instanceof Node\Identifier) {

@ -0,0 +1,37 @@
--TEST--
Type Declarations
--FILE--
<?php
namespace TestApp\Console {
class Command {
public $name;
public function __construct(string $name)
{
$this->name = $name;
}
}
}
namespace TestApp\Command {
use TestApp\Console\Command;
trait CommandCallable {
public function callCommand(string $class): Command
{
return new Command($class);
}
}
class Test {
use CommandCallable;
}
}
namespace {
function main() {
$o = new TestApp\Command\Test;
$o2 = $o->callCommand('TestApp\Console\Command');
var_dump($o2->name);
}
}
?>
--EXPECT--
string(23) "TestApp\Console\Command"
Loading…
Cancel
Save