fix(parser): preserve runtime class in static calls

master
韩天峰 22 hours ago
parent 019c626c37
commit fb79fb832c
  1. 2
      src/CompilerBase.php
  2. 2
      src/Optimizer/FuncCallOptimizer.php
  3. 64
      src/Parser/MethodCallTrait.php
  4. 126
      tests/compiler/static/object-static-call-runtime-class.phpt
  5. 70
      tests/compiler/trait/trait-self-static-call-cross-namespace.phpt

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

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

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

@ -0,0 +1,126 @@
--TEST--
Object static calls use the runtime class rather than the declared type
--FILE--
<?php
class ObjectStaticCallBase
{
public static function identify(): string
{
return 'base';
}
public static function calledClass(): string
{
return static::class;
}
public static function increment(int &$value): string
{
$value++;
return static::class;
}
}
final class ObjectStaticCallChild extends ObjectStaticCallBase
{
public static function identify(): string
{
return 'child';
}
}
function callOnTypedObject(ObjectStaticCallBase $object): array
{
return [$object::identify(), $object::calledClass()];
}
function callOnMixedObject(mixed $object): array
{
return [$object::identify(), $object::calledClass()];
}
function callOnGenericObject(object $object): array
{
return [$object::identify(), $object::calledClass()];
}
function callOnExactLocalObject(): array
{
$object = new ObjectStaticCallChild();
return [$object::identify(), $object::calledClass()];
}
function callViaGetClass(ObjectStaticCallBase $object): array
{
return [get_class($object)::identify(), get_class($object)::calledClass()];
}
function callOnClassString(string $class): array
{
return [$class::identify(), $class::calledClass()];
}
function callReferenceArgument(ObjectStaticCallBase $object): array
{
$value = 1;
$class = $object::increment($value);
return [$class, $value];
}
function main(): void
{
$object = new ObjectStaticCallChild();
var_dump(callOnTypedObject($object));
var_dump(callOnMixedObject($object));
var_dump(callOnGenericObject($object));
var_dump(callOnExactLocalObject());
var_dump(callViaGetClass($object));
var_dump(callOnClassString(ObjectStaticCallChild::class));
var_dump(callReferenceArgument($object));
}
?>
--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)
}

@ -0,0 +1,70 @@
--TEST--
Trait self static calls resolve to the consuming class across namespaces
--FILE--
<?php
namespace TraitSelfCall\Template {
trait Dispatch
{
public function namedCall(): string
{
return self::privateHelper();
}
public function dynamicCall(): string
{
$method = 'helper';
return self::$method();
}
public function selfMembers(): array
{
return [self::class, self::LABEL, self::$label];
}
private static function privateHelper(): string
{
return 'trait-helper';
}
public static function helper(): string
{
return 'trait-helper';
}
}
}
namespace TraitSelfCall\Consumer {
use TraitSelfCall\Template\Dispatch;
final class Example
{
use Dispatch;
private const LABEL = 'consumer-constant';
private static string $label = 'consumer-property';
}
}
namespace {
function main(): void
{
$object = new TraitSelfCall\Consumer\Example();
var_dump($object->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"
}
Loading…
Cancel
Save