diff --git a/src/Latte/Compiler/Block.php b/src/Latte/Compiler/Block.php index 6dc9a61cb6..39995882da 100644 --- a/src/Latte/Compiler/Block.php +++ b/src/Latte/Compiler/Block.php @@ -26,6 +26,7 @@ final class Block /** @var ParameterNode[] */ public array $parameters = []; + public VariableScope $variables; public function __construct( diff --git a/src/Latte/Compiler/PrintContext.php b/src/Latte/Compiler/PrintContext.php index 0a8e502272..c27d71d16f 100644 --- a/src/Latte/Compiler/PrintContext.php +++ b/src/Latte/Compiler/PrintContext.php @@ -77,10 +77,14 @@ final class PrintContext /** @var Escaper[] */ private array $escaperStack = []; + /** @var VariableScope[] */ + private array $scopeStack = []; + public function __construct(string $contentType = ContentType::Html) { $this->escaperStack[] = new Escaper($contentType); + $this->scopeStack[] = new VariableScope; } @@ -159,9 +163,28 @@ public function getEscaper(): Escaper } + public function beginVariableScope(): VariableScope + { + return $this->scopeStack[] = clone end($this->scopeStack); + } + + + public function restoreVariableScope(): void + { + array_pop($this->scopeStack); + } + + + public function getVariableScope(): VariableScope + { + return end($this->scopeStack); + } + + public function addBlock(Block $block): void { $block->escaping = $this->getEscaper()->export(); + $block->variables = clone $this->getVariableScope(); $block->method = 'block' . ucfirst(trim(preg_replace('#\W+#', '_', $block->name->print($this)), '_')); $lower = strtolower($block->method); $used = $this->blocks + ['block' => 1]; diff --git a/src/Latte/Compiler/TemplateGenerator.php b/src/Latte/Compiler/TemplateGenerator.php index 11a319fa6d..4f754892c8 100644 --- a/src/Latte/Compiler/TemplateGenerator.php +++ b/src/Latte/Compiler/TemplateGenerator.php @@ -40,18 +40,21 @@ public function generate( bool $strictMode = false, ): string { $context = new PrintContext($node->contentType); - $code = $node->main->print($context); - $code = self::buildParams($code, [], '$ʟ_args', $context); - $this->addMethod('main', $code, 'array $ʟ_args'); + $scope = $context->getVariableScope(); + $this->addMethod('main', ''); $head = (new NodeTraverser)->traverse($node->head, fn(Node $node) => $node instanceof Nodes\TextNode ? new Nodes\NopNode : $node); $code = $head->print($context); if ($code || $context->paramsExtraction) { $code .= 'return get_defined_vars();'; - $code = self::buildParams($code, $context->paramsExtraction, '$this->params', $context); + $code = self::buildParams($code, $context->paramsExtraction, '$this->params', $context, $scope); $this->addMethod('prepare', $code, '', 'array'); } + $code = $node->main->print($context); + $code = self::buildParams($code, [], '$ʟ_args', $context, $context->getVariableScope()); + $this->addMethod('main', $code, 'array $ʟ_args'); + if ($node->contentType !== ContentType::Html) { $this->addConstant('ContentType', $node->contentType); } @@ -100,7 +103,7 @@ private function generateBlocks(array $blocks, PrintContext $context): void : [$block->method, $block->escaping]; } - $body = $this->buildParams($block->content, $block->parameters, '$ʟ_args', $context); + $body = self::buildParams($block->content, $block->parameters, '$ʟ_args', $context, $block->variables); if (!$block->isDynamic() && str_contains($body, '$')) { $embedded = $block->tag->name === 'block' && is_int($block->layer) && $block->layer; $body = 'extract(' . ($embedded ? 'end($this->varStack)' : '$this->params') . ');' . $body; @@ -121,8 +124,16 @@ private function generateBlocks(array $blocks, PrintContext $context): void } - private function buildParams(string $body, array $params, string $cont, PrintContext $context): string - { + /** + * @param Nodes\Php\ParameterNode[] $params + */ + private static function buildParams( + string $body, + array $params, + string $cont, + PrintContext $context, + VariableScope $scope, + ): string { if (!str_contains($body, '$') && !str_contains($body, 'get_defined_vars()')) { return $body; } @@ -130,7 +141,8 @@ private function buildParams(string $body, array $params, string $cont, PrintCon $res = []; foreach ($params as $i => $param) { $res[] = $context->format( - '%node = %raw[%dump] ?? %raw[%dump] ?? %node;', + '%raw%node = %raw[%dump] ?? %raw[%dump] ?? %node;', + $param->type ? VariableScope::printComment($param->var->name, $param->type->type) . ' ' : '', $param->var, $cont, $i, @@ -143,7 +155,10 @@ private function buildParams(string $body, array $params, string $cont, PrintCon $extract = $params ? implode('', $res) . 'unset($ʟ_args);' : "extract($cont);" . (str_contains($cont, '$this') ? '' : "unset($cont);"); - return $extract . "\n\n" . $body; + + return $extract . "\n" + . $scope->extractTypes() . "\n\n" + . $body; } diff --git a/src/Latte/Compiler/VariableScope.php b/src/Latte/Compiler/VariableScope.php new file mode 100644 index 0000000000..0d77ec94c1 --- /dev/null +++ b/src/Latte/Compiler/VariableScope.php @@ -0,0 +1,51 @@ +types[$name] = $this->printComment($name, $type); + } + + + public function addExpression(Nodes\Php\ExpressionNode $expr, ?Nodes\Php\SuperiorTypeNode $type): string + { + return $expr instanceof Nodes\Php\Expression\VariableNode && is_string($expr->name) + ? $this->addVariable($expr->name, $type?->type) + : ''; + } + + + public static function printComment(string $name, ?string $type): string + { + if (!$type) { + return ''; + } + $str = '@var ' . $type . ' $' . $name; + return '/** ' . str_replace('*/', '* /', $str) . ' */'; + } + + + public function extractTypes(): string + { + return implode('', $this->types) . "\n"; + } +} diff --git a/src/Latte/Essential/Nodes/BlockNode.php b/src/Latte/Essential/Nodes/BlockNode.php index d1a982cc3b..acfea4c439 100644 --- a/src/Latte/Essential/Nodes/BlockNode.php +++ b/src/Latte/Essential/Nodes/BlockNode.php @@ -74,14 +74,20 @@ public static function create(Tag $tag, TemplateParser $parser): \Generator public function print(PrintContext $context): string { - if (!$this->block) { - return $this->printFilter($context); + $context->beginVariableScope(); + try { + if (!$this->block) { + return $this->printFilter($context); - } elseif ($this->block->isDynamic()) { - return $this->printDynamic($context); - } + } elseif ($this->block->isDynamic()) { + return $this->printDynamic($context); - return $this->printStatic($context); + } else { + return $this->printStatic($context); + } + } finally { + $context->restoreVariableScope(); + } } @@ -91,7 +97,9 @@ private function printFilter(PrintContext $context): string <<<'XX' ob_start(fn() => '') %line; try { - (function () { extract(func_get_arg(0)); + (function () { + extract(func_get_arg(0)); + %raw %node })(get_defined_vars()); } finally { @@ -101,6 +109,7 @@ private function printFilter(PrintContext $context): string XX, $this->position, + $context->getVariableScope()->extractTypes(), $this->content, $context->getEscaper()->export(), $this->modifier, diff --git a/src/Latte/Essential/Nodes/ForeachNode.php b/src/Latte/Essential/Nodes/ForeachNode.php index 338a896463..b3c3f888e7 100644 --- a/src/Latte/Essential/Nodes/ForeachNode.php +++ b/src/Latte/Essential/Nodes/ForeachNode.php @@ -88,6 +88,12 @@ private static function parseArguments(TagParser $parser, self $node): void public function print(PrintContext $context): string { + $scope = $context->getVariableScope(); + if ($this->key) { + $scope->addExpression($this->key, null); + } + $scope->addExpression($this->value, null); + $content = $this->content->print($context); $iterator = $this->else || ($this->iterator ?? preg_match('#\$iterator\W|\Wget_defined_vars\W#', $content)); diff --git a/src/Latte/Essential/Nodes/TemplateTypeNode.php b/src/Latte/Essential/Nodes/TemplateTypeNode.php index aa3fb4678b..03298864b2 100644 --- a/src/Latte/Essential/Nodes/TemplateTypeNode.php +++ b/src/Latte/Essential/Nodes/TemplateTypeNode.php @@ -13,6 +13,7 @@ use Latte\Compiler\Nodes\StatementNode; use Latte\Compiler\PrintContext; use Latte\Compiler\Tag; +use Latte\Compiler\Token; /** @@ -20,19 +21,42 @@ */ class TemplateTypeNode extends StatementNode { + public string $class; + + public static function create(Tag $tag): static { if (!$tag->isInHead()) { throw new CompileException('{templateType} is allowed only in template header.', $tag->position); } $tag->expectArguments('class name'); - $tag->parser->parseExpression(); - return new static; + $token = $tag->parser->stream->consume(Token::Php_Identifier, Token::Php_NameQualified, Token::Php_NameFullyQualified); + if (!class_exists($token->text)) { + throw new CompileException("Class '$token->text' used in {templateType} doesn't exist.", $token->position); + } + + $node = new static; + $node->class = $token->text; + return $node; } public function print(PrintContext $context): string { + $scope = $context->getVariableScope(); + $rc = new \ReflectionClass($this->class); + foreach ($rc->getProperties(\ReflectionProperty::IS_PUBLIC) as $property) { + $type = $this->parseAnnotation($property->getDocComment() ?: '') ?: (string) $property->getType(); + $scope->addVariable($property->getName(), $type); + } + return ''; } + + + private function parseAnnotation(string $comment): ?string + { + $comment = trim($comment, '/*'); + return preg_match('#@var ([^$]+)#', $comment, $m) ? trim($m[1]) : null; + } } diff --git a/src/Latte/Essential/Nodes/VarNode.php b/src/Latte/Essential/Nodes/VarNode.php index c3eea61f3f..8b7561a370 100644 --- a/src/Latte/Essential/Nodes/VarNode.php +++ b/src/Latte/Essential/Nodes/VarNode.php @@ -13,6 +13,7 @@ use Latte\Compiler\Nodes\Php\Expression\VariableNode; use Latte\Compiler\Nodes\Php\ExpressionNode; use Latte\Compiler\Nodes\Php\Scalar\NullNode; +use Latte\Compiler\Nodes\Php\SuperiorTypeNode; use Latte\Compiler\Nodes\StatementNode; use Latte\Compiler\PrintContext; use Latte\Compiler\Tag; @@ -27,7 +28,7 @@ class VarNode extends StatementNode { public bool $default; - /** @var AssignNode[] */ + /** @var array{AssignNode, ?SuperiorTypeNode}[] */ public array $assignments = []; @@ -46,14 +47,14 @@ private static function parseAssignments(Tag $tag, bool $default): array $stream = $tag->parser->stream; $res = []; do { - $tag->parser->parseType(); + $type = $tag->parser->parseType(); $save = $stream->getIndex(); $expr = $stream->is(Token::Php_Variable) ? $tag->parser->parseExpression() : null; if ($expr instanceof VariableNode) { - $res[] = new AssignNode($expr, new NullNode); + $res[] = [new AssignNode($expr, new NullNode), $type]; } elseif ($expr instanceof AssignNode && (!$default || $expr->var instanceof VariableNode)) { - $res[] = $expr; + $res[] = [$expr, $type]; } else { $stream->seek($save); $stream->throwUnexpectedException(addendum: ' in ' . $tag->getNotation()); @@ -66,27 +67,29 @@ private static function parseAssignments(Tag $tag, bool $default): array public function print(PrintContext $context): string { - $res = []; + $scope = $context->getVariableScope(); + $res = $types = []; + if ($this->default) { - foreach ($this->assignments as $assign) { - assert($assign->var instanceof VariableNode); - if ($assign->var->name instanceof ExpressionNode) { - $var = $assign->var->name->print($context); - } else { - $var = $context->encodeString($assign->var->name); - } + foreach ($this->assignments as [$assign, $type]) { + $var = $assign->var->name instanceof ExpressionNode + ? $assign->var->name->print($context) + : $context->encodeString($assign->var->name); $res[] = $var . ' => ' . $assign->expr->print($context); + $types[] = $scope->addExpression($var, $type); } return $context->format( - 'extract([%raw], EXTR_SKIP) %line;', + 'extract([%raw], EXTR_SKIP) %line;%raw ', implode(', ', $res), $this->position, + implode('', $types), ); } - foreach ($this->assignments as $assign) { - $res[] = $assign->print($context); + foreach ($this->assignments as [$assign, $type]) { + $comment = $scope->addExpression($assign->var, $type); + $res[] = $comment . $assign->print($context); } return $context->format( @@ -99,8 +102,11 @@ public function print(PrintContext $context): string public function &getIterator(): \Generator { - foreach ($this->assignments as &$assign) { + foreach ($this->assignments as [&$assign, &$type]) { yield $assign; + if ($type) { + yield $type; + } } } } diff --git a/src/Latte/Essential/Nodes/VarTypeNode.php b/src/Latte/Essential/Nodes/VarTypeNode.php index 2585a513b3..dde40d006a 100644 --- a/src/Latte/Essential/Nodes/VarTypeNode.php +++ b/src/Latte/Essential/Nodes/VarTypeNode.php @@ -9,6 +9,8 @@ namespace Latte\Essential\Nodes; +use Latte\Compiler\Nodes\Php\Expression\VariableNode; +use Latte\Compiler\Nodes\Php\SuperiorTypeNode; use Latte\Compiler\Nodes\StatementNode; use Latte\Compiler\PrintContext; use Latte\Compiler\Tag; @@ -20,17 +22,36 @@ */ class VarTypeNode extends StatementNode { + public VariableNode $variable; + public SuperiorTypeNode $type; + + public static function create(Tag $tag): static { $tag->expectArguments(); - $tag->parser->parseType(); - $tag->parser->stream->consume(Token::Php_Variable); - return new static; + $type = $tag->parser->parseType(); + if (!$type) { + $tag->parser->stream->throwUnexpectedException(); + } + $token = $tag->parser->stream->consume(Token::Php_Variable); + + $node = new static; + $node->type = $type; + $node->variable = new VariableNode(substr($token->text, 1)); + return $node; } public function print(PrintContext $context): string { - return ''; + $scope = $context->getVariableScope(); + return $scope->addExpression($this->variable, $this->type) . "\n"; + } + + + public function &getIterator(): \Generator + { + yield $this->variable; + yield $this->type; } } diff --git a/tests/tags/templateType.phpt b/tests/tags/templateType.phpt index 847cb8e799..5b7789ab31 100644 --- a/tests/tags/templateType.phpt +++ b/tests/tags/templateType.phpt @@ -11,6 +11,17 @@ use Tester\Assert; require __DIR__ . '/../bootstrap.php'; +class TemplateClass +{ + public $noType; + public int $intType; + public int|bool $intBoolType; + /** @var array */ + public array $arrayType; + private int $private; +} + + $latte = new Latte\Engine; $latte->setLoader(new Latte\Loaders\StringLoader); @@ -20,10 +31,19 @@ Assert::exception( 'Missing class name in {templateType} (at column 1)', ); +Assert::exception( + fn() => $latte->compile('{templateType AA\BBB}'), + Latte\CompileException::class, + "Class 'AA\BBB' used in {templateType} doesn't exist (at column 15)", +); + Assert::exception( fn() => $latte->compile('{if true}{templateType stdClass}{/if}'), Latte\CompileException::class, '{templateType} is allowed only in template header (at column 10)', ); -Assert::noError(fn() => $latte->compile('{templateType stdClass}')); +Assert::contains( + '/** @var int $intType *//** @var int|bool $intBoolType *//** @var array $arrayType */', + $latte->compile('{templateType TemplateClass}'), +); diff --git a/tests/tags/var.default.phpt b/tests/tags/var.default.phpt index 627dd6392d..648813f01e 100644 --- a/tests/tags/var.default.phpt +++ b/tests/tags/var.default.phpt @@ -25,7 +25,7 @@ test('{var ...}', function () use ($latte) { // types Assert::contains('$temp->var1 = 123 /*', $latte->compile('{var int $temp->var1 = 123}')); Assert::contains('$temp->var1 = 123 /*', $latte->compile('{var null|int|string[] $temp->var1 = 123}')); - Assert::contains('$var1 = 123; $var2 = \'nette framework\' /*', ws($latte->compile('{var int|string[] $var1 = 123, ?class $var2 = "nette framework"}'))); + Assert::contains('/** @var int|string[] $var1 */ $var1 = 123; /** @var ?class $var2 */ $var2 = \'nette framework\' /* line 1 */;', ws($latte->compile('{var int|string[] $var1 = 123, ?class $var2 = "nette framework"}'))); Assert::contains('$var1 = 123; $var2 = 456 /*', ws($latte->compile('{var A\B $var1 = 123, ?A\B $var2 = 456}'))); Assert::contains('$var1 = 123; $var2 = 456 /*', ws($latte->compile('{var \A\B $var1 = 123, ?\A\B $var2 = 456}'))); diff --git a/tests/tags/varType.phpt b/tests/tags/varType.phpt index a3306d9c26..8c32741ccd 100644 --- a/tests/tags/varType.phpt +++ b/tests/tags/varType.phpt @@ -35,13 +35,25 @@ Assert::exception( Assert::exception( fn() => $latte->compile('{varType $var type}'), Latte\CompileException::class, - "Unexpected 'type', expecting end of tag in {varType} (at column 15)", + "Unexpected '\$vartype' (at column 10)", ); -Assert::noError(fn() => $latte->compile('{varType type $var}')); +Assert::contains( + '/** @var type $var */', + $latte->compile('{varType type $var}'), +); -Assert::noError(fn() => $latte->compile('{varType ?\Nm\Class $var}')); +Assert::contains( + '/** @var ?\Nm\Class $var */', + $latte->compile('{varType ?\Nm\Class $var}'), +); -Assert::noError(fn() => $latte->compile('{varType int|null $var}')); +Assert::contains( + '/** @var int|null $var */', + $latte->compile('{varType int|null $var}'), +); -Assert::noError(fn() => $latte->compile('{varType array{0: int, 1: int} $var}')); +Assert::contains( + '/** @var array{0:int,1:int} $var */', + $latte->compile('{varType array{0: int, 1: int} $var}'), +);