Skip to content

Commit

Permalink
Propagate variable types to generated code to allow statical analysis
Browse files Browse the repository at this point in the history
  • Loading branch information
MartinMystikJonas committed Oct 13, 2021
1 parent 2c7ddd9 commit 9fe662e
Show file tree
Hide file tree
Showing 13 changed files with 264 additions and 15 deletions.
10 changes: 7 additions & 3 deletions src/Latte/Compiler/Compiler.php
Original file line number Diff line number Diff line change
Expand Up @@ -50,9 +50,12 @@ class Compiler
/** @var string[] @internal */
public $placeholders = [];

/** @var string|null */
/** @var string */
public $paramsExtraction;

/** @var string */
private $defaultParamsExtraction = 'extract($this->params);';

/** @var Token[] */
private $tokens;

Expand Down Expand Up @@ -166,7 +169,8 @@ private function buildClassBody(array $tokens): string
$output = '';
$this->output = &$output;
$this->inHead = true;
$this->htmlNode = $this->macroNode = $this->context = $this->paramsExtraction = null;
$this->htmlNode = $this->macroNode = $this->context = null;
$this->paramsExtraction = $this->defaultParamsExtraction;
$this->placeholders = $this->properties = $this->constants = [];
$this->methods = ['main' => null, 'prepare' => null];

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

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

