From 91675ed3664d2778faa8feb6ac86168fb10031d6 Mon Sep 17 00:00:00 2001 From: tianfenghan Date: Thu, 18 Jun 2026 10:40:58 +0800 Subject: [PATCH] =?UTF-8?q?feat(php):=20=E6=B7=BB=E5=8A=A0=E9=99=A4?= =?UTF-8?q?=E9=9B=B6=E6=A3=80=E6=9F=A5=E5=92=8C=E6=94=B9=E8=BF=9B=E5=8E=9F?= =?UTF-8?q?=E7=94=9F=E5=B1=9E=E6=80=A7=E8=AE=BF=E9=97=AE=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 添加对字面量除零、模零、赋值除零和赋值模零操作的编译时检查 - 实现对整数、浮点数和字符串类型的零值检测 - 为 AssignOpTrait 和 BinaryOpTrait 添加 guardLiteralDivisionByZero 方法 - 改进原生属性访问的类名解析,使用完整的类名进行查找 - 支持 static 关键字作为类名解析的一部分 - 添加 isSameClassName 和 isSameOrSubclassOf 辅助方法用于类继承关系判断 - 实现受保护属性访问权限控制的 canAccessProtectedProperty 方法 - 添加多个测试文件验证除零检查和原生属性访问功能 - 创建 NativePropertyTest 和 OperatorTest 测试类验证相关功能 --- phpunit/code/assign-divide-by-zero.php | 7 ++ phpunit/code/assign-modulo-by-zero.php | 7 ++ phpunit/code/divide-by-zero-float.php | 6 ++ phpunit/code/divide-by-zero-int.php | 6 ++ phpunit/code/divide-by-zero-string.php | 6 ++ phpunit/code/modulo-by-zero-int.php | 6 ++ phpunit/code/native-property-full-name.php | 69 +++++++++++++++++++ .../native-property-private-other-class.php | 20 ++++++ ...ive-property-protected-unrelated-class.php | 20 ++++++ phpunit/src/NativePropertyTest.php | 40 +++++++++++ phpunit/src/OperatorTest.php | 34 +++++++++ src/Php/CompilerBase.php | 39 +++++++++-- src/Php/Parser/AssignOpTrait.php | 1 + src/Php/Parser/BinaryOpTrait.php | 21 ++++-- tests/aot/float_edge/edge-cases.phpt | 4 -- 15 files changed, 271 insertions(+), 15 deletions(-) create mode 100644 phpunit/code/assign-divide-by-zero.php create mode 100644 phpunit/code/assign-modulo-by-zero.php create mode 100644 phpunit/code/divide-by-zero-float.php create mode 100644 phpunit/code/divide-by-zero-int.php create mode 100644 phpunit/code/divide-by-zero-string.php create mode 100644 phpunit/code/modulo-by-zero-int.php create mode 100644 phpunit/code/native-property-full-name.php create mode 100644 phpunit/code/native-property-private-other-class.php create mode 100644 phpunit/code/native-property-protected-unrelated-class.php create mode 100644 phpunit/src/NativePropertyTest.php create mode 100644 phpunit/src/OperatorTest.php diff --git a/phpunit/code/assign-divide-by-zero.php b/phpunit/code/assign-divide-by-zero.php new file mode 100644 index 00000000..81a41471 --- /dev/null +++ b/phpunit/code/assign-divide-by-zero.php @@ -0,0 +1,7 @@ +value; + } + + public function readInheritedProtected(Target $target): int + { + return $target->protectedValue; + } + + public static function readSelf(): int + { + return self::$count; + } + + public static function readStatic(): int + { + return static::$count; + } + + public static function readParent(): int + { + return parent::$count; + } + } + + function readObject(): int + { + $target = new Target(); + return $target->value; + } + + function readStaticByUse(): int + { + return Target::$count; + } +} + +namespace { + function main(): void + { + $child = new \NativePropSource\Target\Child(); + var_dump($child->readThis()); + var_dump(\NativePropSource\Target\readObject()); + var_dump(\NativePropSource\Target\readStaticByUse()); + var_dump(\NativePropSource\Target\Child::readSelf()); + var_dump(\NativePropSource\Target\Child::readStatic()); + var_dump(\NativePropSource\Target\Child::readParent()); + } +} diff --git a/phpunit/code/native-property-private-other-class.php b/phpunit/code/native-property-private-other-class.php new file mode 100644 index 00000000..d75474f1 --- /dev/null +++ b/phpunit/code/native-property-private-other-class.php @@ -0,0 +1,20 @@ +value; + } +} + +function main(): void +{ +} diff --git a/phpunit/code/native-property-protected-unrelated-class.php b/phpunit/code/native-property-protected-unrelated-class.php new file mode 100644 index 00000000..93a32149 --- /dev/null +++ b/phpunit/code/native-property-protected-unrelated-class.php @@ -0,0 +1,20 @@ +value; + } +} + +function main(): void +{ +} diff --git a/phpunit/src/NativePropertyTest.php b/phpunit/src/NativePropertyTest.php new file mode 100644 index 00000000..6f32781e --- /dev/null +++ b/phpunit/src/NativePropertyTest.php @@ -0,0 +1,40 @@ +addFiles([$testFile]); + $compiler->prepareFile($testFile); + $compiler->convertFile($testFile); + + $this->addToAssertionCount(1); + } + + public function testFindNativePropertyUsesFullClassNameAcrossBranches(): void + { + try { + $this->compile('native-property-full-name.php'); + } catch (TestError $e) { + $this->fail($e->getMessage()); + } + } + + public function testCannotAccessPrivateNativePropertyFromUnrelatedClass(): void + { + $this->exec('Cannot access private property `value` of class `NativePrivateOwner`', 'native-property-private-other-class.php'); + } + + public function testCannotAccessProtectedNativePropertyFromUnrelatedClass(): void + { + $this->exec('Cannot access protected property `value` of class `NativeProtectedOwner`', 'native-property-protected-unrelated-class.php'); + } +} diff --git a/phpunit/src/OperatorTest.php b/phpunit/src/OperatorTest.php new file mode 100644 index 00000000..7bb2d19d --- /dev/null +++ b/phpunit/src/OperatorTest.php @@ -0,0 +1,34 @@ +exec('Cannot divide or modulo by zero', 'divide-by-zero-int.php'); + } + + public function testLiteralFloatDivideByZeroDoesNotCompile(): void + { + $this->exec('Cannot divide or modulo by zero', 'divide-by-zero-float.php'); + } + + public function testLiteralStringDivideByZeroDoesNotCompile(): void + { + $this->exec('Cannot divide or modulo by zero', 'divide-by-zero-string.php'); + } + + public function testLiteralModuloByZeroDoesNotCompile(): void + { + $this->exec('Cannot divide or modulo by zero', 'modulo-by-zero-int.php'); + } + + public function testLiteralDivideAssignByZeroDoesNotCompile(): void + { + $this->exec('Cannot divide or modulo by zero', 'assign-divide-by-zero.php'); + } + + public function testLiteralModuloAssignByZeroDoesNotCompile(): void + { + $this->exec('Cannot divide or modulo by zero', 'assign-modulo-by-zero.php'); + } +} diff --git a/src/Php/CompilerBase.php b/src/Php/CompilerBase.php index 569da39b..8755a759 100644 --- a/src/Php/CompilerBase.php +++ b/src/Php/CompilerBase.php @@ -3866,7 +3866,7 @@ class CompilerBase extends \PhpAot\Core\Translator if ($this->classDef->trait) { goto _dynamic_attr; } - $nativeProperty = $this->findNativeProperty($expr, $propertyName, $this->class, $this->namespace); + $nativeProperty = $this->findNativeProperty($expr, $propertyName, $this->getFullClassName()); } elseif ($this->isTypedObject($objectName)) { $className = $this->getObjectType($objectName); $nativeProperty = $this->findNativeProperty($expr, $propertyName, $className); @@ -4723,7 +4723,7 @@ class CompilerBase extends \PhpAot\Core\Translator if ($this->isNameExpr($expr->class) and $this->isIdExpr($expr->name)) { $class = $this->parseIdentifier($expr->class); $propertyName = $this->parseIdentifier($expr->name); - if ($class === 'self') { + if ($class === 'self' || $class === 'static') { if ($this->classDef->trait) { return Symbol::getStaticProperty() . '(' . Symbol::getCalledCe() . ', ' . $this->getLiteralString($propertyName) . ')'; } @@ -4745,13 +4745,42 @@ class CompilerBase extends \PhpAot\Core\Translator return null; } + protected function isSameClassName(string $classA, string $classB): bool + { + return strcasecmp(ltrim($classA, '\\'), ltrim($classB, '\\')) === 0; + } + + protected function isSameOrSubclassOf(string $class, string $parent): bool + { + $class = strtolower(ltrim($class, '\\')); + $parent = strtolower(ltrim($parent, '\\')); + while ($class !== '') { + if ($class === $parent) { + return true; + } + $class = $this->classExtends[$class] ?? ''; + } + return false; + } + + protected function canAccessProtectedProperty(string $scope, string $declaringClass): bool + { + if ($scope === '') { + return false; + } + return $this->isSameOrSubclassOf($scope, $declaringClass) + || $this->isSameOrSubclassOf($declaringClass, $scope); + } + /** * @param NodeAbstract $expr 仅用于输出错误日志 + * @param string $class 必须传入带有完整命名空间的类名 */ protected function findNativeProperty(NodeAbstract $expr, string $property, string $class, bool $static = false): ?string { + $class = ltrim($class, '\\'); $findClass = $class; - $scope = $this->class ? $class : ''; + $scope = $this->class ? $this->getFullClassName() : ''; $propertyDef = null; $classDef = null; while (true) { @@ -4769,13 +4798,13 @@ class CompilerBase extends \PhpAot\Core\Translator break; } if ($propertyDef->isProtected()) { - if ($scope) { + if ($this->canAccessProtectedProperty($scope, $findClass)) { break; } $displayClass = ltrim($class, '\\'); $this->fatalError($expr, "Cannot access protected property `{$property}` of class `{$displayClass}`"); } else { - if ($scope === $findClass) { + if ($this->isSameClassName($scope, $findClass)) { break; } $displayClass = ltrim($class, '\\'); diff --git a/src/Php/Parser/AssignOpTrait.php b/src/Php/Parser/AssignOpTrait.php index 4ffae7e7..8286bca2 100644 --- a/src/Php/Parser/AssignOpTrait.php +++ b/src/Php/Parser/AssignOpTrait.php @@ -323,6 +323,7 @@ trait AssignOpTrait $var = $this->parseIdentifier($node->var); $this->context->inAssignExpr = $oriInAssignExpr; $expr = $this->parseIdentifier($node->expr); + $this->guardLiteralDivisionByZero($node->expr, $op); if ($this->isVarExpr($node->var)) { if (!$this->hasVar($var)) { diff --git a/src/Php/Parser/BinaryOpTrait.php b/src/Php/Parser/BinaryOpTrait.php index 9b3ece94..3a776444 100644 --- a/src/Php/Parser/BinaryOpTrait.php +++ b/src/Php/Parser/BinaryOpTrait.php @@ -121,9 +121,7 @@ trait BinaryOpTrait $leftExpr = $this->convertExprType($leftExpr, $leftType, self::TYPE_FLOAT); } - if (($op === '/' or $op === '%') and $this->isZeroLiteral($right)) { - $this->fatalError($right, 'Cannot divide or modulo by zero'); - } + $this->guardLiteralDivisionByZero($right, $op); if ($op === '%' and !($leftType === self::TYPE_INT and $rightType === self::TYPE_INT)) { return 'php::fn::mod(' . $leftExpr . ', ' . $rightExpr . ')'; @@ -357,16 +355,27 @@ trait BinaryOpTrait return $this->parseBinaryOp($expr->left, $expr->right, '/'); } - private function isZeroLiteral(NodeAbstract $expr): bool + protected function guardLiteralDivisionByZero(NodeAbstract $right, string $op): void + { + if (($op === '/' or $op === '%' or $op === '/=' or $op === '%=') and $this->isZeroLiteral($right)) { + $this->fatalError($right, 'Cannot divide or modulo by zero'); + } + } + + protected function isZeroLiteral(NodeAbstract $expr): bool { if ($expr instanceof Node\Scalar\Int_) { - return $expr->value == 0; + return $expr->value === 0; } if ($expr instanceof Node\Scalar\Float_) { return $expr->value == 0.0; } + if ($expr instanceof Expr\UnaryMinus || $expr instanceof Expr\UnaryPlus) { + return $this->isZeroLiteral($expr->expr); + } if ($expr instanceof Node\Scalar\String_) { - return $expr->value === '0' || $expr->value === '0.0'; + $value = trim($expr->value); + return $value !== '' && is_numeric($value) && (float) $value == 0.0; } return false; } diff --git a/tests/aot/float_edge/edge-cases.phpt b/tests/aot/float_edge/edge-cases.phpt index 662325c1..e14b1c1e 100644 --- a/tests/aot/float_edge/edge-cases.phpt +++ b/tests/aot/float_edge/edge-cases.phpt @@ -18,8 +18,6 @@ function main(): void { echo -INF . "\n"; var_dump(INF + INF); var_dump(INF / INF); - var_dump(0.0 / 0.0); - var_dump(1.0 / 0.0); } ?> --EXPECT-- @@ -38,5 +36,3 @@ INF -INF float(INF) float(NAN) -float(NAN) -float(NAN)