1: <?php declare(strict_types=1);
2:
3: namespace PhpParser\NodeVisitor;
4:
5: use PhpParser\Comment;
6: use PhpParser\Node;
7: use PhpParser\NodeVisitorAbstract;
8: use PhpParser\Token;
9:
10: class CommentAnnotatingVisitor extends NodeVisitorAbstract {
11: /** @var int Last seen token start position */
12: private int $pos = 0;
13: /** @var Token[] Token array */
14: private array $tokens;
15: /** @var list<int> Token positions of comments */
16: private array $commentPositions = [];
17:
18: /**
19: * Create a comment annotation visitor.
20: *
21: * @param Token[] $tokens Token array
22: */
23: public function __construct(array $tokens) {
24: $this->tokens = $tokens;
25:
26: // Collect positions of comments. We use this to avoid traversing parts of the AST where
27: // there are no comments.
28: foreach ($tokens as $i => $token) {
29: if ($token->id === \T_COMMENT || $token->id === \T_DOC_COMMENT) {
30: $this->commentPositions[] = $i;
31: }
32: }
33: }
34:
35: public function enterNode(Node $node) {
36: $nextCommentPos = current($this->commentPositions);
37: if ($nextCommentPos === false) {
38: // No more comments.
39: return self::STOP_TRAVERSAL;
40: }
41:
42: $oldPos = $this->pos;
43: $this->pos = $pos = $node->getStartTokenPos();
44: if ($nextCommentPos > $oldPos && $nextCommentPos < $pos) {
45: $comments = [];
46: while (--$pos >= $oldPos) {
47: $token = $this->tokens[$pos];
48: if ($token->id === \T_DOC_COMMENT) {
49: $comments[] = new Comment\Doc(
50: $token->text, $token->line, $token->pos, $pos,
51: $token->getEndLine(), $token->getEndPos() - 1, $pos);
52: continue;
53: }
54: if ($token->id === \T_COMMENT) {
55: $comments[] = new Comment(
56: $token->text, $token->line, $token->pos, $pos,
57: $token->getEndLine(), $token->getEndPos() - 1, $pos);
58: continue;
59: }
60: if ($token->id !== \T_WHITESPACE) {
61: break;
62: }
63: }
64: if (!empty($comments)) {
65: $node->setAttribute('comments', array_reverse($comments));
66: }
67:
68: do {
69: $nextCommentPos = next($this->commentPositions);
70: } while ($nextCommentPos !== false && $nextCommentPos < $this->pos);
71: }
72:
73: $endPos = $node->getEndTokenPos();
74: if ($nextCommentPos > $endPos) {
75: // Skip children if there are no comments located inside this node.
76: $this->pos = $endPos;
77: return self::DONT_TRAVERSE_CHILDREN;
78: }
79:
80: return null;
81: }
82: }
83: