Skip to content

Commit b6d0ddb

Browse files
committed
Track variable types on the stack and support looking up the types
1 parent 2443ca8 commit b6d0ddb

7 files changed

Lines changed: 170 additions & 11 deletions

File tree

src/Environment.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@
3333
use Twig\NodeVisitor\NodeVisitorInterface;
3434
use Twig\RuntimeLoader\RuntimeLoaderInterface;
3535
use Twig\TokenParser\TokenParserInterface;
36+
use Twig\TypeHint\ContextStack;
37+
use Twig\TypeHint\TypeInterface;
3638

3739
/**
3840
* Stores the Twig configuration and renders templates.
@@ -69,6 +71,7 @@ class Environment
6971
private $optionsHash;
7072
/** @var bool */
7173
private $useYield;
74+
private $typeHintStack;
7275

7376
/**
7477
* Constructor.
@@ -127,6 +130,7 @@ public function __construct(LoaderInterface $loader, $options = [])
127130
$this->strictVariables = (bool) $options['strict_variables'];
128131
$this->setCache($options['cache']);
129132
$this->extensionSet = new ExtensionSet();
133+
$this->typeHintStack = new ContextStack();
130134

131135
$this->addExtension(new CoreExtension());
132136
$this->addExtension(new EscaperExtension($options['autoescape']));
@@ -845,6 +849,11 @@ public function getBinaryOperators(): array
845849
return $this->extensionSet->getBinaryOperators();
846850
}
847851

852+
public function getTypeHintStack(): ContextStack
853+
{
854+
return $this->typeHintStack;
855+
}
856+
848857
private function updateOptionsHash(): void
849858
{
850859
$this->optionsHash = implode(':', [

src/NodeVisitor/TypeEvaluateNodeVisitor.php

Lines changed: 49 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
use Twig\Node\Expression\BlockReferenceExpression;
4747
use Twig\Node\Expression\ConstantExpression;
4848
use Twig\Node\Expression\GetAttrExpression;
49+
use Twig\Node\Expression\NameExpression;
4950
use Twig\Node\Expression\ParentExpression;
5051
use Twig\Node\Expression\TestExpression;
5152
use Twig\Node\Expression\Unary\NegUnary;
@@ -54,6 +55,7 @@
5455
use Twig\Node\MacroNode;
5556
use Twig\Node\Node;
5657
use Twig\Node\SetNode;
58+
use Twig\Node\WithNode;
5759
use Twig\TypeHint\ArrayType;
5860
use Twig\TypeHint\TypeFactory;
5961
use Twig\TypeHint\TypeInterface;
@@ -72,14 +74,21 @@ final class TypeEvaluateNodeVisitor implements NodeVisitorInterface
7274
{
7375
public function enterNode(Node $node, Environment $env): Node
7476
{
77+
if ($node instanceof WithNode) {
78+
if ($node->hasNode('variables')) {
79+
// we need to store the parent, so we can assign notes to the parent, after the variables' sibling nodes are entered
80+
$node->getNode('variables')->setAttribute('setKeyValuesAsTypeHints', $node->getAttribute('only'));
81+
}
82+
}
83+
7584
return $node;
7685
}
7786

7887
public function leaveNode(Node $node, Environment $env): ?Node
7988
{
8089
$possibleTypes = [];
8190

82-
foreach ($this->getPossibleTypes($node) as $possibleType) {
91+
foreach ($this->getPossibleTypes($node, $env) as $possibleType) {
8392
if (!$possibleType instanceof TypeInterface) {
8493
$possibleType = TypeFactory::createTypeFromText((string) $possibleType);
8594
}
@@ -114,6 +123,10 @@ public function leaveNode(Node $node, Environment $env): ?Node
114123

115124
if ($typedVariables !== []) {
116125
$node->setAttribute('typeHint', new ArrayType($typedVariables));
126+
127+
foreach ($typedVariables as $typedVariableName => $typedVariableTypes) {
128+
$env->getTypeHintStack()->addVariableType($typedVariableName, $typedVariableTypes);
129+
}
117130
}
118131
}
119132

@@ -138,6 +151,33 @@ public function leaveNode(Node $node, Environment $env): ?Node
138151
}
139152
}
140153

154+
// this is the variables child node from WithNode and therefore these are types to note down
155+
if ($node->hasAttribute('setKeyValuesAsTypeHints')) {
156+
$only = $node->getAttribute('setKeyValuesAsTypeHints');
157+
158+
if ($node instanceof ArrayExpression && $node->hasAttribute('typeHint') && $node->getAttribute('typeHint') instanceof ArrayType) {
159+
if ($only) {
160+
$env->getTypeHintStack()->pushMajorStack();
161+
} else {
162+
$env->getTypeHintStack()->pushMinorStack();
163+
}
164+
165+
foreach ($node->getAttribute('typeHint')->getAttributes() as $typedVariableName => $typedVariableTypes) {
166+
$env->getTypeHintStack()->addVariableType($typedVariableName, $typedVariableTypes);
167+
}
168+
}
169+
170+
$node->removeAttribute('setKeyValuesAsTypeHints');
171+
}
172+
173+
if ($node instanceof WithNode) {
174+
if ($node->getAttribute('only')) {
175+
$env->getTypeHintStack()->popMajorStack();
176+
} else {
177+
$env->getTypeHintStack()->popMinorStack();
178+
}
179+
}
180+
141181
return $node;
142182
}
143183

@@ -146,7 +186,7 @@ public function getPriority(): int
146186
return 10;
147187
}
148188

149-
private function getPossibleTypes(Node $node): iterable
189+
private function getPossibleTypes(Node $node, Environment $env): iterable
150190
{
151191
if ($node instanceof AutoEscapeNode) {
152192
yield 'string';
@@ -163,6 +203,9 @@ private function getPossibleTypes(Node $node): iterable
163203

164204
if ($node->getNode('node')->hasAttribute('typeHint')) {
165205
$typeHint = $node->getNode('node')->getAttribute('typeHint');
206+
} elseif ($node->getNode('node') instanceof NameExpression) {
207+
$variableName = $node->getNode('node')->getAttribute('name');
208+
$typeHint = $env->getTypeHintStack()->getVariableType($variableName);
166209
}
167210

168211
if ($typeHint instanceof TypeInterface) {
@@ -192,6 +235,10 @@ private function getPossibleTypes(Node $node): iterable
192235
yield 'string';
193236
}
194237

238+
if ($node instanceof NameExpression && !$node instanceof AssignNameExpression) {
239+
yield $env->getTypeHintStack()->getVariableType($node->getAttribute('name'));
240+
}
241+
195242
if ($node instanceof TestExpression) {
196243
yield 'boolean';
197244
}

src/TypeHint/ContextStack.php

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
<?php
2+
3+
/*
4+
* This file is part of Twig.
5+
*
6+
* (c) Fabien Potencier
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
declare(strict_types=1);
13+
14+
namespace Twig\TypeHint;
15+
16+
/**
17+
* Describes a stack of possible types for a variable.
18+
*
19+
* @author Joshua Behrens <code@joshua-behrens.de>
20+
*/
21+
class ContextStack
22+
{
23+
/**
24+
* First/major layer non-sharing stacks (with-node e.g. tagged as only)
25+
* Second/minor layer for sharing stacks (with-node e.g. not tagged as only)
26+
*
27+
* @var list<list<array<string, list<TypeInterface>>>>
28+
*/
29+
private array $variables = [[]];
30+
31+
public function getVariableType(string $name): ?TypeInterface
32+
{
33+
$result = [];
34+
35+
foreach ($this->variables[0] as $types) {
36+
foreach ($types[$name] ?? [] as $type) {
37+
$result[] = $type;
38+
}
39+
}
40+
41+
return TypeFactory::createTypeFromCollection($result);
42+
}
43+
44+
public function addVariableType(string $name, TypeInterface $type): void
45+
{
46+
$this->variables[0][0][$name][] = $type;
47+
}
48+
49+
public function pushMajorStack(): void
50+
{
51+
\array_unshift($this->variables, []);
52+
}
53+
54+
public function popMajorStack(): void
55+
{
56+
\array_shift($this->variables);
57+
}
58+
59+
public function pushMinorStack(): void
60+
{
61+
\array_unshift($this->variables[0], []);
62+
}
63+
64+
public function popMinorStack(): void
65+
{
66+
\array_shift($this->variables[0]);
67+
}
68+
}

