diff --git a/src/Parser/MethodCallTrait.php b/src/Parser/MethodCallTrait.php index 0f6ec71e..6697f5c5 100644 --- a/src/Parser/MethodCallTrait.php +++ b/src/Parser/MethodCallTrait.php @@ -457,6 +457,13 @@ trait MethodCallTrait $object = empty($expr->args) ? $this->parseIdentifier($expr->var) : $this->parseOrderedOperand($expr->var, false); + // Preserve the receiver expression boundary for no-argument + // calls. Without these parentheses, C++ member access binds more + // tightly than assignment, so `($b = $a)->method()` was emitted + // as `b = a.call(...)` instead of `(b = a).call(...)`. + if (empty($expr->args) && !$this->isVarExpr($expr->var)) { + $object = '(' . $object . ')'; + } } if ($this->isVarExpr($expr->var)) { if (!$this->hasVar($object)) { diff --git a/tests/compiler/dynamic_call/assignment-expression-receiver.phpt b/tests/compiler/dynamic_call/assignment-expression-receiver.phpt new file mode 100644 index 00000000..e8be849a --- /dev/null +++ b/tests/compiler/dynamic_call/assignment-expression-receiver.phpt @@ -0,0 +1,105 @@ +--TEST-- +Assignment expressions used as method receivers preserve PHP evaluation semantics +--FILE-- +value = $value; + } + + public function id(): string + { + return $this->value; + } + + public function append(string $suffix): string + { + return $this->value . $suffix; + } +} + +class AssignmentReceiverFactory extends AssignmentReceiver +{ + public static function fromSelf(): string + { + return ($receiver = new self('self'))->id(); + } + + public static function fromStatic(): string + { + return ($receiver = new static('static'))->id(); + } +} + +class AssignmentReceiverChild extends AssignmentReceiverFactory +{ +} + +class AssignmentReceiverHolder +{ + public AssignmentReceiver $receiver; + + public function __construct(AssignmentReceiver $receiver) + { + $this->receiver = $receiver; + } +} + +function makeAssignmentReceiver(int &$calls, string $value): AssignmentReceiver +{ + ++$calls; + return new AssignmentReceiver($value); +} + +function main(): void +{ + $original = new AssignmentReceiver('base'); + + echo ($assigned = $original)->id(), "\n"; + var_dump($assigned === $original); + + // A call with arguments already materializes its receiver; retain it as a + // control for the no-argument path fixed by this regression. + echo ($withArgument = $original)->append('-arg'), "\n"; + + $calls = 0; + echo ($created = makeAssignmentReceiver($calls, 'factory'))->id(), "\n"; + var_dump($calls); + echo $created->id(), "\n"; + + echo AssignmentReceiverFactory::fromSelf(), "\n"; + echo AssignmentReceiverChild::fromStatic(), "\n"; + + $holder = new AssignmentReceiverHolder($original); + echo ($holder->receiver = new AssignmentReceiver('property'))->id(), "\n"; + + $items = []; + echo ($items[0] = new AssignmentReceiver('array'))->id(), "\n"; + + echo ($left = $right = new AssignmentReceiver('chain'))->id(), "\n"; + var_dump($left === $right); + + echo ($reference =& $original)->id(), "\n"; + var_dump($reference === $original); +} +?> +--EXPECT-- +base +bool(true) +base-arg +factory +int(1) +factory +self +static +property +array +chain +bool(true) +base +bool(true)