feat(php): 添加对构造函数析构函数返回类型和魔术方法的严格检查

- 实现构造函数和析构函数不能声明返回类型的检查
- 实现克隆方法返回类型必须为void的验证
- 添加魔术方法静态性和可见性检查规则
- 实现魔术方法参数数量和类型的严格校验
- 添加魔术方法参数类型检查如__get方法必须接受字符串参数
- 实现可选参数不能在必需参数之前的检查
- 添加闭包引用参数和变长引用参数的错误检查
- 优化参数计数错误消息中的预期参数数量提示
- 添加AOT编译器语言设计原则文档说明
pull/5/head
韩天峰 2 months ago
parent 399f77d5c5
commit 4214e1e278
  1. 14
      CLAUDE.md
  2. 13
      phpunit/code/clone-invalid-return-type.php
  3. 8
      phpunit/code/closure-ref-param.php
  4. 13
      phpunit/code/constructor-return-type.php
  5. 12
      phpunit/code/destructor-return-type.php
  6. 13
      phpunit/code/magic-call-static.php
  7. 13
      phpunit/code/magic-callstatic-nonstatic.php
  8. 12
      phpunit/code/magic-destruct-args.php
  9. 13
      phpunit/code/magic-get-param-type.php
  10. 13
      phpunit/code/magic-get-protected.php
  11. 13
      phpunit/code/magic-set-state-nonstatic.php
  12. 13
      phpunit/code/magic-tostring-args.php
  13. 12
      phpunit/code/method-optional-before-required-param.php
  14. 9
      phpunit/code/optional-before-required-param.php
  15. 9
      phpunit/code/variadic-ref-param.php
  16. 50
      phpunit/src/ClassTest.php
  17. 20
      phpunit/src/FunctionTest.php
  18. 3
      src/Php/Generator/ClosureGenerator.php
  19. 128
      src/Php/MagicMethodDetector.php
  20. 48
      src/Php/Preprocessor.php
  21. 3
      src/Php/Translator.php
  22. 12
      tests/aot/closure/closure-param-defaults.phpt
  23. 15
      tests/aot/type_decl/nullable-required-param-check.phpt

