EmulativeTest.php 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429
  1. <?php declare(strict_types=1);
  2. namespace PhpParser\Lexer;
  3. use PhpParser\ErrorHandler;
  4. use PhpParser\Lexer;
  5. use PhpParser\LexerTest;
  6. use PhpParser\Parser\Php7;
  7. use PhpParser\PhpVersion;
  8. use PhpParser\Token;
  9. require __DIR__ . '/../../../lib/PhpParser/compatibility_tokens.php';
  10. class EmulativeTest extends LexerTest {
  11. protected function getLexer() {
  12. return new Emulative();
  13. }
  14. /**
  15. * @dataProvider provideTestReplaceKeywords
  16. */
  17. public function testReplaceKeywords(string $keyword, int $expectedToken): void {
  18. $lexer = $this->getLexer();
  19. $code = '<?php ' . $keyword;
  20. $this->assertEquals([
  21. new Token(\T_OPEN_TAG, '<?php ', 1, 0),
  22. new Token($expectedToken, $keyword, 1, 6),
  23. new Token(0, "\0", 1, \strlen($code)),
  24. ], $lexer->tokenize($code));
  25. }
  26. /**
  27. * @dataProvider provideTestReplaceKeywords
  28. */
  29. public function testReplaceKeywordsUppercase(string $keyword, int $expectedToken): void {
  30. $lexer = $this->getLexer();
  31. $code = '<?php ' . strtoupper($keyword);
  32. $this->assertEquals([
  33. new Token(\T_OPEN_TAG, '<?php ', 1, 0),
  34. new Token($expectedToken, \strtoupper($keyword), 1, 6),
  35. new Token(0, "\0", 1, \strlen($code)),
  36. ], $lexer->tokenize($code));
  37. }
  38. /**
  39. * @dataProvider provideTestReplaceKeywords
  40. */
  41. public function testNoReplaceKeywordsAfterObjectOperator(string $keyword): void {
  42. $lexer = $this->getLexer();
  43. $code = '<?php ->' . $keyword;
  44. $this->assertEquals([
  45. new Token(\T_OPEN_TAG, '<?php ', 1, 0),
  46. new Token(\T_OBJECT_OPERATOR, '->', 1, 6),
  47. new Token(\T_STRING, $keyword, 1, 8),
  48. new Token(0, "\0", 1, \strlen($code)),
  49. ], $lexer->tokenize($code));
  50. }
  51. /**
  52. * @dataProvider provideTestReplaceKeywords
  53. */
  54. public function testNoReplaceKeywordsAfterObjectOperatorWithSpaces(string $keyword): void {
  55. $lexer = $this->getLexer();
  56. $code = '<?php -> ' . $keyword;
  57. $this->assertEquals([
  58. new Token(\T_OPEN_TAG, '<?php ', 1, 0),
  59. new Token(\T_OBJECT_OPERATOR, '->', 1, 6),
  60. new Token(\T_WHITESPACE, ' ', 1, 8),
  61. new Token(\T_STRING, $keyword, 1, 12),
  62. new Token(0, "\0", 1, \strlen($code)),
  63. ], $lexer->tokenize($code));
  64. }
  65. /**
  66. * @dataProvider provideTestReplaceKeywords
  67. */
  68. public function testNoReplaceKeywordsAfterNullsafeObjectOperator(string $keyword): void {
  69. $lexer = $this->getLexer();
  70. $code = '<?php ?->' . $keyword;
  71. $this->assertEquals([
  72. new Token(\T_OPEN_TAG, '<?php ', 1, 0),
  73. new Token(\T_NULLSAFE_OBJECT_OPERATOR, '?->', 1, 6),
  74. new Token(\T_STRING, $keyword, 1, 9),
  75. new Token(0, "\0", 1, \strlen($code)),
  76. ], $lexer->tokenize($code));
  77. }
  78. public static function provideTestReplaceKeywords() {
  79. return [
  80. // PHP 8.0
  81. ['match', \T_MATCH],
  82. // PHP 7.4
  83. ['fn', \T_FN],
  84. // PHP 5.5
  85. ['finally', \T_FINALLY],
  86. ['yield', \T_YIELD],
  87. // PHP 5.4
  88. ['callable', \T_CALLABLE],
  89. ['insteadof', \T_INSTEADOF],
  90. ['trait', \T_TRAIT],
  91. ['__TRAIT__', \T_TRAIT_C],
  92. // PHP 5.3
  93. ['__DIR__', \T_DIR],
  94. ['goto', \T_GOTO],
  95. ['namespace', \T_NAMESPACE],
  96. ['__NAMESPACE__', \T_NS_C],
  97. ];
  98. }
  99. private function assertSameTokens(array $expectedTokens, array $tokens): void {
  100. $reducedTokens = [];
  101. foreach ($tokens as $token) {
  102. if ($token->id === 0 || $token->isIgnorable()) {
  103. continue;
  104. }
  105. $reducedTokens[] = [$token->id, $token->text];
  106. }
  107. $this->assertSame($expectedTokens, $reducedTokens);
  108. }
  109. /**
  110. * @dataProvider provideTestLexNewFeatures
  111. */
  112. public function testLexNewFeatures(string $code, array $expectedTokens): void {
  113. $lexer = $this->getLexer();
  114. $this->assertSameTokens($expectedTokens, $lexer->tokenize('<?php ' . $code));
  115. }
  116. /**
  117. * @dataProvider provideTestLexNewFeatures
  118. */
  119. public function testLeaveStuffAloneInStrings(string $code): void {
  120. $stringifiedToken = '"' . addcslashes($code, '"\\') . '"';
  121. $lexer = $this->getLexer();
  122. $fullCode = '<?php ' . $stringifiedToken;
  123. $this->assertEquals([
  124. new Token(\T_OPEN_TAG, '<?php ', 1, 0),
  125. new Token(\T_CONSTANT_ENCAPSED_STRING, $stringifiedToken, 1, 6),
  126. new Token(0, "\0", \substr_count($fullCode, "\n") + 1, \strlen($fullCode)),
  127. ], $lexer->tokenize($fullCode));
  128. }
  129. /**
  130. * @dataProvider provideTestLexNewFeatures
  131. */
  132. public function testErrorAfterEmulation($code): void {
  133. $errorHandler = new ErrorHandler\Collecting();
  134. $lexer = $this->getLexer();
  135. $lexer->tokenize('<?php ' . $code . "\0", $errorHandler);
  136. $errors = $errorHandler->getErrors();
  137. $this->assertCount(1, $errors);
  138. $error = $errors[0];
  139. $this->assertSame('Unexpected null byte', $error->getRawMessage());
  140. $attrs = $error->getAttributes();
  141. $expPos = strlen('<?php ' . $code);
  142. $expLine = 1 + substr_count('<?php ' . $code, "\n");
  143. $this->assertSame($expPos, $attrs['startFilePos']);
  144. $this->assertSame($expPos, $attrs['endFilePos']);
  145. $this->assertSame($expLine, $attrs['startLine']);
  146. $this->assertSame($expLine, $attrs['endLine']);
  147. }
  148. public static function provideTestLexNewFeatures() {
  149. return [
  150. ['yield from', [
  151. [\T_YIELD_FROM, 'yield from'],
  152. ]],
  153. ["yield\r\nfrom", [
  154. [\T_YIELD_FROM, "yield\r\nfrom"],
  155. ]],
  156. ['...', [
  157. [\T_ELLIPSIS, '...'],
  158. ]],
  159. ['**', [
  160. [\T_POW, '**'],
  161. ]],
  162. ['**=', [
  163. [\T_POW_EQUAL, '**='],
  164. ]],
  165. ['??', [
  166. [\T_COALESCE, '??'],
  167. ]],
  168. ['<=>', [
  169. [\T_SPACESHIP, '<=>'],
  170. ]],
  171. ['0b1010110', [
  172. [\T_LNUMBER, '0b1010110'],
  173. ]],
  174. ['0b1011010101001010110101010010101011010101010101101011001110111100', [
  175. [\T_DNUMBER, '0b1011010101001010110101010010101011010101010101101011001110111100'],
  176. ]],
  177. ['\\', [
  178. [\T_NS_SEPARATOR, '\\'],
  179. ]],
  180. ["<<<'NOWDOC'\nNOWDOC;\n", [
  181. [\T_START_HEREDOC, "<<<'NOWDOC'\n"],
  182. [\T_END_HEREDOC, 'NOWDOC'],
  183. [ord(';'), ';'],
  184. ]],
  185. ["<<<'NOWDOC'\nFoobar\nNOWDOC;\n", [
  186. [\T_START_HEREDOC, "<<<'NOWDOC'\n"],
  187. [\T_ENCAPSED_AND_WHITESPACE, "Foobar\n"],
  188. [\T_END_HEREDOC, 'NOWDOC'],
  189. [ord(';'), ';'],
  190. ]],
  191. // PHP 7.3: Flexible heredoc/nowdoc
  192. ["<<<LABEL\nLABEL,", [
  193. [\T_START_HEREDOC, "<<<LABEL\n"],
  194. [\T_END_HEREDOC, "LABEL"],
  195. [ord(','), ','],
  196. ]],
  197. ["<<<LABEL\n LABEL,", [
  198. [\T_START_HEREDOC, "<<<LABEL\n"],
  199. [\T_END_HEREDOC, " LABEL"],
  200. [ord(','), ','],
  201. ]],
  202. ["<<<LABEL\n Foo\n LABEL;", [
  203. [\T_START_HEREDOC, "<<<LABEL\n"],
  204. [\T_ENCAPSED_AND_WHITESPACE, " Foo\n"],
  205. [\T_END_HEREDOC, " LABEL"],
  206. [ord(';'), ';'],
  207. ]],
  208. ["<<<A\n A,<<<A\n A,", [
  209. [\T_START_HEREDOC, "<<<A\n"],
  210. [\T_END_HEREDOC, " A"],
  211. [ord(','), ','],
  212. [\T_START_HEREDOC, "<<<A\n"],
  213. [\T_END_HEREDOC, " A"],
  214. [ord(','), ','],
  215. ]],
  216. ["<<<LABEL\nLABELNOPE\nLABEL\n", [
  217. [\T_START_HEREDOC, "<<<LABEL\n"],
  218. [\T_ENCAPSED_AND_WHITESPACE, "LABELNOPE\n"],
  219. [\T_END_HEREDOC, "LABEL"],
  220. ]],
  221. // Interpretation changed
  222. ["<<<LABEL\n LABEL\nLABEL\n", [
  223. [\T_START_HEREDOC, "<<<LABEL\n"],
  224. [\T_END_HEREDOC, " LABEL"],
  225. [\T_STRING, "LABEL"],
  226. ]],
  227. // PHP 7.4: Null coalesce equal
  228. ['??=', [
  229. [\T_COALESCE_EQUAL, '??='],
  230. ]],
  231. // PHP 7.4: Number literal separator
  232. ['1_000', [
  233. [\T_LNUMBER, '1_000'],
  234. ]],
  235. ['0x7AFE_F00D', [
  236. [\T_LNUMBER, '0x7AFE_F00D'],
  237. ]],
  238. ['0b0101_1111', [
  239. [\T_LNUMBER, '0b0101_1111'],
  240. ]],
  241. ['0137_041', [
  242. [\T_LNUMBER, '0137_041'],
  243. ]],
  244. ['1_000.0', [
  245. [\T_DNUMBER, '1_000.0'],
  246. ]],
  247. ['1_0.0', [
  248. [\T_DNUMBER, '1_0.0']
  249. ]],
  250. ['1_000_000_000.0', [
  251. [\T_DNUMBER, '1_000_000_000.0']
  252. ]],
  253. ['0e1_0', [
  254. [\T_DNUMBER, '0e1_0']
  255. ]],
  256. ['1_0e+10', [
  257. [\T_DNUMBER, '1_0e+10']
  258. ]],
  259. ['1_0e-10', [
  260. [\T_DNUMBER, '1_0e-10']
  261. ]],
  262. ['0b1011010101001010_110101010010_10101101010101_0101101011001_110111100', [
  263. [\T_DNUMBER, '0b1011010101001010_110101010010_10101101010101_0101101011001_110111100'],
  264. ]],
  265. ['0xFFFF_FFFF_FFFF_FFFF', [
  266. [\T_DNUMBER, '0xFFFF_FFFF_FFFF_FFFF'],
  267. ]],
  268. ['1_000+1', [
  269. [\T_LNUMBER, '1_000'],
  270. [ord('+'), '+'],
  271. [\T_LNUMBER, '1'],
  272. ]],
  273. ['1_0abc', [
  274. [\T_LNUMBER, '1_0'],
  275. [\T_STRING, 'abc'],
  276. ]],
  277. ['?->', [
  278. [\T_NULLSAFE_OBJECT_OPERATOR, '?->'],
  279. ]],
  280. ['#[Attr]', [
  281. [\T_ATTRIBUTE, '#['],
  282. [\T_STRING, 'Attr'],
  283. [ord(']'), ']'],
  284. ]],
  285. ["#[\nAttr\n]", [
  286. [\T_ATTRIBUTE, '#['],
  287. [\T_STRING, 'Attr'],
  288. [ord(']'), ']'],
  289. ]],
  290. // Test interaction of two patch-based emulators
  291. ["<<<LABEL\n LABEL, #[Attr]", [
  292. [\T_START_HEREDOC, "<<<LABEL\n"],
  293. [\T_END_HEREDOC, " LABEL"],
  294. [ord(','), ','],
  295. [\T_ATTRIBUTE, '#['],
  296. [\T_STRING, 'Attr'],
  297. [ord(']'), ']'],
  298. ]],
  299. ["#[Attr] <<<LABEL\n LABEL,", [
  300. [\T_ATTRIBUTE, '#['],
  301. [\T_STRING, 'Attr'],
  302. [ord(']'), ']'],
  303. [\T_START_HEREDOC, "<<<LABEL\n"],
  304. [\T_END_HEREDOC, " LABEL"],
  305. [ord(','), ','],
  306. ]],
  307. // Enums use a contextual keyword
  308. ['enum Foo {}', [
  309. [\T_ENUM, 'enum'],
  310. [\T_STRING, 'Foo'],
  311. [ord('{'), '{'],
  312. [ord('}'), '}'],
  313. ]],
  314. ['class Enum {}', [
  315. [\T_CLASS, 'class'],
  316. [\T_STRING, 'Enum'],
  317. [ord('{'), '{'],
  318. [ord('}'), '}'],
  319. ]],
  320. ['class Enum extends X {}', [
  321. [\T_CLASS, 'class'],
  322. [\T_STRING, 'Enum'],
  323. [\T_EXTENDS, 'extends'],
  324. [\T_STRING, 'X'],
  325. [ord('{'), '{'],
  326. [ord('}'), '}'],
  327. ]],
  328. ['class Enum implements X {}', [
  329. [\T_CLASS, 'class'],
  330. [\T_STRING, 'Enum'],
  331. [\T_IMPLEMENTS, 'implements'],
  332. [\T_STRING, 'X'],
  333. [ord('{'), '{'],
  334. [ord('}'), '}'],
  335. ]],
  336. ['0o123', [
  337. [\T_LNUMBER, '0o123'],
  338. ]],
  339. ['0O123', [
  340. [\T_LNUMBER, '0O123'],
  341. ]],
  342. ['0o1_2_3', [
  343. [\T_LNUMBER, '0o1_2_3'],
  344. ]],
  345. ['0o1000000000000000000000', [
  346. [\T_DNUMBER, '0o1000000000000000000000'],
  347. ]],
  348. ['readonly class', [
  349. [\T_READONLY, 'readonly'],
  350. [\T_CLASS, 'class'],
  351. ]],
  352. ['function readonly(', [
  353. [\T_FUNCTION, 'function'],
  354. [\T_READONLY, 'readonly'],
  355. [ord('('), '('],
  356. ]],
  357. ['function readonly (', [
  358. [\T_FUNCTION, 'function'],
  359. [\T_READONLY, 'readonly'],
  360. [ord('('), '('],
  361. ]],
  362. ];
  363. }
  364. /**
  365. * @dataProvider provideTestTargetVersion
  366. */
  367. public function testTargetVersion(string $phpVersion, string $code, array $expectedTokens): void {
  368. $lexer = new Emulative(PhpVersion::fromString($phpVersion));
  369. $this->assertSameTokens($expectedTokens, $lexer->tokenize('<?php ' . $code));
  370. }
  371. public static function provideTestTargetVersion() {
  372. return [
  373. ['8.0', 'match', [[\T_MATCH, 'match']]],
  374. ['7.4', 'match', [[\T_STRING, 'match']]],
  375. // Keywords are not case-sensitive.
  376. ['8.0', 'MATCH', [[\T_MATCH, 'MATCH']]],
  377. ['7.4', 'MATCH', [[\T_STRING, 'MATCH']]],
  378. // Tested here to skip testLeaveStuffAloneInStrings.
  379. ['8.0', '"$foo?->bar"', [
  380. [ord('"'), '"'],
  381. [\T_VARIABLE, '$foo'],
  382. [\T_NULLSAFE_OBJECT_OPERATOR, '?->'],
  383. [\T_STRING, 'bar'],
  384. [ord('"'), '"'],
  385. ]],
  386. ['8.0', '"$foo?->bar baz"', [
  387. [ord('"'), '"'],
  388. [\T_VARIABLE, '$foo'],
  389. [\T_NULLSAFE_OBJECT_OPERATOR, '?->'],
  390. [\T_STRING, 'bar'],
  391. [\T_ENCAPSED_AND_WHITESPACE, ' baz'],
  392. [ord('"'), '"'],
  393. ]],
  394. ];
  395. }
  396. }