diff --git a/src/Php/ArgInfo.php b/src/Php/ArgInfo.php index 680f1d33..70902959 100644 --- a/src/Php/ArgInfo.php +++ b/src/Php/ArgInfo.php @@ -23,6 +23,7 @@ class ArgInfo public bool $byRef = false; public bool $variadic = false; public bool $nullable = false; + public bool $undeclared = false; public bool $property = false; /** diff --git a/src/Php/Entity/FunctionDef.php b/src/Php/Entity/FunctionDef.php index ff98ccbc..2c520efc 100644 --- a/src/Php/Entity/FunctionDef.php +++ b/src/Php/Entity/FunctionDef.php @@ -25,6 +25,7 @@ class FunctionDef public string $namespace; public bool $method = false; public bool $stub = false; + public bool $returnTypeUndeclared = false; /** * @var string 必须是带有命名空间的完整类名 diff --git a/src/Php/MagicMethodDetector.php b/src/Php/MagicMethodDetector.php index 0d32ea02..f7f8b2d6 100644 --- a/src/Php/MagicMethodDetector.php +++ b/src/Php/MagicMethodDetector.php @@ -16,53 +16,151 @@ trait MagicMethodDetector public function checkRequiredArgNum(string $name, MethodDef $methodDef, NodeAbstract $v): void { $argInfoList = $methodDef->functionDef->argInfoList; - $returnType = $methodDef->functionDef->returnType; + $fnDef = $methodDef->functionDef; + $returnTypeUndeclared = $fnDef->returnTypeUndeclared; $nameLower = strtolower($name); - if ($nameLower == '__call' or $nameLower == '__callstatic' or $nameLower == '__set') { + if ($nameLower == '__call' or $nameLower == '__callstatic') { if (count($argInfoList) != 2) { $this->fatalError($v, 'Method ' . $this->class . "::{$name}() must take exactly 2 arguments"); } - if (!$this->checkArgType($argInfoList[0]->type, self::TYPE_STR)) { - $this->fatalError($v, 'Method ' . $this->class . "::{$name}() must take string as 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 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') { if (count($argInfoList) != 1) { $this->fatalError($v, 'Method ' . $this->class . "::{$name}() must take exactly 1 argument"); } } 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"); } } 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"); } } elseif ($nameLower == '__unserialize') { if (count($argInfoList) != 1) { $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"); } - } 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) { $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"); } - if ($nameLower == '__set_state') { - if (!$this->checkArgType($returnType, self::TYPE_ARRAY)) { - $this->fatalError($v, 'Method ' . $this->class . "::{$name}() must return array"); - } + if ($returnTypeUndeclared) { + $fnDef->returnType = self::TYPE_BOOL; + } 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') { - 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"); } + } 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 diff --git a/src/Php/Preprocessor.php b/src/Php/Preprocessor.php index 4d2f3e35..81773edc 100644 --- a/src/Php/Preprocessor.php +++ b/src/Php/Preprocessor.php @@ -229,6 +229,7 @@ class Preprocessor extends CompilerBase } $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)) { $argInfo->class = $class; } @@ -357,6 +358,7 @@ class Preprocessor extends CompilerBase $functionDef = new FunctionDef($fnName, $returnType, $this->namespace); $functionDef->returnClass = $class; $functionDef->stub = $this->stubFile; + $functionDef->returnTypeUndeclared = $v->returnType === null; if ($v->returnType instanceof NullableType or $v->returnType instanceof UnionType) { $typeInfo = $this->buildTypeCheckFromNode($v->returnType); diff --git a/src/gen_stub.php b/src/gen_stub.php index f7cac0f3..c314aba1 100755 --- a/src/gen_stub.php +++ b/src/gen_stub.php @@ -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( PrettyPrinterAbstract $prettyPrinter, FunctionOrMethodName $name, @@ -4930,7 +4975,8 @@ function parseFunctionLike( $type = $param->type ? Type::fromNode($param->type) : null; 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 && @@ -4974,7 +5020,7 @@ function parseFunctionLike( $returnType = $func->getReturnType(); if ($returnType === null && $docReturnType === null && !$name->isConstructor() && !$name->isDestructor()) { - $docReturnType = "mixed"; + $docReturnType = getMagicMethodDefaultReturnType($name) ?? 'mixed'; } $return = new ReturnInfo(