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/Preprocessor.php b/src/Preprocessor.php index 0273d2cd..fd690242 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 33d07f86..2a25518a 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,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. * PHP allows multiple traits to declare the same abstract method as long @@ -3613,6 +3676,14 @@ 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 @@ -3663,6 +3734,86 @@ 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. * 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)