From 43b9797df8f6366c38a80f89a0db633ae8b64b5d Mon Sep 17 00:00:00 2001 From: tianfenghan Date: Fri, 16 Jan 2026 20:00:07 +0800 Subject: [PATCH] =?UTF-8?q?test(classes):=20=E6=B7=BB=E5=8A=A0=E9=AD=94?= =?UTF-8?q?=E6=9C=AF=E6=96=B9=E6=B3=95=E7=9B=B8=E5=85=B3=E6=B5=8B=E8=AF=95?= =?UTF-8?q?=E7=94=A8=E4=BE=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 添加 __call 方法异常处理测试 - 添加 __set 和 __get 魔术方法功能测试 - 添加魔术方法签名检查测试 - 添加抽象类和抽象方法调用测试 - 添加对象转数组键名转换测试 - 添加赋值操作符属性测试 - 添加数据损坏相关测试 fix(compiler): 禁止在 echo 中使用赋值表达式 - 在 parseEcho 方法中检测赋值表达式并报错 - 修复属性赋值操作符解析逻辑 refactor(stub): 优化全局变量默认值处理 - 统一数组类型默认值处理方式 - 移除特殊空数组处理逻辑 feat(magic): 添加魔术方法参数数量检测机制 - 实现 MagicMethodDetector trait - 集成到 Translator 类中使用 --- bin/gen_stub.php | 16 ++-- src/Php/CompilerBase.php | 12 ++- src/Php/MagicMethodDetector.php | 21 +++++ src/Php/Translator.php | 8 +- tests/core/classes/__call_006.phpt | 79 +++++++++++++++++++ tests/core/classes/__set__get_001.phpt | 71 +++++++++++++++++ tests/core/classes/__set__get_002.phpt | 16 ++++ tests/core/classes/__set__get_003.phpt | 16 ++++ tests/core/classes/__set__get_004.phpt | 33 ++++++++ tests/core/classes/__set__get_005.phpt | 62 +++++++++++++++ tests/core/classes/__set_data_corrupt.phpt | 30 +++++++ tests/core/classes/abstract.phpt | 35 ++++++++ tests/core/classes/abstract_class.phpt | 32 ++++++++ tests/core/classes/abstract_final.phpt | 16 ++++ tests/core/classes/array_conversion_keys.phpt | 20 +++++ .../core/classes/assign_op_property_001.phpt | 32 ++++++++ 16 files changed, 481 insertions(+), 18 deletions(-) create mode 100644 src/Php/MagicMethodDetector.php create mode 100644 tests/core/classes/__call_006.phpt create mode 100644 tests/core/classes/__set__get_001.phpt create mode 100644 tests/core/classes/__set__get_002.phpt create mode 100644 tests/core/classes/__set__get_003.phpt create mode 100644 tests/core/classes/__set__get_004.phpt create mode 100644 tests/core/classes/__set__get_005.phpt create mode 100644 tests/core/classes/__set_data_corrupt.phpt create mode 100644 tests/core/classes/abstract.phpt create mode 100644 tests/core/classes/abstract_class.phpt create mode 100644 tests/core/classes/abstract_final.phpt create mode 100644 tests/core/classes/array_conversion_keys.phpt create mode 100644 tests/core/classes/assign_op_property_001.phpt diff --git a/bin/gen_stub.php b/bin/gen_stub.php index 6949ec54..6014fedb 100755 --- a/bin/gen_stub.php +++ b/bin/gen_stub.php @@ -2394,16 +2394,12 @@ class EvaluatedValue $code .= "\tZVAL_STR(&$zvalName, $forStringDef);\n"; } } elseif ($this->type->isArray()) { - if ($cExpr == '[]') { - $code .= "\tZVAL_EMPTY_ARRAY(&$zvalName);\n"; - } else { - global $translator; - $tmpVar = $translator->genTmpVarName(); - $code .= "\tauto $tmpVar = " . $translator->parseExpr($this->expr) . ";\n"; - $code .= "\t{$tmpVar}.moveTo(&$zvalName);\n"; - $code .= "\tphp::global_vars[\"$varName\"] = $zvalName;\n"; - $code .= "\tZ_TYPE_FLAGS($zvalName) = 0;\n"; - } + global $translator; + $tmpVar = $translator->genTmpVarName(); + $code .= "\tauto $tmpVar = " . $translator->parseExpr($this->expr) . ";\n"; + $code .= "\t{$tmpVar}.moveTo(&$zvalName);\n"; + $code .= "\tphp::global_vars[\"$varName\"] = $zvalName;\n"; + $code .= "\tZ_TYPE_FLAGS($zvalName) = 0;\n"; } else { throw new Exception("Invalid default value: " . print_r($this->value, true) . ", type: " . print_r($this->type, true)); } diff --git a/src/Php/CompilerBase.php b/src/Php/CompilerBase.php index 484d7f19..a74741b9 100644 --- a/src/Php/CompilerBase.php +++ b/src/Php/CompilerBase.php @@ -790,7 +790,11 @@ class CompilerBase extends \PhpAot\Core\Translator protected function parseEcho(mixed $v): string { foreach ($v->exprs as $expr) { - $lines[] = 'php::echo(' . $this->parseExpr($expr) . ');'; + if ($expr instanceof Node\Expr\Assign) { + $this->fatalError($expr, 'Cannot echo assign expression'); + } else { + $lines[] = 'php::echo(' . $this->parseExpr($expr) . ');'; + } } return implode("\n" . $this->getIndent(), $lines); } @@ -1231,11 +1235,13 @@ class CompilerBase extends \PhpAot\Core\Translator $this->convertExprType($expr, $type, $rightType) . ';'; } return $this->parseArrayDimStore($node->var->var, $dim, $tmpVar); + } elseif ($this->isPropertyFetch($node->var)) { + $obj = $this->parseIdentifier($node->var->var); + $prop = $this->identifierToStr($node->var->name); + return $obj . '.setProperty(' . $prop . ', ' . $obj . '.getProperty(' . $prop . ') ' . $this->removeAssignOp($op) . ' ' . $expr . ')'; } else { return $var . ' ' . $op . ' (' . $expr . ')'; } - - // TODO 属性设置 } protected function parseAssignOpConcat(mixed $expr): string diff --git a/src/Php/MagicMethodDetector.php b/src/Php/MagicMethodDetector.php new file mode 100644 index 00000000..f431377c --- /dev/null +++ b/src/Php/MagicMethodDetector.php @@ -0,0 +1,21 @@ +functionDef->argInfoList) != 2) { + $this->fatalError($v, 'Method ' . $this->class . "::$name() must take exactly 2 arguments"); + } + } elseif ($name == '__get') { + if (count($methodDef->functionDef->argInfoList) != 1) { + $this->fatalError($v, 'Method ' . $this->class . "::$name() must take exactly 1 argument"); + } + } + } +} \ No newline at end of file diff --git a/src/Php/Translator.php b/src/Php/Translator.php index e661983e..07c2feaa 100644 --- a/src/Php/Translator.php +++ b/src/Php/Translator.php @@ -10,6 +10,8 @@ use PhpParser\NodeTraverser; class Translator extends Preprocessor { + use MagicMethodDetector; + protected bool $verbose = false; protected array $unsupportedFunctions = [ 'compact', @@ -798,11 +800,7 @@ class Translator extends Preprocessor $this->method = $name; $methodCodes[$name] = $this->parseFunction($v); $methodDef = new MethodDef($v->flags, $name, $this->functionDef); - if ($name == '__call') { - if (count($methodDef->functionDef->argInfoList) != 2) { - $this->fatalError($v, 'Method ' . $this->class . '::__call() must take exactly 2 arguments'); - } - } + $this->checkRequiredArgNum($name, $methodDef, $v); $this->classDef->methods[$name] = $methodDef; $this->method = ''; } diff --git a/tests/core/classes/__call_006.phpt b/tests/core/classes/__call_006.phpt new file mode 100644 index 00000000..45842cab --- /dev/null +++ b/tests/core/classes/__call_006.phpt @@ -0,0 +1,79 @@ +--TEST-- +Ensure exceptions are handled properly when thrown in __call. +--SKIPIF-- + +--FILE-- + Invoke __call via simple method call.\n"; + try { + $a->unknown(); + } catch (Exception $e) { + echo "Exception caught OK; continuing.\n"; + } + + echo "\n\n---> Invoke __call via scope resolution operator within instance.\n"; + try { + $a->test(); + } catch (Exception $e) { + echo "Exception caught OK; continuing.\n"; + } + + echo "\n\n---> Invoke __call via scope resolution operator within child instance.\n"; + $b = new B(); + try { + $b->test(); + } catch (Exception $e) { + echo "Exception caught OK; continuing.\n"; + } + + echo "\n\n---> Invoke __call via callback.\n"; + try { + call_user_func(array($b, 'unknownCallback'), 1,2,3); + } catch (Exception $e) { + echo "Exception caught OK; continuing.\n"; + } +} +?> +--EXPECTF-- +---> Invoke __call via simple method call. +object(A)#%d (0) { +} +Exception caught OK; continuing. + + +---> Invoke __call via scope resolution operator within instance. +object(A)#%d (0) { +} +Exception caught OK; continuing. + + +---> Invoke __call via scope resolution operator within child instance. +object(B)#%d (0) { +} +Exception caught OK; continuing. + + +---> Invoke __call via callback. +object(B)#%d (0) { +} +Exception caught OK; continuing. diff --git a/tests/core/classes/__set__get_001.phpt b/tests/core/classes/__set__get_001.phpt new file mode 100644 index 00000000..149bde08 --- /dev/null +++ b/tests/core/classes/__set__get_001.phpt @@ -0,0 +1,71 @@ +--TEST-- +ZE2 __set() and __get() +--FILE-- + 1, 'b' => 2, 'c' => 3); + + function __get($nm) { + echo "Getting [$nm]\n"; + + if (isset($this->x[$nm])) { + $r = $this->x[$nm]; + echo "Returning: $r\n"; + return $r; + } + else { + echo "Nothing!\n"; + } + } + + function __set($nm, $val) { + echo "Setting [$nm] to $val\n"; + + if (isset($this->x[$nm])) { + $this->x[$nm] = $val; + echo "OK!\n"; + } + else { + echo "Not OK!\n"; + } + } +} + +function main() { + $foo = new Setter(); + + // this doesn't go through __set()... should it? + $foo->n = 1; + + // the rest are fine... + $foo->a = 100; + $foo->a++; + $foo->z++; + var_dump($foo); +} +?> +--EXPECTF-- +Setting [a] to 100 +OK! +Getting [a] +Returning: 100 +Setting [a] to 101 +OK! +Getting [z] +Nothing! +Setting [z] to 1 +Not OK! +object(setter)#%d (2) { + ["n"]=> + int(1) + ["x"]=> + array(3) { + ["a"]=> + int(101) + ["b"]=> + int(2) + ["c"]=> + int(3) + } +} diff --git a/tests/core/classes/__set__get_002.phpt b/tests/core/classes/__set__get_002.phpt new file mode 100644 index 00000000..bbfa359c --- /dev/null +++ b/tests/core/classes/__set__get_002.phpt @@ -0,0 +1,16 @@ +--TEST-- +ZE2 __get() signature check +--SKIPIF-- + +--FILE-- + +--EXPECTF-- +Fatal error: Method Test::__get() must take exactly 1 argument in %s__set__get_002.php on line %d diff --git a/tests/core/classes/__set__get_003.phpt b/tests/core/classes/__set__get_003.phpt new file mode 100644 index 00000000..62e491af --- /dev/null +++ b/tests/core/classes/__set__get_003.phpt @@ -0,0 +1,16 @@ +--TEST-- +ZE2 __set() signature check +--SKIPIF-- + +--FILE-- + +--EXPECTF-- +Fatal error: Method Test::__set() must take exactly 2 arguments in %s__set__get_003.php on line %d diff --git a/tests/core/classes/__set__get_004.phpt b/tests/core/classes/__set__get_004.phpt new file mode 100644 index 00000000..4479b1a5 --- /dev/null +++ b/tests/core/classes/__set__get_004.phpt @@ -0,0 +1,33 @@ +--TEST-- +ZE2 __set() and __get() +--FILE-- +x[$name])) { + return $this->x[$name]; + } else { + return NULL; + } + } + + function __set($name, $val) { + $this->x[$name] = $val; + } +} +function main() { + $foo = new Test(); + $bar = new Test(); + $bar->baz = "Check"; + + $foo->bar = $bar; + + var_dump($bar->baz); + var_dump($foo->bar->baz); +} +?> +--EXPECT-- +string(5) "Check" +string(5) "Check" diff --git a/tests/core/classes/__set__get_005.phpt b/tests/core/classes/__set__get_005.phpt new file mode 100644 index 00000000..e5d28609 --- /dev/null +++ b/tests/core/classes/__set__get_005.phpt @@ -0,0 +1,62 @@ +--TEST-- +ZE2 __set() and __get() +--FILE-- +x[$name])) { + return $this->x[$name]; + } else { + return NULL; + } + } + + function __set($name, $val) { + echo __METHOD__ . "\n"; + $this->x[$name] = $val; + } +} + +class AutoGen +{ + protected $x = []; + + function __get($name) { + echo __METHOD__ . "\n"; + if (!isset($this->x[$name])) { + $this->x[$name] = new Test(); + } + return $this->x[$name]; + } + + function __set($name, $val) { + echo __METHOD__ . "\n"; + $this->x[$name] = $val; + } +} +function main() { + $foo = new AutoGen(); + $foo->bar->baz = "Check"; + + var_dump($foo->bar); + var_dump($foo->bar->baz); +} +?> +--EXPECTF-- +AutoGen::__get +Test::__set +AutoGen::__get +object(Test)#%d (1) { + ["x":protected]=> + array(1) { + ["baz"]=> + string(5) "Check" + } +} +AutoGen::__get +Test::__get +string(5) "Check" diff --git a/tests/core/classes/__set_data_corrupt.phpt b/tests/core/classes/__set_data_corrupt.phpt new file mode 100644 index 00000000..db01ed85 --- /dev/null +++ b/tests/core/classes/__set_data_corrupt.phpt @@ -0,0 +1,30 @@ +--TEST-- +ZE2 Data corruption in __set +--FILE-- +null); + + function bar() { + $this->t = 'f'; + echo $this->t; + } + function __get($prop) + { + return $this->pp[$prop]; + } + function __set($prop, $val) + { + echo "__set"; + $this->pp[$prop] = '__test'; + } +} + +function main() { + $f = new foo; + $f->bar(); +} +?> +--EXPECT-- +__set__test diff --git a/tests/core/classes/abstract.phpt b/tests/core/classes/abstract.phpt new file mode 100644 index 00000000..14af18f9 --- /dev/null +++ b/tests/core/classes/abstract.phpt @@ -0,0 +1,35 @@ +--TEST-- +ZE2 An abstract method may not be called +--FILE-- +show(); + $t->error(); + + echo "Done\n"; // shouldn't be displayed +} +?> +--EXPECTF-- +Call to function show() + +Fatal error: Uncaught Error: Cannot call abstract method fail::show() in %s:%d +Stack trace: +#0 [internal function]: pass->error() +#1 Unknown(0) : %s +#2 {main} + thrown in %s on line %d diff --git a/tests/core/classes/abstract_class.phpt b/tests/core/classes/abstract_class.phpt new file mode 100644 index 00000000..cae9c8a5 --- /dev/null +++ b/tests/core/classes/abstract_class.phpt @@ -0,0 +1,32 @@ +--TEST-- +ZE2 An abstract class cannot be instantiated +--FILE-- +show(); + + $t = new fail(); + $t->show(); + + echo "Done\n"; // shouldn't be displayed +} +?> +--EXPECTF-- +Call to function show() + +Fatal error: Uncaught Error: Cannot instantiate abstract class fail in %s:%d +Stack trace: +#0 Unknown(0) : %s +#1 {main} + thrown in %s on line %d diff --git a/tests/core/classes/abstract_final.phpt b/tests/core/classes/abstract_final.phpt new file mode 100644 index 00000000..b05be6d2 --- /dev/null +++ b/tests/core/classes/abstract_final.phpt @@ -0,0 +1,16 @@ +--TEST-- +ZE2 A final method cannot be abstract +--SKIPIF-- + +--FILE-- + +--EXPECTF-- +Fatal error: Cannot use the final modifier on an abstract method in %s on line %d diff --git a/tests/core/classes/array_conversion_keys.phpt b/tests/core/classes/array_conversion_keys.phpt new file mode 100644 index 00000000..c6c2f672 --- /dev/null +++ b/tests/core/classes/array_conversion_keys.phpt @@ -0,0 +1,20 @@ +--TEST-- +Verifies the correct conversion of objects to arrays +--FILE-- + +--EXPECT-- +array ( + '' . "\0" . 'foo' . "\0" . 'private' => 'private', + '' . "\0" . '*' . "\0" . 'protected' => 'protected', + 'public' => 'public', +) diff --git a/tests/core/classes/assign_op_property_001.phpt b/tests/core/classes/assign_op_property_001.phpt new file mode 100644 index 00000000..d85eb666 --- /dev/null +++ b/tests/core/classes/assign_op_property_001.phpt @@ -0,0 +1,32 @@ +--TEST-- +ZE2 assign_op property of overloaded object +--FILE-- +real_a = $value; + } + } + + function __get($property) { + if ($property == "a") { + return $this->real_a; + } + } +} +function main() { + $obj = new Test; + var_dump($obj->a); + $obj->a += 2; + var_dump($obj->a); + echo "---Done---\n"; +} +?> +--EXPECT-- +int(2) +int(4) +---Done---