src/TypeHint/TypeFactory.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,11 @@ public static function createTypeFromText(string $type): ?TypeInterface
4040
}
4141
}
4242

43+
return static::createTypeFromCollection($types);
44+
}
45+
46+
public static function createTypeFromCollection(array $types): ?TypeInterface
47+
{
4348
if ($types === []) {
4449
return null;
4550
}

src/TypeHint/UnionType.php

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -65,14 +65,6 @@ public function getAttributeType(string|int $attribute): ?TypeInterface
6565
}
6666
}
6767

68-
if ($result === []) {
69-
return null;
70-
}
71-
72-
if (\count($result) === 1) {
73-
return $result[0];
74-
}
75-
76-
return new UnionType($result);
68+
return TypeFactory::createTypeFromCollection($result);
7769
}
7870
}

tests/Node/Expression/GetAttrTest.php

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,22 @@ public function getTests()
7575
$optimizedEnv,
7676
];
7777

78+
$tests[] = [
79+
$optimizedEnv->parse(
80+
$optimizedEnv->tokenize(
81+
new Source("{% set foo = { bar: { baz: 42 } } %}\n{{ foo.bar.baz|raw }}", 'index.twig')
82+
)
83+
)->getNode('body'),
84+
<<<'PHP'
85+
// line 1
86+
$context["foo"] = ["bar" => ["baz" => 42]];
87+
// line 2
88+
echo ((((($context["foo"] ?? null))["bar"] ?? null))["baz"] ?? null);
89+
PHP,
90+
$optimizedEnv,
91+
true,
92+
];
93+
7894
return $tests;
7995
}
8096
}

