Skip to content

Commit

Permalink
feature #4402 Rework macros handling (fabpot)
Browse files Browse the repository at this point in the history
This PR was squashed before being merged into the 3.x branch.

Discussion
----------

Rework macros handling

Commits
-------

f1481be Remove MacroAutoImportNodeVisitor
7269388 Simplify code
1105964 Rework macros handling
  • Loading branch information
fabpot committed Oct 22, 2024
2 parents bcceaa3 + f1481be commit af40907
Show file tree
Hide file tree
Showing 21 changed files with 317 additions and 208 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# 3.15.0 (2024-XX-XX)

* Remove `MacroAutoImportNodeVisitor`
* Deprecate `MethodCallExpression` in favor of `MacroReferenceExpression`
* Fix support for the "is defined" test on `_self.xxx` (auto-imported) macros
* Fix support for the "is defined" test on inherited macros
* Add named arguments support for the dot operator arguments (`foo.bar(some: arg)`)
* Add named arguments support for macros
* Add a new `guard` tag that allows to test if some Twig callables are available at compilation time
Expand Down
3 changes: 3 additions & 0 deletions doc/deprecated.rst
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,9 @@ Nodes
deprecated as of Twig 3.12: ``arguments``, ``callable``, ``is_variadic``,
and ``dynamic_name``.

* The ``MethodCallExpression`` class is deprecated as of Twig 3.15, use
``MacroReferenceExpression`` instead.

Node Visitors
-------------

Expand Down
178 changes: 94 additions & 84 deletions src/ExpressionParser.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
use Twig\Node\Expression\ConditionalExpression;
use Twig\Node\Expression\ConstantExpression;
use Twig\Node\Expression\GetAttrExpression;
use Twig\Node\Expression\MethodCallExpression;
use Twig\Node\Expression\MacroReferenceExpression;
use Twig\Node\Expression\NameExpression;
use Twig\Node\Expression\TempNameExpression;
use Twig\Node\Expression\TestExpression;
Expand All @@ -33,6 +33,7 @@
use Twig\Node\Expression\Unary\NotUnary;
use Twig\Node\Expression\Unary\PosUnary;
use Twig\Node\Expression\Unary\SpreadUnary;
use Twig\Node\Expression\Variable\TemplateVariable;
use Twig\Node\Node;
use Twig\Node\Nodes;

Expand Down Expand Up @@ -531,11 +532,7 @@ public function parsePostfixExpression($node)
public function getFunctionNode($name, $line)
{
if (null !== $alias = $this->parser->getImportedSymbol('function', $name)) {
$arguments = $this->createArguments($line);
$node = new MethodCallExpression($alias['node'], $alias['name'], $arguments, $line);
$node->setAttribute('safe', true);

return $node;
return new MacroReferenceExpression(new TemplateVariable($alias['node'], $line), $alias['name'], $this->createArguments($line), $line);
}

$args = $this->parseOnlyArguments();
Expand All @@ -561,84 +558,11 @@ public function getFunctionNode($name, $line)

public function parseSubscriptExpression($node)
{
$stream = $this->parser->getStream();
$token = $stream->next();
$lineno = $token->getLine();
$arguments = new ArrayExpression([], $lineno);
$type = Template::ANY_CALL;
if ('.' == $token->getValue()) {
if ($stream->nextIf(Token::PUNCTUATION_TYPE, '(')) {
$arg = $this->parseExpression();
$stream->expect(Token::PUNCTUATION_TYPE, ')');
if ($stream->test(Token::PUNCTUATION_TYPE, '(')) {
$type = Template::METHOD_CALL;
$arguments = $this->createArguments($lineno);
}

return new GetAttrExpression($node, $arg, $arguments, $type, $lineno);
}
$token = $stream->next();
if (
Token::NAME_TYPE == $token->getType()
||
Token::NUMBER_TYPE == $token->getType()
||
(Token::OPERATOR_TYPE == $token->getType() && preg_match(Lexer::REGEX_NAME, $token->getValue()))
) {
$arg = new ConstantExpression($token->getValue(), $lineno);

if ($stream->test(Token::PUNCTUATION_TYPE, '(')) {
$type = Template::METHOD_CALL;
$arguments = $this->createArguments($lineno);
}
} else {
throw new SyntaxError(\sprintf('Expected name or number, got value "%s" of type %s.', $token->getValue(), Token::typeToEnglish($token->getType())), $lineno, $stream->getSourceContext());
}

if ($node instanceof NameExpression && null !== $this->parser->getImportedSymbol('template', $node->getAttribute('name'))) {
$name = $arg->getAttribute('value');

$node = new MethodCallExpression($node, 'macro_'.$name, $arguments, $lineno);
$node->setAttribute('safe', true);

return $node;
}
} else {
$type = Template::ARRAY_CALL;

// slice?
$slice = false;
if ($stream->test(Token::PUNCTUATION_TYPE, ':')) {
$slice = true;
$arg = new ConstantExpression(0, $token->getLine());
} else {
$arg = $this->parseExpression();
}

if ($stream->nextIf(Token::PUNCTUATION_TYPE, ':')) {
$slice = true;
}

if ($slice) {
if ($stream->test(Token::PUNCTUATION_TYPE, ']')) {
$length = new ConstantExpression(null, $token->getLine());
} else {
$length = $this->parseExpression();
}

$filter = $this->getFilter('slice', $token->getLine());
$arguments = new Nodes([$arg, $length]);
$filter = new ($filter->getNodeClass())($node, $filter, $arguments, $token->getLine());

$stream->expect(Token::PUNCTUATION_TYPE, ']');

return $filter;
}

$stream->expect(Token::PUNCTUATION_TYPE, ']');
if ('.' === $this->parser->getStream()->next()->getValue()) {
return $this->parseSubscriptExpressionDot($node);
}

return new GetAttrExpression($node, $arg, $arguments, $type, $lineno);
return $this->parseSubscriptExpressionArray($node);
}

public function parseFilterExpression($node)
Expand Down Expand Up @@ -825,8 +749,7 @@ private function parseTestExpression(Node $node): TestExpression
}

if ('defined' === $test->getName() && $node instanceof NameExpression && null !== $alias = $this->parser->getImportedSymbol('function', $node->getAttribute('name'))) {
$node = new MethodCallExpression($alias['node'], $alias['name'], new ArrayExpression([], $node->getTemplateLine()), $node->getTemplateLine());
$node->setAttribute('safe', true);
$node = new MacroReferenceExpression(new TemplateVariable($alias['node'], $node->getTemplateLine()), $alias['name'], new ArrayExpression([], $node->getTemplateLine()), $node->getTemplateLine());
}

$ready = $test instanceof TwigTest;
Expand Down Expand Up @@ -997,4 +920,91 @@ public function parseOnlyArguments()

return new Nodes($args);
}

