Skip to content

Commit b65237d

Browse files
committed
Compile method calls
1 parent b6d0ddb commit b65237d

File tree

5 files changed

+187
-20
lines changed

5 files changed

+187
-20
lines changed

src/Node/Expression/GetAttrExpression.php

Lines changed: 49 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
use Twig\Extension\SandboxExtension;
1717
use Twig\Template;
1818
use Twig\TypeHint\ArrayType;
19-
use Twig\TypeHint\TypeFactory;
19+
use Twig\TypeHint\ObjectType;
2020

2121
class GetAttrExpression extends AbstractExpression
2222
{
@@ -35,7 +35,7 @@ public function compile(Compiler $compiler): void
3535
$env = $compiler->getEnvironment();
3636

3737
if ($this->getNode('attribute') instanceof ConstantExpression) {
38-
$type = TypeFactory::createTypeFromText('null');
38+
$type = null;
3939

4040
if ($this->getNode('node')->hasAttribute('typeHint')) {
4141
$type = $this->getNode('node')->getAttribute('typeHint');
@@ -51,6 +51,53 @@ public function compile(Compiler $compiler): void
5151
;
5252

5353
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;
67+
}
68+
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+
}
54101
}
55102
}
56103

src/NodeVisitor/TypeEvaluateNodeVisitor.php

Lines changed: 31 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
use Twig\Environment;
1515
use Twig\Node\AutoEscapeNode;
16+
use Twig\Node\BodyNode;
1617
use Twig\Node\Expression\ArrayExpression;
1718
use Twig\Node\Expression\AssignNameExpression;
1819
use Twig\Node\Expression\Binary\AddBinary;
@@ -55,6 +56,7 @@
5556
use Twig\Node\MacroNode;
5657
use Twig\Node\Node;
5758
use Twig\Node\SetNode;
59+
use Twig\Node\TypeHintNode;
5860
use Twig\Node\WithNode;
5961
use Twig\TypeHint\ArrayType;
6062
use Twig\TypeHint\TypeFactory;
@@ -81,6 +83,14 @@ public function enterNode(Node $node, Environment $env): Node
8183
}
8284
}
8385

86+
if ($node instanceof TypeHintNode) {
87+
$env->getTypeHintStack()->addVariableType($node->getAttribute('name'), TypeFactory::createTypeFromText($node->getAttribute('type')));
88+
}
89+
90+
if ($node instanceof BodyNode) {
91+
$env->getTypeHintStack()->pushMinorStack();
92+
}
93+
8494
return $node;
8595
}
8696

@@ -89,17 +99,19 @@ public function leaveNode(Node $node, Environment $env): ?Node
8999
$possibleTypes = [];
90100

91101
foreach ($this->getPossibleTypes($node, $env) as $possibleType) {
92-
if (!$possibleType instanceof TypeInterface) {
102+
if (!$possibleType instanceof TypeInterface && $possibleType !== null) {
93103
$possibleType = TypeFactory::createTypeFromText((string) $possibleType);
94104
}
95105

96106
$possibleTypes[] = $possibleType;
97107
}
98108

99-
if (\count($possibleTypes) !== 1) {
100-
$node->setAttribute('typeHint', new UnionType($possibleTypes));
101-
} elseif ($possibleTypes !== []) {
102-
$node->setAttribute('typeHint', $possibleTypes[0]);
109+
if ($possibleTypes !== []) {
110+
if (\count($possibleTypes) === 1) {
111+
$node->setAttribute('typeHint', $possibleTypes[0]);
112+
} else {
113+
$node->setAttribute('typeHint', new UnionType($possibleTypes));
114+
}
103115
}
104116

105117
if ($node instanceof SetNode) {
@@ -178,6 +190,10 @@ public function leaveNode(Node $node, Environment $env): ?Node
178190
}
179191
}
180192

193+
if ($node instanceof BodyNode) {
194+
$env->getTypeHintStack()->popMinorStack();
195+
}
196+
181197
return $node;
182198
}
183199

@@ -236,7 +252,11 @@ private function getPossibleTypes(Node $node, Environment $env): iterable
236252
}
237253

238254
if ($node instanceof NameExpression && !$node instanceof AssignNameExpression) {
239-
yield $env->getTypeHintStack()->getVariableType($node->getAttribute('name'));
255+
$result = $env->getTypeHintStack()->getVariableType($node->getAttribute('name'));
256+
257+
if ($result !== null) {
258+
yield $result;
259+
}
240260
}
241261

242262
if ($node instanceof TestExpression) {
@@ -266,7 +286,11 @@ private function getPossibleTypes(Node $node, Environment $env): iterable
266286
continue;
267287
}
268288

269-
yield $innerNode->getAttribute('typeHint');
289+
$typeHint = $innerNode->getAttribute('typeHint');
290+
291+
if ($typeHint !== null) {
292+
yield $typeHint;
293+
}
270294
}
271295
}
272296
}

src/TypeHint/ObjectType.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ public function getAttributeType(string|int $attribute): ?TypeInterface
4747
?? $this->getMethodType('has' . $attribute);
4848
}
4949

