diff --git a/ChangeLog.md b/ChangeLog.md index 500c4f6..d9ac4bb 100755 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -3,6 +3,12 @@ XP AST ChangeLog ## ?.?.? / ????-??-?? +## 7.1.0 / ????-??-?? + +* Merged PR #23: Add syntactic support for PHP 8.1 enums. Implementation + in the compiler is in pull request xp-framework/compiler#106 + (@thekid) + ## 7.0.4 / 2021-03-07 * Fixed *Call to undefined method ::emitoperator()* caused by standalone diff --git a/src/main/php/lang/ast/nodes/EnumCase.class.php b/src/main/php/lang/ast/nodes/EnumCase.class.php new file mode 100755 index 0000000..57d3a4a --- /dev/null +++ b/src/main/php/lang/ast/nodes/EnumCase.class.php @@ -0,0 +1,26 @@ +name= $name; + $this->expression= $expression; + $this->annotations= $annotations; + $this->line= $line; + } + + /** @return string */ + public function lookup() { return $this->name; } + + /** + * Checks whether this node is of a given kind + * + * @param string $kind + * @return bool + */ + public function is($kind) { + return $this->kind === $kind || '@member' === $kind || parent::is($kind); + } +} \ No newline at end of file diff --git a/src/main/php/lang/ast/nodes/EnumDeclaration.class.php b/src/main/php/lang/ast/nodes/EnumDeclaration.class.php new file mode 100755 index 0000000..1e7402d --- /dev/null +++ b/src/main/php/lang/ast/nodes/EnumDeclaration.class.php @@ -0,0 +1,18 @@ +implements= $implements; + $this->base= $base; + } + + public function interfaces() { return $this->implements; } + + public function case($name) { return $this->body[$name] ?? null; } +} \ No newline at end of file diff --git a/src/main/php/lang/ast/syntax/PHP.class.php b/src/main/php/lang/ast/syntax/PHP.class.php index b5b3809..24a465d 100755 --- a/src/main/php/lang/ast/syntax/PHP.class.php +++ b/src/main/php/lang/ast/syntax/PHP.class.php @@ -14,6 +14,8 @@ ContinueStatement, DoLoop, EchoStatement, + EnumCase, + EnumDeclaration, ForLoop, ForeachLoop, FunctionDeclaration, @@ -860,7 +862,74 @@ public function __construct() { return $decl; }); - $this->body('use', function($parse, &$body, $annotations, $modifiers, $holder) { + $this->stmt('enum', function($parse, $token) { + $name= $parse->scope->resolve($parse->token->value); + $parse->forward(); + $comment= $parse->comment; + $parse->comment= null; + + $implements= []; + if ('implements' === $parse->token->value) { + $parse->forward(); + do { + $implements[]= $parse->scope->resolve($parse->token->value); + $parse->forward(); + if (',' === $parse->token->value) { + $parse->forward(); + continue; + } else if ('{' === $parse->token->value) { + break; + } else { + $parse->expecting(', or {', 'interfaces list'); + } + } while (null !== $parse->token->value); + } + + // Backed enums vs. unit enums + if (':' === $parse->token->value) { + $parse->forward(); + $base= $parse->token->value; + $parse->forward(); + } else { + $base= null; + } + + $decl= new EnumDeclaration([], $name, $base, $implements, [], [], $comment, $token->line); + $parse->expecting('{', 'enum'); + $decl->body= $this->typeBody($parse, $decl->name); + $parse->expecting('}', 'enum'); + + return $decl; + }); + + $this->body('case', function($parse, &$body, $meta, $modifiers, $holder) { + $parse->forward(); + do { + $line= $parse->token->line; + $name= $parse->token->value; + + $parse->forward(); + if ('=' === $parse->token->value) { + $parse->forward(); + $expr= $this->expression($parse, 0); + } else { + $expr= null; + } + + $body[$name]= new EnumCase($name, $expr, $meta[DETAIL_ANNOTATIONS] ?? [], $line); + $body[$name]->holder= $holder; + + if (',' === $parse->token->value) { + $parse->forward(); + continue; + } else { + $parse->expecting(';', 'case'); + break; + } + } while ($parse->token->value); + }); + + $this->body('use', function($parse, &$body, $meta, $modifiers, $holder) { $line= $parse->token->line; $parse->forward(); diff --git a/src/test/php/lang/ast/unittest/parse/AttributesTest.class.php b/src/test/php/lang/ast/unittest/parse/AttributesTest.class.php index 315965a..16ca13d 100755 --- a/src/test/php/lang/ast/unittest/parse/AttributesTest.class.php +++ b/src/test/php/lang/ast/unittest/parse/AttributesTest.class.php @@ -199,6 +199,11 @@ public function on_interface($attributes, $expected) { $this->assertAnnotated($expected, $this->type($attributes.' interface T { }')); } + #[Test, Values('attributes')] + public function on_enum($attributes, $expected) { + $this->assertAnnotated($expected, $this->type($attributes.' enum T { }')); + } + #[Test, Values('attributes')] public function on_constant($attributes, $expected) { $type= $this->type('class T { '.$attributes.' const FIXTURE = 1; }'); @@ -222,4 +227,10 @@ public function on_parameter($attributes, $expected) { $type= $this->type('class T { public function fixture('.$attributes.' $p) { } }'); $this->assertAnnotated($expected, $type->method('fixture')->signature->parameters[0]); } + + #[Test, Values('attributes')] + public function on_enum_case($attributes, $expected) { + $type= $this->type('enum T { '.$attributes.' case ONE; }'); + $this->assertAnnotated($expected, $type->case('ONE')); + } } \ No newline at end of file diff --git a/src/test/php/lang/ast/unittest/parse/TypesTest.class.php b/src/test/php/lang/ast/unittest/parse/TypesTest.class.php index 185000f..3e5dba9 100755 --- a/src/test/php/lang/ast/unittest/parse/TypesTest.class.php +++ b/src/test/php/lang/ast/unittest/parse/TypesTest.class.php @@ -1,7 +1,16 @@ assertParsed( + [new EnumDeclaration([], '\\A', null, [], [], [], null, self::LINE)], + 'enum A { }' + ); + } + + #[Test] + public function empty_backed_enum() { + $this->assertParsed( + [new EnumDeclaration([], '\\A', 'string', [], [], [], null, self::LINE)], + 'enum A: string { }' + ); + } + + #[Test] + public function unit_enum_with_cases() { + $enum= new EnumDeclaration([], '\\A', null, [], [], [], null, self::LINE); + $enum->declare(new EnumCase('ONE', null, [], self::LINE)); + $enum->declare(new EnumCase('TWO', null, [], self::LINE)); + $this->assertParsed([$enum], 'enum A { case ONE; case TWO; }'); + } + + #[Test] + public function backed_enum_with_cases() { + $enum= new EnumDeclaration([], '\\A', 'int', [], [], [], null, self::LINE); + $enum->declare(new EnumCase('ONE', new Literal('1', self::LINE), [], self::LINE)); + $enum->declare(new EnumCase('TWO', new Literal('2', self::LINE), [], self::LINE)); + $this->assertParsed([$enum], 'enum A: int { case ONE = 1; case TWO = 2; }'); + } + + #[Test] + public function unit_enum_with_grouped_cases() { + $enum= new EnumDeclaration([], '\\A', null, [], [], [], null, self::LINE); + $enum->declare(new EnumCase('ONE', null, [], self::LINE)); + $enum->declare(new EnumCase('TWO', null, [], self::LINE)); + $this->assertParsed([$enum], 'enum A { case ONE, TWO; }'); + } + #[Test] public function class_with_trait() { $class= new ClassDeclaration([], '\\A', null, [], [], [], null, self::LINE);