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
20 changes: 20 additions & 0 deletions phpunit/code/enum_case_cross_file_def.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?php

// Declaring half of the cross-file pair (see enum_case_cross_file_ref.php):
// the case expression names Provider through this file's `use` import, so a
// lazy evaluation triggered from another file must restore this context.
namespace Lib {
class Provider
{
public const BASE = 40;
}
}

namespace A {
use Lib\Provider;

enum E: int
{
case X = Provider::BASE + 2;
}
}
19 changes: 19 additions & 0 deletions phpunit/code/enum_case_cross_file_ref.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?php

// Referencing half of the cross-file pair (see enum_case_cross_file_def.php).
// This file is converted first, so Holder's constant initializer performs the
// first fetch of A\E::X while namespace Consumer is active.
namespace Consumer {
class Holder
{
public const REF = \A\E::X;
}
}

namespace {
function main()
{
var_dump(\Consumer\Holder::REF);
var_dump(\A\E::X->value);
}
}
37 changes: 37 additions & 0 deletions phpunit/code/enum_case_cross_namespace.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<?php

// Namespace B is converted first: Holder's constant initializer forces the
// lazy evaluation of A\E's case expression while B is the active conversion
// namespace. `Helper::V` inside the case expression must resolve to A\Helper
// (20), never to the decoy B\Helper (999).
namespace B {
class Helper
{
public const V = 999;
}

class Holder
{
public const REF = \A\E::X;
}
}

namespace A {
class Helper
{
public const V = 20;
}

enum E: int
{
case X = Helper::V + 1;
}
}

namespace {
function main()
{
var_dump(\B\Holder::REF);
var_dump(\A\E::X->value);
}
}
12 changes: 12 additions & 0 deletions phpunit/code/enum_case_mutual_reference.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?php

enum E: int
{
case A = E::B;
case B = E::A;
}

function main()
{
var_dump(E::A);
}
11 changes: 11 additions & 0 deletions phpunit/code/enum_case_self_reference.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?php

enum E: int
{
case A = E::A;
}

function main()
{
var_dump(E::A);
}
4 changes: 4 additions & 0 deletions phpunit/code/enum_rule_abstract_method.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<?php
enum Suit { case Hearts; abstract public function f(): void; }

function main() {}
4 changes: 4 additions & 0 deletions phpunit/code/enum_rule_backing_type.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<?php
enum Suit: float { case Hearts = 1.0; }

function main() {}
4 changes: 4 additions & 0 deletions phpunit/code/enum_rule_case_const_clash.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<?php
enum Suit { const Hearts = 1; case Hearts; }

function main() {}
4 changes: 4 additions & 0 deletions phpunit/code/enum_rule_case_missing_value.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<?php
enum Suit: int { case Hearts; }

function main() {}
4 changes: 4 additions & 0 deletions phpunit/code/enum_rule_case_value_nonbacked.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<?php
enum Suit { case Hearts = 1; }

function main() {}
4 changes: 4 additions & 0 deletions phpunit/code/enum_rule_duplicate_case.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<?php
enum Suit { case Hearts; case Hearts; }

function main() {}
5 changes: 5 additions & 0 deletions phpunit/code/enum_rule_extends_enum.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<?php
enum Suit { case Hearts; }
class Deck extends Suit {}

function main() {}
4 changes: 4 additions & 0 deletions phpunit/code/enum_rule_implements_backedenum_nonbacked.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<?php
enum Suit implements BackedEnum { case Hearts; }

function main() {}
4 changes: 4 additions & 0 deletions phpunit/code/enum_rule_implements_unitenum.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<?php
enum Suit implements UnitEnum { case Hearts; }

function main() {}
4 changes: 4 additions & 0 deletions phpunit/code/enum_rule_magic_construct.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<?php
enum Suit { case Hearts; public function __construct() {} }

function main() {}
4 changes: 4 additions & 0 deletions phpunit/code/enum_rule_magic_tostring.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<?php
enum Suit { case Hearts; public function __toString(): string { return ""; } }

function main() {}
4 changes: 4 additions & 0 deletions phpunit/code/enum_rule_property.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<?php
enum Suit { case Hearts; public int $x = 1; }

function main() {}
4 changes: 4 additions & 0 deletions phpunit/code/enum_rule_static_property.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<?php
enum Suit { case Hearts; public static int $x = 1; }

