1: <?php declare(strict_types=1);
2:
3: namespace PhpParser;
4:
5: class Comment implements \JsonSerializable {
6: protected string $text;
7: protected int $startLine;
8: protected int $startFilePos;
9: protected int $startTokenPos;
10: protected int $endLine;
11: protected int $endFilePos;
12: protected int $endTokenPos;
13:
14: /**
15: * Constructs a comment node.
16: *
17: * @param string $text Comment text (including comment delimiters like /*)
18: * @param int $startLine Line number the comment started on
19: * @param int $startFilePos File offset the comment started on
20: * @param int $startTokenPos Token offset the comment started on
21: */
22: public function __construct(
23: string $text,
24: int $startLine = -1, int $startFilePos = -1, int $startTokenPos = -1,
25: int $endLine = -1, int $endFilePos = -1, int $endTokenPos = -1
26: ) {
27: $this->text = $text;
28: $this->startLine = $startLine;
29: $this->startFilePos = $startFilePos;
30: $this->startTokenPos = $startTokenPos;
31: $this->endLine = $endLine;
32: $this->endFilePos = $endFilePos;
33: $this->endTokenPos = $endTokenPos;
34: }
35:
36: /**
37: * Gets the comment text.
38: *
39: * @return string The comment text (including comment delimiters like /*)
40: */
41: public function getText(): string {
42: return $this->text;
43: }
44:
45: /**
46: * Gets the line number the comment started on.
47: *
48: * @return int Line number (or -1 if not available)
49: * @phpstan-return -1|positive-int
50: */
51: public function getStartLine(): int {
52: return $this->startLine;
53: }
54:
55: /**
56: * Gets the file offset the comment started on.
57: *
58: * @return int File offset (or -1 if not available)
59: */
60: public function getStartFilePos(): int {
61: return $this->startFilePos;
62: }
63:
64: /**
65: * Gets the token offset the comment started on.
66: *
67: * @return int Token offset (or -1 if not available)
68: */
69: public function getStartTokenPos(): int {
70: return $this->startTokenPos;
71: }
72:
73: /**
74: * Gets the line number the comment ends on.
75: *
76: * @return int Line number (or -1 if not available)
77: * @phpstan-return -1|positive-int
78: */
79: public function getEndLine(): int {
80: return $this->endLine;
81: }
82:
83: /**
84: * Gets the file offset the comment ends on.
85: *
86: * @return int File offset (or -1 if not available)
87: */
88: public function getEndFilePos(): int {
89: return $this->endFilePos;
90: }
91:
92: /**
93: * Gets the token offset the comment ends on.
94: *
95: * @return int Token offset (or -1 if not available)
96: */
97: public function getEndTokenPos(): int {
98: return $this->endTokenPos;
99: }
100:
101: /**
102: * Gets the comment text.
103: *
104: * @return string The comment text (including comment delimiters like /*)
105: */
106: public function __toString(): string {
107: return $this->text;
108: }
109:
110: /**
111: * Gets the reformatted comment text.
112: *
113: * "Reformatted" here means that we try to clean up the whitespace at the
114: * starts of the lines. This is necessary because we receive the comments
115: * without leading whitespace on the first line, but with leading whitespace
116: * on all subsequent lines.
117: *
118: * Additionally, this normalizes CRLF newlines to LF newlines.
119: */
120: public function getReformattedText(): string {
121: $text = str_replace("\r\n", "\n", $this->text);
122: $newlinePos = strpos($text, "\n");
123: if (false === $newlinePos) {
124: // Single line comments don't need further processing
125: return $text;
126: }
127: if (preg_match('(^.*(?:\n\s+\*.*)+$)', $text)) {
128: // Multi line comment of the type
129: //
130: // /*
131: // * Some text.
132: // * Some more text.
133: // */
134: //
135: // is handled by replacing the whitespace sequences before the * by a single space
136: return preg_replace('(^\s+\*)m', ' *', $text);
137: }
138: if (preg_match('(^/\*\*?\s*\n)', $text) && preg_match('(\n(\s*)\*/$)', $text, $matches)) {
139: // Multi line comment of the type
140: //
141: // /*
142: // Some text.
143: // Some more text.
144: // */
145: //
146: // is handled by removing the whitespace sequence on the line before the closing
147: // */ on all lines. So if the last line is " */", then " " is removed at the
148: // start of all lines.
149: return preg_replace('(^' . preg_quote($matches[1]) . ')m', '', $text);
150: }
151: if (preg_match('(^/\*\*?\s*(?!\s))', $text, $matches)) {
152: // Multi line comment of the type
153: //
154: // /* Some text.
155: // Some more text.
156: // Indented text.
157: // Even more text. */
158: //
159: // is handled by removing the difference between the shortest whitespace prefix on all
160: // lines and the length of the "/* " opening sequence.
161: $prefixLen = $this->getShortestWhitespacePrefixLen(substr($text, $newlinePos + 1));
162: $removeLen = $prefixLen - strlen($matches[0]);
163: return preg_replace('(^\s{' . $removeLen . '})m', '', $text);
164: }
165:
166: // No idea how to format this comment, so simply return as is
167: return $text;
168: }
169:
170: /**
171: * Get length of shortest whitespace prefix (at the start of a line).
172: *
173: * If there is a line with no prefix whitespace, 0 is a valid return value.
174: *
175: * @param string $str String to check
176: * @return int Length in characters. Tabs count as single characters.
177: */
178: private function getShortestWhitespacePrefixLen(string $str): int {
179: $lines = explode("\n", $str);
180: $shortestPrefixLen = \PHP_INT_MAX;
181: foreach ($lines as $line) {
182: preg_match('(^\s*)', $line, $matches);
183: $prefixLen = strlen($matches[0]);
184: if ($prefixLen < $shortestPrefixLen) {
185: $shortestPrefixLen = $prefixLen;
186: }
187: }
188: return $shortestPrefixLen;
189: }
190:
191: /**
192: * @return array{nodeType:string, text:mixed, line:mixed, filePos:mixed}
193: */
194: public function jsonSerialize(): array {
195: // Technically not a node, but we make it look like one anyway
196: $type = $this instanceof Comment\Doc ? 'Comment_Doc' : 'Comment';
197: return [
198: 'nodeType' => $type,
199: 'text' => $this->text,
200: // TODO: Rename these to include "start".
201: 'line' => $this->startLine,
202: 'filePos' => $this->startFilePos,
203: 'tokenPos' => $this->startTokenPos,
204: 'endLine' => $this->endLine,
205: 'endFilePos' => $this->endFilePos,
206: 'endTokenPos' => $this->endTokenPos,
207: ];
208: }
209: }
210: