fix: harden typed constant inheritance

master
韩天峰 9 hours ago
parent e2c32adda2
commit f03c0b12c6
  1. 28
      phpunit/code/const_override_dnf_covariant.php
  2. 15
      phpunit/code/interface_const_inheritance_cycle.php
  3. 20
      phpunit/code/interface_const_trait_mismatch.php
  4. 5
      phpunit/src/ConstantOverrideCovarianceTest.php
  5. 16
      phpunit/src/InterfaceConstantTest.php
  6. 69
      src/Translator.php
  7. 38
      src/gen_stub.php
  8. 51
      tests/compiler/class/typed-class-constant-covariance.phpt

@ -0,0 +1,28 @@
<?php
interface DnfLeft
{
}
interface DnfRight
{
}
enum DnfBoth implements DnfLeft, DnfRight
{
case Value;
}
class DnfConstantParent
{
const (DnfLeft&DnfRight)|stdClass VALUE = DnfBoth::Value;
}
class DnfConstantChild extends DnfConstantParent
{
const DnfLeft&DnfRight VALUE = DnfBoth::Value;
}
function main(): void
{
}

@ -0,0 +1,15 @@
<?php
interface ConstantCycleA extends ConstantCycleB
{
const int A = 1;
}
interface ConstantCycleB extends ConstantCycleA
{
const int B = 2;
}
function main(): void
{
}

@ -0,0 +1,20 @@
<?php
interface TraitConstantContract
{
const int VALUE = 1;
}
trait IncompatibleConstantTrait
{
const string VALUE = 'wrong';
}
class TraitConstantImplementation implements TraitConstantContract
{
use IncompatibleConstantTrait;
}
function main(): void
{
}

@ -14,6 +14,11 @@ class ConstantOverrideCovarianceTest extends BaseTest
$this->compile('const_override_covariant.php');
}
public function testNarrowingDnfDeclaredTypeCompiles(): void
{
$this->compile('const_override_dnf_covariant.php');
}
public function testWideningDeclaredTypeIsRejected(): void
{
$this->exec(

@ -71,4 +71,20 @@ class InterfaceConstantTest extends BaseTest
'enum_interface_const_final.php',
);
}
public function testInterfaceInheritanceCycleFailsWithoutRecursingForever(): void
{
$this->exec(
'Interface inheritance cycle detected',
'interface_const_inheritance_cycle.php',
);
}
public function testTraitConstantMustSatisfyImplementedInterface(): void
{
$this->exec(
'Declaration of `TraitConstantImplementation::VALUE` must be compatible with `TraitConstantContract::VALUE`',
'interface_const_trait_mismatch.php',
);
}
}

