TokenParser.php 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223
  1. <?php
  2. /*
  3. * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
  4. * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
  5. * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
  6. * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
  7. * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
  8. * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
  9. * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
  10. * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
  11. * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
  12. * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
  13. * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  14. *
  15. * This software consists of voluntary contributions made by many individuals
  16. * and is licensed under the MIT license. For more information, see
  17. * <http://www.doctrine-project.org>.
  18. */
  19. namespace EasySwoole\DoctrineAnnotation;
  20. use function array_merge;
  21. use function count;
  22. use function explode;
  23. use function strtolower;
  24. use function token_get_all;
  25. use const PHP_VERSION_ID;
  26. use const T_AS;
  27. use const T_COMMENT;
  28. use const T_DOC_COMMENT;
  29. use const T_NAMESPACE;
  30. use const T_NS_SEPARATOR;
  31. use const T_STRING;
  32. use const T_USE;
  33. use const T_WHITESPACE;
  34. /**
  35. * Parses a file for namespaces/use/class declarations.
  36. */
  37. class TokenParser
  38. {
  39. /**
  40. * The token list.
  41. *
  42. * @phpstan-var list<mixed[]>
  43. */
  44. private $tokens;
  45. /**
  46. * The number of tokens.
  47. *
  48. * @var int
  49. */
  50. private $numTokens;
  51. /**
  52. * The current array pointer.
  53. *
  54. * @var int
  55. */
  56. private $pointer = 0;
  57. /**
  58. * @param string $contents
  59. */
  60. public function __construct($contents)
  61. {
  62. $this->tokens = token_get_all($contents);
  63. // The PHP parser sets internal compiler globals for certain things. Annoyingly, the last docblock comment it
  64. // saw gets stored in doc_comment. When it comes to compile the next thing to be include()d this stored
  65. // doc_comment becomes owned by the first thing the compiler sees in the file that it considers might have a
  66. // docblock. If the first thing in the file is a class without a doc block this would cause calls to
  67. // getDocBlock() on said class to return our long lost doc_comment. Argh.
  68. // To workaround, cause the parser to parse an empty docblock. Sure getDocBlock() will return this, but at least
  69. // it's harmless to us.
  70. token_get_all("<?php\n/**\n *\n */");
  71. $this->numTokens = count($this->tokens);
  72. }
  73. /**
  74. * Gets the next non whitespace and non comment token.
  75. *
  76. * @param bool $docCommentIsComment If TRUE then a doc comment is considered a comment and skipped.
  77. * If FALSE then only whitespace and normal comments are skipped.
  78. *
  79. * @return mixed[]|string|null The token if exists, null otherwise.
  80. */
  81. public function next($docCommentIsComment = true)
  82. {
  83. for ($i = $this->pointer; $i < $this->numTokens; $i++) {
  84. $this->pointer++;
  85. if (
  86. $this->tokens[$i][0] === T_WHITESPACE ||
  87. $this->tokens[$i][0] === T_COMMENT ||
  88. ($docCommentIsComment && $this->tokens[$i][0] === T_DOC_COMMENT)
  89. ) {
  90. continue;
  91. }
  92. return $this->tokens[$i];
  93. }
  94. return null;
  95. }
  96. /**
  97. * Parses a single use statement.
  98. *
  99. * @return array<string, string> A list with all found class names for a use statement.
  100. */
  101. public function parseUseStatement()
  102. {
  103. $groupRoot = '';
  104. $class = '';
  105. $alias = '';
  106. $statements = [];
  107. $explicitAlias = false;
  108. while (($token = $this->next())) {
  109. if (! $explicitAlias && $token[0] === T_STRING) {
  110. $class .= $token[1];
  111. $alias = $token[1];
  112. } elseif ($explicitAlias && $token[0] === T_STRING) {
  113. $alias = $token[1];
  114. } elseif (
  115. PHP_VERSION_ID >= 80000 &&
  116. ($token[0] === T_NAME_QUALIFIED || $token[0] === T_NAME_FULLY_QUALIFIED)
  117. ) {
  118. $class .= $token[1];
  119. $classSplit = explode('\\', $token[1]);
  120. $alias = $classSplit[count($classSplit) - 1];
  121. } elseif ($token[0] === T_NS_SEPARATOR) {
  122. $class .= '\\';
  123. $alias = '';
  124. } elseif ($token[0] === T_AS) {
  125. $explicitAlias = true;
  126. $alias = '';
  127. } elseif ($token === ',') {
  128. $statements[strtolower($alias)] = $groupRoot . $class;
  129. $class = '';
  130. $alias = '';
  131. $explicitAlias = false;
  132. } elseif ($token === ';') {
  133. $statements[strtolower($alias)] = $groupRoot . $class;
  134. break;
  135. } elseif ($token === '{') {
  136. $groupRoot = $class;
  137. $class = '';
  138. } elseif ($token === '}') {
  139. continue;
  140. } else {
  141. break;
  142. }
  143. }
  144. return $statements;
  145. }
  146. /**
  147. * Gets all use statements.
  148. *
  149. * @param string $namespaceName The namespace name of the reflected class.
  150. *
  151. * @return array<string, string> A list with all found use statements.
  152. */
  153. public function parseUseStatements($namespaceName)
  154. {
  155. $statements = [];
  156. while (($token = $this->next())) {
  157. if ($token[0] === T_USE) {
  158. $statements = array_merge($statements, $this->parseUseStatement());
  159. continue;
  160. }
  161. if ($token[0] !== T_NAMESPACE || $this->parseNamespace() !== $namespaceName) {
  162. continue;
  163. }
  164. // Get fresh array for new namespace. This is to prevent the parser to collect the use statements
  165. // for a previous namespace with the same name. This is the case if a namespace is defined twice
  166. // or if a namespace with the same name is commented out.
  167. $statements = [];
  168. }
  169. return $statements;
  170. }
  171. /**
  172. * Gets the namespace.
  173. *
  174. * @return string The found namespace.
  175. */
  176. public function parseNamespace()
  177. {
  178. $name = '';
  179. while (
  180. ($token = $this->next()) && ($token[0] === T_STRING || $token[0] === T_NS_SEPARATOR || (
  181. PHP_VERSION_ID >= 80000 &&
  182. ($token[0] === T_NAME_QUALIFIED || $token[0] === T_NAME_FULLY_QUALIFIED)
  183. ))
  184. ) {
  185. $name .= $token[1];
  186. }
  187. return $name;
  188. }
  189. /**
  190. * Gets the class name.
  191. *
  192. * @return string The found class name.
  193. */
  194. public function parseClass()
  195. {
  196. // Namespaces and class names are tokenized the same: T_STRINGs
  197. // separated by T_NS_SEPARATOR so we can use one function to provide
  198. // both.
  199. return $this->parseNamespace();
  200. }
  201. }