Skip to content

Commit bb498bc

Browse files
Propagate variable types to generated code to allow statical analysis
1 parent 2c7ddd9 commit bb498bc

9 files changed

Lines changed: 161 additions & 13 deletions

src/Latte/Compiler/Compiler.php

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -50,9 +50,12 @@ class Compiler
5050
/** @var string[] @internal */
5151
public $placeholders = [];
5252

53-
/** @var string|null */
53+
/** @var string */
5454
public $paramsExtraction;
5555

56+
/** @var string */
57+
private $defaultParamsExtraction = 'extract($this->params);';
58+
5659
/** @var Token[] */
5760
private $tokens;
5861

@@ -166,7 +169,8 @@ private function buildClassBody(array $tokens): string
166169
$output = '';
167170
$this->output = &$output;
168171
$this->inHead = true;
169-
$this->htmlNode = $this->macroNode = $this->context = $this->paramsExtraction = null;
172+
$this->htmlNode = $this->context = null;
173+
$this->paramsExtraction = $this->defaultParamsExtraction;
170174
$this->placeholders = $this->properties = $this->constants = [];
171175
$this->methods = ['main' => null, 'prepare' => null];
172176

@@ -215,7 +219,7 @@ private function buildClassBody(array $tokens): string
215219
$epilogs = (empty($res[1]) ? '' : "<?php $res[1] ?>") . $epilogs;
216220
}
217221

218-
$extractParams = $this->paramsExtraction ?? 'extract($this->params);';
222+
$extractParams = $this->paramsExtraction;
219223
$this->addMethod('main', $this->expandTokens($extractParams . "?>\n$output$epilogs<?php return get_defined_vars();"), '', 'array');
220224

