Merge remote-tracking branch 'origin/master' into fix-property-default-value

pull/24/head
韩天峰 1 month ago
commit 8c3cbd4dce
  1. 13
      phpunit/code/inheritance_error_const_final.php
  2. 13
      phpunit/code/inheritance_error_const_missing_type.php
  3. 23
      phpunit/code/parent-method-private.php
  4. 24
      phpunit/code/trait-method-override-final.php
  5. 24
      phpunit/code/trait-method-override-incompatible.php
  6. 27
      phpunit/code/trait-method-shadows-private.php
  7. 28
      phpunit/code/trait-parent-method-private.php
  8. 28
      phpunit/code/trait-parent-method-protected.php
  9. 18
      phpunit/code/trait-parent-without-parent.php
  10. 8
      phpunit/code/variable-variable-arraydim.php
  11. 12
      phpunit/code/variable-variable-function-call.php
  12. 22
      phpunit/src/ClassTest.php
  13. 29
      phpunit/src/InheritanceErrorTest.php
  14. 14
      phpunit/src/VariableVariableTest.php
  15. 23
      src/CompilerBase.php
  16. 8
      src/Entity/ArgInfo.php
  17. 2
      src/Entity/ConstantDef.php
  18. 9
      src/Entity/FunctionDef.php
  19. 16
      src/Entity/MethodDef.php
  20. 28
      src/Generator/CallArgumentGenerator.php
  21. 2
      src/Generator/TypeCheckGenerator.php
  22. 4
      src/Parser/ArrayExpressionTrait.php
  23. 13
      src/Parser/AssignOpTrait.php
  24. 9
      src/Parser/ClassConstantFetchTrait.php
  25. 32
      src/Parser/MethodCallTrait.php
  26. 97
      src/Preprocessor.php
  27. 10
      src/Resolver/ClassConstantValueTrait.php
  28. 2
      src/Resolver/NameResolutionTrait.php
  29. 338
      src/Translator.php
  30. 41
      src/gen_stub.php
  31. 42
      tests/compiler/class/interface-method-self-return.phpt
  32. 28
      tests/compiler/coalesce/assign-coalesce-undefined-var.phpt
  33. 51
      tests/compiler/const/class-const-override-variants.phpt
  34. 30
      tests/compiler/const/class-const-override.phpt
  35. 43
      tests/compiler/const/class-const-parent-cross-ns.phpt
  36. 62
      tests/compiler/namespace/interface-impl-param-type-cross-ns.phpt
  37. 56
      tests/compiler/namespace/interface-self-return-namespaced.phpt
  38. 80
      tests/compiler/ref/dynamic-return-reference-argument.phpt
  39. 32
      tests/compiler/trait/trait-method-parent-return.phpt
  40. 38
      tests/compiler/trait/trait-method-self-return-interface.phpt
  41. 35
      tests/compiler/trait/trait-method-static-return-interface.phpt
  42. 36
      tests/compiler/trait/trait-parent-constructor.phpt
  43. 52
      tests/compiler/trait/trait-parent-method-inherited.phpt
  44. 34
      tests/compiler/trait/trait-parent-method-protected.phpt
  45. 36
      tests/compiler/trait/trait-parent-method-reference.phpt
  46. 34
      tests/compiler/trait/trait-parent-method.phpt

@ -0,0 +1,13 @@
<?php
class FinalConstantParent
{
final public const VALUE = 1;
}
class FinalConstantChild extends FinalConstantParent
{
public const VALUE = 2;
}
function main() {}

@ -0,0 +1,13 @@
<?php
class TypedConstantParent
{
public const int VALUE = 1;
}
class UntypedConstantChild extends TypedConstantParent
{
public const VALUE = 1;
}
function main() {}

@ -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
{
}

@ -0,0 +1,8 @@
<?php
function variable_variable_with_array_dim(): void
{
$foo = ['bar' => 'hello'];
${$foo['bar']} = 'world';
echo $hello;
}

@ -0,0 +1,12 @@
<?php
function variable_name(): string
{
return 'hello';
}
function variable_variable_with_function_call(): void
{
${variable_name()} = 'world';
echo $hello;
}

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

