Fix correctness visitor regressions

This commit is contained in:
Fabien Potencier
2026-06-04 13:09:00 +02:00
parent 89e8699a73
commit d96eac3895
9 changed files with 75 additions and 4 deletions
+1
View File
@@ -7,6 +7,7 @@
* Track the source offset of each token and expose it via `Token::getOffset()`
* Fix nested `block()` calls to resolve against the overriding template when a block rendered through `block(name, template)` calls `parent()`
* Deprecate the possibility to use a `block` tag within a capture node (like `set`)
* Deprecate the possibility to use a `block` tag within a capture node (like `set`) in child templates
* Deprecate using a `macro`, `extends`, or `use` tag outside the root of a template
* Stop reporting a skipped test in `IntegrationTestCase` when there is no legacy test to run
* Fix `markdown_to_html` to strip the indentation shared by all lines instead of mangling content that starts with a blank line
+4 -3
View File
@@ -302,9 +302,10 @@ Templates
instances of ``Twig\TemplateWrapper`` instead.
* Having a "block" definition nested in another node that captures the output
(like "set") is deprecated in Twig 3.14 and will throw in Twig 4.0. Such use
cases should be avoided as the "block" tag is used to both define the block
AND display it in place. Here is how you can decouple both easily:
(like "set") in a child template is deprecated in Twig 3.14 and will throw
in Twig 4.0. Such use cases should be avoided as the "block" tag is used to
both define the block AND display it in place. Here is how you can decouple
both:
Before::
@@ -20,6 +20,7 @@ use Twig\Node\MacroNode;
use Twig\Node\ModuleNode;
use Twig\Node\Node;
use Twig\Node\NodeCaptureInterface;
use Twig\Node\NodeOutputInterface;
use Twig\Node\Nodes;
use Twig\Node\TextNode;
@@ -145,6 +146,10 @@ final class CorrectnessNodeVisitor implements NodeVisitorInterface
return $node->isBlank();
}
if (!$node instanceof BlockReferenceNode && $node instanceof NodeOutputInterface) {
return false;
}
foreach ($node as $n) {
if (!$this->isEmptyOutputNode($n)) {
return false;
+10 -1
View File
@@ -12,6 +12,7 @@
namespace Twig\TokenParser;
use Twig\Error\SyntaxError;
use Twig\Node\ConfigNode;
use Twig\Node\Node;
use Twig\Token;
@@ -27,8 +28,16 @@ final class ExtendsTokenParser extends AbstractTokenParser
{
public function parse(Token $token): Node
{
$stream = $this->parser->getStream();
if ($this->parser->peekBlockStack()) {
throw new SyntaxError('Cannot use "extend" in a block.', $token->getLine(), $stream->getSourceContext());
} elseif (!$this->parser->isMainScope()) {
throw new SyntaxError('Cannot use "extend" in a macro.', $token->getLine(), $stream->getSourceContext());
}
$this->parser->setParent($this->parser->parseExpression());
$this->parser->getStream()->expect(Token::BLOCK_END_TYPE);
$stream->expect(Token::BLOCK_END_TYPE);
return new ConfigNode($token->getLine());
}
@@ -0,0 +1,17 @@
--TEST--
Exception for child templates printing content outside blocks defined by parent
--TEMPLATE--
{% extends 'base.twig' %}
{{ content }}
{% block sidebar %}
Content inside a block.
{% endblock %}
--TEMPLATE(base.twig)--
{% block sidebar %}
{% endblock %}
--DATA--
return ['content' => 'Content outside a block.'];
--EXCEPTION--
Twig\Error\SyntaxError: A template that extends another one cannot include content outside Twig blocks. Did you forget to put the content inside a {% block %} tag in "index.twig" at line 4?
@@ -0,0 +1,14 @@
--TEST--
"extends" tag in a block after a parent has been defined
--TEMPLATE--
{% extends "base.twig" %}
{% block foo %}
{% extends "foo.twig" %}
{% endblock %}
--TEMPLATE(base.twig)--
--TEMPLATE(foo.twig)--
--DATA--
return []
--EXCEPTION--
Twig\Error\SyntaxError: Cannot use "extend" in a block in "index.twig" at line 5.
@@ -0,0 +1,14 @@
--TEST--
"extends" tag in a macro after a parent has been defined
--TEMPLATE--
{% extends "base.twig" %}
{% macro foo() %}
{% extends "foo.twig" %}
{% endmacro %}
--TEMPLATE(base.twig)--
--TEMPLATE(foo.twig)--
--DATA--
return []
--EXCEPTION--
Twig\Error\SyntaxError: Cannot use "extend" in a macro in "index.twig" at line 5.
+2
View File
@@ -20,6 +20,7 @@ namespace Twig\Tests\Node;
* file that was distributed with this source code.
*/
use PHPUnit\Framework\Attributes\DataProvider;
use Twig\Node\TextNode;
use Twig\Test\NodeTestCase;
@@ -43,6 +44,7 @@ class TextTest extends NodeTestCase
/**
* @dataProvider getIsBlankData
*/
#[DataProvider('getIsBlankData')]
public function testIsBlank($blank)
{
$this->assertTrue((new TextNode($blank, 1))->isBlank());
+8
View File
@@ -11,6 +11,7 @@ namespace Twig\Tests\NodeVisitor;
* file that was distributed with this source code.
*/
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\TestCase;
use Twig\Environment;
use Twig\Error\SyntaxError;
@@ -18,9 +19,11 @@ use Twig\Loader\ArrayLoader;
use Twig\Node\BodyNode;
use Twig\Node\EmptyNode;
use Twig\Node\Expression\ConstantExpression;
use Twig\Node\IncludeNode;
use Twig\Node\ModuleNode;
use Twig\Node\Node;
use Twig\Node\Nodes;
use Twig\Node\PrintNode;
use Twig\Node\SetNode;
use Twig\Node\TextNode;
use Twig\NodeTraverser;
@@ -32,6 +35,7 @@ class CorrectnessTest extends TestCase
/**
* @dataProvider getFilterBodyNodesData
*/
#[DataProvider('getFilterBodyNodesData')]
public function testFilterBodyNodes($input, $expected)
{
$this->assertEquals($expected, $this->traverse($input, $expected));
@@ -54,6 +58,7 @@ class CorrectnessTest extends TestCase
/**
* @dataProvider getFilterBodyNodesDataThrowsException
*/
#[DataProvider('getFilterBodyNodesDataThrowsException')]
public function testFilterBodyNodesThrowsException($input)
{
$this->expectException(SyntaxError::class);
@@ -64,6 +69,8 @@ class CorrectnessTest extends TestCase
{
return [
[new TextNode('foo', 1)],
[new PrintNode(new ConstantExpression('foo', 1), 1)],
[new IncludeNode(new ConstantExpression('foo', 1), null, false, false, 1)],
[new Nodes([new Nodes([new TextNode('foo', 1)])])],
];
}
@@ -71,6 +78,7 @@ class CorrectnessTest extends TestCase
/**
* @dataProvider getFilterBodyNodesWithBOMData
*/
#[DataProvider('getFilterBodyNodesWithBOMData')]
public function testFilterBodyNodesWithBOM($emptyText)
{
$input = new TextNode(\chr(0xEF).\chr(0xBB).\chr(0xBF).$emptyText, 1);