if ($prepare) {
Expand Down
13 changes: 9 additions & 4 deletions src/Latte/Macros/BlockMacros.php
Original file line number Diff line number Diff line change
Expand Up @@ -334,15 +334,20 @@ public function macroDefine(MacroNode $node, PhpWriter $writer): string
$tokens = $node->tokenizer;
$params = [];
while ($tokens->isNext()) {
if ($tokens->nextToken($tokens::T_SYMBOL, '?', 'null', '\\')) { // type
$tokens->nextAll($tokens::T_SYMBOL, '\\', '|', '[', ']', 'null');
$type = $tokens->nextValue($tokens::T_SYMBOL, '?', 'null', '\\');
if ($type) {
$type .= $tokens->joinAll($tokens::T_SYMBOL, '\\', '|', '[', ']', 'null');
}
$param = $tokens->consumeValue($tokens::T_VARIABLE);
$default = $tokens->nextToken('=')
? $tokens->joinUntilSameDepth(',')
: 'null';
$mask ='%raw = $ʟ_args[%var] ?? $ʟ_args[%var] ?? %raw;';
if($type) {
$mask = "/** @var $type $param */\n" . $mask;
}
$params[] = $writer->write(
'%raw = $ʟ_args[%var] ?? $ʟ_args[%var] ?? %raw;',
$mask,
$param,
count($params),
substr($param, 1),
Expand Down Expand Up @@ -556,7 +561,7 @@ private function addBlock(MacroNode $node, string $layer = null): Block
private function extractMethod(MacroNode $node, Block $block, string $params = null): void
{
if (preg_match('#\$|n:#', $node->content)) {
$node->content = '<?php extract(' . ($node->name === 'block' && $node->closest(['embed']) ? 'end($this->varStack)' : '$this->params') . ');'
$node->content = '<?php ' . ($node->name === 'block' && $node->closest(['embed']) ? 'extract(end($this->varStack));' : $this->getCompiler()->paramsExtraction)
. ($params ?? 'extract($ʟ_args);')
. 'unset($ʟ_args);?>'
. $node->content;
Expand Down
51 changes: 44 additions & 7 deletions src/Latte/Macros/CoreMacros.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@
use Latte\PhpHelpers;
use Latte\PhpWriter;


/**
* Basic macros for Latte.
*/
Expand Down Expand Up @@ -813,15 +812,20 @@ public function macroParameters(MacroNode $node, PhpWriter $writer): void
$tokens = $node->tokenizer;
$params = [];
while ($tokens->isNext()) {
if ($tokens->nextToken($tokens::T_SYMBOL, '?', 'null', '\\')) { // type
$tokens->nextAll($tokens::T_SYMBOL, '\\', '|', '[', ']', 'null');
$type = $tokens->nextValue($tokens::T_SYMBOL, '?', 'null', '\\');
if ($type) {
$type .= $tokens->joinAll($tokens::T_SYMBOL, '\\', '|', '[', ']', 'null');
}
$param = $tokens->consumeValue($tokens::T_VARIABLE);
$default = $tokens->nextToken('=')
? $tokens->joinUntilSameDepth(',')
: 'null';
$mask ='%raw = $this->params[%var] ?? $this->params[%var] ?? %raw;';
if($type) {
$mask = "/** @var $type $param */\n" . $mask;
}
$params[] = $writer->write(
'%raw = $this->params[%var] ?? $this->params[%var] ?? %raw;',
$mask,
$param,
count($params),
substr($param, 1),
Expand All @@ -838,7 +842,7 @@ public function macroParameters(MacroNode $node, PhpWriter $writer): void
/**
* {varType type $var}
*/
public function macroVarType(MacroNode $node): void
public function macroVarType(MacroNode $node, PhpWriter $writer): string
{
if ($node->modifiers) {
$node->setArgs($node->args . $node->modifiers);
Expand All @@ -847,10 +851,17 @@ public function macroVarType(MacroNode $node): void
$node->validate(true);

$type = trim($node->tokenizer->joinUntil($node->tokenizer::T_VARIABLE));
$variable = $node->tokenizer->nextToken($node->tokenizer::T_VARIABLE);
$variable = $node->tokenizer->nextValue($node->tokenizer::T_VARIABLE);
if (!$type || !$variable) {
throw new CompileException('Unexpected content, expecting {varType type $var}.');
}
$comment = "/** @var $type $variable */\n";
if ($this->getCompiler()->isInHead()) {
$this->getCompiler()->paramsExtraction .= $comment;
return "";
} else {
return $writer->write($comment);
}
}


Expand All @@ -869,12 +880,38 @@ public function macroVarPrint(MacroNode $node): string
/**
* {templateType ClassName}
*/
public function macroTemplateType(MacroNode $node): void
public function macroTemplateType(MacroNode $node)
{
if (!$this->getCompiler()->isInHead()) {
throw new CompileException($node->getNotation() . ' is allowed only in template header.');
}
$node->validate('class name');
try {
$reflectionClass = new \ReflectionClass($node->args);
foreach ($reflectionClass->getProperties() as $property) {
if(!$property->isPublic()) {
continue;
}
$propertyName = $property->getName();
$type = $property->getType();
$typeName = null;
if ($type instanceof \ReflectionNamedType) {
$typeName = ($type->allowsNull() ? "?" : "") . $type->getName();
} elseif ($type instanceof \ReflectionUnionType) {
$typeName = implode("|", array_map(
function(\ReflectionNamedType $type) { return $type->getName(); },
$type->getTypes()
));
}
if(!$typeName) {
$typeName = "mixed";
}
$comment = "/** @var $typeName \$$propertyName ({$node->args}) */\n";
$this->getCompiler()->paramsExtraction .= $comment;
}
} catch (\ReflectionException $e) {

}
}


Expand Down
23 changes: 23 additions & 0 deletions tests/Latte/BlockMacros.define.args.phpt
Original file line number Diff line number Diff line change
Expand Up @@ -157,3 +157,26 @@ Assert::matchFile(
__DIR__ . '/expected/BlockMacros.define.args5.html',
$latte->renderToString($template)
);

// types
$latte->setLoader(new Latte\Loaders\StringLoader);
$template = <<<'XX'
default values
{define test $var1 = 0, array $var2 = [1, 2, 3], int $var3 = 10}
Variables {$var1}, {$var2|implode}, {$var3}
{/define}
a) {include test, 1}
b) {include test, var1 => 1}
XX;

Assert::matchFile(
__DIR__ . '/expected/BlockMacros.define.args6.phtml',
$latte->compile($template)
);
Assert::matchFile(
__DIR__ . '/expected/BlockMacros.define.args6.html',
$latte->renderToString($template)
);
13 changes: 12 additions & 1 deletion tests/Latte/CoreMacros.parameters.phpt
Original file line number Diff line number Diff line change
Expand Up @@ -19,17 +19,28 @@ $latte->setLoader(new Latte\Loaders\StringLoader([
'main3' => '{include inc3.latte, a: 10}',
'main4' => '{include inc4.latte, a: 10}',
'main5' => '{include inc5.latte, a: 10}',
'main6' => '{include inc6.latte, a: 10}',
'main7' => '{include inc7.latte, a: 10}',
'main8' => '{include inc8.latte, a: 10}',

'inc1.latte' => '{$a ?? "-"} {$b ?? "-"} {$glob ?? "-"}',
'inc2.latte' => '{parameters $a} {$a ?? "-"} {$b ?? "-"} {$glob ?? "-"}',
'inc3.latte' => '{parameters int $a = 5} {$a ?? "-"} {$b ?? "-"} {$glob ?? "-"}',
'inc4.latte' => '{parameters $a, int $b = 5} {$a ?? "-"} {$b ?? "-"} {$glob ?? "-"}',
'inc5.latte' => '{parameters $glob} {$a ?? "-"} {$b ?? "-"} {$glob ?? "-"}',
'inc6.latte' => '{parameters ?\Exception $glob} {$a ?? "-"} {$b ?? "-"} {$glob->getMessage() ?? "-"}',
'inc7.latte' => '{parameters $a, int $b = 5} {block x}{$a ?? "-"} {$b ?? "-"} {$glob ?? "-"}{/block}',
'inc8.latte' => '{parameters $a, int $b = 5} {define x}{$a ?? "-"} {$b ?? "-"} {$glob ?? "-"}{/define}{include x}',
]));


Assert::same('10 - 123', $latte->renderToString('main1', ['glob' => 123]));
Assert::same(' 10 - -', $latte->renderToString('main2', ['glob' => 123]));
Assert::same(' 10 - -', $latte->renderToString('main3', ['glob' => 123]));
Assert::same(' 10 5 -', $latte->renderToString('main4', ['glob' => 123]));
Assert::same(' - - 123', $latte->renderToString('main5', ['glob' => 123]));
Assert::same(' - - 123', $latte->renderToString('main6', ['glob' => new \Exception("123")]));
Assert::same(' 10 5 -', $latte->renderToString('main7', ['glob' => 123]));
Assert::same(' 10 5 -', $latte->renderToString('main8', ['glob' => 123]));

Assert::contains('/** @var int $a */', $latte->compile('inc3.latte'));
Assert::contains('/** @var ?\Exception $glob */', $latte->compile('inc6.latte'));
29 changes: 29 additions & 0 deletions tests/Latte/CoreMacros.templateType.80.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<?php

/**
* Test: {templateType}
* @phpVersion 8
*/

declare(strict_types=1);

use Tester\Assert;


require __DIR__ . '/../bootstrap.php';


$latte = new Latte\Engine;
$latte->setLoader(new Latte\Loaders\StringLoader);

class ExampleTemplateType {
public $a;
public int $b;
public ExampleTemplateType|int|null $c;
private $private;
}

Assert::matchFile(
__DIR__ . '/expected/CoreMacros.templateType.80.phtml',
$latte->compile('{templateType ExampleTemplateType}{define test}{$a}{/define}')
);
12 changes: 12 additions & 0 deletions tests/Latte/CoreMacros.templateType.phpt
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,15 @@ Assert::exception(function () use ($latte) {
Assert::noError(function () use ($latte) {
$latte->compile('{templateType stdClass}');
});

class ExampleTemplateType {
public $a;
public int $b;
public ?ExampleTemplateType $c;
private $private;
}

Assert::matchFile(
__DIR__ . '/expected/CoreMacros.templateType.phtml',
$latte->compile('{templateType ExampleTemplateType}{define test}{$a}{/define}')
);
25 changes: 25 additions & 0 deletions tests/Latte/CoreMacros.varType.phpt
Original file line number Diff line number Diff line change
Expand Up @@ -46,3 +46,28 @@ Assert::noError(function () use ($latte) {
Assert::noError(function () use ($latte) {
$latte->compile('{varType array{0: int, 1: int} $var}');
});

Assert::contains('/** @var int|null $var */', $latte->compile('{varType int|null $var}'));

$template = <<<'XX'
{varType string $a}
{$a}
{varType string $c}
{include test}
{define test}
{varType int $b}
{var $b = 5}
{$a}{$b}
{/define}

XX;

Assert::matchFile(
__DIR__ . '/expected/CoreMacros.varType.phtml',
$latte->compile($template)
);
7 changes: 7 additions & 0 deletions tests/Latte/expected/BlockMacros.define.args6.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
default values


a) Variables 1, 123, 10


b) Variables 1, 123, 10
49 changes: 49 additions & 0 deletions tests/Latte/expected/BlockMacros.define.args6.phtml
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
<?php
%A%
final class Template%a% extends Latte\Runtime\Template
{
protected const BLOCKS = [
['test' => 'blockTest'],
];


public function main(): array
{
extract($this->params);
echo 'default values
';
if ($this->getParentName()) {
return get_defined_vars();
}
echo '
a) ';
$this->renderBlock('test', [1] + [], 'html') /* line %d% */;
echo '
b) ';
$this->renderBlock('test', ['var1' => 1] + [], 'html') /* line %d% */;
return get_defined_vars();
}


/** {define test $var1 = 0, array $var2 = [1, 2, 3], int $var3 = 10} on line %d% */
public function blockTest(array $ʟ_args): void
{
extract($this->params);
$var1 = $ʟ_args[0] ?? $ʟ_args['var1'] ?? 0;
/** @var array $var2 */
$var2 = $ʟ_args[1] ?? $ʟ_args['var2'] ?? [1, 2, 3];
/** @var int $var3 */
$var3 = $ʟ_args[2] ?? $ʟ_args['var3'] ?? 10;
unset($ʟ_args);
echo ' Variables ';
echo LR\Filters::escapeHtmlText($var1) /* line %d% */;
echo ', ';
echo LR\Filters::escapeHtmlText(($this->filters->implode)($var2)) /* line %d% */;
echo ', ';
echo LR\Filters::escapeHtmlText($var3) /* line %d% */;
echo "\n";
}

}
16 changes: 16 additions & 0 deletions tests/Latte/expected/CoreMacros.templateType.80.phtml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?php
%A%
public function main(): array
{
extract($this->params);
/** @var mixed $a (ExampleTemplateType) */
/** @var int $b (ExampleTemplateType) */
/** @var ExampleTemplateType|int|null $c (ExampleTemplateType) */
%A%
public function blockTest(array $ʟ_args): void
{
extract($this->params);
/** @var mixed $a (ExampleTemplateType) */
/** @var int $b (ExampleTemplateType) */
/** @var ExampleTemplateType|int|null $c (ExampleTemplateType) */
%A%
16 changes: 16 additions & 0 deletions tests/Latte/expected/CoreMacros.templateType.phtml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?php
%A%
public function main(): array
{
extract($this->params);
/** @var mixed $a (ExampleTemplateType) */
/** @var int $b (ExampleTemplateType) */
/** @var ?ExampleTemplateType $c (ExampleTemplateType) */
%A%
public function blockTest(array $ʟ_args): void
{
extract($this->params);
/** @var mixed $a (ExampleTemplateType) */
/** @var int $b (ExampleTemplateType) */
/** @var ?ExampleTemplateType $c (ExampleTemplateType) */
%A%
Loading

0 comments on commit 9fe662e

Please sign in to comment.