function main() {}
5 changes: 5 additions & 0 deletions phpunit/code/enum_rule_trait_alias_magic.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<?php
trait Builder { public function build(): void {} }
enum Suit { use Builder { build as __destruct; } case Hearts; }

function main() {}
5 changes: 5 additions & 0 deletions phpunit/code/enum_rule_trait_magic_construct.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<?php
trait Builder { public function __construct() {} }
enum Suit { use Builder; case Hearts; }

function main() {}
4 changes: 4 additions & 0 deletions phpunit/code/enum_rule_valid.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<?php
enum Suit: string { const Wild = "w"; case Hearts = "h"; case Spades = "s"; public function label(): string { return $this->value; } public function __invoke(): string { return $this->label(); } }

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

use TypePhp\CompilerTest;

/**
* Lazy evaluation of backed enum case value expressions (kept as ASTs during
* prepare, evaluated on first convert-phase access): cycle detection matching
* Zend's "Cannot declare self-referencing constant", and name resolution
* against the enum's own declaration context rather than whatever namespace
* the translator is converting when the first access happens.
*/
class EnumCaseExprEvaluationTest extends BaseTest
{
public function testSelfReferencingCaseIsRejected(): void
{
$this->exec('Cannot declare self-referencing constant `E::A`', 'enum_case_self_reference.php');
}

public function testMutuallyRecursiveCasesAreRejected(): void
{
// Zend reports the first constant fetched twice while walking the
// cycle (E::B for `case A = E::B; case B = E::A;`, probed on 8.4.13),
// not the case whose evaluation started the walk.
$this->exec('Cannot declare self-referencing constant `E::B`', 'enum_case_mutual_reference.php');
}

public function testCaseExprResolvesInDeclaringNamespace(): void
{
// Verified against Zend 8.4.13: B\Holder::REF and A\E::X->value are
// both 21 (A\Helper::V + 1); the decoy B\Helper::V is 999.
[$stub] = $this->convertFiles(['enum_case_cross_namespace.php']);
self::assertStringContainsString('ZVAL_LONG(&enum_case_X_value, 21)', $stub);
self::assertStringContainsString('ZVAL_LONG(&const_REF_value, 21)', $stub);
self::assertStringNotContainsString('1000', $stub);
}

public function testCaseExprResolvesAcrossFiles(): void
{
// The referencing file converts first, so the lazy evaluation of
// A\E::X runs while namespace Consumer is active; Provider inside the
// case expression must still resolve through the declaring file's
// `use Lib\Provider`. Verified against Zend 8.4.13: both values are 42.
[$ref, $def] = $this->convertFiles([
'enum_case_cross_file_ref.php',
'enum_case_cross_file_def.php',
]);
self::assertStringContainsString('ZVAL_LONG(&const_REF_value, 42)', $ref);
self::assertStringContainsString('ZVAL_LONG(&enum_case_X_value, 42)', $def);
}

/**
* Compile the given phpunit/code files as one program and return each
* file's generated stub registration code (where constant and enum case
* values are emitted), in argument order — conversion happens in that
* order, which the cross-context tests rely on.
*
* @param list<string> $files
* @return list<string>
*/
private function convertFiles(array $files): array
{
global $translator;

$compiler = CompilerTest::create(TYPEPHP_ROOT_PATH);
$translator = $compiler;
$paths = array_map(static fn (string $file): string => TYPEPHP_ROOT_PATH . '/phpunit/code/' . $file, $files);
$compiler->addFiles($paths);
foreach ($paths as $path) {
$compiler->prepareFile($path);
}
$generated = [];
foreach ($paths as $path) {
$compiler->convertFile($path);
$generated[] = file_get_contents($compiler->getArgInfoHeaderFile($path));
}
return $generated;
}
}
96 changes: 96 additions & 0 deletions phpunit/src/EnumDeclarationRulesTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
<?php