private function parseSubscriptExpressionDot(Node $node): AbstractExpression
{
$stream = $this->parser->getStream();
$token = $stream->getCurrent();
$lineno = $token->getLine();
$arguments = new ArrayExpression([], $lineno);
$type = Template::ANY_CALL;

if ($stream->nextIf(Token::PUNCTUATION_TYPE, '(')) {
$attribute = $this->parseExpression();
$stream->expect(Token::PUNCTUATION_TYPE, ')');
} else {
$token = $stream->next();
if (
Token::NAME_TYPE == $token->getType()
||
Token::NUMBER_TYPE == $token->getType()
||
(Token::OPERATOR_TYPE == $token->getType() && preg_match(Lexer::REGEX_NAME, $token->getValue()))
) {
$attribute = new ConstantExpression($token->getValue(), $token->getLine());
} else {
throw new SyntaxError(\sprintf('Expected name or number, got value "%s" of type %s.', $token->getValue(), Token::typeToEnglish($token->getType())), $token->getLine(), $stream->getSourceContext());
}
}

if ($stream->test(Token::PUNCTUATION_TYPE, '(')) {
$type = Template::METHOD_CALL;
$arguments = $this->createArguments($token->getLine());
}

if (
$node instanceof NameExpression
&&
(
null !== $this->parser->getImportedSymbol('template', $node->getAttribute('name'))
||
'_self' === $node->getAttribute('name') && $attribute instanceof ConstantExpression
)
) {
return new MacroReferenceExpression(new TemplateVariable($node->getAttribute('name'), $node->getTemplateLine()), 'macro_'.$attribute->getAttribute('value'), $arguments, $node->getTemplateLine());
}

return new GetAttrExpression($node, $attribute, $arguments, $type, $lineno);
}

private function parseSubscriptExpressionArray(Node $node): AbstractExpression
{
$stream = $this->parser->getStream();
$token = $stream->getCurrent();
$lineno = $token->getLine();
$arguments = new ArrayExpression([], $lineno);

// slice?
$slice = false;
if ($stream->test(Token::PUNCTUATION_TYPE, ':')) {
$slice = true;
$attribute = new ConstantExpression(0, $token->getLine());
} else {
$attribute = $this->parseExpression();
}

if ($stream->nextIf(Token::PUNCTUATION_TYPE, ':')) {
$slice = true;
}

if ($slice) {
if ($stream->test(Token::PUNCTUATION_TYPE, ']')) {
$length = new ConstantExpression(null, $token->getLine());
} else {
$length = $this->parseExpression();
}

$filter = $this->getFilter('slice', $token->getLine());
$arguments = new Nodes([$attribute, $length]);
$filter = new ($filter->getNodeClass())($node, $filter, $arguments, $token->getLine());

$stream->expect(Token::PUNCTUATION_TYPE, ']');

return $filter;
}

$stream->expect(Token::PUNCTUATION_TYPE, ']');

return new GetAttrExpression($node, $attribute, $arguments, Template::ARRAY_CALL, $lineno);
}
}
5 changes: 3 additions & 2 deletions src/Extension/CoreExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,6 @@
use Twig\Node\Expression\Unary\NotUnary;
use Twig\Node\Expression\Unary\PosUnary;
use Twig\Node\Node;
use Twig\NodeVisitor\MacroAutoImportNodeVisitor;
use Twig\OperatorPrecedenceChange;
use Twig\Parser;
use Twig\Source;
Expand Down Expand Up @@ -293,7 +292,7 @@ public function getTests(): array

