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 #276

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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 @@ -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