@ -168,6 +168,16 @@ class InheritanceErrorTest extends TestCase
$this->exec('must be compatible', 'inheritance_error_const_type.php');
}
public function testTypedConstantCannotBeOverriddenWithoutDeclaredType()
{
$this->exec('must be compatible', 'inheritance_error_const_missing_type.php');
}
public function testFinalConstantCannotBeOverridden()
{
$this->exec('Cannot override final constant', 'inheritance_error_const_final.php');
}
public function testConstantVisibilityMismatch()
{
$this->exec('must be compatible', 'inheritance_error_const_visibility.php');
@ -306,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');
}
}

@ -0,0 +1,14 @@
<?php
class VariableVariableTest extends \BaseTest
{
public function testVariableVariableWithArrayDimThrowsUnsupportedError(): void
{
$this->exec('The `$$` syntax is not supported', 'variable-variable-arraydim.php');
}
public function testVariableVariableWithFunctionCallThrowsUnsupportedError(): void
{
$this->exec('The `$$` syntax is not supported', 'variable-variable-function-call.php');
}
}

@ -1020,6 +1020,12 @@ class CompilerBase implements PropertyAccessContext
return ltrim($this->namespace . '\\' . $this->class, '\\');
}
protected function getFullClassLikeName(): string
{
$name = $this->class !== '' ? $this->class : $this->interface;
return ltrim($this->namespace . '\\' . $name, '\\');
}
protected function getFullMethodName(string $fullClassName, string $method): string
{
return strtolower($fullClassName . '::' . $method);
@ -1066,6 +1072,21 @@ class CompilerBase implements PropertyAccessContext
return $this->wrapVoidExprAsNull($expr, $this->parseExpr($expr));
}
/**
* Snapshot a reference-returning call before a by-value container can retain
* its php::Ref. Assigning to an existing Var detaches the reference, unlike
* constructing a Variant directly from Ref. Keep the assignment inline so
* earlier arguments or array elements retain PHP's evaluation order.
*/
protected function materializeRefReturnAsValue(NodeAbstract $value, string $expr): string
{
if ($value instanceof Expr\CallLike && $this->resolveRefReturningCall($value) !== false) {
$tmpVar = $this->addTmpVar(Type::VAR);
return '(' . $tmpVar . ' = ' . $expr . ')';
}
return $expr;
}
protected function getObjectPropVarName(string $object, string $prop): string
{
return self::OBJECT_PROP . $object . self::NAMESPACE_SEPARATOR . $prop;
@ -1273,7 +1294,7 @@ class CompilerBase implements PropertyAccessContext
protected function parseVariable(Variable $expr): string
{
if (is_object($expr->name) and $this->isVarExpr($expr->name)) {
if (!is_string($expr->name)) {
$this->fatalError($expr, 'The `$$` syntax is not supported');
}
if ($this->isSuperGlobal($expr->name)) {

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

@ -19,6 +19,8 @@ class ConstantDef
public string $arrayExpr = '';
public string $class = '';
public ?NodeAbstract $valueExpr = null;
/** Explicit declared type (e.g. `const int FOO`); null for inferred/untyped constants. */
public ?string $declaredType = null;
public function __construct(string $name, int $flags, string $type, string $value)
{

@ -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;
@ -586,6 +598,13 @@ trait CallArgumentGenerator
protected function materializeCallArgValue(NodeAbstract $value, string $expr): string
{
// A call that returns by reference yields a live php::Ref aliasing the
// callee's storage. When such a call feeds a by-value argument, PHP takes
// a value snapshot at evaluation time (left to right), so later mutations
// to the aliased storage must not be observable. The dynamic ArgList keeps
// references verbatim (Ctor::CopyRef), so we dereference into a temporary
// value at the point of the call.
$expr = $this->materializeRefReturnAsValue($value, $expr);
if (!$this->shouldMaterializeCallArg($value)) {
return $expr;
}
@ -709,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))) {
@ -781,4 +802,3 @@ trait CallArgumentGenerator
}
}

@ -104,7 +104,7 @@ trait TypeCheckGenerator
}
if ($name === 'self') {
$class = $this->getFullClassName();
$class = $this->getFullClassLikeName();
} elseif ($name === 'parent') {
$class = $this->classDef->extends ?? '';
} elseif ($name === 'static') {

@ -60,7 +60,7 @@ trait ArrayExpressionTrait
$this->indentLevel++;
foreach ($items as $item) {
$this->assertExprCanBeUsedAsValue($item->value, 'array value');
$value = $this->parseIdentifier($item->value);
$value = $this->materializeRefReturnAsValue($item->value, $this->parseIdentifier($item->value));
if ($item->key) {
$this->assertExprCanBeUsedAsValue($item->key, 'array key');
$key = $this->parseArrayKey($item->key);
@ -221,7 +221,7 @@ trait ArrayExpressionTrait
}
$value = $this->convertToRef($item->value);
} else {
$value = $this->parseIdentifier($item->value);
$value = $this->materializeRefReturnAsValue($item->value, $this->parseIdentifier($item->value));
}
if ($item->unpack) {
$this->context->beforeStmtLines[] = $this->getIndent() . $tmpVar . '.merge(' . $value . ');';

@ -862,9 +862,17 @@ trait AssignOpTrait
protected function parseAssignOpCoalesce(Expr\AssignOp\Coalesce $expr): string
{
$this->checkLeftValue($expr->var);
// An undefined variable must exist before generating its isset check.
// Keep it as Variant so NULL remains distinguishable from native defaults.
$var = $this->isVarExpr($expr->var) ? $this->parseIdentifier($expr->var) : null;
if ($var !== null && !$this->hasVar($var)) {
$this->addLocalVar($var, Type::VAR);
}
$isset = $this->parseChainedExpr($expr->var, self::OP_ISSET);
$var = $this->parseWritableIdentifier($expr->var);
$var ??= $this->parseWritableIdentifier($expr->var);
$propertyWriteTarget = $this->preparePropertyWriteTarget($expr->var);
if ($propertyWriteTarget !== null) {
@ -878,9 +886,6 @@ trait AssignOpTrait
if ($this->isVarExpr($expr->expr) and !$this->hasVar($right)) {
$this->errorUndefinedVariable($expr->expr);
}
if ($this->isVarExpr($expr->var) and !$this->hasVar($var)) {
$this->addLocalVar($var, $this->getNormalAssignType($this->detectTypeOfExpr($expr->expr)));
}
return '(' . $isset . '?' . $var . ':(' . $var . ' = ' . $right . '))';
}

@ -31,6 +31,14 @@ trait ClassConstantFetchTrait
$self = true;
$class = $this->class;
}
} elseif ($class === 'parent') {
if (!$this->classDef || !$this->classDef->extends) {
$this->fatalError($expr, 'Cannot use "parent" outside a class or class does not extend any class');
}
// extends is already fully resolved. Keep the leading slash so the
// current namespace is not applied again below.
$class = '\\' . $this->classDef->extends;
$self = true;
}
$const = $this->escapeString($this->parseIdentifier($expr->name));
@ -98,4 +106,3 @@ trait ClassConstantFetchTrait
}
}