public function getNodeVisitors(): array
{
return [new MacroAutoImportNodeVisitor()];
return [];
}

public function getOperators(): array
Expand Down Expand Up @@ -1286,6 +1285,8 @@ public static function capitalize(string $charset, $string): string

/**
* @internal
*
* to be removed in 4.0
*/
public static function callMacro(Template $template, string $method, array $args, int $lineno, array $context, Source $source)
{
Expand Down
56 changes: 56 additions & 0 deletions src/Node/Expression/MacroReferenceExpression.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
<?php

/*
* This file is part of Twig.
*
* (c) Fabien Potencier
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Twig\Node\Expression;

use Twig\Compiler;
use Twig\Node\Expression\Variable\TemplateVariable;

/**
* Represents a macro call node.
*
* @author Fabien Potencier <[email protected]>
*/
class MacroReferenceExpression extends AbstractExpression
{
public function __construct(TemplateVariable $template, string $name, AbstractExpression $arguments, int $lineno)
{
parent::__construct(['template' => $template, 'arguments' => $arguments], ['name' => $name, 'is_defined_test' => false], $lineno);
}

public function compile(Compiler $compiler): void
{
if ($this->getAttribute('is_defined_test')) {
$compiler
->subcompile($this->getNode('template'))
->raw('->hasMacro(')
->repr($this->getAttribute('name'))
->raw(', $context')
->raw(')')
;

return;
}

$compiler
->subcompile($this->getNode('template'))
->raw('->getTemplateForMacro(')
->repr($this->getAttribute('name'))
->raw(', $context, ')
->repr($this->getTemplateLine())
->raw(', $this->getSourceContext())')
->raw(\sprintf('->%s', $this->getAttribute('name')))
->raw('(...')
->subcompile($this->getNode('arguments'))
->raw(')')
;
}
}
2 changes: 2 additions & 0 deletions src/Node/Expression/MethodCallExpression.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ class MethodCallExpression extends AbstractExpression
{
public function __construct(AbstractExpression $node, string $method, ArrayExpression $arguments, int $lineno)
{
trigger_deprecation('twig/twig', '3.15', 'The "%s" class is deprecated, use "%s" instead.', __CLASS__, MacroReferenceExpression::class);

parent::__construct(['node' => $node, 'arguments' => $arguments], ['method' => $method, 'safe' => false, 'is_defined_test' => false], $lineno);

if ($node instanceof NameExpression) {
Expand Down
3 changes: 3 additions & 0 deletions src/Node/Expression/Test/DefinedTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
use Twig\Node\Expression\ConstantExpression;
use Twig\Node\Expression\FunctionExpression;
use Twig\Node\Expression\GetAttrExpression;
use Twig\Node\Expression\MacroReferenceExpression;
use Twig\Node\Expression\MethodCallExpression;
use Twig\Node\Expression\NameExpression;
use Twig\Node\Expression\TestExpression;
Expand Down Expand Up @@ -55,6 +56,8 @@ public function __construct(Node $node, TwigTest|string $name, ?Node $arguments,
$this->changeIgnoreStrictCheck($node);
} elseif ($node instanceof BlockReferenceExpression) {
$node->setAttribute('is_defined_test', true);
} elseif ($node instanceof MacroReferenceExpression) {
$node->setAttribute('is_defined_test', true);
} elseif ($node instanceof FunctionExpression && 'constant' === $node->getAttribute('name')) {
$node->setAttribute('is_defined_test', true);
} elseif ($node instanceof ConstantExpression || $node instanceof ArrayExpression) {
Expand Down
31 changes: 31 additions & 0 deletions src/Node/Expression/Variable/TemplateVariable.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?php

/*
* This file is part of Twig.
*
* (c) Fabien Potencier
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Twig\Node\Expression\Variable;

use Twig\Compiler;
use Twig\Node\Expression\NameExpression;

final class TemplateVariable extends NameExpression
{
public function compile(Compiler $compiler): void
{
if ('_self' === $this->getAttribute('name')) {
$compiler->raw('$this');
} else {
$compiler
->raw('$macros[')
->string($this->getAttribute('name'))
->raw(']')
;
}
}
}
Loading

0 comments on commit af40907

Please sign in to comment.