feat(php): 添加魔术方法类型推断支持

- 在 ArgInfo 类中添加 undeclared 属性用于标识未声明类型
- 在 FunctionDef 类中添加 returnTypeUndeclared 属性用于标识返回类型未声明
- 实现 getMagicMethodDefaultReturnType 函数为魔术方法提供默认返回类型映射
- 实现 getMagicMethodDefaultParamType 函数为魔术方法提供默认参数类型映射
- 更新魔术方法检测器中的类型检查逻辑,支持未声明类型的自动填充
- 重构 MagicMethodDetector 中的参数验证,为不同魔术方法设置正确的类型约束
- 添加对魔术方法参数和返回类型的自动类型补全功能
pull/3/head
韩天峰 2 months ago
parent 1e6fb4972a
commit 99c3dec641
  1. 1
      src/Php/ArgInfo.php
  2. 1
      src/Php/Entity/FunctionDef.php
  3. 126
      src/Php/MagicMethodDetector.php
  4. 2
      src/Php/Preprocessor.php
  5. 50
      src/gen_stub.php

@ -23,6 +23,7 @@ class ArgInfo
public bool $byRef = false; public bool $byRef = false;
public bool $variadic = false; public bool $variadic = false;
public bool $nullable = false; public bool $nullable = false;
public bool $undeclared = false;
public bool $property = false; public bool $property = false;
/** /**

@ -25,6 +25,7 @@ class FunctionDef
public string $namespace; public string $namespace;
public bool $method = false; public bool $method = false;
public bool $stub = false; public bool $stub = false;
public bool $returnTypeUndeclared = false;
/** /**
* @var string 必须是带有命名空间的完整类名 * @var string 必须是带有命名空间的完整类名

@ -16,53 +16,151 @@ trait MagicMethodDetector
public function checkRequiredArgNum(string $name, MethodDef $methodDef, NodeAbstract $v): void public function checkRequiredArgNum(string $name, MethodDef $methodDef, NodeAbstract $v): void
{ {
$argInfoList = $methodDef->functionDef->argInfoList; $argInfoList = $methodDef->functionDef->argInfoList;
$returnType = $methodDef->functionDef->returnType; $fnDef = $methodDef->functionDef;
$returnTypeUndeclared = $fnDef->returnTypeUndeclared;
$nameLower = strtolower($name); $nameLower = strtolower($name);
if ($nameLower == '__call' or $nameLower == '__callstatic' or $nameLower == '__set') { if ($nameLower == '__call' or $nameLower == '__callstatic') {
if (count($argInfoList) != 2) { if (count($argInfoList) != 2) {
$this->fatalError($v, 'Method ' . $this->class . "::{$name}() must take exactly 2 arguments"); $this->fatalError($v, 'Method ' . $this->class . "::{$name}() must take exactly 2 arguments");
} }
if (!$this->checkArgType($argInfoList[0]->type, self::TYPE_STR)) { if ($argInfoList[0]->undeclared) {
$this->fatalError($v, 'Method ' . $this->class . "::{$name}() must take string as argument"); $argInfoList[0]->type = self::TYPE_STR;
} elseif ($argInfoList[0]->type !== self::TYPE_STR) {
$this->fatalError($v, 'Method ' . $this->class . "::{$name}() must take string as first argument");
}
if ($argInfoList[1]->undeclared) {
$argInfoList[1]->type = self::TYPE_ARRAY;
} elseif ($argInfoList[1]->type !== self::TYPE_ARRAY) {
$this->fatalError($v, 'Method ' . $this->class . "::{$name}() must take array as second argument");
}
} elseif ($nameLower == '__set') {
if (count($argInfoList) != 2) {
$this->fatalError($v, 'Method ' . $this->class . "::{$name}() must take exactly 2 arguments");
}
if ($argInfoList[0]->undeclared) {
$argInfoList[0]->type = self::TYPE_STR;
} elseif ($argInfoList[0]->type !== self::TYPE_STR) {
$this->fatalError($v, 'Method ' . $this->class . "::{$name}() must take string as first argument");
}
if ($returnTypeUndeclared) {
$fnDef->returnType = self::TYPE_VOID;
} elseif ($fnDef->returnType !== self::TYPE_VOID) {
$this->fatalError($v, 'Method ' . $this->class . "::{$name}() must return void");
} }
} elseif ($nameLower == '__get') { } elseif ($nameLower == '__get') {
if (count($argInfoList) != 1) { if (count($argInfoList) != 1) {
$this->fatalError($v, 'Method ' . $this->class . "::{$name}() must take exactly 1 argument"); $this->fatalError($v, 'Method ' . $this->class . "::{$name}() must take exactly 1 argument");
} }
} elseif ($nameLower == '__tostring') { } elseif ($nameLower == '__tostring') {
if (!$this->checkArgType($returnType, self::TYPE_STR, false)) { if ($returnTypeUndeclared) {
$fnDef->returnType = self::TYPE_STR;
} elseif ($fnDef->returnType !== self::TYPE_STR) {
$this->fatalError($v, 'Method ' . $this->class . "::{$name}() must return string"); $this->fatalError($v, 'Method ' . $this->class . "::{$name}() must return string");
} }
} elseif ($nameLower == '__serialize') { } elseif ($nameLower == '__serialize') {
if (!$this->checkArgType($returnType, self::TYPE_ARRAY)) { if ($returnTypeUndeclared) {
$fnDef->returnType = self::TYPE_ARRAY;
} elseif ($fnDef->returnType !== self::TYPE_ARRAY) {
$this->fatalError($v, 'Method ' . $this->class . "::{$name}() must return array"); $this->fatalError($v, 'Method ' . $this->class . "::{$name}() must return array");
} }
} elseif ($nameLower == '__unserialize') { } elseif ($nameLower == '__unserialize') {
if (count($argInfoList) != 1) { if (count($argInfoList) != 1) {
$this->fatalError($v, 'Method ' . $this->class . "::{$name}() must take exactly 1 argument"); $this->fatalError($v, 'Method ' . $this->class . "::{$name}() must take exactly 1 argument");
} }
if (!$this->checkArgType($argInfoList[0]->type, self::TYPE_ARRAY)) { if ($argInfoList[0]->undeclared) {
$argInfoList[0]->type = self::TYPE_ARRAY;
} elseif ($argInfoList[0]->type !== self::TYPE_ARRAY) {
$this->fatalError($v, 'Method ' . $this->class . "::{$name}() must take array as argument"); $this->fatalError($v, 'Method ' . $this->class . "::{$name}() must take array as argument");
} }
} elseif ($nameLower == '__isset' or $nameLower == '__unset' or $nameLower == '__set_state') { if ($returnTypeUndeclared) {
$fnDef->returnType = self::TYPE_VOID;
} elseif ($fnDef->returnType !== self::TYPE_VOID) {
$this->fatalError($v, 'Method ' . $this->class . "::{$name}() must return void");
}
} elseif ($nameLower == '__isset') {
if (count($argInfoList) != 1) { if (count($argInfoList) != 1) {
$this->fatalError($v, 'Method ' . $this->class . "::{$name}() must take exactly 1 argument"); $this->fatalError($v, 'Method ' . $this->class . "::{$name}() must take exactly 1 argument");
} }
if (!$this->checkArgType($argInfoList[0]->type, self::TYPE_STR)) { if ($argInfoList[0]->undeclared) {
$argInfoList[0]->type = self::TYPE_STR;
} elseif ($argInfoList[0]->type !== self::TYPE_STR) {
$this->fatalError($v, 'Method ' . $this->class . "::{$name}() must take string as argument"); $this->fatalError($v, 'Method ' . $this->class . "::{$name}() must take string as argument");
} }
if ($nameLower == '__set_state') { if ($returnTypeUndeclared) {
if (!$this->checkArgType($returnType, self::TYPE_ARRAY)) { $fnDef->returnType = self::TYPE_BOOL;
$this->fatalError($v, 'Method ' . $this->class . "::{$name}() must return array"); } elseif ($fnDef->returnType !== self::TYPE_BOOL) {
} $this->fatalError($v, 'Method ' . $this->class . "::{$name}() must return bool");
}
} elseif ($nameLower == '__unset') {
if (count($argInfoList) != 1) {
$this->fatalError($v, 'Method ' . $this->class . "::{$name}() must take exactly 1 argument");
}
if ($argInfoList[0]->undeclared) {
$argInfoList[0]->type = self::TYPE_STR;
} elseif ($argInfoList[0]->type !== self::TYPE_STR) {
$this->fatalError($v, 'Method ' . $this->class . "::{$name}() must take string as argument");
}
if ($returnTypeUndeclared) {
$fnDef->returnType = self::TYPE_VOID;
} elseif ($fnDef->returnType !== self::TYPE_VOID) {
$this->fatalError($v, 'Method ' . $this->class . "::{$name}() must return void");
}
} elseif ($nameLower == '__set_state') {
if (count($argInfoList) != 1) {
$this->fatalError($v, 'Method ' . $this->class . "::{$name}() must take exactly 1 argument");
}
if ($argInfoList[0]->undeclared) {
$argInfoList[0]->type = self::TYPE_ARRAY;
} elseif ($argInfoList[0]->type !== self::TYPE_ARRAY) {
$this->fatalError($v, 'Method ' . $this->class . "::{$name}() must take array as argument");
}
if ($returnTypeUndeclared) {
$fnDef->returnType = self::TYPE_OBJECT;
} elseif ($fnDef->returnType !== self::TYPE_OBJECT) {
$this->fatalError($v, 'Method ' . $this->class . "::{$name}() must return object");
} }
} elseif ($nameLower == '__debuginfo') { } elseif ($nameLower == '__debuginfo') {
if (!$this->checkArgType($returnType, self::TYPE_ARRAY)) { if ($returnTypeUndeclared) {
$fnDef->returnType = self::TYPE_ARRAY;
} elseif ($fnDef->returnType !== self::TYPE_ARRAY) {
$this->fatalError($v, 'Method ' . $this->class . "::{$name}() must return array");
}
} elseif ($nameLower == '__sleep') {
if ($returnTypeUndeclared) {
$fnDef->returnType = self::TYPE_ARRAY;
} elseif ($fnDef->returnType !== self::TYPE_ARRAY) {
$this->fatalError($v, 'Method ' . $this->class . "::{$name}() must return array"); $this->fatalError($v, 'Method ' . $this->class . "::{$name}() must return array");
} }
} elseif ($nameLower == '__wakeup') {
if ($returnTypeUndeclared) {
$fnDef->returnType = self::TYPE_VOID;
} elseif ($fnDef->returnType !== self::TYPE_VOID) {
$this->fatalError($v, 'Method ' . $this->class . "::{$name}() must return void");
}
} elseif ($nameLower == '__clone') {
if ($returnTypeUndeclared) {
$fnDef->returnType = self::TYPE_VOID;
} elseif ($fnDef->returnType !== self::TYPE_VOID) {
$this->fatalError($v, 'Method ' . $this->class . "::{$name}() must return void");
}
}
// 重建 params 字符串,使 C++ 函数签名使用 auto-fill 后的类型
$list = [];
foreach ($fnDef->argInfoList as $argInfo) {
if ($argInfo->variadic) {
$list[] = self::TYPE_ARRAY . ' ' . $argInfo->name;
} else {
$type = $argInfo->type;
if ($type === self::TYPE_STREAM || $type === self::TYPE_BOX) {
$type = self::TYPE_VAR;
}
$list[] = $type . ' ' . $argInfo->name;
}
} }
$fnDef->params = implode(', ', $list);
} }
protected function checkArgType(string $givenType, string $expectType, bool $canBeVar = true): bool protected function checkArgType(string $givenType, string $expectType, bool $canBeVar = true): bool

@ -229,6 +229,7 @@ class Preprocessor extends CompilerBase
} }
$class = ''; $class = '';
$type = $this->parseTypeDecl($param->type, self::DECL_TYPE_OF_PARAM, $class); $type = $this->parseTypeDecl($param->type, self::DECL_TYPE_OF_PARAM, $class);
$argInfo->undeclared = $param->type === null;
if ($class and !$this->hasInterface($class) and !$this->isAbstractClass($class)) { if ($class and !$this->hasInterface($class) and !$this->isAbstractClass($class)) {
$argInfo->class = $class; $argInfo->class = $class;
} }
@ -357,6 +358,7 @@ class Preprocessor extends CompilerBase
$functionDef = new FunctionDef($fnName, $returnType, $this->namespace); $functionDef = new FunctionDef($fnName, $returnType, $this->namespace);
$functionDef->returnClass = $class; $functionDef->returnClass = $class;
$functionDef->stub = $this->stubFile; $functionDef->stub = $this->stubFile;
$functionDef->returnTypeUndeclared = $v->returnType === null;
if ($v->returnType instanceof NullableType or $v->returnType instanceof UnionType) { if ($v->returnType instanceof NullableType or $v->returnType instanceof UnionType) {
$typeInfo = $this->buildTypeCheckFromNode($v->returnType); $typeInfo = $this->buildTypeCheckFromNode($v->returnType);

@ -4809,6 +4809,51 @@ class FramelessFunctionInfo {
} }
} }
/**
* 获取魔术方法的默认返回值类型。只有用户未显式声明类型时才使用。
*/
function getMagicMethodDefaultReturnType(FunctionOrMethodName $name): ?string
{
if (!($name instanceof MethodName)) {
return null;
}
$map = [
'__set' => 'void',
'__tostring' => 'string',
'__serialize' => 'array',
'__unserialize' => 'void',
'__isset' => 'bool',
'__unset' => 'void',
'__set_state' => 'object',
'__debuginfo' => 'array',
'__sleep' => 'array',
'__wakeup' => 'void',
'__clone' => 'void',
];
return $map[strtolower($name->methodName)] ?? null;
}
/**
* 获取魔术方法的默认参数类型。只有用户未显式声明类型时才使用。
*/
function getMagicMethodDefaultParamType(FunctionOrMethodName $name, int $index): ?string
{
if (!($name instanceof MethodName)) {
return null;
}
$map = [
'__call' => ['string', 'array'],
'__callstatic' => ['string', 'array'],
'__set' => ['string'],
'__unserialize' => ['array'],
'__isset' => ['string'],
'__unset' => ['string'],
'__set_state' => ['array'],
];
$methodParams = $map[strtolower($name->methodName)] ?? null;
return $methodParams[$index] ?? null;
}
function parseFunctionLike( function parseFunctionLike(
PrettyPrinterAbstract $prettyPrinter, PrettyPrinterAbstract $prettyPrinter,
FunctionOrMethodName $name, FunctionOrMethodName $name,
@ -4930,7 +4975,8 @@ function parseFunctionLike(
$type = $param->type ? Type::fromNode($param->type) : null; $type = $param->type ? Type::fromNode($param->type) : null;
if ($type === null && !isset($docParamTypes[$varName])) { if ($type === null && !isset($docParamTypes[$varName])) {
$type = Type::fromString("mixed"); $defaultParamType = getMagicMethodDefaultParamType($name, $i);
$type = Type::fromString($defaultParamType ?? 'mixed');
} }
if ($param->default instanceof Expr\ConstFetch && if ($param->default instanceof Expr\ConstFetch &&
@ -4974,7 +5020,7 @@ function parseFunctionLike(
$returnType = $func->getReturnType(); $returnType = $func->getReturnType();
if ($returnType === null && $docReturnType === null && !$name->isConstructor() && !$name->isDestructor()) { if ($returnType === null && $docReturnType === null && !$name->isConstructor() && !$name->isDestructor()) {
$docReturnType = "mixed"; $docReturnType = getMagicMethodDefaultReturnType($name) ?? 'mixed';
} }
$return = new ReturnInfo( $return = new ReturnInfo(

Loading…
Cancel
Save