Skip to content

Commit acd92c6

Browse files
committed
Propagate variable types to code to allow statical analysis [WIP]
Thanks to @MartinMystikJonas
1 parent 8742292 commit acd92c6

12 files changed

Lines changed: 233 additions & 45 deletions

File tree

src/Latte/Compiler/Block.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ final class Block
2626

2727
/** @var ParameterNode[] */
2828
public array $parameters = [];
29+
public VariableScope $variables;
2930

3031

3132
public function __construct(

src/Latte/Compiler/PrintContext.php

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,10 +77,14 @@ final class PrintContext
7777
/** @var Escaper[] */
7878
private array $escaperStack = [];
7979

80+
/** @var VariableScope[] */
81+
private array $scopeStack = [];
82+
8083

8184
public function __construct(string $contentType = ContentType::Html)
8285
{
8386
$this->escaperStack[] = new Escaper($contentType);
87+
$this->scopeStack[] = new VariableScope;
8488
}
8589

8690

@@ -159,9 +163,28 @@ public function getEscaper(): Escaper
159163
}
160164

161165

166+
public function beginVariableScope(): VariableScope
167+
{
168+
return $this->scopeStack[] = clone end($this->scopeStack);
169+
}
170+
171+
172+
public function restoreVariableScope(): void
173+
{
174+
array_pop($this->scopeStack);
175+
}
176+
177+
178+
public function getVariableScope(): VariableScope
179+
{
180+
return end($this->scopeStack);
181+
}
182+
183+
162184
public function addBlock(Block $block): void
163185
{
164186
$block->escaping = $this->getEscaper()->export();
187+
$block->variables = clone $this->getVariableScope();
165188
$block->method = 'block' . ucfirst(trim(preg_replace('#\W+#', '_', $block->name->print($this)), '_'));
166189
$lower = strtolower($block->method);
167190
$used = $this->blocks + ['block' => 1];

src/Latte/Compiler/TemplateGenerator.php

Lines changed: 24 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -40,18 +40,21 @@ public function generate(
4040
bool $strictMode = false,
4141
): string {
4242
$context = new PrintContext($node->contentType);
43-
$code = $node->main->print($context);
44-
$code = self::buildParams($code, [], '$ʟ_args', $context);
45-
$this->addMethod('main', $code, 'array $ʟ_args');
43+
$scope = $context->getVariableScope();
44+
$this->addMethod('main', '');
4645

4746
$head = (new NodeTraverser)->traverse($node->head, fn(Node $node) => $node instanceof Nodes\TextNode ? new Nodes\NopNode : $node);
4847
$code = $head->print($context);
4948
if ($code || $context->paramsExtraction) {
5049
$code .= 'return get_defined_vars();';
51-
$code = self::buildParams($code, $context->paramsExtraction, '$this->params', $context);
50+
$code = self::buildParams($code, $context->paramsExtraction, '$this->params', $context, $scope);
5251
$this->addMethod('prepare', $code, '', 'array');
5352
}
5453

54+
$code = $node->main->print($context);
55+
$code = self::buildParams($code, [], '$ʟ_args', $context, $context->getVariableScope());
56+
$this->addMethod('main', $code, 'array $ʟ_args');
57+
5558
if ($node->contentType !== ContentType::Html) {
5659
$this->addConstant('ContentType', $node->contentType);
5760
}
@@ -100,7 +103,7 @@ private function generateBlocks(array $blocks, PrintContext $context): void
100103
: [$block->method, $block->escaping];
101104
}
102105

103-
$body = $this->buildParams($block->content, $block->parameters, '$ʟ_args', $context);
106+
$body = self::buildParams($block->content, $block->parameters, '$ʟ_args', $context, $block->variables);
104107
if (!$block->isDynamic() && str_contains($body, '$')) {
105108
$embedded = $block->tag->name === 'block' && is_int($block->layer) && $block->layer;
106109
$body = 'extract(' . ($embedded ? 'end($this->varStack)' : '$this->params') . ');' . $body;
@@ -121,16 +124,25 @@ private function generateBlocks(array $blocks, PrintContext $context): void
121124
}
122125

123126

124-
private function buildParams(string $body, array $params, string $cont, PrintContext $context): string
125-
{
127+
/**
128+
* @param Nodes\Php\ParameterNode[] $params
129+
*/
130+
private static function buildParams(
131+
string $body,
132+
array $params,
133+
string $cont,
134+
PrintContext $context,
135+
VariableScope $scope,
136+
): string {
126137
if (!str_contains($body, '$') && !str_contains($body, 'get_defined_vars()')) {
127138
return $body;
128139
}
129140

130141
$res = [];
131142
foreach ($params as $i => $param) {
132143
$res[] = $context->format(
133-
'%node = %raw[%dump] ?? %raw[%dump] ?? %node;',
144+
'%raw%node = %raw[%dump] ?? %raw[%dump] ?? %node;',
145+
$param->type ? VariableScope::printComment($param->var->name, $param->type->type) . ' ' : '',
134146
$param->var,
135147
$cont,
136148
$i,
@@ -143,7 +155,10 @@ private function buildParams(string $body, array $params, string $cont, PrintCon
143155
$extract = $params
144156
? implode('', $res) . 'unset($ʟ_args);'
145157
: "extract($cont);" . (str_contains($cont, '$this') ? '' : "unset($cont);");
146-
return $extract . "\n\n" . $body;
158+
159+
return $extract . "\n"
160+
. $scope->extractTypes() . "\n\n"
161+
. $body;
147162
}
148163

149164

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
<?php
2+
3+
/**
4+
* This file is part of the Latte (https://latte.nette.org)
5+
* Copyright (c) 2008 David Grudl (https://davidgrudl.com)
6+
*/
7+
8+
declare(strict_types=1);
9+
10+
namespace Latte\Compiler;
11+
12+
use Latte;
13+
14+
15+
final class VariableScope
16+
{
17+
use Latte\Strict;
18+
19+
/** @var string[] */
20+
public array $types = [];
21+
22+
23+
public function addVariable(string $name, ?string $type): string
24+
{
25+
return $this->types[$name] = $this->printComment($name, $type);
26+
}
27+
28+
29+
public function addExpression(Nodes\Php\ExpressionNode $expr, ?Nodes\Php\SuperiorTypeNode $type): string
30+
{
31+
return $expr instanceof Nodes\Php\Expression\VariableNode && is_string($expr->name)
32+
? $this->addVariable($expr->name, $type?->type)
33+
: '';
34+
}
35+
36+
37+
public static function printComment(string $name, ?string $type): string
38+
{
39+
if (!$type) {
40+
return '';
41+
}
42+
$str = '@var ' . $type . ' $' . $name;
43+
return '/** ' . str_replace('*/', '* /', $str) . ' */';
44+
}
45+
46+
47+
public function extractTypes(): string
48+
{
49+
return implode('', $this->types) . "\n";
50+
}
51+
}

src/Latte/Essential/Nodes/BlockNode.php

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -74,14 +74,20 @@ public static function create(Tag $tag, TemplateParser $parser): \Generator
7474

7575
public function print(PrintContext $context): string
7676
{
77-
if (!$this->block) {
78-
return $this->printFilter($context);
77+
$context->beginVariableScope();
78+
try {
79+
if (!$this->block) {
80+
return $this->printFilter($context);
7981

80-
} elseif ($this->block->isDynamic()) {
81-
return $this->printDynamic($context);
82-
}
82+
} elseif ($this->block->isDynamic()) {
83+
return $this->printDynamic($context);
8384

84-
return $this->printStatic($context);
85+
} else {
86+
return $this->printStatic($context);
87+
}
88+
} finally {
89+
$context->restoreVariableScope();
90+
}
8591
}
8692

8793

@@ -91,7 +97,9 @@ private function printFilter(PrintContext $context): string
9197
<<<'XX'
9298
ob_start(fn() => '') %line;
9399
try {
94-
(function () { extract(func_get_arg(0));
100+
(function () {
101+
extract(func_get_arg(0));
102+
%raw
95103
%node
96104
})(get_defined_vars());
97105
} finally {
@@ -101,6 +109,7 @@ private function printFilter(PrintContext $context): string
101109

102110
XX,
103111
$this->position,
112+
$context->getVariableScope()->extractTypes(),
104113
$this->content,
105114
$context->getEscaper()->export(),
106115
$this->modifier,

src/Latte/Essential/Nodes/ForeachNode.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,12 @@ private static function parseArguments(TagParser $parser, self $node): void
8888

8989
public function print(PrintContext $context): string
9090
{
91+
$scope = $context->getVariableScope();
92+
if ($this->key) {
93+
$scope->addExpression($this->key, null);
94+
}
95+
$scope->addExpression($this->value, null);
96+
9197
$content = $this->content->print($context);
9298
$iterator = $this->else || ($this->iterator ?? preg_match('#\$iterator\W|\Wget_defined_vars\W#', $content));
9399

src/Latte/Essential/Nodes/TemplateTypeNode.php

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,26 +13,50 @@
1313
use Latte\Compiler\Nodes\StatementNode;
1414
use Latte\Compiler\PrintContext;
1515
use Latte\Compiler\Tag;
16+
use Latte\Compiler\Token;
1617

1718

1819
/**
1920
* {templateType ClassName}
2021
*/
2122
class TemplateTypeNode extends StatementNode
2223
{
24+
public string $class;
25+
26+
2327
public static function create(Tag $tag): static
2428
{
2529
if (!$tag->isInHead()) {
2630
throw new CompileException('{templateType} is allowed only in template header.', $tag->position);
2731
}
2832
$tag->expectArguments('class name');
29-
$tag->parser->parseExpression();
30-
return new static;
33+
$token = $tag->parser->stream->consume(Token::Php_Identifier, Token::Php_NameQualified, Token::Php_NameFullyQualified);
34+
if (!class_exists($token->text)) {
35+
throw new CompileException("Class '$token->text' used in {templateType} doesn't exist.", $token->position);
36+
}
37+
38+
$node = new static;
39+
$node->class = $token->text;
40+
return $node;
3141
}
3242

3343

3444
public function print(PrintContext $context): string
3545
{
46+
$scope = $context->getVariableScope();
47+
$rc = new \ReflectionClass($this->class);
48+
foreach ($rc->getProperties(\ReflectionProperty::IS_PUBLIC) as $property) {
49+
$type = $this->parseAnnotation($property->getDocComment() ?: '') ?: (string) $property->getType();
50+
$scope->addVariable($property->getName(), $type);
51+
}
52+
3653
return '';
3754
}
55+
56+
57+
private function parseAnnotation(string $comment): ?string
58+
{
59+
$comment = trim($comment, '/*');
60+
return preg_match('#@var ([^$]+)#', $comment, $m) ? trim($m[1]) : null;
61+
}
3862
}

src/Latte/Essential/Nodes/VarNode.php

Lines changed: 22 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
use Latte\Compiler\Nodes\Php\Expression\VariableNode;
1414
use Latte\Compiler\Nodes\Php\ExpressionNode;
1515
use Latte\Compiler\Nodes\Php\Scalar\NullNode;
16+
use Latte\Compiler\Nodes\Php\SuperiorTypeNode;
1617
use Latte\Compiler\Nodes\StatementNode;
1718
use Latte\Compiler\PrintContext;
1819
use Latte\Compiler\Tag;
@@ -27,7 +28,7 @@ class VarNode extends StatementNode
2728
{
2829
public bool $default;
2930

30-
/** @var AssignNode[] */
31+
/** @var array{AssignNode, ?SuperiorTypeNode}[] */
3132
public array $assignments = [];
3233

3334

@@ -46,14 +47,14 @@ private static function parseAssignments(Tag $tag, bool $default): array
4647
$stream = $tag->parser->stream;
4748
$res = [];
4849
do {
49-
$tag->parser->parseType();
50+
$type = $tag->parser->parseType();
5051

5152
$save = $stream->getIndex();
5253
$expr = $stream->is(Token::Php_Variable) ? $tag->parser->parseExpression() : null;
5354
if ($expr instanceof VariableNode) {
54-
$res[] = new AssignNode($expr, new NullNode);
55+
$res[] = [new AssignNode($expr, new NullNode), $type];
5556
} elseif ($expr instanceof AssignNode && (!$default || $expr->var instanceof VariableNode)) {
56-
$res[] = $expr;
57+
$res[] = [$expr, $type];
5758
} else {
5859
$stream->seek($save);
5960
$stream->throwUnexpectedException(addendum: ' in ' . $tag->getNotation());
@@ -66,27 +67,29 @@ private static function parseAssignments(Tag $tag, bool $default): array
6667

6768
public function print(PrintContext $context): string
6869
{
69-
$res = [];
70+
$scope = $context->getVariableScope();
71+
$res = $types = [];
72+
7073
if ($this->default) {
71-
foreach ($this->assignments as $assign) {
72-
assert($assign->var instanceof VariableNode);
73-
if ($assign->var->name instanceof ExpressionNode) {
74-
$var = $assign->var->name->print($context);
75-
} else {
76-
$var = $context->encodeString($assign->var->name);
77-
}
74+
foreach ($this->assignments as [$assign, $type]) {
75+
$var = $assign->var->name instanceof ExpressionNode
76+
? $assign->var->name->print($context)
77+
: $context->encodeString($assign->var->name);
7878
$res[] = $var . ' => ' . $assign->expr->print($context);
79+
$types[] = $scope->addExpression($var, $type);
7980
}
8081

8182
return $context->format(
82-
'extract([%raw], EXTR_SKIP) %line;',
83+
'extract([%raw], EXTR_SKIP) %line;%raw ',
8384
implode(', ', $res),
8485
$this->position,
86+
implode('', $types),
8587
);
8688
}
8789

88-
foreach ($this->assignments as $assign) {
89-
$res[] = $assign->print($context);
90+
foreach ($this->assignments as [$assign, $type]) {
91+
$comment = $scope->addExpression($assign->var, $type);
92+
$res[] = $comment . $assign->print($context);
9093
}
9194

9295
return $context->format(
@@ -99,8 +102,11 @@ public function print(PrintContext $context): string
99102

100103
public function &getIterator(): \Generator
101104
{
102-
foreach ($this->assignments as &$assign) {
105+
foreach ($this->assignments as [&$assign, &$type]) {
103106
yield $assign;
107+
if ($type) {
108+
yield $type;
109+
}
104110
}
105111
}
106112
}

0 commit comments

Comments
 (0)