Remove the documentation comments compilation overhead

This commit is contained in:
Fabien Potencier
2026-08-27 18:42:36 +02:00
parent 0c9d0c77c0
commit 60eed4ccd5
7 changed files with 66 additions and 92 deletions
+6 -3
View File
@@ -175,13 +175,16 @@ Reading Documentation from Nodes
Node visitors can read documentation with ``Node::getDocumentation()``. Twig
automatically attaches documentation before a custom tag to the node returned
by its token parser. To support inline documentation, custom token parsers can
pass the corresponding tokens to ``NodeDocumentation::add()``.
by its token parser. If that node is a placeholder, the token parser can call
``Parser::setDocumentationTarget()`` once while parsing the tag to select the
semantic node instead. To support inline documentation, custom token parsers
can pass the corresponding tokens to ``NodeDocumentation::add()``.
The metadata is stored on the semantic node represented by the source:
* output documentation is stored on the ``PrintNode``;
* tag documentation is stored on the node produced by the tag;
* tag documentation is stored on the node produced by the tag or the target
selected by its token parser;
* block documentation is stored on the ``BlockNode``;
* macro documentation is stored on the ``MacroNode``;
* type documentation is stored on each ``TypeNode``;
-2
View File
@@ -87,7 +87,6 @@ use Twig\Node\Expression\Unary\PosUnary;
use Twig\Node\Expression\Unary\SpreadUnary;
use Twig\Node\Node;
use Twig\NodeVisitor\CorrectnessNodeVisitor;
use Twig\NodeVisitor\DocumentationNodeVisitor;
use Twig\Parser;
use Twig\Sandbox\SecurityNotAllowedMethodError;
use Twig\Sandbox\SecurityNotAllowedPropertyError;
@@ -341,7 +340,6 @@ final class CoreExtension extends AbstractExtension
public function getNodeVisitors(): array
{
return [
new DocumentationNodeVisitor(),
new CorrectnessNodeVisitor(),
];
}
@@ -1,75 +0,0 @@
<?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\NodeVisitor;
use Twig\Environment;
use Twig\Node\BlockReferenceNode;
use Twig\Node\MacroDeclarationNode;
use Twig\Node\ModuleNode;
use Twig\Node\Node;
use Twig\Node\NodeDocumentation;
/**
* @internal
*/
final class DocumentationNodeVisitor implements NodeVisitorInterface
{
/** @var list<ModuleNode> */
private array $modules = [];
/** @var list<array<string, MacroDeclarationNode>> */
private array $macroDeclarations = [];
public function enterNode(Node $node, Environment $env): Node
{
if ($node instanceof ModuleNode) {
$this->modules[] = $node;
$this->macroDeclarations[] = [];
}
$module = end($this->modules);
if ($node instanceof BlockReferenceNode && $module->getNode('blocks')->hasNode($name = $node->getAttribute('name'))) {
NodeDocumentation::move($node, $module->getNode('blocks')->getNode($name)->getNode('0'));
}
if ($node instanceof MacroDeclarationNode && $module->getNode('macros')->hasNode($name = $node->getAttribute('name'))) {
$macro = $module->getNode('macros')->getNode($name);
$index = array_key_last($this->macroDeclarations);
if ($node->getTemplateLine() === $macro->getTemplateLine()) {
if (isset($this->macroDeclarations[$index][$name])) {
$this->macroDeclarations[$index][$name]->setDocumentation(null);
}
$this->macroDeclarations[$index][$name] = $node;
} else {
$node->setDocumentation(null);
}
}
return $node;
}
public function leaveNode(Node $node, Environment $env): Node
{
if ($node instanceof ModuleNode) {
foreach (array_pop($this->macroDeclarations) as $name => $declaration) {
NodeDocumentation::move($declaration, $node->getNode('macros')->getNode($name));
}
array_pop($this->modules);
}
return $node;
}
public function getPriority(): int
{
return -512;
}
}
+26 -10
View File
@@ -50,6 +50,8 @@ class Parser
private $expressionParser;
private $blocks;
private $blockStack;
/** @var list<Node|null> */
private array $documentationTargets = [];
private $macros;
private $importedSymbols;
private $traits;
@@ -99,6 +101,7 @@ class Parser
$this->stream = $stream;
$this->parent = null;
$this->blocks = [];
$this->documentationTargets = [];
$this->macros = [];
$this->traits = [];
$this->blockStack = [];
@@ -229,12 +232,22 @@ class Parser
$this->stream->next();
$subparser->setParser($this);
$node = $subparser->parse($token);
$documentationTargetIndex = \count($this->documentationTargets);
$this->documentationTargets[] = null;
try {
$node = $subparser->parse($token);
$documentationTarget = $this->documentationTargets[$documentationTargetIndex];
} finally {
array_pop($this->documentationTargets);
}
if (!$node) {
trigger_deprecation('twig/twig', '3.12', 'Returning "null" from "%s" is deprecated and forbidden by "TokenParserInterface".', $subparser::class);
} else {
$node->setNodeTag($subparser->getTag());
NodeDocumentation::add($node, $startToken);
if (null !== $documentationTarget && $node !== $documentationTarget) {
NodeDocumentation::move($node, $documentationTarget);
}
$rv[] = $node;
}
break;
@@ -306,6 +319,18 @@ class Parser
return isset($this->macros[$name]);
}
public function setDocumentationTarget(Node $node): void
{
if (null === $index = array_key_last($this->documentationTargets)) {
throw new \LogicException('A documentation target can only be set while parsing a tag.');
}
if (null !== $this->documentationTargets[$index]) {
throw new \LogicException('The documentation target for a tag can only be set once.');
}
$this->documentationTargets[$index] = $node;
}
public function setMacro(string $name, MacroNode $node): void
{
if (isset($this->macros[$name])) {
@@ -564,11 +589,6 @@ class Parser
private function cleanupBodyForChildTemplates(Node $body): Node
{
if ($body instanceof BlockReferenceNode) {
$name = $body->getAttribute('name');
if (isset($this->blocks[$name])) {
NodeDocumentation::move($body, $this->blocks[$name]->getNode('0'));
}
return new EmptyNode();
}
if ($body instanceof TextNode && $body->isBlank()) {
@@ -578,10 +598,6 @@ class Parser
foreach ($body as $k => $node) {
if ($node instanceof BlockReferenceNode) {
// as it has a parent, the block reference won't be used
$name = $node->getAttribute('name');
if (isset($this->blocks[$name])) {
NodeDocumentation::move($node, $this->blocks[$name]->getNode('0'));
}
$body->removeNode($k);
} elseif ($node instanceof TextNode && $node->isBlank()) {
// remove nodes considered as "empty"
+1
View File
@@ -39,6 +39,7 @@ final class BlockTokenParser extends AbstractTokenParser
$stream = $this->parser->getStream();
$name = $stream->expect(Token::NAME_TYPE)->getValue();
$this->parser->setBlock($name, $block = new BlockNode($name, new EmptyNode(), $lineno));
$this->parser->setDocumentationTarget($block);
$this->parser->pushLocalScope();
$this->parser->pushBlockStack($name);
+2 -1
View File
@@ -56,7 +56,8 @@ final class MacroTokenParser extends AbstractTokenParser
$this->parser->popLocalScope();
$stream->expect(Token::BLOCK_END_TYPE);
$this->parser->setMacro($name, new MacroNode($name, new BodyNode([$body]), $arguments, $lineno, $variadicName));
$this->parser->setMacro($name, $macro = new MacroNode($name, new BodyNode([$body]), $arguments, $lineno, $variadicName));
$this->parser->setDocumentationTarget($macro);
return new MacroDeclarationNode($name, $lineno);
}
+31 -1
View File
@@ -41,6 +41,7 @@ use Twig\Node\IfNode;
use Twig\Node\MacroDeclarationNode;
use Twig\Node\MacroNode;
use Twig\Node\Node;
use Twig\Node\Nodes;
use Twig\Node\PrintNode;
use Twig\Node\TextNode;
use Twig\NodeVisitor\NodeVisitorInterface;
@@ -384,6 +385,18 @@ TWIG, 'index')));
], $visitor->documentation);
}
public function testCustomTokenParserCanSetDocumentationTarget(): void
{
$twig = new Environment(new ArrayLoader());
$twig->addTokenParser(new DocumentationTargetTokenParser());
$module = $twig->parse($twig->tokenize(new Source('{## Target documentation #}{% documentation_target %}', 'index')));
$node = $module->getNode('body')->getNode('0');
$this->assertNull($node->getDocumentation());
$this->assertSame('Target documentation', $node->getNode('target')->getDocumentation());
}
public function testDocumentationIsRemovedFromUnsupportedOptimizedNodes(): void
{
$twig = new Environment(new ArrayLoader(), ['autoescape' => false]);
@@ -552,7 +565,24 @@ class DocumentationReadingNodeVisitor implements NodeVisitorInterface
public function getPriority(): int
{
return 0;
return -1024;
}
}
class DocumentationTargetTokenParser extends AbstractTokenParser
{
public function parse(Token $token): Node
{
$this->parser->getStream()->expect(Token::BLOCK_END_TYPE);
$target = new EmptyNode($token->getLine());
$this->parser->setDocumentationTarget($target);
return new Nodes(['target' => $target], $token->getLine());
}
public function getTag(): string
{
return 'documentation_target';
}
}