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 Sep 27, 2021
1 parent a575495 commit 2cf67a0
Show file tree
Hide file tree
Showing 6 changed files with 76 additions and 13 deletions.
10 changes: 7 additions & 3 deletions src/Latte/Compiler/Compiler.php
Original file line number Diff line number Diff line change
Expand Up @@ -57,9 +57,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 @@ -174,7 +177,8 @@ private function buildClassBody(array $tokens): string
$this->output = &$output;
$this->inHead = true;
$this->macroNode = new RootNode;
$this->htmlNode = $this->context = $this->paramsExtraction = null;
$this->htmlNode = $this->context = null;
$this->paramsExtraction = $this->defaultParamsExtraction;
$this->placeholders = $this->properties = $this->constants = [];
$this->methods = ['main' => null, 'prepare' => null];

Expand Down Expand Up @@ -223,7 +227,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 @@ -325,15 +325,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 @@ -547,7 +552,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
22 changes: 17 additions & 5 deletions src/Latte/Macros/CoreMacros.php
Original file line number Diff line number Diff line change
Expand Up @@ -819,15 +819,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 @@ -844,7 +849,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 @@ -853,10 +858,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 . $this->getCompiler()->paramsExtraction;
return "";
} else {
return $writer->write($comment);
}
}


Expand Down
7 changes: 6 additions & 1 deletion tests/Latte/CoreMacros.parameters.phpt
Original file line number Diff line number Diff line change
Expand Up @@ -19,17 +19,22 @@ $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}',

'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() ?? "-"}',
]));


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::contains('/** @var int $a */', $latte->compile('inc3.latte'));
Assert::contains('/** @var ?\Exception $glob */', $latte->compile('inc6.latte'));
22 changes: 22 additions & 0 deletions tests/Latte/CoreMacros.varType.phpt
Original file line number Diff line number Diff line change
Expand Up @@ -46,3 +46,25 @@ 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}
{include test}
{define test}
{varType int $b}
{var $b = 5}
{$a}{$b}
{/define}

XX;

Assert::matchFile(
__DIR__ . '/expected/CoreMacros.varType.phtml',
$latte->compile($template)
);
15 changes: 15 additions & 0 deletions tests/Latte/expected/CoreMacros.varType.phtml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?php
%A%
public function main(): array
{
/** @var string $a */
extract($this->params);
%A%
public function blockTest(array $ʟ_args): void
{
/** @var string $a */
extract($this->params);
%A%
/** @var int $b */
$b = 5%a%;
%A%

0 comments on commit 2cf67a0

Please sign in to comment.