50-
private function getPropertyType(string $name): ?TypeInterface
50+
public function getPropertyType(string $name): ?TypeInterface
5151
{
5252
if (\array_key_exists($name, $this->properties)) {
5353
return $this->properties[$name];
@@ -75,7 +75,7 @@ private function createPropertyType(string $name): ?TypeInterface
7575
}
7676
}
7777

78-
private function getMethodType(string $name): ?TypeInterface
78+
public function getMethodType(string $name): ?TypeInterface
7979
{
8080
if (\array_key_exists($name, $this->methods)) {
8181
return $this->methods[$name];

src/TypeHint/TypeFactory.php

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -29,13 +29,13 @@ public static function createTypeFromText(string $type): ?TypeInterface
2929
$types = [];
3030

3131
foreach (\explode('|', $type) as $propertyType) {
32-
if (\str_starts_with($propertyType, '\\')) {
33-
try {
34-
$types[] = self::createObjectType($propertyType);
35-
} catch (\Throwable) {
36-
continue;
37-
}
38-
} else {
32+
if ($propertyType === '') {
33+
continue;
34+
}
35+
36+
try {
37+
$types[] = self::createObjectType(\ltrim($propertyType, '\\'));
38+
} catch (\Throwable) {
3939
$types[] = self::createPlainType($propertyType);
4040
}
4141
}
@@ -62,6 +62,6 @@ private static function createPlainType(string $type): Type
6262
*/
6363
private static function createObjectType(string $class): ObjectType
6464
{
65-
return self::$objectTypeCache[$class] ??= new ObjectType(new \ReflectionClass(\ltrim($class, '\\')));
65+
return self::$objectTypeCache[$class] ??= new ObjectType(new \ReflectionClass($class));
6666
}
6767
}

tests/Node/Expression/GetAttrTest.php

Lines changed: 97 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,9 +88,105 @@ public function getTests()
8888
echo ((((($context["foo"] ?? null))["bar"] ?? null))["baz"] ?? null);
8989
PHP,
9090
$optimizedEnv,
91-
true,
91+
];
92+
93+
$tests[] = [
94+
$optimizedEnv->parse($optimizedEnv->tokenize(new Source(<<<'TWIG'
95+
{% type obj "\\Twig\\Tests\\Node\\Expression\\ClassWithPublicProperty" %}
96+
{{ obj.name|raw }}
97+
TWIG, 'index.twig')))->getNode('body'),
98+
<<<'PHP'
99+
// line 1
100+
// line 2
101+
echo ((($context["obj"] ?? null))?->name);
102+
PHP,
103+
$optimizedEnv,
104+
];
105+
106+
$tests[] = [
107+
$optimizedEnv->parse($optimizedEnv->tokenize(new Source(<<<'TWIG'
108+
{% type obj "\\Twig\\Tests\\Node\\Expression\\ClassWithPublicGetter" %}
109+
{{ obj.name|raw }}
110+
TWIG, 'index.twig')))->getNode('body'),
111+
<<<'PHP'
112+
// line 1
113+
// line 2
114+
echo ((($context["obj"] ?? null))?->getname());
115+
PHP,
116+
$optimizedEnv,
117+
];
118+
119+
$tests[] = [
120+
$optimizedEnv->parse($optimizedEnv->tokenize(new Source(<<<'TWIG'
121+
{% type obj "\\Twig\\Tests\\Node\\Expression\\ClassWithPublicFactory" %}
122+
{{ obj.byName("foobar")|raw }}
123+
TWIG, 'index.twig')))->getNode('body'),
124+
<<<'PHP'
125+
// line 1
126+
// line 2
127+
echo ((($context["obj"] ?? null))?->byName("foobar"));
128+
PHP,
129+
$optimizedEnv,
130+
];
131+
132+
$tests[] = [
133+
$optimizedEnv->parse($optimizedEnv->tokenize(new Source(<<<'TWIG'
134+
{% type obj "\\Twig\\Tests\\Node\\Expression\\ClassWithPublicComplexGetter" %}
135+
{{ obj.instance.name|raw }}
136+
TWIG, 'index.twig')))->getNode('body'),
137+
<<<'PHP'
138+
// line 1
139+
// line 2
140+
echo ((((($context["obj"] ?? null))?->getinstance()))?->getname());
141+
PHP,
142+
$optimizedEnv,
92143
];
93144

94145
return $tests;
95146
}
96147
}
148+
149+
class ClassWithPublicProperty
150+
{
151+
public function __construct(
152+
public string $name
153+
)
154+
{
155+
}
156+
}
157+
158+
class ClassWithPublicGetter
159+
{
160+
public function __construct(
161+
private string $name
162+
)
163+
{
164+
}
165+
166+
public function getName(): string
167+
{
168+
return $this->name;
169+
}
170+
}
171+
172+
class ClassWithPublicFactory
173+
{
174+
public function byName(string $name): string
175+
{
176+
return $name;
177+
}
178+
}
179+
180+
class ClassWithPublicComplexGetter
181+
{
182+
public function __construct(
183+
private string $name
184+
)
185+
{
186+
}
187+
188+
public function getInstance(): ClassWithPublicGetter
189+
{
190+
return new ClassWithPublicGetter($this->name);
191+
}
192+
}

0 commit comments

Comments
 (0)