From fb79fb832c10a0d1456c829a3576dcd1f7065bfc Mon Sep 17 00:00:00 2001 From: tianfenghan Date: Thu, 27 Aug 2026 19:31:57 +0800 Subject: [PATCH] fix(parser): preserve runtime class in static calls --- src/CompilerBase.php | 2 +- src/Optimizer/FuncCallOptimizer.php | 2 +- src/Parser/MethodCallTrait.php | 64 ++++++--- .../object-static-call-runtime-class.phpt | 126 ++++++++++++++++++ ...rait-self-static-call-cross-namespace.phpt | 70 ++++++++++ 5 files changed, 244 insertions(+), 20 deletions(-) create mode 100644 tests/compiler/static/object-static-call-runtime-class.phpt create mode 100644 tests/compiler/trait/trait-self-static-call-cross-namespace.phpt diff --git a/src/CompilerBase.php b/src/CompilerBase.php index c5655dd5..0de8db77 100644 --- a/src/CompilerBase.php +++ b/src/CompilerBase.php @@ -4589,7 +4589,7 @@ class CompilerBase implements PropertyAccessContext return $id; } if ($id === 'self') { - $id = $this->getNamespacedClassName($this->class); + $id = $this->getFullClassName(); } elseif ($id === 'static') { return Symbol::getCalledClass(); } diff --git a/src/Optimizer/FuncCallOptimizer.php b/src/Optimizer/FuncCallOptimizer.php index fc1f2f35..73297492 100644 --- a/src/Optimizer/FuncCallOptimizer.php +++ b/src/Optimizer/FuncCallOptimizer.php @@ -731,7 +731,7 @@ trait FuncCallOptimizer 'Native classes do not support runtime class introspection; use `NativeClass::class`', ); } - if ($this->isVarExpr($obj) && $this->isTypedObject($obj->name)) { + if ($this->isVarExpr($obj) && $this->isStableObject($obj->name)) { return $this->getLiteralString($this->getObjectType($obj->name)); } return 'php::fn::get_class(' . $this->parseIdentifier($obj) . ')'; diff --git a/src/Parser/MethodCallTrait.php b/src/Parser/MethodCallTrait.php index 38395c54..959123fc 100644 --- a/src/Parser/MethodCallTrait.php +++ b/src/Parser/MethodCallTrait.php @@ -830,6 +830,24 @@ trait MethodCallTrait return false; } + /** + * Materialize a dynamic static-call target exactly once and normalize it + * to the runtime class name accepted by PHP callbacks. + * + * PHP permits both an object and a class-name string before `::`. A + * declared object type is only an upper bound, so using it directly would + * lose late static binding when the runtime object is a subclass. + */ + private function materializeDynamicStaticCallClassName(Expr $target): string + { + [$value, $beforeStmts, $afterStmts] = $this->parseExprWithCapturedStmts($target); + $this->appendCapturedStmtLinesToContext($beforeStmts); + $classVar = $this->addTmpVar(Type::VAR); + $this->context->beforeStmtLines[] = $classVar . ' = ' . $value . ';'; + $this->appendCapturedStmtLinesToContext($afterStmts); + + return '(' . $classVar . '.isObject() ? php::fn::get_class(' . $classVar . ') : php::toString(' . $classVar . '))'; + } protected function parseStaticCall(Expr\StaticCall $expr): string { @@ -850,7 +868,10 @@ trait MethodCallTrait $callScope = []; $rtFunc = ''; $rtClass = ''; - $class = $this->parseIdentifier($expr->class); + $canUseDirectCallScope = $this->isNameExpr($expr->class) && $this->isIdExpr($expr->name); + $class = ($this->isNameExpr($expr->class) || $this->isVarExpr($expr->class)) + ? $this->parseIdentifier($expr->class) + : ''; if ($this->isNameExpr($expr->class) && $this->isIdExpr($expr->name) @@ -870,19 +891,28 @@ trait MethodCallTrait return $this->parseParentMethodCall($expr); } - if ($this->isVarExpr($expr->class) or $this->isVarExpr($expr->name)) { - $var = $class; - if ($this->isTypedObject($var)) { - $class = $this->getObjectType($var); + if (!$this->isNameExpr($expr->class)) { + if ($this->isVarExpr($expr->class) && $this->isStableObject($class)) { + $class = $this->getObjectType($class); goto _do_call; } - if ($this->getVarType($var) == Type::OBJECT) { - $fn = 'php::concat({' . $var . '.getClassName(), "::", ' . $this->methodNameToStr($expr->name) . '})'; - } else { - $fn = 'php::concat({' . $this->identifierToStr($expr->class) . ', "::", ' . $this->methodNameToStr($expr->name) . '})'; + $className = $this->materializeDynamicStaticCallClassName($expr->class); + $fn = 'php::concat({' . $className . ', "::", ' . $this->methodNameToStr($expr->name) . '})'; + if ($this->isVarExpr($expr->class) && $this->isIdExpr($expr->name)) { + $declaredClass = $this->getDeclaredObjectType($class); + if ($declaredClass !== '') { + // Dispatch remains runtime-bound, but PHP requires an + // overriding method to keep the reference signature + // compatible with the declared base method. + $rtFunc = $this->parseIdentifier($expr->name); + $rtClass = $declaredClass; + } } $placeHolder = $fn; - } elseif ($this->isNameExpr($expr->class) and $class === 'static') { + } elseif ($this->isVarExpr($expr->name)) { + $fn = 'php::concat({' . $this->identifierToStr($expr->class) . ', "::", ' . $this->methodNameToStr($expr->name) . '})'; + $placeHolder = $fn; + } elseif ($class === 'static') { if ($this->classDef?->nativeObject) { $this->fatalError( $expr, @@ -901,15 +931,16 @@ trait MethodCallTrait $placeHolder = $this->genArray([Symbol::getCalledClass(), $methodPtr]); // 用于在按引用参数检测时解析方法签名(late static binding 在当前类层级中解析) $rtFunc = $method; - $rtClass = $this->getNamespacedClassName($this->class); - } elseif ($this->isNameExpr($expr->class)) { + $rtClass = $this->getFullClassName(); + } else { if ($class === 'self') { - $class = $this->class; + $class = $this->getFullClassName(); $self = true; } elseif ($class === 'std') { return $this->parseStdCall($expr); + } else { + $class = $this->getNamespacedClassName($class); } - $class = $this->getNamespacedClassName($class); _do_call: $method = $this->parseIdentifier($expr->name); @@ -922,7 +953,7 @@ trait MethodCallTrait ); } - if ($this->isNameExpr($expr->class) and $this->isIdExpr($expr->name)) { + if ($canUseDirectCallScope) { $callScope = [$this->genCharPtr($class, true), $this->genCharPtr($method)]; } @@ -963,9 +994,6 @@ trait MethodCallTrait // reusable handlers and never stores transient trampolines. $fn = $this->getLiteralString($class . '::' . $method); $placeHolder = $this->genArray($callScope); - } else { - $fn = 'php::concat({' . $this->identifierToStr($expr->class) . ', "::", ' . $this->methodNameToStr($expr->name) . '})'; - $placeHolder = $fn; } $call = 'php::call'; diff --git a/tests/compiler/static/object-static-call-runtime-class.phpt b/tests/compiler/static/object-static-call-runtime-class.phpt new file mode 100644 index 00000000..c7d8f61a --- /dev/null +++ b/tests/compiler/static/object-static-call-runtime-class.phpt @@ -0,0 +1,126 @@ +--TEST-- +Object static calls use the runtime class rather than the declared type +--FILE-- + +--EXPECT-- +array(2) { + [0]=> + string(5) "child" + [1]=> + string(21) "ObjectStaticCallChild" +} +array(2) { + [0]=> + string(5) "child" + [1]=> + string(21) "ObjectStaticCallChild" +} +array(2) { + [0]=> + string(5) "child" + [1]=> + string(21) "ObjectStaticCallChild" +} +array(2) { + [0]=> + string(5) "child" + [1]=> + string(21) "ObjectStaticCallChild" +} +array(2) { + [0]=> + string(5) "child" + [1]=> + string(21) "ObjectStaticCallChild" +} +array(2) { + [0]=> + string(5) "child" + [1]=> + string(21) "ObjectStaticCallChild" +} +array(2) { + [0]=> + string(21) "ObjectStaticCallChild" + [1]=> + int(2) +} diff --git a/tests/compiler/trait/trait-self-static-call-cross-namespace.phpt b/tests/compiler/trait/trait-self-static-call-cross-namespace.phpt new file mode 100644 index 00000000..b199f4ee --- /dev/null +++ b/tests/compiler/trait/trait-self-static-call-cross-namespace.phpt @@ -0,0 +1,70 @@ +--TEST-- +Trait self static calls resolve to the consuming class across namespaces +--FILE-- +namedCall()); + var_dump($object->dynamicCall()); + var_dump($object->selfMembers()); + } +} + +?> +--EXPECT-- +string(12) "trait-helper" +string(12) "trait-helper" +array(3) { + [0]=> + string(30) "TraitSelfCall\Consumer\Example" + [1]=> + string(17) "consumer-constant" + [2]=> + string(17) "consumer-property" +}