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 writeStatic(int $value): int + { + static::$count = $value; + 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::writeStatic(4)); + 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..b01b6b80 --- /dev/null +++ b/phpunit/src/NativePropertyTest.php @@ -0,0 +1,54 @@ +addFiles([$testFile]); + $compiler->prepareFile($testFile); + $compiler->convertFile($testFile); + + $this->addToAssertionCount(1); + return ROOT_PATH . '/build/phpunit/code/' . basename($file, '.php') . '.cc'; + } + + public function testFindNativePropertyUsesFullClassNameAcrossBranches(): void + { + try { + $this->compile('native-property-full-name.php'); + } catch (TestError $e) { + $this->fail($e->getMessage()); + } + } + + public function testStaticStaticPropertyUsesDynamicCalledClassPath(): void + { + try { + $outputFile = $this->compile('native-property-full-name.php'); + } catch (TestError $e) { + $this->fail($e->getMessage()); + } + + $code = file_get_contents($outputFile); + $this->assertStringContainsString('php::getStaticProperty(php_get_called_class(this_), "count")', $code); + $this->assertStringContainsString('php::getStaticProperty(php_get_called_class(this_), "count") = php::toInt(value)', $code); + } + + 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 0172422f..3257b200 100644 --- a/src/Php/CompilerBase.php +++ b/src/Php/CompilerBase.php @@ -143,6 +143,7 @@ class CompilerBase extends \PhpAot\Core\Translator public const string DYNAMIC_CALLED_CLASS = '__dynamic_called_class__'; public const string STATIC_VAR = '_static_var_'; public const string GLOBAL_VAR = '_global_var_'; + public const string CONST_VAR = '_const_var_'; public const string OBJECT_PROP = '_object_prop_'; public const string CLASS_MAP = 'class_map'; public const string FUNC_MAP = 'func_map'; @@ -3865,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); @@ -4722,6 +4723,9 @@ 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 === 'static') { + return null; + } if ($class === 'self') { if ($this->classDef->trait) { return Symbol::getStaticProperty() . '(' . Symbol::getCalledCe() . ', ' . $this->getLiteralString($propertyName) . ')'; @@ -4732,8 +4736,10 @@ class CompilerBase extends \PhpAot\Core\Translator $this->fatalError($expr, 'Cannot access parent:: when current class does not extend any class'); } $class = $this->classDef->extends; + } else { + $class = $this->getNamespacedClassName($class); } - $nativeProperty = $this->findNativeProperty($expr, $propertyName, $class, $this->namespace, true); + $nativeProperty = $this->findNativeProperty($expr, $propertyName, $class, true); if ($nativeProperty) { $expr->setAttribute('nativeProperty', $nativeProperty); return $nativeProperty; @@ -4742,16 +4748,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, string $namespace = '', bool $static = false): ?string + protected function findNativeProperty(NodeAbstract $expr, string $property, string $class, bool $static = false): ?string { + $class = ltrim($class, '\\'); $findClass = $class; - if ($namespace) { - $findClass = $namespace . '\\' . $class; - } - $scope = $this->class ? ltrim($namespace . '\\' . $class, '\\') : ''; + $scope = $this->class ? $this->getFullClassName() : ''; $propertyDef = null; $classDef = null; while (true) { @@ -4769,13 +4801,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, '\\'); @@ -5022,22 +5054,22 @@ class CompilerBase extends \PhpAot\Core\Translator $constInfo->type = $this->detectStrValueType($value); $constInfo->namespace = $this->namespace; $constInfo->name = $name; - $this->constants[$this->escapeNamespace($name)] = $constInfo; + $this->constants[$this->escapeConstVar($name)] = $constInfo; } protected function hasConstant(string $name): bool { - return isset($this->constants[$this->escapeNamespace($name)]); + return isset($this->constants[$this->escapeConstVar($name)]); } protected function getConstant(string $name): string { - return $this->escapeNamespace($name); + return $this->escapeConstVar($name); } protected function getConstantType(string $name): string { - return $this->constants[$this->escapeNamespace($name)]->type; + return $this->constants[$this->escapeConstVar($name)]->type; } protected function detectStrValueType(mixed $constant): string diff --git a/src/Php/Generator/Utils.php b/src/Php/Generator/Utils.php index 89391995..284bcf8f 100644 --- a/src/Php/Generator/Utils.php +++ b/src/Php/Generator/Utils.php @@ -95,6 +95,11 @@ trait Utils return self::GLOBAL_VAR . $name; } + protected function escapeConstVar(string $name): string + { + return self::CONST_VAR . str_replace('\\', self::NAMESPACE_SEPARATOR, $name); + } + protected function escapeNamespace(string $ns): string { return str_replace('\\', self::NAMESPACE_SEPARATOR, strtolower($ns)); 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 c69f639a..3a776444 100644 --- a/src/Php/Parser/BinaryOpTrait.php +++ b/src/Php/Parser/BinaryOpTrait.php @@ -121,6 +121,8 @@ trait BinaryOpTrait $leftExpr = $this->convertExprType($leftExpr, $leftType, self::TYPE_FLOAT); } + $this->guardLiteralDivisionByZero($right, $op); + if ($op === '%' and !($leftType === self::TYPE_INT and $rightType === self::TYPE_INT)) { return 'php::fn::mod(' . $leftExpr . ', ' . $rightExpr . ')'; } @@ -353,6 +355,31 @@ trait BinaryOpTrait return $this->parseBinaryOp($expr->left, $expr->right, '/'); } + 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; + } + 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_) { + $value = trim($expr->value); + return $value !== '' && is_numeric($value) && (float) $value == 0.0; + } + return false; + } + protected function parseBinaryOpMinus(Expr\BinaryOp\Minus $expr): string { return $this->parseBinaryOp($expr->left, $expr->right, '-'); diff --git a/tests/aot/basic/update-object-arg.phpt b/tests/aot/basic/object-arg.phpt similarity index 100% rename from tests/aot/basic/update-object-arg.phpt rename to tests/aot/basic/object-arg.phpt diff --git a/tests/aot/const/const-same-name.phpt b/tests/aot/const/const-same-name.phpt new file mode 100644 index 00000000..8a024fa3 --- /dev/null +++ b/tests/aot/const/const-same-name.phpt @@ -0,0 +1,15 @@ +--TEST-- +Constant with the same name (case-insensitive) resolve independently +--FILE-- + +--EXPECT-- +int(123) +int(456) diff --git a/tests/aot/const/const-var-same-name.phpt b/tests/aot/const/const-var-same-name.phpt new file mode 100644 index 00000000..28bf1a9c --- /dev/null +++ b/tests/aot/const/const-var-same-name.phpt @@ -0,0 +1,15 @@ +--TEST-- +Constant and variable with the same name (case-insensitive) resolve independently +--FILE-- + +--EXPECT-- +int(123) +int(456) diff --git a/tests/aot/float_edge/edge-cases.phpt b/tests/aot/float_edge/edge-cases.phpt index 1afab394..e14b1c1e 100644 --- a/tests/aot/float_edge/edge-cases.phpt +++ b/tests/aot/float_edge/edge-cases.phpt @@ -18,7 +18,6 @@ function main(): void { echo -INF . "\n"; var_dump(INF + INF); var_dump(INF / INF); - var_dump(0.0 / 0.0); } ?> --EXPECT-- @@ -37,4 +36,3 @@ INF -INF float(INF) float(NAN) -float(NAN) diff --git a/tests/aot/namespace/ns-same-as-class.phpt b/tests/aot/namespace/ns-same-as-class.phpt new file mode 100644 index 00000000..9e2fbd7b --- /dev/null +++ b/tests/aot/namespace/ns-same-as-class.phpt @@ -0,0 +1,52 @@ +--TEST-- +use class A\B (without alias) inside namespace A\B, call B::$v +--FILE-- + +--EXPECT-- +int(123) +int(123) +int(123) +int(123) diff --git a/tests/aot/static/static-prop-late-static-binding.phpt b/tests/aot/static/static-prop-late-static-binding.phpt new file mode 100644 index 00000000..573bdfa3 --- /dev/null +++ b/tests/aot/static/static-prop-late-static-binding.phpt @@ -0,0 +1,39 @@ +--TEST-- +Static native properties use late static binding for static::$prop +--FILE-- + +--EXPECT-- +int(1) +int(2) +int(1) +int(9) +int(9)