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

Merged
韩天峰 merged 4 commits from fix-trait into master 1 month ago
  1. 23
      phpunit/code/parent-method-private.php
  2. 24
      phpunit/code/trait-method-override-final.php
  3. 24
      phpunit/code/trait-method-override-incompatible.php
  4. 27
      phpunit/code/trait-method-shadows-private.php
  5. 28
      phpunit/code/trait-parent-method-private.php
  6. 28
      phpunit/code/trait-parent-method-protected.php
  7. 18
      phpunit/code/trait-parent-without-parent.php
  8. 22
      phpunit/src/ClassTest.php
  9. 19
      phpunit/src/InheritanceErrorTest.php
  10. 8
      src/Entity/ArgInfo.php
  11. 9
      src/Entity/FunctionDef.php
  12. 16
      src/Entity/MethodDef.php
  13. 21
      src/Generator/CallArgumentGenerator.php
  14. 32
      src/Parser/MethodCallTrait.php
  15. 26
      src/Preprocessor.php
  16. 300
      src/Translator.php
  17. 32
      tests/compiler/trait/trait-method-parent-return.phpt
  18. 38
      tests/compiler/trait/trait-method-self-return-interface.phpt
  19. 35
      tests/compiler/trait/trait-method-static-return-interface.phpt
  20. 36
      tests/compiler/trait/trait-parent-constructor.phpt
  21. 52
      tests/compiler/trait/trait-parent-method-inherited.phpt
  22. 34
      tests/compiler/trait/trait-parent-method-protected.phpt
  23. 36
      tests/compiler/trait/trait-parent-method-reference.phpt
  24. 34
      tests/compiler/trait/trait-parent-method.phpt

@ -0,0 +1,23 @@
<?php
class Base
{
private function secret(): string
{
return 'secret';
}
}
class Child extends Base
{
public function reveal(): string
{
return parent::secret();
}
}
function main(): void
{
$c = new Child();
var_dump($c->reveal());
}

@ -0,0 +1,24 @@
<?php
trait FinalOverrideTrait
{
public function execute(): void
{
}
}
class FinalMethodParent
{
final public function execute(): void
{
}
}
class FinalMethodChild extends FinalMethodParent
{
use FinalOverrideTrait;
}
function main(): void
{
}

@ -0,0 +1,24 @@
<?php
trait IncompatibleTrait
{
public function test(int $value)
{
}
}
class IncompatibleParent
{
protected function test(int $value, bool $bool)
{
}
}
class IncompatibleChild extends IncompatibleParent
{
use IncompatibleTrait;
}
function main(): void
{
}

@ -0,0 +1,27 @@
<?php
trait PrivateShadowTrait
{
public function execute(string $value): string
{
return $value;
}
}
class PrivateMethodParent
{
private function execute(int $value, bool $flag): int
{
return $value;
}
}
class PrivateMethodChild extends PrivateMethodParent
{
use PrivateShadowTrait;
}
function main(): void
{
var_dump((new PrivateMethodChild())->execute('ok'));
}

@ -0,0 +1,28 @@
<?php
trait SecretTrait
{
public function reveal(): string
{
return parent::secret();
}
}
class BaseSecret
{
private function secret(): string
{
return 'secret';
}
}
class ChildSecret extends BaseSecret
{
use SecretTrait;
}
function main(): void
{
$c = new ChildSecret();
var_dump($c->reveal());
}

@ -0,0 +1,28 @@
<?php
trait GreetTrait
{
public function greet(string $name): string
{
return parent::greet($name) . ' [via trait]';
}
}
class BaseGreeter
{
protected function greet(string $name): string
{
return 'Hello ' . $name;
}
}
class ChildGreeter extends BaseGreeter
{
use GreetTrait;
}
function main(): void
{
$g = new ChildGreeter();
var_dump($g->greet('World'));
}

@ -0,0 +1,18 @@
<?php
trait MissingParentTrait
{
public function execute(): void
{
parent::execute();
}
}
class NoParentClass
{
use MissingParentTrait;
}
function main(): void
{
}