@ -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;
@ -757,24 +783,89 @@ class Preprocessor extends CompilerBase
'Scalar_String' => Type::STR,
default => Type::VAR,
};
// `::class` is a compile-time magic constant that always yields a string,
// so a constant declared as `X = self::class` (or `Foo::class`) must be
// typed as a string rather than a generic variant.
if ($type === Type::VAR
&& $const->value instanceof Node\Expr\ClassConstFetch
&& strtolower((string) $const->value->name) === 'class') {
$type = Type::STR;
}
// A constant whose value references another class constant
// (e.g. `X = ParentClass::Y` or `X = self::Y`) must take the referenced
// constant's type. This keeps override compatibility checks and the C++
// declaration correct, mirroring PHP where overriding an untyped constant
// with a value of any (compatible) type is allowed.
if ($type === Type::VAR
&& $const->value instanceof Node\Expr\ClassConstFetch
&& $const->value->class instanceof Node\Name) {
$refType = $this->resolveReferencedConstantType($const->value, $this->getFullClassName());
if ($refType !== null) {
$type = $refType;
}
}
}
$constName = $this->parseIdentifier($const->name);
if ($this->classDef->hasConstant($constName)) {
$this->fatalError($v, "Duplicate constant `{$constName}`");
}
$constInfo = $this->parseClassLikeConstant($const, $flags, $type, $class);
$constInfo = $this->parseClassLikeConstant($const, $flags, $type, $class, $declaredType);
$constInfo->class = $class;
$this->classDef->constants[$constInfo->name] = $constInfo;
}
}
private function parseClassLikeConstant(Node\Const_ $const, int $flags, string $type, string $class = ''): ConstantDef
/**
* Resolve the compile-time type of a class constant whose value is a
* `ClassConstFetch` referencing another constant (e.g. `X = ParentClass::Y`
* or `X = self::Y`). Returns the referenced constant's type, or null when
* the reference cannot be resolved yet (for instance when the referenced
* class has not been prepared). `::class` always resolves to a string.
*/
private function resolveReferencedConstantType(Node\Expr\ClassConstFetch $fetch, string $currentClass): ?string
{
$constName = $fetch->name->toString();
if (strcasecmp($constName, 'class') === 0) {
return Type::STR;
}
if (!($fetch->class instanceof Node\Name)) {
return null;
}
$className = $fetch->class->toString();
if (strcasecmp($className, 'self') === 0 || strcasecmp($className, 'static') === 0) {
$targetClass = $currentClass;
} elseif (strcasecmp($className, 'parent') === 0) {
$targetClass = $this->getParentClass($currentClass);
} else {
$targetClass = $this->getNamespacedClassName($className);
}
if ($targetClass === '' || !$this->hasClass($targetClass)) {
return null;
}
$def = $this->getClass($targetClass);
if (!$def->hasConstant($constName)) {
return null;
}
$refConst = $def->getConstant($constName);
// Follow the chain in case the referenced constant is itself an
// expression that resolves to another constant.
if ($refConst->type !== Type::VAR) {
return $refConst->type;
}
if ($refConst->valueExpr instanceof Node\Expr\ClassConstFetch) {
return $this->resolveReferencedConstantType($refConst->valueExpr, $targetClass);
}
return null;
}
private function parseClassLikeConstant(Node\Const_ $const, int $flags, string $type, string $class = '', ?string $declaredType = null): ConstantDef
{
$constName = $this->parseIdentifier($const->name);
$constValue = $this->parseIdentifier($const->value);
$constInfo = new ConstantDef($constName, $flags, $type, $constValue);
$constInfo->valueExpr = $const->value;
$constInfo->declaredType = $declaredType;
if ($this->context->beforeStmtLines) {
$arrayExpr = '';
@ -1132,7 +1223,7 @@ class Preprocessor extends CompilerBase
default => Type::VAR,
};
}
$constInfo = $this->parseClassLikeConstant($const, $this->parseModifiers($stmt->flags), $type, $class);
$constInfo = $this->parseClassLikeConstant($const, $this->parseModifiers($stmt->flags), $type, $class, $stmt->type ? $type : null);
$this->interfaceDef->constants[$constName] = $constInfo;
}
continue;

