From 96f9f0bdb9fd18b80d75209046d2b6994a3d97d7 Mon Sep 17 00:00:00 2001 From: Tinywan Date: Sun, 6 Sep 2026 21:13:33 +0800 Subject: [PATCH] fix(compiler): allow re-assigning inferred local object to another class A local variable has no type declaration; its class is inferred from the first assignment. Re-assigning an unrelated concrete class in a mutually exclusive branch (e.g. ReflectionFunction in `if` and ReflectionMethod in `else`) is valid PHP, but was rejected with "Cannot re-assign typed object". Route both re-assignment sites through reassignInferredObjectVar(), which widens an inferred local to a generic dynamic object. Parameters, native objects, and explicitly declared object types keep the strict check. --- src/Parser/AssignOpTrait.php | 25 ++++++++++++++-- .../reflection/inferred-object-reassign.phpt | 29 +++++++++++++++++++ 2 files changed, 52 insertions(+), 2 deletions(-) create mode 100644 tests/compiler/reflection/inferred-object-reassign.phpt diff --git a/src/Parser/AssignOpTrait.php b/src/Parser/AssignOpTrait.php index 8c7510a1..442dbd3c 100644 --- a/src/Parser/AssignOpTrait.php +++ b/src/Parser/AssignOpTrait.php @@ -568,7 +568,7 @@ protected function parseAssignFinally( // Parent/interface/abstract declarations are not precise enough for a concrete typed object. $runtimeObjectAssignClass = $leftClass; } else { - $this->fatalError($left, "Cannot re-assign typed object `\${$var}` from `{$leftClass}` to `{$rightClass}`"); + $this->reassignInferredObjectVar($left, $var, $leftClass, $rightClass); } } else { $this->checkVarAssignExpr($left, $this->getVarType($var), Type::OBJECT); @@ -644,7 +644,7 @@ protected function parseAssignFinally( } elseif ($this->isInterface($rightClass) || $this->isAbstractClass($rightClass) || $this->isObjectClassStaticallyAssignableTo($leftClass, $rightClass)) { $runtimeObjectAssignClass = $leftClass; } else { - $this->fatalError($left, "Cannot re-assign typed object `\${$var}` from `{$leftClass}` to `{$rightClass}`"); + $this->reassignInferredObjectVar($left, $var, $leftClass, $rightClass); } } } @@ -733,6 +733,27 @@ protected function parseAssignFinally( return $var . ' = ' . $assignedExpr; } + /** + * PHP local variables carry no type declaration; the class tracked for + * `$var` was inferred from an earlier assignment. Re-assigning an unrelated + * concrete class in a mutually exclusive branch (for example if/else) is + * valid PHP, so widen the inferred local to a generic dynamic object rather + * than rejecting it. Parameters, native objects, and explicitly declared + * object types keep their strict re-assignment check. + */ + protected function reassignInferredObjectVar(Expr $left, string $var, string $leftClass, string $rightClass): void + { + $canWiden = !$this->hasArgument($var) + && !$this->isNativeObjectVar($var) + && isset($this->context->objects[$var]) + && !isset($this->context->declaredObjects[$var]); + if (!$canWiden) { + $this->fatalError($left, "Cannot re-assign typed object `\${$var}` from `{$leftClass}` to `{$rightClass}`"); + } + unset($this->context->objects[$var], $this->context->stableObjects[$var], $this->context->declaredObjects[$var]); + $this->addLocalVar($var, Type::OBJECT); + } + protected function parseAssignPropertyHook( Expr\PropertyFetch $left, Expr $right, diff --git a/tests/compiler/reflection/inferred-object-reassign.phpt b/tests/compiler/reflection/inferred-object-reassign.phpt new file mode 100644 index 00000000..a7e248f1 --- /dev/null +++ b/tests/compiler/reflection/inferred-object-reassign.phpt @@ -0,0 +1,29 @@ +--TEST-- +Re-assign an inferred local object to a different class across if/else +--FILE-- + +--EXPECT-- +ReflectionFunction +ReflectionMethod