/**
* Compile-time enum declaration rules mirrored from Zend 8.4:
* no properties, restricted magic methods, case/value pairing per
* backing type, case name collisions, backing-type whitelist, and the
* implicit UnitEnum/BackedEnum/final markers.
*/
class EnumDeclarationRulesTest extends BaseTest
{
public function testEnumCannotIncludeProperties(): void
{
$this->exec('Enum `Suit` cannot include properties', 'enum_rule_property.php');
}

public function testEnumCannotIncludeStaticProperties(): void
{
$this->exec('Enum `Suit` cannot include properties', 'enum_rule_static_property.php');
}

public function testEnumCannotIncludeConstructor(): void
{
$this->exec('Enum `Suit` cannot include magic method `__construct`', 'enum_rule_magic_construct.php');
}

public function testEnumCannotIncludeToString(): void
{
$this->exec('Enum `Suit` cannot include magic method `__toString`', 'enum_rule_magic_tostring.php');
}

public function testNonBackedCaseMustNotHaveValue(): void
{
$this->exec('Case `Hearts` of non-backed enum `Suit` must not have a value', 'enum_rule_case_value_nonbacked.php');
}

public function testBackedCaseMustHaveValue(): void
{
$this->exec('Case `Hearts` of backed enum `Suit` must have a value', 'enum_rule_case_missing_value.php');
}

public function testDuplicateCaseIsRejected(): void
{
$this->exec('Cannot redefine class constant `Suit::Hearts`', 'enum_rule_duplicate_case.php');
}

public function testCaseClashingWithConstantIsRejected(): void
{
$this->exec('Cannot redefine class constant `Suit::Hearts`', 'enum_rule_case_const_clash.php');
}

public function testBackingTypeMustBeIntOrString(): void
{
$this->exec('Enum backing type must be `int` or `string`, `float` given', 'enum_rule_backing_type.php');
}

public function testExplicitUnitEnumImplementsIsRejected(): void
{
$this->exec('Enum `Suit` cannot implement previously implemented interface `UnitEnum`', 'enum_rule_implements_unitenum.php');
}

public function testNonBackedEnumCannotImplementBackedEnum(): void
{
$this->exec('Non-backed enum `Suit` cannot implement interface `BackedEnum`', 'enum_rule_implements_backedenum_nonbacked.php');
}

public function testEnumCannotIncludeAbstractMethod(): void
{
$this->exec('Enum `Suit` cannot include abstract method `f()`', 'enum_rule_abstract_method.php');
}

public function testClassCannotExtendEnum(): void
{
// Enum ClassDef flags carry Modifiers::FINAL, so the regular
// final-class inheritance check rejects the extension.
$this->exec('Class `Deck` cannot extend final class `Suit`', 'enum_rule_extends_enum.php');
}

public function testWellFormedEnumStillCompiles(): void
{
$this->compile('enum_rule_valid.php');
}

public function testTraitInjectedMagicMethodIsRejected(): void
{
// The forbidden-magic-method check must also cover methods composed
// into the enum from a trait, not only ones declared in its body.
$this->exec('Enum `Suit` cannot include magic method `__construct`', 'enum_rule_trait_magic_construct.php');
}

public function testTraitAliasToMagicNameIsRejected(): void
{
// A trait alias that renames an ordinary method to a forbidden magic
// name installs that magic method into the enum; Zend rejects it.
$this->exec('Enum `Suit` cannot include magic method `__destruct`', 'enum_rule_trait_alias_magic.php');
}
}
25 changes: 25 additions & 0 deletions src/Entity/ClassDef.php
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,31 @@ class ClassDef extends ClassLikeDef
* @var array<string, int|string|null>
*/
public array $enumCases = [];

/**
* Backed case values that are not scalar literals, keyed by case name.
* The expression AST is captured during prepare (the symbol environment
* is incomplete there) and evaluated+memoized into $enumCases on first
* convert-phase access. The entry survives until evaluation succeeds.
* @var array<string, \PhpParser\Node\Expr>
*/
public array $enumCaseExprs = [];

/**
* Lexical import context of the file declaring the enum, captured when a
* backed case value is kept as an expression AST. The lazy evaluation may
* run while the translator is converting a different file, so names in
* the stored expressions must resolve against the enum's own namespace
* and `use` imports rather than the current conversion context.
* @var list<string>
*/
public array $enumUseNamespaces = [];
/** @var array<string, string> */
public array $enumUseAliases = [];
/** @var array<string, string> */
public array $enumUseFunctions = [];
/** @var array<string, string> */
public array $enumUseConstants = [];
/**
* Abstract method name (lowercase) => flags
* @var array<string, int>
Expand Down
Loading
Loading