fix(parser): 解决空安全操作符在写入上下文中的使用问题

- 在赋值操作中添加空安全操作符检查,防止在写入上下文中使用
- 修复前置递增、后置操作、递减和unset操作中的空安全检查
- 添加对空安全属性访问的类型检测和错误处理
- 禁止对void函数调用结果进行赋值或二元运算
- 修复继承中私有属性重声明的错误处理逻辑
- 添加空安全写入操作的相关测试用例
pull/5/head
韩天峰 2 months ago
parent 5027650611
commit 64cf047f07
  1. 18
      phpunit/code/accessibility/private-prop-in-parent.php
  2. 18
      phpunit/code/accessibility/private-prop-in-trait.php
  3. 6
      phpunit/code/internal-void-function-assignment.php
  4. 6
      phpunit/code/internal-void-function-binary-operand.php
  5. 24
      phpunit/code/nullsafe-nested-private-property.php
  6. 14
      phpunit/code/nullsafe-private-property.php
  7. 12
      phpunit/code/nullsafe-write-assign-op.php
  8. 13
      phpunit/code/nullsafe-write-assign-ref-left.php
  9. 12
      phpunit/code/nullsafe-write-assign-ref-right.php
  10. 12
      phpunit/code/nullsafe-write-assign.php
  11. 12
      phpunit/code/nullsafe-write-inc.php
  12. 12
      phpunit/code/nullsafe-write-unset.php
  13. 10
      phpunit/src/FunctionTest.php
  14. 14
      phpunit/src/InheritanceErrorTest.php
  15. 40
      phpunit/src/NativePropertyTest.php
  16. 68
      src/Php/CompilerBase.php
  17. 7
      src/Php/Parser/AssignOpTrait.php
  18. 4
      src/Php/Translator.php

@ -0,0 +1,18 @@
<?php
class Base {
private $prop = 999;
public function dump()
{
var_dump($this->prop);
}
}
class User extends Base {
public $prop;
}
function main() {
$u = new User();
$u->prop = 12;
var_dump($u);
$u->dump();
}

@ -0,0 +1,18 @@
<?php
trait TestTraitProp {
private $prop = 999;
}
class Base {
use TestTraitProp;
}
class User extends Base {
public $prop;
}
function main() {
$u = new User();
$u->prop = 12;
var_dump($u);
}

@ -0,0 +1,6 @@
<?php
function main(): void
{
$value = var_dump('hello');
}

@ -0,0 +1,6 @@
<?php
function main(): void
{
$value = var_dump('hello') + 1;
}

@ -0,0 +1,24 @@
<?php
use native_types;
class NullsafeNestedOwner
{
public NullsafeNestedChild $child;
public function __construct()
{
$this->child = new NullsafeNestedChild();
}
}
class NullsafeNestedChild
{
private int $value = 1;
}
function main(): void
{
$owner = new NullsafeNestedOwner();
var_dump($owner?->child?->value);
}

@ -0,0 +1,14 @@
<?php
use native_types;
class NullsafePrivateOwner
{
private int $value = 1;
}
function main(): void
{
$owner = new NullsafePrivateOwner();
var_dump($owner?->value);
}

@ -0,0 +1,12 @@
<?php
class NullsafeWriteAssignOp
{
public int $value = 1;
}
function main(): void
{
$object = new NullsafeWriteAssignOp();
$object?->value += 2;
}

@ -0,0 +1,13 @@
<?php
class NullsafeWriteAssignRefLeft
{
public int $value = 1;
}
function main(): void
{
$object = new NullsafeWriteAssignRefLeft();
$value = 2;
$object?->value =& $value;
}

@ -0,0 +1,12 @@
<?php
class NullsafeWriteAssignRefRight
{
public int $value = 1;
}
function main(): void
{
$object = new NullsafeWriteAssignRefRight();
$value =& $object?->value;
}

@ -0,0 +1,12 @@
<?php
class NullsafeWriteAssign
{
public int $value = 1;
}
function main(): void
{
$object = new NullsafeWriteAssign();
$object?->value = 2;
}

@ -0,0 +1,12 @@
<?php
class NullsafeWriteInc
{
public int $value = 1;
}
function main(): void
{
$object = new NullsafeWriteInc();
$object?->value++;
}

@ -0,0 +1,12 @@
<?php
class NullsafeWriteUnset
{
public int $value = 1;
}
function main(): void
{
$object = new NullsafeWriteUnset();
unset($object?->value);
}

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

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

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

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

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

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

Loading…
Cancel
Save