Skip the string cast in PrintNode when the expression is already a string and add tests

This commit is contained in:
Fabien Potencier
2026-06-02 22:51:56 +02:00
parent 86840f9351
commit 89f886e324
6 changed files with 75 additions and 12 deletions
+5 -5
View File
@@ -73,7 +73,7 @@ class ForTest extends NodeTestCase
\$context['_parent'] = \$context;
\$context['_seq'] = CoreExtension::ensureTraversable($itemsGetter);
foreach (\$context['_seq'] as \$context["key"] => \$context["item"]) {
yield $fooGetter;
yield (string) $fooGetter;
}
\$_parent = \$context['_parent'];
unset(\$context['_seq'], \$context['key'], \$context['item'], \$context['_parent']);
@@ -107,7 +107,7 @@ if (is_array(\$context['_seq']) || (is_object(\$context['_seq']) && \$context['_
\$context['loop']['last'] = 1 === \$length;
}
foreach (\$context['_seq'] as \$context["k"] => \$context["v"]) {
yield $fooGetter;
yield (string) $fooGetter;
++\$context['loop']['index0'];
++\$context['loop']['index'];
\$context['loop']['first'] = false;
@@ -149,7 +149,7 @@ if (is_array(\$context['_seq']) || (is_object(\$context['_seq']) && \$context['_
\$context['loop']['last'] = 1 === \$length;
}
foreach (\$context['_seq'] as \$context["k"] => \$context["v"]) {
yield $fooGetter;
yield (string) $fooGetter;
++\$context['loop']['index0'];
++\$context['loop']['index'];
\$context['loop']['first'] = false;
@@ -192,7 +192,7 @@ if (is_array(\$context['_seq']) || (is_object(\$context['_seq']) && \$context['_
\$context['loop']['last'] = 1 === \$length;
}
foreach (\$context['_seq'] as \$context["k"] => \$context["v"]) {
yield $fooGetter;
yield (string) $fooGetter;
\$context['_iterated'] = true;
++\$context['loop']['index0'];
++\$context['loop']['index'];
@@ -206,7 +206,7 @@ foreach (\$context['_seq'] as \$context["k"] => \$context["v"]) {
// line 5
if (!\$context['_iterated']) {
// line 6
yield $fooGetter;
yield (string) $fooGetter;
}
\$_parent = \$context['_parent'];
unset(\$context['_seq'], \$context['k'], \$context['v'], \$context['_parent'], \$context['_iterated'], \$context['loop']);
+5 -5
View File
@@ -63,7 +63,7 @@ class IfTest extends NodeTestCase
$tests[] = [$node, <<<EOF
// line 1
if (true) {
yield $fooGetter;
yield (string) $fooGetter;
}
EOF
];
@@ -80,9 +80,9 @@ EOF
$tests[] = [$node, <<<EOF
// line 1
if (true) {
yield $fooGetter;
yield (string) $fooGetter;
} elseif (false) {
yield $barGetter;
yield (string) $barGetter;
}
EOF
];
@@ -97,9 +97,9 @@ EOF
$tests[] = [$node, <<<EOF
// line 1
if (true) {
yield $fooGetter;
yield (string) $fooGetter;
} else {
yield $barGetter;
yield (string) $barGetter;
}
EOF
];
+13
View File
@@ -20,6 +20,7 @@ namespace Twig\Tests\Node;
* file that was distributed with this source code.
*/
use Twig\Node\Expression\Binary\ConcatBinary;
use Twig\Node\Expression\ConstantExpression;
use Twig\Node\Expression\GetAttrExpression;
use Twig\Node\Expression\Variable\ContextVariable;
@@ -40,8 +41,20 @@ class PrintTest extends NodeTestCase
public static function provideTests(): iterable
{
$tests = [];
// a string literal is known to be a string, so the cast is skipped
$tests[] = [new PrintNode(new ConstantExpression('foo', 1), 1), "// line 1\nyield \"foo\";"];
// a non-string expression is cast to make the conversion happen here for a useful stack trace
$tests[] = [new PrintNode(new ContextVariable('foo', 1), 1), "// line 1\nyield (string) (\$context[\"foo\"] ?? null);"];
// a non-string constant is cast as well
$tests[] = [new PrintNode(new ConstantExpression(42, 1), 1), "// line 1\nyield (string) 42;"];
// a concatenation always returns a string, so the cast is skipped
$concat = new ConcatBinary(new ContextVariable('foo', 1), new ContextVariable('bar', 1), 1);
$tests[] = [new PrintNode($concat, 1), "// line 1\nyield ((\$context[\"foo\"] ?? null) . (\$context[\"bar\"] ?? null));"];
$expr = new ContextVariable('foo', 1);
$attr = new ConstantExpression('bar', 1);
$node = new GetAttrExpression($expr, $attr, null, Template::METHOD_CALL, 1);
+29
View File
@@ -376,6 +376,35 @@ class TemplateTest extends TestCase
$this->assertNull(CoreExtension::getAttribute($twig, $template->getSourceContext(), $object, 'foo'));
}
/**
* @dataProvider provideNonStringPrintValues
*/
public function testPrintingANonStringReportsTheErrorAtThePrintLocation($value, string $expectedMessage)
{
$twig = new Environment(new ArrayLoader(['index' => "foo\n{{ value }}\nbar"]));
set_error_handler(static function (int $type, string $msg, string $file, int $line): bool {
throw new \ErrorException($msg, 0, $type, $file, $line);
}, \E_WARNING);
try {
$twig->render('index', ['value' => $value]);
$this->fail('Printing a non-string value should fail.');
} catch (RuntimeError $e) {
$this->assertSame('index', $e->getSourceContext()->getName());
$this->assertSame(2, $e->getTemplateLine());
$this->assertSame($expectedMessage, $e->getPrevious()->getMessage());
} finally {
restore_error_handler();
}
}
public static function provideNonStringPrintValues(): iterable
{
yield 'array' => [['a', 'b'], 'Array to string conversion'];
yield 'non-Stringable object' => [new \stdClass(), 'Object of class stdClass could not be converted to string'];
}
public static function getGetAttributeTests()
{
$array = [