Tokens.php 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303
  1. <?php
  2. declare(strict_types=1);
  3. /*
  4. * This file is part of PHP CS Fixer.
  5. *
  6. * (c) Fabien Potencier <fabien@symfony.com>
  7. * Dariusz Rumiński <dariusz.ruminski@gmail.com>
  8. *
  9. * This source file is subject to the MIT license that is bundled
  10. * with this source code in the file LICENSE.
  11. */
  12. namespace PhpCsFixer\Doctrine\Annotation;
  13. use PhpCsFixer\Preg;
  14. use PhpCsFixer\Tokenizer\Token as PhpToken;
  15. /**
  16. * A list of Doctrine annotation tokens.
  17. *
  18. * @internal
  19. *
  20. * @extends \SplFixedArray<Token>
  21. */
  22. final class Tokens extends \SplFixedArray
  23. {
  24. /**
  25. * @param list<string> $ignoredTags
  26. *
  27. * @throws \InvalidArgumentException
  28. */
  29. public static function createFromDocComment(PhpToken $input, array $ignoredTags = []): self
  30. {
  31. if (!$input->isGivenKind(T_DOC_COMMENT)) {
  32. throw new \InvalidArgumentException('Input must be a T_DOC_COMMENT token.');
  33. }
  34. $tokens = [];
  35. $content = $input->getContent();
  36. $ignoredTextPosition = 0;
  37. $currentPosition = 0;
  38. $token = null;
  39. while (false !== $nextAtPosition = strpos($content, '@', $currentPosition)) {
  40. if (0 !== $nextAtPosition && !Preg::match('/\s/', $content[$nextAtPosition - 1])) {
  41. $currentPosition = $nextAtPosition + 1;
  42. continue;
  43. }
  44. $lexer = new DocLexer();
  45. $lexer->setInput(substr($content, $nextAtPosition));
  46. $scannedTokens = [];
  47. $index = 0;
  48. $nbScannedTokensToUse = 0;
  49. $nbScopes = 0;
  50. while (null !== $token = $lexer->peek()) {
  51. if (0 === $index && !$token->isType(DocLexer::T_AT)) {
  52. break;
  53. }
  54. if (1 === $index) {
  55. if (!$token->isType(DocLexer::T_IDENTIFIER) || \in_array($token->getContent(), $ignoredTags, true)) {
  56. break;
  57. }
  58. $nbScannedTokensToUse = 2;
  59. }
  60. if ($index >= 2 && 0 === $nbScopes && !$token->isType([DocLexer::T_NONE, DocLexer::T_OPEN_PARENTHESIS])) {
  61. break;
  62. }
  63. $scannedTokens[] = $token;
  64. if ($token->isType(DocLexer::T_OPEN_PARENTHESIS)) {
  65. ++$nbScopes;
  66. } elseif ($token->isType(DocLexer::T_CLOSE_PARENTHESIS)) {
  67. if (0 === --$nbScopes) {
  68. $nbScannedTokensToUse = \count($scannedTokens);
  69. break;
  70. }
  71. }
  72. ++$index;
  73. }
  74. if (0 !== $nbScopes) {
  75. break;
  76. }
  77. if (0 !== $nbScannedTokensToUse) {
  78. $ignoredTextLength = $nextAtPosition - $ignoredTextPosition;
  79. if (0 !== $ignoredTextLength) {
  80. $tokens[] = new Token(DocLexer::T_NONE, substr($content, $ignoredTextPosition, $ignoredTextLength));
  81. }
  82. $lastTokenEndIndex = 0;
  83. foreach (\array_slice($scannedTokens, 0, $nbScannedTokensToUse) as $scannedToken) {
  84. $token = $scannedToken->isType(DocLexer::T_STRING)
  85. ? new Token(
  86. $scannedToken->getType(),
  87. '"'.str_replace('"', '""', $scannedToken->getContent()).'"',
  88. $scannedToken->getPosition()
  89. )
  90. : $scannedToken;
  91. $missingTextLength = $token->getPosition() - $lastTokenEndIndex;
  92. if ($missingTextLength > 0) {
  93. $tokens[] = new Token(DocLexer::T_NONE, substr(
  94. $content,
  95. $nextAtPosition + $lastTokenEndIndex,
  96. $missingTextLength
  97. ));
  98. }
  99. $tokens[] = new Token($token->getType(), $token->getContent());
  100. $lastTokenEndIndex = $token->getPosition() + \strlen($token->getContent());
  101. }
  102. $currentPosition = $ignoredTextPosition = $nextAtPosition + $token->getPosition() + \strlen($token->getContent());
  103. } else {
  104. $currentPosition = $nextAtPosition + 1;
  105. }
  106. }
  107. if ($ignoredTextPosition < \strlen($content)) {
  108. $tokens[] = new Token(DocLexer::T_NONE, substr($content, $ignoredTextPosition));
  109. }
  110. return self::fromArray($tokens);
  111. }
  112. /**
  113. * Create token collection from array.
  114. *
  115. * @param array<int, Token> $array the array to import
  116. * @param ?bool $saveIndices save the numeric indices used in the original array, default is yes
  117. */
  118. public static function fromArray($array, $saveIndices = null): self
  119. {
  120. $tokens = new self(\count($array));
  121. if (null === $saveIndices || $saveIndices) {
  122. foreach ($array as $key => $val) {
  123. $tokens[$key] = $val;
  124. }
  125. } else {
  126. $index = 0;
  127. foreach ($array as $val) {
  128. $tokens[$index++] = $val;
  129. }
  130. }
  131. return $tokens;
  132. }
  133. /**
  134. * Returns the index of the closest next token that is neither a comment nor a whitespace token.
  135. */
  136. public function getNextMeaningfulToken(int $index): ?int
  137. {
  138. return $this->getMeaningfulTokenSibling($index, 1);
  139. }
  140. /**
  141. * Returns the index of the closest previous token that is neither a comment nor a whitespace token.
  142. */
  143. public function getPreviousMeaningfulToken(int $index): ?int
  144. {
  145. return $this->getMeaningfulTokenSibling($index, -1);
  146. }
  147. /**
  148. * Returns the index of the last token that is part of the annotation at the given index.
  149. */
  150. public function getAnnotationEnd(int $index): ?int
  151. {
  152. $currentIndex = null;
  153. if (isset($this[$index + 2])) {
  154. if ($this[$index + 2]->isType(DocLexer::T_OPEN_PARENTHESIS)) {
  155. $currentIndex = $index + 2;
  156. } elseif (
  157. isset($this[$index + 3])
  158. && $this[$index + 2]->isType(DocLexer::T_NONE)
  159. && $this[$index + 3]->isType(DocLexer::T_OPEN_PARENTHESIS)
  160. && Preg::match('/^(\R\s*\*\s*)*\s*$/', $this[$index + 2]->getContent())
  161. ) {
  162. $currentIndex = $index + 3;
  163. }
  164. }
  165. if (null !== $currentIndex) {
  166. $level = 0;
  167. for ($max = \count($this); $currentIndex < $max; ++$currentIndex) {
  168. if ($this[$currentIndex]->isType(DocLexer::T_OPEN_PARENTHESIS)) {
  169. ++$level;
  170. } elseif ($this[$currentIndex]->isType(DocLexer::T_CLOSE_PARENTHESIS)) {
  171. --$level;
  172. }
  173. if (0 === $level) {
  174. return $currentIndex;
  175. }
  176. }
  177. return null;
  178. }
  179. return $index + 1;
  180. }
  181. /**
  182. * Returns the code from the tokens.
  183. */
  184. public function getCode(): string
  185. {
  186. $code = '';
  187. foreach ($this as $token) {
  188. $code .= $token->getContent();
  189. }
  190. return $code;
  191. }
  192. /**
  193. * Inserts a token at the given index.
  194. */
  195. public function insertAt(int $index, Token $token): void
  196. {
  197. $this->setSize($this->getSize() + 1);
  198. for ($i = $this->getSize() - 1; $i > $index; --$i) {
  199. $this[$i] = $this[$i - 1] ?? new Token();
  200. }
  201. $this[$index] = $token;
  202. }
  203. public function offsetSet($index, $token): void
  204. {
  205. if (null === $token) {
  206. throw new \InvalidArgumentException('Token must be an instance of PhpCsFixer\Doctrine\Annotation\Token, "null" given.');
  207. }
  208. if (!$token instanceof Token) {
  209. $type = \gettype($token);
  210. if ('object' === $type) {
  211. $type = \get_class($token);
  212. }
  213. throw new \InvalidArgumentException(\sprintf('Token must be an instance of PhpCsFixer\Doctrine\Annotation\Token, "%s" given.', $type));
  214. }
  215. parent::offsetSet($index, $token);
  216. }
  217. /**
  218. * @param mixed $index
  219. *
  220. * @throws \OutOfBoundsException
  221. */
  222. public function offsetUnset($index): void
  223. {
  224. if (!isset($this[$index])) {
  225. throw new \OutOfBoundsException(\sprintf('Index "%s" is invalid or does not exist.', $index));
  226. }
  227. $max = \count($this) - 1;
  228. while ($index < $max) {
  229. $this[$index] = $this[$index + 1];
  230. ++$index;
  231. }
  232. parent::offsetUnset($index);
  233. $this->setSize($max);
  234. }
  235. private function getMeaningfulTokenSibling(int $index, int $direction): ?int
  236. {
  237. while (true) {
  238. $index += $direction;
  239. if (!$this->offsetExists($index)) {
  240. break;
  241. }
  242. if (!$this[$index]->isType(DocLexer::T_NONE)) {
  243. return $index;
  244. }
  245. }
  246. return null;
  247. }
  248. }