tests/NodeVisitor/TypeEvaluateNodeVisitorTest.php

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
use Twig\Node\Expression\BlockReferenceExpression;
2121
use Twig\Node\PrintNode;
2222
use Twig\Node\SetNode;
23+
use Twig\Node\WithNode;
2324
use Twig\Source;
2425
use Twig\TypeHint\ArrayType;
2526
use Twig\TypeHint\Type;
@@ -100,4 +101,25 @@ public function testSetVariableIsAssignedArrayObject(): void
100101
$this->assertInstanceOf(Type::class, $barType);
101102
$this->assertSame('integer', $barType->getType());
102103
}
104+
105+
public function testArrayExpressionReferencingOtherVariable(): void
106+
{
107+
$env = new Environment($this->createMock(LoaderInterface::class), ['cache' => false, 'autoescape' => false]);
108+
$env->addExtension(new TypeOptimizerExtension());
109+
110+
$stream = $env->parse($env->tokenize(new Source('{% set thing = 42 %}{{ { foo: \'bar\', baz: thing } }}', 'index')));
111+
112+
$node = $stream->getNode('body')->getNode(0)->getNode(1)->getNode('expr');
113+
$type = $node->getAttribute('typeHint');
114+
115+
$this->assertInstanceOf(ArrayType::class, $type);
116+
117+
$fooType = $type->getAttributeType('foo');
118+
$bazType = $type->getAttributeType('baz');
119+
120+
$this->assertInstanceOf(Type::class, $fooType);
121+
$this->assertInstanceOf(Type::class, $bazType);
122+
$this->assertSame('string', $fooType->getType());
123+
$this->assertSame('integer', $bazType->getType());
124+
}
103125
}

0 commit comments

Comments
 (0)