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