Comment.php 6.7 KB

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