1 <?php declare(strict_types=1);
5 class Comment implements \JsonSerializable
13 * Constructs a comment node.
15 * @param string $text Comment text (including comment delimiters like /*)
16 * @param int $startLine Line number the comment started on
17 * @param int $startFilePos File offset the comment started on
18 * @param int $startTokenPos Token offset the comment started on
20 public function __construct(
21 string $text, int $startLine = -1, int $startFilePos = -1, int $startTokenPos = -1
24 $this->line = $startLine;
25 $this->filePos = $startFilePos;
26 $this->tokenPos = $startTokenPos;
30 * Gets the comment text.
32 * @return string The comment text (including comment delimiters like /*)
34 public function getText() : string {
39 * Gets the line number the comment started on.
41 * @return int Line number
43 public function getLine() : int {
48 * Gets the file offset the comment started on.
50 * @return int File offset
52 public function getFilePos() : int {
53 return $this->filePos;
57 * Gets the token offset the comment started on.
59 * @return int Token offset
61 public function getTokenPos() : int {
62 return $this->tokenPos;
66 * Gets the comment text.
68 * @return string The comment text (including comment delimiters like /*)
70 public function __toString() : string {
75 * Gets the reformatted comment text.
77 * "Reformatted" here means that we try to clean up the whitespace at the
78 * starts of the lines. This is necessary because we receive the comments
79 * without trailing whitespace on the first line, but with trailing whitespace
80 * on all subsequent lines.
82 * @return mixed|string
84 public function getReformattedText() {
85 $text = trim($this->text);
86 $newlinePos = strpos($text, "\n");
87 if (false === $newlinePos) {
88 // Single line comments don't need further processing
90 } elseif (preg_match('((*BSR_ANYCRLF)(*ANYCRLF)^.*(?:\R\s+\*.*)+$)', $text)) {
91 // Multi line comment of the type
98 // is handled by replacing the whitespace sequences before the * by a single space
99 return preg_replace('(^\s+\*)m', ' *', $this->text);
100 } elseif (preg_match('(^/\*\*?\s*[\r\n])', $text) && preg_match('(\n(\s*)\*/$)', $text, $matches)) {
101 // Multi line comment of the type
108 // is handled by removing the whitespace sequence on the line before the closing
109 // */ on all lines. So if the last line is " */", then " " is removed at the
110 // start of all lines.
111 return preg_replace('(^' . preg_quote($matches[1]) . ')m', '', $text);
112 } elseif (preg_match('(^/\*\*?\s*(?!\s))', $text, $matches)) {
113 // Multi line comment of the type
118 // Even more text. */
120 // is handled by removing the difference between the shortest whitespace prefix on all
121 // lines and the length of the "/* " opening sequence.
122 $prefixLen = $this->getShortestWhitespacePrefixLen(substr($text, $newlinePos + 1));
123 $removeLen = $prefixLen - strlen($matches[0]);
124 return preg_replace('(^\s{' . $removeLen . '})m', '', $text);
127 // No idea how to format this comment, so simply return as is
132 * Get length of shortest whitespace prefix (at the start of a line).
134 * If there is a line with no prefix whitespace, 0 is a valid return value.
136 * @param string $str String to check
137 * @return int Length in characters. Tabs count as single characters.
139 private function getShortestWhitespacePrefixLen(string $str) : int {
140 $lines = explode("\n", $str);
141 $shortestPrefixLen = \INF;
142 foreach ($lines as $line) {
143 preg_match('(^\s*)', $line, $matches);
144 $prefixLen = strlen($matches[0]);
145 if ($prefixLen < $shortestPrefixLen) {
146 $shortestPrefixLen = $prefixLen;
149 return $shortestPrefixLen;
154 * @psalm-return array{nodeType:string, text:mixed, line:mixed, filePos:mixed}
156 public function jsonSerialize() : array {
157 // Technically not a node, but we make it look like one anyway
158 $type = $this instanceof Comment\Doc ? 'Comment_Doc' : 'Comment';
161 'text' => $this->text,
162 'line' => $this->line,
163 'filePos' => $this->filePos,
164 'tokenPos' => $this->tokenPos,