@ -116,6 +116,16 @@ trait ClassConstantValueTrait
if ($expr instanceof Node\Expr\ClassConstFetch && $expr->class instanceof Node\Name) {
$constName = $expr->name->toString();
$className = $expr->class->toString();
if (strcasecmp($constName, 'class') === 0) {
// `::class` is a compile-time magic constant that resolves to the
// fully qualified class name of the referenced class.
if (strcasecmp($className, 'self') === 0 || strcasecmp($className, 'static') === 0) {
$className = $class;
} elseif (strcasecmp($className, 'parent') === 0) {
$className = $this->getParentClass($class);
}
return ltrim($this->getNamespacedClassName($className, $this->getNamespaceOfClass($class)), '\\');
}
if (strcasecmp($className, 'self') === 0) {
$className = $class;
} elseif (strcasecmp($className, 'parent') === 0) {

@ -166,7 +166,7 @@ trait NameResolutionTrait
return $this->getTypeFromZendType($typeNameLower);
} else {
if ($typeName === 'self') {
$class = $this->getFullClassName();
$class = $this->getFullClassLikeName();
} elseif ($typeName === 'parent') {
if (!$this->classDef) {
$this->fatalError($type, 'Cannot use "parent" type declaration outside a class');

@ -2453,6 +2453,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) {
@ -2571,6 +2578,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
@ -2770,7 +2832,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 = '';
@ -2819,7 +2886,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, ',') : '';
}
@ -2880,7 +2948,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;
}
@ -3077,6 +3156,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 .= ', ';
}
@ -3341,6 +3423,7 @@ CODE;
return $arg->typeCheck;
}
$declaredClass = $arg->declaredClass ?: $arg->class;
return match ($arg->type) {
Type::INT => [['kind' => 'isInt']],
Type::FLOAT => [['kind' => 'isFloat']],
@ -3348,8 +3431,8 @@ CODE;
Type::STR => [['kind' => 'isString']],
Type::ARRAY => [['kind' => 'isArray']],
Type::RESOURCE => [['kind' => 'isResource']],
Type::OBJECT => $arg->class
? [['kind' => 'instanceof', 'class' => $arg->class]]
Type::OBJECT => $declaredClass
? [['kind' => 'instanceof', 'class' => $declaredClass]]
: [['kind' => 'isObject']],
default => null,
};
@ -3576,10 +3659,37 @@ CODE;
if ($parentConst->flags & Modifiers::PRIVATE) {
continue;
}
if ($childConst->type !== $parentConst->type || $childConst->class !== $parentConst->class) {
if ($parentConst->flags & Modifiers::FINAL) {
$this->fatalError($classStmt,
"Declaration of `{$className}::{$name}` must be compatible " .
"with `{$parentClass}::{$name}`");
"Cannot override final constant `{$parentClass}::{$name}`");
}
// PHP only enforces type compatibility when the parent constant
// carries an explicit declared type. Overriding an untyped constant
// with a value of any type is permitted, so the type check is skipped
// in that case. Visibility is always enforced below.
if ($parentConst->declaredType !== null) {
if ($childConst->declaredType === null) {
$this->fatalError($classStmt,
"Declaration of `{$className}::{$name}` must be compatible " .
"with `{$parentClass}::{$name}`");
}
// An untyped child constant whose value is an expression (e.g.
// `X = ParentClass::Y`) is inferred as a variant. Resolve its real
// type from the referenced constant so the compatibility check uses
// the actual value type.
$childType = $childConst->type;
if ($childType === Type::VAR
&& $childConst->valueExpr instanceof Node\Expr\ClassConstFetch) {
$resolved = $this->resolveReferencedConstantType($childConst->valueExpr, $this->getFullClassName());
if ($resolved !== null) {
$childType = $resolved;
}
}
if ($childType !== $parentConst->type || $childConst->class !== $parentConst->class) {
$this->fatalError($classStmt,
"Declaration of `{$className}::{$name}` must be compatible " .
"with `{$parentClass}::{$name}`");
}
}
if ($this->getVisibilityRank($childConst->flags) < $this->getVisibilityRank($parentConst->flags)) {
$this->fatalError($classStmt,
@ -3599,6 +3709,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);
@ -3709,10 +3822,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;
}
@ -3742,6 +3885,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 &&

