{"record":{"id":"0ce0b43d2f1525cf","repo":"twigphp/Twig","slug":"a-documentation-target-can-only-be-set-while-parsing-a-tag","errorCode":null,"errorMessage":"A documentation target can only be set while parsing a tag.","messagePattern":"A documentation target can only be set while parsing a tag\\.","errorType":"exception","errorClass":"LogicException","httpStatus":null,"severity":"error","filePath":"src/Parser.php","lineNumber":325,"sourceCode":"    {\n        if (isset($this->blocks[$name])) {\n            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());\n        }\n\n        $this->blocks[$name] = new BodyNode([$value], [], $value->getTemplateLine());\n    }\n\n    public function hasMacro(string $name): bool\n    {\n        trigger_deprecation('twig/twig', '3.12', 'Method \"%s()\" is deprecated.', __METHOD__);\n\n        return isset($this->macros[$name]);\n    }\n\n    public function setDocumentationTarget(Node $node): void\n    {\n        if (null === $index = array_key_last($this->documentationTargets)) {\n            throw new \\LogicException('A documentation target can only be set while parsing a tag.');\n        }\n        if (null !== $this->documentationTargets[$index]) {\n            throw new \\LogicException('The documentation target for a tag can only be set once.');\n        }\n\n        $this->documentationTargets[$index] = $node;\n    }\n\n    public function setMacro(string $name, MacroNode $node): void\n    {\n        if (isset($this->macros[$name])) {\n            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());\n        }\n\n        $this->macros[$name] = $node;\n    }\n\n    public function addTrait($trait): void","sourceCodeStart":307,"sourceCodeEnd":343,"githubUrl":"https://github.com/twigphp/Twig/blob/a414c3a491defb5a60f2fc88ef79ff37c90010cd/src/Parser.php#L307-L343","documentation":"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.","triggerScenarios":"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).","commonSituations":"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.","solutions":["Move the setDocumentationTarget() call so it happens while the tag's parse is still in progress (inside the TokenParser's parse() before returning).","Remove the call entirely if the custom tag does not support the documentation feature.","Guard the call with a state check if it comes from optional code paths that may run outside tag parsing."],"exampleFix":"// before (TokenParser, too late)\npublic function parse(Token $token): Node\n{\n    $node = $this->parser->parseExpression();\n    $this->parser->getStream()->expect(Token::BLOCK_END_TYPE);\n    $this->parser->setDocumentationTarget($node); // stack already popped\n    return $node;\n}\n\n// after\npublic function parse(Token $token): Node\n{\n    $node = $this->parser->parseExpression();\n    $this->parser->setDocumentationTarget($node); // still inside tag parsing\n    $this->parser->getStream()->expect(Token::BLOCK_END_TYPE);\n    return $node;\n}","handlingStrategy":"type-guard","validationCode":null,"typeGuard":"// Only call inside a TokenParser::parse() that has not yet consumed BLOCK_END_TYPE\nif (!($this->parser instanceof \\Twig\\Parser)) return; // and only within parse scope","tryCatchPattern":"try { $parser->setDocumentationTarget($node); } catch (\\LogicException $e) { if (str_contains($e->getMessage(), 'while parsing a tag')) { /* called outside tag parsing */ } throw $e; }","preventionTips":["Only invoke parser state-mutating methods from within TokenParser::parse()","Never call setDocumentationTarget from NodeVisitors","Re-check custom parsers after upgrading Twig versions"],"tags":["twig","php","parser","api-misuse"],"backgroundTag":"invalid-state-transition","analyzedSha":"a414c3a491defb5a60f2fc88ef79ff37c90010cd","analyzedAt":"2026-09-13T15:10:46.849Z","contentChangedAt":"2026-09-13T15:10:46.849Z","schemaVersion":2},"datasetVersion":"2026-09-16T04:17:20.429Z"}