fix: preserve object property reference semantics

master
韩天峰 10 hours ago
parent e0c1298798
commit e8927bade0
  1. 27
      src/Parser/AssignOpTrait.php
  2. 170
      tests/compiler/ref/property-reference-rebind.phpt

@ -1303,7 +1303,26 @@ trait AssignOpTrait
$this->assertReadonlyPropertyReferenceForbidden($expr->var, $expr, true);
$this->assertReadonlyPropertyReferenceForbidden($expr->expr, $expr, false);
$left = $this->parseWritableIdentifier($expr->var);
$propertyReferenceTarget = null;
$nativeObjectProperty = $expr->var instanceof Expr\PropertyFetch
&& $this->getNativePropertyClassDef($expr->var)?->nativeObject === true;
if ($expr->var instanceof Expr\PropertyFetch && !$nativeObjectProperty) {
// A property reference assignment must go through Zend's property
// metadata path. A plain Variant indirect slot cannot maintain
// typed-property reference sources safely. Parse the complete LHS
// before the RHS so PHP's source evaluation order is preserved.
$object = $this->parseOrderedOperand($expr->var->var, false);
$member = $this->isIdExpr($expr->var->name)
? $this->propertyNameToStr($expr->var->name, literal: true)
: $this->parseOrderedOperand($expr->var->name, false, true);
$scope = $this->usesTraitPropertyScope($object)
? 'php::FakeScopeGuard::current()'
: ($this->class ? $this->getClassEntryPtr($this->getFullClassName()) : 'nullptr');
$propertyReferenceTarget = [$object, $member, $scope];
$left = '';
} else {
$left = $this->parseWritableIdentifier($expr->var);
}
// Keep this write-context form for every RHS kind. Re-parsing it as a
// read later breaks append and missing-key targets such as
// `$array[] =& $source`.
@ -1397,8 +1416,10 @@ trait AssignOpTrait
}
$this->context->beforeStmtLines[] = $rightExpr . ';';
if ($expr->var instanceof Expr\PropertyFetch && $this->isNativePropertyAccess($expr->var)) {
return $left . '.rebindReference(' . $tmpVar . ')';
if ($propertyReferenceTarget !== null) {
[$object, $member, $scope] = $propertyReferenceTarget;
return 'typephp_rebind_property_reference('
. $object . ', ' . $member . ', ' . $tmpVar . ', ' . $scope . ')';
}
return $left . ' = &' . $tmpVar;
}

@ -0,0 +1,170 @@
--TEST--
Object property reference assignment preserves aliases and typed-property sources
--FILE--
<?php
declare(strict_types=1);
final class PropertyReferenceHolder
{
public ?array $value = null;
}
final class PrivatePropertyReferenceHolder
{
private ?array $value = null;
public function bind(array &$source): void
{
$this->value = &$source;
}
public function value(): ?array
{
return $this->value;
}
}
final class PropertyReferenceDep
{
public ?array $map = null;
}
function bindObjectProperty(object $holder, mixed &$source): void
{
$holder->value = &$source;
}
function replaceReference(mixed &$target, mixed $value): void
{
$target = $value;
}
function propertyReferenceTarget(array &$events, object $holder): object
{
$events[] = 'object';
return $holder;
}
function propertyReferenceName(array &$events): string
{
$events[] = 'property';
return 'value';
}
function &propertyReferenceSource(array &$events, mixed &$source): mixed
{
$events[] = 'source';
return $source;
}
function trackThroughSplObjectStorage(object $target): PropertyReferenceDep
{
$storage = new SplObjectStorage();
$storage->offsetSet($target, []);
$map = $storage[$target];
$dep = new PropertyReferenceDep();
$dep->map = &$map;
$map['dep'] = $dep;
$storage->offsetSet($target, $map);
return $dep;
}
function main(): void
{
$holder = new PropertyReferenceHolder();
$source = ['initial' => 1];
$holder->value = &$source;
$source['source'] = 2;
$holder->value['property'] = 3;
var_dump($source);
$dynamicHolder = new PropertyReferenceHolder();
$dynamicSource = [];
bindObjectProperty($dynamicHolder, $dynamicSource);
$dynamicSource['dynamic'] = true;
var_dump($dynamicHolder->value);
$wrong = 'invalid';
try {
bindObjectProperty($dynamicHolder, $wrong);
echo "missing initial TypeError\n";
} catch (TypeError $error) {
echo "initial TypeError\n";
}
$dynamicSource['preserved'] = true;
var_dump($dynamicHolder->value);
$replacement = ['replacement' => true];
$holder->value = &$replacement;
replaceReference($source, 'detached');
var_dump($source);
try {
replaceReference($replacement, 'invalid');
echo "missing write TypeError\n";
} catch (TypeError $error) {
echo "write TypeError\n";
}
var_dump($holder->value);
$privateSource = [];
$privateHolder = new PrivatePropertyReferenceHolder();
$privateHolder->bind($privateSource);
$privateSource['private'] = true;
var_dump($privateHolder->value());
$events = [];
$orderedSource = [];
$orderedHolder = new PropertyReferenceHolder();
propertyReferenceTarget($events, $orderedHolder)->{propertyReferenceName($events)}
= &propertyReferenceSource($events, $orderedSource);
$orderedSource['ordered'] = true;
var_dump($events, $orderedHolder->value);
$dep = trackThroughSplObjectStorage(new stdClass());
var_dump(isset($dep->map['dep']));
}
?>
--EXPECT--
array(3) {
["initial"]=>
int(1)
["source"]=>
int(2)
["property"]=>
int(3)
}
array(1) {
["dynamic"]=>
bool(true)
}
initial TypeError
array(2) {
["dynamic"]=>
bool(true)
["preserved"]=>
bool(true)
}
string(8) "detached"
write TypeError
array(1) {
["replacement"]=>
bool(true)
}
array(1) {
["private"]=>
bool(true)
}
array(3) {
[0]=>
string(6) "object"
[1]=>
string(8) "property"
[2]=>
string(6) "source"
}
array(1) {
["ordered"]=>
bool(true)
}
bool(true)
Loading…
Cancel
Save