Skip to content

Commit

Permalink
Implement values as complement to annotations
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
thekid committed Sep 13, 2020
1 parent f0c0cf0 commit d00ac58
Show file tree
Hide file tree
Showing 4 changed files with 110 additions and 1 deletion.
2 changes: 2 additions & 0 deletions src/main/php/lang/ast/Tokens.class.php
Original file line number Diff line number Diff line change
Expand Up @@ -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];
}
Expand Down
2 changes: 1 addition & 1 deletion src/main/php/lang/ast/nodes/Annotated.class.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
24 changes: 24 additions & 0 deletions src/main/php/lang/ast/syntax/PHP.class.php
Original file line number Diff line number Diff line change
Expand Up @@ -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(':');
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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();
}
Expand Down Expand Up @@ -992,6 +1007,7 @@ public function __construct() {
$line
);
$body[$lookup]->holder= $holder;
$body[$lookup]->values= $meta[DETAIL_VALUES] ?? null;
});
}

Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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');
Expand Down
83 changes: 83 additions & 0 deletions src/test/php/lang/ast/unittest/parse/ValuesTest.class.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
<?php namespace lang\ast\unittest\parse;

use lang\ast\nodes\{Annotated, Literal, LambdaExpression};
use unittest\Assert;

/** @see https://github.com/xp-framework/rfc/issues/336 */
class ValuesTest extends ParseTest {

/**
* Parses source into type
*
* @param string $source
* @return lang.ast.TypeDeclaration
*/
private function type($source) {
return $this->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']);
}
}

0 comments on commit d00ac58

Please sign in to comment.