fix(compiler): 修复trait方法self/static/parent类型的延迟绑定解析

pull/26/head
Yurun 1 month ago
parent 9d22e8ca24
commit 78039a28db
  1. 8
      src/Entity/ArgInfo.php
  2. 9
      src/Entity/FunctionDef.php
  3. 26
      src/Preprocessor.php
  4. 151
      src/Translator.php
  5. 32
      tests/compiler/trait/trait-method-parent-return.phpt
  6. 38
      tests/compiler/trait/trait-method-self-return-interface.phpt
  7. 35
      tests/compiler/trait/trait-method-static-return-interface.phpt

@ -22,6 +22,14 @@ class ArgInfo
public ?Expr $defaultValue = null; public ?Expr $defaultValue = null;
public string $class = ''; public string $class = '';
/**
* Late-bound type keyword: 'self', 'static' or 'parent'.
* Empty for ordinary class-name parameter types. When set, the effective
* class depends on the consuming context (e.g. a trait method's `self`
* parameter resolves to the class that uses the trait).
*/
public string $typeKeyword = '';
/** /**
* Object type declared in the PHP signature, including interfaces. * Object type declared in the PHP signature, including interfaces.
* Unlike $class, this is only an assignment/type-check constraint and must * Unlike $class, this is only an assignment/type-check constraint and must

@ -40,6 +40,15 @@ class FunctionDef
*/ */
public string $returnClass = ''; public string $returnClass = '';
/**
* Late-bound return type keyword: 'self', 'static' or 'parent'.
* Empty for ordinary class-name return types. When set, the effective class
* depends on the consuming context (e.g. a trait method's `self` resolves to
* the class that uses the trait), so it must be re-resolved when the method
* is flattened into a class.
*/
public string $returnTypeKeyword = '';
/** Same format as ArgInfo::$typeCheck. Null means no runtime return type check. */ /** Same format as ArgInfo::$typeCheck. Null means no runtime return type check. */
public ?array $returnTypeCheck = null; public ?array $returnTypeCheck = null;