@ -8,6 +8,20 @@ Swoole-Compiler is an AOT (Ahead-of-Time) compiler that translates PHP source co
**Prerequisites**: PHP 8.2+, GCC 9+ (C++17), CMake 3.24+. The `swoole/phpx` extension must be compiled (see README.md).
## AOT Language Design Principles
The AOT compiler should not blindly mirror every PHP language behavior. Most PHP syntax and semantics should remain compatible with ZendPHP, but some legal PHP constructs are historical baggage or language-design mistakes that conflict with static compilation, clear semantics, or robust generated C++ code.
When reviewing or changing compiler behavior:
- Prefer PHP compatibility for common, well-defined syntax that does not weaken the AOT static model.
- Reject PHP historical baggage when the syntax is ambiguous, surprising, or only preserved for legacy compatibility.
- Diagnose such cases as early as possible during preprocessing/static compilation, instead of deferring to runtime TypeCheck or ZendVM errors.
- Provide precise errors that include the relevant function/method name, parameter/property name, and type information where applicable.
- Compare with other statically compiled languages such as C/C++, Java, C#, Go, Rust, Kotlin, and TypeScript before deciding whether AOT should preserve or reject a PHP behavior.
Example: `function test($a = 1, $b, $c) {}` is legal in PHP, but the default value for `$a` is effectively ignored and all parameters become required. This is a PHP historical compatibility artifact. AOT should reject it during preprocessing instead of preserving the behavior.
## Build & Test Commands
```bash

@ -0,0 +1,13 @@
<?php
class CloneInvalidReturnType
{
public function __clone(): int
{
return 1;
}
}
function main(): void
{
}

@ -0,0 +1,8 @@
<?php
function main(): void
{
$fn = function (&$value): void {
$value = 1;
};
}

@ -0,0 +1,13 @@
<?php
class ConstructorReturnType
{
public function __construct(): int
{
return 1;
}
}
function main(): void
{
}

@ -0,0 +1,12 @@
<?php
class DestructorReturnType
{
public function __destruct(): void
{
}
}
function main(): void
{
}

@ -0,0 +1,13 @@
<?php
class MagicCallStaticInvalid
{
public static function __call(string $name, array $arguments): mixed
{
return null;
}
}
function main(): void
{
}

@ -0,0 +1,13 @@
<?php
class MagicCallStaticNonStaticInvalid
{
public function __callStatic(string $name, array $arguments): mixed
{
return null;
}
}
function main(): void
{
}

@ -0,0 +1,12 @@
<?php
class MagicDestructArgsInvalid
{
public function __destruct($extra)
{
}
}
function main(): void
{
}

@ -0,0 +1,13 @@
<?php
class MagicGetParamTypeInvalid
{
public function __get(int $name): mixed
{
return null;
}
}
function main(): void
{
}

@ -0,0 +1,13 @@
<?php
class MagicGetProtectedInvalid
{
protected function __get(string $name): mixed
{
return null;
}
}
function main(): void
{
}

@ -0,0 +1,13 @@
<?php
class MagicSetStateNonStaticInvalid
{
public function __set_state(array $properties): object
{
return new self();
}
}
function main(): void
{
}

@ -0,0 +1,13 @@
<?php
class MagicToStringArgsInvalid
{
public function __toString($extra): string
{
return '';
}
}
function main(): void
{
}

@ -0,0 +1,12 @@
<?php
class OptionalBeforeRequired
{
public function method($first = 1, $second): void
{
}
}
function main(): void
{
}

@ -0,0 +1,9 @@
<?php
function test($a = 1, $b, $c): void
{
}
function main(): void
{
}

@ -0,0 +1,9 @@
<?php
function collect(&...$args): void
{
}
function main(): void
{
}

@ -65,4 +65,54 @@ class ClassTest extends \BaseTest
{
$this->exec("Type 'static' cannot be part of an intersection type", 'intersection_type_static_not_allowed.php');
}
public function testConstructorCannotDeclareReturnType()
{
$this->exec('Method `ConstructorReturnType::__construct()` cannot declare a return type', 'constructor-return-type.php');
}
public function testDestructorCannotDeclareReturnType()
{
$this->exec('Method `DestructorReturnType::__destruct()` cannot declare a return type', 'destructor-return-type.php');
}
public function testCloneReturnTypeMustBeVoid()
{
$this->exec('Method `CloneInvalidReturnType::__clone()` return type must be void when declared', 'clone-invalid-return-type.php');
}
public function testCallMagicMethodCannotBeStatic()
{
$this->exec('Method MagicCallStaticInvalid::__call() cannot be static', 'magic-call-static.php');
}
public function testCallStaticMagicMethodMustBeStatic()
{
$this->exec('Method MagicCallStaticNonStaticInvalid::__callStatic() must be static', 'magic-callstatic-nonstatic.php');
}
public function testToStringMagicMethodCannotTakeArguments()
{
$this->exec('Method MagicToStringArgsInvalid::__toString() must take exactly 0 arguments', 'magic-tostring-args.php');
}
public function testSetStateMagicMethodMustBeStatic()
{
$this->exec('Method MagicSetStateNonStaticInvalid::__set_state() must be static', 'magic-set-state-nonstatic.php');
}
public function testDestructMagicMethodCannotTakeArguments()
{
$this->exec('Method MagicDestructArgsInvalid::__destruct() must take exactly 0 arguments', 'magic-destruct-args.php');
}
public function testGetMagicMethodParameterMustBeString()
{
$this->exec('Method MagicGetParamTypeInvalid::__get() must take string as argument', 'magic-get-param-type.php');
}
public function testMagicMethodMustBePublic()
{
$this->exec('Method MagicGetProtectedInvalid::__get() must have public visibility', 'magic-get-protected.php');
}
}

@ -57,4 +57,24 @@ class FunctionTest extends \BaseTest
$this->exec('Cannot use positional argument after argument unpacking', 'new-positional-after-unpack.php');
}
public function testClosureReferenceParameter()
{
$this->exec('Closure cannot use reference parameter', 'closure-ref-param.php');
}
public function testVariadicReferenceParameter()
{
$this->exec('Variadic parameters cannot be passed by reference', 'variadic-ref-param.php');
}
public function testOptionalParameterBeforeRequiredParameter()
{
$this->exec('test(): optional parameter `$a` cannot be declared before required parameter `$c`', 'optional-before-required-param.php');
}
public function testMethodOptionalParameterBeforeRequiredParameter()
{
$this->exec('OptionalBeforeRequired::method(): optional parameter `$first` cannot be declared before required parameter `$second`', 'method-optional-before-required-param.php');
}
}

@ -58,10 +58,11 @@ trait ClosureGenerator
$requiredArgCount++;
}
if ($requiredArgCount > 0) {
$expected = $requiredArgCount === count($params) ? 'exactly' : 'at least';
$message = 'php::concat({'
. 'php::Str(' . $this->genCharPtr('Too few arguments to function {closure}(), ', true) . '), '
. 'php::toString(php::getCallArgNum()), '
. 'php::Str(' . $this->genCharPtr(' passed and exactly ' . $requiredArgCount . ' expected', true) . ')'
. 'php::Str(' . $this->genCharPtr(' passed and ' . $expected . ' ' . $requiredArgCount . ' expected', true) . ')'
. '})';
$code .= $this->getIndent() . 'if (UNEXPECTED(php::getCallArgNum() < ' . $requiredArgCount . ')) {' . PHP_EOL;
$this->indentLevel++;

@ -20,130 +20,182 @@ trait MagicMethodDetector
$returnTypeUndeclared = $fnDef->returnTypeUndeclared;
$nameLower = strtolower($name);
$methodName = $this->class . "::{$name}";
$isStatic = (bool) ($methodDef->flags & \PhpParser\Modifiers::STATIC);
if ($nameLower == '__call' or $nameLower == '__callstatic') {
if (count($argInfoList) != 2) {
$this->fatalError($v, 'Method ' . $this->class . "::{$name}() must take exactly 2 arguments");
$mustBeStatic = ['__callstatic' => true, '__set_state' => true];
$mustNotBeStatic = [
'__construct' => true,
'__destruct' => true,
'__clone' => true,
'__call' => true,
'__get' => true,
'__set' => true,
'__isset' => true,
'__unset' => true,
'__sleep' => true,
'__wakeup' => true,
'__serialize' => true,
'__unserialize' => true,
'__debuginfo' => true,
'__tostring' => true,
'__invoke' => true,
];
if (isset($mustBeStatic[$nameLower]) && !$isStatic) {
$this->fatalError($v, 'Method ' . $methodName . '() must be static');
}
if (isset($mustNotBeStatic[$nameLower]) && $isStatic) {
$this->fatalError($v, 'Method ' . $methodName . '() cannot be static');
}
$mustBePublic = [
'__call' => true,
'__callstatic' => true,
'__get' => true,
'__set' => true,
'__isset' => true,
'__unset' => true,
'__sleep' => true,
'__wakeup' => true,
'__serialize' => true,
'__unserialize' => true,
'__debuginfo' => true,
'__tostring' => true,
'__invoke' => true,
'__set_state' => true,
];
if (isset($mustBePublic[$nameLower]) && !($methodDef->flags & \PhpParser\Modifiers::PUBLIC)) {
$this->fatalError($v, 'Method ' . $methodName . '() must have public visibility');
}
$exactArgCount = [
'__destruct' => 0,
'__clone' => 0,
'__tostring' => 0,
'__sleep' => 0,
'__wakeup' => 0,
'__serialize' => 0,
'__debuginfo' => 0,
'__call' => 2,
'__callstatic' => 2,
'__set' => 2,
'__get' => 1,
'__isset' => 1,
'__unset' => 1,
'__set_state' => 1,
'__unserialize' => 1,
];
if (array_key_exists($nameLower, $exactArgCount) && count($argInfoList) !== $exactArgCount[$nameLower]) {
$this->fatalError($v, 'Method ' . $methodName . '() must take exactly ' . $exactArgCount[$nameLower] . ' arguments');
}
if ($nameLower == '__call' or $nameLower == '__callstatic') {
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");
$this->fatalError($v, 'Method ' . $methodName . '() 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");
$this->fatalError($v, 'Method ' . $methodName . '() 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");
$this->fatalError($v, 'Method ' . $methodName . '() 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");
$this->fatalError($v, 'Method ' . $methodName . '() must return void');
}
} elseif ($nameLower == '__get') {
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 ' . $methodName . '() must take string as argument');
}
} elseif ($nameLower == '__tostring') {
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 ' . $methodName . '() must return string');
}
} elseif ($nameLower == '__serialize') {
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 ' . $methodName . '() must return array');
}
} elseif ($nameLower == '__unserialize') {
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");
$this->fatalError($v, 'Method ' . $methodName . '() must take array 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");
$this->fatalError($v, 'Method ' . $methodName . '() must return void');
}
} elseif ($nameLower == '__isset') {
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");
$this->fatalError($v, 'Method ' . $methodName . '() must take string as argument');
}
if ($returnTypeUndeclared) {
$fnDef->returnType = self::TYPE_BOOL;
} elseif ($fnDef->returnType !== self::TYPE_BOOL) {
$this->fatalError($v, 'Method ' . $this->class . "::{$name}() must return bool");
$this->fatalError($v, 'Method ' . $methodName . '() 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");
$this->fatalError($v, 'Method ' . $methodName . '() 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");
$this->fatalError($v, 'Method ' . $methodName . '() 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");
$this->fatalError($v, 'Method ' . $methodName . '() 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");
$this->fatalError($v, 'Method ' . $methodName . '() must return object');
}
} elseif ($nameLower == '__debuginfo') {
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 ' . $methodName . '() 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 ' . $methodName . '() 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");
$this->fatalError($v, 'Method ' . $methodName . '() 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");
$this->fatalError($v, 'Method ' . $methodName . '() must return void');
}
}

@ -245,8 +245,17 @@ class Preprocessor extends CompilerBase
{
$list = [];
$functionDef->argCountRequired = count($params);
$defaultValueCount = 0;
$lastRequiredIndex = -1;
$lastRequiredName = '';
$last = array_key_last($params);
foreach ($params as $i => $param) {
if (!$param->default && !$param->variadic) {
$lastRequiredIndex = $i;
if (is_string($param->var->name)) {
$lastRequiredName = $param->var->name;
}
}
}
foreach ($params as $i => $param) {
if (!is_string($param->var->name)) {
@ -276,6 +285,14 @@ class Preprocessor extends CompilerBase
$this->fatalError($param, 'Variadic parameters cannot be passed by reference');
}
}
if ($param->default && $i < $lastRequiredIndex) {
$this->fatalError(
$param,
$this->getFunctionDisplayName($functionDef)
. '(): optional parameter `$' . $phpName . '` cannot be declared before required parameter `$'
. $lastRequiredName . '`'
);
}
if ($this->method and $name === 'this_') {
$this->fatalError($param, 'Cannot use `$this` as parameter of class method');
}
@ -325,17 +342,23 @@ class Preprocessor extends CompilerBase
$argInfo->arrayInitPlan = $arrayInitPlan;
$argInfo->defaultValue = $param->default;
}
$defaultValueCount++;
} elseif ($param->variadic) {
// 变长参数可以视为空数组默认值
$defaultValueCount++;
$argInfo->default = '{}';
$argInfo->defaultValue = new Node\Expr\Array_();
}
$functionDef->argInfoList[] = $argInfo;
}
$functionDef->params = implode(', ', $list);
$functionDef->argCountRequired -= $defaultValueCount;
$functionDef->argCountRequired = $lastRequiredIndex + 1;
}
protected function getFunctionDisplayName(FunctionDef $functionDef): string
{
if ($this->class) {
return $this->class . '::' . $functionDef->name;
}
return $functionDef->getNamespacedName();
}
protected function parseFunctionDecl(Node\Stmt\Function_|Node\Stmt\ClassMethod $v): FunctionDef
@ -352,6 +375,16 @@ class Preprocessor extends CompilerBase
if ($v->byRef) {
$this->fatalError($v, 'The return type of the function `' . $v->name . '` cannot be a reference type');
}
if ($this->method and $v->returnType !== null) {
$methodName = $this->class . '::' . $this->method;
if (in_array($this->method, ['__construct', '__destruct'], true)) {
$this->fatalError($v, 'Method `' . $methodName . '()` cannot declare a return type');
}
if ($this->method === '__clone'
and (!$v->returnType instanceof Node\Identifier or strtolower($v->returnType->name) !== 'void')) {
$this->fatalError($v, 'Method `' . $methodName . '()` return type must be void when declared');
}
}
$fnName = $this->parseIdentifier($v->name);
$class = '';
@ -435,6 +468,9 @@ class Preprocessor extends CompilerBase
} else {
$flags = Modifiers::PUBLIC;
}
if (isset($this->symbolDeclInFile[$fullClassNameLower])) {
$this->fatalError($class, "Duplicate class `{$fullClassName}`");
}
$this->classDef = new ClassDef($this->class, $flags, $this->namespace);
$this->addClass($fullClassName, $this->classDef);
@ -466,10 +502,6 @@ class Preprocessor extends CompilerBase
} else {
$this->classDef->trait = $class;
}
if (isset($this->symbolDeclInFile[$fullClassNameLower])) {
$this->fatalError($class, "Duplicate class `{$fullClassName}`");
}
$this->symbolDeclInFile[$fullClassNameLower] = $this->file;
$code = '';

