Comment.php 7.4 KB

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