Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions phpunit/code/const_override_covariant.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?php
class A
{
const int|string X = 1;
const mixed M = 1;
const ?int N = null;
}

class B extends A
{
// PHP 8.3 typed constants are covariant: narrowing is allowed.
const int X = 2;
const string M = 'a';
const int N = 5;
}

function main() {}
12 changes: 12 additions & 0 deletions phpunit/code/const_override_unrelated.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?php
class A
{
const int X = 1;
}

class B extends A
{
const string X = 'a';
}

function main() {}
12 changes: 12 additions & 0 deletions phpunit/code/const_override_widened.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?php
class A
{
const int X = 1;
}

class B extends A
{
const int|string X = 2;
}

function main() {}
13 changes: 13 additions & 0 deletions phpunit/code/enum_interface_const_final.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php
interface I
{
final const X = 1;
}

enum E implements I
{
const X = 2;
case A;
}

function main() {}
14 changes: 14 additions & 0 deletions phpunit/code/interface_const_ambiguous.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?php
interface I1
{
const X = 1;
}

interface I2
{
const X = 1;
}

class C implements I1, I2 {}

function main() {}
12 changes: 12 additions & 0 deletions phpunit/code/interface_const_final_override.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?php
interface I
{
final const X = 1;
}

class C implements I
{
const X = 2;
}

function main() {}
14 changes: 14 additions & 0 deletions phpunit/code/interface_const_final_via_parent.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?php
interface I
{
final const X = 1;
}

class P implements I {}

class C extends P
{
const X = 2;
}

function main() {}
12 changes: 12 additions & 0 deletions phpunit/code/interface_const_type_mismatch.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?php
interface I
{
const int X = 1;
}

class C implements I
{
const string X = 'a';
}

function main() {}
58 changes: 58 additions & 0 deletions phpunit/code/interface_const_valid.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
<?php
interface Base
{
const X = 1;
}

// Diamond: the same original declaration reached through two paths.
interface L extends Base {}
interface R extends Base {}
class Diamond implements L, R {}

interface Typed
{
const int|string N = 1;
const ?int M = null;
}

// Covariant narrowing of a typed interface constant.
class Narrowed implements Typed
{
const int N = 2;
const int M = 5;
}

interface Untyped
{
const V = 1;
}

// An untyped interface constant may be redefined with any value and type.
class Redefined implements Untyped
{
const V = 'other';
}

interface AConst
{
const W = 1;
}

interface BConst
{
const W = 2;
}

// The class's own declaration resolves the two-interface ambiguity.
class Resolves implements AConst, BConst
{
const W = 3;
}

// Enum cases live in a separate table and never conflict with constants.
enum CaseName implements Untyped
{
case V;
}

function main() {}
12 changes: 12 additions & 0 deletions phpunit/code/interface_const_visibility.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?php
interface I
{
const X = 1;
}

class C implements I
{
protected const X = 1;
}

function main() {}
12 changes: 12 additions & 0 deletions phpunit/code/interface_extends_const_incompatible.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?php
interface I
{
const int X = 1;
}

interface J extends I
{
const string X = 'a';
}

function main() {}
32 changes: 32 additions & 0 deletions phpunit/src/ConstantOverrideCovarianceTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?php

use TypePhp\Exception\TestError;

/**
* PHP 8.3 typed class constants are covariant: an override may narrow the
* declared type (int|string -> int, mixed -> string, ?int -> int) but never
* widen it or move to an unrelated type.
*/
class ConstantOverrideCovarianceTest extends BaseTest
{
public function testNarrowingDeclaredTypeCompiles(): void
{
$this->compile('const_override_covariant.php');
}

public function testWideningDeclaredTypeIsRejected(): void
{
$this->exec(
'Declaration of `B::X` must be compatible with `A::X`',
'const_override_widened.php',
);
}

public function testUnrelatedDeclaredTypeIsRejected(): void
{
$this->exec(
'Declaration of `B::X` must be compatible with `A::X`',
'const_override_unrelated.php',
);
}
}
74 changes: 74 additions & 0 deletions phpunit/src/InterfaceConstantTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
<?php

use TypePhp\Exception\TestError;

/**
* Interface constants are real contracts in Zend: a final one cannot be
* overridden anywhere below (the original declaring interface is kept in the
* inherited constants table), a typed one is covariant, an override must stay
* public, and the same name arriving from two different declarations is
* ambiguous unless the class declares the constant itself.
*/
class InterfaceConstantTest extends BaseTest
{
public function testValidInterfaceConstantPatternsCompile(): void
{
$this->compile('interface_const_valid.php');
}

public function testTypedInterfaceConstantMustBeCovariant(): void
{
$this->exec(
'Declaration of `C::X` must be compatible with `I::X`',
'interface_const_type_mismatch.php',
);
}

public function testFinalInterfaceConstantCannotBeOverridden(): void
{
$this->exec(
'`C::X` cannot override final constant `I::X`',
'interface_const_final_override.php',
);
}

public function testFinalInterfaceConstantBindsTransitiveSubclasses(): void
{
$this->exec(
'`C::X` cannot override final constant `I::X`',
'interface_const_final_via_parent.php',
);
}

public function testSameConstantFromTwoInterfacesIsAmbiguous(): void
{
$this->exec(
'Class `C` inherits both `I1::X` and `I2::X`, which is ambiguous',
'interface_const_ambiguous.php',
);
}

public function testInterfaceConstantOverrideMustStayPublic(): void
{
$this->exec(
'Access level to `C::X` must be public (as in interface `I`)',
'interface_const_visibility.php',
);
}

public function testInterfaceExtendingInterfaceChecksConstantTypes(): void
{
$this->exec(
'Declaration of `J::X` must be compatible with `I::X`',
'interface_extends_const_incompatible.php',
);
}

public function testEnumCannotOverrideFinalInterfaceConstant(): void
{
$this->exec(
'`E::X` cannot override final constant `I::X`',
'enum_interface_const_final.php',
);
}
}
10 changes: 10 additions & 0 deletions src/Entity/ConstantDef.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,16 @@ class ConstantDef
/** Explicit declared type (e.g. `const int FOO`); null for inferred/untyped constants. */
public ?string $declaredType = null;

/**
* Accepted-types DNF for the explicitly declared type, in the same format
* as ArgInfo::$typeCheck. Empty when the constant is untyped or the
* declared type accepts everything (`mixed`).
*/
public array $typeCheck = [];

/** Human-readable declared type string for diagnostics ('' when untyped). */
public string $typeStr = '';

public function __construct(string $name, int $flags, string $type, string $value)
{
$this->name = $name;
Expand Down
Loading
Loading