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

Statical analysis for Latte [WIP] #297

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
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
4 changes: 4 additions & 0 deletions phpstan.neon
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
parameters:
excludePaths:
analyse:
- src/Latte/Compiler/TagParser.php

level: 5

paths:
Expand Down
1 change: 1 addition & 0 deletions src/Latte/Compiler/Block.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ final class Block

/** @var ParameterNode[] */
public array $parameters = [];
public VariableScope $variables;


public function __construct(
Expand Down
23 changes: 23 additions & 0 deletions src/Latte/Compiler/PrintContext.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}


Expand Down Expand Up @@ -160,9 +164,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];
Expand Down
76 changes: 66 additions & 10 deletions src/Latte/Compiler/TemplateGenerator.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@

use Latte;
use Latte\ContentType;
use Latte\Essential\Blueprint;
use Nette\PhpGenerator as Php;


/**
Expand Down Expand Up @@ -38,20 +40,24 @@ public function generate(
string $className,
?string $comment = null,
bool $strictMode = false,
array $filters = [],
): 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);
}
Expand All @@ -75,13 +81,18 @@ public function generate(
. ($method['body'] ? "\t\t$method[body]\n" : '') . "\t}";
}

$comment .= "\n@property Filters$className \$filters";
$comment = str_replace('*/', '* /', $comment);
$comment = str_replace("\n", "\n * ", "/**\n" . trim($comment)) . "\n */\n";

$code = "<?php\n\n"
. ($strictMode ? "declare(strict_types=1);\n\n" : '')
. "use Latte\\Runtime as LR;\n\n"
. ($comment === null ? '' : '/** ' . str_replace('*/', '* /', $comment) . " */\n")
. $comment
. "final class $className extends Latte\\Runtime\\Template\n{\n"
. implode("\n\n", $members)
. "\n}\n";
. "\n}\n\n\n"
. $this->generateStub($node, 'Filters' . $className, $filters);

$code = PhpHelpers::optimizeEcho($code);
$code = PhpHelpers::reformatCode($code);
Expand All @@ -100,7 +111,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;
Expand All @@ -121,16 +132,58 @@ private function generateBlocks(array $blocks, PrintContext $context): void
}


private function buildParams(string $body, array $params, string $cont, PrintContext $context): string
private function generateStub(Node $node, string $className, $filters): string
{
if (!class_exists(Php\ClassType::class)) {
return '';
}

$used = [];
(new NodeTraverser)->traverse($node, function (Node $node) use (&$used) {
if ($node instanceof Nodes\Php\FilterNode) {
$used[$node->name->name] = true;
}
});

$class = new Php\ClassType($className);
$filters = array_intersect_key($filters, $used);
foreach ($filters as $name => $callback) {
$func = (new Php\Factory)->fromCallable($callback);
$type = Blueprint::printType($func->getReturnType(), $func->isReturnNullable(), null) ?: 'mixed';
$params = [];
$list = $func->getParameters();
foreach ($list as $param) {
$variadic = $func->isVariadic() && $param === end($list);
$params[] = (Blueprint::printType($param->getType(), $param->isNullable(), null) ?: 'mixed')
. ($variadic ? '...' : '');
}

$class->addComment('@property callable(' . implode(', ', $params) . "): $type \$$name");
}

return (string) $class;
}


/**
* @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;
}

$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,
Expand All @@ -143,7 +196,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;
}


Expand Down
51 changes: 51 additions & 0 deletions src/Latte/Compiler/VariableScope.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
<?php

/**
* This file is part of the Latte (https://latte.nette.org)
* Copyright (c) 2008 David Grudl (https://davidgrudl.com)
*/

declare(strict_types=1);

namespace Latte\Compiler;

use Latte;


final class VariableScope
{
use Latte\Strict;

/** @var string[] */
public array $types = [];


public function addVariable(string $name, ?string $type): string
{
return $this->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";
}
}
1 change: 1 addition & 0 deletions src/Latte/Engine.php
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,7 @@ public function generate(TemplateNode $node, string $name): string
$this->getTemplateClass($name),
$comment,
$this->strictTypes,
$this->getFilters(),
);
}

Expand Down
4 changes: 2 additions & 2 deletions src/Latte/Essential/Blueprint.php
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ public function addFunctions(Php\ClassType $class, array $funcs): void
}


private function printType(?string $type, bool $nullable, ?Php\PhpNamespace $namespace): string
public static function printType(?string $type, bool $nullable, ?Php\PhpNamespace $namespace): string
{
if ($type === null) {
return '';
Expand Down Expand Up @@ -123,7 +123,7 @@ public function printParameters(
$list = $function->getParameters();
foreach ($list as $param) {
$variadic = $function->isVariadic() && $param === end($list);
$params[] = ltrim($this->printType($param->getType(), $param->isNullable(), $namespace) . ' ')
$params[] = ltrim(self::printType($param->getType(), $param->isNullable(), $namespace) . ' ')
. ($param->isReference() ? '&' : '')
. ($variadic ? '...' : '')
. '$' . $param->getName()
Expand Down
23 changes: 16 additions & 7 deletions src/Latte/Essential/Nodes/BlockNode.php
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
}


Expand All @@ -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 {
Expand All @@ -101,6 +109,7 @@ private function printFilter(PrintContext $context): string

XX,
$this->position,
$context->getVariableScope()->extractTypes(),
$this->content,
$context->getEscaper()->export(),
$this->modifier,
Expand Down
6 changes: 6 additions & 0 deletions src/Latte/Essential/Nodes/ForeachNode.php
Original file line number Diff line number Diff line change
Expand Up @@ -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));

Expand Down
28 changes: 26 additions & 2 deletions src/Latte/Essential/Nodes/TemplateTypeNode.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,26 +13,50 @@
use Latte\Compiler\Nodes\StatementNode;
use Latte\Compiler\PrintContext;
use Latte\Compiler\Tag;
use Latte\Compiler\Token;


/**
* {templateType ClassName}
*/
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;
}
}
Loading