Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Propagate variable types to generated code to allow statical analysis (master) #275

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
33bcc0b
opened 3.0-dev
dg Jul 16, 2019
80980c7
anonymous {block} create variable scope (possible BC break)
dg Feb 18, 2021
90950cd
added macro n:nonce
dg Jan 12, 2020
b0a436b
added {cycle}
dg Nov 4, 2020
67e124f
SnippetBridge: added typehints
dg Feb 20, 2019
163fa75
Template: removed old accumulators $_l, $_g (deprecated in 2.4, BC br…
dg Feb 18, 2021
0c208d5
removed aliases for old classes ILoader, IMacro, IHtmlString, IISnipp…
dg Nov 22, 2020
028d2b8
Filters::escapeJS: invalid UTF-8 is replaced with Unicode Replacement…
dg Nov 21, 2020
24d33f3
MacroTokens: correctly parses UTF-8 Combining character (TODO: normal…
dg Mar 19, 2020
6a4e283
optional chaining: removed support for deprecated syntax
dg Dec 3, 2020
29bee53
PhpWriter: supports named arguments in modifiers (in PHP 8)
dg Nov 2, 2020
9790492
Colons as argument separator in modifiers are deprecated everywhere (…
dg Nov 2, 2020
1dabdcf
Auto-empty is deprecated
dg May 25, 2020
3279000
Empty closing macro {/} is deprecated
dg Jun 15, 2020
814db41
phpdoc wip
dg Nov 15, 2020
6a67fe6
BlockMacros: {include} is not passing all local variables by default …
dg Jan 15, 2021
1031a05
compiler-related classes moved to namespace Latte\Compiler (BC break)
dg Nov 22, 2020
63998bd
RootNode
dg Nov 22, 2020
a575495
Filters: compatibility with JS binding II.
dg Mar 23, 2021
74c7abf
Propagate variable types to generated code to allow statical analysis
MartinMystikJonas Sep 27, 2021
a680f8a
Merge branch 'master' into types-for-static-analysis
MartinMystikJonas Oct 3, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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;
Copy link
Author

@MartinMystikJonas MartinMystikJonas Oct 3, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Default params extraction needs to be set in the beggining so macros can extend it with type information (prepend @var annotations)

$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)
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change is necessary to keep params extraction inside block same as in top-level. Previous version always extracted all params inside block even when {parameters} is used.

. ($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
2 changes: 1 addition & 1 deletion src/Latte/Runtime/Filters.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
class Filters
{
/** @var string @deprecated */
public static $dateFormat = 'j.�n.�Y';
public static $dateFormat = 'j. n. Y';

/** @var bool @internal use XHTML syntax? */
public static $xhtml = false;
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'));
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)
);
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";
}

}
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%