@ -91,6 +91,9 @@ class Translator extends Preprocessor
* @var array<string, array<string, array{const: ConstantDef, origin: string}>>
*/
private array $effectiveConstantTables = [];
/** @var array<string, true> Class-like constants tables being constructed. */
private array $effectiveConstantTableVisiting = [];
protected array $globalHeaders = [
'cstring',
'phpx.h',
@ -5794,42 +5797,52 @@ CODE;
*
* @return array<string, array{const: ConstantDef, origin: string}>
*/
private function getEffectiveConstantTable(ClassDef|InterfaceDef $def): array
private function getEffectiveConstantTable(ClassDef|InterfaceDef $def, NodeAbstract $errorNode): 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;
if (isset($this->effectiveConstantTableVisiting[$key])) {
$kind = $def instanceof InterfaceDef ? 'Interface' : 'Class';
$this->fatalError($errorNode, "{$kind} inheritance cycle detected at `{$ownName}`");
}
$this->effectiveConstantTableVisiting[$key] = true;
try {
$table = [];
if ($def instanceof ClassDef) {
if ($def->extends !== '' && !$def->inheritedFromInternalClass && $this->hasClass($def->extends)) {
foreach ($this->getEffectiveConstantTable($this->getClass($def->extends), $errorNode) 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 ($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;
foreach ($parents as $interfaceName) {
if (!$this->hasInterface($interfaceName)) {
continue;
}
foreach ($this->getEffectiveConstantTable($this->getInterface($interfaceName), $errorNode) as $name => $entry) {
$table[$name] ??= $entry;
}
}
return $this->effectiveConstantTables[$key] = $table;
} finally {
unset($this->effectiveConstantTableVisiting[$key]);
}
return $this->effectiveConstantTables[$key] = $table;
}
/**
@ -5854,7 +5867,7 @@ CODE;
// 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) {
foreach ($this->getEffectiveConstantTable($this->getClass($classDef->extends), $classStmt) as $name => $entry) {
if (!($entry['const']->flags & Modifiers::PRIVATE)) {
$table[$name] = $entry;
}
@ -5881,7 +5894,7 @@ CODE;
if (!$this->hasInterface($interfaceName)) {
continue;
}
foreach ($this->getEffectiveConstantTable($this->getInterface($interfaceName)) as $name => $entry) {
foreach ($this->getEffectiveConstantTable($this->getInterface($interfaceName), $classStmt) as $name => $entry) {
if (!isset($table[$name])) {
$table[$name] = $entry;
continue;
@ -5920,7 +5933,7 @@ CODE;
if (!$this->hasInterface($parentName)) {
continue;
}
foreach ($this->getEffectiveConstantTable($this->getInterface($parentName)) as $constName => $entry) {
foreach ($this->getEffectiveConstantTable($this->getInterface($parentName), $interfaceStmt) as $constName => $entry) {
if (!isset($table[$constName])) {
$table[$constName] = $entry;
continue;

@ -2728,9 +2728,17 @@ class EvaluatedValue
$result = $evaluator->evaluateDirectly($expr);
// The declared type is useful when an UNKNOWN placeholder must be
// emitted through its @cvalue macro. For a concrete null expression,
// however, the zval must be initialized as null even when the declared
// type is nullable (for example, `const ?int VALUE = null`).
$valueType = $result === null && !$isUnknownConstValue
? SimpleType::null()
: ($constType ?? SimpleType::fromValue($result));
return new EvaluatedValue(
$result, // note: we are generally not interested in the actual value of $result, unless it's a bare value, without constants
$constType ?? SimpleType::fromValue($result),
$valueType,
$cConstName === null ? $expr : new Expr\ConstFetch(new Node\Name($cConstName)),
$visitor->visitedConstants,
$isUnknownConstValue
@ -2907,7 +2915,7 @@ abstract class VariableLike
$typeCode = "";
if ($this->type) {
if ($this->type->isDnf()) {
assert($this instanceof PropertyInfo);
assert($this instanceof PropertyInfo || $this instanceof ConstInfo);
return $this->type->getDnfTypeExpression($this->getDnfTypeFactorySymbol(), '0');
}
$arginfoType = $this->type->toArginfoType();
@ -3069,6 +3077,22 @@ class ConstInfo extends VariableLike
return "constant";
}
public function getDnfTypeFactorySymbol(): string
{
assert($this->name instanceof ClassConstName);
return 'constant_' . implode('_', $this->name->class->getParts())
. '_' . $this->name->getDeclarationName() . '_dnf';
}
public function getDnfTypeFactoryCode(): string
{
if (!$this->type?->isDnf()) {
return '';
}
assert($this->name instanceof ClassConstName);
return $this->type->getDnfTypeDeclarations($this->getDnfTypeFactorySymbol());
}
protected function getFieldSynopsisDefaultLinkend(): string
{
$className = str_replace(["\\", "_"], ["-", "-"], $this->name->class->toLowerString());
@ -4055,6 +4079,15 @@ class ClassInfo {
return $code;
}
public function getDnfConstantTypeFactoryCode(): string
{
$code = '';
foreach ($this->constInfos as $constInfo) {
$code .= $constInfo->getDnfTypeFactoryCode();
}
return $code;
}
/** @param array<string, ConstInfo> $allConstInfos */
public function getRegistration(array $allConstInfos): string
{
@ -5854,6 +5887,7 @@ function generateArgInfoCode(
. " * Stub hash: $stubHash */\n";
foreach ($fileInfo->classInfos as $classInfo) {
$code .= $classInfo->getDnfConstantTypeFactoryCode();
$code .= $classInfo->getDnfPropertyTypeFactoryCode();
}
if (!str_ends_with($code, "*/\n")) {

@ -0,0 +1,51 @@
--TEST--
Typed class and interface constants support covariant overrides at runtime
--FILE--
<?php
interface TypedConstantContract
{
const int|string NUMBER = 1;
const ?int OPTIONAL = null;
}
trait CompatibleConstantTrait
{
const int TRAIT_VALUE = 3;
}
class TypedConstantBase
{
const int|string VALUE = 1;
const ?int EMPTY_VALUE = null;
}
class TypedConstantChild extends TypedConstantBase implements TypedConstantContract
{
use CompatibleConstantTrait;
const int VALUE = 2;
const int EMPTY_VALUE = 6;
const int NUMBER = 4;
const int OPTIONAL = 5;
}
function main(): void
{
var_dump(TypedConstantBase::VALUE);
var_dump(TypedConstantBase::EMPTY_VALUE);
var_dump(TypedConstantChild::VALUE);
var_dump(TypedConstantChild::EMPTY_VALUE);
var_dump(TypedConstantChild::NUMBER);
var_dump(TypedConstantChild::OPTIONAL);
var_dump(TypedConstantChild::TRAIT_VALUE);
}
?>
--EXPECT--
int(1)
NULL
int(2)
int(6)
int(4)
int(5)
int(3)
Loading…
Cancel
Save