test(classes): 添加魔术方法相关测试用例

- 添加 __call 方法异常处理测试
- 添加 __set 和 __get 魔术方法功能测试
- 添加魔术方法签名检查测试
- 添加抽象类和抽象方法调用测试
- 添加对象转数组键名转换测试
- 添加赋值操作符属性测试
- 添加数据损坏相关测试

fix(compiler): 禁止在 echo 中使用赋值表达式

- 在 parseEcho 方法中检测赋值表达式并报错
- 修复属性赋值操作符解析逻辑

refactor(stub): 优化全局变量默认值处理

- 统一数组类型默认值处理方式
- 移除特殊空数组处理逻辑

feat(magic): 添加魔术方法参数数量检测机制

- 实现 MagicMethodDetector trait
- 集成到 Translator 类中使用
pull/1/head
韩天峰 7 months ago
parent 66249916f5
commit 43b9797df8
  1. 16
      bin/gen_stub.php
  2. 12
      src/Php/CompilerBase.php
  3. 21
      src/Php/MagicMethodDetector.php
  4. 8
      src/Php/Translator.php
  5. 79
      tests/core/classes/__call_006.phpt
  6. 71
      tests/core/classes/__set__get_001.phpt
  7. 16
      tests/core/classes/__set__get_002.phpt
  8. 16
      tests/core/classes/__set__get_003.phpt
  9. 33
      tests/core/classes/__set__get_004.phpt
  10. 62
      tests/core/classes/__set__get_005.phpt
  11. 30
      tests/core/classes/__set_data_corrupt.phpt
  12. 35
      tests/core/classes/abstract.phpt
  13. 32
      tests/core/classes/abstract_class.phpt
  14. 16
      tests/core/classes/abstract_final.phpt
  15. 20
      tests/core/classes/array_conversion_keys.phpt
  16. 32
      tests/core/classes/assign_op_property_001.phpt

@ -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));
}

@ -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

@ -0,0 +1,21 @@
<?php
namespace PhpAot\Php;
use PhpParser\NodeAbstract;
trait MagicMethodDetector
{
function checkRequiredArgNum(string $name, MethodDef $methodDef, NodeAbstract $v): void
{
if ($name == '__call' or $name == '__callStatic' or $name == '__set') {
if (count($methodDef->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");
}
}
}
}

@ -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 = '';
}

@ -0,0 +1,79 @@
--TEST--
Ensure exceptions are handled properly when thrown in __call.
--SKIPIF--
<?php die('skip'); ?>
--FILE--
<?php
class A {
function __call($strMethod, $arrArgs) {
var_dump($this);
throw new Exception;
echo "You should not see this";
}
function test() {
A::unknownCalledWithSRO(1,2,3);
}
}
class B extends A {
function test() {
B::unknownCalledWithSROFromChild(1,2,3);
}
}
function main() {
$a = new A();
echo "---> 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.

@ -0,0 +1,71 @@
--TEST--
ZE2 __set() and __get()
--FILE--
<?php
class setter {
public $n;
public $x = array('a' => 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)
}
}

@ -0,0 +1,16 @@
--TEST--
ZE2 __get() signature check
--SKIPIF--
<?php die('skip, failed at compile time'); ?>
--FILE--
<?php
class Test {
function __get($x,$y) {
}
}
function main() {
}
?>
--EXPECTF--
Fatal error: Method Test::__get() must take exactly 1 argument in %s__set__get_002.php on line %d

@ -0,0 +1,16 @@
--TEST--
ZE2 __set() signature check
--SKIPIF--
<?php die('skip, failed at compile time'); ?>
--FILE--
<?php
class Test {
function __set() {
}
}
function main() {
}
?>
--EXPECTF--
Fatal error: Method Test::__set() must take exactly 2 arguments in %s__set__get_003.php on line %d

@ -0,0 +1,33 @@
--TEST--
ZE2 __set() and __get()
--FILE--
<?php
class Test {
protected $x = [];
function __get($name) {
if (isset($this->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"

@ -0,0 +1,62 @@
--TEST--
ZE2 __set() and __get()
--FILE--
<?php
class Test
{
protected $x = [];
function __get($name) {
echo __METHOD__ . "\n";
if (isset($this->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"

@ -0,0 +1,30 @@
--TEST--
ZE2 Data corruption in __set
--FILE--
<?php
class foo {
const foobar=1;
public $pp = array('t'=>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

@ -0,0 +1,35 @@
--TEST--
ZE2 An abstract method may not be called
--FILE--
<?php
abstract class fail {
abstract function show();
}
class pass extends fail {
function show() {
echo "Call to function show()\n";
}
function error() {
parent::show();
}
}
function main() {
$t = new pass();
$t->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

@ -0,0 +1,32 @@
--TEST--
ZE2 An abstract class cannot be instantiated
--FILE--
<?php
abstract class fail {
abstract function show();
}
class pass extends fail {
function show() {
echo "Call to function show()\n";
}
}
function main() {
$t2 = new pass();
$t2->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

@ -0,0 +1,16 @@
--TEST--
ZE2 A final method cannot be abstract
--SKIPIF--
<?php die('skip, failed at compile time'); ?>
--FILE--
<?php
class fail {
abstract final function show();
}
function main() {
echo "Done\n"; // Shouldn't be displayed
}
?>
--EXPECTF--
Fatal error: Cannot use the final modifier on an abstract method in %s on line %d

@ -0,0 +1,20 @@
--TEST--
Verifies the correct conversion of objects to arrays
--FILE--
<?php
class foo
{
private $private = 'private';
protected $protected = 'protected';
public $public = 'public';
}
function main() {
var_export((array) new foo);
}
?>
--EXPECT--
array (
'' . "\0" . 'foo' . "\0" . 'private' => 'private',
'' . "\0" . '*' . "\0" . 'protected' => 'protected',
'public' => 'public',
)

@ -0,0 +1,32 @@
--TEST--
ZE2 assign_op property of overloaded object
--FILE--
<?php
class Test {
private $real_a = 2;
function __set($property, $value) {
if ($property == "a") {
$this->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---
Loading…
Cancel
Save