Skip to content

Commit 6fa36e8

Browse files
committed
Allow union type predicted type access
1 parent 8531626 commit 6fa36e8

File tree

2 files changed

+292
-80
lines changed

2 files changed

+292
-80
lines changed

src/Node/Expression/GetAttrExpression.php

Lines changed: 270 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,13 @@
1313
namespace Twig\Node\Expression;
1414

1515
use Twig\Compiler;
16+
use Twig\Environment;
1617
use Twig\Extension\SandboxExtension;
1718
use Twig\Template;
1819
use Twig\TypeHint\ArrayType;
1920
use Twig\TypeHint\ObjectType;
21+
use Twig\TypeHint\TypeInterface;
22+
use Twig\TypeHint\UnionType;
2023

2124
class GetAttrExpression extends AbstractExpression
2225
{
@@ -41,63 +44,23 @@ public function compile(Compiler $compiler): void
4144
$type = $this->getNode('node')->getAttribute('typeHint');
4245
}
4346

44-
if ($type instanceof ArrayType) {
45-
$compiler
46-
->raw('((')
47-
->subcompile($this->getNode('node'))
48-
->raw(')[')
49-
->subcompile($this->getNode('attribute'))
50-
->raw('] ?? null)')
51-
;
47+
if ($type instanceof TypeInterface) {
48+
$sourceCompiler = $this->createNodeSourceCompiler();
49+
$accessCompiler = $this->createAccessCompiler($type, $env);
5250

53-
return;
54-
} else if ($type instanceof ObjectType && $this->getNode('attribute') instanceof ConstantExpression) {
55-
$attributeName = $this->getNode('attribute')->getAttribute('value');
56-
57-
if ($type->getPropertyType($attributeName) !== null) {
58-
$compiler
59-
->raw('((')
60-
->subcompile($this->getNode('node'))
61-
->raw(')?->')
62-
->raw($attributeName)
63-
->raw(')')
64-
;
65-
66-
return;
51+
if (true || $accessCompiler['condition'] === null) {
52+
$accessCompiler['accessor']($compiler, $sourceCompiler);
53+
} else {
54+
$compiler->raw('(');
55+
$accessCompiler['condition']($compiler, $sourceCompiler);
56+
$compiler->raw(' ? ');
57+
$accessCompiler['accessor']($compiler, $sourceCompiler);
58+
$compiler->raw(' : ');
59+
$this->createGuessingAccessCompiler($env->hasExtension(SandboxExtension::class))['accessor']($compiler, $sourceCompiler);
60+
$compiler->raw(')');
6761
}
6862

69-
/** Keep similar to @see \Twig\TypeHint\ObjectType::getAttributeType */
70-
$methodNames = [
71-
$attributeName,
72-
'get' . $attributeName,
73-
'is' . $attributeName,
74-
'has' . $attributeName,
75-
];
76-
77-
foreach ($methodNames as $methodName) {
78-
if ($type->getMethodType($methodName) !== null) {
79-
$compiler
80-
->raw('((')
81-
->subcompile($this->getNode('node'))
82-
->raw(')?->')
83-
->raw($methodName)
84-
->raw('(')
85-
;
86-
87-
if ($this->hasNode('arguments') && $this->getNode('arguments') instanceof ArrayExpression && $this->getNode('arguments')->count() > 0) {
88-
for ($argIndex = 0; $argIndex < $this->getNode('arguments')->count(); $argIndex += 2) {
89-
if ($argIndex > 0) {
90-
$compiler->raw(', ');
91-
}
92-
93-
$compiler->subcompile($this->getNode('arguments')->getNode($argIndex + 1));
94-
}
95-
}
96-
97-
$compiler->raw('))');
98-
return;
99-
}
100-
}
63+
return;
10164
}
10265
}
10366

@@ -126,31 +89,264 @@ public function compile(Compiler $compiler): void
12689
return;
12790
}
12891

