@ -82,6 +82,15 @@ class Translator extends Preprocessor
// Windows resource file configuration (icon, version info, etc.)
protected array $resourceConfig = [];
/**
* Memoized effective constant tables (constant name => constant and its
* original declaring class/interface), keyed by lowercased class-like
* name. See getEffectiveConstantTable().
*
* @var array< string , array < string , array { const: ConstantDef , origin: string } > >
*/
private array $effectiveConstantTables = [];
protected array $globalHeaders = [
'cstring',
'phpx.h',
@ -2856,6 +2865,7 @@ CODE;
$this->parseConstDef($v);
} elseif ($v instanceof Node\Stmt\Interface_) {
$this->validateInterfaceOverrideAttributes($v);
$this->validateInterfaceConstants($v);
} elseif (!$v instanceof Node\Stmt\Nop) {
$this->unsupportedSyntax($v);
}
@ -3030,6 +3040,7 @@ CODE;
$this->parseGroupUse($v2);
} elseif ($v2 instanceof Node\Stmt\Interface_) {
$this->validateInterfaceOverrideAttributes($v2);
$this->validateInterfaceConstants($v2);
} elseif (!$v2 instanceof Node\Stmt\Nop) {
$this->unsupportedSyntax($v2);
}
@ -4109,6 +4120,7 @@ CODE;
if (!$class instanceof Node\Stmt\Trait_) {
$this->validateOverrideAttributes($class);
$this->checkInterfaceImplementations($class);
$this->checkInheritedConstantContracts($class);
$this->checkInheritedAbstractMethodsAreImplemented($class);
}
$code = $this->genNativeMethod($methodCodes);
@ -5481,6 +5493,303 @@ CODE;
return $this->getVisibilityRank($property->flags);
}
/**
* Parse a class constant declaration and additionally record the
* accepted-types DNF of its declared type. The base implementation
* collapses composite declared types (unions, nullables, intersections)
* to a single variant type, which is too coarse for the covariant
* constant-override checks; the DNF preserves the full declaration.
*/
protected function parseClassConstDef(Node\Stmt\ClassConst $v): void
{
parent::parseClassConstDef($v);
if ($v->type === null) {
return;
}
$typeInfo = $this->buildTypeCheckFromNode($v->type, true);
foreach ($v->consts as $const) {
$constName = $this->parseIdentifier($const->name);
if ($this->classDef !== null & & $this->classDef->hasConstant($constName)) {
$constDef = $this->classDef->getConstant($constName);
$constDef->typeCheck = $typeInfo['check'];
$constDef->typeStr = $typeInfo['typeStr'];
}
}
}
/**
* Parse an interface declaration and additionally record the
* accepted-types DNF of each typed constant, mirroring the
* parseClassConstDef() override above. The base implementation collapses
* composite declared types to a single variant type.
*/
protected function parseInterface(Node\Stmt\Interface_ $v): void
{
parent::parseInterface($v);
$name = $this->parseIdentifier($v->name);
$interfaceName = $this->namespace === '' ? $name : $this->namespace . '\\' . $name;
if (!$this->hasInterface($interfaceName)) {
return;
}
$interfaceDef = $this->getInterface($interfaceName);
foreach ($v->stmts as $stmt) {
if (!$stmt instanceof Node\Stmt\ClassConst || $stmt->type === null) {
continue;
}
$typeInfo = $this->buildTypeCheckFromNode($stmt->type, true);
foreach ($stmt->consts as $const) {
$constName = $this->parseIdentifier($const->name);
if (isset($interfaceDef->constants[$constName])) {
$interfaceDef->constants[$constName]->typeCheck = $typeInfo['check'];
$interfaceDef->constants[$constName]->typeStr = $typeInfo['typeStr'];
}
}
}
}
/**
* Accepted-types DNF for a constant's DECLARED type, or null when the
* constant is untyped (no type contract to enforce on overrides).
*/
private function getConstantAcceptedTypes(ConstantDef $const): ?array
{
if ($const->declaredType === null) {
return null;
}
if ($const->typeCheck !== []) {
return $const->typeCheck;
}
return match ($const->declaredType) {
Type::INT => [['kind' => 'isInt']],
Type::FLOAT => [['kind' => 'isFloat']],
Type::BOOL => [['kind' => 'isBool']],
Type::STR => [['kind' => 'isString']],
Type::ARRAY => [['kind' => 'isArray']],
Type::RESOURCE => [['kind' => 'isResource']],
Type::OBJECT => $const->class !== ''
? [['kind' => 'instanceof', 'class' => $const->class]]
: [['kind' => 'isObject']],
default => [['kind' => 'isMixed']],
};
}
/**
* PHP 8.3 typed class constants are covariant: an override may narrow the
* declared type but never widen it or move to an unrelated type.
*/
private function isConstantTypeOverrideCompatible(ConstantDef $childConst, array $parentAccepted): bool
{
if ($childConst->declaredType === null) {
return false;
}
foreach ($this->getConstantAcceptedTypes($childConst) as $childType) {
if (!$this->isReturnTypeCoveredBy($childType, $parentAccepted)) {
return false;
}
}
return true;
}
/**
* The constants visible on a class-like, keyed by name, each entry
* carrying its ConstantDef and the ORIGINAL declaring class or interface.
* Mirrors Zend's constants-table build order (parent class first, own
* declarations, then interfaces). Conflicts between ancestors are resolved
* silently here — first entry wins — because they are reported by the
* declaring type's own validation pass when it is compiled.
*
* @return array< string , array { const: ConstantDef , origin: string } >
*/
private function getEffectiveConstantTable(ClassDef|InterfaceDef $def): array
{
$ownName = $def->getNamespacedName(false);
$key = strtolower($ownName);
if (isset($this->effectiveConstantTables[$key])) {
return $this->effectiveConstantTables[$key];
}
$table = [];
if ($def instanceof ClassDef) {
if ($def->extends !== '' & & !$def->inheritedFromInternalClass & & $this->hasClass($def->extends)) {
foreach ($this->getEffectiveConstantTable($this->getClass($def->extends)) as $name => $entry) {
// Private constants are not inherited.
if (!($entry['const']->flags & Modifiers::PRIVATE)) {
$table[$name] = $entry;
}
}
}
foreach ($def->constants as $name => $const) {
$table[$name] = ['const' => $const, 'origin' => $ownName];
}
$parents = $def->implements;
} else {
foreach ($def->constants as $name => $const) {
$table[$name] = ['const' => $const, 'origin' => $ownName];
}
$parents = $def->extendsList ?: ($def->extends ? [$def->extends] : []);
}
foreach ($parents as $interfaceName) {
if (!$this->hasInterface($interfaceName)) {
continue;
}
foreach ($this->getEffectiveConstantTable($this->getInterface($interfaceName)) as $name => $entry) {
$table[$name] ??= $entry;
}
}
return $this->effectiveConstantTables[$key] = $table;
}
/**
* Validate the class's constants against every constant contract arriving
* through an interface — implemented directly, inherited through a parent
* interface, or carried by an ancestor class (Zend keeps the original
* declaring interface in the inherited constants table, so a final or
* typed interface constant binds every transitive subclass):
*
* - a final interface constant cannot be overridden;
* - the same constant name arriving from two different declarations is
* ambiguous unless the class declares it itself;
* - an override of an interface constant must stay public;
* - typed constants are covariant (see checkConstantOverride()).
*/
private function checkInheritedConstantContracts(Node\Stmt\Class_|Node\Stmt\Enum_ $classStmt): void
{
$classDef = $this->classDef;
$className = $classDef->getNamespacedName(false);
// Constants visible before this class's interfaces are merged: the
// parent chain's effective table, then the class's own declarations.
$table = [];
if ($classDef->extends !== '' & & !$classDef->inheritedFromInternalClass & & $this->hasClass($classDef->extends)) {
foreach ($this->getEffectiveConstantTable($this->getClass($classDef->extends)) as $name => $entry) {
if (!($entry['const']->flags & Modifiers::PRIVATE)) {
$table[$name] = $entry;
}
}
}
foreach ($classDef->constants as $name => $const) {
if (isset($table[$name]) & & $this->hasInterface($table[$name]['origin'])) {
// The nearest inherited declaration originates in an interface
// reached through an ancestor class; class-chain declarations
// are validated by checkConstantOverride() instead.
$this->validateConstantAgainstInheritedEntry(
$classStmt,
'Class',
$className,
$name,
['const' => $const, 'origin' => $className],
$table[$name],
);
}
$table[$name] = ['const' => $const, 'origin' => $className];
}
foreach ($classDef->implements as $interfaceName) {
if (!$this->hasInterface($interfaceName)) {
continue;
}
foreach ($this->getEffectiveConstantTable($this->getInterface($interfaceName)) as $name => $entry) {
if (!isset($table[$name])) {
$table[$name] = $entry;
continue;
}
$this->validateConstantAgainstInheritedEntry(
$classStmt,
'Class',
$className,
$name,
$table[$name],
$entry,
);
}
}
}
/**
* Validate an interface's own constants against the ones inherited from
* its parent interfaces, and inherited same-name constants against each
* other (Zend: ambiguous unless declared by the interface itself).
*/
private function validateInterfaceConstants(Node\Stmt\Interface_ $interfaceStmt): void
{
$name = $this->parseIdentifier($interfaceStmt->name);
$interfaceName = $this->namespace === '' ? $name : $this->namespace . '\\' . $name;
if (!$this->hasInterface($interfaceName)) {
return;
}
$interfaceDef = $this->getInterface($interfaceName);
$table = [];
foreach ($interfaceDef->constants as $constName => $const) {
$table[$constName] = ['const' => $const, 'origin' => $interfaceName];
}
foreach ($interfaceDef->extendsList ?: ($interfaceDef->extends ? [$interfaceDef->extends] : []) as $parentName) {
if (!$this->hasInterface($parentName)) {
continue;
}
foreach ($this->getEffectiveConstantTable($this->getInterface($parentName)) as $constName => $entry) {
if (!isset($table[$constName])) {
$table[$constName] = $entry;
continue;
}
$this->validateConstantAgainstInheritedEntry(
$interfaceStmt,
'Interface',
$interfaceName,
$constName,
$table[$constName],
$entry,
);
}
}
}
/**
* Zend's do_inherit_constant_check: $existing is the constant already in
* the type's table (its own declaration, or one inherited earlier), and
* $incoming the same-name constant arriving from another declaration.
*
* @param array{const: ConstantDef, origin: string} $existing
* @param array{const: ConstantDef, origin: string} $incoming
*/
private function validateConstantAgainstInheritedEntry(
NodeAbstract $node,
string $kind,
string $typeName,
string $constName,
array $existing,
array $incoming,
): void {
if ($existing['origin'] === $incoming['origin']) {
// The same original declaration reached through two paths
// (diamond inheritance) never conflicts.
return;
}
if ($incoming['const']->flags & Modifiers::FINAL) {
$this->fatalError($node,
"`{$existing['origin']}::{$constName}` cannot override final constant " .
"`{$incoming['origin']}::{$constName}`");
}
if ($existing['origin'] !== $typeName) {
$this->fatalError($node,
"{$kind} `{$typeName}` inherits both `{$existing['origin']}::{$constName}` and " .
"`{$incoming['origin']}::{$constName}`, which is ambiguous");
}
// The type's own declaration overrides the inherited constant.
$childConst = $existing['const'];
if ($this->getVisibilityRank($childConst->flags) < $this->getVisibilityRank($incoming['const']->flags)) {
$this->fatalError($node,
"Access level to `{$typeName}::{$constName}` must be public " .
"(as in interface `{$incoming['origin']}`)");
}
$parentAccepted = $this->getConstantAcceptedTypes($incoming['const']);
if ($parentAccepted !== null & & !$this->isConstantTypeOverrideCompatible($childConst, $parentAccepted)) {
$this->fatalError($node,
"Declaration of `{$typeName}::{$constName}` must be compatible " .
"with `{$incoming['origin']}::{$constName}`");
}
}
private function checkConstantOverride(Node\Stmt\Class_|Node\Stmt\Trait_|Node\Stmt\Enum_ $classStmt): void
{
$classDef = $this->classDef;
@ -5505,30 +5814,16 @@ CODE;
// 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}`");
}
// in that case. Typed constants are covariant (PHP 8.3): the child
// may narrow the declared type (e.g. int|string -> int) but never
// widen it or move to an unrelated type. Visibility is always
// enforced below.
$parentAccepted = $this->getConstantAcceptedTypes($parentConst);
if ($parentAccepted !== null
& & !$this->isConstantTypeOverrideCompatible($childConst, $parentAccepted)) {
$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,