@ -2906,10 +2906,11 @@ CODE;
private function genWrapperRequiredArgCountCheck(FunctionDef $functionDef, string $displayName): string
{
$required = $functionDef->argCountRequired;
$expected = $required === count($functionDef->argInfoList) ? 'exactly' : 'at least';
$message = 'php::concat({'
. 'php::Str(' . $this->genCharPtr('Too few arguments to function ' . $displayName . '(), ', true) . '), '
. 'php::toString(php::getCallArgNum()), '
. 'php::Str(' . $this->genCharPtr(' passed and exactly ' . $required . ' expected', true) . ')'
. 'php::Str(' . $this->genCharPtr(' passed and ' . $expected . ' ' . $required . ' expected', true) . ')'
. '})';
$code = $this->getIndent() . 'if (UNEXPECTED(php::getCallArgNum() < ' . $required . ')) {' . PHP_EOL;

@ -25,6 +25,16 @@ function main(): void
var_dump(get_class($e));
var_dump($e->getMessage());
}
$requiredWithDefault = function ($value, $default = 42) {
var_dump($value, $default);
};
try {
$requiredWithDefault();
} catch (\Throwable $e) {
var_dump(get_class($e));
var_dump($e->getMessage());
}
}
?>
--EXPECT--
@ -39,3 +49,5 @@ array(3) {
}
string(18) "ArgumentCountError"
string(74) "Too few arguments to function {closure}(), 0 passed and exactly 1 expected"
string(18) "ArgumentCountError"
string(75) "Too few arguments to function {closure}(), 0 passed and at least 1 expected"

@ -7,6 +7,11 @@ function expect_nullable_int(?int $x): void
var_dump($x);
}
function expect_nullable_with_default(?int $x, int $fallback = 1): void
{
var_dump($x, $fallback);
}
function main(): void
{
$fn = 'expect_nullable_int';
@ -16,8 +21,18 @@ function main(): void
var_dump(get_class($e));
var_dump($e->getMessage());
}
$fn = 'expect_nullable_with_default';
try {
$fn();
} catch (\Throwable $e) {
var_dump(get_class($e));
var_dump($e->getMessage());
}
}
?>
--EXPECT--
string(18) "ArgumentCountError"
string(84) "Too few arguments to function expect_nullable_int(), 0 passed and exactly 1 expected"
string(18) "ArgumentCountError"
string(94) "Too few arguments to function expect_nullable_with_default(), 0 passed and at least 1 expected"

Loading…
Cancel
Save