From d00ac5803e529dc4deb7784522ab01a17c217221 Mon Sep 17 00:00:00 2001 From: Timm Friebe Date: Sun, 13 Sep 2020 18:33:31 +0200 Subject: [PATCH] Implement values as complement to annotations See xp-framework/rfc#336, values are key/value pairs that can be attached to types and type members. They serve as a complement to PHP 8 attributes which do not support arbitrary expressions in their argument lists. --- src/main/php/lang/ast/Tokens.class.php | 2 + .../php/lang/ast/nodes/Annotated.class.php | 2 +- src/main/php/lang/ast/syntax/PHP.class.php | 24 ++++++ .../ast/unittest/parse/ValuesTest.class.php | 83 +++++++++++++++++++ 4 files changed, 110 insertions(+), 1 deletion(-) create mode 100755 src/test/php/lang/ast/unittest/parse/ValuesTest.class.php diff --git a/src/main/php/lang/ast/Tokens.class.php b/src/main/php/lang/ast/Tokens.class.php index ea3f2ee..a6a0fc3 100755 --- a/src/main/php/lang/ast/Tokens.class.php +++ b/src/main/php/lang/ast/Tokens.class.php @@ -137,6 +137,8 @@ public function getIterator() { $offset-= strlen($t); yield 'operator' => ['#[', $line]; } + } else if ('$' === $t) { + yield 'operator' => ['#$', $line]; } else { yield 'comment' => ['#'.$t.$next("\r\n"), $line]; } diff --git a/src/main/php/lang/ast/nodes/Annotated.class.php b/src/main/php/lang/ast/nodes/Annotated.class.php index d4c8549..da68eb2 100755 --- a/src/main/php/lang/ast/nodes/Annotated.class.php +++ b/src/main/php/lang/ast/nodes/Annotated.class.php @@ -3,7 +3,7 @@ use lang\ast\Node; abstract class Annotated extends Node { - public $annotations; + public $annotations, $values; /** * Returns an annotation for a given name, or NULL if no annotation diff --git a/src/main/php/lang/ast/syntax/PHP.class.php b/src/main/php/lang/ast/syntax/PHP.class.php index 10b3f68..b501ea8 100755 --- a/src/main/php/lang/ast/syntax/PHP.class.php +++ b/src/main/php/lang/ast/syntax/PHP.class.php @@ -66,6 +66,10 @@ class PHP extends Language { private $body= []; + static function __static() { + define('DETAIL_VALUES', 0x100); + } + /** Setup language parser */ public function __construct() { $this->symbol(':'); @@ -808,6 +812,16 @@ public function __construct() { return $type; }); + $this->stmt('#$', function($parse, $token) { + $name= $parse->token->value; + $parse->forward(); + $parse->expecting(':', 'value'); + $value= $this->expression($parse, 0); + $type= $this->statement($parse); + $type->values[$name]= $value; + return $type; + }); + $this->stmt('class', function($parse, $token) { $type= $parse->scope->resolve($parse->token->value); $parse->forward(); @@ -944,6 +958,7 @@ public function __construct() { $line ); $body[$name]->holder= $holder; + $body[$name]->values= $meta[DETAIL_VALUES] ?? null; if (',' === $parse->token->value) { $parse->forward(); } @@ -992,6 +1007,7 @@ public function __construct() { $line ); $body[$lookup]->holder= $holder; + $body[$lookup]->values= $meta[DETAIL_VALUES] ?? null; }); } @@ -1114,6 +1130,7 @@ private function properties($parse, &$body, $meta, $modifiers, $type, $holder) { } $body[$lookup]= new Property($modifiers, $name, $type, $expr, $annotations, $comment, $line); $body[$lookup]->holder= $holder; + $body[$lookup]->values= $meta[DETAIL_VALUES] ?? null; if (',' === $parse->token->value) { $parse->forward(); @@ -1353,6 +1370,13 @@ public function typeBody($parse, $holder) { } else if ('#[' === $parse->token->value) { $parse->forward(); $meta= [DETAIL_ANNOTATIONS => $this->attributes($parse, 'member attributes')]; + } else if ('#$' === $parse->token->value) { + $parse->forward(); + $name= $parse->token->value; + $parse->forward(); + $parse->expecting(':', 'value'); + $value= $this->expression($parse, 0); + $meta[DETAIL_VALUES][$name]= $value; } else if ('#[@' === $parse->token->value) { $parse->forward(); $meta= $this->meta($parse, 'member annotations'); diff --git a/src/test/php/lang/ast/unittest/parse/ValuesTest.class.php b/src/test/php/lang/ast/unittest/parse/ValuesTest.class.php new file mode 100755 index 0000000..fc3fc43 --- /dev/null +++ b/src/test/php/lang/ast/unittest/parse/ValuesTest.class.php @@ -0,0 +1,83 @@ +parse($source)->tree()->type('T'); + } + + /** + * Assertion helper + * + * @param var $expected + * @param lang.ast.Node $node + * @throws unittest.AssertionFailedError + * @return void + */ + private function assertValues($expected, $node) { + Assert::equals($expected, cast($node, Annotated::class)->values); + } + + #[@test] + public function on_class() { + $this->assertValues( + ['using' => new Literal('"value"', self::LINE)], + $this->type('#$using: "value" class T { }') + ); + } + + #[@test] + public function on_constant() { + $this->assertValues( + ['using' => new Literal('"value"', self::LINE)], + $this->type('class T { #$using: "value" const FIXTURE = "test"; }')->constant('FIXTURE') + ); + } + + #[@test] + public function on_property() { + $this->assertValues( + ['using' => new Literal('"value"', self::LINE)], + $this->type('class T { #$using: "value" public $fixture; }')->property('fixture') + ); + } + + #[@test] + public function on_method() { + $this->assertValues( + ['using' => new Literal('"value"', self::LINE)], + $this->type('class T { #$using: "value" public function fixture() { } }')->method('fixture') + ); + } + + #[@test] + public function values_on_multiple_lines() { + $this->assertValues( + ['author' => new Literal('"test"', self::LINE + 1), 'version' => new Literal('1', self::LINE + 2)], + $this->type(' + #$author: "test" + #$version: 1 + class T { } + ') + ); + } + + #[@test] + public function with_function() { + $type= $this->type(' + #$using: fn() => version_compare(PHP_VERSION, "7.0.0", ">=") + class T { } + '); + Assert::instance(LambdaExpression::class, cast($type, Annotated::class)->values['using']); + } +} \ No newline at end of file