Token.php 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519
  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\Tokenizer;
  13. use PhpCsFixer\Utils;
  14. /**
  15. * Representation of single token.
  16. * As a token prototype you should understand a single element generated by token_get_all.
  17. *
  18. * @author Dariusz Rumiński <dariusz.ruminski@gmail.com>
  19. */
  20. final class Token
  21. {
  22. /**
  23. * Content of token prototype.
  24. */
  25. private string $content;
  26. /**
  27. * ID of token prototype, if available.
  28. */
  29. private ?int $id = null;
  30. /**
  31. * If token prototype is an array.
  32. */
  33. private bool $isArray;
  34. /**
  35. * Flag is token was changed.
  36. */
  37. private bool $changed = false;
  38. /**
  39. * @param array{int, string}|string $token token prototype
  40. */
  41. public function __construct($token)
  42. {
  43. if (\is_array($token)) {
  44. if (!\is_int($token[0])) {
  45. throw new \InvalidArgumentException(sprintf(
  46. 'Id must be an int, got "%s".',
  47. get_debug_type($token[0])
  48. ));
  49. }
  50. if (!\is_string($token[1])) {
  51. throw new \InvalidArgumentException(sprintf(
  52. 'Content must be a string, got "%s".',
  53. get_debug_type($token[1])
  54. ));
  55. }
  56. if ('' === $token[1]) {
  57. throw new \InvalidArgumentException('Cannot set empty content for id-based Token.');
  58. }
  59. $this->isArray = true;
  60. $this->id = $token[0];
  61. $this->content = $token[1];
  62. } elseif (\is_string($token)) {
  63. $this->isArray = false;
  64. $this->content = $token;
  65. } else {
  66. throw new \InvalidArgumentException(sprintf('Cannot recognize input value as valid Token prototype, got "%s".', get_debug_type($token)));
  67. }
  68. }
  69. /**
  70. * @return list<int>
  71. */
  72. public static function getCastTokenKinds(): array
  73. {
  74. static $castTokens = [T_ARRAY_CAST, T_BOOL_CAST, T_DOUBLE_CAST, T_INT_CAST, T_OBJECT_CAST, T_STRING_CAST, T_UNSET_CAST];
  75. return $castTokens;
  76. }
  77. /**
  78. * Get classy tokens kinds: T_CLASS, T_INTERFACE and T_TRAIT.
  79. *
  80. * @return list<int>
  81. */
  82. public static function getClassyTokenKinds(): array
  83. {
  84. static $classTokens;
  85. if (null === $classTokens) {
  86. $classTokens = [T_CLASS, T_TRAIT, T_INTERFACE];
  87. if (\defined('T_ENUM')) { // @TODO: drop condition when PHP 8.1+ is required
  88. $classTokens[] = T_ENUM;
  89. }
  90. }
  91. return $classTokens;
  92. }
  93. /**
  94. * Get object operator tokens kinds: T_OBJECT_OPERATOR and (if available) T_NULLSAFE_OBJECT_OPERATOR.
  95. *
  96. * @return list<int>
  97. */
  98. public static function getObjectOperatorKinds(): array
  99. {
  100. static $objectOperators = null;
  101. if (null === $objectOperators) {
  102. $objectOperators = [T_OBJECT_OPERATOR];
  103. if (\defined('T_NULLSAFE_OBJECT_OPERATOR')) {
  104. $objectOperators[] = T_NULLSAFE_OBJECT_OPERATOR;
  105. }
  106. }
  107. return $objectOperators;
  108. }
  109. /**
  110. * Check if token is equals to given one.
  111. *
  112. * If tokens are arrays, then only keys defined in parameter token are checked.
  113. *
  114. * @param array{0: int, 1?: string}|string|Token $other token or it's prototype
  115. * @param bool $caseSensitive perform a case sensitive comparison
  116. */
  117. public function equals($other, bool $caseSensitive = true): bool
  118. {
  119. if (\defined('T_AMPERSAND_FOLLOWED_BY_VAR_OR_VARARG')) { // @TODO: drop condition when PHP 8.1+ is required
  120. if ('&' === $other) {
  121. return '&' === $this->content && (null === $this->id || $this->isGivenKind([T_AMPERSAND_FOLLOWED_BY_VAR_OR_VARARG, T_AMPERSAND_NOT_FOLLOWED_BY_VAR_OR_VARARG]));
  122. }
  123. if (null === $this->id && '&' === $this->content) {
  124. return $other instanceof self && '&' === $other->content && (null === $other->id || $other->isGivenKind([T_AMPERSAND_FOLLOWED_BY_VAR_OR_VARARG, T_AMPERSAND_NOT_FOLLOWED_BY_VAR_OR_VARARG]));
  125. }
  126. }
  127. if ($other instanceof self) {
  128. // Inlined getPrototype() on this very hot path.
  129. // We access the private properties of $other directly to save function call overhead.
  130. // This is only possible because $other is of the same class as `self`.
  131. if (!$other->isArray) {
  132. $otherPrototype = $other->content;
  133. } else {
  134. $otherPrototype = [
  135. $other->id,
  136. $other->content,
  137. ];
  138. }
  139. } else {
  140. $otherPrototype = $other;
  141. }
  142. if ($this->isArray !== \is_array($otherPrototype)) {
  143. return false;
  144. }
  145. if (!$this->isArray) {
  146. return $this->content === $otherPrototype;
  147. }
  148. if ($this->id !== $otherPrototype[0]) {
  149. return false;
  150. }
  151. if (isset($otherPrototype[1])) {
  152. if ($caseSensitive) {
  153. if ($this->content !== $otherPrototype[1]) {
  154. return false;
  155. }
  156. } elseif (0 !== strcasecmp($this->content, $otherPrototype[1])) {
  157. return false;
  158. }
  159. }
  160. // detect unknown keys
  161. unset($otherPrototype[0], $otherPrototype[1]);
  162. return [] === $otherPrototype;
  163. }
  164. /**
  165. * Check if token is equals to one of given.
  166. *
  167. * @param list<array{0: int, 1?: string}|string|Token> $others array of tokens or token prototypes
  168. * @param bool $caseSensitive perform a case sensitive comparison
  169. */
  170. public function equalsAny(array $others, bool $caseSensitive = true): bool
  171. {
  172. foreach ($others as $other) {
  173. if ($this->equals($other, $caseSensitive)) {
  174. return true;
  175. }
  176. }
  177. return false;
  178. }
  179. /**
  180. * A helper method used to find out whether a certain input token has to be case-sensitively matched.
  181. *
  182. * @param array<int, bool>|bool $caseSensitive global case sensitiveness or an array of booleans, whose keys should match
  183. * the ones used in $sequence. If any is missing, the default case-sensitive
  184. * comparison is used
  185. * @param int $key the key of the token that has to be looked up
  186. *
  187. * @deprecated
  188. */
  189. public static function isKeyCaseSensitive($caseSensitive, int $key): bool
  190. {
  191. Utils::triggerDeprecation(new \InvalidArgumentException(sprintf(
  192. 'Method "%s" is deprecated and will be removed in the next major version.',
  193. __METHOD__
  194. )));
  195. if (\is_array($caseSensitive)) {
  196. return $caseSensitive[$key] ?? true;
  197. }
  198. return $caseSensitive;
  199. }
  200. /**
  201. * @return array{int, string}|string
  202. */
  203. public function getPrototype()
  204. {
  205. if (!$this->isArray) {
  206. return $this->content;
  207. }
  208. return [
  209. $this->id,
  210. $this->content,
  211. ];
  212. }
  213. /**
  214. * Get token's content.
  215. *
  216. * It shall be used only for getting the content of token, not for checking it against excepted value.
  217. *
  218. * @return non-empty-string
  219. */
  220. public function getContent(): string
  221. {
  222. return $this->content;
  223. }
  224. /**
  225. * Get token's id.
  226. *
  227. * It shall be used only for getting the internal id of token, not for checking it against excepted value.
  228. */
  229. public function getId(): ?int
  230. {
  231. return $this->id;
  232. }
  233. /**
  234. * Get token's name.
  235. *
  236. * It shall be used only for getting the name of token, not for checking it against excepted value.
  237. *
  238. * @return null|string token name
  239. */
  240. public function getName(): ?string
  241. {
  242. if (null === $this->id) {
  243. return null;
  244. }
  245. return self::getNameForId($this->id);
  246. }
  247. /**
  248. * Get token's name.
  249. *
  250. * It shall be used only for getting the name of token, not for checking it against excepted value.
  251. *
  252. * @return null|string token name
  253. */
  254. public static function getNameForId(int $id): ?string
  255. {
  256. if (CT::has($id)) {
  257. return CT::getName($id);
  258. }
  259. $name = token_name($id);
  260. return 'UNKNOWN' === $name ? null : $name;
  261. }
  262. /**
  263. * Generate array containing all keywords that exists in PHP version in use.
  264. *
  265. * @return list<int>
  266. */
  267. public static function getKeywords(): array
  268. {
  269. static $keywords = null;
  270. if (null === $keywords) {
  271. $keywords = self::getTokenKindsForNames(['T_ABSTRACT', 'T_ARRAY', 'T_AS', 'T_BREAK', 'T_CALLABLE', 'T_CASE',
  272. 'T_CATCH', 'T_CLASS', 'T_CLONE', 'T_CONST', 'T_CONTINUE', 'T_DECLARE', 'T_DEFAULT', 'T_DO',
  273. 'T_ECHO', 'T_ELSE', 'T_ELSEIF', 'T_EMPTY', 'T_ENDDECLARE', 'T_ENDFOR', 'T_ENDFOREACH',
  274. 'T_ENDIF', 'T_ENDSWITCH', 'T_ENDWHILE', 'T_EVAL', 'T_EXIT', 'T_EXTENDS', 'T_FINAL',
  275. 'T_FINALLY', 'T_FN', 'T_FOR', 'T_FOREACH', 'T_FUNCTION', 'T_GLOBAL', 'T_GOTO', 'T_HALT_COMPILER',
  276. 'T_IF', 'T_IMPLEMENTS', 'T_INCLUDE', 'T_INCLUDE_ONCE', 'T_INSTANCEOF', 'T_INSTEADOF',
  277. 'T_INTERFACE', 'T_ISSET', 'T_LIST', 'T_LOGICAL_AND', 'T_LOGICAL_OR', 'T_LOGICAL_XOR',
  278. 'T_NAMESPACE', 'T_MATCH', 'T_NEW', 'T_PRINT', 'T_PRIVATE', 'T_PROTECTED', 'T_PUBLIC', 'T_REQUIRE',
  279. 'T_REQUIRE_ONCE', 'T_RETURN', 'T_STATIC', 'T_SWITCH', 'T_THROW', 'T_TRAIT', 'T_TRY',
  280. 'T_UNSET', 'T_USE', 'T_VAR', 'T_WHILE', 'T_YIELD', 'T_YIELD_FROM', 'T_READONLY', 'T_ENUM',
  281. ]) + [
  282. CT::T_ARRAY_TYPEHINT => CT::T_ARRAY_TYPEHINT,
  283. CT::T_CLASS_CONSTANT => CT::T_CLASS_CONSTANT,
  284. CT::T_CONST_IMPORT => CT::T_CONST_IMPORT,
  285. CT::T_CONSTRUCTOR_PROPERTY_PROMOTION_PRIVATE => CT::T_CONSTRUCTOR_PROPERTY_PROMOTION_PRIVATE,
  286. CT::T_CONSTRUCTOR_PROPERTY_PROMOTION_PROTECTED => CT::T_CONSTRUCTOR_PROPERTY_PROMOTION_PROTECTED,
  287. CT::T_CONSTRUCTOR_PROPERTY_PROMOTION_PUBLIC => CT::T_CONSTRUCTOR_PROPERTY_PROMOTION_PUBLIC,
  288. CT::T_FUNCTION_IMPORT => CT::T_FUNCTION_IMPORT,
  289. CT::T_NAMESPACE_OPERATOR => CT::T_NAMESPACE_OPERATOR,
  290. CT::T_USE_LAMBDA => CT::T_USE_LAMBDA,
  291. CT::T_USE_TRAIT => CT::T_USE_TRAIT,
  292. ];
  293. }
  294. return $keywords;
  295. }
  296. /**
  297. * Generate array containing all predefined constants that exists in PHP version in use.
  298. *
  299. * @see https://php.net/manual/en/language.constants.predefined.php
  300. *
  301. * @return array<int, int>
  302. */
  303. public static function getMagicConstants(): array
  304. {
  305. static $magicConstants = null;
  306. if (null === $magicConstants) {
  307. $magicConstants = self::getTokenKindsForNames(['T_CLASS_C', 'T_DIR', 'T_FILE', 'T_FUNC_C', 'T_LINE', 'T_METHOD_C', 'T_NS_C', 'T_TRAIT_C']);
  308. }
  309. return $magicConstants;
  310. }
  311. /**
  312. * Check if token prototype is an array.
  313. *
  314. * @return bool is array
  315. */
  316. public function isArray(): bool
  317. {
  318. return $this->isArray;
  319. }
  320. /**
  321. * Check if token is one of type cast tokens.
  322. */
  323. public function isCast(): bool
  324. {
  325. return $this->isGivenKind(self::getCastTokenKinds());
  326. }
  327. /**
  328. * Check if token is one of classy tokens: T_CLASS, T_INTERFACE, T_TRAIT or T_ENUM.
  329. */
  330. public function isClassy(): bool
  331. {
  332. return $this->isGivenKind(self::getClassyTokenKinds());
  333. }
  334. /**
  335. * Check if token is one of comment tokens: T_COMMENT or T_DOC_COMMENT.
  336. */
  337. public function isComment(): bool
  338. {
  339. static $commentTokens = [T_COMMENT, T_DOC_COMMENT];
  340. return $this->isGivenKind($commentTokens);
  341. }
  342. /**
  343. * Check if token is one of object operator tokens: T_OBJECT_OPERATOR or T_NULLSAFE_OBJECT_OPERATOR.
  344. */
  345. public function isObjectOperator(): bool
  346. {
  347. return $this->isGivenKind(self::getObjectOperatorKinds());
  348. }
  349. /**
  350. * Check if token is one of given kind.
  351. *
  352. * @param int|list<int> $possibleKind kind or array of kinds
  353. */
  354. public function isGivenKind($possibleKind): bool
  355. {
  356. return $this->isArray && (\is_array($possibleKind) ? \in_array($this->id, $possibleKind, true) : $this->id === $possibleKind);
  357. }
  358. /**
  359. * Check if token is a keyword.
  360. */
  361. public function isKeyword(): bool
  362. {
  363. $keywords = self::getKeywords();
  364. return $this->isArray && isset($keywords[$this->id]);
  365. }
  366. /**
  367. * Check if token is a native PHP constant: true, false or null.
  368. */
  369. public function isNativeConstant(): bool
  370. {
  371. static $nativeConstantStrings = ['true', 'false', 'null'];
  372. return $this->isArray && \in_array(strtolower($this->content), $nativeConstantStrings, true);
  373. }
  374. /**
  375. * Returns if the token is of a Magic constants type.
  376. *
  377. * @see https://php.net/manual/en/language.constants.predefined.php
  378. */
  379. public function isMagicConstant(): bool
  380. {
  381. $magicConstants = self::getMagicConstants();
  382. return $this->isArray && isset($magicConstants[$this->id]);
  383. }
  384. /**
  385. * Check if token is whitespace.
  386. *
  387. * @param null|string $whitespaces whitespace characters, default is " \t\n\r\0\x0B"
  388. */
  389. public function isWhitespace(?string $whitespaces = " \t\n\r\0\x0B"): bool
  390. {
  391. if (null === $whitespaces) {
  392. $whitespaces = " \t\n\r\0\x0B";
  393. }
  394. if ($this->isArray && !$this->isGivenKind(T_WHITESPACE)) {
  395. return false;
  396. }
  397. return '' === trim($this->content, $whitespaces);
  398. }
  399. /**
  400. * @return array{
  401. * id: int|null,
  402. * name: string|null,
  403. * content: string,
  404. * isArray: bool,
  405. * changed: bool,
  406. * }
  407. */
  408. public function toArray(): array
  409. {
  410. return [
  411. 'id' => $this->id,
  412. 'name' => $this->getName(),
  413. 'content' => $this->content,
  414. 'isArray' => $this->isArray,
  415. 'changed' => $this->changed,
  416. ];
  417. }
  418. public function toJson(): string
  419. {
  420. $jsonResult = json_encode($this->toArray(), JSON_PRETTY_PRINT | JSON_NUMERIC_CHECK);
  421. if (JSON_ERROR_NONE !== json_last_error()) {
  422. $jsonResult = json_encode(
  423. [
  424. 'errorDescription' => 'Cannot encode Tokens to JSON.',
  425. 'rawErrorMessage' => json_last_error_msg(),
  426. ],
  427. JSON_PRETTY_PRINT | JSON_NUMERIC_CHECK
  428. );
  429. }
  430. return $jsonResult;
  431. }
  432. /**
  433. * @param list<string> $tokenNames
  434. *
  435. * @return array<int, int>
  436. */
  437. private static function getTokenKindsForNames(array $tokenNames): array
  438. {
  439. $keywords = [];
  440. foreach ($tokenNames as $keywordName) {
  441. if (\defined($keywordName)) {
  442. $keyword = \constant($keywordName);
  443. $keywords[$keyword] = $keyword;
  444. }
  445. }
  446. return $keywords;
  447. }
  448. }