diff options
author | Ilija Tovilo <ilija.tovilo@me.com> | 2020-06-10 23:10:18 +0200 |
---|---|---|
committer | Ilija Tovilo <ilija.tovilo@me.com> | 2021-03-17 19:08:03 +0100 |
commit | 269c8dac1d56ee85d71ae94d9b28dd7d8e8de7b7 (patch) | |
tree | 810ac41b2157ff4e8063f9696f97e1a9d77837c4 /Zend | |
parent | a6fc427b8c51015c16541c112a26dd06bd75e99e (diff) | |
download | php-git-269c8dac1d56ee85d71ae94d9b28dd7d8e8de7b7.tar.gz |
Implement enums
RFC: https://wiki.php.net/rfc/enumerations
Co-authored-by: Nikita Popov <nikita.ppv@gmail.com>
Closes GH-6489.
Diffstat (limited to 'Zend')
131 files changed, 3211 insertions, 18 deletions
diff --git a/Zend/Optimizer/zend_inference.c b/Zend/Optimizer/zend_inference.c index e600139229..42f37fa0c9 100644 --- a/Zend/Optimizer/zend_inference.c +++ b/Zend/Optimizer/zend_inference.c @@ -3485,7 +3485,7 @@ static zend_always_inline int _zend_update_type_info( break; case ZEND_FETCH_CONSTANT: case ZEND_FETCH_CLASS_CONSTANT: - UPDATE_SSA_TYPE(MAY_BE_RC1|MAY_BE_RCN|MAY_BE_NULL|MAY_BE_FALSE|MAY_BE_TRUE|MAY_BE_LONG|MAY_BE_DOUBLE|MAY_BE_STRING|MAY_BE_RESOURCE|MAY_BE_ARRAY|MAY_BE_ARRAY_KEY_ANY|MAY_BE_ARRAY_OF_ANY, ssa_op->result_def); + UPDATE_SSA_TYPE(MAY_BE_RC1|MAY_BE_RCN|MAY_BE_ANY|MAY_BE_ARRAY_KEY_ANY|MAY_BE_ARRAY_OF_ANY, ssa_op->result_def); break; case ZEND_STRLEN: tmp = MAY_BE_LONG; diff --git a/Zend/tests/enum/__call.phpt b/Zend/tests/enum/__call.phpt new file mode 100644 index 0000000000..b2be5eb040 --- /dev/null +++ b/Zend/tests/enum/__call.phpt @@ -0,0 +1,29 @@ +--TEST-- +Enum __call +--FILE-- +<?php + +enum Foo { + case Bar; + + public function __call(string $name, array $args) + { + return [$name, $args]; + } +} + +var_dump(Foo::Bar->baz('qux', 'quux')); + +?> +--EXPECT-- +array(2) { + [0]=> + string(3) "baz" + [1]=> + array(2) { + [0]=> + string(3) "qux" + [1]=> + string(4) "quux" + } +} diff --git a/Zend/tests/enum/__callStatic.phpt b/Zend/tests/enum/__callStatic.phpt new file mode 100644 index 0000000000..d3acd38239 --- /dev/null +++ b/Zend/tests/enum/__callStatic.phpt @@ -0,0 +1,27 @@ +--TEST-- +Enum __callStatic +--FILE-- +<?php + +enum Foo { + public static function __callStatic(string $name, array $args) + { + return [$name, $args]; + } +} + +var_dump(Foo::bar('baz', 'qux')); + +?> +--EXPECT-- +array(2) { + [0]=> + string(3) "bar" + [1]=> + array(2) { + [0]=> + string(3) "baz" + [1]=> + string(3) "qux" + } +} diff --git a/Zend/tests/enum/__class__.phpt b/Zend/tests/enum/__class__.phpt new file mode 100644 index 0000000000..0b9a5834e4 --- /dev/null +++ b/Zend/tests/enum/__class__.phpt @@ -0,0 +1,19 @@ +--TEST-- +Enum __CLASS__ +--FILE-- +<?php + +enum Foo { + case Bar; + + public function printClass() + { + echo __CLASS__ . "\n"; + } +} + +Foo::Bar->printClass(); + +?> +--EXPECT-- +Foo diff --git a/Zend/tests/enum/__function__.phpt b/Zend/tests/enum/__function__.phpt new file mode 100644 index 0000000000..d460593873 --- /dev/null +++ b/Zend/tests/enum/__function__.phpt @@ -0,0 +1,19 @@ +--TEST-- +Enum __FUNCTION__ +--FILE-- +<?php + +enum Foo { + case Bar; + + public function printFunction() + { + echo __FUNCTION__ . "\n"; + } +} + +Foo::Bar->printFunction(); + +?> +--EXPECT-- +printFunction diff --git a/Zend/tests/enum/__get.phpt b/Zend/tests/enum/__get.phpt new file mode 100644 index 0000000000..885cc2f2c8 --- /dev/null +++ b/Zend/tests/enum/__get.phpt @@ -0,0 +1,17 @@ +--TEST-- +Enum __get +--FILE-- +<?php + +enum Foo { + case Bar; + + public function __get(string $name) + { + return '__get'; + } +} + +?> +--EXPECTF-- +Fatal error: Enum may not include __get in %s on line %d diff --git a/Zend/tests/enum/__invoke.phpt b/Zend/tests/enum/__invoke.phpt new file mode 100644 index 0000000000..63a35b56fd --- /dev/null +++ b/Zend/tests/enum/__invoke.phpt @@ -0,0 +1,24 @@ +--TEST-- +Enum __invoke +--FILE-- +<?php + +enum Foo { + case Bar; + + public function __invoke(...$args) + { + return $args; + } +} + +var_dump((Foo::Bar)('baz', 'qux')); + +?> +--EXPECT-- +array(2) { + [0]=> + string(3) "baz" + [1]=> + string(3) "qux" +} diff --git a/Zend/tests/enum/__isset.phpt b/Zend/tests/enum/__isset.phpt new file mode 100644 index 0000000000..76409f075b --- /dev/null +++ b/Zend/tests/enum/__isset.phpt @@ -0,0 +1,16 @@ +--TEST-- +Enum __isset +--FILE-- +<?php + +enum Foo { + case Bar; + + public function __isset($property) { + return true; + } +} + +?> +--EXPECTF-- +Fatal error: Enum may not include __isset in %s on line %d diff --git a/Zend/tests/enum/__method__.phpt b/Zend/tests/enum/__method__.phpt new file mode 100644 index 0000000000..5d4195566e --- /dev/null +++ b/Zend/tests/enum/__method__.phpt @@ -0,0 +1,19 @@ +--TEST-- +Enum __METHOD__ +--FILE-- +<?php + +enum Foo { + case Bar; + + public function printMethod() + { + echo __METHOD__ . "\n"; + } +} + +Foo::Bar->printMethod(); + +?> +--EXPECT-- +Foo::printMethod diff --git a/Zend/tests/enum/ast-dumper.phpt b/Zend/tests/enum/ast-dumper.phpt new file mode 100644 index 0000000000..ed38e44e7c --- /dev/null +++ b/Zend/tests/enum/ast-dumper.phpt @@ -0,0 +1,48 @@ +--TEST-- +Enum AST dumper +--FILE-- +<?php + +try { + assert((function () { + enum Foo { + case Bar; + } + + #[EnumAttr] + enum IntFoo: int { + #[CaseAttr] + case Bar = 1 << 0; + case Baz = 1 << 1; + + public function self() { + return $this; + } + } + + return false; + })()); +} catch (Error $e) { + echo $e->getMessage(); +} + +?> +--EXPECT-- +assert(function () { + enum Foo { + case Bar; + } + + #[EnumAttr] + enum IntFoo: int { + #[CaseAttr] + case Bar = 1 << 0; + case Baz = 1 << 1; + public function self() { + return $this; + } + + } + + return false; +}()) diff --git a/Zend/tests/enum/backed-cases-int.phpt b/Zend/tests/enum/backed-cases-int.phpt new file mode 100644 index 0000000000..85d2527c39 --- /dev/null +++ b/Zend/tests/enum/backed-cases-int.phpt @@ -0,0 +1,26 @@ +--TEST-- +Int backed enums with can list cases +--FILE-- +<?php + +enum Suit: int { + case Hearts = 2; + case Diamonds = 1; + case Clubs = 4; + case Spades = 3; +} + +var_dump(Suit::cases()); + +?> +--EXPECT-- +array(4) { + [0]=> + enum(Suit::Hearts) + [1]=> + enum(Suit::Diamonds) + [2]=> + enum(Suit::Clubs) + [3]=> + enum(Suit::Spades) +} diff --git a/Zend/tests/enum/backed-cases-string.phpt b/Zend/tests/enum/backed-cases-string.phpt new file mode 100644 index 0000000000..6d7deef77e --- /dev/null +++ b/Zend/tests/enum/backed-cases-string.phpt @@ -0,0 +1,26 @@ +--TEST-- +String backed enums can list cases +--FILE-- +<?php + +enum Suit: string { + case Hearts = 'H'; + case Diamonds = 'D'; + case Clubs = 'C'; + case Spades = 'S'; +} + +var_dump(Suit::cases()); + +?> +--EXPECT-- +array(4) { + [0]=> + enum(Suit::Hearts) + [1]=> + enum(Suit::Diamonds) + [2]=> + enum(Suit::Clubs) + [3]=> + enum(Suit::Spades) +} diff --git a/Zend/tests/enum/backed-duplicate-int.phpt b/Zend/tests/enum/backed-duplicate-int.phpt new file mode 100644 index 0000000000..4da70bc3dd --- /dev/null +++ b/Zend/tests/enum/backed-duplicate-int.phpt @@ -0,0 +1,13 @@ +--TEST-- +Backed enums reject duplicate int values +--FILE-- +<?php + +enum Foo: int { + case Bar = 0; + case Baz = 0; +} + +?> +--EXPECTF-- +Fatal error: Duplicate value in enum Foo for cases Bar and Baz in %s on line %s diff --git a/Zend/tests/enum/backed-duplicate-string.phpt b/Zend/tests/enum/backed-duplicate-string.phpt new file mode 100644 index 0000000000..7d42c2ac30 --- /dev/null +++ b/Zend/tests/enum/backed-duplicate-string.phpt @@ -0,0 +1,15 @@ +--TEST-- +Backed enums reject duplicate string values +--FILE-- +<?php + +enum Suit: string { + case Hearts = 'H'; + case Diamonds = 'D'; + case Clubs = 'C'; + case Spades = 'H'; +} + +?> +--EXPECTF-- +Fatal error: Duplicate value in enum Suit for cases Hearts and Spades in %s on line %s diff --git a/Zend/tests/enum/backed-from-invalid-int.phpt b/Zend/tests/enum/backed-from-invalid-int.phpt new file mode 100644 index 0000000000..d976f3d0fb --- /dev/null +++ b/Zend/tests/enum/backed-from-invalid-int.phpt @@ -0,0 +1,19 @@ +--TEST-- +BackedEnum::from() reject invalid int +--FILE-- +<?php + +enum Foo: int { + case Bar = 0; + case Baz = 1; +} + +try { + var_dump(Foo::from(2)); +} catch (Error $e) { + echo $e->getMessage() . "\n"; +} + +?> +--EXPECT-- +2 is not a valid backing value for enum "Foo" diff --git a/Zend/tests/enum/backed-from-invalid-string.phpt b/Zend/tests/enum/backed-from-invalid-string.phpt new file mode 100644 index 0000000000..db8b791d8b --- /dev/null +++ b/Zend/tests/enum/backed-from-invalid-string.phpt @@ -0,0 +1,21 @@ +--TEST-- +BackedEnum::from() reject invalid string +--FILE-- +<?php + +enum Suit: string { + case Hearts = 'H'; + case Diamonds = 'D'; + case Clubs = 'C'; + case Spades = 'S'; +} + +try { + var_dump(Suit::from('A')); +} catch (Error $e) { + echo $e->getMessage() . "\n"; +} + +?> +--EXPECT-- +"A" is not a valid backing value for enum "Suit" diff --git a/Zend/tests/enum/backed-from-invalid-type.phpt b/Zend/tests/enum/backed-from-invalid-type.phpt new file mode 100644 index 0000000000..3f35bef64f --- /dev/null +++ b/Zend/tests/enum/backed-from-invalid-type.phpt @@ -0,0 +1,34 @@ +--TEST-- +BackedEnum::from() reject invalid type +--FILE-- +<?php + +enum Suit: string { + case Hearts = 'H'; + case Diamonds = 'D'; + case Clubs = 'C'; + case Spades = 'S'; +} + +try { + var_dump(Suit::from(42)); +} catch (Error $e) { + echo $e->getMessage() . "\n"; +} + +enum Foo: int { + case Bar = 0; + case Baz = 1; +} + +try { + var_dump(Foo::from('H')); +} catch (Error $e) { + echo $e->getMessage() . "\n"; +} + + +?> +--EXPECT-- +"42" is not a valid backing value for enum "Suit" +Foo::from(): Argument #1 ($value) must be of type int, string given diff --git a/Zend/tests/enum/backed-from-unknown-hash.phpt b/Zend/tests/enum/backed-from-unknown-hash.phpt new file mode 100644 index 0000000000..eb5938d0d6 --- /dev/null +++ b/Zend/tests/enum/backed-from-unknown-hash.phpt @@ -0,0 +1,17 @@ +--TEST-- +BackedEnum::from() unknown hash +--FILE-- +<?php + +enum Foo: string { + case Bar = 'B'; +} + +$s = 'A'; +$s++; + +var_dump(Foo::from($s)); + +?> +--EXPECT-- +enum(Foo::Bar) diff --git a/Zend/tests/enum/backed-from.phpt b/Zend/tests/enum/backed-from.phpt new file mode 100644 index 0000000000..2c77c3e388 --- /dev/null +++ b/Zend/tests/enum/backed-from.phpt @@ -0,0 +1,36 @@ +--TEST-- +BackedEnum::from() +--FILE-- +<?php + +enum Suit: string { + case Hearts = 'H'; + case Diamonds = 'D'; + case Clubs = 'C'; + case Spades = 'S'; +} + +var_dump(Suit::from('H')); +var_dump(Suit::from('D')); +var_dump(Suit::from('C')); +var_dump(Suit::from('S')); + +enum Foo: int { + case Bar = 1; + case Baz = 2; + case Beep = 3; +} + +var_dump(Foo::from(1)); +var_dump(Foo::from(2)); +var_dump(Foo::from(3)); + +?> +--EXPECT-- +enum(Suit::Hearts) +enum(Suit::Diamonds) +enum(Suit::Clubs) +enum(Suit::Spades) +enum(Foo::Bar) +enum(Foo::Baz) +enum(Foo::Beep) diff --git a/Zend/tests/enum/backed-int-case-without-value.phpt b/Zend/tests/enum/backed-int-case-without-value.phpt new file mode 100644 index 0000000000..4b84c069b4 --- /dev/null +++ b/Zend/tests/enum/backed-int-case-without-value.phpt @@ -0,0 +1,14 @@ +--TEST-- +Int backed enums with case without value +--FILE-- +<?php + +enum Foo: int { + case Bar; +} + +var_dump(Foo::Bar->value); + +?> +--EXPECTF-- +Fatal error: Case Bar of backed enum Foo must have a value in %s on line %d diff --git a/Zend/tests/enum/backed-int-const-expr.phpt b/Zend/tests/enum/backed-int-const-expr.phpt new file mode 100644 index 0000000000..afb6491c51 --- /dev/null +++ b/Zend/tests/enum/backed-int-const-expr.phpt @@ -0,0 +1,20 @@ +--TEST-- +Int enum const expr +--FILE-- +<?php + +enum Foo: int { + case Bar = 1 << 0; + case Baz = 1 << 1; + case Qux = 1 << 2; +} + +var_dump(Foo::Bar->value); +var_dump(Foo::Baz->value); +var_dump(Foo::Qux->value); + +?> +--EXPECT-- +int(1) +int(2) +int(4) diff --git a/Zend/tests/enum/backed-int-const-invalid-expr.phpt b/Zend/tests/enum/backed-int-const-invalid-expr.phpt new file mode 100644 index 0000000000..097bb27883 --- /dev/null +++ b/Zend/tests/enum/backed-int-const-invalid-expr.phpt @@ -0,0 +1,14 @@ +--TEST-- +Int enum invalid const expr +--FILE-- +<?php + +enum Foo: int { + case Bar = 1 + $x; +} + +var_dump(Foo::Bar->value); + +?> +--EXPECTF-- +Fatal error: Enum case value must be constant in %s on line %d diff --git a/Zend/tests/enum/backed-int.phpt b/Zend/tests/enum/backed-int.phpt new file mode 100644 index 0000000000..113629f692 --- /dev/null +++ b/Zend/tests/enum/backed-int.phpt @@ -0,0 +1,17 @@ +--TEST-- +Int enum value +--FILE-- +<?php + +enum Foo: int { + case Bar = 0; + case Baz = 1; +} + +var_dump(Foo::Bar->value); +var_dump(Foo::Baz->value); + +?> +--EXPECT-- +int(0) +int(1) diff --git a/Zend/tests/enum/backed-invalid.phpt b/Zend/tests/enum/backed-invalid.phpt new file mode 100644 index 0000000000..bc6f71f79d --- /dev/null +++ b/Zend/tests/enum/backed-invalid.phpt @@ -0,0 +1,10 @@ +--TEST-- +Invalid enum backing type +--FILE-- +<?php + +enum Foo: Bar {} + +?> +--EXPECTF-- +Fatal error: Enum backing type must be int or string, Bar given in %s on line %d diff --git a/Zend/tests/enum/backed-mismatch.phpt b/Zend/tests/enum/backed-mismatch.phpt new file mode 100644 index 0000000000..0b28fb3be5 --- /dev/null +++ b/Zend/tests/enum/backed-mismatch.phpt @@ -0,0 +1,12 @@ +--TEST-- +Mismatched enum backing type +--FILE-- +<?php + +enum Foo: int { + case Bar = 'bar'; +} + +?> +--EXPECTF-- +Fatal error: Enum case type string does not match enum backing type int in %s on line %d diff --git a/Zend/tests/enum/backed-negative-int.phpt b/Zend/tests/enum/backed-negative-int.phpt new file mode 100644 index 0000000000..4342ecd7b2 --- /dev/null +++ b/Zend/tests/enum/backed-negative-int.phpt @@ -0,0 +1,21 @@ +--TEST-- +Backed enum with negative int +--FILE-- +<?php + +enum Foo: int { + case Bar = -1; + case Baz = -2; +} + +var_dump(Foo::Bar->value); +var_dump(Foo::Baz->value); +var_dump(Foo::from(-1)); +var_dump(Foo::from(-2)); + +?> +--EXPECT-- +int(-1) +int(-2) +enum(Foo::Bar) +enum(Foo::Baz) diff --git a/Zend/tests/enum/backed-string-heredoc.phpt b/Zend/tests/enum/backed-string-heredoc.phpt new file mode 100644 index 0000000000..b2e9b8f3e9 --- /dev/null +++ b/Zend/tests/enum/backed-string-heredoc.phpt @@ -0,0 +1,30 @@ +--TEST-- +String enum value with heredoc +--FILE-- +<?php + +enum Foo: string { + case Bar = <<<BAR + Bar + bar + bar + BAR; + + case Baz = <<<'BAZ' + Baz + baz + baz + BAZ; +} + +echo Foo::Bar->value . "\n"; +echo Foo::Baz->value . "\n"; + +?> +--EXPECT-- +Bar +bar +bar +Baz +baz +baz diff --git a/Zend/tests/enum/backed-string.phpt b/Zend/tests/enum/backed-string.phpt new file mode 100644 index 0000000000..afa7db827e --- /dev/null +++ b/Zend/tests/enum/backed-string.phpt @@ -0,0 +1,17 @@ +--TEST-- +String enum value +--FILE-- +<?php + +enum Foo: string { + case Bar = 'bar'; + case Baz = 'baz'; +} + +echo Foo::Bar->value . "\n"; +echo Foo::Baz->value . "\n"; + +?> +--EXPECT-- +bar +baz diff --git a/Zend/tests/enum/backed-tryFrom-casing.phpt b/Zend/tests/enum/backed-tryFrom-casing.phpt new file mode 100644 index 0000000000..213faa3a62 --- /dev/null +++ b/Zend/tests/enum/backed-tryFrom-casing.phpt @@ -0,0 +1,15 @@ +--TEST-- +BackedEnum::tryFrom() casing in reflection +--FILE-- +<?php + +enum Foo: string {} + +$reflectionEnum = new ReflectionEnum(Foo::class); +$reflectionMethod = $reflectionEnum->getMethod('tryFrom'); + +echo $reflectionMethod->getName() . "\n"; + +?> +--EXPECT-- +tryFrom diff --git a/Zend/tests/enum/backed-tryFrom-unknown-hash.phpt b/Zend/tests/enum/backed-tryFrom-unknown-hash.phpt new file mode 100644 index 0000000000..80ffe1dc39 --- /dev/null +++ b/Zend/tests/enum/backed-tryFrom-unknown-hash.phpt @@ -0,0 +1,17 @@ +--TEST-- +BackedEnum::tryFrom() unknown hash +--FILE-- +<?php + +enum Foo: string { + case Bar = 'B'; +} + +$s = 'A'; +$s++; + +var_dump(Foo::tryFrom($s)); + +?> +--EXPECT-- +enum(Foo::Bar) diff --git a/Zend/tests/enum/backed-tryFrom.phpt b/Zend/tests/enum/backed-tryFrom.phpt new file mode 100644 index 0000000000..81c36fcc2b --- /dev/null +++ b/Zend/tests/enum/backed-tryFrom.phpt @@ -0,0 +1,40 @@ +--TEST-- +BackedEnum::tryFrom() +--FILE-- +<?php + +enum Suit: string { + case Hearts = 'H'; + case Diamonds = 'D'; + case Clubs = 'C'; + case Spades = 'S'; +} + +var_dump(Suit::tryFrom('H')); +var_dump(Suit::tryFrom('D')); +var_dump(Suit::tryFrom('C')); +var_dump(Suit::tryFrom('S')); +var_dump(Suit::tryFrom('X')); + +enum Foo: int { + case Bar = 1; + case Baz = 2; + case Beep = 3; +} + +var_dump(Foo::tryFrom(1)); +var_dump(Foo::tryFrom(2)); +var_dump(Foo::tryFrom(3)); +var_dump(Foo::tryFrom(4)); + +?> +--EXPECT-- +enum(Suit::Hearts) +enum(Suit::Diamonds) +enum(Suit::Clubs) +enum(Suit::Spades) +NULL +enum(Foo::Bar) +enum(Foo::Baz) +enum(Foo::Beep) +NULL diff --git a/Zend/tests/enum/backed-type-no-union.phpt b/Zend/tests/enum/backed-type-no-union.phpt new file mode 100644 index 0000000000..8330f8de3d --- /dev/null +++ b/Zend/tests/enum/backed-type-no-union.phpt @@ -0,0 +1,10 @@ +--TEST-- +Backed enums type can't be union +--FILE-- +<?php + +enum Foo: int|string {} + +?> +--EXPECTF-- +Fatal error: Enum backing type must be int or string, string|int given in %s on line %d diff --git a/Zend/tests/enum/basic-methods.phpt b/Zend/tests/enum/basic-methods.phpt new file mode 100644 index 0000000000..ee1c2c24d7 --- /dev/null +++ b/Zend/tests/enum/basic-methods.phpt @@ -0,0 +1,21 @@ +--TEST-- +Enum methods +--FILE-- +<?php + +enum Foo { + case Bar; + case Baz; + + public function dump() { + var_dump($this); + } +} + +Foo::Bar->dump(); +Foo::Baz->dump(); + +?> +--EXPECT-- +enum(Foo::Bar) +enum(Foo::Baz) diff --git a/Zend/tests/enum/case-attributes.phpt b/Zend/tests/enum/case-attributes.phpt new file mode 100644 index 0000000000..a5ec8600cd --- /dev/null +++ b/Zend/tests/enum/case-attributes.phpt @@ -0,0 +1,25 @@ +--TEST-- +Enum case attributes +--FILE-- +<?php + +#[Attribute(Attribute::TARGET_CLASS_CONSTANT)] +class EnumCaseAttribute { + public function __construct( + public string $value, + ) {} +} + +enum Foo { + #[EnumCaseAttribute('Bar')] + case Bar; +} + +var_dump((new \ReflectionClassConstant(Foo::class, 'Bar'))->getAttributes(EnumCaseAttribute::class)[0]->newInstance()); + +?> +--EXPECT-- +object(EnumCaseAttribute)#1 (1) { + ["value"]=> + string(3) "Bar" +} diff --git a/Zend/tests/enum/case-in-class.phpt b/Zend/tests/enum/case-in-class.phpt new file mode 100644 index 0000000000..61713e6f89 --- /dev/null +++ b/Zend/tests/enum/case-in-class.phpt @@ -0,0 +1,12 @@ +--TEST-- +Enum case in class +--FILE-- +<?php + +class Foo { + case Bar; +} + +?> +--EXPECTF-- +Fatal error: Case can only be used in enums in %s on line %d diff --git a/Zend/tests/enum/cases-refcount.phpt b/Zend/tests/enum/cases-refcount.phpt new file mode 100644 index 0000000000..040589ca23 --- /dev/null +++ b/Zend/tests/enum/cases-refcount.phpt @@ -0,0 +1,22 @@ +--TEST-- +Enum cases increases refcount +--FILE-- +<?php + +enum Foo { + case Bar; +} + +function callCases() { + Foo::cases(); +} + +callCases(); +debug_zval_dump(Foo::Bar); + +?> +--EXPECT-- +object(Foo)#1 (1) refcount(2){ + ["name"]=> + string(3) "Bar" interned +} diff --git a/Zend/tests/enum/comparison.phpt b/Zend/tests/enum/comparison.phpt new file mode 100644 index 0000000000..5df2f282ec --- /dev/null +++ b/Zend/tests/enum/comparison.phpt @@ -0,0 +1,57 @@ +--TEST-- +Enum comparison +--FILE-- +<?php + +enum Foo { + case Bar; + case Baz; +} + +$bar = Foo::Bar; +$baz = Foo::Baz; + +var_dump($bar === $bar); +var_dump($bar == $bar); + +var_dump($bar === $baz); +var_dump($bar == $baz); + +var_dump($baz === $bar); +var_dump($baz == $bar); + +var_dump($bar > $bar); +var_dump($bar < $bar); +var_dump($bar >= $bar); +var_dump($bar <= $bar); + +var_dump($bar > $baz); +var_dump($bar < $baz); +var_dump($bar >= $baz); +var_dump($bar <= $baz); + +var_dump($bar > true); +var_dump($bar < true); +var_dump($bar >= true); +var_dump($bar <= true); + +?> +--EXPECT-- +bool(true) +bool(true) +bool(false) +bool(false) +bool(false) +bool(false) +bool(false) +bool(false) +bool(true) +bool(true) +bool(false) +bool(false) +bool(false) +bool(false) +bool(false) +bool(false) +bool(false) +bool(false) diff --git a/Zend/tests/enum/constant-aliases.phpt b/Zend/tests/enum/constant-aliases.phpt new file mode 100644 index 0000000000..1652eb3c48 --- /dev/null +++ b/Zend/tests/enum/constant-aliases.phpt @@ -0,0 +1,21 @@ +--TEST-- +Enum constants can alias cases +--FILE-- +<?php + +enum Foo { + case Bar; + const Baz = self::Bar; +} + +function test(Foo $var) { + echo "works\n"; +} + +test(Foo::Bar); +test(Foo::Baz); + +?> +--EXPECT-- +works +works diff --git a/Zend/tests/enum/constants.phpt b/Zend/tests/enum/constants.phpt new file mode 100644 index 0000000000..cc983e5184 --- /dev/null +++ b/Zend/tests/enum/constants.phpt @@ -0,0 +1,14 @@ +--TEST-- +Enum allows constants +--FILE-- +<?php + +enum Foo { + const BAR = 'Bar'; +} + +echo Foo::BAR . "\n"; + +?> +--EXPECT-- +Bar diff --git a/Zend/tests/enum/default-parameter.phpt b/Zend/tests/enum/default-parameter.phpt new file mode 100644 index 0000000000..02aa05b5a8 --- /dev/null +++ b/Zend/tests/enum/default-parameter.phpt @@ -0,0 +1,18 @@ +--TEST-- +Enum in default parameter +--FILE-- +<?php + +enum Foo { + case Bar; +} + +function baz(Foo $foo = Foo::Bar) { + var_dump($foo); +} + +baz(); + +?> +--EXPECT-- +enum(Foo::Bar) diff --git a/Zend/tests/enum/enum-as-constant.phpt b/Zend/tests/enum/enum-as-constant.phpt new file mode 100644 index 0000000000..26a589ae79 --- /dev/null +++ b/Zend/tests/enum/enum-as-constant.phpt @@ -0,0 +1,22 @@ +--TEST-- +Enum cases can be referenced by constants +--FILE-- +<?php + +enum Foo { + case Bar; +} + +const Beep = Foo::Bar; + +class Test { + const Beep = Foo::Bar; +} + +var_dump(Beep); +var_dump(Test::Beep); + +?> +--EXPECT-- +enum(Foo::Bar) +enum(Foo::Bar) diff --git a/Zend/tests/enum/enum-as-params.phpt b/Zend/tests/enum/enum-as-params.phpt new file mode 100644 index 0000000000..90a1f0b5d0 --- /dev/null +++ b/Zend/tests/enum/enum-as-params.phpt @@ -0,0 +1,35 @@ +--TEST-- +Enum types as parameters +--FILE-- +<?php + +enum Foo { + case Bar; +} + +enum Baz { + case Qux; +} + +function takesFoo(Foo $foo) {} +function takesBaz(Baz $baz) {} + +takesFoo(Foo::Bar); +takesBaz(Baz::Qux); + +try { + takesBaz(Foo::Bar); +} catch (Error $e) { + echo $e->getMessage() . "\n"; +} + +try { + takesFoo(Baz::Qux); +} catch (Error $e) { + echo $e->getMessage() . "\n"; +} + +?> +--EXPECTF-- +takesBaz(): Argument #1 ($baz) must be of type Baz, Foo given, called in %s on line %d +takesFoo(): Argument #1 ($foo) must be of type Foo, Baz given, called in %s on line %d diff --git a/Zend/tests/enum/enum-attributes.phpt b/Zend/tests/enum/enum-attributes.phpt new file mode 100644 index 0000000000..e0e57c744c --- /dev/null +++ b/Zend/tests/enum/enum-attributes.phpt @@ -0,0 +1,23 @@ +--TEST-- +Enum attributes +--FILE-- +<?php + +#[Attribute] +class EnumAttribute { + public function __construct( + public string $value, + ) {} +} + +#[EnumAttribute('Foo')] +enum Foo {} + +var_dump((new \ReflectionClass(Foo::class))->getAttributes(EnumAttribute::class)[0]->newInstance()); + +?> +--EXPECT-- +object(EnumAttribute)#1 (1) { + ["value"]=> + string(3) "Foo" +} diff --git a/Zend/tests/enum/enum-in-constant.phpt b/Zend/tests/enum/enum-in-constant.phpt new file mode 100644 index 0000000000..487b4a6aad --- /dev/null +++ b/Zend/tests/enum/enum-in-constant.phpt @@ -0,0 +1,22 @@ +--TEST-- +Enum in constant +--FILE-- +<?php + +enum Foo { + case Bar; +} + +class Baz { + const BAR = Foo::Bar; +} + +var_dump(Foo::Bar); +var_dump(Baz::BAR); +var_dump(Foo::Bar === Baz::BAR); + +?> +--EXPECT-- +enum(Foo::Bar) +enum(Foo::Bar) +bool(true) diff --git a/Zend/tests/enum/enum-in-static-var.phpt b/Zend/tests/enum/enum-in-static-var.phpt new file mode 100644 index 0000000000..9f5ebc6264 --- /dev/null +++ b/Zend/tests/enum/enum-in-static-var.phpt @@ -0,0 +1,21 @@ +--TEST-- +Enum in static var +--FILE-- +<?php + +enum Foo { + case Bar; +} + +function example() { + static $bar = Foo::Bar; + return $bar; +} + +var_dump(example()); +var_dump(example()); + +?> +--EXPECT-- +enum(Foo::Bar) +enum(Foo::Bar) diff --git a/Zend/tests/enum/enum-reserved-non-modifiers.phpt b/Zend/tests/enum/enum-reserved-non-modifiers.phpt new file mode 100644 index 0000000000..b07114856f --- /dev/null +++ b/Zend/tests/enum/enum-reserved-non-modifiers.phpt @@ -0,0 +1,32 @@ +--TEST-- +enum keyword is reserved_non_modifiers +--FILE-- +<?php + +namespace enum { + class Foo { + public static function bar() { + return 'enum\Foo::bar()'; + } + } +} + +namespace { + class Foo { + const enum = 'enum const'; + + public static function enum() { + return 'enum static method'; + } + } + + echo \enum\Foo::bar() . "\n"; + echo Foo::enum . "\n"; + echo Foo::enum() . "\n"; +} + +?> +--EXPECT-- +enum\Foo::bar() +enum const +enum static method diff --git a/Zend/tests/enum/enum_exists.phpt b/Zend/tests/enum/enum_exists.phpt new file mode 100644 index 0000000000..d1e86447c4 --- /dev/null +++ b/Zend/tests/enum/enum_exists.phpt @@ -0,0 +1,41 @@ +--TEST-- +enum_exists +--FILE-- +<?php + +enum Foo { + case Bar; +} + +class Baz {} + +spl_autoload_register(function ($className) { + echo "Triggered autoloader with class $className\n"; + + if ($className === 'Quux') { + enum Quux {} + } +}); + +var_dump(enum_exists(Foo::class)); +var_dump(enum_exists(Foo::Bar::class)); +var_dump(enum_exists(Baz::class)); +var_dump(enum_exists(Qux::class)); +var_dump(enum_exists(Quux::class, false)); +var_dump(enum_exists(Quux::class, true)); +var_dump(enum_exists(Quuz::class, false)); +var_dump(enum_exists(Quuz::class, true)); + +?> +--EXPECT-- +bool(true) +bool(true) +bool(false) +Triggered autoloader with class Qux +bool(false) +bool(false) +Triggered autoloader with class Quux +bool(true) +bool(false) +Triggered autoloader with class Quuz +bool(false) diff --git a/Zend/tests/enum/final.phpt b/Zend/tests/enum/final.phpt new file mode 100644 index 0000000000..ca13b7da8c --- /dev/null +++ b/Zend/tests/enum/final.phpt @@ -0,0 +1,12 @@ +--TEST-- +Enum is final +--FILE-- +<?php + +enum Foo {} + +class Bar extends Foo {} + +?> +--EXPECTF-- +Fatal error: Class Bar may not inherit from final class (Foo) in %s on line %d diff --git a/Zend/tests/enum/implements.phpt b/Zend/tests/enum/implements.phpt new file mode 100644 index 0000000000..fc90ab2aef --- /dev/null +++ b/Zend/tests/enum/implements.phpt @@ -0,0 +1,34 @@ +--TEST-- +Enum implements +--FILE-- +<?php + +interface Colorful { + public function color(): string; +} + +enum Suit implements Colorful { + case Hearts; + case Diamonds; + case Clubs; + case Spades; + + public function color(): string { + return match ($this) { + self::Hearts, self::Diamonds => 'Red', + self::Clubs, self::Spades => 'Black', + }; + } +} + +echo Suit::Hearts->color() . "\n"; +echo Suit::Diamonds->color() . "\n"; +echo Suit::Clubs->color() . "\n"; +echo Suit::Spades->color() . "\n"; + +?> +--EXPECT-- +Red +Red +Black +Black diff --git a/Zend/tests/enum/instanceof-backed-enum.phpt b/Zend/tests/enum/instanceof-backed-enum.phpt new file mode 100644 index 0000000000..4716835d11 --- /dev/null +++ b/Zend/tests/enum/instanceof-backed-enum.phpt @@ -0,0 +1,20 @@ +--TEST-- +Auto implement BackedEnum interface +--FILE-- +<?php + +enum Foo { + case Bar; +} + +enum Baz: int { + case Qux = 0; +} + +var_dump(Foo::Bar instanceof BackedEnum); +var_dump(Baz::Qux instanceof BackedEnum); + +?> +--EXPECT-- +bool(false) +bool(true) diff --git a/Zend/tests/enum/instanceof-unitenum.phpt b/Zend/tests/enum/instanceof-unitenum.phpt new file mode 100644 index 0000000000..5523796325 --- /dev/null +++ b/Zend/tests/enum/instanceof-unitenum.phpt @@ -0,0 +1,18 @@ +--TEST-- +Auto implement UnitEnum interface +--FILE-- +<?php + +enum Foo { + case Bar; +} + +class Baz {} + +var_dump(Foo::Bar instanceof UnitEnum); +var_dump((new Baz()) instanceof UnitEnum); + +?> +--EXPECT-- +bool(true) +bool(false) diff --git a/Zend/tests/enum/instanceof.phpt b/Zend/tests/enum/instanceof.phpt new file mode 100644 index 0000000000..0e29b9d282 --- /dev/null +++ b/Zend/tests/enum/instanceof.phpt @@ -0,0 +1,25 @@ +--TEST-- +Enum instanceof +--FILE-- +<?php + +enum Foo { + case Bar; +} + +enum Baz { + case Qux; +} + +var_dump(Foo::Bar instanceof Foo); +var_dump(Baz::Qux instanceof Baz); + +var_dump(Foo::Bar instanceof Baz); +var_dump(Baz::Qux instanceof Foo); + +?> +--EXPECT-- +bool(true) +bool(true) +bool(false) +bool(false) diff --git a/Zend/tests/enum/json_encode.phpt b/Zend/tests/enum/json_encode.phpt new file mode 100644 index 0000000000..016ca6107f --- /dev/null +++ b/Zend/tests/enum/json_encode.phpt @@ -0,0 +1,59 @@ +--TEST-- +Enum in json_encode +--FILE-- +<?php + +enum Foo { + case Bar; +} + +enum IntFoo: int { + case Bar = 0; +} + +enum StringFoo: string { + case Bar = 'Bar'; +} + +enum CustomFoo implements JsonSerializable { + case Bar; + + public function jsonSerialize() { + return 'Custom ' . $this->name; + } +} + +function test($value) { + var_dump(json_encode($value)); + echo json_last_error_msg() . "\n"; + + try { + var_dump(json_encode($value, JSON_THROW_ON_ERROR)); + echo json_last_error_msg() . "\n"; + } catch (Exception $e) { + echo get_class($e) . ': ' . $e->getMessage() . "\n"; + } +} + +test(Foo::Bar); +test(IntFoo::Bar); +test(StringFoo::Bar); +test(CustomFoo::Bar); + +?> +--EXPECT-- +bool(false) +Non-backed enums have no default serialization +JsonException: Non-backed enums have no default serialization +string(1) "0" +No error +string(1) "0" +No error +string(5) ""Bar"" +No error +string(5) ""Bar"" +No error +string(12) ""Custom Bar"" +No error +string(12) ""Custom Bar"" +No error diff --git a/Zend/tests/enum/keyword-no-bc-break.phpt b/Zend/tests/enum/keyword-no-bc-break.phpt new file mode 100644 index 0000000000..92b8226afa --- /dev/null +++ b/Zend/tests/enum/keyword-no-bc-break.phpt @@ -0,0 +1,42 @@ +--TEST-- +Enum keyword can still be used in classes, namespaces, functions and constants +--FILE-- +<?php + +namespace enum { + class Foo {} +} + +namespace foo { + class Bar {} + class enum extends Bar {} +} + +namespace bar { + interface Baz {} + class enum implements Baz {} +} + +namespace { + class enum {} + + function enum() { + return 'enum function'; + } + + const enum = 'enum constant'; + + var_dump(new enum\Foo()); + var_dump(new enum()); + var_dump(enum()); + var_dump(enum); +} + +?> +--EXPECT-- +object(enum\Foo)#1 (0) { +} +object(enum)#1 (0) { +} +string(13) "enum function" +string(13) "enum constant" diff --git a/Zend/tests/enum/keyword-whitespace.phpt b/Zend/tests/enum/keyword-whitespace.phpt new file mode 100644 index 0000000000..bdcbd94079 --- /dev/null +++ b/Zend/tests/enum/keyword-whitespace.phpt @@ -0,0 +1,16 @@ +--TEST-- +Enum keyword can be followed by arbitrary whitespaces +--FILE-- +<?php + +enum A {} +enum B {} +enum C {} +enum E {} +enum +F {} +enum + G {} + +?> +--EXPECT-- diff --git a/Zend/tests/enum/name-property.phpt b/Zend/tests/enum/name-property.phpt new file mode 100644 index 0000000000..2197f235bb --- /dev/null +++ b/Zend/tests/enum/name-property.phpt @@ -0,0 +1,50 @@ +--TEST-- +Enum name property +--FILE-- +<?php + +enum Foo { + case Bar; + case Baz; +} + +enum IntFoo: int { + case Bar = 0; + case Baz = 1; +} + +var_dump((new ReflectionClass(Foo::class))->getProperties()); +var_dump(Foo::Bar->name); + +var_dump((new ReflectionClass(IntFoo::class))->getProperties()); +var_dump(IntFoo::Bar->name); + +?> +--EXPECT-- +array(1) { + [0]=> + object(ReflectionProperty)#2 (2) { + ["name"]=> + string(4) "name" + ["class"]=> + string(3) "Foo" + } +} +string(3) "Bar" +array(2) { + [0]=> + object(ReflectionProperty)#3 (2) { + ["name"]=> + string(4) "name" + ["class"]=> + string(6) "IntFoo" + } + [1]=> + object(ReflectionProperty)#4 (2) { + ["name"]=> + string(5) "value" + ["class"]=> + string(6) "IntFoo" + } +} +string(3) "Bar" diff --git a/Zend/tests/enum/namespaces.phpt b/Zend/tests/enum/namespaces.phpt new file mode 100644 index 0000000000..261a885b55 --- /dev/null +++ b/Zend/tests/enum/namespaces.phpt @@ -0,0 +1,32 @@ +--TEST-- +Enum namespace +--FILE-- +<?php + +namespace Foo { + enum Bar { + case Baz; + + public function dump() { + var_dump(Bar::Baz); + } + } + + function dumpBar() { + Bar::Baz->dump(); + } +} + +namespace { + use Foo\Bar; + + \Foo\dumpBar(); + \Foo\Bar::Baz->dump(); + Bar::Baz->dump(); +} + +?> +--EXPECT-- +enum(Foo\Bar::Baz) +enum(Foo\Bar::Baz) +enum(Foo\Bar::Baz) diff --git a/Zend/tests/enum/no-cases.phpt b/Zend/tests/enum/no-cases.phpt new file mode 100644 index 0000000000..cf518aefb9 --- /dev/null +++ b/Zend/tests/enum/no-cases.phpt @@ -0,0 +1,16 @@ +--TEST-- +Enum no manual cases method +--FILE-- +<?php + +enum Foo { + case Bar; + + public static function cases(): array { + return []; + } +} + +?> +--EXPECTF-- +Fatal error: Cannot redeclare Foo::cases() in %s on line %d diff --git a/Zend/tests/enum/no-class-implements-backed-enum.phpt b/Zend/tests/enum/no-class-implements-backed-enum.phpt new file mode 100644 index 0000000000..f6f37818da --- /dev/null +++ b/Zend/tests/enum/no-class-implements-backed-enum.phpt @@ -0,0 +1,10 @@ +--TEST-- +Class cannot implement BackedEnum +--FILE-- +<?php + +class Foo implements BackedEnum {} + +?> +--EXPECTF-- +Fatal error: Non-enum class Foo cannot implement interface BackedEnum in %s on line %d diff --git a/Zend/tests/enum/no-class-implements-unit-enum.phpt b/Zend/tests/enum/no-class-implements-unit-enum.phpt new file mode 100644 index 0000000000..02ddd4567f --- /dev/null +++ b/Zend/tests/enum/no-class-implements-unit-enum.phpt @@ -0,0 +1,10 @@ +--TEST-- +Class cannot implement UnitEnum +--FILE-- +<?php + +class Foo implements UnitEnum {} + +?> +--EXPECTF-- +Fatal error: Non-enum class Foo cannot implement interface UnitEnum in %s on line %d diff --git a/Zend/tests/enum/no-clone.phpt b/Zend/tests/enum/no-clone.phpt new file mode 100644 index 0000000000..2fadbb26b7 --- /dev/null +++ b/Zend/tests/enum/no-clone.phpt @@ -0,0 +1,18 @@ +--TEST-- +Enum disallows cloning +--FILE-- +<?php + +enum Foo { + case Bar; +} + +try { + var_dump(clone Foo::Bar); +} catch (Error $e) { + echo $e->getMessage() . "\n"; +} + +?> +--EXPECT-- +Trying to clone an uncloneable object of class Foo diff --git a/Zend/tests/enum/no-constructors.phpt b/Zend/tests/enum/no-constructors.phpt new file mode 100644 index 0000000000..70504e14eb --- /dev/null +++ b/Zend/tests/enum/no-constructors.phpt @@ -0,0 +1,12 @@ +--TEST-- +Enum disallows constructor +--FILE-- +<?php + +enum Foo { + public function __construct() {} +} + +?> +--EXPECTF-- +Fatal error: Enum may not include __construct in %s on line %d diff --git a/Zend/tests/enum/no-destruct.phpt b/Zend/tests/enum/no-destruct.phpt new file mode 100644 index 0000000000..730c29f847 --- /dev/null +++ b/Zend/tests/enum/no-destruct.phpt @@ -0,0 +1,12 @@ +--TEST-- +Enum disallows destructor +--FILE-- +<?php + +enum Foo { + public function __destruct() {} +} + +?> +--EXPECTF-- +Fatal error: Enum may not include __destruct in %s on line %d diff --git a/Zend/tests/enum/no-dynamic-properties.phpt b/Zend/tests/enum/no-dynamic-properties.phpt new file mode 100644 index 0000000000..21766ce1b4 --- /dev/null +++ b/Zend/tests/enum/no-dynamic-properties.phpt @@ -0,0 +1,20 @@ +--TEST-- +Enum case disallows dynamic properties +--FILE-- +<?php + +enum Foo { + case Bar; +} + +$bar = Foo::Bar; + +try { + $bar->baz = 'Baz'; +} catch (\Error $e) { + echo $e->getMessage(); +} + +?> +--EXPECT-- +Enum properties are immutable diff --git a/Zend/tests/enum/no-enum-implements-backed-enum.phpt b/Zend/tests/enum/no-enum-implements-backed-enum.phpt new file mode 100644 index 0000000000..69922c13a8 --- /dev/null +++ b/Zend/tests/enum/no-enum-implements-backed-enum.phpt @@ -0,0 +1,10 @@ +--TEST-- +Enum cannot manually implement BackedEnum +--FILE-- +<?php + +enum Foo: int implements BackedEnum {} + +?> +--EXPECTF-- +Fatal error: Class Foo cannot implement previously implemented interface BackedEnum in %s on line %d diff --git a/Zend/tests/enum/no-enum-implements-unit-enum.phpt b/Zend/tests/enum/no-enum-implements-unit-enum.phpt new file mode 100644 index 0000000000..458efbb67c --- /dev/null +++ b/Zend/tests/enum/no-enum-implements-unit-enum.phpt @@ -0,0 +1,10 @@ +--TEST-- +Enum cannot manually implement UnitEnum +--FILE-- +<?php + +enum Foo implements UnitEnum {} + +?> +--EXPECTF-- +Fatal error: Class Foo cannot implement previously implemented interface UnitEnum in %s on line %d diff --git a/Zend/tests/enum/no-from.phpt b/Zend/tests/enum/no-from.phpt new file mode 100644 index 0000000000..1cf717de88 --- /dev/null +++ b/Zend/tests/enum/no-from.phpt @@ -0,0 +1,16 @@ +--TEST-- +Enum no manual from method +--FILE-- +<?php + +enum Foo: int { + case Bar = 0; + + public static function from(string|int $value): self { + return $this; + } +} + +?> +--EXPECTF-- +Fatal error: Cannot redeclare Foo::from() in %s on line %d diff --git a/Zend/tests/enum/no-implement-serializable-indirect.phpt b/Zend/tests/enum/no-implement-serializable-indirect.phpt new file mode 100644 index 0000000000..5e7bdc9338 --- /dev/null +++ b/Zend/tests/enum/no-implement-serializable-indirect.phpt @@ -0,0 +1,24 @@ +--TEST-- +Enum must not implement Serializable indirectly +--FILE-- +<?php + +interface MySerializable extends Serializable {} + +enum Foo implements MySerializable { + case Bar; + + public function serialize() { + return serialize('Hello'); + } + + public function unserialize($data) { + return unserialize($data); + } +} + +var_dump(unserialize(serialize(Foo::Bar))); + +?> +--EXPECTF-- +Fatal error: Enums may not implement the Serializable interface in %s on line %d diff --git a/Zend/tests/enum/no-implement-serializable.phpt b/Zend/tests/enum/no-implement-serializable.phpt new file mode 100644 index 0000000000..62485f2934 --- /dev/null +++ b/Zend/tests/enum/no-implement-serializable.phpt @@ -0,0 +1,22 @@ +--TEST-- +Enum must not implement Serializable +--FILE-- +<?php + +enum Foo implements Serializable { + case Bar; + + public function serialize() { + return serialize('Hello'); + } + + public function unserialize($data) { + return unserialize($data); + } +} + +var_dump(unserialize(serialize(Foo::Bar))); + +?> +--EXPECTF-- +Fatal error: Enums may not implement the Serializable interface in %s on line %d diff --git a/Zend/tests/enum/no-name-property.phpt b/Zend/tests/enum/no-name-property.phpt new file mode 100644 index 0000000000..a565c08e83 --- /dev/null +++ b/Zend/tests/enum/no-name-property.phpt @@ -0,0 +1,12 @@ +--TEST-- +Enum disallows name property +--FILE-- +<?php + +enum Foo { + public string $name; +} + +?> +--EXPECTF-- +Fatal error: Enums may not include properties in %s on line %d diff --git a/Zend/tests/enum/no-new-through-reflection.phpt b/Zend/tests/enum/no-new-through-reflection.phpt new file mode 100644 index 0000000000..9a92559cd3 --- /dev/null +++ b/Zend/tests/enum/no-new-through-reflection.phpt @@ -0,0 +1,16 @@ +--TEST-- +Enum no new through reflection +--FILE-- +<?php + +enum Foo {} + +try { + (new \ReflectionClass(Foo::class))->newInstanceWithoutConstructor(); +} catch (Error $e) { + echo $e->getMessage() . "\n"; +} + +?> +--EXPECT-- +Cannot instantiate enum Foo diff --git a/Zend/tests/enum/no-new.phpt b/Zend/tests/enum/no-new.phpt new file mode 100644 index 0000000000..698e954882 --- /dev/null +++ b/Zend/tests/enum/no-new.phpt @@ -0,0 +1,16 @@ +--TEST-- +Enum no new +--FILE-- +<?php + +enum Foo {} + +try { + new Foo(); +} catch (\Error $e) { + echo $e->getMessage(); +} + +?> +--EXPECT-- +Cannot instantiate enum Foo diff --git a/Zend/tests/enum/no-non-backed-enum-implements-backed-enum.phpt b/Zend/tests/enum/no-non-backed-enum-implements-backed-enum.phpt new file mode 100644 index 0000000000..f560490de4 --- /dev/null +++ b/Zend/tests/enum/no-non-backed-enum-implements-backed-enum.phpt @@ -0,0 +1,10 @@ +--TEST-- +Non-backed enum cannot implement BackedEnum +--FILE-- +<?php + +enum Foo implements BackedEnum {} + +?> +--EXPECTF-- +Fatal error: Non-backed enum Foo cannot implement interface BackedEnum in %s on line %d diff --git a/Zend/tests/enum/no-pass-properties-by-ref.phpt b/Zend/tests/enum/no-pass-properties-by-ref.phpt new file mode 100644 index 0000000000..8f429dfa14 --- /dev/null +++ b/Zend/tests/enum/no-pass-properties-by-ref.phpt @@ -0,0 +1,26 @@ +--TEST-- +Enum properties cannot be passed by-ref +--FILE-- +<?php + +enum Foo: int { + case Bar = 0; +} + +function setBarValueByRef(&$bar, $value) { + $bar = $value; +} + +try { + $bar = Foo::Bar; + $value = setBarValueByRef($bar->value, 1); +} catch (Error $e) { + echo $e->getMessage() . "\n"; +} + +var_dump(Foo::Bar->value); + +?> +--EXPECT-- +Cannot acquire reference to property Foo::$value +int(0) diff --git a/Zend/tests/enum/no-properties.phpt b/Zend/tests/enum/no-properties.phpt new file mode 100644 index 0000000000..e846845d22 --- /dev/null +++ b/Zend/tests/enum/no-properties.phpt @@ -0,0 +1,12 @@ +--TEST-- +Enum disallows properties +--FILE-- +<?php + +enum Foo { + public $bar; +} + +?> +--EXPECTF-- +Fatal error: Enums may not include properties in %s on line %d diff --git a/Zend/tests/enum/no-return-properties-by-ref.phpt b/Zend/tests/enum/no-return-properties-by-ref.phpt new file mode 100644 index 0000000000..988d480de1 --- /dev/null +++ b/Zend/tests/enum/no-return-properties-by-ref.phpt @@ -0,0 +1,27 @@ +--TEST-- +Enum properties cannot be returned by-ref +--FILE-- +<?php + +enum Foo: int { + case Bar = 0; +} + +function &getBarValueByRef() { + $bar = Foo::Bar; + return $bar->value; +} + +try { + $value = &getBarValueByRef(); + $value = 1; +} catch (Error $e) { + echo $e->getMessage() . "\n"; +} + +var_dump(Foo::Bar->value); + +?> +--EXPECT-- +Cannot acquire reference to property Foo::$value +int(0) diff --git a/Zend/tests/enum/no-static-properties.phpt b/Zend/tests/enum/no-static-properties.phpt new file mode 100644 index 0000000000..4b823e98e9 --- /dev/null +++ b/Zend/tests/enum/no-static-properties.phpt @@ -0,0 +1,12 @@ +--TEST-- +Enum disallows static properties +--FILE-- +<?php + +enum Foo { + public static $bar; +} + +?> +--EXPECTF-- +Fatal error: Enums may not include properties in %s on line %d diff --git a/Zend/tests/enum/no-unsed-value.phpt b/Zend/tests/enum/no-unsed-value.phpt new file mode 100644 index 0000000000..a1cbdd43ca --- /dev/null +++ b/Zend/tests/enum/no-unsed-value.phpt @@ -0,0 +1,14 @@ +--TEST-- +Enum prevent unsetting value +--FILE-- +<?php + +enum Foo: int { + case Bar = 0; +} + +unset(Foo::Bar->value); + +?> +--EXPECTF-- +Fatal error: Cannot use temporary expression in write context in %s on line %d diff --git a/Zend/tests/enum/no-unset-propertes.phpt b/Zend/tests/enum/no-unset-propertes.phpt new file mode 100644 index 0000000000..338b22a026 --- /dev/null +++ b/Zend/tests/enum/no-unset-propertes.phpt @@ -0,0 +1,37 @@ +--TEST-- +Enum properties cannot be unset +--FILE-- +<?php + +enum Foo { + case Bar; +} + +enum IntFoo: int { + case Bar = 0; +} + +$foo = Foo::Bar; +try { + unset($foo->name); +} catch (Error $e) { + echo $e->getMessage() . "\n"; +} + +$intFoo = IntFoo::Bar; +try { + unset($intFoo->name); +} catch (Error $e) { + echo $e->getMessage() . "\n"; +} +try { + unset($intFoo->value); +} catch (Error $e) { + echo $e->getMessage() . "\n"; +} + +?> +--EXPECT-- +Enum properties are immutable +Enum properties are immutable +Enum properties are immutable diff --git a/Zend/tests/enum/no-value-property.phpt b/Zend/tests/enum/no-value-property.phpt new file mode 100644 index 0000000000..d8b12f2ca8 --- /dev/null +++ b/Zend/tests/enum/no-value-property.phpt @@ -0,0 +1,12 @@ +--TEST-- +Enum disallows value property +--FILE-- +<?php + +enum Foo: int { + public int $value; +} + +?> +--EXPECTF-- +Fatal error: Enums may not include properties in %s on line %d diff --git a/Zend/tests/enum/no-write-properties-through-foreach-reference.phpt b/Zend/tests/enum/no-write-properties-through-foreach-reference.phpt new file mode 100644 index 0000000000..d1c211802f --- /dev/null +++ b/Zend/tests/enum/no-write-properties-through-foreach-reference.phpt @@ -0,0 +1,22 @@ +--TEST-- +Enum properties cannot be written to through reference in foreach +--FILE-- +<?php + +enum Foo: int { + case Bar = 0; +} + +try { + $bar = Foo::Bar; + foreach ([1] as &$bar->value) {} +} catch (Error $e) { + echo $e->getMessage() . "\n"; +} + +var_dump(Foo::Bar->value); + +?> +--EXPECT-- +Cannot acquire reference to property Foo::$value +int(0) diff --git a/Zend/tests/enum/no-write-properties-through-references.phpt b/Zend/tests/enum/no-write-properties-through-references.phpt new file mode 100644 index 0000000000..81543af78d --- /dev/null +++ b/Zend/tests/enum/no-write-properties-through-references.phpt @@ -0,0 +1,23 @@ +--TEST-- +Enum properties cannot be written to through references +--FILE-- +<?php + +enum Foo: int { + case Bar = 0; +} + +try { + $bar = Foo::Bar; + $value = &$bar->value; + $value = 1; +} catch (Error $e) { + echo $e->getMessage() . "\n"; +} + +var_dump(Foo::Bar->value); + +?> +--EXPECT-- +Cannot acquire reference to property Foo::$value +int(0) diff --git a/Zend/tests/enum/no-write-properties.phpt b/Zend/tests/enum/no-write-properties.phpt new file mode 100644 index 0000000000..13cbe69e74 --- /dev/null +++ b/Zend/tests/enum/no-write-properties.phpt @@ -0,0 +1,37 @@ +--TEST-- +Enum properties cannot be written to +--FILE-- +<?php + +enum Foo { + case Bar; +} + +enum IntFoo: int { + case Bar = 0; +} + +$bar = Foo::Bar; +try { + $bar->name = 'Baz'; +} catch (Error $e) { + echo $e->getMessage() . "\n"; +} + +$intBar = Foo::Bar; +try { + $intBar->name = 'Baz'; +} catch (Error $e) { + echo $e->getMessage() . "\n"; +} +try { + $intBar->value = 1; +} catch (Error $e) { + echo $e->getMessage() . "\n"; +} + +?> +--EXPECT-- +Enum properties are immutable +Enum properties are immutable +Enum properties are immutable diff --git a/Zend/tests/enum/non-backed-enum-with-expr-value.phpt b/Zend/tests/enum/non-backed-enum-with-expr-value.phpt new file mode 100644 index 0000000000..95bd85f396 --- /dev/null +++ b/Zend/tests/enum/non-backed-enum-with-expr-value.phpt @@ -0,0 +1,12 @@ +--TEST-- +Non-backed enum errors when case has int expression value +--FILE-- +<?php + +enum Foo { + case Bar = 1 + 1; +} + +?> +--EXPECTF-- +Fatal error: Case Bar of non-backed enum Foo must not have a value, try adding ": int" to the enum declaration in %s on line %d diff --git a/Zend/tests/enum/non-backed-enum-with-int-value.phpt b/Zend/tests/enum/non-backed-enum-with-int-value.phpt new file mode 100644 index 0000000000..4932e63d18 --- /dev/null +++ b/Zend/tests/enum/non-backed-enum-with-int-value.phpt @@ -0,0 +1,12 @@ +--TEST-- +Non-backed enum errors when case has int value +--FILE-- +<?php + +enum Foo { + case Bar = 1; +} + +?> +--EXPECTF-- +Fatal error: Case Bar of non-backed enum Foo must not have a value, try adding ": int" to the enum declaration in %s on line %d diff --git a/Zend/tests/enum/non-backed-enum-with-invalid-value.phpt b/Zend/tests/enum/non-backed-enum-with-invalid-value.phpt new file mode 100644 index 0000000000..24b8277805 --- /dev/null +++ b/Zend/tests/enum/non-backed-enum-with-invalid-value.phpt @@ -0,0 +1,12 @@ +--TEST-- +Non-backed enum errors when case has invalid value +--FILE-- +<?php + +enum Foo { + case Bar = 3.141; +} + +?> +--EXPECTF-- +Fatal error: Case Bar of non-backed enum Foo must not have a value in %s on line %d diff --git a/Zend/tests/enum/non-backed-enum-with-string-value.phpt b/Zend/tests/enum/non-backed-enum-with-string-value.phpt new file mode 100644 index 0000000000..f1a06e1a17 --- /dev/null +++ b/Zend/tests/enum/non-backed-enum-with-string-value.phpt @@ -0,0 +1,12 @@ +--TEST-- +Non-backed enum errors when case has string value +--FILE-- +<?php + +enum Foo { + case Bar = 'Bar'; +} + +?> +--EXPECTF-- +Fatal error: Case Bar of non-backed enum Foo must not have a value, try adding ": string" to the enum declaration in %s on line %d diff --git a/Zend/tests/enum/offsetGet-in-const-expr.phpt b/Zend/tests/enum/offsetGet-in-const-expr.phpt new file mode 100644 index 0000000000..e288b9a694 --- /dev/null +++ b/Zend/tests/enum/offsetGet-in-const-expr.phpt @@ -0,0 +1,29 @@ +--TEST-- +Enum offsetGet in constant expression +--FILE-- +<?php + +enum Foo implements ArrayAccess { + case Bar; + + public function offsetGet($key) { + return 42; + } + + public function offsetExists($key) {} + public function offsetSet($key, $value) {} + public function offsetUnset($key) {} +} + +class X { + const FOO_BAR = Foo::Bar[0]; +} + +var_dump(X::FOO_BAR); + +?> +--EXPECTF-- +Fatal error: Uncaught Error: Cannot use [] on objects in constant expression in %s:%d +Stack trace: +#0 {main} + thrown in %s on line %d diff --git a/Zend/tests/enum/print_r.phpt b/Zend/tests/enum/print_r.phpt new file mode 100644 index 0000000000..9cb08fec73 --- /dev/null +++ b/Zend/tests/enum/print_r.phpt @@ -0,0 +1,37 @@ +--TEST-- +Enum print_r +--FILE-- +<?php + +enum Foo { + case Bar; +} + +enum IntFoo: int { + case Bar = 42; +} + +enum StringFoo: string { + case Bar = 'Bar'; +} + +print_r(Foo::Bar); +print_r(IntFoo::Bar); +print_r(StringFoo::Bar); + +?> +--EXPECT-- +Foo Enum +( + [name] => Bar +) +IntFoo Enum:int +( + [name] => Bar + [value] => 42 +) +StringFoo Enum:string +( + [name] => Bar + [value] => Bar +) diff --git a/Zend/tests/enum/reflectionclass.phpt b/Zend/tests/enum/reflectionclass.phpt new file mode 100644 index 0000000000..59b99b3e6d --- /dev/null +++ b/Zend/tests/enum/reflectionclass.phpt @@ -0,0 +1,20 @@ +--TEST-- +Enum reflection getConstants() +--FILE-- +<?php + +enum Foo { + case Bar; + case Baz; +} + +var_dump((new \ReflectionClass(Foo::class))->getConstants()); + +?> +--EXPECT-- +array(2) { + ["Bar"]=> + enum(Foo::Bar) + ["Baz"]=> + enum(Foo::Baz) +} diff --git a/Zend/tests/enum/serialization-round-trip.phpt b/Zend/tests/enum/serialization-round-trip.phpt new file mode 100644 index 0000000000..d69cbea71f --- /dev/null +++ b/Zend/tests/enum/serialization-round-trip.phpt @@ -0,0 +1,14 @@ +--TEST-- +Enum unserialize same instance +--FILE-- +<?php + +enum Foo { + case Bar; +} + +var_dump(Foo::Bar === unserialize(serialize(Foo::Bar))); + +?> +--EXPECT-- +bool(true) diff --git a/Zend/tests/enum/serialize.phpt b/Zend/tests/enum/serialize.phpt new file mode 100644 index 0000000000..56545ebfae --- /dev/null +++ b/Zend/tests/enum/serialize.phpt @@ -0,0 +1,14 @@ +--TEST-- +Enum serialize +--FILE-- +<?php + +enum Foo { + case Bar; +} + +echo serialize(Foo::Bar); + +?> +--EXPECT-- +E:7:"Foo:Bar"; diff --git a/Zend/tests/enum/spl-object-storage.phpt b/Zend/tests/enum/spl-object-storage.phpt new file mode 100644 index 0000000000..7c72299c34 --- /dev/null +++ b/Zend/tests/enum/spl-object-storage.phpt @@ -0,0 +1,37 @@ +--TEST-- +Enum in SplObjectStorage +--FILE-- +<?php + +enum Foo { + case Bar; + case Baz; + case Qux; +} + +$storage = new SplObjectStorage(); +$storage[Foo::Bar] = 'Bar'; +$storage[Foo::Baz] = 'Baz'; + +var_dump($storage[Foo::Bar]); +var_dump($storage[Foo::Baz]); + +var_dump($storage->contains(Foo::Bar)); +var_dump($storage->contains(Foo::Qux)); + +$serialized = serialize($storage); +var_dump($serialized); + +$unserialized = unserialize($serialized); +var_dump($unserialized[Foo::Bar]); +var_dump($unserialized[Foo::Baz]); + +?> +--EXPECT-- +string(3) "Bar" +string(3) "Baz" +bool(true) +bool(false) +string(112) "O:16:"SplObjectStorage":2:{i:0;a:4:{i:0;E:7:"Foo:Bar";i:1;s:3:"Bar";i:2;E:7:"Foo:Baz";i:3;s:3:"Baz";}i:1;a:0:{}}" +string(3) "Bar" +string(3) "Baz" diff --git a/Zend/tests/enum/static-methods.phpt b/Zend/tests/enum/static-methods.phpt new file mode 100644 index 0000000000..361ee77664 --- /dev/null +++ b/Zend/tests/enum/static-methods.phpt @@ -0,0 +1,28 @@ +--TEST-- +Enum supports static methods +--FILE-- +<?php + +enum Size { + case Small; + case Medium; + case Large; + + public static function fromLength(int $cm) { + return match(true) { + $cm < 50 => static::Small, + $cm < 100 => static::Medium, + default => static::Large, + }; + } +} + +var_dump(Size::fromLength(23)); +var_dump(Size::fromLength(63)); +var_dump(Size::fromLength(123)); + +?> +--EXPECT-- +enum(Size::Small) +enum(Size::Medium) +enum(Size::Large) diff --git a/Zend/tests/enum/traits-no-__construct.phpt b/Zend/tests/enum/traits-no-__construct.phpt new file mode 100644 index 0000000000..0529a2a989 --- /dev/null +++ b/Zend/tests/enum/traits-no-__construct.phpt @@ -0,0 +1,21 @@ +--TEST-- +Enum traits no __construct +--FILE-- +<?php + +trait Foo { + public function __construct() { + echo "Evil code\n"; + } +} + +enum Bar { + use Foo; + case Baz; +} + +var_dump(Bar::Baz); + +?> +--EXPECTF-- +Fatal error: Enum may not include __construct in %s on line %d diff --git a/Zend/tests/enum/traits-no-cases-method.phpt b/Zend/tests/enum/traits-no-cases-method.phpt new file mode 100644 index 0000000000..9acfb52d0e --- /dev/null +++ b/Zend/tests/enum/traits-no-cases-method.phpt @@ -0,0 +1,34 @@ +--TEST-- +Using cases method from traits in enums has no effect +--FILE-- +<?php + +trait Rectangle { + public static function cases(): array { + return []; + } +} + +enum Suit { + use Rectangle; + + case Hearts; + case Diamonds; + case Clubs; + case Spades; +} + +var_dump(Suit::cases()); + +?> +--EXPECT-- +array(4) { + [0]=> + enum(Suit::Hearts) + [1]=> + enum(Suit::Diamonds) + [2]=> + enum(Suit::Clubs) + [3]=> + enum(Suit::Spades) +} diff --git a/Zend/tests/enum/traits-no-forbidden-methods.phpt b/Zend/tests/enum/traits-no-forbidden-methods.phpt new file mode 100644 index 0000000000..1f310f48cd --- /dev/null +++ b/Zend/tests/enum/traits-no-forbidden-methods.phpt @@ -0,0 +1,21 @@ +--TEST-- +Enum cannot have forbidden methods, even via traits +--FILE-- +<?php + +trait Rectangle { + public function __construct() {} +} + +enum Suit { + use Rectangle; + + case Hearts; + case Diamonds; + case Clubs; + case Spades; +} + +?> +--EXPECTF-- +Fatal error: Enum may not include __construct in %s on line %d diff --git a/Zend/tests/enum/traits-no-properties.phpt b/Zend/tests/enum/traits-no-properties.phpt new file mode 100644 index 0000000000..5a6faf9dea --- /dev/null +++ b/Zend/tests/enum/traits-no-properties.phpt @@ -0,0 +1,25 @@ +--TEST-- +Enum cannot have properties, even via traits +--FILE-- +<?php + +trait Rectangle { + protected string $shape = "Rectangle"; + + public function shape(): string { + return $this->shape; + } +} + +enum Suit { + use Rectangle; + + case Hearts; + case Diamonds; + case Clubs; + case Spades; +} + +?> +--EXPECTF-- +Fatal error: Enum "Suit" may not include properties in %s on line %d diff --git a/Zend/tests/enum/traits.phpt b/Zend/tests/enum/traits.phpt new file mode 100644 index 0000000000..3de8e1559f --- /dev/null +++ b/Zend/tests/enum/traits.phpt @@ -0,0 +1,31 @@ +--TEST-- +Enum can use traits +--FILE-- +<?php + +trait Rectangle { + public function shape(): string { + return "Rectangle"; + } +} + +enum Suit { + use Rectangle; + + case Hearts; + case Diamonds; + case Clubs; + case Spades; +} + +echo Suit::Hearts->shape() . PHP_EOL; +echo Suit::Diamonds->shape() . PHP_EOL; +echo Suit::Clubs->shape() . PHP_EOL; +echo Suit::Spades->shape() . PHP_EOL; + +?> +--EXPECT-- +Rectangle +Rectangle +Rectangle +Rectangle diff --git a/Zend/tests/enum/unit-cases.phpt b/Zend/tests/enum/unit-cases.phpt new file mode 100644 index 0000000000..83f2232159 --- /dev/null +++ b/Zend/tests/enum/unit-cases.phpt @@ -0,0 +1,28 @@ +--TEST-- +Unit enums can list cases +--FILE-- +<?php + +enum Suit { + case Hearts; + case Diamonds; + case Clubs; + case Spades; + /** @deprecated Typo, use Suit::Hearts */ + const Hearst = self::Hearts; +} + +var_dump(Suit::cases()); + +?> +--EXPECT-- +array(4) { + [0]=> + enum(Suit::Hearts) + [1]=> + enum(Suit::Diamonds) + [2]=> + enum(Suit::Clubs) + [3]=> + enum(Suit::Spades) +} diff --git a/Zend/tests/enum/unserialize-const.phpt b/Zend/tests/enum/unserialize-const.phpt new file mode 100644 index 0000000000..0731a68dcc --- /dev/null +++ b/Zend/tests/enum/unserialize-const.phpt @@ -0,0 +1,18 @@ +--TEST-- +Enum unserialize const +--FILE-- +<?php + +enum Foo { + case Bar; + const Baz = Foo::Bar; +} + +var_dump(unserialize('E:7:"Foo:Baz";')); + +?> +--EXPECTF-- +Warning: unserialize(): Foo::Baz is not an enum case in %s on line %d + +Notice: unserialize(): Error at offset 14 of 14 bytes in %s on line %d +bool(false) diff --git a/Zend/tests/enum/unserialize-missing-colon.phpt b/Zend/tests/enum/unserialize-missing-colon.phpt new file mode 100644 index 0000000000..86cce32df4 --- /dev/null +++ b/Zend/tests/enum/unserialize-missing-colon.phpt @@ -0,0 +1,17 @@ +--TEST-- +Enum unserialize with missing colon +--FILE-- +<?php + +enum Foo { + case Bar; +} + +var_dump(unserialize('E:6:"FooBar";')); + +?> +--EXPECTF-- +Warning: unserialize(): Invalid enum name 'FooBar' (missing colon) in %s on line %d + +Notice: unserialize(): Error at offset 0 of 13 bytes in %s on line %d +bool(false) diff --git a/Zend/tests/enum/unserialize-non-enum.phpt b/Zend/tests/enum/unserialize-non-enum.phpt new file mode 100644 index 0000000000..82f4ec93db --- /dev/null +++ b/Zend/tests/enum/unserialize-non-enum.phpt @@ -0,0 +1,15 @@ +--TEST-- +Enum unserialize non-enum +--FILE-- +<?php + +class Foo {} + +var_dump(unserialize('E:7:"Foo:Bar";')); + +?> +--EXPECTF-- +Warning: unserialize(): Class 'Foo' is not an enum in %s on line %d + +Notice: unserialize(): Error at offset 0 of 14 bytes in %s on line %d +bool(false) diff --git a/Zend/tests/enum/unserialize-non-existent-case.phpt b/Zend/tests/enum/unserialize-non-existent-case.phpt new file mode 100644 index 0000000000..2364db5b15 --- /dev/null +++ b/Zend/tests/enum/unserialize-non-existent-case.phpt @@ -0,0 +1,17 @@ +--TEST-- +Enum unserialize non-existent case +--FILE-- +<?php + +enum Foo { + case Bar; +} + +var_dump(unserialize('E:7:"Foo:Baz";')); + +?> +--EXPECTF-- +Warning: unserialize(): Undefined constant Foo::Baz in %s on line %d + +Notice: unserialize(): Error at offset 14 of 14 bytes in %s on line %d +bool(false) diff --git a/Zend/tests/enum/unserialize-refcount.phpt b/Zend/tests/enum/unserialize-refcount.phpt new file mode 100644 index 0000000000..447b3af182 --- /dev/null +++ b/Zend/tests/enum/unserialize-refcount.phpt @@ -0,0 +1,45 @@ +--TEST-- +Enum unserialize refcount +--FILE-- +<?php + +enum Foo { + case Bar; +} + +debug_zval_dump(Foo::Bar); + +$foo = Foo::Bar; +debug_zval_dump($foo); + +$bar = unserialize('E:7:"Foo:Bar";'); +debug_zval_dump($foo); + +unset($bar); +debug_zval_dump($foo); + +unset($foo); +debug_zval_dump(Foo::Bar); + +?> +--EXPECT-- +object(Foo)#1 (1) refcount(2){ + ["name"]=> + string(3) "Bar" interned +} +object(Foo)#1 (1) refcount(3){ + ["name"]=> + string(3) "Bar" interned +} +object(Foo)#1 (1) refcount(4){ + ["name"]=> + string(3) "Bar" interned +} +object(Foo)#1 (1) refcount(3){ + ["name"]=> + string(3) "Bar" interned +} +object(Foo)#1 (1) refcount(2){ + ["name"]=> + string(3) "Bar" interned +} diff --git a/Zend/tests/enum/unserialize.phpt b/Zend/tests/enum/unserialize.phpt new file mode 100644 index 0000000000..c5a10a8fe5 --- /dev/null +++ b/Zend/tests/enum/unserialize.phpt @@ -0,0 +1,24 @@ +--TEST-- +Enum unserialize +--FILE-- +<?php + +enum Foo { + case Bar; + case Quux; +} + +$bar = unserialize('E:7:"Foo:Bar";'); +var_dump($bar); +var_dump($bar === Foo::Bar); + +$quux = unserialize('E:8:"Foo:Quux";'); +var_dump($quux); +var_dump($quux === Foo::Quux); + +?> +--EXPECT-- +enum(Foo::Bar) +bool(true) +enum(Foo::Quux) +bool(true) diff --git a/Zend/tests/enum/value-property-type.phpt b/Zend/tests/enum/value-property-type.phpt new file mode 100644 index 0000000000..ed009dd981 --- /dev/null +++ b/Zend/tests/enum/value-property-type.phpt @@ -0,0 +1,20 @@ +--TEST-- +Enum value property has automatic type +--FILE-- +<?php + +enum IntEnum: int { + case Foo = 0; +} + +enum StringEnum: string { + case Foo = 'Foo'; +} + +echo (new ReflectionProperty(IntEnum::class, 'value'))->getType() . "\n"; +echo (new ReflectionProperty(StringEnum::class, 'value'))->getType() . "\n"; + +?> +--EXPECT-- +int +string diff --git a/Zend/tests/enum/var_dump-nested.phpt b/Zend/tests/enum/var_dump-nested.phpt new file mode 100644 index 0000000000..73a4e51c38 --- /dev/null +++ b/Zend/tests/enum/var_dump-nested.phpt @@ -0,0 +1,20 @@ +--TEST-- +Enum var_dump nested +--FILE-- +<?php + +enum Foo { + case Bar; +} + +var_dump([[Foo::Bar]]); + +?> +--EXPECT-- +array(1) { + [0]=> + array(1) { + [0]=> + enum(Foo::Bar) + } +} diff --git a/Zend/tests/enum/var_dump-reference.phpt b/Zend/tests/enum/var_dump-reference.phpt new file mode 100644 index 0000000000..fe04e7865c --- /dev/null +++ b/Zend/tests/enum/var_dump-reference.phpt @@ -0,0 +1,22 @@ +--TEST-- +Enum var_dump reference +--FILE-- +<?php + +enum Foo { + case Bar; + case Baz; +} + +$arr = [Foo::Bar]; +$arr[1] = &$arr[0]; +var_dump($arr); + +?> +--EXPECT-- +array(2) { + [0]=> + &enum(Foo::Bar) + [1]=> + &enum(Foo::Bar) +} diff --git a/Zend/tests/enum/var_export.phpt b/Zend/tests/enum/var_export.phpt new file mode 100644 index 0000000000..430444d13d --- /dev/null +++ b/Zend/tests/enum/var_export.phpt @@ -0,0 +1,14 @@ +--TEST-- +Enum var_export +--FILE-- +<?php + +enum Foo { + case Bar; +} + +var_export(Foo::Bar); + +?> +--EXPECT-- +Foo::Bar diff --git a/Zend/zend.c b/Zend/zend.c index 822160508a..c76a7e6fba 100644 --- a/Zend/zend.c +++ b/Zend/zend.c @@ -457,11 +457,22 @@ static void zend_print_zval_r_to_buf(smart_str *buf, zval *expr, int indent) /* { HashTable *properties; - zend_string *class_name = Z_OBJ_HANDLER_P(expr, get_class_name)(Z_OBJ_P(expr)); + zend_object *zobj = Z_OBJ_P(expr); + zend_string *class_name = Z_OBJ_HANDLER_P(expr, get_class_name)(zobj); smart_str_appends(buf, ZSTR_VAL(class_name)); zend_string_release_ex(class_name, 0); - smart_str_appends(buf, " Object\n"); + if (!(zobj->ce->ce_flags & ZEND_ACC_ENUM)) { + smart_str_appends(buf, " Object\n"); + } else { + smart_str_appends(buf, " Enum"); + if (zobj->ce->enum_backing_type != IS_UNDEF) { + smart_str_appendc(buf, ':'); + smart_str_appends(buf, zend_get_type_by_const(zobj->ce->enum_backing_type)); + } + smart_str_appendc(buf, '\n'); + } + if (GC_IS_RECURSIVE(Z_OBJ_P(expr))) { smart_str_appends(buf, " *RECURSION*"); return; diff --git a/Zend/zend.h b/Zend/zend.h index e09884941c..aed352cf8d 100644 --- a/Zend/zend.h +++ b/Zend/zend.h @@ -197,6 +197,9 @@ struct _zend_class_entry { zend_trait_precedence **trait_precedences; HashTable *attributes; + uint32_t enum_backing_type; + HashTable *backed_enum_table; + union { struct { zend_string *filename; diff --git a/Zend/zend_API.c b/Zend/zend_API.c index e532e9754a..ca6214ea1f 100644 --- a/Zend/zend_API.c +++ b/Zend/zend_API.c @@ -1583,11 +1583,13 @@ ZEND_API void object_properties_load(zend_object *object, HashTable *properties) * calling zend_merge_properties(). */ static zend_always_inline zend_result _object_and_properties_init(zval *arg, zend_class_entry *class_type, HashTable *properties) /* {{{ */ { - if (UNEXPECTED(class_type->ce_flags & (ZEND_ACC_INTERFACE|ZEND_ACC_TRAIT|ZEND_ACC_IMPLICIT_ABSTRACT_CLASS|ZEND_ACC_EXPLICIT_ABSTRACT_CLASS))) { + if (UNEXPECTED(class_type->ce_flags & (ZEND_ACC_INTERFACE|ZEND_ACC_TRAIT|ZEND_ACC_IMPLICIT_ABSTRACT_CLASS|ZEND_ACC_EXPLICIT_ABSTRACT_CLASS|ZEND_ACC_ENUM))) { if (class_type->ce_flags & ZEND_ACC_INTERFACE) { zend_throw_error(NULL, "Cannot instantiate interface %s", ZSTR_VAL(class_type->name)); } else if (class_type->ce_flags & ZEND_ACC_TRAIT) { zend_throw_error(NULL, "Cannot instantiate trait %s", ZSTR_VAL(class_type->name)); + } else if (class_type->ce_flags & ZEND_ACC_ENUM) { + zend_throw_error(NULL, "Cannot instantiate enum %s", ZSTR_VAL(class_type->name)); } else { zend_throw_error(NULL, "Cannot instantiate abstract class %s", ZSTR_VAL(class_type->name)); } diff --git a/Zend/zend_ast.c b/Zend/zend_ast.c index 73f2390c11..b8d3515155 100644 --- a/Zend/zend_ast.c +++ b/Zend/zend_ast.c @@ -24,6 +24,7 @@ #include "zend_smart_str.h" #include "zend_exceptions.h" #include "zend_constants.h" +#include "zend_enum.h" ZEND_API zend_ast_process_t zend_ast_process = NULL; @@ -740,19 +741,49 @@ ZEND_API zend_result ZEND_FASTCALL zend_ast_evaluate(zval *result, zend_ast *ast if (UNEXPECTED(zend_ast_evaluate(&op1, ast->child[0], scope) != SUCCESS)) { ret = FAILURE; - } else if (UNEXPECTED(zend_ast_evaluate(&op2, ast->child[1], scope) != SUCCESS)) { + break; + } + + // DIM on objects is disallowed because it allows executing arbitrary expressions + if (Z_TYPE(op1) == IS_OBJECT) { zval_ptr_dtor_nogc(&op1); + zend_throw_error(NULL, "Cannot use [] on objects in constant expression"); ret = FAILURE; - } else { - zend_fetch_dimension_const(result, &op1, &op2, (ast->attr & ZEND_DIM_IS) ? BP_VAR_IS : BP_VAR_R); + break; + } + if (UNEXPECTED(zend_ast_evaluate(&op2, ast->child[1], scope) != SUCCESS)) { zval_ptr_dtor_nogc(&op1); - zval_ptr_dtor_nogc(&op2); - if (UNEXPECTED(EG(exception))) { - return FAILURE; - } + ret = FAILURE; + break; } + + zend_fetch_dimension_const(result, &op1, &op2, (ast->attr & ZEND_DIM_IS) ? BP_VAR_IS : BP_VAR_R); + + zval_ptr_dtor_nogc(&op1); + zval_ptr_dtor_nogc(&op2); + if (UNEXPECTED(EG(exception))) { + return FAILURE; + } + break; + case ZEND_AST_CONST_ENUM_INIT: + { + zend_ast *class_name_ast = ast->child[0]; + zend_string *class_name = zend_ast_get_str(class_name_ast); + + zend_ast *case_name_ast = ast->child[1]; + zend_string *case_name = zend_ast_get_str(case_name_ast); + + zend_ast *case_value_ast = ast->child[2]; + zval *case_value_zv = case_value_ast != NULL + ? zend_ast_get_zval(case_value_ast) + : NULL; + + zend_class_entry *ce = zend_lookup_class(class_name); + zend_enum_new(result, ce, case_name, case_value_zv); + break; + } default: zend_throw_error(NULL, "Unsupported constant expression"); ret = FAILURE; @@ -1558,6 +1589,8 @@ tail_call: smart_str_appends(str, "interface "); } else if (decl->flags & ZEND_ACC_TRAIT) { smart_str_appends(str, "trait "); + } else if (decl->flags & ZEND_ACC_ENUM) { + smart_str_appends(str, "enum "); } else { if (decl->flags & ZEND_ACC_EXPLICIT_ABSTRACT_CLASS) { smart_str_appends(str, "abstract "); @@ -1568,6 +1601,10 @@ tail_call: smart_str_appends(str, "class "); } smart_str_appendl(str, ZSTR_VAL(decl->name), ZSTR_LEN(decl->name)); + if (decl->flags & ZEND_ACC_ENUM && decl->child[4]) { + smart_str_appends(str, ": "); + zend_ast_export_type(str, decl->child[4], indent); + } zend_ast_export_class_no_header(str, decl, indent); smart_str_appendc(str, '\n'); break; @@ -2150,6 +2187,17 @@ simple_list: smart_str_appendc(str, '$'); zend_ast_export_name(str, ast->child[1], 0, indent); APPEND_DEFAULT_VALUE(2); + case ZEND_AST_ENUM_CASE: + if (ast->child[2]) { + zend_ast_export_attributes(str, ast->child[2], indent, 1); + } + smart_str_appends(str, "case "); + zend_ast_export_name(str, ast->child[0], 0, indent); + if (ast->child[1]) { + smart_str_appends(str, " = "); + zend_ast_export_ex(str, ast->child[1], 0, indent); + } + break; /* 4 child nodes */ case ZEND_AST_FOR: @@ -2273,6 +2321,9 @@ zend_ast * ZEND_FASTCALL zend_ast_with_attributes(zend_ast *ast, zend_ast *attr) case ZEND_AST_CLASS_CONST_GROUP: ast->child[1] = attr; break; + case ZEND_AST_ENUM_CASE: + ast->child[2] = attr; + break; EMPTY_SWITCH_DEFAULT_CASE() } diff --git a/Zend/zend_ast.h b/Zend/zend_ast.h index 4d7335853e..fb6587b48c 100644 --- a/Zend/zend_ast.h +++ b/Zend/zend_ast.h @@ -159,6 +159,10 @@ enum _zend_ast_kind { ZEND_AST_PROP_GROUP, ZEND_AST_PROP_ELEM, ZEND_AST_CONST_ELEM, + ZEND_AST_ENUM_CASE, + + // Pseudo node for initializing enums + ZEND_AST_CONST_ENUM_INIT, /* 4 child nodes */ ZEND_AST_FOR = 4 << ZEND_AST_NUM_CHILDREN_SHIFT, diff --git a/Zend/zend_builtin_functions.c b/Zend/zend_builtin_functions.c index 47c7c6ea31..4c720c53db 100644 --- a/Zend/zend_builtin_functions.c +++ b/Zend/zend_builtin_functions.c @@ -1039,6 +1039,11 @@ ZEND_FUNCTION(trait_exists) } /* }}} */ +ZEND_FUNCTION(enum_exists) +{ + class_exists_impl(INTERNAL_FUNCTION_PARAM_PASSTHRU, ZEND_ACC_ENUM, 0); +} + /* {{{ Checks if the function exists */ ZEND_FUNCTION(function_exists) { diff --git a/Zend/zend_builtin_functions.stub.php b/Zend/zend_builtin_functions.stub.php index 87a530200d..9df6659f2d 100644 --- a/Zend/zend_builtin_functions.stub.php +++ b/Zend/zend_builtin_functions.stub.php @@ -63,6 +63,8 @@ function interface_exists(string $interface, bool $autoload = true): bool {} function trait_exists(string $trait, bool $autoload = true): bool {} +function enum_exists(string $enum, bool $autoload = true): bool {} + function function_exists(string $function): bool {} function class_alias(string $class, string $alias, bool $autoload = true): bool {} diff --git a/Zend/zend_builtin_functions_arginfo.h b/Zend/zend_builtin_functions_arginfo.h index 6baaead27d..f707e0a8a3 100644 --- a/Zend/zend_builtin_functions_arginfo.h +++ b/Zend/zend_builtin_functions_arginfo.h @@ -1,5 +1,5 @@ /* This is a generated file, edit the .stub.php file instead. - * Stub hash: 429fc9b22054348101d0b9d6746494e52dc04edf */ + * Stub hash: c333499e3ea100d976f0fa8bb8800ed21b04f1c6 */ ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_zend_version, 0, 0, IS_STRING, 0) ZEND_END_ARG_INFO() @@ -108,6 +108,11 @@ ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_trait_exists, 0, 1, _IS_BOOL, 0) ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, autoload, _IS_BOOL, 0, "true") ZEND_END_ARG_INFO() +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_enum_exists, 0, 1, _IS_BOOL, 0) + ZEND_ARG_TYPE_INFO(0, enum, IS_STRING, 0) + ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, autoload, _IS_BOOL, 0, "true") +ZEND_END_ARG_INFO() + ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_function_exists, 0, 1, _IS_BOOL, 0) ZEND_ARG_TYPE_INFO(0, function, IS_STRING, 0) ZEND_END_ARG_INFO() @@ -238,6 +243,7 @@ ZEND_FUNCTION(property_exists); ZEND_FUNCTION(class_exists); ZEND_FUNCTION(interface_exists); ZEND_FUNCTION(trait_exists); +ZEND_FUNCTION(enum_exists); ZEND_FUNCTION(function_exists); ZEND_FUNCTION(class_alias); ZEND_FUNCTION(get_included_files); @@ -298,6 +304,7 @@ static const zend_function_entry ext_functions[] = { ZEND_FE(class_exists, arginfo_class_exists) ZEND_FE(interface_exists, arginfo_interface_exists) ZEND_FE(trait_exists, arginfo_trait_exists) + ZEND_FE(enum_exists, arginfo_enum_exists) ZEND_FE(function_exists, arginfo_function_exists) ZEND_FE(class_alias, arginfo_class_alias) ZEND_FE(get_included_files, arginfo_get_included_files) diff --git a/Zend/zend_compile.c b/Zend/zend_compile.c index 3e0eae5f57..76c2ed9644 100644 --- a/Zend/zend_compile.c +++ b/Zend/zend_compile.c @@ -32,6 +32,7 @@ #include "zend_language_scanner.h" #include "zend_inheritance.h" #include "zend_vm.h" +#include "zend_enum.h" #define SET_NODE(target, src) do { \ target ## _type = (src)->op_type; \ @@ -1873,6 +1874,8 @@ ZEND_API void zend_initialize_class_data(zend_class_entry *ce, bool nullify_hand ce->default_static_members_count = 0; ce->properties_info_table = NULL; ce->attributes = NULL; + ce->enum_backing_type = IS_UNDEF; + ce->backed_enum_table = NULL; if (nullify_handlers) { ce->constructor = NULL; @@ -7131,7 +7134,11 @@ void zend_compile_prop_decl(zend_ast *ast, zend_ast *type_ast, uint32_t flags, z uint32_t i, children = list->children; if (ce->ce_flags & ZEND_ACC_INTERFACE) { - zend_error_noreturn(E_COMPILE_ERROR, "Interfaces may not include member variables"); + zend_error_noreturn(E_COMPILE_ERROR, "Interfaces may not include properties"); + } + + if (ce->ce_flags & ZEND_ACC_ENUM) { + zend_error_noreturn(E_COMPILE_ERROR, "Enums may not include properties"); } if (flags & ZEND_ACC_ABSTRACT) { @@ -7422,12 +7429,36 @@ static zend_string *zend_generate_anon_class_name(zend_ast_decl *decl) return zend_new_interned_string(result); } +static void zend_compile_enum_backing_type(zend_class_entry *ce, zend_ast *enum_backing_type_ast) +{ + ZEND_ASSERT(ce->ce_flags & ZEND_ACC_ENUM); + zend_type type = zend_compile_typename(enum_backing_type_ast, 0); + uint32_t type_mask = ZEND_TYPE_PURE_MASK(type); + if (ZEND_TYPE_HAS_CLASS(type) || (type_mask != MAY_BE_LONG && type_mask != MAY_BE_STRING)) { + zend_string *type_string = zend_type_to_string(type); + zend_error_noreturn(E_COMPILE_ERROR, + "Enum backing type must be int or string, %s given", + ZSTR_VAL(type_string)); + } + if (type_mask == MAY_BE_LONG) { + ce->enum_backing_type = IS_LONG; + } else { + ZEND_ASSERT(type_mask == MAY_BE_STRING); + ce->enum_backing_type = IS_STRING; + } + zend_type_release(type, 0); + + ce->backed_enum_table = emalloc(sizeof(HashTable)); + zend_hash_init(ce->backed_enum_table, 0, NULL, ZVAL_PTR_DTOR, 0); +} + void zend_compile_class_decl(znode *result, zend_ast *ast, bool toplevel) /* {{{ */ { zend_ast_decl *decl = (zend_ast_decl *) ast; zend_ast *extends_ast = decl->child[0]; zend_ast *implements_ast = decl->child[1]; zend_ast *stmt_ast = decl->child[2]; + zend_ast *enum_backing_type_ast = decl->child[4]; zend_string *name, *lcname; zend_class_entry *ce = zend_arena_alloc(&CG(arena), sizeof(zend_class_entry)); zend_op *opline; @@ -7508,6 +7539,14 @@ void zend_compile_class_decl(znode *result, zend_ast *ast, bool toplevel) /* {{{ zend_compile_implements(implements_ast); } + if (ce->ce_flags & ZEND_ACC_ENUM) { + if (enum_backing_type_ast != NULL) { + zend_compile_enum_backing_type(ce, enum_backing_type_ast); + } + zend_enum_add_interfaces(ce); + zend_enum_register_props(ce); + } + zend_compile_stmt(stmt_ast); /* Reset lineno for final opcodes and errors */ @@ -7604,6 +7643,99 @@ void zend_compile_class_decl(znode *result, zend_ast *ast, bool toplevel) /* {{{ } /* }}} */ +static void zend_compile_enum_case(zend_ast *ast) +{ + zend_class_entry *enum_class = CG(active_class_entry); + if (!(enum_class->ce_flags & ZEND_ACC_ENUM)) { + zend_error_noreturn(E_COMPILE_ERROR, "Case can only be used in enums"); + } + + zend_string *enum_case_name = zval_make_interned_string(zend_ast_get_zval(ast->child[0])); + zend_string *enum_class_name = enum_class->name; + + zval class_name_zval; + ZVAL_STR_COPY(&class_name_zval, enum_class_name); + zend_ast *class_name_ast = zend_ast_create_zval(&class_name_zval); + + zval case_name_zval; + ZVAL_STR_COPY(&case_name_zval, enum_case_name); + zend_ast *case_name_ast = zend_ast_create_zval(&case_name_zval); + + zend_ast *case_value_zval_ast = NULL; + zend_ast *case_value_ast = ast->child[1]; + if (enum_class->enum_backing_type != IS_UNDEF && case_value_ast == NULL) { + zend_error_noreturn(E_COMPILE_ERROR, "Case %s of backed enum %s must have a value", + ZSTR_VAL(enum_case_name), + ZSTR_VAL(enum_class_name)); + } + if (case_value_ast != NULL) { + zend_eval_const_expr(&ast->child[1]); + case_value_ast = ast->child[1]; + if (case_value_ast->kind != ZEND_AST_ZVAL) { + zend_error_noreturn(E_COMPILE_ERROR, "Enum case value must be constant"); + } + + zval case_value_zv; + ZVAL_COPY(&case_value_zv, zend_ast_get_zval(case_value_ast)); + if (enum_class->enum_backing_type == IS_UNDEF) { + if (Z_TYPE(case_value_zv) == IS_LONG || Z_TYPE(case_value_zv) == IS_STRING) { + zend_error_noreturn(E_COMPILE_ERROR, "Case %s of non-backed enum %s must not have a value, try adding \": %s\" to the enum declaration", + ZSTR_VAL(enum_case_name), + ZSTR_VAL(enum_class_name), + zend_zval_type_name(&case_value_zv)); + } else { + zend_error_noreturn(E_COMPILE_ERROR, "Case %s of non-backed enum %s must not have a value", + ZSTR_VAL(enum_case_name), + ZSTR_VAL(enum_class_name)); + } + } + + if (enum_class->enum_backing_type != Z_TYPE(case_value_zv)) { + zend_error_noreturn(E_COMPILE_ERROR, "Enum case type %s does not match enum backing type %s", + zend_get_type_by_const(Z_TYPE(case_value_zv)), + zend_get_type_by_const(enum_class->enum_backing_type)); + } + + case_value_zval_ast = zend_ast_create_zval(&case_value_zv); + Z_TRY_ADDREF(case_name_zval); + if (enum_class->enum_backing_type == IS_LONG) { + zend_long long_key = Z_LVAL(case_value_zv); + zval *existing_case_name = zend_hash_index_find(enum_class->backed_enum_table, long_key); + if (existing_case_name) { + zend_error_noreturn(E_COMPILE_ERROR, "Duplicate value in enum %s for cases %s and %s", + ZSTR_VAL(enum_class_name), + Z_STRVAL_P(existing_case_name), + ZSTR_VAL(enum_case_name)); + } + zend_hash_index_add_new(enum_class->backed_enum_table, long_key, &case_name_zval); + } else { + ZEND_ASSERT(enum_class->enum_backing_type == IS_STRING); + zend_string *string_key = Z_STR(case_value_zv); + zval *existing_case_name = zend_hash_find(enum_class->backed_enum_table, string_key); + if (existing_case_name != NULL) { + zend_error_noreturn(E_COMPILE_ERROR, "Duplicate value in enum %s for cases %s and %s", + ZSTR_VAL(enum_class_name), + Z_STRVAL_P(existing_case_name), + ZSTR_VAL(enum_case_name)); + } + zend_hash_add_new(enum_class->backed_enum_table, string_key, &case_name_zval); + } + } + + zend_ast *const_enum_init_ast = zend_ast_create(ZEND_AST_CONST_ENUM_INIT, class_name_ast, case_name_ast, case_value_zval_ast); + + zval value_zv; + zend_const_expr_to_zval(&value_zv, &const_enum_init_ast); + zend_class_constant *c = zend_declare_class_constant_ex(enum_class, enum_case_name, &value_zv, ZEND_ACC_PUBLIC, NULL); + Z_ACCESS_FLAGS(c->value) |= ZEND_CLASS_CONST_IS_CASE; + zend_ast_destroy(const_enum_init_ast); + + zend_ast *attr_ast = ast->child[2]; + if (attr_ast) { + zend_compile_attributes(&c->attributes, attr_ast, 0, ZEND_ATTRIBUTE_TARGET_CLASS_CONST); + } +} + static HashTable *zend_get_import_ht(uint32_t type) /* {{{ */ { switch (type) { @@ -9317,7 +9449,8 @@ bool zend_is_allowed_in_const_expr(zend_ast_kind kind) /* {{{ */ || kind == ZEND_AST_UNPACK || kind == ZEND_AST_CONST || kind == ZEND_AST_CLASS_CONST || kind == ZEND_AST_CLASS_NAME - || kind == ZEND_AST_MAGIC_CONST || kind == ZEND_AST_COALESCE; + || kind == ZEND_AST_MAGIC_CONST || kind == ZEND_AST_COALESCE + || kind == ZEND_AST_CONST_ENUM_INIT; } /* }}} */ @@ -9572,6 +9705,9 @@ void zend_compile_stmt(zend_ast *ast) /* {{{ */ case ZEND_AST_METHOD: zend_compile_func_decl(NULL, ast, 0); break; + case ZEND_AST_ENUM_CASE: + zend_compile_enum_case(ast); + break; case ZEND_AST_PROP_GROUP: zend_compile_prop_group(ast); break; diff --git a/Zend/zend_compile.h b/Zend/zend_compile.h index 6f64624fd8..9e22837d61 100644 --- a/Zend/zend_compile.h +++ b/Zend/zend_compile.h @@ -233,13 +233,19 @@ typedef struct _zend_oparray_context { /* op_array or class is preloaded | | | */ #define ZEND_ACC_PRELOADED (1 << 10) /* X | X | | */ /* | | | */ -/* Class Flags (unused: 28...) | | | */ +/* Flag to differenciate cases from constants. | | | */ +/* Stored in Z_ACCESS_FLAGS, must not conflict with | | | */ +/* ZEND_ACC_ visibility flags or IS_CONSTANT_VISITED_MARK | | | */ +#define ZEND_CLASS_CONST_IS_CASE (1 << 6) /* | | | X */ +/* | | | */ +/* Class Flags (unused: 29...) | | | */ /* =========== | | | */ /* | | | */ /* Special class types | | | */ #define ZEND_ACC_INTERFACE (1 << 0) /* X | | | */ #define ZEND_ACC_TRAIT (1 << 1) /* X | | | */ #define ZEND_ACC_ANON_CLASS (1 << 2) /* X | | | */ +#define ZEND_ACC_ENUM (1 << 28) /* X | | | */ /* | | | */ /* Class linked with parent, interfaces and traits | | | */ #define ZEND_ACC_LINKED (1 << 3) /* X | | | */ @@ -387,7 +393,7 @@ typedef struct _zend_property_info { ((offset - OBJ_PROP_TO_OFFSET(0)) / sizeof(zval)) typedef struct _zend_class_constant { - zval value; /* access flags are stored in reserved: zval.u2.access_flags */ + zval value; /* access flags and other constant flags are stored in reserved: zval.u2.access_flags */ zend_string *doc_comment; HashTable *attributes; zend_class_entry *ce; diff --git a/Zend/zend_default_classes.c b/Zend/zend_default_classes.c index 63cdf66d58..c0fa6f5a67 100644 --- a/Zend/zend_default_classes.c +++ b/Zend/zend_default_classes.c @@ -26,6 +26,7 @@ #include "zend_closures.h" #include "zend_generators.h" #include "zend_weakrefs.h" +#include "zend_enum.h" ZEND_API void zend_register_default_classes(void) { @@ -36,4 +37,5 @@ ZEND_API void zend_register_default_classes(void) zend_register_generator_ce(); zend_register_weakref_ce(); zend_register_attribute_ce(); + zend_register_enum_ce(); } diff --git a/Zend/zend_enum.c b/Zend/zend_enum.c new file mode 100644 index 0000000000..728da8af36 --- /dev/null +++ b/Zend/zend_enum.c @@ -0,0 +1,373 @@ +/* + +----------------------------------------------------------------------+ + | Zend Engine | + +----------------------------------------------------------------------+ + | Copyright (c) Zend Technologies Ltd. (http://www.zend.com) | + +----------------------------------------------------------------------+ + | This source file is subject to version 2.00 of the Zend license, | + | that is bundled with this package in the file LICENSE, and is | + | available through the world-wide-web at the following url: | + | http://www.zend.com/license/2_00.txt. | + | If you did not receive a copy of the Zend license and are unable to | + | obtain it through the world-wide-web, please send a note to | + | license@zend.com so we can mail you a copy immediately. | + +----------------------------------------------------------------------+ + | Authors: Ilija Tovilo <ilutov@php.net> | + +----------------------------------------------------------------------+ +*/ + +#include "zend.h" +#include "zend_API.h" +#include "zend_compile.h" +#include "zend_enum_arginfo.h" +#include "zend_interfaces.h" + +#define ZEND_ENUM_PROPERTY_ERROR() \ + zend_throw_error(NULL, "Enum properties are immutable") + +#define ZEND_ENUM_DISALLOW_MAGIC_METHOD(propertyName, methodName) \ + do { \ + if (ce->propertyName) { \ + zend_error_noreturn(E_COMPILE_ERROR, "Enum may not include %s", methodName); \ + } \ + } while (0); + +ZEND_API zend_class_entry *zend_ce_unit_enum; +ZEND_API zend_class_entry *zend_ce_backed_enum; + +static zend_object_handlers enum_handlers; + +zend_object *zend_enum_new(zval *result, zend_class_entry *ce, zend_string *case_name, zval *backing_value_zv) +{ + zend_object *zobj = zend_objects_new(ce); + ZVAL_OBJ(result, zobj); + + ZVAL_STR_COPY(OBJ_PROP_NUM(zobj, 0), case_name); + if (backing_value_zv != NULL) { + ZVAL_COPY(OBJ_PROP_NUM(zobj, 1), backing_value_zv); + } + + zobj->handlers = &enum_handlers; + + return zobj; +} + +static void zend_verify_enum_properties(zend_class_entry *ce) +{ + zend_property_info *property_info; + + ZEND_HASH_FOREACH_PTR(&ce->properties_info, property_info) { + if (zend_string_equals_literal(property_info->name, "name")) { + continue; + } + if ( + ce->enum_backing_type != IS_UNDEF + && zend_string_equals_literal(property_info->name, "value") + ) { + continue; + } + // FIXME: File/line number for traits? + zend_error_noreturn(E_COMPILE_ERROR, "Enum \"%s\" may not include properties", + ZSTR_VAL(ce->name)); + } ZEND_HASH_FOREACH_END(); +} + +static void zend_verify_enum_magic_methods(zend_class_entry *ce) +{ + // Only __get, __call and __invoke are allowed + + ZEND_ENUM_DISALLOW_MAGIC_METHOD(constructor, "__construct"); + ZEND_ENUM_DISALLOW_MAGIC_METHOD(destructor, "__destruct"); + ZEND_ENUM_DISALLOW_MAGIC_METHOD(clone, "__clone"); + ZEND_ENUM_DISALLOW_MAGIC_METHOD(__get, "__get"); + ZEND_ENUM_DISALLOW_MAGIC_METHOD(__set, "__set"); + ZEND_ENUM_DISALLOW_MAGIC_METHOD(__unset, "__unset"); + ZEND_ENUM_DISALLOW_MAGIC_METHOD(__isset, "__isset"); + ZEND_ENUM_DISALLOW_MAGIC_METHOD(__tostring, "__toString"); + ZEND_ENUM_DISALLOW_MAGIC_METHOD(__debugInfo, "__debugInfo"); + ZEND_ENUM_DISALLOW_MAGIC_METHOD(__serialize, "__serialize"); + ZEND_ENUM_DISALLOW_MAGIC_METHOD(__unserialize, "__unserialize"); + + const char *forbidden_methods[] = { + "__sleep", + "__wakeup", + "__set_state", + }; + + uint32_t forbidden_methods_length = sizeof(forbidden_methods) / sizeof(forbidden_methods[0]); + for (uint32_t i = 0; i < forbidden_methods_length; ++i) { + const char *forbidden_method = forbidden_methods[i]; + + if (zend_hash_str_exists(&ce->function_table, forbidden_method, strlen(forbidden_method))) { + zend_error_noreturn(E_COMPILE_ERROR, "Enum may not include magic method %s", forbidden_method); + } + } +} + +static void zend_verify_enum_interfaces(zend_class_entry *ce) +{ + if (zend_class_implements_interface(ce, zend_ce_serializable)) { + zend_error_noreturn(E_COMPILE_ERROR, + "Enums may not implement the Serializable interface"); + } +} + +void zend_verify_enum(zend_class_entry *ce) +{ + zend_verify_enum_properties(ce); + zend_verify_enum_magic_methods(ce); + zend_verify_enum_interfaces(ce); +} + +static zval *zend_enum_read_property(zend_object *zobj, zend_string *name, int type, void **cache_slot, zval *rv) /* {{{ */ +{ + if (type == BP_VAR_W || type == BP_VAR_RW || type == BP_VAR_UNSET) { + zend_throw_error(NULL, "Cannot acquire reference to property %s::$%s", ZSTR_VAL(zobj->ce->name), ZSTR_VAL(name)); + return &EG(uninitialized_zval); + } + + return zend_std_read_property(zobj, name, type, cache_slot, rv); +} + +static ZEND_COLD zval *zend_enum_write_property(zend_object *object, zend_string *member, zval *value, void **cache_slot) +{ + ZEND_ENUM_PROPERTY_ERROR(); + return &EG(uninitialized_zval); +} + +static ZEND_COLD void zend_enum_unset_property(zend_object *object, zend_string *member, void **cache_slot) +{ + ZEND_ENUM_PROPERTY_ERROR(); +} + +static zval *zend_enum_get_property_ptr_ptr(zend_object *zobj, zend_string *name, int type, void **cache_slot) +{ + return NULL; +} + +static int zend_implement_unit_enum(zend_class_entry *interface, zend_class_entry *class_type) +{ + if (class_type->ce_flags & ZEND_ACC_ENUM) { + return SUCCESS; + } + + zend_error_noreturn(E_ERROR, "Non-enum class %s cannot implement interface %s", + ZSTR_VAL(class_type->name), + ZSTR_VAL(interface->name)); + + return FAILURE; +} + +static int zend_implement_backed_enum(zend_class_entry *interface, zend_class_entry *class_type) +{ + if (!(class_type->ce_flags & ZEND_ACC_ENUM)) { + zend_error_noreturn(E_ERROR, "Non-enum class %s cannot implement interface %s", + ZSTR_VAL(class_type->name), + ZSTR_VAL(interface->name)); + return FAILURE; + } + + if (class_type->enum_backing_type == IS_UNDEF) { + zend_error_noreturn(E_ERROR, "Non-backed enum %s cannot implement interface %s", + ZSTR_VAL(class_type->name), + ZSTR_VAL(interface->name)); + return FAILURE; + } + + return SUCCESS; +} + +void zend_register_enum_ce(void) +{ + zend_ce_unit_enum = register_class_UnitEnum(); + zend_ce_unit_enum->interface_gets_implemented = zend_implement_unit_enum; + + zend_ce_backed_enum = register_class_BackedEnum(zend_ce_unit_enum); + zend_ce_backed_enum->interface_gets_implemented = zend_implement_backed_enum; + + memcpy(&enum_handlers, &std_object_handlers, sizeof(zend_object_handlers)); + enum_handlers.read_property = zend_enum_read_property; + enum_handlers.write_property = zend_enum_write_property; + enum_handlers.unset_property = zend_enum_unset_property; + enum_handlers.get_property_ptr_ptr = zend_enum_get_property_ptr_ptr; + enum_handlers.clone_obj = NULL; + enum_handlers.compare = zend_objects_not_comparable; +} + +void zend_enum_add_interfaces(zend_class_entry *ce) +{ + uint32_t num_interfaces_before = ce->num_interfaces; + + ce->num_interfaces++; + if (ce->enum_backing_type != IS_UNDEF) { + ce->num_interfaces++; + } + + ZEND_ASSERT(!(ce->ce_flags & ZEND_ACC_RESOLVED_INTERFACES)); + + ce->interface_names = erealloc(ce->interface_names, sizeof(zend_class_name) * ce->num_interfaces); + + ce->interface_names[num_interfaces_before].name = zend_string_copy(zend_ce_unit_enum->name); + ce->interface_names[num_interfaces_before].lc_name = zend_string_init("unitenum", sizeof("unitenum") - 1, 0); + + if (ce->enum_backing_type != IS_UNDEF) { + ce->interface_names[num_interfaces_before + 1].name = zend_string_copy(zend_ce_backed_enum->name); + ce->interface_names[num_interfaces_before + 1].lc_name = zend_string_init("backedenum", sizeof("backedenum") - 1, 0); + } +} + +static ZEND_NAMED_FUNCTION(zend_enum_cases_func) +{ + zend_class_entry *ce = execute_data->func->common.scope; + zend_class_constant *c; + + ZEND_PARSE_PARAMETERS_NONE(); + + array_init(return_value); + + ZEND_HASH_FOREACH_PTR(CE_CONSTANTS_TABLE(ce), c) { + if (!(Z_ACCESS_FLAGS(c->value) & ZEND_CLASS_CONST_IS_CASE)) { + continue; + } + zval *zv = &c->value; + if (Z_TYPE_P(zv) == IS_CONSTANT_AST) { + if (zval_update_constant_ex(zv, c->ce) == FAILURE) { + RETURN_THROWS(); + } + } + Z_ADDREF_P(zv); + zend_hash_next_index_insert(Z_ARRVAL_P(return_value), zv); + } ZEND_HASH_FOREACH_END(); +} + +static void zend_enum_from_base(INTERNAL_FUNCTION_PARAMETERS, bool try) +{ + zend_class_entry *ce = execute_data->func->common.scope; + zend_string *string_key; + zend_long long_key; + + zval *case_name_zv; + if (ce->enum_backing_type == IS_LONG) { + ZEND_PARSE_PARAMETERS_START(1, 1) + Z_PARAM_LONG(long_key) + ZEND_PARSE_PARAMETERS_END(); + + case_name_zv = zend_hash_index_find(ce->backed_enum_table, long_key); + } else { + ZEND_PARSE_PARAMETERS_START(1, 1) + Z_PARAM_STR(string_key) + ZEND_PARSE_PARAMETERS_END(); + + ZEND_ASSERT(ce->enum_backing_type == IS_STRING); + case_name_zv = zend_hash_find(ce->backed_enum_table, string_key); + } + + if (case_name_zv == NULL) { + if (try) { + RETURN_NULL(); + } + + if (ce->enum_backing_type == IS_LONG) { + zend_value_error(ZEND_LONG_FMT " is not a valid backing value for enum \"%s\"", long_key, ZSTR_VAL(ce->name)); + } else { + ZEND_ASSERT(ce->enum_backing_type == IS_STRING); + zend_value_error("\"%s\" is not a valid backing value for enum \"%s\"", ZSTR_VAL(string_key), ZSTR_VAL(ce->name)); + } + RETURN_THROWS(); + } + + // TODO: We might want to store pointers to constants in backed_enum_table instead of names, + // to make this lookup more efficient. + ZEND_ASSERT(Z_TYPE_P(case_name_zv) == IS_STRING); + zend_class_constant *c = zend_hash_find_ptr(CE_CONSTANTS_TABLE(ce), Z_STR_P(case_name_zv)); + ZEND_ASSERT(c != NULL); + zval *case_zv = &c->value; + if (Z_TYPE_P(case_zv) == IS_CONSTANT_AST) { + if (zval_update_constant_ex(case_zv, c->ce) == FAILURE) { + RETURN_THROWS(); + } + } + + ZVAL_COPY(return_value, case_zv); +} + +static ZEND_NAMED_FUNCTION(zend_enum_from_func) +{ + zend_enum_from_base(INTERNAL_FUNCTION_PARAM_PASSTHRU, 0); +} + +static ZEND_NAMED_FUNCTION(zend_enum_try_from_func) +{ + zend_enum_from_base(INTERNAL_FUNCTION_PARAM_PASSTHRU, 1); +} + +void zend_enum_register_funcs(zend_class_entry *ce) +{ + const uint32_t fn_flags = + ZEND_ACC_PUBLIC|ZEND_ACC_STATIC|ZEND_ACC_HAS_RETURN_TYPE|ZEND_ACC_ARENA_ALLOCATED; + zend_internal_function *cases_function = + zend_arena_alloc(&CG(arena), sizeof(zend_internal_function)); + memset(cases_function, 0, sizeof(zend_internal_function)); + cases_function->type = ZEND_INTERNAL_FUNCTION; + cases_function->module = EG(current_module); + cases_function->handler = zend_enum_cases_func; + cases_function->function_name = ZSTR_KNOWN(ZEND_STR_CASES); + cases_function->scope = ce; + cases_function->fn_flags = fn_flags; + cases_function->arg_info = (zend_internal_arg_info *) (arginfo_class_UnitEnum_cases + 1); + if (!zend_hash_add_ptr(&ce->function_table, ZSTR_KNOWN(ZEND_STR_CASES), cases_function)) { + zend_error_noreturn(E_COMPILE_ERROR, "Cannot redeclare %s::cases()", ZSTR_VAL(ce->name)); + } + + if (ce->enum_backing_type != IS_UNDEF) { + zend_internal_function *from_function = + zend_arena_alloc(&CG(arena), sizeof(zend_internal_function)); + memset(from_function, 0, sizeof(zend_internal_function)); + from_function->type = ZEND_INTERNAL_FUNCTION; + from_function->module = EG(current_module); + from_function->handler = zend_enum_from_func; + from_function->function_name = ZSTR_KNOWN(ZEND_STR_FROM); + from_function->scope = ce; + from_function->fn_flags = fn_flags; + from_function->num_args = 1; + from_function->required_num_args = 1; + from_function->arg_info = (zend_internal_arg_info *) (arginfo_class_BackedEnum_from + 1); + if (!zend_hash_add_ptr(&ce->function_table, ZSTR_KNOWN(ZEND_STR_FROM), from_function)) { + zend_error_noreturn(E_COMPILE_ERROR, + "Cannot redeclare %s::from()", ZSTR_VAL(ce->name)); + } + + zend_internal_function *try_from_function = + zend_arena_alloc(&CG(arena), sizeof(zend_internal_function)); + memset(try_from_function, 0, sizeof(zend_internal_function)); + try_from_function->type = ZEND_INTERNAL_FUNCTION; + try_from_function->module = EG(current_module); + try_from_function->handler = zend_enum_try_from_func; + try_from_function->function_name = ZSTR_KNOWN(ZEND_STR_TRYFROM); + try_from_function->scope = ce; + try_from_function->fn_flags = fn_flags; + try_from_function->num_args = 1; + try_from_function->required_num_args = 1; + try_from_function->arg_info = (zend_internal_arg_info *) (arginfo_class_BackedEnum_tryFrom + 1); + if (!zend_hash_add_ptr( + &ce->function_table, ZSTR_KNOWN(ZEND_STR_TRYFROM_LOWERCASE), try_from_function)) { + zend_error_noreturn(E_COMPILE_ERROR, + "Cannot redeclare %s::tryFrom()", ZSTR_VAL(ce->name)); + } + } +} + +void zend_enum_register_props(zend_class_entry *ce) +{ + zval name_default_value; + ZVAL_UNDEF(&name_default_value); + zend_type name_type = ZEND_TYPE_INIT_CODE(IS_STRING, 0, 0); + zend_declare_typed_property(ce, ZSTR_KNOWN(ZEND_STR_NAME), &name_default_value, ZEND_ACC_PUBLIC, NULL, name_type); + + if (ce->enum_backing_type != IS_UNDEF) { + zval value_default_value; + ZVAL_UNDEF(&value_default_value); + zend_type value_type = ZEND_TYPE_INIT_CODE(ce->enum_backing_type, 0, 0); + zend_declare_typed_property(ce, ZSTR_KNOWN(ZEND_STR_VALUE), &value_default_value, ZEND_ACC_PUBLIC, NULL, value_type); + } +} diff --git a/Zend/zend_enum.h b/Zend/zend_enum.h new file mode 100644 index 0000000000..c0abe7311f --- /dev/null +++ b/Zend/zend_enum.h @@ -0,0 +1,52 @@ +/* + +----------------------------------------------------------------------+ + | Zend Engine | + +----------------------------------------------------------------------+ + | Copyright (c) Zend Technologies Ltd. (http://www.zend.com) | + +----------------------------------------------------------------------+ + | This source file is subject to version 2.00 of the Zend license, | + | that is bundled with this package in the file LICENSE, and is | + | available through the world-wide-web at the following url: | + | http://www.zend.com/license/2_00.txt. | + | If you did not receive a copy of the Zend license and are unable to | + | obtain it through the world-wide-web, please send a note to | + | license@zend.com so we can mail you a copy immediately. | + +----------------------------------------------------------------------+ + | Authors: Ilija Tovilo <ilutov@php.net> | + +----------------------------------------------------------------------+ +*/ + +#ifndef ZEND_ENUM_H +#define ZEND_ENUM_H + +#include "zend.h" +#include "zend_types.h" + +BEGIN_EXTERN_C() + +extern ZEND_API zend_class_entry *zend_ce_unit_enum; +extern ZEND_API zend_class_entry *zend_ce_backed_enum; + +void zend_register_enum_ce(void); +void zend_enum_add_interfaces(zend_class_entry *ce); +zend_object *zend_enum_new(zval *result, zend_class_entry *ce, zend_string *case_name, zval *backing_value_zv); +void zend_verify_enum(zend_class_entry *ce); +void zend_enum_register_funcs(zend_class_entry *ce); +void zend_enum_register_props(zend_class_entry *ce); + +static zend_always_inline zval *zend_enum_fetch_case_name(zend_object *zobj) +{ + ZEND_ASSERT(zobj->ce->ce_flags & ZEND_ACC_ENUM); + return OBJ_PROP_NUM(zobj, 0); +} + +static zend_always_inline zval *zend_enum_fetch_case_value(zend_object *zobj) +{ + ZEND_ASSERT(zobj->ce->ce_flags & ZEND_ACC_ENUM); + ZEND_ASSERT(zobj->ce->enum_backing_type != IS_UNDEF); + return OBJ_PROP_NUM(zobj, 1); +} + +END_EXTERN_C() + +#endif /* ZEND_ENUM_H */ diff --git a/Zend/zend_enum.stub.php b/Zend/zend_enum.stub.php new file mode 100644 index 0000000000..727514a7bd --- /dev/null +++ b/Zend/zend_enum.stub.php @@ -0,0 +1,15 @@ +<?php + +/** @generate-class-entries */ + +interface UnitEnum +{ + public static function cases(): array; +} + +interface BackedEnum extends UnitEnum +{ + public static function from(int|string $value): static; + + public static function tryFrom(int|string $value): ?static; +} diff --git a/Zend/zend_enum_arginfo.h b/Zend/zend_enum_arginfo.h new file mode 100644 index 0000000000..a1b53cfde4 --- /dev/null +++ b/Zend/zend_enum_arginfo.h @@ -0,0 +1,49 @@ +/* This is a generated file, edit the .stub.php file instead. + * Stub hash: 7092f1d4ba651f077cff37050899f090f00abf22 */ + +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_UnitEnum_cases, 0, 0, IS_ARRAY, 0) +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_BackedEnum_from, 0, 1, IS_STATIC, 0) + ZEND_ARG_TYPE_MASK(0, value, MAY_BE_LONG|MAY_BE_STRING, NULL) +ZEND_END_ARG_INFO() + +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_class_BackedEnum_tryFrom, 0, 1, IS_STATIC, 1) + ZEND_ARG_TYPE_MASK(0, value, MAY_BE_LONG|MAY_BE_STRING, NULL) +ZEND_END_ARG_INFO() + + + + +static const zend_function_entry class_UnitEnum_methods[] = { + ZEND_ABSTRACT_ME_WITH_FLAGS(UnitEnum, cases, arginfo_class_UnitEnum_cases, ZEND_ACC_PUBLIC|ZEND_ACC_STATIC|ZEND_ACC_ABSTRACT) + ZEND_FE_END +}; + + +static const zend_function_entry class_BackedEnum_methods[] = { + ZEND_ABSTRACT_ME_WITH_FLAGS(BackedEnum, from, arginfo_class_BackedEnum_from, ZEND_ACC_PUBLIC|ZEND_ACC_STATIC|ZEND_ACC_ABSTRACT) + ZEND_ABSTRACT_ME_WITH_FLAGS(BackedEnum, tryFrom, arginfo_class_BackedEnum_tryFrom, ZEND_ACC_PUBLIC|ZEND_ACC_STATIC|ZEND_ACC_ABSTRACT) + ZEND_FE_END +}; + +static zend_class_entry *register_class_UnitEnum(void) +{ + zend_class_entry ce, *class_entry; + + INIT_CLASS_ENTRY(ce, "UnitEnum", class_UnitEnum_methods); + class_entry = zend_register_internal_interface(&ce); + + return class_entry; +} + +static zend_class_entry *register_class_BackedEnum(zend_class_entry *class_entry_UnitEnum) +{ + zend_class_entry ce, *class_entry; + + INIT_CLASS_ENTRY(ce, "BackedEnum", class_BackedEnum_methods); + class_entry = zend_register_internal_interface(&ce); + zend_class_implements(class_entry, 1, class_entry_UnitEnum); + + return class_entry; +} diff --git a/Zend/zend_inheritance.c b/Zend/zend_inheritance.c index c4e67c1e3e..ba2610e6ab 100644 --- a/Zend/zend_inheritance.c +++ b/Zend/zend_inheritance.c @@ -26,6 +26,7 @@ #include "zend_smart_str.h" #include "zend_operators.h" #include "zend_exceptions.h" +#include "zend_enum.h" ZEND_API zend_class_entry* (*zend_inheritance_cache_get)(zend_class_entry *ce, zend_class_entry *parent, zend_class_entry **traits_and_interfaces) = NULL; ZEND_API zend_class_entry* (*zend_inheritance_cache_add)(zend_class_entry *ce, zend_class_entry *proto, zend_class_entry *parent, zend_class_entry **traits_and_interfaces, HashTable *dependencies) = NULL; @@ -2684,6 +2685,12 @@ ZEND_API zend_class_entry *zend_do_link_class(zend_class_entry *ce, zend_string } } +#ifndef ZEND_WIN32 + if (ce->ce_flags & ZEND_ACC_ENUM) { + /* We will add internal methods. */ + is_cacheable = false; + } +#endif if (ce->ce_flags & ZEND_ACC_IMMUTABLE) { if (is_cacheable) { @@ -2725,6 +2732,12 @@ ZEND_API zend_class_entry *zend_do_link_class(zend_class_entry *ce, zend_string orig_linking_class = CG(current_linking_class); CG(current_linking_class) = is_cacheable ? ce : NULL; + if (ce->ce_flags & ZEND_ACC_ENUM) { + /* Only register builtin enum methods during inheritance to avoid persisting them in + * opcache. */ + zend_enum_register_funcs(ce); + } + if (parent) { if (!(parent->ce_flags & ZEND_ACC_LINKED)) { add_dependency_obligation(ce, parent); @@ -2756,6 +2769,9 @@ ZEND_API zend_class_entry *zend_do_link_class(zend_class_entry *ce, zend_string ) { zend_verify_abstract_class(ce); } + if (ce->ce_flags & ZEND_ACC_ENUM) { + zend_verify_enum(ce); + } zend_build_properties_info_table(ce); diff --git a/Zend/zend_language_parser.y b/Zend/zend_language_parser.y index ed800c46f8..54ec8eab25 100644 --- a/Zend/zend_language_parser.y +++ b/Zend/zend_language_parser.y @@ -162,6 +162,7 @@ static YYSIZE_T zend_yytnamerr(char*, const char*); %token <ident> T_CLASS "'class'" %token <ident> T_TRAIT "'trait'" %token <ident> T_INTERFACE "'interface'" +%token <ident> T_ENUM "'enum'" %token <ident> T_EXTENDS "'extends'" %token <ident> T_IMPLEMENTS "'implements'" %token <ident> T_NAMESPACE "'namespace'" @@ -268,6 +269,7 @@ static YYSIZE_T zend_yytnamerr(char*, const char*); %type <ast> attributed_statement attributed_class_statement attributed_parameter %type <ast> attribute_decl attribute attributes attribute_group namespace_declaration_name %type <ast> match match_arm_list non_empty_match_arm_list match_arm match_arm_cond_list +%type <ast> enum_declaration_statement enum_backing_type enum_case enum_case_expr %type <num> returns_ref function fn is_reference is_variadic variable_modifiers %type <num> method_modifiers non_empty_member_modifiers member_modifier optional_visibility_modifier @@ -291,7 +293,7 @@ reserved_non_modifiers: | T_THROW | T_USE | T_INSTEADOF | T_GLOBAL | T_VAR | T_UNSET | T_ISSET | T_EMPTY | T_CONTINUE | T_GOTO | T_FUNCTION | T_CONST | T_RETURN | T_PRINT | T_YIELD | T_LIST | T_SWITCH | T_ENDSWITCH | T_CASE | T_DEFAULT | T_BREAK | T_ARRAY | T_CALLABLE | T_EXTENDS | T_IMPLEMENTS | T_NAMESPACE | T_TRAIT | T_INTERFACE | T_CLASS - | T_CLASS_C | T_TRAIT_C | T_FUNC_C | T_METHOD_C | T_LINE | T_FILE | T_DIR | T_NS_C | T_FN | T_MATCH + | T_CLASS_C | T_TRAIT_C | T_FUNC_C | T_METHOD_C | T_LINE | T_FILE | T_DIR | T_NS_C | T_FN | T_MATCH | T_ENUM ; semi_reserved: @@ -366,6 +368,7 @@ attributed_statement: | class_declaration_statement { $$ = $1; } | trait_declaration_statement { $$ = $1; } | interface_declaration_statement { $$ = $1; } + | enum_declaration_statement { $$ = $1; } ; top_statement: @@ -592,6 +595,27 @@ interface_declaration_statement: { $$ = zend_ast_create_decl(ZEND_AST_CLASS, ZEND_ACC_INTERFACE, $<num>2, $5, zend_ast_get_str($3), NULL, $4, $7, NULL, NULL); } ; +enum_declaration_statement: + T_ENUM { $<num>$ = CG(zend_lineno); } + T_STRING enum_backing_type implements_list backup_doc_comment '{' class_statement_list '}' + { $$ = zend_ast_create_decl(ZEND_AST_CLASS, ZEND_ACC_ENUM|ZEND_ACC_FINAL, $<num>2, $6, zend_ast_get_str($3), NULL, $5, $8, NULL, $4); } +; + +enum_backing_type: + %empty { $$ = NULL; } + | ':' type_expr { $$ = $2; } +; + +enum_case: + T_CASE identifier enum_case_expr ';' + { $$ = zend_ast_create(ZEND_AST_ENUM_CASE, $2, $3, NULL); } +; + +enum_case_expr: + %empty { $$ = NULL; } + | '=' expr { $$ = $2; } +; + extends_from: %empty { $$ = NULL; } | T_EXTENDS class_name { $$ = $2; } @@ -859,6 +883,7 @@ attributed_class_statement: return_type backup_fn_flags method_body backup_fn_flags { $$ = zend_ast_create_decl(ZEND_AST_METHOD, $3 | $1 | $12, $2, $5, zend_ast_get_str($4), $7, NULL, $11, $9, NULL); CG(extra_fn_flags) = $10; } + | enum_case { $$ = $1; } ; class_statement: diff --git a/Zend/zend_language_scanner.l b/Zend/zend_language_scanner.l index 02714b4e1f..4fb4202264 100644 --- a/Zend/zend_language_scanner.l +++ b/Zend/zend_language_scanner.l @@ -1538,6 +1538,19 @@ NEWLINE ("\r"|"\n"|"\r\n") RETURN_TOKEN_WITH_IDENT(T_TRAIT); } +/* + * The enum keyword must be followed by whitespace and another identifier. + * This avoids the BC break of using enum in classes, namespaces, functions and constants. + */ +<ST_IN_SCRIPTING>"enum"{WHITESPACE}("extends"|"implements") { + yyless(4); + RETURN_TOKEN_WITH_STR(T_STRING, 0); +} +<ST_IN_SCRIPTING>"enum"{WHITESPACE}[a-zA-Z_\x80-\xff] { + yyless(4); + RETURN_TOKEN_WITH_IDENT(T_ENUM); +} + <ST_IN_SCRIPTING>"extends" { RETURN_TOKEN_WITH_IDENT(T_EXTENDS); } diff --git a/Zend/zend_objects_API.c b/Zend/zend_objects_API.c index 104cda6141..b0e9d2f8a8 100644 --- a/Zend/zend_objects_API.c +++ b/Zend/zend_objects_API.c @@ -113,7 +113,10 @@ ZEND_API void ZEND_FASTCALL zend_objects_store_free_object_storage(zend_objects_ if (IS_OBJ_VALID(obj)) { if (!(OBJ_FLAGS(obj) & IS_OBJ_FREE_CALLED)) { GC_ADD_FLAGS(obj, IS_OBJ_FREE_CALLED); - GC_ADDREF(obj); + // FIXME: This causes constant objects to leak + if (!(obj->ce->ce_flags & ZEND_ACC_ENUM)) { + GC_ADDREF(obj); + } obj->handlers->free_obj(obj); } } diff --git a/Zend/zend_opcode.c b/Zend/zend_opcode.c index 4e30b9ff7a..0cced91a84 100644 --- a/Zend/zend_opcode.c +++ b/Zend/zend_opcode.c @@ -356,6 +356,9 @@ ZEND_API void destroy_zend_class(zval *zv) if (ce->attributes) { zend_hash_release(ce->attributes); } + if (ce->backed_enum_table) { + zend_hash_release(ce->backed_enum_table); + } if (ce->num_interfaces > 0 && !(ce->ce_flags & ZEND_ACC_RESOLVED_INTERFACES)) { uint32_t i; diff --git a/Zend/zend_string.h b/Zend/zend_string.h index 88a2fddfb3..230552e3c4 100644 --- a/Zend/zend_string.h +++ b/Zend/zend_string.h @@ -553,6 +553,10 @@ EMPTY_SWITCH_DEFAULT_CASE() _(ZEND_STR_MIXED, "mixed") \ _(ZEND_STR_SLEEP, "__sleep") \ _(ZEND_STR_WAKEUP, "__wakeup") \ + _(ZEND_STR_CASES, "cases") \ + _(ZEND_STR_FROM, "from") \ + _(ZEND_STR_TRYFROM, "tryFrom") \ + _(ZEND_STR_TRYFROM_LOWERCASE, "tryfrom") \ typedef enum _zend_known_string_id { |