Skip to content

Commit 1c45061

Browse files
authored
Merge pull request #169 from xp-framework/refactor/annotation-emit
Refactor how annotations with non-constant arguments are emitted
2 parents 3a51fe6 + ef3a171 commit 1c45061

File tree

4 files changed

+137
-68
lines changed

4 files changed

+137
-68
lines changed

composer.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,12 @@
66
"description" : "XP Compiler",
77
"keywords": ["module", "xp"],
88
"require" : {
9-
"xp-framework/core": "^11.0 | ^10.0",
9+
"xp-framework/core": "^11.6 | ^10.16",
1010
"xp-framework/ast": "^10.1",
1111
"php" : ">=7.0.0"
1212
},
1313
"require-dev" : {
14-
"xp-framework/reflection": "^2.11",
14+
"xp-framework/reflection": "^2.13",
1515
"xp-framework/test": "^1.5"
1616
},
1717
"bin": ["bin/xp.xp-framework.compiler.compile", "bin/xp.xp-framework.compiler.ast"],

src/main/php/lang/ast/emit/AttributesAsComments.class.php

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,27 @@ protected function emitAnnotation($result, $annotation) {
1616
$result->out->write('\\'.$annotation->name);
1717
if (empty($annotation->arguments)) return;
1818

19+
// Check whether arguments are constant, enclose in `eval` array
20+
// otherwise. This is not strictly necessary but will ensure
21+
// forward compatibility with PHP 8
22+
foreach ($annotation->arguments as $argument) {
23+
if ($this->isConstant($result, $argument)) continue;
24+
25+
$escaping= new Escaping($result->out, ["'" => "\\'", '\\' => '\\\\']);
26+
$result->out->write('(eval: [');
27+
foreach ($annotation->arguments as $name => $argument) {
28+
is_string($name) && $result->out->write("'{$name}'=>");
29+
30+
$result->out->write("'");
31+
$result->out= $escaping;
32+
$this->emitOne($result, $argument);
33+
$result->out= $escaping->original();
34+
$result->out->write("',");
35+
}
36+
$result->out->write('])');
37+
return;
38+
}
39+
1940
// We can use named arguments here as PHP 8 attributes are parsed
2041
// by the XP reflection API when using PHP 7. However, we may not
2142
// emit trailing commas here!

src/main/php/lang/ast/emit/PHP.class.php

Lines changed: 11 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -495,24 +495,18 @@ protected function emitAnnotation($result, $annotation) {
495495
if ($this->isConstant($result, $argument)) continue;
496496

497497
// Found first non-constant argument, enclose in `eval`
498-
$result->out->write('(eval: \'');
499-
$result->out= new Escaping($result->out, ["'" => "\\'", '\\' => '\\\\']);
500-
501-
// If exactly one unnamed argument exists, emit its value directly
502-
if (1 === sizeof($annotation->arguments) && 0 === key($annotation->arguments)) {
503-
$this->emitOne($result, current($annotation->arguments));
504-
} else {
505-
$result->out->write('[');
506-
foreach ($annotation->arguments as $key => $argument) {
507-
$result->out->write("'{$key}'=>");
508-
$this->emitOne($result, $argument);
509-
$result->out->write(',');
510-
}
511-
$result->out->write(']');
498+
$escaping= new Escaping($result->out, ["'" => "\\'", '\\' => '\\\\']);
499+
$result->out->write('(eval: [');
500+
foreach ($annotation->arguments as $name => $argument) {
501+
is_string($name) && $result->out->write("'{$name}'=>");
502+
503+
$result->out->write("'");
504+
$result->out= $escaping;
505+
$this->emitOne($result, $argument);
506+
$result->out= $escaping->original();
507+
$result->out->write("',");
512508
}
513-
514-
$result->out= $result->out->original();
515-
$result->out->write('\')');
509+
$result->out->write('])');
516510
return;
517511
}
518512

Lines changed: 103 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<?php namespace lang\ast\unittest\emit;
22

3-
use lang\IllegalArgumentException;
3+
use lang\{Reflection, IllegalArgumentException};
44
use test\{Assert, Expect, Test, Values};
55

66
/**
@@ -11,138 +11,192 @@
1111
*/
1212
abstract class AnnotationSupport extends EmittingTest {
1313

14+
/**
15+
* Declares annotations, optionally including a type
16+
*
17+
* @param string $declaration
18+
* @return lang.reflection.Type
19+
*/
20+
private function declare($declaration) {
21+
return Reflection::type($this->type(
22+
$declaration.(strstr($declaration, '<T>') ? '' : ' class <T> { }')
23+
));
24+
}
25+
26+
/**
27+
* Returns annotations present in the given type
28+
*
29+
* @param lang.reflection.Annotated $annotated
30+
* @return [:var[]]
31+
*/
32+
private function annotations($annotated) {
33+
$r= [];
34+
foreach ($annotated->annotations() as $name => $annotation) {
35+
$r[$name]= $annotation->arguments();
36+
}
37+
return $r;
38+
}
39+
1440
#[Test]
1541
public function without_value() {
16-
$t= $this->type('#[Test] class <T> { }');
17-
Assert::equals(['test' => null], $t->getAnnotations());
42+
Assert::equals(
43+
['Test' => []],
44+
$this->annotations($this->declare('#[Test]'))
45+
);
1846
}
1947

2048
#[Test]
2149
public function within_namespace() {
22-
$t= $this->type('namespace tests; #[Test] class <T> { }');
23-
Assert::equals(['test' => null], $t->getAnnotations());
50+
Assert::equals(
51+
['tests\\Test' => []],
52+
$this->annotations($this->declare('namespace tests; #[Test]'))
53+
);
2454
}
2555

2656
#[Test]
2757
public function resolved_against_import() {
28-
$t= $this->type('use unittest\Test; #[Test] class <T> { }');
29-
Assert::equals(['test' => null], $t->getAnnotations());
58+
Assert::equals(
59+
['unittest\\Test' => []],
60+
$this->annotations($this->declare('use unittest\Test; #[Test]'))
61+
);
3062
}
3163

3264
#[Test]
3365
public function primitive_value() {
34-
$t= $this->type('#[Author("Timm")] class <T> { }');
35-
Assert::equals(['author' => 'Timm'], $t->getAnnotations());
66+
Assert::equals(
67+
['Author' => ['Timm']],
68+
$this->annotations($this->declare('#[Author("Timm")]'))
69+
);
3670
}
3771

3872
#[Test]
3973
public function array_value() {
40-
$t= $this->type('#[Authors(["Timm", "Alex"])] class <T> { }');
41-
Assert::equals(['authors' => ['Timm', 'Alex']], $t->getAnnotations());
74+
Assert::equals(
75+
['Authors' => [['Timm', 'Alex']]],
76+
$this->annotations($this->declare('#[Authors(["Timm", "Alex"])]'))
77+
);
4278
}
4379

4480
#[Test]
4581
public function map_value() {
46-
$t= $this->type('#[Expect(["class" => \lang\IllegalArgumentException::class])] class <T> { }');
47-
Assert::equals(['expect' => ['class' => IllegalArgumentException::class]], $t->getAnnotations());
82+
Assert::equals(
83+
['Expect' => [['class' => IllegalArgumentException::class]]],
84+
$this->annotations($this->declare('#[Expect(["class" => \lang\IllegalArgumentException::class])]'))
85+
);
4886
}
4987

5088
#[Test]
5189
public function named_argument() {
52-
$t= $this->type('#[Expect(class: \lang\IllegalArgumentException::class)] class <T> { }');
53-
Assert::equals(['expect' => ['class' => IllegalArgumentException::class]], $t->getAnnotations());
90+
Assert::equals(
91+
['Expect' => ['class' => IllegalArgumentException::class]],
92+
$this->annotations($this->declare('#[Expect(class: \lang\IllegalArgumentException::class)]'))
93+
);
5494
}
5595

5696
#[Test]
5797
public function closure_value() {
58-
$t= $this->type('#[Verify(function($arg) { return $arg; })] class <T> { }');
59-
$f= $t->getAnnotation('verify');
60-
Assert::equals('test', $f('test'));
98+
$verify= $this->annotations($this->declare('#[Verify(function($arg) { return $arg; })]'))['Verify'];
99+
Assert::equals('test', $verify[0]('test'));
61100
}
62101

63102
#[Test]
64103
public function arrow_function_value() {
65-
$t= $this->type('#[Verify(fn($arg) => $arg)] class <T> { }');
66-
$f= $t->getAnnotation('verify');
67-
Assert::equals('test', $f('test'));
104+
$verify= $this->annotations($this->declare('#[Verify(fn($arg) => $arg)]'))['Verify'];
105+
Assert::equals('test', $verify[0]('test'));
68106
}
69107

70108
#[Test]
71109
public function array_of_arrow_function_value() {
72-
$t= $this->type('#[Verify([fn($arg) => $arg])] class <T> { }');
73-
$f= $t->getAnnotation('verify');
74-
Assert::equals('test', $f[0]('test'));
110+
$verify= $this->annotations($this->declare('#[Verify([fn($arg) => $arg])]'))['Verify'];
111+
Assert::equals('test', $verify[0][0]('test'));
75112
}
76113

77114
#[Test]
78115
public function named_arrow_function_value() {
79-
$t= $this->type('#[Verify(func: fn($arg) => $arg)] class <T> { }');
80-
$f= $t->getAnnotation('verify');
81-
Assert::equals('test', $f['func']('test'));
116+
$verify= $this->annotations($this->declare('#[Verify(func: fn($arg) => $arg)]'))['Verify'];
117+
Assert::equals('test', $verify['func']('test'));
82118
}
83119

84120
#[Test]
85121
public function single_quoted_string_inside_non_constant_expression() {
86-
$t= $this->type('#[Verify(fn($arg) => \'php\\\\\'.$arg)] class <T> { }');
87-
$f= $t->getAnnotation('verify');
88-
Assert::equals('php\\test', $f('test'));
122+
$verify= $this->annotations($this->declare('#[Verify(fn($arg) => \'php\\\\\'.$arg)]'))['Verify'];
123+
Assert::equals('php\\test', $verify[0]('test'));
89124
}
90125

91126
#[Test]
92127
public function has_access_to_class() {
93-
$t= $this->type('#[Expect(self::SUCCESS)] class <T> { const SUCCESS = true; }');
94-
Assert::equals(['expect' => true], $t->getAnnotations());
128+
Assert::equals(
129+
['Expect' => [true]],
130+
$this->annotations($this->declare('#[Expect(self::SUCCESS)] class <T> { const SUCCESS = true; }'))
131+
);
95132
}
96133

97134
#[Test]
98135
public function method() {
99-
$t= $this->type('class <T> { #[Test] public function fixture() { } }');
100-
Assert::equals(['test' => null], $t->getMethod('fixture')->getAnnotations());
136+
$t= $this->declare('class <T> { #[Test] public function fixture() { } }');
137+
Assert::equals(
138+
['Test' => []],
139+
$this->annotations($t->method('fixture'))
140+
);
101141
}
102142

103143
#[Test]
104144
public function field() {
105-
$t= $this->type('class <T> { #[Test] public $fixture; }');
106-
Assert::equals(['test' => null], $t->getField('fixture')->getAnnotations());
145+
$t= $this->declare('class <T> { #[Test] public $fixture; }');
146+
Assert::equals(
147+
['Test' => []],
148+
$this->annotations($t->property('fixture'))
149+
);
107150
}
108151

109152
#[Test]
110153
public function param() {
111-
$t= $this->type('class <T> { public function fixture(#[Test] $param) { } }');
112-
Assert::equals(['test' => null], $t->getMethod('fixture')->getParameter(0)->getAnnotations());
154+
$t= $this->declare('class <T> { public function fixture(#[Test] $param) { } }');
155+
Assert::equals(
156+
['Test' => []],
157+
$this->annotations($t->method('fixture')->parameter(0))
158+
);
113159
}
114160

115161
#[Test]
116162
public function params() {
117-
$t= $this->type('class <T> { public function fixture(#[Inject(["name" => "a"])] $a, #[Inject] $b) { } }');
118-
$m= $t->getMethod('fixture');
163+
$t= $this->declare('class <T> { public function fixture(#[Inject(["name" => "a"])] $a, #[Inject] $b) { } }');
119164
Assert::equals(
120-
[['inject' => ['name' => 'a']], ['inject' => null]],
121-
[$m->getParameter(0)->getAnnotations(), $m->getParameter(1)->getAnnotations()]
165+
['Inject' => [['name' => 'a']]],
166+
$this->annotations($t->method('fixture')->parameter(0))
167+
);
168+
Assert::equals(
169+
['Inject' => []],
170+
$this->annotations($t->method('fixture')->parameter(1))
122171
);
123172
}
124173

125174
#[Test]
126175
public function multiple_class_annotations() {
127-
$t= $this->type('#[Resource("/"), Authenticated] class <T> { }');
128-
Assert::equals(['resource' => '/', 'authenticated' => null], $t->getAnnotations());
176+
Assert::equals(
177+
['Resource' => ['/'], 'Authenticated' => []],
178+
$this->annotations($this->declare('#[Resource("/"), Authenticated]'))
179+
);
129180
}
130181

131182
#[Test]
132183
public function multiple_member_annotations() {
133-
$t= $this->type('class <T> { #[Test, Values([1, 2, 3])] public function fixture() { } }');
134-
Assert::equals(['test' => null, 'values' => [1, 2, 3]], $t->getMethod('fixture')->getAnnotations());
184+
$t= $this->declare('class <T> { #[Test, Values([1, 2, 3])] public function fixture() { } }');
185+
Assert::equals(
186+
['Test' => [], 'Values' => [[1, 2, 3]]],
187+
$this->annotations($t->method('fixture'))
188+
);
135189
}
136190

137191
#[Test]
138192
public function multiline_annotations() {
139-
$t= $this->type('
193+
$annotations= $this->annotations($this->declare('
140194
#[Authors([
141195
"Timm",
142196
"Mr. Midori",
143197
])]
144198
class <T> { }'
145-
);
146-
Assert::equals(['authors' => ['Timm', 'Mr. Midori']], $t->getAnnotations());
199+
));
200+
Assert::equals(['Authors' => [['Timm', 'Mr. Midori']]], $annotations);
147201
}
148202
}

0 commit comments

Comments
 (0)