@ -37,6 +37,28 @@ class ClassTest extends \BaseTest
$this->exec('Cannot override private method `Base::doWork()`', 'override-private-method.php');
}
public function testTraitMayCallProtectedParentMethod()
{
// A protected parent method is reachable via parent:: from a trait,
// matching PHP runtime behaviour.
$this->compile('trait-parent-method-protected.php');
}
public function testCannotAccessPrivateParentMethodFromRegularClass()
{
$this->exec('Cannot access private method `Base::secret()`', 'parent-method-private.php');
}
public function testCannotAccessPrivateParentMethodFromTrait()
{
$this->exec('Cannot access private method `BaseSecret::secret()`', 'trait-parent-method-private.php');
}
public function testTraitMethodMayShadowPrivateParentMethod()
{
$this->compile('trait-method-shadows-private.php');
}
public function testSelfCanBePartOfUnionType()
{
global $translator;

@ -316,4 +316,23 @@ class InheritanceErrorTest extends TestCase
{
$this->exec('must be compatible', 'abstract_method_signature_mismatch.php');
}
public function testTraitMethodMustBeCompatibleWithParent()
{
// A trait method flattened into a class must remain signature-compatible
// with any same-named parent method, just like a directly-declared
// override. Without this check the incompatibility only surfaces as a
// runtime fatal error that the compiled binary would otherwise ignore.
$this->exec('must be compatible', 'trait-method-override-incompatible.php');
}
public function testTraitMethodCannotOverrideFinalParentMethod()
{
$this->exec('Cannot override final method', 'trait-method-override-final.php');
}
public function testTraitParentCallRequiresParentClass()
{
$this->exec('has no parent', 'trait-parent-without-parent.php');
}
}

@ -22,6 +22,14 @@ class ArgInfo
public ?Expr $defaultValue = null;
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.
* Unlike $class, this is only an assignment/type-check constraint and must

@ -40,6 +40,15 @@ class FunctionDef
*/
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. */
public ?array $returnTypeCheck = null;

