twigphp/Twig · error · LogicException

A documentation target can only be set while parsing a tag.

Error message

A documentation target can only be set while parsing a tag.

What it means

Parser::setDocumentationTarget() records which Node a documentation comment (like {@documentation}) applies to, but only while a tag is being parsed. Parser internals push a placeholder onto the documentationTargets stack when a tag starts and pop it when the tag ends; calling setDocumentationTarget with an empty stack means no tag is currently open, so the call is invalid.

Solutions

  1. Move the setDocumentationTarget() call so it happens while the tag's parse is still in progress (inside the TokenParser's parse() before returning).
  2. Remove the call entirely if the custom tag does not support the documentation feature.
  3. Guard the call with a state check if it comes from optional code paths that may run outside tag parsing.

Example fix

// before (TokenParser, too late)
public function parse(Token $token): Node
{
    $node = $this->parser->parseExpression();
    $this->parser->getStream()->expect(Token::BLOCK_END_TYPE);
    $this->parser->setDocumentationTarget($node); // stack already popped
    return $node;
}

// after
public function parse(Token $token): Node
{
    $node = $this->parser->parseExpression();
    $this->parser->setDocumentationTarget($node); // still inside tag parsing
    $this->parser->getStream()->expect(Token::BLOCK_END_TYPE);
    return $node;
}
Defensive patterns

Strategy: type-guard

Type guard

// Only call inside a TokenParser::parse() that has not yet consumed BLOCK_END_TYPE
if (!($this->parser instanceof \Twig\Parser)) return; // and only within parse scope

Try / catch

try { $parser->setDocumentationTarget($node); } catch (\LogicException $e) { if (str_contains($e->getMessage(), 'while parsing a tag')) { /* called outside tag parsing */ } throw $e; }

Prevention

When it happens

Trigger: Calling $parser->setDocumentationTarget($node) from a TokenParser after the tag's subparse has finished (the placeholder was already popped), or from code outside tag parsing entirely (e.g. a NodeVisitor invoking it during traversal).

Common situations: Custom TokenParser implementations calling setDocumentationTarget too late/early in parseTag lifecycle; debugging or monkey-patching the parser; extension code ported across Twig versions where documentation-target handling changed.

Understand the failure class

Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.

Related errors


AI-assisted analysis of twigphp/Twig@a414c3a491 (2026-09-13). Data as JSON: /api/errors/0ce0b43d2f1525cf. Report an issue: GitHub.

Appendix: source

Thrown at src/Parser.php:325

    {
        if (isset($this->blocks[$name])) {
            throw new SyntaxError(\sprintf("The block '%s' has already been defined line %d.", $name, $this->blocks[$name]->getTemplateLine()), $this->getCurrentToken()->getLine(), $this->blocks[$name]->getSourceContext());
        }

        $this->blocks[$name] = new BodyNode([$value], [], $value->getTemplateLine());
    }

    public function hasMacro(string $name): bool
    {
        trigger_deprecation('twig/twig', '3.12', 'Method "%s()" is deprecated.', __METHOD__);

        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])) {
            trigger_deprecation('twig/twig', '3.29', 'Defining the macro "%s" more than once in "%s" is deprecated and will throw a SyntaxError in Twig 4.0 (previous definition at line %d, new definition at line %d). The last definition is used in Twig 3.', $name, $this->stream->getSourceContext()->getName(), $this->macros[$name]->getTemplateLine(), $node->getTemplateLine());
        }

        $this->macros[$name] = $node;
    }

    public function addTrait($trait): void

View on GitHub (pinned to a414c3a491)