bug #4915 Fix the default filter fallback reusing a null-safe temporary variable (lazerg, fabpot)

This PR was merged into the 3.x branch.

Discussion
----------

Fix the default filter fallback reusing a null-safe temporary variable

The `default` filter reuses the fallback argument node in both branches of the conditional it compiles to. Because `GetAttrExpression` records the temporary variable it allocated for a null-safe chain on the node itself, the second compilation skipped the assignment and emitted a bare `$_vN` reference, so `{{ item?.label|default(item?.name) }}` warned about an undefined variable whenever `item` was null.

The fallback node is now cloned, like the node used for the defined test already is, so each branch compiles its own temporary.

Fixes #4914

Commits
-------

2d0c30b075 Remove redundant default filter cases
0b4199c522 Strengthen the default filter regression test
86c830ef45 Fix the default filter fallback reusing a null-safe temporary variable
This commit is contained in:
Fabien Potencier
2026-09-03 08:51:35 +02:00
3 changed files with 18 additions and 1 deletions
+1
View File
@@ -1,5 +1,6 @@
# 3.29.0 (2026-XX-XX)
* Fix the `default` filter fallback emitting an undefined variable warning when it uses the null-safe operator
* Fix the `matches` operator silently treating PCRE execution errors as non-matches
* Add the `HtmlExtension::htmlAttrValue()` method to resolve a single HTML attribute value the way the `html_attr` function renders it
* Fix `html_attr` JSON encoding a `Stringable` value in a `data-*` attribute instead of using its string representation
+1 -1
View File
@@ -55,7 +55,7 @@ class DefaultFilter extends FilterExpression
if ('default' === $name && ($node instanceof ContextVariable || $node instanceof GetAttrExpression)) {
$test = new DefinedTest(clone $node, new TwigTest('defined', null, ['always_allowed_in_sandbox' => true]), new EmptyNode(), $node->getTemplateLine());
$false = \count($arguments) ? $arguments->getNode('0') : new ConstantExpression('', $node->getTemplateLine());
$false = \count($arguments) ? clone $arguments->getNode('0') : new ConstantExpression('', $node->getTemplateLine());
$node = new ConditionalTernary($test, $default, $false, $node->getTemplateLine());
} else {
+16
View File
@@ -445,6 +445,22 @@ class ExpressionParserTest extends TestCase
['foo' => (object) ['bar' => (object) ['baz' => null]]],
'',
],
// default filter fallback
[
'{{ foo?.bar|default(foo?.baz) }}',
['foo' => null],
'',
],
[
'{{ foo?.bar|default(foo?.baz.qux) }}',
['foo' => null],
'',
],
[
'{% for foo in foos %}[{{ foo?.bar|default(foo?.baz) }}]{% endfor %}',
['foos' => [(object) ['bar' => 'corge', 'baz' => 'grault'], (object) ['baz' => 'qux']]],
'[corge][qux]',
],
];
}