@ -15,6 +15,22 @@ class MethodDef
public ?FunctionDef $functionDef = null;
public bool $hasDynamicCall = false;
/**
* The original `ClassMethod` AST node this definition was parsed from.
* Stored so that later validation (e.g. trait method override compatibility
* checks performed at the `use` site) can report accurate line information.
*/
public ?\PhpParser\Node\Stmt\ClassMethod $node = null;
/**
* For methods defined inside a trait, records `parent::method()` calls so
* the compiler can validate their visibility against the parent of each
* class that uses the trait (the trait itself has no parent at compile time).
*
* @var array<int, array{method: string, node: \PhpParser\NodeAbstract}>
*/
public array $parentMethodCalls = [];
public function __construct(int $flags, string $name)
{
$this->flags = $flags;

@ -348,7 +348,8 @@ trait CallArgumentGenerator
string $funcName = '',
string $className = '',
bool $separateNamedArgs = true,
bool $forceArrayArgs = false
bool $forceArrayArgs = false,
bool $preserveExistingReferences = false
): string
{
$list_args = [];
@ -390,7 +391,8 @@ trait CallArgumentGenerator
$this->fatalError($arg, "Duplicate named argument `{$arg->name->name}`");
}
$namedArgs[$arg->name->name] = true;
$byRef = $funcName && $this->isReferenceNamedArgument($funcName, $className, $arg->name->name);
$byRef = ($funcName && $this->isReferenceNamedArgument($funcName, $className, $arg->name->name))
|| ($preserveExistingReferences && $this->isExistingReferenceCallArg($arg));
$value = ($byRef || $this->isRefvalCall($arg->value) || $this->isToRefCall($arg->value))
? $this->parseReferenceCallArgValue($arg)
: $this->parseCallArgValue($arg);
@ -409,7 +411,8 @@ trait CallArgumentGenerator
if ($hasUnpack) {
$this->fatalError($arg, 'Cannot use positional argument after argument unpacking');
}
$byRef = $funcName && $this->isReferenceArgument($funcName, $className, $i);
$byRef = ($funcName && $this->isReferenceArgument($funcName, $className, $i))
|| ($preserveExistingReferences && $this->isExistingReferenceCallArg($arg));
if (($funcName === 'call_user_func' || $funcName === 'call_user_func_array') && $i === 0) {
$callback = $this->parseScopedCallbackArg($arg);
if ($callback !== null) {
@ -499,6 +502,15 @@ trait CallArgumentGenerator
return $namedArgsVar !== null ? $callArgs . ', ' . $namedArgsVar . '.array()' : $callArgs;
}
private function isExistingReferenceCallArg(Node\Arg $arg): bool
{
if (!$this->isVarExpr($arg->value)) {
return $this->isReferenceWrapperCall($arg->value);
}
$name = $this->parseIdentifier($arg->value);
return $this->hasVar($name) && $this->getVarType($name) === Type::REF;
}
protected function parseScopedCallbackArg(Node\Arg $arg): ?string
{
$value = $arg->value;
@ -716,6 +728,8 @@ trait CallArgumentGenerator
if (!$this->hasVar($name)) {
// 若参数是引用类型,可以传入未定义变量,将立即创建变量作为引用
$this->addLocalVar($name, Type::REF);
} elseif ($this->getVarType($name) === Type::REF) {
return '&' . $name;
} else {
// 本地变量,且是原生类型,则转为普通变量
if ($this->hasLocalVar($name) and $this->isNativeType($this->getVarType($name))) {
@ -788,4 +802,3 @@ trait CallArgumentGenerator
}
}

@ -243,6 +243,33 @@ trait MethodCallTrait
protected function parseParentMethodCall(Expr\StaticCall $expr): string
{
// A trait's parent scope is supplied by the wrapper generated for the
// class that composes it. It must not be derived from the runtime
// object's class: an inherited trait method is still lexically bound to
// the parent of the composing class, not to the runtime object's parent.
if ($this->classDef !== null && $this->classDef->trait !== null) {
$method = $this->isIdExpr($expr->name) ? $this->parseIdentifier($expr->name) : '';
// Record the parent:: call so it can be validated against the parent
// of every class that uses this trait (the trait itself has no parent
// at compile time). Dynamic method names cannot be validated statically.
if ($method !== '' && isset($this->methodDef)) {
$this->methodDef->parentMethodCalls[] = ['method' => $method, 'node' => $expr];
}
$methodPtr = 'php::getMethod(trait_parent_ce, ' . $this->identifierToStr($expr->name) . ')';
if (empty($expr->args)) {
return 'this_.call(' . $methodPtr . ')';
}
// The concrete parent signature is only known at each trait use
// site. Preserve arguments that are already references so forwarding
// a by-reference trait parameter does not silently drop its alias.
return 'this_.call(' . $methodPtr . ', ' . $this->parseCallArgs(
$expr->args,
$method,
'',
preserveExistingReferences: true
) . ')';
}
if (!$this->classDef->extends) {
$this->fatalError($expr, 'Cannot call parent method because class `' . $this->classDef->name . '` does not extend any class');
}
@ -250,6 +277,11 @@ trait MethodCallTrait
if ($this->isIdExpr($expr->name)) {
$method = $this->parseIdentifier($expr->name);
$this->guardAbstractMethod($parentClass, $method, $expr);
// A private parent method is not reachable via parent:: — PHP throws
// "Call to private method" at runtime, so report it at compile time.
if ($this->getMethodFlags($parentClass, $method) & Modifiers::PRIVATE) {
$this->fatalError($expr, "Cannot access private method `{$parentClass}::{$method}()` via parent::");
}
$methodPtr = $this->getMethodPtr($parentClass, $method);
} else {
$method = '';

@ -268,6 +268,16 @@ class Preprocessor extends CompilerBase
if ($param->byRef) {
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);
$argInfo->undeclared = $param->type === null;
if (
@ -284,6 +294,9 @@ class Preprocessor extends CompilerBase
if ($class and !$this->hasInterface($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;
}
@ -432,6 +445,16 @@ class Preprocessor extends CompilerBase
}
$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);
// 构造、析构、克隆方法不能有返回值
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->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->returnTypeUndeclared = $v->returnType === null;
$functionDef->returnsByRef = $v->byRef;

@ -2354,6 +2354,13 @@ CODE;
if ($traitStmt instanceof Node\Stmt\ClassMethod) {
$methodName = strtolower($traitStmt->name->toString());
$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) {
$aliasName = strtolower($alias['newName']);
if ($aliasName === $methodName) {
@ -2472,6 +2479,61 @@ 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` remains late-bound in the composed method signature.
$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.
* PHP allows multiple traits to declare the same abstract method as long
@ -2671,7 +2733,12 @@ CODE;
return $code;
}
protected function genWrapperFunctionArgs(string $fn, FunctionDef $functionDef, string $displayName): string
protected function genWrapperFunctionArgs(
string $fn,
FunctionDef $functionDef,
string $displayName,
array $implicitMethodArgs = []
): string
{
$cppCode = '';
$callParams = '';
@ -2720,7 +2787,8 @@ CODE;
}
if ($functionDef->method) {
$callParams = $functionDef->argInfoList ? 'this_, ' . rtrim($callParams, ',') : 'this_';
$methodArgs = implode(', ', array_merge(['this_'], $implicitMethodArgs));
$callParams = $functionDef->argInfoList ? $methodArgs . ', ' . rtrim($callParams, ',') : $methodArgs;
} else {
$callParams = $functionDef->argInfoList ? rtrim($callParams, ',') : '';
}
@ -2781,7 +2849,18 @@ CODE;
$cppCode = 'ZEND_METHOD(' . $name . ', ' . $methodDef->name . '){' . PHP_EOL;
$cppCode .= $this->getIndent() . Type::OBJECT . ' this_(&execute_data->This);' . PHP_EOL;
$fn = self::PREFIX . $this->getNativeMethodName($classDef, $methodDef);
$cppCode .= $this->genWrapperFunctionArgs($fn, $methodDef->functionDef, $classDef->getNamespacedName(false) . '::' . $methodDef->name);
$implicitMethodArgs = [];
if ($classDef->trait !== null && $methodDef->parentMethodCalls) {
// Trait methods are not directly callable without a composing class,
// but keep the generated Zend wrapper well-formed.
$implicitMethodArgs[] = 'this_.parent_ce()';
}
$cppCode .= $this->genWrapperFunctionArgs(
$fn,
$methodDef->functionDef,
$classDef->getNamespacedName(false) . '::' . $methodDef->name,
$implicitMethodArgs
);
return $cppCode;
}
@ -2978,6 +3057,9 @@ CODE;
$functionDeclCode = $cppReturnType . ' ' . ($multiReturn ? $this->getMultiReturnImplName($name) : $nativeName) . '(';
if ($this->class) {
$functionDeclCode .= Type::OBJECT . ' &this_';
if ($this->classDef?->trait !== null && $this->methodDef?->parentMethodCalls) {
$functionDeclCode .= ', zend_class_entry *trait_parent_ce';
}
if ($this->functionDef->params) {
$functionDeclCode .= ', ';
}
@ -3528,6 +3610,9 @@ CODE;
if (!($flags & Modifiers::ABSTRACT)) {
$this->methodDef = $this->classDef->getMethod($name);
// Keep the AST node so trait-composed methods can report accurate
// line numbers when validated for override compatibility later.
$this->methodDef->node = $v;
// 预处理阶段没有父类的信息,只能在实现阶段检查
$this->checkParentMethodCanBeOverridden($v, $name);
$methodCodes[$name] = $this->parseFunction($v);
@ -3638,10 +3723,40 @@ CODE;
string $traitMethodName,
string $classMethodName
): 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
// 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
// (and therefore the visibility of its methods) is known.
foreach ($methodDef->parentMethodCalls as $parentCall) {
$this->validateTraitParentCall($traitDef, $classDef, $parentCall['method'], $parentCall['node']);
}
// A trait method flattened into a class participates in the inheritance
// hierarchy: it must remain signature-compatible with any same-named
// parent method, exactly as a directly-declared override would. PHP
// enforces this at class declaration time ("Declaration of X::m() must
// be compatible with Y::m()"); without this check the incompatibility
// only surfaces as a runtime fatal error that the compiled binary would
// otherwise ignore and keep executing past.
$this->checkTraitMethodOverrideCompatibility($classDef, $methodDef, $classMethodName);
$classDef->addMethod($methodDef);
$traitMethodNativeName = $this->getNativeName($traitMethodName, $traitDef->namespace, $traitDef->name);
$classMethodNativeName = $this->getNativeName($classMethodName, $classDef->namespace, $classDef->name);
$argList = ['this_'];
if ($methodDef->parentMethodCalls) {
// Bind parent:: to the class that actually composes the trait. This
// remains correct when the generated wrapper is inherited further.
$argList[] = $this->getClassEntryPtr($classDef->extends);
}
foreach ($methodDef->functionDef->argInfoList as $argInfo) {
$argList[] = $argInfo->name;
}
@ -3671,6 +3786,185 @@ 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.
*
* The trait has no parent of its own, so the only point at which the parent
* class is known is when a class actually uses the trait. At that moment we
* can statically resolve the parent method and reject private methods, which
* PHP would otherwise only report as a runtime "Call to private method" error.
*/
private function validateTraitParentCall(ClassDef $traitDef, ClassDef $usingClassDef, string $method, NodeAbstract $node): void
{
if (!$usingClassDef->extends) {
$this->fatalError(
$node,
"Cannot access parent when class `{$usingClassDef->getNamespacedName(false)}` has no parent"
);
}
$parentClass = $usingClassDef->extends;
// Internal / not-compiled parents are opaque to the compiler; let the
// runtime enforce visibility for those.
if (!$this->hasClass($parentClass)) {
return;
}
if ($this->getMethodFlags($parentClass, $method) & Modifiers::PRIVATE) {
$this->fatalError(
$node,
"Cannot access private method `{$parentClass}::{$method}()` via parent:: in trait `{$traitDef->name}`"
);
}
}
/**
* Validate that a trait method being flattened into a class remains
* signature-compatible with any same-named method declared up the parent
* chain — the same compatibility contract a directly-declared override must
* satisfy (see `checkParentMethodCanBeOverridden`).
*
* Only the signature contract is enforced here (not the "cannot override
* private/final" rule), because a trait method is flattened into the class
* and, like a normal subclass method, is allowed to shadow a private parent
* method. PHP reports the incompatibility as a class-declaration fatal error
* ("Declaration of X::m() must be compatible with Y::m()"), which we surface
* at compile time so the broken program is rejected instead of being emitted
* and executed past a runtime fatal error.
*/
private function checkTraitMethodOverrideCompatibility(ClassDef $usingClassDef, MethodDef $methodDef, string $methodName): void
{
if ($methodName === '__construct' || $methodDef->node === null) {
return;
}
$classDef = $usingClassDef;
while (true) {
$extends = $classDef->extends;
if (!$extends) {
break;
}
if ($classDef->inheritedFromInternalClass) {
$modifiers = Reflection::getClassMethodModifiers($extends, $methodName);
if ($modifiers !== null && ($modifiers & \ReflectionMethod::IS_FINAL)) {
$this->fatalError($methodDef->node, "Cannot override final method `{$extends}::{$methodName}()`");
}
break;
}
// Dynamically supplied parents are opaque to the compiler.
if (!$this->hasClass($extends)) {
break;
}
$classDef = $this->getClass($extends);
if ($classDef->hasMethod($methodName)) {
$parentMethodDef = $classDef->getMethod($methodName);
// A private method is a separate slot and may be shadowed by the
// method imported from the trait.
if ($parentMethodDef->flags & Modifiers::PRIVATE) {
break;
}
if ($parentMethodDef->flags & Modifiers::FINAL) {
$this->fatalError($methodDef->node, "Cannot override final method `{$extends}::{$methodName}()`");
}
$this->validateMethodOverrideSignature(
$methodDef->node,
$methodName,
$methodDef,
$parentMethodDef,
$extends
);
break;
}
if ($classDef->hasAbstractMethod($methodName) && isset($classDef->abstractMethodDefs[strtolower($methodName)])) {
$this->validateMethodOverrideSignature(
$methodDef->node,
$methodName,
$methodDef,
$classDef->getAbstractMethod($methodName),
$extends
);
break;
}
}
}
private function isCompatibleTraitConstant(ConstantDef $existing, ConstantDef $incoming): bool
{
return $existing->flags === $incoming->flags &&

@ -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)

@ -0,0 +1,36 @@
--TEST--
Trait constructor calling parent::__construct of the composing class
--FILE--
<?php
declare(strict_types=1);
trait TestTrait
{
public function __construct(int $value)
{
parent::__construct($value, true);
}
}
class ParentClass
{
public function __construct(int $value, bool $bool)
{
var_dump($value, $bool);
}
}
class TestClass extends ParentClass
{
use TestTrait;
}
function main()
{
new TestClass(123);
}
?>
--EXPECT--
int(123)
bool(true)

@ -0,0 +1,52 @@
--TEST--
Trait parent:: call remains bound to the composing class when inherited
--FILE--
<?php
trait ParentCallTrait
{
public function source(): string
{
return parent::source();
}
}
class RootClass
{
public function source(): string
{
return 'root';
}
}
class TraitUser extends RootClass
{
use ParentCallTrait;
}
class ChildClass extends TraitUser
{
}
class OtherRootClass
{
public function source(): string
{
return 'other';
}
}
class OtherTraitUser extends OtherRootClass
{
use ParentCallTrait;
}
function main(): void
{
var_dump((new ChildClass())->source());
var_dump((new OtherTraitUser())->source());
}
?>
--EXPECT--
string(4) "root"
string(5) "other"

@ -0,0 +1,34 @@
--TEST--
Trait method calling protected parent::method() of the composing class
--FILE--
<?php
trait GreetTrait
{
public function greet(string $name): string
{
return parent::greet($name) . ' [via trait]';
}
}
class BaseGreeter
{
protected function greet(string $name): string
{
return 'Hello ' . $name;
}
}
class ChildGreeter extends BaseGreeter
{
use GreetTrait;
}
function main(): void
{
$g = new ChildGreeter();
var_dump($g->greet('World'));
}
?>
--EXPECT--
string(23) "Hello World [via trait]"

@ -0,0 +1,36 @@
--TEST--
Trait parent:: call forwards an existing reference parameter
--FILE--
<?php
trait ParentReferenceTrait
{
public function update(string &$value): void
{
parent::update($value);
}
}
class ReferenceParent
{
public function update(string &$value): void
{
$value = 'updated';
}
}
class ReferenceChild extends ReferenceParent
{
use ParentReferenceTrait;
}
function main(): void
{
$value = 'initial';
$child = new ReferenceChild();
$child->update($value);
var_dump($value);
}
?>
--EXPECT--
string(7) "updated"

@ -0,0 +1,34 @@
--TEST--
Trait method calling parent::method() of the composing class
--FILE--
<?php
trait GreetTrait
{
public function greet(string $name): string
{
return parent::greet($name) . ' [via trait]';
}
}
class BaseGreeter
{
public function greet(string $name): string
{
return 'Hello ' . $name;
}
}
class ChildGreeter extends BaseGreeter
{
use GreetTrait;
}
function main(): void
{
$g = new ChildGreeter();
var_dump($g->greet('World'));
}
?>
--EXPECT--
string(23) "Hello World [via trait]"
Loading…
Cancel
Save