EmulativeTest.php 14 KB

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