221225
if ($prepare) {

src/Latte/Macros/BlockMacros.php

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -334,15 +334,20 @@ public function macroDefine(MacroNode $node, PhpWriter $writer): string
334334
$tokens = $node->tokenizer;
335335
$params = [];
336336
while ($tokens->isNext()) {
337-
if ($tokens->nextToken($tokens::T_SYMBOL, '?', 'null', '\\')) { // type
338-
$tokens->nextAll($tokens::T_SYMBOL, '\\', '|', '[', ']', 'null');
337+
$type = $tokens->nextValue($tokens::T_SYMBOL, '?', 'null', '\\');
338+
if ($type) {
339+
$type .= $tokens->joinAll($tokens::T_SYMBOL, '\\', '|', '[', ']', 'null');
339340
}
340341
$param = $tokens->consumeValue($tokens::T_VARIABLE);
341342
$default = $tokens->nextToken('=')
342343
? $tokens->joinUntilSameDepth(',')
343344
: 'null';
345+
$mask ='%raw = $ʟ_args[%var] ?? $ʟ_args[%var] ?? %raw;';
346+
if($type) {
347+
$mask = "/** @var $type $param */\n" . $mask;
348+
}
344349
$params[] = $writer->write(
345-
'%raw = $ʟ_args[%var] ?? $ʟ_args[%var] ?? %raw;',
350+
$mask,
346351
$param,
347352
count($params),
348353
substr($param, 1),
@@ -556,7 +561,7 @@ private function addBlock(MacroNode $node, string $layer = null): Block
556561
private function extractMethod(MacroNode $node, Block $block, string $params = null): void
557562
{
558563
if (preg_match('#\$|n:#', $node->content)) {
559-
$node->content = '<?php extract(' . ($node->name === 'block' && $node->closest(['embed']) ? 'end($this->varStack)' : '$this->params') . ');'
564+
$node->content = '<?php ' . ($node->name === 'block' && $node->closest(['embed']) ? 'extract(end($this->varStack));' : $this->getCompiler()->paramsExtraction)
560565
. ($params ?? 'extract($ʟ_args);')
561566
. 'unset($ʟ_args);?>'
562567
. $node->content;

src/Latte/Macros/CoreMacros.php

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -813,15 +813,20 @@ public function macroParameters(MacroNode $node, PhpWriter $writer): void
813813
$tokens = $node->tokenizer;
814814
$params = [];
815815
while ($tokens->isNext()) {
816-
if ($tokens->nextToken($tokens::T_SYMBOL, '?', 'null', '\\')) { // type
817-
$tokens->nextAll($tokens::T_SYMBOL, '\\', '|', '[', ']', 'null');
816+
$type = $tokens->nextValue($tokens::T_SYMBOL, '?', 'null', '\\');
817+
if ($type) {
818+
$type .= $tokens->joinAll($tokens::T_SYMBOL, '\\', '|', '[', ']', 'null');
818819
}
819820
$param = $tokens->consumeValue($tokens::T_VARIABLE);
820821
$default = $tokens->nextToken('=')
821822
? $tokens->joinUntilSameDepth(',')
822823
: 'null';
824+
$mask ='%raw = $this->params[%var] ?? $this->params[%var] ?? %raw;';
825+
if($type) {
826+
$mask = "/** @var $type $param */\n" . $mask;
827+
}
823828
$params[] = $writer->write(
824-
'%raw = $this->params[%var] ?? $this->params[%var] ?? %raw;',
829+
$mask,
825830
$param,
826831
count($params),
827832
substr($param, 1),
@@ -838,7 +843,7 @@ public function macroParameters(MacroNode $node, PhpWriter $writer): void
838843
/**
839844
* {varType type $var}
840845
*/
841-
public function macroVarType(MacroNode $node): void
846+
public function macroVarType(MacroNode $node, PhpWriter $writer): string
842847
{
843848
if ($node->modifiers) {
844849
$node->setArgs($node->args . $node->modifiers);
@@ -847,10 +852,17 @@ public function macroVarType(MacroNode $node): void
847852
$node->validate(true);
848853

849854
$type = trim($node->tokenizer->joinUntil($node->tokenizer::T_VARIABLE));
850-
$variable = $node->tokenizer->nextToken($node->tokenizer::T_VARIABLE);
855+
$variable = $node->tokenizer->nextValue($node->tokenizer::T_VARIABLE);
851856
if (!$type || !$variable) {
852857
throw new CompileException('Unexpected content, expecting {varType type $var}.');
853858
}
859+
$comment = "/** @var $type $variable */\n";
860+
if ($this->getCompiler()->isInHead()) {
861+
$this->getCompiler()->paramsExtraction = $comment . $this->getCompiler()->paramsExtraction;
862+
return "";
863+
} else {
864+
return $writer->write($comment);
865+
}
854866
}
855867

856868

tests/Latte/BlockMacros.define.args.phpt

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,3 +157,26 @@ Assert::matchFile(
157157
__DIR__ . '/expected/BlockMacros.define.args5.html',
158158
$latte->renderToString($template)
159159
);
160+
161+
// types
162+
$latte->setLoader(new Latte\Loaders\StringLoader);
163+
$template = <<<'XX'
164+
default values
165+
166+
{define test $var1 = 0, array $var2 = [1, 2, 3], int $var3 = 10}
167+
Variables {$var1}, {$var2|implode}, {$var3}
168+
{/define}
169+
170+
a) {include test, 1}
171+
172+
b) {include test, var1 => 1}
173+
XX;
174+
175+
Assert::matchFile(
176+
__DIR__ . '/expected/BlockMacros.define.args6.phtml',
177+
$latte->compile($template)
178+
);
179+
Assert::matchFile(
180+
__DIR__ . '/expected/BlockMacros.define.args6.html',
181+
$latte->renderToString($template)
182+
);

tests/Latte/CoreMacros.parameters.phpt

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,17 +19,28 @@ $latte->setLoader(new Latte\Loaders\StringLoader([
1919
'main3' => '{include inc3.latte, a: 10}',
2020
'main4' => '{include inc4.latte, a: 10}',
2121
'main5' => '{include inc5.latte, a: 10}',
22+
'main6' => '{include inc6.latte, a: 10}',
23+
'main7' => '{include inc7.latte, a: 10}',
24+
'main8' => '{include inc8.latte, a: 10}',
2225

2326
'inc1.latte' => '{$a ?? "-"} {$b ?? "-"} {$glob ?? "-"}',
2427
'inc2.latte' => '{parameters $a} {$a ?? "-"} {$b ?? "-"} {$glob ?? "-"}',
2528
'inc3.latte' => '{parameters int $a = 5} {$a ?? "-"} {$b ?? "-"} {$glob ?? "-"}',
2629
'inc4.latte' => '{parameters $a, int $b = 5} {$a ?? "-"} {$b ?? "-"} {$glob ?? "-"}',
2730
'inc5.latte' => '{parameters $glob} {$a ?? "-"} {$b ?? "-"} {$glob ?? "-"}',
31+
'inc6.latte' => '{parameters ?\Exception $glob} {$a ?? "-"} {$b ?? "-"} {$glob->getMessage() ?? "-"}',
32+
'inc7.latte' => '{parameters $a, int $b = 5} {block x}{$a ?? "-"} {$b ?? "-"} {$glob ?? "-"}{/block}',
33+
'inc8.latte' => '{parameters $a, int $b = 5} {define x}{$a ?? "-"} {$b ?? "-"} {$glob ?? "-"}{/define}{include x}',
2834
]));
2935

30-
3136
Assert::same('10 - 123', $latte->renderToString('main1', ['glob' => 123]));
3237
Assert::same(' 10 - -', $latte->renderToString('main2', ['glob' => 123]));
3338
Assert::same(' 10 - -', $latte->renderToString('main3', ['glob' => 123]));
3439
Assert::same(' 10 5 -', $latte->renderToString('main4', ['glob' => 123]));
3540
Assert::same(' - - 123', $latte->renderToString('main5', ['glob' => 123]));
41+
Assert::same(' - - 123', $latte->renderToString('main6', ['glob' => new \Exception("123")]));
42+
Assert::same(' 10 5 -', $latte->renderToString('main7', ['glob' => 123]));
43+
Assert::same(' 10 5 -', $latte->renderToString('main8', ['glob' => 123]));
44+
45+
Assert::contains('/** @var int $a */', $latte->compile('inc3.latte'));
46+
Assert::contains('/** @var ?\Exception $glob */', $latte->compile('inc6.latte'));

tests/Latte/CoreMacros.varType.phpt

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,3 +46,25 @@ Assert::noError(function () use ($latte) {
4646
Assert::noError(function () use ($latte) {
4747
$latte->compile('{varType array{0: int, 1: int} $var}');
4848
});
49+
50+
Assert::contains('/** @var int|null $var */', $latte->compile('{varType int|null $var}'));
51+
52+
$template = <<<'XX'
53+
{varType string $a}
54+
55+
{$a}
56+
57+
{include test}
58+
59+
{define test}
60+
{varType int $b}
61+
{var $b = 5}
62+
{$a}{$b}
63+
{/define}
64+
65+
XX;
66+
67+
Assert::matchFile(
68+
__DIR__ . '/expected/CoreMacros.varType.phtml',
69+
$latte->compile($template)
70+
);
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
default values
2+
3+
4+
a) Variables 1, 123, 10
5+
6+
7+
b) Variables 1, 123, 10
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
<?php
2+
%A%
3+
final class Template%a% extends Latte\Runtime\Template
4+
{
5+
protected const BLOCKS = [
6+
['test' => 'blockTest'],
7+
];
8+
9+
10+
public function main(): array
11+
{
12+
extract($this->params);
13+
echo 'default values
14+
15+
';
16+
if ($this->getParentName()) {
17+
return get_defined_vars();
18+
}
19+
echo '
20+
a) ';
21+
$this->renderBlock('test', [1] + [], 'html') /* line %d% */;
22+
echo '
23+
24+
b) ';
25+
$this->renderBlock('test', ['var1' => 1] + [], 'html') /* line %d% */;
26+
return get_defined_vars();
27+
}
28+
29+
30+
/** {define test $var1 = 0, array $var2 = [1, 2, 3], int $var3 = 10} on line %d% */
31+
public function blockTest(array $ʟ_args): void
32+
{
33+
extract($this->params);
34+
$var1 = $ʟ_args[0] ?? $ʟ_args['var1'] ?? 0;
35+
/** @var array $var2 */
36+
$var2 = $ʟ_args[1] ?? $ʟ_args['var2'] ?? [1, 2, 3];
37+
/** @var int $var3 */
38+
$var3 = $ʟ_args[2] ?? $ʟ_args['var3'] ?? 10;
39+
unset($ʟ_args);
40+
echo ' Variables ';
41+
echo LR\Filters::escapeHtmlText($var1) /* line %d% */;
42+
echo ', ';
43+
echo LR\Filters::escapeHtmlText(($this->filters->implode)($var2)) /* line %d% */;
44+
echo ', ';
45+
echo LR\Filters::escapeHtmlText($var3) /* line %d% */;
46+
echo "\n";
47+
}
48+
49+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<?php
2+
%A%
3+
public function main(): array
4+
{
5+
/** @var string $a */
6+
extract($this->params);
7+
%A%
8+
public function blockTest(array $ʟ_args): void
9+
{
10+
/** @var string $a */
11+
extract($this->params);
12+
%A%
13+
/** @var int $b */
14+
$b = 5%a%;
15+
%A%

0 commit comments

Comments
 (0)