Attach documentation comments to nodes - #4871
Conversation
|
Will it works for Also, I forgot to close #4768, but it was already resolved with our Since my initial issue is closed, do we really need this new feature here? It would requires UX to change again, and the Symfony PHPStorm plugin should be modified as well. |
|
📋 PR Summary This incremental change narrows the documentation-comment feature: inline Changes
|
This is generic, so it should work just fine. |
|
Cool, thanks! When merged/released, I will work on UX & Symfony PHPStorm plugin to re-align things with this PR. |
fcbb983 to
12bb3aa
Compare
There was a problem hiding this comment.
📋 Upsun Dispatch Review: incremental · 18 files reviewed · no new issues · 2 still open
Outstanding from earlier reviews:
- #3650339795 — src/Node/TypesNode.php:32: Two divergent construction sites for the same nodes invite line-number drift and maintenance errors.
- #3650339797 — src/Node/Node.php:145: Ambiguous @internal marking on a method the feature relies on causes API-stability confusion.
12bb3aa to
d864497
Compare
There was a problem hiding this comment.
📋 Upsun Dispatch Review: full · 17 files reviewed · 🔵 1 info · 2 still open
Outstanding from earlier reviews:
- #3650339795 — src/Node/TypesNode.php:32: Two divergent construction sites for the same nodes invite line-number drift and maintenance errors.
- #3650339797 — src/Node/Node.php:145: Ambiguous @internal marking on a method the feature relies on causes API-stability confusion.
|
Opened Haehnchen/idea-php-symfony2-plugin#2836, UX PR will follows when this one get merged/released. |
d864497 to
4636611
Compare
|
|
||
| return new TypesNode($types, $token->getLine()); | ||
| $node = new TypesNode($types, $token->getLine()); | ||
| foreach ($nodes as $name => $type) { |
There was a problem hiding this comment.
🔵 Info — Redundant construction and duplicated type state make the model wasteful and easy to desynchronize.
TypesNode's constructor already builds a TypeNode child for every entry of the mapping (TypesNode.php: foreach ($types as $name => $type) { $nodes[$name] = new TypeNode(...) }), and this loop then immediately overwrites each of those children via setNode() with a second TypeNode that carries the documentation and the correct per-name line number. The constructor-built nodes are discarded on every parse, and the type data now lives in three copies (the mapping attribute plus two sets of TypeNodes). A visitor mutating a TypeNode will silently diverge from the mapping attribute compiled elsewhere.
There was a problem hiding this comment.
📋 Upsun Dispatch Review: incremental · 26 files reviewed · 🔵 2 infos · 1 still open
Outstanding from earlier reviews:
- #3652057652 — src/TokenParser/TypesTokenParser.php:40: Redundant construction and duplicated type state make the model wasteful and easy to desynchronize.
| $stream->expect(Token::NAME_TYPE, null, 'Only variables can be assigned to'); | ||
| } | ||
| $targets[] = new AssignContextVariable($token->getValue(), $token->getLine()); | ||
| $target = new AssignContextVariable($token->getValue(), $token->getLine()); |
There was a problem hiding this comment.
🔵 Info — Duplicated conversion logic across parsers is easy to desynchronize as documentation handling evolves.
The removed Parser::createAssignContextVariable/convertToAssignContextVariable helpers are now re-implemented as near-duplicate blocks in at least five places: this inline new AssignContextVariable(...) + NodeDocumentation::set(...), an identical block in AbstractTokenParser::parseAssignmentExpression, a private createAssignContextVariable in GroupingExpressionParser, a new AssignContextVariable(...) + NodeDocumentation::promote(...) variant in AssignmentExpressionParser and ArrowExpressionParser, and manual getDocumentation()/setDocumentation() copies in SetBinary and ArrowFunctionExpression::toAssignContextVariable. These copies already diverge in how they carry documentation (set vs promote vs direct copy), so a future change to conversion semantics must be replicated in every site.
| final class NodeDocumentation | ||
| { | ||
| /** @var \WeakMap<Node, string>|null */ | ||
| private static ?\WeakMap $pending = null; |
There was a problem hiding this comment.
🔵 Info — Global mutable static state is harder to test and reason about than the instance state it replaced.
$pending and $targets are process-global static \WeakMaps, replacing the former per-Parser instance state (pendingDocumentation/documentationTargets) that was reset at the start and end of each parse(). The state is now shared across all Parser instances and never reset; entries only clear when the keyed Node is garbage-collected. This is safe for single-threaded parsing (nodes are distinct objects per parse and WeakMap keys are weak), but it makes the stage→promote / set→setTarget lifecycle harder to test and reason about than the scoped instance state it replaced.
37205b4 to
099455d
Compare
There was a problem hiding this comment.
📋 Upsun Dispatch Review: incremental · 26 files reviewed · no new issues · 3 still open
Outstanding from earlier reviews:
- #3652057652 — src/TokenParser/TypesTokenParser.php:40: Redundant construction and duplicated type state make the model wasteful and easy to desynchronize.
- #3653256238 — src/ExpressionParser.php:314: Duplicated conversion logic across parsers is easy to desynchronize as documentation handling evolves.
- #3653256239 — src/Node/NodeDocumentation.php:22: Global mutable static state is harder to test and reason about than the instance state it replaced.
099455d to
fc2ffbe
Compare
There was a problem hiding this comment.
📋 Upsun Dispatch Review: full · 67 files reviewed · 🔵 3 infos · 2 still open
🔵 Info (P2 — suggestions) — outside diff range
src/ExpressionParser.php· design
parseExpression()unconditionally callsNodeDocumentation::set($expr, $documentationToken)on every expression it parses, so the leading token's documentation is attached to arbitrary expression nodes and correctness depends entirely onDocumentationNodeVisitor::leaveNode()later stripping it from everyAbstractExpressionthat is not anAssignContextVariable/LocalVariable. If that visitor is unregistered or reordered, or a new documented-target expression type is introduced, documentation silently leaks onto (or is wrongly removed from) nodes.
Review details
Commit: Commit fc2ffbe
Model: claude-opus-4-8
Panel: security · correctness · robustness · design
Outstanding from earlier reviews:
- #3652057652 — src/TokenParser/TypesTokenParser.php:40: Redundant construction and duplicated type state make the model wasteful and easy to desynchronize.
- #3653256238 — src/ExpressionParser.php:314: Duplicated conversion logic across parsers is easy to desynchronize as documentation handling evolves.
| $this->modules[] = $node; | ||
| } | ||
|
|
||
| $module = end($this->modules); |
There was a problem hiding this comment.
🔵 Info — Undocumented root-node assumption makes the visitor unsafe to reuse on subtrees.
$module = end($this->modules) returns false when the stack is empty, and the following BlockReferenceNode/MacroDeclarationNode branches immediately dereference it with $module->getNode('blocks')/$module->getNode('macros'). The visitor assumes traversal always begins at a ModuleNode (so one is pushed before any block/macro node is entered); traversing a subtree not rooted at a ModuleNode would call ->getNode() on false and fatal, with nothing documenting or enforcing that assumption.
| } | ||
|
|
||
| if ($node instanceof ModuleNode) { | ||
| array_pop($this->modules); |
There was a problem hiding this comment.
🔵 Info — Stale AST references accumulate across compilations when traversal throws.
The $this->modules stack is pushed in enterNode and popped here in leaveNode, but the visitor instance is reused across compilations. If traversal aborts after a ModuleNode was pushed (e.g. another visitor or a leaveNode throws), this array_pop never runs, so the aborted ModuleNode and the AST it references stay on the stack and are never released; repeated failed compilations accumulate them.
There was a problem hiding this comment.
📋 Upsun Dispatch Review: incremental · 59 files reviewed · no new issues · 3 still open
Outstanding from earlier reviews:
- #3652057652 — src/TokenParser/TypesTokenParser.php:40: Redundant construction and duplicated type state make the model wasteful and easy to desynchronize.
- #3659503446 — src/NodeVisitor/DocumentationNodeVisitor.php:35: Undocumented root-node assumption makes the visitor unsafe to reuse on subtrees.
- #3659503454 — src/NodeVisitor/DocumentationNodeVisitor.php:53: Stale AST references accumulate across compilations when traversal throws.
| * @author Fabien Potencier <fabien@symfony.com> | ||
| */ | ||
| #[YieldReady] | ||
| final class TypeNode extends Node |
There was a problem hiding this comment.
should this class (or at least its constructor) be @internal if it is only meant to be instantiated by TypesNode ?
| use Twig\Token; | ||
|
|
||
| /** | ||
| * @internal |
There was a problem hiding this comment.
should this be @internal ? Looking at the implementation of token parsers, it looks like custom token parsers provided by extensions might need to use it as well if they want to support documentation on their nodes.
|
|
||
| $this->moveCursor(substr($this->code, $this->cursor, $match[0][1] - $this->cursor).$match[0][0]); | ||
| $comment = substr($this->code, $this->cursor, $match[0][1] - $this->cursor); | ||
| if (str_starts_with($comment, '#')) { |
There was a problem hiding this comment.
how does the ## marker combine with whitespace control markers ?
Alternatives to #4870
To avoid BC breaks, I have another idea, using
##as a new syntax, a bit like/** */in PHP vs/* */."Documentation" is attached as metadata to the next relevant node:
Documentation comments can also describe variables declared with the
typestag:{% types { ## The unique identifier of the article id: 'string', ## Whether the article should be highlighted featured?: 'boolean', } %}Node visitors can access this metadata through
Node::getDocumentation(), allowing IDEs, static analyzers, and documentation generators to consume it without affecting template rendering. Documentation is preserved when visitors or optimizations replace nodes.Closes #4768
Closes #4870