diff --git a/phpunit/code/accessibility/private-prop-in-parent.php b/phpunit/code/accessibility/private-prop-in-parent.php new file mode 100644 index 00000000..0a044bfe --- /dev/null +++ b/phpunit/code/accessibility/private-prop-in-parent.php @@ -0,0 +1,18 @@ +prop); + } +} +class User extends Base { + public $prop; +} + +function main() { + $u = new User(); + $u->prop = 12; + var_dump($u); + $u->dump(); +} diff --git a/phpunit/code/accessibility/private-prop-in-trait.php b/phpunit/code/accessibility/private-prop-in-trait.php new file mode 100644 index 00000000..d94013c7 --- /dev/null +++ b/phpunit/code/accessibility/private-prop-in-trait.php @@ -0,0 +1,18 @@ +prop = 12; + var_dump($u); +} diff --git a/phpunit/code/internal-void-function-assignment.php b/phpunit/code/internal-void-function-assignment.php new file mode 100644 index 00000000..1339725c --- /dev/null +++ b/phpunit/code/internal-void-function-assignment.php @@ -0,0 +1,6 @@ +child = new NullsafeNestedChild(); + } +} + +class NullsafeNestedChild +{ + private int $value = 1; +} + +function main(): void +{ + $owner = new NullsafeNestedOwner(); + var_dump($owner?->child?->value); +} diff --git a/phpunit/code/nullsafe-private-property.php b/phpunit/code/nullsafe-private-property.php new file mode 100644 index 00000000..4ed46c0e --- /dev/null +++ b/phpunit/code/nullsafe-private-property.php @@ -0,0 +1,14 @@ +value); +} diff --git a/phpunit/code/nullsafe-write-assign-op.php b/phpunit/code/nullsafe-write-assign-op.php new file mode 100644 index 00000000..81727455 --- /dev/null +++ b/phpunit/code/nullsafe-write-assign-op.php @@ -0,0 +1,12 @@ +value += 2; +} diff --git a/phpunit/code/nullsafe-write-assign-ref-left.php b/phpunit/code/nullsafe-write-assign-ref-left.php new file mode 100644 index 00000000..9eeac740 --- /dev/null +++ b/phpunit/code/nullsafe-write-assign-ref-left.php @@ -0,0 +1,13 @@ +value =& $value; +} diff --git a/phpunit/code/nullsafe-write-assign-ref-right.php b/phpunit/code/nullsafe-write-assign-ref-right.php new file mode 100644 index 00000000..9e5a3c44 --- /dev/null +++ b/phpunit/code/nullsafe-write-assign-ref-right.php @@ -0,0 +1,12 @@ +value; +} diff --git a/phpunit/code/nullsafe-write-assign.php b/phpunit/code/nullsafe-write-assign.php new file mode 100644 index 00000000..c6c66f66 --- /dev/null +++ b/phpunit/code/nullsafe-write-assign.php @@ -0,0 +1,12 @@ +value = 2; +} diff --git a/phpunit/code/nullsafe-write-inc.php b/phpunit/code/nullsafe-write-inc.php new file mode 100644 index 00000000..25725f27 --- /dev/null +++ b/phpunit/code/nullsafe-write-inc.php @@ -0,0 +1,12 @@ +value++; +} diff --git a/phpunit/code/nullsafe-write-unset.php b/phpunit/code/nullsafe-write-unset.php new file mode 100644 index 00000000..77ee238c --- /dev/null +++ b/phpunit/code/nullsafe-write-unset.php @@ -0,0 +1,12 @@ +value); +} diff --git a/phpunit/src/FunctionTest.php b/phpunit/src/FunctionTest.php index 0881e86c..e547d5cb 100644 --- a/phpunit/src/FunctionTest.php +++ b/phpunit/src/FunctionTest.php @@ -77,4 +77,14 @@ class FunctionTest extends \BaseTest $this->exec('OptionalBeforeRequired::method(): optional parameter `$first` cannot be declared before required parameter `$second`', 'method-optional-before-required-param.php'); } + public function testInternalVoidFunctionCannotBeAssigned() + { + $this->exec('Cannot use void expression as assignment value', 'internal-void-function-assignment.php'); + } + + public function testInternalVoidFunctionCannotBeUsedAsBinaryOperand() + { + $this->exec('Cannot use void expression as binary operand', 'internal-void-function-binary-operand.php'); + } + } diff --git a/phpunit/src/InheritanceErrorTest.php b/phpunit/src/InheritanceErrorTest.php index dcc38a33..8731232a 100644 --- a/phpunit/src/InheritanceErrorTest.php +++ b/phpunit/src/InheritanceErrorTest.php @@ -138,9 +138,19 @@ class InheritanceErrorTest extends TestCase $this->assertCompiles('inheritance_prop_visibility_widen.php'); } - public function testPrivateParentPropertyMayBeRedeclared() + public function testPrivateParentPropertyCannotBeRedeclared() { - $this->assertCompiles('inheritance_private_prop_redeclare.php'); + $this->exec('property shadowing across inheritance is not allowed', 'inheritance_private_prop_redeclare.php'); + } + + public function testPrivateParentPropertyCannotBeShadowedByPublicProperty() + { + $this->exec('property shadowing across inheritance is not allowed', 'accessibility/private-prop-in-parent.php'); + } + + public function testPrivateTraitPropertyCannotBeShadowedByPublicProperty() + { + $this->exec('property shadowing across inheritance is not allowed', 'accessibility/private-prop-in-trait.php'); } public function testConstantTypeMismatch() diff --git a/phpunit/src/NativePropertyTest.php b/phpunit/src/NativePropertyTest.php index b01b6b80..11c31544 100644 --- a/phpunit/src/NativePropertyTest.php +++ b/phpunit/src/NativePropertyTest.php @@ -51,4 +51,44 @@ class NativePropertyTest extends \BaseTest { $this->exec('Cannot access protected property `value` of class `NativeProtectedOwner`', 'native-property-protected-unrelated-class.php'); } + + public function testCannotAccessPrivateNativePropertyThroughNullsafe(): void + { + $this->exec('Cannot access private property `value` of class `NullsafePrivateOwner`', 'nullsafe-private-property.php'); + } + + public function testCannotAccessNestedPrivateNativePropertyThroughNullsafe(): void + { + $this->exec('Cannot access private property `value` of class `NullsafeNestedChild`', 'nullsafe-nested-private-property.php'); + } + + public function testCannotAssignThroughNullsafeProperty(): void + { + $this->exec("Can't use nullsafe operator in write context", 'nullsafe-write-assign.php'); + } + + public function testCannotUseCompoundAssignThroughNullsafeProperty(): void + { + $this->exec("Can't use nullsafe operator in write context", 'nullsafe-write-assign-op.php'); + } + + public function testCannotIncrementThroughNullsafeProperty(): void + { + $this->exec("Can't use nullsafe operator in write context", 'nullsafe-write-inc.php'); + } + + public function testCannotUnsetThroughNullsafeProperty(): void + { + $this->exec("Can't use nullsafe operator in write context", 'nullsafe-write-unset.php'); + } + + public function testCannotAssignReferenceToNullsafeProperty(): void + { + $this->exec("Can't use nullsafe operator in write context", 'nullsafe-write-assign-ref-left.php'); + } + + public function testCannotTakeReferenceOfNullsafeProperty(): void + { + $this->exec('Cannot take reference of a nullsafe chain', 'nullsafe-write-assign-ref-right.php'); + } } diff --git a/src/Php/CompilerBase.php b/src/Php/CompilerBase.php index 9bd09c47..e4a6d079 100644 --- a/src/Php/CompilerBase.php +++ b/src/Php/CompilerBase.php @@ -2768,6 +2768,7 @@ class CompilerBase extends \PhpAot\Core\Translator protected function parsePreInc(Expr\PreInc $expr): string { + $this->assertNotNullsafeWriteContext($expr->var); $oriInAssignExpr = $this->context->inAssignExpr; $this->context->inAssignExpr = true; @@ -3825,6 +3826,7 @@ class CompilerBase extends \PhpAot\Core\Translator protected function parsePostOp(Expr\PostDec|Expr\PostInc $expr, string $op): string { + $this->assertNotNullsafeWriteContext($expr->var); $result = $this->genDynamicPropIncDec($expr->var, $op, false); if ($result !== null) { return $result; @@ -4030,6 +4032,7 @@ class CompilerBase extends \PhpAot\Core\Translator protected function parsePreDec(Expr\PreDec $expr): string { + $this->assertNotNullsafeWriteContext($expr->var); $oriInAssignExpr = $this->context->inAssignExpr; $this->context->inAssignExpr = true; @@ -4792,6 +4795,7 @@ class CompilerBase extends \PhpAot\Core\Translator $vars = $node->vars; $lines = []; foreach ($vars as $var) { + $this->assertNotNullsafeWriteContext($var); if ($this->isArrayDimFetch($var)) { if ($var->dim === null) { $this->fatalError($var, 'Cannot use [] for array unset'); @@ -5365,11 +5369,19 @@ class CompilerBase extends \PhpAot\Core\Translator */ protected function checkLeftValue(NodeAbstract $expr): void { + $this->assertNotNullsafeWriteContext($expr); if (!$this->isVarExpr($expr) && !$this->isArrayDimFetch($expr) && !$this->isPropertyFetch($expr) && !$this->isStaticPropertyFetch($expr)) { $this->fatalError($expr, 'The left value of assignment operation can only be variable, array item, object property, class static property'); } } + protected function assertNotNullsafeWriteContext(NodeAbstract $expr): void + { + if ($expr instanceof Expr\NullsafePropertyFetch) { + $this->fatalError($expr, "Can't use nullsafe operator in write context"); + } + } + protected function getChainedFunc(string $op): string { return match ($op) { @@ -5477,9 +5489,7 @@ class CompilerBase extends \PhpAot\Core\Translator { if ($this->isInternalFunction($name)) { $returnType = Reflection::getFunctionReturnType($name); - // void 类型将被忽略,类型推测仅用于赋值操作的右值,即使返回值为 void , 赋值操作也应该继续运行,右值会被当做 null - // 例如 $a = var_dump('hello'); 虽然 var_dump 返回值为 void ,但是 $a 的类型是 mixed,值为 null - if ($returnType and $returnType !== 'void') { + if ($returnType) { return $this->getTypeFromZendType($returnType); } } @@ -5489,7 +5499,7 @@ class CompilerBase extends \PhpAot\Core\Translator protected function detectMethodCallReturnType(string $class, string $method): string { $returnType = Reflection::getMethodReturnType($class, $method); - if ($returnType and $returnType !== 'void') { + if ($returnType) { return $this->getTypeFromZendType($returnType); } return self::TYPE_VAR; @@ -6774,6 +6784,17 @@ class CompilerBase extends \PhpAot\Core\Translator return $beforeCode . PHP_EOL . $code . ';' . PHP_EOL . 'return ' . self::VALUE_NULL . ';'; } } + if ($this->detectTypeOfExpr($expr->expr) === self::TYPE_VOID) { + if ($this->context->closureReturnTypeCheck) { + $tmpVar = $this->genTmpVarName(); + $this->addLocalVar($tmpVar, self::TYPE_VAR); + return $beforeCode . PHP_EOL . $code . ';' . PHP_EOL + . $tmpVar . ' = ' . self::VALUE_NULL . ';' . PHP_EOL + . $this->genClosureReturnCheck($tmpVar) + . $this->getIndent() . 'return ' . $tmpVar . ';'; + } + return $beforeCode . PHP_EOL . $code . ';' . PHP_EOL . 'return ' . self::VALUE_NULL . ';'; + } if ($this->context->closureReturnTypeCheck) { $tmpVar = $this->genTmpVarName(); $this->addLocalVar($tmpVar, self::TYPE_VAR); @@ -6829,7 +6850,7 @@ class CompilerBase extends \PhpAot\Core\Translator while (1) { if ($expr instanceof Expr\NullsafePropertyFetch) { - $list[] = ['property', $this->identifierToStr($expr->name, literal: true)]; + $list[] = ['property', $this->identifierToStr($expr->name, literal: true), $expr]; $expr = $expr->var; } elseif ($expr instanceof Expr\NullsafeMethodCall) { $list[] = ['method', $this->identifierToStr($expr->name, literal: true), $expr->args]; @@ -6852,6 +6873,7 @@ class CompilerBase extends \PhpAot\Core\Translator } $list = array_reverse($list); + $this->checkNullsafePropertyAccesses($expr, $list); $last = array_key_last($list); $tmpFn = $this->genTmpVarName(); @@ -6886,6 +6908,42 @@ class CompilerBase extends \PhpAot\Core\Translator return "{$tmpFn}()"; } + private function checkNullsafePropertyAccesses(NodeAbstract $baseExpr, array $list): void + { + $className = $this->detectClassOfExpr($baseExpr); + if ($className === '') { + return; + } + + foreach ($list as $item) { + if ($item[0] !== 'property') { + $className = ''; + continue; + } + + /** @var Expr\NullsafePropertyFetch $node */ + $node = $item[2]; + if (!$this->isIdExpr($node->name)) { + $className = ''; + continue; + } + + $property = $this->parseIdentifier($node->name); + $this->findNativeProperty($node, $property, $className); + if (!$node->hasAttribute('nativePropertyDef')) { + $className = ''; + continue; + } + + /** @var PropertyDef $def */ + $def = $node->getAttribute('nativePropertyDef'); + $className = $def->type === self::TYPE_OBJECT ? $def->class : ''; + if ($className === '') { + return; + } + } + } + protected function parseFullyQualifiedName(Node\Name\FullyQualified $expr): string { return $expr->name; diff --git a/src/Php/Parser/AssignOpTrait.php b/src/Php/Parser/AssignOpTrait.php index 1db08b7d..eab77ac8 100644 --- a/src/Php/Parser/AssignOpTrait.php +++ b/src/Php/Parser/AssignOpTrait.php @@ -143,6 +143,7 @@ trait AssignOpTrait protected function parseAssignFinally(Expr $left, Expr $right): string { + $this->assertNotNullsafeWriteContext($left); if ($left instanceof Expr\List_) { return $this->parseAssignToList($left, $right); } @@ -325,6 +326,7 @@ trait AssignOpTrait protected function parseAssignOp(Expr\AssignOp $node, string $op): string { + $this->assertNotNullsafeWriteContext($node->var); $oriInAssignExpr = $this->context->inAssignExpr; $this->context->inAssignExpr = true; $var = $this->parseIdentifier($node->var); @@ -521,6 +523,11 @@ trait AssignOpTrait protected function parseAssignRef(Expr\AssignRef $expr): string { + $this->assertNotNullsafeWriteContext($expr->var); + if ($expr->expr instanceof Expr\NullsafePropertyFetch) { + $this->fatalError($expr->expr, 'Cannot take reference of a nullsafe chain'); + } + $this->context->inAssignExpr = true; $left = $this->parseIdentifier($expr->var); $this->context->inAssignExpr = false; diff --git a/src/Php/Translator.php b/src/Php/Translator.php index 2eef2ce0..3cf325a5 100644 --- a/src/Php/Translator.php +++ b/src/Php/Translator.php @@ -3441,7 +3441,9 @@ CODE; if ($chainNode->hasProperty($name)) { $parentProp = $chainNode->getProperty($name); if ($parentProp->flags & Modifiers::PRIVATE) { - continue; + $this->fatalError($classStmt, + "Declaration of `{$className}::\${$name}` conflicts with private property " . + "`{$parentClass}::\${$name}`; property shadowing across inheritance is not allowed"); } if ($childProp->type !== $parentProp->type || $childProp->class !== $parentProp->class) { $this->fatalError($classStmt,