Skip to content

Commit 9fe662e

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

13 files changed

Lines changed: 264 additions & 15 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->macroNode = $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: 44 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@
1717
use Latte\PhpHelpers;
1818
use Latte\PhpWriter;
1919

20-
2120
/**
2221
* Basic macros for Latte.
2322
*/
@@ -813,15 +812,20 @@ public function macroParameters(MacroNode $node, PhpWriter $writer): void
813812
$tokens = $node->tokenizer;
814813
$params = [];
815814
while ($tokens->isNext()) {
816-
if ($tokens->nextToken($tokens::T_SYMBOL, '?', 'null', '\\')) { // type
817-
$tokens->nextAll($tokens::T_SYMBOL, '\\', '|', '[', ']', 'null');
815+
$type = $tokens->nextValue($tokens::T_SYMBOL, '?', 'null', '\\');
816+
if ($type) {
817+
$type .= $tokens->joinAll($tokens::T_SYMBOL, '\\', '|', '[', ']', 'null');
818818
}
819819
$param = $tokens->consumeValue($tokens::T_VARIABLE);
820820
$default = $tokens->nextToken('=')
821821
? $tokens->joinUntilSameDepth(',')
822822
: 'null';
823+
$mask ='%raw = $this->params[%var] ?? $this->params[%var] ?? %raw;';
824+
if($type) {
825+
$mask = "/** @var $type $param */\n" . $mask;
826+
}
823827
$params[] = $writer->write(
824-
'%raw = $this->params[%var] ?? $this->params[%var] ?? %raw;',
828+
$mask,
825829
$param,
826830
count($params),
827831
substr($param, 1),
@@ -838,7 +842,7 @@ public function macroParameters(MacroNode $node, PhpWriter $writer): void
838842
/**
839843
* {varType type $var}
840844
*/
841-
public function macroVarType(MacroNode $node): void
845+
public function macroVarType(MacroNode $node, PhpWriter $writer): string
842846
{
843847
if ($node->modifiers) {
844848
$node->setArgs($node->args . $node->modifiers);
@@ -847,10 +851,17 @@ public function macroVarType(MacroNode $node): void
847851
$node->validate(true);
848852

849853
$type = trim($node->tokenizer->joinUntil($node->tokenizer::T_VARIABLE));
850-
$variable = $node->tokenizer->nextToken($node->tokenizer::T_VARIABLE);
854+
$variable = $node->tokenizer->nextValue($node->tokenizer::T_VARIABLE);
851855
if (!$type || !$variable) {
852856
throw new CompileException('Unexpected content, expecting {varType type $var}.');
853857
}
858+
$comment = "/** @var $type $variable */\n";
859+
if ($this->getCompiler()->isInHead()) {
860+
$this->getCompiler()->paramsExtraction .= $comment;
861+
return "";
862+
} else {
863+
return $writer->write($comment);
864+
}
854865
}
855866

856867

@@ -869,12 +880,38 @@ public function macroVarPrint(MacroNode $node): string
869880
/**
870881
* {templateType ClassName}
871882
*/
872-
public function macroTemplateType(MacroNode $node): void
883+
public function macroTemplateType(MacroNode $node)
873884
{
874885
if (!$this->getCompiler()->isInHead()) {
875886
throw new CompileException($node->getNotation() . ' is allowed only in template header.');
876887
}
877888
$node->validate('class name');
889+
try {
890+
$reflectionClass = new \ReflectionClass($node->args);
891+
foreach ($reflectionClass->getProperties() as $property) {
892+
if(!$property->isPublic()) {
893+
continue;
894+
}
895+
$propertyName = $property->getName();
896+
$type = $property->getType();
897+
$typeName = null;
898+
if ($type instanceof \ReflectionNamedType) {
899+
$typeName = ($type->allowsNull() ? "?" : "") . $type->getName();
900+
} elseif ($type instanceof \ReflectionUnionType) {
901+
$typeName = implode("|", array_map(
902+
function(\ReflectionNamedType $type) { return $type->getName(); },
903+
$type->getTypes()
904+
));
905+
}
906+
if(!$typeName) {
907+
$typeName = "mixed";
908+
}
909+
$comment = "/** @var $typeName \$$propertyName ({$node->args}) */\n";
910+
$this->getCompiler()->paramsExtraction .= $comment;
911+
}
912+
} catch (\ReflectionException $e) {
913+
914+
}
878915
}
879916

880917

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'));
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<?php
2+
3+
/**
4+
* Test: {templateType}
5+
* @phpVersion 8
6+
*/
7+
8+
declare(strict_types=1);
9+
10+
use Tester\Assert;
11+
12+
13+
require __DIR__ . '/../bootstrap.php';
14+
15+
16+
$latte = new Latte\Engine;
17+
$latte->setLoader(new Latte\Loaders\StringLoader);
18+
19+
class ExampleTemplateType {
20+
public $a;
21+
public int $b;
22+
public ExampleTemplateType|int|null $c;
23+
private $private;
24+
}
25+
26+
Assert::matchFile(
27+
__DIR__ . '/expected/CoreMacros.templateType.80.phtml',
28+
$latte->compile('{templateType ExampleTemplateType}{define test}{$a}{/define}')
29+
);

tests/Latte/CoreMacros.templateType.phpt

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,3 +26,15 @@ Assert::exception(function () use ($latte) {
2626
Assert::noError(function () use ($latte) {
2727
$latte->compile('{templateType stdClass}');
2828
});
29+
30+
class ExampleTemplateType {
31+
public $a;
32+
public int $b;
33+
public ?ExampleTemplateType $c;
34+
private $private;
35+
}
36+
37+
Assert::matchFile(
38+
__DIR__ . '/expected/CoreMacros.templateType.phtml',
39+
$latte->compile('{templateType ExampleTemplateType}{define test}{$a}{/define}')
40+
);

tests/Latte/CoreMacros.varType.phpt

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,3 +46,28 @@ 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+
54+
{varType string $a}
55+
56+
{$a}
57+
58+
{varType string $c}
59+
60+
{include test}
61+
62+
{define test}
63+
{varType int $b}
64+
{var $b = 5}
65+
{$a}{$b}
66+
{/define}
67+
68+
XX;
69+
70+
Assert::matchFile(
71+
__DIR__ . '/expected/CoreMacros.varType.phtml',
72+
$latte->compile($template)
73+
);
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+
}

0 commit comments

Comments
 (0)