@ -2354,18 +2354,29 @@ class EvaluatedValue
}
if ($expr instanceof Expr\ClassConstFetch) {
$constName = $expr->name->__toString();
if (strcasecmp($constName, 'class') === 0) {
// `::class` is a compile-time magic constant that resolves to the
// fully qualified class name of the referenced class.
$className = getClassConstFetchClassName($expr);
if (strcasecmp($className, 'self') === 0 || strcasecmp($className, 'static') === 0) {
return ClassInfo::$currentClass;
}
if (strcasecmp($className, 'parent') === 0) {
return getTranslator()->getParentClass(ClassInfo::$currentClass);
}
return ltrim($className, '\\');
}
$class = getClassConstFetchClassName($expr);
if ($class === 'self') {
$constName = ClassInfo::$currentClass . "::" . $expr->name->__toString();
$constName = ClassInfo::$currentClass . "::" . $constName;
if (isset($allConstInfos[$constName])) {
return formatConstValue($allConstInfos[$constName]->getValue($allConstInfos)->value);
} else {
return formatConstValue(getTranslator()->getClassConstValue($expr, ClassInfo::$currentClass, $expr->name->toString()));
return formatConstValue(getTranslator()->getClassConstValue($expr, ClassInfo::$currentClass, $constName));
}
} elseif ($expr->name->__toString() === 'class') {
return $class;
} else {
return formatConstValue(getTranslator()->getClassConstValue($expr, $class, $expr->name->__toString(), ClassInfo::$currentClass));
return formatConstValue(getTranslator()->getClassConstValue($expr, $class, $constName, ClassInfo::$currentClass));
}
} else {
$constName = $expr->name->__toString();
@ -2490,16 +2501,14 @@ class EvaluatedValue
// PHP single-quote to C double-quote string
if ($this->type->isString()) {
if (
$this->expr instanceof PhpParser\Node\Expr\ClassConstFetch
) {
if ($this->expr->class instanceof PhpParser\Node\Name\FullyQualified and
$this->expr->name instanceof PhpParser\Node\Identifier and
if ($this->expr instanceof PhpParser\Node\Expr\ClassConstFetch) {
if ($this->expr->name instanceof PhpParser\Node\Identifier and
$this->expr->name->__toString() === 'class') {
$expr = '"' . addcslashes($this->expr->class->name, '\\') . '"';
} else {
return $this->value;
// `::class` is a compile-time magic constant that resolves to the
// fully qualified class name (already stored in $this->value).
return '"' . addcslashes($this->value, '\\') . '"';
}
return $this->value;
} elseif ($this->expr instanceof Expr\ConstFetch) {
return getTranslator()->getConstValue($this->expr->name->toString());
} elseif (!($this->expr instanceof String_)) {
@ -2899,6 +2908,12 @@ class ConstInfo extends VariableLike
$code .= "\tzend_string *const_{$constName}_name = zend_string_init_interned(\"$constName\", sizeof(\"$constName\") - 1, true);\n";
$nameCode = "const_{$constName}_name";
// A child class may override a constant inherited from its parent. The
// runtime copies the parent's constants into the child, so re-declaring
// the constant would fail with "Cannot redefine class constant".
// Drop any inherited entry first so the child's value replaces it.
$code .= "\tzend_hash_del(&class_entry->constants_table, $nameCode);\n";
if ($this->exposedDocComment) {
$commentCode = "const_{$constName}_comment";
$escapedCommentInit = $this->exposedDocComment->getInitCode();

@ -0,0 +1,42 @@
--TEST--
interface method with `self` return type implemented by class (fluent interface), and namespace block containing comments
--FILE--
<?php
namespace {
interface TestInterface
{
public function get(): self;
}
class TestClass implements TestInterface
{
public int $value = 0;
public function get(): self
{
return $this;
}
public function setValue(int $value): self
{
$this->value = $value;
return $this;
}
}
function main()
{
$test = new TestClass;
// get() returns self, so the result still satisfies the interface
var_dump($test->get() instanceof TestInterface);
var_dump($test === $test->get());
// fluent chaining of self-returning methods
var_dump($test->get()->setValue(42)->value);
}
}
?>
--EXPECT--
bool(true)
bool(true)
int(42)

@ -0,0 +1,28 @@
--TEST--
assign coalesce on undefined variable
--FILE--
<?php
$a ??= 123;
var_dump($a);
$b ??= 'foo';
$b ??= 'bar';
var_dump($b);
$c ??= null;
var_dump(isset($c));
$c ??= 'after-null';
var_dump($c);
for ($i = 0; $i < 2; $i++) {
$d ??= printf("default\n");
}
var_dump($d);
?>
--EXPECT--
int(123)
string(3) "foo"
bool(false)
string(10) "after-null"
default
int(8)

@ -0,0 +1,51 @@
--TEST--
class const override variants (self::class, parent::class, references, multi-level)
--FILE--
<?php
declare(strict_types=1);
class Base
{
public const NAME = 'Base';
public const GREETING = 'hello';
public const VALUE = 42;
}
class Other
{
public const TAG = 'other';
}
class Mid extends Base
{
public const NAME = Base::GREETING; // 'hello'
public const SELF_NAME = self::class; // 'Mid'
public const PARENT_NAME = parent::class; // 'Base'
public const CROSS = Other::TAG; // 'other'
}
class Leaf extends Mid
{
public const VALUE = Mid::NAME; // 'hello' (overrides Base::VALUE int with a string)
public const LEAF_NAME = self::class; // 'Leaf'
public const GREET = Base::GREETING; // 'hello'
}
function main()
{
var_dump(Mid::NAME, Mid::SELF_NAME, Mid::PARENT_NAME, Mid::CROSS);
var_dump(Leaf::VALUE, Leaf::LEAF_NAME, Leaf::GREET);
var_dump(Base::VALUE, Leaf::VALUE);
}
?>
--EXPECT--
string(5) "hello"
string(3) "Mid"
string(4) "Base"
string(5) "other"
string(5) "hello"
string(4) "Leaf"
string(5) "hello"
int(42)
string(5) "hello"

@ -0,0 +1,30 @@
--TEST--
class const override referencing another constant
--FILE--
<?php
declare(strict_types=1);
abstract class ParentClass
{
public const A = 'A';
public const B = 'B';
}
class TestClass extends ParentClass
{
public const A = ParentClass::B;
public const B = 'bbb';
}
function main()
{
var_dump(ParentClass::A, ParentClass::B);
var_dump(TestClass::A, TestClass::B);
}
?>
--EXPECT--
string(1) "A"
string(1) "B"
string(1) "B"
string(3) "bbb"

@ -0,0 +1,43 @@
--TEST--
parent class constants resolve across namespaces
--FILE--
<?php
namespace Library {
class Base
{
public const TOKEN = 'base';
}
}
namespace Application {
class Sibling
{
}
class Child extends \Library\Base
{
public const PARENT_NAME = parent::class;
public const SIBLING_NAME = Sibling::class;
public static function dumpParent(): void
{
var_dump(parent::class, parent::TOKEN);
}
}
}
namespace {
function main(): void
{
var_dump(\Application\Child::PARENT_NAME);
var_dump(\Application\Child::SIBLING_NAME);
\Application\Child::dumpParent();
}
}
?>
--EXPECT--
string(12) "Library\Base"
string(19) "Application\Sibling"
string(12) "Library\Base"
string(4) "base"

@ -0,0 +1,62 @@
--TEST--
Cross-namespace interface implementation parameter compatibility is declaration-order independent
--FILE--
<?php
namespace A {
use B\I0;
use B\I1;
use B\I2;
abstract class Test implements I2
{
public function test(I1 $a): bool
{
return true;
}
public function testParent(I0 $a): bool
{
return true;
}
}
}
namespace B {
interface I0
{
}
interface I1 extends I0
{
}
interface I2
{
public function test(I1 $a): bool;
public function testParent(I1 $a): bool;
}
class Impl1 implements I1
{
}
}
namespace {
class Concrete extends \A\Test
{
}
function main()
{
$obj = new Concrete();
var_dump($obj->test(new \B\Impl1()));
var_dump($obj->testParent(new \B\Impl1()));
echo "done\n";
}
}
?>
--EXPECT--
bool(true)
bool(true)
done

@ -0,0 +1,56 @@
--TEST--
interface method `self` return type resolves to the interface's fully-qualified name inside a named namespace
--FILE--
<?php
namespace App {
interface Chainable
{
public function chain(): self;
public function maybe(bool $present): ?self;
public function combine(self $other): self;
}
// comment inside a named namespace block (Stmt_Nop)
class Widget implements Chainable
{
public array $log = [];
public function chain(): self
{
$this->log[] = 'chain';
return $this;
}
public function maybe(bool $present): ?self
{
return $present ? $this : null;
}
public function combine(Chainable $other): self
{
return $this;
}
}
}
namespace {
function main()
{
$w = new \App\Widget();
var_dump($w->chain()->chain() instanceof \App\Chainable);
var_dump(count($w->log));
var_dump($w->maybe(true) instanceof \App\Chainable);
var_dump($w->maybe(false));
var_dump($w->combine(new \App\Widget()) === $w);
}
}
?>
--EXPECT--
bool(true)
int(2)
bool(true)
NULL
bool(true)

@ -0,0 +1,80 @@
--TEST--
Reference-returning calls are copied by value when used as call arguments or array elements
--FILE--
<?php
function main()
{
$v1 = &test1();
var_dump($v1);
$v2 = &test1();
var_dump($v2);
var_dump($v1, $v2);
$v1 = 0;
var_dump($v1, $v2);
var_dump(test1(), test2());
var_dump([test1(), test2()]);
var_dump(['first' => test1(), test2()]);
var_dump(value_order('arg-left'), ref_order('arg-ref'));
var_dump([value_order('array-left'), ref_order('array-ref')]);
}
function &test1()
{
$callback = 'test2';
return $callback();
}
function &test2()
{
static $value = 0;
++$value;
return $value;
}
function value_order(string $label): string
{
echo "$label\n";
return $label;
}
function &ref_order(string $label)
{
static $value = 42;
echo "$label\n";
return $value;
}
?>
--EXPECT--
int(1)
int(2)
int(2)
int(2)
int(0)
int(0)
int(1)
int(2)
array(2) {
[0]=>
int(3)
[1]=>
int(4)
}
array(2) {
["first"]=>
int(5)
[0]=>
int(6)
}
arg-left
arg-ref
string(8) "arg-left"
int(42)
array-left
array-ref
array(2) {
[0]=>
string(10) "array-left"
[1]=>
int(42)
}

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