mirror of
https://github.com/twigphp/Twig.git
synced 2026-10-02 18:07:35 +00:00
Report the macro call parentheses deprecation once per call site and name the macro
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
# 3.29.1 (2026-XX-XX)
|
||||
|
||||
* n/a
|
||||
* Fix the deprecation about omitting parentheses when calling a macro being reported twice for the same call
|
||||
* Add the macro name to the deprecation about omitting parentheses when calling a macro
|
||||
|
||||
# 3.29.0 (2026-09-18)
|
||||
|
||||
|
||||
@@ -33,7 +33,10 @@ use Twig\Node\TextNode;
|
||||
final class CorrectnessNodeVisitor implements NodeVisitorInterface
|
||||
{
|
||||
private ?\WeakMap $rootNodes = null;
|
||||
private ?\WeakMap $checkedMacroReferences = null;
|
||||
/**
|
||||
* @var array<string, true>
|
||||
*/
|
||||
private array $reportedMacroCallSites = [];
|
||||
/**
|
||||
* Stack of the output-wrapping tags ("if", "for", "set", ...) currently open;
|
||||
* the top one is the nearest tag a "block" definition would be nested under.
|
||||
@@ -94,7 +97,6 @@ final class CorrectnessNodeVisitor implements NodeVisitorInterface
|
||||
{
|
||||
$this->resetState();
|
||||
$this->rootNodes = new \WeakMap();
|
||||
$this->checkedMacroReferences = new \WeakMap();
|
||||
$this->hasParent = $node->hasNode('parent');
|
||||
|
||||
foreach ($this->getRootNodes($node) as $n) {
|
||||
@@ -108,7 +110,7 @@ final class CorrectnessNodeVisitor implements NodeVisitorInterface
|
||||
private function resetState(): void
|
||||
{
|
||||
$this->rootNodes = null;
|
||||
$this->checkedMacroReferences = null;
|
||||
$this->reportedMacroCallSites = [];
|
||||
$this->tagStack = [];
|
||||
$this->hasParent = false;
|
||||
$this->blockDepth = 0;
|
||||
@@ -162,13 +164,27 @@ final class CorrectnessNodeVisitor implements NodeVisitorInterface
|
||||
|
||||
private function checkMacroCallParentheses(MacroReferenceExpression $node): void
|
||||
{
|
||||
if (isset($this->checkedMacroReferences[$node])) {
|
||||
if ($node->hasCallParentheses() || $node->isDefinedTestEnabled()) {
|
||||
return;
|
||||
}
|
||||
$this->checkedMacroReferences[$node] = true;
|
||||
|
||||
if (false === $node->hasCallParentheses() && !$node->isDefinedTestEnabled()) {
|
||||
trigger_deprecation('twig/twig', '3.29', 'Omitting parentheses when calling a macro is deprecated and will throw a SyntaxError in Twig 4.0; add parentheses after the macro name in "%s" at line %d.', $node->getSourceContext()->getName(), $node->getTemplateLine());
|
||||
$sourceName = $node->getSourceContext()->getName();
|
||||
$line = $node->getTemplateLine();
|
||||
// A dynamic macro name is only known at runtime.
|
||||
$name = $node->getAttribute('name');
|
||||
|
||||
// A single call site can be visited more than once: "??" reuses its left node,
|
||||
// while "?:" and the "default" filter put a clone of it in the compiled tree.
|
||||
$callSite = $sourceName."\0".$line."\0".$name;
|
||||
if (isset($this->reportedMacroCallSites[$callSite])) {
|
||||
return;
|
||||
}
|
||||
$this->reportedMacroCallSites[$callSite] = true;
|
||||
|
||||
if (null === $name) {
|
||||
trigger_deprecation('twig/twig', '3.29', 'Omitting parentheses when calling a macro is deprecated and will throw a SyntaxError in Twig 4.0; add parentheses after the macro name in "%s" at line %d.', $sourceName, $line);
|
||||
} else {
|
||||
trigger_deprecation('twig/twig', '3.29', 'Omitting parentheses when calling the macro "%s" is deprecated and will throw a SyntaxError in Twig 4.0; add parentheses after the macro name in "%s" at line %d.', $name, $sourceName, $line);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
--TEST--
|
||||
Omitting parentheses when calling macros is deprecated
|
||||
--DEPRECATION--
|
||||
Since twig/twig 3.29: Omitting parentheses when calling a macro is deprecated and will throw a SyntaxError in Twig 4.0; add parentheses after the macro name in "index.twig" at line 4.
|
||||
Since twig/twig 3.29: Omitting parentheses when calling the macro "hello" is deprecated and will throw a SyntaxError in Twig 4.0; add parentheses after the macro name in "index.twig" at line 4.
|
||||
Since twig/twig 3.29: Omitting parentheses when calling a macro is deprecated and will throw a SyntaxError in Twig 4.0; add parentheses after the macro name in "index.twig" at line 5.
|
||||
Since twig/twig 3.29: Omitting parentheses when calling a macro is deprecated and will throw a SyntaxError in Twig 4.0; add parentheses after the macro name in "index.twig" at line 6.
|
||||
Since twig/twig 3.29: Omitting parentheses when calling the macro "hello" is deprecated and will throw a SyntaxError in Twig 4.0; add parentheses after the macro name in "index.twig" at line 6.
|
||||
--TEMPLATE--
|
||||
{% import _self as macros %}
|
||||
{% set name = 'hello' %}
|
||||
|
||||
+136
-7
@@ -165,11 +165,13 @@ EOF, 'index')));
|
||||
*/
|
||||
#[DataProvider('provideMacroTargetExpressionsWithoutParentheses')]
|
||||
#[Group('legacy')]
|
||||
public function testMacroTargetsWithoutParenthesesAreDeprecated(string $expression): void
|
||||
public function testMacroTargetsWithoutParenthesesAreDeprecated(string $expression, ?string $macroName): void
|
||||
{
|
||||
$twig = new Environment(new ArrayLoader());
|
||||
|
||||
$this->expectDeprecation('Since twig/twig 3.29: Omitting parentheses when calling a macro is deprecated and will throw a SyntaxError in Twig 4.0; add parentheses after the macro name in "index" at line 1.');
|
||||
$this->expectDeprecation(null === $macroName
|
||||
? 'Since twig/twig 3.29: Omitting parentheses when calling a macro is deprecated and will throw a SyntaxError in Twig 4.0; add parentheses after the macro name in "index" at line 1.'
|
||||
: \sprintf('Since twig/twig 3.29: Omitting parentheses when calling the macro "%s" is deprecated and will throw a SyntaxError in Twig 4.0; add parentheses after the macro name in "index" at line 1.', $macroName));
|
||||
|
||||
$module = $twig->parse($twig->tokenize(new Source("{% import _self as macros %}{{ $expression }}", 'index')));
|
||||
$macroReferences = [];
|
||||
@@ -184,18 +186,120 @@ EOF, 'index')));
|
||||
public static function provideMacroTargetExpressionsWithoutParentheses(): iterable
|
||||
{
|
||||
foreach (['_self', 'macros'] as $target) {
|
||||
yield $target.' static without parentheses' => [$target.'.foo'];
|
||||
yield $target.' grouped static without parentheses' => ['('.$target.'.foo)'];
|
||||
yield $target.' dynamic without parentheses' => [$target.'.(name)'];
|
||||
yield $target.' grouped dynamic without parentheses' => ['('.$target.'.(name))'];
|
||||
yield $target.' static without parentheses' => [$target.'.foo', 'foo'];
|
||||
yield $target.' grouped static without parentheses' => ['('.$target.'.foo)', 'foo'];
|
||||
yield $target.' dynamic without parentheses' => [$target.'.(name)', null];
|
||||
yield $target.' grouped dynamic without parentheses' => ['('.$target.'.(name))', null];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider provideExpressionsReusingAMacroReference
|
||||
*
|
||||
* @group legacy
|
||||
*/
|
||||
#[DataProvider('provideExpressionsReusingAMacroReference')]
|
||||
#[Group('legacy')]
|
||||
public function testAMacroCallWithoutParenthesesIsReportedOncePerCallSite(string $expression): void
|
||||
{
|
||||
$twig = new Environment(new ArrayLoader());
|
||||
$source = new Source("{% import _self as macros %}{{ $expression }}", 'index');
|
||||
|
||||
$deprecations = $this->collectDeprecations(static function () use ($twig, $source) {
|
||||
$twig->parse($twig->tokenize($source));
|
||||
});
|
||||
|
||||
$this->assertSame([
|
||||
'Since twig/twig 3.29: Omitting parentheses when calling the macro "foo" is deprecated and will throw a SyntaxError in Twig 4.0; add parentheses after the macro name in "index" at line 1.',
|
||||
], $deprecations);
|
||||
}
|
||||
|
||||
public static function provideExpressionsReusingAMacroReference(): iterable
|
||||
{
|
||||
yield 'null-coalescing operator' => ['macros.foo ?? "x"'];
|
||||
yield 'elvis operator' => ['macros.foo ?: "x"'];
|
||||
yield 'default filter' => ['name|default(macros.foo)'];
|
||||
}
|
||||
|
||||
/**
|
||||
* @group legacy
|
||||
*/
|
||||
#[Group('legacy')]
|
||||
public function testEachMacroCallWithoutParenthesesIsReportedOnItsOwnLine(): void
|
||||
{
|
||||
$twig = new Environment(new ArrayLoader());
|
||||
$source = new Source("{% import _self as macros %}{{ macros.foo ?: 'x' }}\n{{ macros.foo ?: 'x' }}", 'index');
|
||||
|
||||
$deprecations = $this->collectDeprecations(static function () use ($twig, $source) {
|
||||
$twig->parse($twig->tokenize($source));
|
||||
});
|
||||
|
||||
$this->assertSame([
|
||||
'Since twig/twig 3.29: Omitting parentheses when calling the macro "foo" is deprecated and will throw a SyntaxError in Twig 4.0; add parentheses after the macro name in "index" at line 1.',
|
||||
'Since twig/twig 3.29: Omitting parentheses when calling the macro "foo" is deprecated and will throw a SyntaxError in Twig 4.0; add parentheses after the macro name in "index" at line 2.',
|
||||
], $deprecations);
|
||||
}
|
||||
|
||||
/**
|
||||
* @group legacy
|
||||
*/
|
||||
#[Group('legacy')]
|
||||
public function testDistinctMacroCallsWithoutParenthesesOnTheSameLineAreBothReported(): void
|
||||
{
|
||||
$twig = new Environment(new ArrayLoader());
|
||||
$source = new Source('{% import _self as macros %}{{ macros.foo }}{{ macros.bar }}', 'index');
|
||||
|
||||
$deprecations = $this->collectDeprecations(static function () use ($twig, $source) {
|
||||
$twig->parse($twig->tokenize($source));
|
||||
});
|
||||
|
||||
$this->assertSame([
|
||||
'Since twig/twig 3.29: Omitting parentheses when calling the macro "foo" is deprecated and will throw a SyntaxError in Twig 4.0; add parentheses after the macro name in "index" at line 1.',
|
||||
'Since twig/twig 3.29: Omitting parentheses when calling the macro "bar" is deprecated and will throw a SyntaxError in Twig 4.0; add parentheses after the macro name in "index" at line 1.',
|
||||
], $deprecations);
|
||||
}
|
||||
|
||||
/**
|
||||
* @group legacy
|
||||
*/
|
||||
#[Group('legacy')]
|
||||
public function testADynamicMacroCallWithoutParenthesesIsReportedWithoutAName(): void
|
||||
{
|
||||
$twig = new Environment(new ArrayLoader());
|
||||
$source = new Source('{% import _self as macros %}{{ macros.(name) ?: "x" }}', 'index');
|
||||
|
||||
$deprecations = $this->collectDeprecations(static function () use ($twig, $source) {
|
||||
$twig->parse($twig->tokenize($source));
|
||||
});
|
||||
|
||||
$this->assertSame([
|
||||
'Since twig/twig 3.29: Omitting parentheses when calling a macro is deprecated and will throw a SyntaxError in Twig 4.0; add parentheses after the macro name in "index" at line 1.',
|
||||
], $deprecations);
|
||||
}
|
||||
|
||||
/**
|
||||
* @group legacy
|
||||
*/
|
||||
#[Group('legacy')]
|
||||
public function testAMacroCallWithParenthesesDoesNotSilenceOneWithoutOnTheSameLine(): void
|
||||
{
|
||||
$twig = new Environment(new ArrayLoader());
|
||||
$source = new Source('{% import _self as macros %}{{ macros.foo() }}{{ macros.foo }}', 'index');
|
||||
|
||||
$deprecations = $this->collectDeprecations(static function () use ($twig, $source) {
|
||||
$twig->parse($twig->tokenize($source));
|
||||
});
|
||||
|
||||
$this->assertSame([
|
||||
'Since twig/twig 3.29: Omitting parentheses when calling the macro "foo" is deprecated and will throw a SyntaxError in Twig 4.0; add parentheses after the macro name in "index" at line 1.',
|
||||
], $deprecations);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider provideMacroTargetExpressionsWithoutParentheses
|
||||
*/
|
||||
#[DataProvider('provideMacroTargetExpressionsWithoutParentheses')]
|
||||
public function testMacroTargetsWithoutParenthesesAreAllowedInDefinedTest(string $expression): void
|
||||
public function testMacroTargetsWithoutParenthesesAreAllowedInDefinedTest(string $expression, ?string $macroName): void
|
||||
{
|
||||
$twig = new Environment(new ArrayLoader());
|
||||
|
||||
@@ -533,6 +637,31 @@ EOF, 'index')));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<string>
|
||||
*/
|
||||
private function collectDeprecations(callable $fn): array
|
||||
{
|
||||
$deprecations = [];
|
||||
set_error_handler(static function ($type, $message) use (&$deprecations) {
|
||||
if (\E_USER_DEPRECATED === $type) {
|
||||
$deprecations[] = $message;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
try {
|
||||
$fn();
|
||||
} finally {
|
||||
restore_error_handler();
|
||||
}
|
||||
|
||||
return $deprecations;
|
||||
}
|
||||
|
||||
protected function getParser()
|
||||
{
|
||||
$parser = new Parser(new Environment(new ArrayLoader()));
|
||||
|
||||
Reference in New Issue
Block a user