@ -268,6 +268,16 @@ class Preprocessor extends CompilerBase
if ($param->byRef) { if ($param->byRef) {
return Type::REF; return Type::REF;
} }
// Capture the late-bound parameter type keyword *before* resolveTypeDecl
// runs, because resolveTypeDecl mutates the `self`/`static`/`parent` node
// name to the declaring class when the method belongs to a trait.
$typeKeyword = '';
if ($param->type instanceof Node\Name) {
$ptLower = strtolower($param->type->toString());
if ($ptLower === 'self' || $ptLower === 'static' || $ptLower === 'parent') {
$typeKeyword = $ptLower;
}
}
[$type, $class] = $this->resolveTypeDecl($param->type, self::DECL_TYPE_OF_PARAM); [$type, $class] = $this->resolveTypeDecl($param->type, self::DECL_TYPE_OF_PARAM);
$argInfo->undeclared = $param->type === null; $argInfo->undeclared = $param->type === null;
if ( if (
@ -284,6 +294,9 @@ class Preprocessor extends CompilerBase
if ($class and !$this->hasInterface($class)) { if ($class and !$this->hasInterface($class)) {
$argInfo->class = $class; $argInfo->class = $class;
} }
// Record late-bound parameter type keywords so they can be re-resolved
// to the consuming class when a trait method is flattened into a class.
$argInfo->typeKeyword = $typeKeyword;
return $type; return $type;
} }
@ -432,6 +445,16 @@ class Preprocessor extends CompilerBase
} }
$fnName = $this->parseIdentifier($v->name); $fnName = $this->parseIdentifier($v->name);
// Capture the late-bound return type keyword *before* resolveTypeDecl runs,
// because resolveTypeDecl mutates the `self`/`static`/`parent` node name to
// the declaring class when the method belongs to a trait.
$returnTypeKeyword = '';
if ($v->returnType instanceof Node\Name) {
$rtLower = strtolower($v->returnType->toString());
if ($rtLower === 'self' || $rtLower === 'static' || $rtLower === 'parent') {
$returnTypeKeyword = $rtLower;
}
}
[$returnType, $class] = $this->resolveTypeDecl($v->returnType, self::DECL_TYPE_OF_RETURN); [$returnType, $class] = $this->resolveTypeDecl($v->returnType, self::DECL_TYPE_OF_RETURN);
// 构造、析构、克隆方法不能有返回值 // 构造、析构、克隆方法不能有返回值
if ($this->method and in_array($this->method, ['__construct', '__destruct', '__clone'])) { if ($this->method and in_array($this->method, ['__construct', '__destruct', '__clone'])) {
@ -440,6 +463,9 @@ class Preprocessor extends CompilerBase
$functionDef = new FunctionDef($fnName, $returnType, $this->namespace); $functionDef = new FunctionDef($fnName, $returnType, $this->namespace);
$functionDef->returnClass = $class; $functionDef->returnClass = $class;
// Record late-bound return type keywords so they can be re-resolved to
// the consuming class when a trait method is flattened into a class.
$functionDef->returnTypeKeyword = $returnTypeKeyword;
$functionDef->stub = $this->stubFile; $functionDef->stub = $this->stubFile;
$functionDef->returnTypeUndeclared = $v->returnType === null; $functionDef->returnTypeUndeclared = $v->returnType === null;
$functionDef->returnsByRef = $v->byRef; $functionDef->returnsByRef = $v->byRef;

@ -2354,6 +2354,13 @@ CODE;
if ($traitStmt instanceof Node\Stmt\ClassMethod) { if ($traitStmt instanceof Node\Stmt\ClassMethod) {
$methodName = strtolower($traitStmt->name->toString()); $methodName = strtolower($traitStmt->name->toString());
$fullMethodName = $this->getFullMethodName($traitFullName, $methodName); $fullMethodName = $this->getFullMethodName($traitFullName, $methodName);
// A trait method's `self`/`static`/`parent` return and parameter
// types refer to the class that uses the trait, not the trait
// itself. Re-resolve them on the cloned AST so the generated
// arginfo reflects the consuming class (PHP trait semantics) and
// passes ZendVM's runtime signature-compatibility checks. The
// alias clones below inherit this rewrite.
$this->reresolveTraitMethodAstLateBoundTypes($classDef, $traitFullName, $traitStmt);
foreach ($classDef->traitAliases[$fullMethodName] ?? [] as $alias) { foreach ($classDef->traitAliases[$fullMethodName] ?? [] as $alias) {
$aliasName = strtolower($alias['newName']); $aliasName = strtolower($alias['newName']);
if ($aliasName === $methodName) { if ($aliasName === $methodName) {
@ -2472,6 +2479,62 @@ CODE;
} }
} }
/**
* Re-resolve a trait method's late-bound `self`/`static`/`parent` return and
* parameter types on the cloned AST that is being flattened into a class.
*
* `resolveTypeDecl()` mutates a trait method's `self`/`static`/`parent` type
* node to the trait's own name at parse time, so the cloned AST carries the
* trait name rather than the late-bound keyword. We instead rewrite those
* nodes to the consuming class (or its parent) using the keyword recorded on
* the trait method's FunctionDef, matching PHP's trait semantics. This keeps
* the generated arginfo correct for ZendVM's runtime compatibility checks.
*/
private function reresolveTraitMethodAstLateBoundTypes(
ClassDef $usingClassDef,
string $traitFullName,
Node\Stmt\ClassMethod $methodStmt
): void {
if (!$this->hasClass($traitFullName)) {
return;
}
$traitDef = $this->getClass($traitFullName);
if (!$traitDef->hasMethod($methodStmt->name->toString())) {
return;
}
$fn = $traitDef->getMethod($methodStmt->name->toString())->functionDef;
if ($fn->returnTypeKeyword !== '' && $methodStmt->returnType instanceof Node\Name) {
if ($fn->returnTypeKeyword === 'static') {
// `static` is late-static-bound: keep the keyword so ZendVM
// resolves it to the concrete class at call time.
$methodStmt->returnType = new Node\Name('static');
} else {
$resolved = $this->resolveLateBoundClass($usingClassDef, $fn->returnTypeKeyword);
if ($resolved !== null) {
$methodStmt->returnType = new Node\Name($resolved);
}
}
}
foreach ($fn->argInfoList as $i => $arg) {
if (
$arg->typeKeyword !== ''
&& isset($methodStmt->params[$i])
&& $methodStmt->params[$i]->type instanceof Node\Name
) {
if ($arg->typeKeyword === 'static') {
$methodStmt->params[$i]->type = new Node\Name('static');
} else {
$resolved = $this->resolveLateBoundClass($usingClassDef, $arg->typeKeyword);
if ($resolved !== null) {
$methodStmt->params[$i]->type = new Node\Name($resolved);
}
}
}
}
}
/** /**
* Validate that two abstract trait methods have compatible signatures. * Validate that two abstract trait methods have compatible signatures.
* PHP allows multiple traits to declare the same abstract method as long * PHP allows multiple traits to declare the same abstract method as long
@ -3613,6 +3676,14 @@ CODE;
string $traitMethodName, string $traitMethodName,
string $classMethodName string $classMethodName
): string { ): string {
// A trait method's `self`/`static`/`parent` return and parameter types
// refer to the class that uses the trait, not the trait itself. Re-resolve
// them to the consuming class so signature-compatibility checks (against
// parent classes and interfaces) and `detectClassOfExpr()` observe the
// correct type. The cloned FunctionDef keeps the trait's own native
// function untouched.
$this->reresolveTraitLateBoundTypes($classDef, $methodDef);
// Validate `parent::` calls emitted from this trait method against the // Validate `parent::` calls emitted from this trait method against the
// parent of the class that is composing the trait. The trait itself has // parent of the class that is composing the trait. The trait itself has
// no parent at compile time, so this is the only place the parent class // no parent at compile time, so this is the only place the parent class
@ -3663,6 +3734,86 @@ CODE;
return $code; return $code;
} }
/**
* Re-resolve a trait method's late-bound `self`/`static`/`parent` return and
* parameter types to the class that is composing the trait.
*
* In PHP, `self` (and `static`) inside a trait refers to the using class, and
* `parent` refers to the using class's parent. The compiler records these as
* the trait's own name at parse time, which is wrong once the method is
* flattened into a class: interface/trait `self` comparisons and
* `detectClassOfExpr()` would otherwise observe the trait name instead of the
* consuming class. We clone the FunctionDef so the trait's standalone native
* function keeps its original (trait-context) types.
*/
private function reresolveTraitLateBoundTypes(ClassDef $usingClassDef, MethodDef $methodDef): void
{
$fn = $methodDef->functionDef;
$needsClone = false;
if ($fn->returnTypeKeyword !== '') {
$resolved = $this->resolveLateBoundClass($usingClassDef, $fn->returnTypeKeyword);
if ($resolved !== null && $resolved !== $fn->returnClass) {
$needsClone = true;
}
}
foreach ($fn->argInfoList as $arg) {
if ($arg->typeKeyword !== '') {
$resolved = $this->resolveLateBoundClass($usingClassDef, $arg->typeKeyword);
if ($resolved !== null && ($resolved !== $arg->class || $resolved !== $arg->declaredClass)) {
$needsClone = true;
break;
}
}
}
if (!$needsClone) {
return;
}
$newFn = clone $fn;
if ($fn->returnTypeKeyword !== '') {
$resolved = $this->resolveLateBoundClass($usingClassDef, $fn->returnTypeKeyword);
if ($resolved !== null && $resolved !== $newFn->returnClass) {
$newFn->returnClass = $resolved;
}
}
$newArgs = [];
foreach ($newFn->argInfoList as $arg) {
$newArg = clone $arg;
if ($newArg->typeKeyword !== '') {
$resolved = $this->resolveLateBoundClass($usingClassDef, $newArg->typeKeyword);
if ($resolved !== null) {
if ($newArg->class !== '') {
$newArg->class = $resolved;
}
if ($newArg->declaredClass !== '') {
$newArg->declaredClass = $resolved;
}
}
}
$newArgs[] = $newArg;
}
$newFn->argInfoList = $newArgs;
$methodDef->functionDef = $newFn;
}
private function resolveLateBoundClass(ClassDef $usingClassDef, string $keyword): ?string
{
if ($keyword === 'self') {
return $usingClassDef->getNamespacedName(false);
}
if ($keyword === 'parent') {
return $usingClassDef->extends !== '' ? $usingClassDef->extends : null;
}
// `static` is late-static-bound and resolved to the concrete class only at
// call time, so it must keep an empty class (matching a directly-declared
// `: static` method). Resolving it to the consuming class here would break
// interface/trait signature-compatibility checks, which compare the empty
// `static` class on both sides.
return null;
}
/** /**
* Validate a `parent::method()` call recorded inside a trait method. * Validate a `parent::method()` call recorded inside a trait method.
* *

@ -0,0 +1,32 @@
--TEST--
Trait method with `parent` return type flattened into a subclass
--FILE--
<?php
class Base
{
}
trait TestTrait
{
public function who(): parent
{
return $this;
}
}
class Child extends Base
{
use TestTrait;
}
function main()
{
$c = new Child;
// The trait method's `parent` resolves to the consuming class's parent (Base).
$r = $c->who();
var_dump($r instanceof Child);
}
?>
--EXPECT--
bool(true)

@ -0,0 +1,38 @@
--TEST--
Trait method with `self` return type flattened into a class that implements an interface declaring `self` return
--FILE--
<?php
interface TestInterface
{
public function test(): self;
}
trait TestTrait
{
public function test(): self
{
return $this;
}
}
class TestClass implements TestInterface
{
use TestTrait;
}
function main()
{
$test = new TestClass;
// The trait method's `self` resolves to the consuming class (TestClass),
// which must be compatible with the interface's `self` (TestInterface).
$result = $test->test();
var_dump($result instanceof TestClass);
var_dump($result === $test);
var_dump($result instanceof TestInterface);
}
?>
--EXPECT--
bool(true)
bool(true)
bool(true)

@ -0,0 +1,35 @@
--TEST--
Trait method with `static` return type flattened into a class that implements an interface declaring `static` return
--FILE--
<?php
interface TestInterface
{
public function make(): static;
}
trait TestTrait
{
public function make(): static
{
return new static;
}
}
class TestClass implements TestInterface
{
use TestTrait;
}
function main()
{
$a = new TestClass;
$b = $a->make();
// `static` is late-static-bound to the consuming class (TestClass).
var_dump($b instanceof TestClass);
var_dump($b !== $a);
}
?>
--EXPECT--
bool(true)
bool(true)
Loading…
Cancel
Save