diff --git a/phpunit/code/parent-method-private.php b/phpunit/code/parent-method-private.php new file mode 100644 index 00000000..cd030d1e --- /dev/null +++ b/phpunit/code/parent-method-private.php @@ -0,0 +1,23 @@ +reveal()); +} diff --git a/phpunit/code/trait-method-override-final.php b/phpunit/code/trait-method-override-final.php new file mode 100644 index 00000000..2cf6beeb --- /dev/null +++ b/phpunit/code/trait-method-override-final.php @@ -0,0 +1,24 @@ +execute('ok')); +} diff --git a/phpunit/code/trait-parent-method-private.php b/phpunit/code/trait-parent-method-private.php new file mode 100644 index 00000000..13afd0af --- /dev/null +++ b/phpunit/code/trait-parent-method-private.php @@ -0,0 +1,28 @@ +reveal()); +} diff --git a/phpunit/code/trait-parent-method-protected.php b/phpunit/code/trait-parent-method-protected.php new file mode 100644 index 00000000..1a94d8b9 --- /dev/null +++ b/phpunit/code/trait-parent-method-protected.php @@ -0,0 +1,28 @@ +greet('World')); +} diff --git a/phpunit/code/trait-parent-without-parent.php b/phpunit/code/trait-parent-without-parent.php new file mode 100644 index 00000000..c3a025d2 --- /dev/null +++ b/phpunit/code/trait-parent-without-parent.php @@ -0,0 +1,18 @@ +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; diff --git a/phpunit/src/InheritanceErrorTest.php b/phpunit/src/InheritanceErrorTest.php index 5d1852b0..59c07e14 100644 --- a/phpunit/src/InheritanceErrorTest.php +++ b/phpunit/src/InheritanceErrorTest.php @@ -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'); + } } diff --git a/src/Entity/ArgInfo.php b/src/Entity/ArgInfo.php index a7bac66c..76f3926f 100644 --- a/src/Entity/ArgInfo.php +++ b/src/Entity/ArgInfo.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 diff --git a/src/Entity/FunctionDef.php b/src/Entity/FunctionDef.php index b4ec6058..0f37718c 100644 --- a/src/Entity/FunctionDef.php +++ b/src/Entity/FunctionDef.php @@ -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; diff --git a/src/Entity/MethodDef.php b/src/Entity/MethodDef.php index 41bbecba..3973e385 100644 --- a/src/Entity/MethodDef.php +++ b/src/Entity/MethodDef.php @@ -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 + */ + public array $parentMethodCalls = []; + public function __construct(int $flags, string $name) { $this->flags = $flags; diff --git a/src/Generator/CallArgumentGenerator.php b/src/Generator/CallArgumentGenerator.php index 79431f24..826516eb 100644 --- a/src/Generator/CallArgumentGenerator.php +++ b/src/Generator/CallArgumentGenerator.php @@ -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 } } - diff --git a/src/Parser/MethodCallTrait.php b/src/Parser/MethodCallTrait.php index 8b86e45c..6ede7f2c 100644 --- a/src/Parser/MethodCallTrait.php +++ b/src/Parser/MethodCallTrait.php @@ -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 = ''; diff --git a/src/Preprocessor.php b/src/Preprocessor.php index f2253b45..b65cea84 100644 --- a/src/Preprocessor.php +++ b/src/Preprocessor.php @@ -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; diff --git a/src/Translator.php b/src/Translator.php index aa6e2eb7..1f15eca3 100644 --- a/src/Translator.php +++ b/src/Translator.php @@ -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 && diff --git a/tests/compiler/trait/trait-method-parent-return.phpt b/tests/compiler/trait/trait-method-parent-return.phpt new file mode 100644 index 00000000..c1a9c8b3 --- /dev/null +++ b/tests/compiler/trait/trait-method-parent-return.phpt @@ -0,0 +1,32 @@ +--TEST-- +Trait method with `parent` return type flattened into a subclass +--FILE-- +who(); + var_dump($r instanceof Child); +} +?> +--EXPECT-- +bool(true) diff --git a/tests/compiler/trait/trait-method-self-return-interface.phpt b/tests/compiler/trait/trait-method-self-return-interface.phpt new file mode 100644 index 00000000..96545bec --- /dev/null +++ b/tests/compiler/trait/trait-method-self-return-interface.phpt @@ -0,0 +1,38 @@ +--TEST-- +Trait method with `self` return type flattened into a class that implements an interface declaring `self` return +--FILE-- +test(); + var_dump($result instanceof TestClass); + var_dump($result === $test); + var_dump($result instanceof TestInterface); +} +?> +--EXPECT-- +bool(true) +bool(true) +bool(true) diff --git a/tests/compiler/trait/trait-method-static-return-interface.phpt b/tests/compiler/trait/trait-method-static-return-interface.phpt new file mode 100644 index 00000000..9084c93d --- /dev/null +++ b/tests/compiler/trait/trait-method-static-return-interface.phpt @@ -0,0 +1,35 @@ +--TEST-- +Trait method with `static` return type flattened into a class that implements an interface declaring `static` return +--FILE-- +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) diff --git a/tests/compiler/trait/trait-parent-constructor.phpt b/tests/compiler/trait/trait-parent-constructor.phpt new file mode 100644 index 00000000..941968a6 --- /dev/null +++ b/tests/compiler/trait/trait-parent-constructor.phpt @@ -0,0 +1,36 @@ +--TEST-- +Trait constructor calling parent::__construct of the composing class +--FILE-- + +--EXPECT-- +int(123) +bool(true) diff --git a/tests/compiler/trait/trait-parent-method-inherited.phpt b/tests/compiler/trait/trait-parent-method-inherited.phpt new file mode 100644 index 00000000..c6f5a752 --- /dev/null +++ b/tests/compiler/trait/trait-parent-method-inherited.phpt @@ -0,0 +1,52 @@ +--TEST-- +Trait parent:: call remains bound to the composing class when inherited +--FILE-- +source()); + var_dump((new OtherTraitUser())->source()); +} +?> +--EXPECT-- +string(4) "root" +string(5) "other" diff --git a/tests/compiler/trait/trait-parent-method-protected.phpt b/tests/compiler/trait/trait-parent-method-protected.phpt new file mode 100644 index 00000000..661ab30c --- /dev/null +++ b/tests/compiler/trait/trait-parent-method-protected.phpt @@ -0,0 +1,34 @@ +--TEST-- +Trait method calling protected parent::method() of the composing class +--FILE-- +greet('World')); +} +?> +--EXPECT-- +string(23) "Hello World [via trait]" diff --git a/tests/compiler/trait/trait-parent-method-reference.phpt b/tests/compiler/trait/trait-parent-method-reference.phpt new file mode 100644 index 00000000..4e72026d --- /dev/null +++ b/tests/compiler/trait/trait-parent-method-reference.phpt @@ -0,0 +1,36 @@ +--TEST-- +Trait parent:: call forwards an existing reference parameter +--FILE-- +update($value); + var_dump($value); +} +?> +--EXPECT-- +string(7) "updated" diff --git a/tests/compiler/trait/trait-parent-method.phpt b/tests/compiler/trait/trait-parent-method.phpt new file mode 100644 index 00000000..02e4839b --- /dev/null +++ b/tests/compiler/trait/trait-parent-method.phpt @@ -0,0 +1,34 @@ +--TEST-- +Trait method calling parent::method() of the composing class +--FILE-- +greet('World')); +} +?> +--EXPECT-- +string(23) "Hello World [via trait]"