129-
$compiler->raw('twig_get_attribute($this->env, $this->source, ');
92+
$this->createGuessingAccessCompiler($env->hasExtension(SandboxExtension::class))['accessor']($compiler, $this->createNodeSourceCompiler());
93+
}
94+
95+
/**
96+
* @return array{
97+
* condition: \Closure(Compiler, \Closure(Compiler): void): void|null,
98+
* accessor: \Closure(Compiler, \Closure(Compiler): void): void
99+
* }
100+
*/
101+
private function createAccessCompiler(TypeInterface $type, Environment $env): array
102+
{
103+
if ($type instanceof UnionType) {
104+
return $this->createUnionAccessCompiler($type, $env);
105+
}
106+
107+
if ($type instanceof ArrayType) {
108+
return $this->createArrayAccessCompiler();
109+
}
110+
111+
if ($type instanceof ObjectType && $this->getNode('attribute') instanceof ConstantExpression) {
112+
$attributeName = $this->getNode('attribute')->getAttribute('value');
113+
114+
if ($type->getPropertyType($attributeName) !== null) {
115+
return $this->createObjectPropertyAccessCompiler($type, $attributeName);
116+
}
117+
118+
/** Keep similar to @see \Twig\TypeHint\ObjectType::getAttributeType */
119+
$methodNames = [
120+
$attributeName,
121+
'get' . $attributeName,
122+
'is' . $attributeName,
123+
'has' . $attributeName,
124+
];
125+
126+
foreach ($methodNames as $methodName) {
127+
if ($type->getMethodType($methodName) === null) {
128+
continue;
129+
}
130130

131-
if ($this->getAttribute('ignore_strict_check')) {
132-
$this->getNode('node')->setAttribute('ignore_strict_check', true);
131+
return $this->createObjectMethodAccessCompiler($type, $methodName);
132+
}
133133
}
134134

135-
$compiler
136-
->subcompile($this->getNode('node'))
137-
->raw(', ')
138-
->subcompile($this->getNode('attribute'))
139-
;
135+
return $this->createGuessingAccessCompiler($env->hasExtension(SandboxExtension::class));
136+
}
137+
138+
/**
139+
* @return array{
140+
* condition: null,
141+
* accessor: \Closure(Compiler, \Closure(Compiler): void): void
142+
* }
143+
*/
144+
private function createGuessingAccessCompiler(bool $isSandboxed): array
145+
{
146+
return [
147+
'condition' => null,
148+
'accessor' => function (Compiler $compiler, \Closure $sourceCompiler) use ($isSandboxed): void {
149+
$compiler->raw('twig_get_attribute($this->env, $this->source, ');
150+
151+
if ($this->getAttribute('ignore_strict_check')) {
152+
$this->getNode('node')->setAttribute('ignore_strict_check', true);
153+
}
154+
155+
$sourceCompiler($compiler);
156+
157+
$compiler
158+
->raw(', ')
159+
->subcompile($this->getNode('attribute'))
160+
;
161+
162+
if ($this->hasNode('arguments')) {
163+
$compiler->raw(', ')->subcompile($this->getNode('arguments'));
164+
} else {
165+
$compiler->raw(', []');
166+
}
167+
168+
$compiler->raw(', ')
169+
->repr($this->getAttribute('type'))
170+
->raw(', ')->repr($this->getAttribute('is_defined_test'))
171+
->raw(', ')->repr($this->getAttribute('ignore_strict_check'))
172+
->raw(', ')->repr($isSandboxed)
173+
->raw(', ')->repr($this->getNode('node')->getTemplateLine())
174+
->raw(')')
175+
;
176+
},
177+
];
178+
}
179+
180+
/**
181+
* @return array{
182+
* condition: \Closure(Compiler, \Closure(Compiler): void): void,
183+
* accessor: \Closure(Compiler, \Closure(Compiler): void): void
184+
* }
185+
*/
186+
private function createObjectMethodAccessCompiler(ObjectType $type, string $attributeName): array
187+
{
188+
return [
189+
'condition' => function (Compiler $compiler, \Closure $sourceCompiler) use ($type): void {
190+
$sourceCompiler($compiler);
191+
$compiler->raw(' instanceof \\')->raw($type->getType());
192+
},
193+
'accessor' => function (Compiler $compiler, \Closure $sourceCompiler) use ($attributeName): void {
194+
$compiler->raw('(');
195+
196+
$sourceCompiler($compiler);
140197

141-
if ($this->hasNode('arguments')) {
142-
$compiler->raw(', ')->subcompile($this->getNode('arguments'));
143-
} else {
144-
$compiler->raw(', []');
198+
$compiler->raw('?->')->raw($attributeName)->raw('(');
199+
200+
if ($this->hasNode('arguments') && $this->getNode('arguments') instanceof ArrayExpression && $this->getNode('arguments')->count() > 0) {
201+
for ($argIndex = 0; $argIndex < $this->getNode('arguments')->count(); $argIndex += 2) {
202+
if ($argIndex > 0) {
203+
$compiler->raw(', ');
204+
}
205+
206+
$compiler->subcompile($this->getNode('arguments')->getNode($argIndex + 1));
207+
}
208+
}
209+
210+
$compiler->raw('))');
211+
},
212+
];
213+
}
214+
215+
/**
216+
* @return array{
217+
* condition: \Closure(Compiler, \Closure(Compiler): void): void,
218+
* accessor: \Closure(Compiler, \Closure(Compiler): void): void
219+
* }
220+
*/
221+
private function createObjectPropertyAccessCompiler(ObjectType $type, string $attributeName): array
222+
{
223+
return [
224+
'condition' => function (Compiler $compiler, \Closure $sourceCompiler) use ($type): void {
225+
$sourceCompiler($compiler);
226+
$compiler->raw(' instanceof \\')->raw($type->getType());
227+
},
228+
'accessor' => function (Compiler $compiler, \Closure $sourceCompiler) use ($attributeName): void {
229+
$sourceCompiler($compiler);
230+
$compiler
231+
->raw('?->')
232+
->raw($attributeName);
233+
},
234+
];
235+
}
236+
237+
/**
238+
* @return array{
239+
* condition: null,
240+
* accessor: \Closure(Compiler, \Closure(Compiler): void): void
241+
* }
242+
*/
243+
private function createUnionAccessCompiler(UnionType $type, Environment $env): array
244+
{
245+
$accessors = [];
246+
247+
foreach ($type->getTypes() as $innerType) {
248+
$accessors[] = $this->createAccessCompiler($innerType, $env);
145249
}
146250

147-
$compiler->raw(', ')
148-
->repr($this->getAttribute('type'))
149-
->raw(', ')->repr($this->getAttribute('is_defined_test'))
150-
->raw(', ')->repr($this->getAttribute('ignore_strict_check'))
151-
->raw(', ')->repr($env->hasExtension(SandboxExtension::class))
152-
->raw(', ')->repr($this->getNode('node')->getTemplateLine())
153-
->raw(')')
154-
;
251+
return [
252+
'condition' => null,
253+
'accessor' => function (Compiler $compiler, \Closure $sourceCompiler) use ($accessors) {
254+
$compiler->raw('match ([');
255+
$compiler->indent();
256+
$sourceCompiler($compiler);
257+
$compiler->raw(", true][1]) {\n");
258+
259+
foreach ($accessors as $accessor) {
260+
if ($accessor['condition'] === null) {
261+
$compiler->raw('default');
262+
} else {
263+
$accessor['condition']($compiler, $sourceCompiler);
264+
}
265+
266+
$compiler->raw(' => ');
267+
$accessor['accessor']($compiler, $sourceCompiler);
268+
$compiler->raw(";\n");
269+
}
270+
271+
$compiler->outdent();
272+
$compiler->raw('}');
273+
}
274+
];
275+
}
276+
277+
/**
278+
* @return array{
279+
* condition: \Closure(Compiler, \Closure(Compiler): void): void,
280+
* accessor: \Closure(Compiler, \Closure(Compiler): void): void
281+
* }
282+
*/
283+
private function createArrayAccessCompiler(): array
284+
{
285+
return [
286+
'condition' => function (Compiler $compiler, \Closure $sourceCompiler): void {
287+
$compiler->raw('(\is_array(');
288+
$sourceCompiler($compiler);
289+
$compiler->raw(') || ');
290+
$sourceCompiler($compiler);
291+
$compiler->raw(' instanceof \\ArrayAccess)');
292+
},
293+
'accessor' => function (Compiler $compiler, \Closure $sourceCompiler): void {
294+
$compiler->raw('(');
295+
$sourceCompiler($compiler);
296+
$compiler
297+
->raw('[')
298+
->subcompile($this->getNode('attribute'))
299+
->raw('] ?? null)');
300+
},
301+
];
302+
}
303+
304+
/**
305+
* @return \Closure(Compiler): void
306+
*/
307+
private function createAutoInlineSourceCompiler(): \Closure
308+
{
309+
$varName = null;
310+
$sourceCompiler = $this->createNodeSourceCompiler();
311+
312+
return function (Compiler $compiler) use (&$varName, &$sourceCompiler): void {
313+
if ($varName === null) {
314+
$varName = $compiler->getVarName();
315+
$newSourceCompiler = $this->createVarNameSourceCompiler($varName);
316+
317+
$compiler->raw('(');
318+
$newSourceCompiler($compiler);
319+
$compiler->raw(' = ');
320+
$sourceCompiler($compiler);
321+
$compiler->raw(')');
322+
323+
$sourceCompiler = $newSourceCompiler;
324+
} else {
325+
$sourceCompiler($compiler);
326+
}
327+
};
328+
}
329+
330+
/**
331+
* @return \Closure(Compiler): void
332+
*/
333+
private function createNodeSourceCompiler(): \Closure
334+
{
335+
return function (Compiler $compiler): void {
336+
$compiler->subcompile($this->getNode('node'));
337+
};
338+
}
339+
340+
/**
341+
* @return \Closure(Compiler): void
342+
*/
343+
private function createVarNameSourceCompiler(string $varName): \Closure
344+
{
345+
return function (Compiler $compiler) use ($varName): void {
346+
$compiler
347+
->raw('$')
348+
->raw($varName)
349+
;
350+
};
155351
}
156352
}

0 commit comments

Comments
 (0)