TokensTest.php 28 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027
  1. <?php
  2. /*
  3. * This file is part of PHP CS Fixer.
  4. *
  5. * (c) Fabien Potencier <fabien@symfony.com>
  6. * Dariusz Rumiński <dariusz.ruminski@gmail.com>
  7. *
  8. * This source file is subject to the MIT license that is bundled
  9. * with this source code in the file LICENSE.
  10. */
  11. namespace PhpCsFixer\Tests\Tokenizer;
  12. use PhpCsFixer\Test\Assert\AssertTokensTrait;
  13. use PhpCsFixer\Tokenizer\Token;
  14. use PhpCsFixer\Tokenizer\Tokens;
  15. use PHPUnit\Framework\TestCase;
  16. /**
  17. * @author Dariusz Rumiński <dariusz.ruminski@gmail.com>
  18. * @author SpacePossum
  19. *
  20. * @internal
  21. *
  22. * @covers \PhpCsFixer\Tokenizer\Tokens
  23. */
  24. final class TokensTest extends TestCase
  25. {
  26. use AssertTokensTrait;
  27. public function testReadFromCacheAfterClearing()
  28. {
  29. $code = '<?php echo 1;';
  30. $tokens = Tokens::fromCode($code);
  31. $countBefore = $tokens->count();
  32. for ($i = 0; $i < $countBefore; ++$i) {
  33. $tokens[$i]->clear();
  34. }
  35. $tokens = Tokens::fromCode($code);
  36. $this->assertSame($countBefore, $tokens->count());
  37. }
  38. /**
  39. * @param string $source
  40. * @param null|array $expected
  41. * @param Token[] $sequence
  42. * @param int $start
  43. * @param int|null $end
  44. * @param bool|array $caseSensitive
  45. *
  46. * @dataProvider provideFindSequence
  47. */
  48. public function testFindSequence(
  49. $source,
  50. array $expected = null,
  51. array $sequence,
  52. $start = 0,
  53. $end = null,
  54. $caseSensitive = true
  55. ) {
  56. $tokens = Tokens::fromCode($source);
  57. $this->assertEqualsTokensArray(
  58. $expected,
  59. $tokens->findSequence(
  60. $sequence,
  61. $start,
  62. $end,
  63. $caseSensitive
  64. )
  65. );
  66. }
  67. public function provideFindSequence()
  68. {
  69. return [
  70. [
  71. '<?php $x = 1;',
  72. null,
  73. [
  74. new Token(';'),
  75. ],
  76. 7,
  77. ],
  78. [
  79. '<?php $x = 2;',
  80. null,
  81. [
  82. [T_OPEN_TAG],
  83. [T_VARIABLE, '$y'],
  84. ],
  85. ],
  86. [
  87. '<?php $x = 3;',
  88. [
  89. 0 => new Token([T_OPEN_TAG, '<?php ']),
  90. 1 => new Token([T_VARIABLE, '$x']),
  91. ],
  92. [
  93. [T_OPEN_TAG],
  94. [T_VARIABLE, '$x'],
  95. ],
  96. ],
  97. [
  98. '<?php $x = 4;',
  99. [
  100. 3 => new Token('='),
  101. 5 => new Token([T_LNUMBER, '4']),
  102. 6 => new Token(';'),
  103. ],
  104. [
  105. '=',
  106. [T_LNUMBER, '4'],
  107. ';',
  108. ],
  109. ],
  110. [
  111. '<?php $x = 5;',
  112. [
  113. 0 => new Token([T_OPEN_TAG, '<?php ']),
  114. 1 => new Token([T_VARIABLE, '$x']),
  115. ],
  116. [
  117. [T_OPEN_TAG],
  118. [T_VARIABLE, '$x'],
  119. ],
  120. 0,
  121. ],
  122. [
  123. '<?php $x = 6;',
  124. null,
  125. [
  126. [T_OPEN_TAG],
  127. [T_VARIABLE, '$x'],
  128. ],
  129. 1,
  130. ],
  131. [
  132. '<?php $x = 7;',
  133. [
  134. 3 => new Token('='),
  135. 5 => new Token([T_LNUMBER, '7']),
  136. 6 => new Token(';'),
  137. ],
  138. [
  139. '=',
  140. [T_LNUMBER, '7'],
  141. ';',
  142. ],
  143. 3,
  144. 6,
  145. ],
  146. [
  147. '<?php $x = 8;',
  148. null,
  149. [
  150. '=',
  151. [T_LNUMBER, '8'],
  152. ';',
  153. ],
  154. 4,
  155. 6,
  156. ],
  157. [
  158. '<?php $x = 9;',
  159. null,
  160. [
  161. '=',
  162. [T_LNUMBER, '9'],
  163. ';',
  164. ],
  165. 3,
  166. 5,
  167. ],
  168. [
  169. '<?php $x = 10;',
  170. [
  171. 0 => new Token([T_OPEN_TAG, '<?php ']),
  172. 1 => new Token([T_VARIABLE, '$x']),
  173. ],
  174. [
  175. [T_OPEN_TAG],
  176. [T_VARIABLE, '$x'],
  177. ],
  178. 0,
  179. 1,
  180. true,
  181. ],
  182. [
  183. '<?php $x = 11;',
  184. null,
  185. [
  186. [T_OPEN_TAG],
  187. [T_VARIABLE, '$X'],
  188. ],
  189. 0,
  190. 1,
  191. true,
  192. ],
  193. [
  194. '<?php $x = 12;',
  195. null,
  196. [
  197. [T_OPEN_TAG],
  198. [T_VARIABLE, '$X'],
  199. ],
  200. 0,
  201. 1,
  202. [1, true],
  203. ],
  204. [
  205. '<?php $x = 13;',
  206. [
  207. 0 => new Token([T_OPEN_TAG, '<?php ']),
  208. 1 => new Token([T_VARIABLE, '$x']),
  209. ],
  210. [
  211. [T_OPEN_TAG],
  212. [T_VARIABLE, '$X'],
  213. ],
  214. 0,
  215. 1,
  216. false,
  217. ],
  218. [
  219. '<?php $x = 14;',
  220. [
  221. 0 => new Token([T_OPEN_TAG, '<?php ']),
  222. 1 => new Token([T_VARIABLE, '$x']),
  223. ],
  224. [
  225. [T_OPEN_TAG],
  226. [T_VARIABLE, '$X'],
  227. ],
  228. 0,
  229. 1,
  230. [1, false],
  231. ],
  232. [
  233. '<?php $x = 15;',
  234. [
  235. 0 => new Token([T_OPEN_TAG, '<?php ']),
  236. 1 => new Token([T_VARIABLE, '$x']),
  237. ],
  238. [
  239. [T_OPEN_TAG],
  240. [T_VARIABLE, '$X'],
  241. ],
  242. 0,
  243. 1,
  244. [1 => false],
  245. ],
  246. [
  247. '<?php $x = 16;',
  248. null,
  249. [
  250. [T_OPEN_TAG],
  251. [T_VARIABLE, '$X'],
  252. ],
  253. 0,
  254. 1,
  255. [2 => false],
  256. ],
  257. [
  258. '<?php $x = 17;',
  259. null,
  260. [
  261. [T_VARIABLE, '$X'],
  262. '=',
  263. ],
  264. 0,
  265. 10,
  266. ],
  267. ];
  268. }
  269. /**
  270. * @param string $message
  271. * @param array $sequence
  272. *
  273. * @dataProvider provideFindSequenceExceptions
  274. */
  275. public function testFindSequenceException($message, array $sequence)
  276. {
  277. $this->setExpectedException(
  278. \InvalidArgumentException::class,
  279. $message
  280. );
  281. $tokens = Tokens::fromCode('<?php $x = 1;');
  282. $tokens->findSequence($sequence);
  283. }
  284. public function provideFindSequenceExceptions()
  285. {
  286. $emptyToken = new Token('!');
  287. $emptyToken->clear();
  288. return [
  289. ['Invalid sequence.', []],
  290. ['Non-meaningful token at position: 0.', [
  291. [T_WHITESPACE, ' '],
  292. ]],
  293. ['Non-meaningful token at position: 1.', [
  294. '{', [T_COMMENT, '// Foo'], '}',
  295. ]],
  296. ['Non-meaningful token at position: 2.', [
  297. '{', '!', $emptyToken, '}',
  298. ]],
  299. ];
  300. }
  301. public function testClearRange()
  302. {
  303. $source = <<<'PHP'
  304. <?php
  305. class FooBar
  306. {
  307. public function foo()
  308. {
  309. return 'bar';
  310. }
  311. public function bar()
  312. {
  313. return 'foo';
  314. }
  315. }
  316. PHP;
  317. $tokens = Tokens::fromCode($source);
  318. list($fooIndex, $barIndex) = array_keys($tokens->findGivenKind(T_PUBLIC));
  319. $tokens->clearRange($fooIndex, $barIndex - 1);
  320. $newPublicIndexes = array_keys($tokens->findGivenKind(T_PUBLIC));
  321. $this->assertSame($barIndex, reset($newPublicIndexes));
  322. for ($i = $fooIndex; $i < $barIndex; ++$i) {
  323. $this->assertTrue($tokens[$i]->isWhitespace());
  324. }
  325. }
  326. /**
  327. * @dataProvider provideMonolithicPhpDetection
  328. *
  329. * @param string $source
  330. * @param bool $monolithic
  331. */
  332. public function testMonolithicPhpDetection($source, $monolithic)
  333. {
  334. $tokens = Tokens::fromCode($source);
  335. $this->assertSame($monolithic, $tokens->isMonolithicPhp());
  336. }
  337. public function provideMonolithicPhpDetection()
  338. {
  339. return [
  340. ["<?php\n", true],
  341. ["<?php\n?>", true],
  342. ['', false],
  343. [' ', false],
  344. ["#!/usr/bin/env php\n<?php\n", false],
  345. [" <?php\n", false],
  346. ["<?php\n?> ", false],
  347. ["<?php\n?><?php\n", false],
  348. ];
  349. }
  350. /**
  351. * @dataProvider provideShortOpenTagMonolithicPhpDetection
  352. *
  353. * @param string $source
  354. * @param bool $monolithic
  355. */
  356. public function testShortOpenTagMonolithicPhpDetection($source, $monolithic)
  357. {
  358. /*
  359. * short_open_tag setting is ignored by HHVM
  360. * @see https://github.com/facebook/hhvm/issues/4758
  361. */
  362. if (!ini_get('short_open_tag') && !defined('HHVM_VERSION')) {
  363. // Short open tag is parsed as T_INLINE_HTML
  364. $monolithic = false;
  365. }
  366. $tokens = Tokens::fromCode($source);
  367. $this->assertSame($monolithic, $tokens->isMonolithicPhp());
  368. }
  369. public function provideShortOpenTagMonolithicPhpDetection()
  370. {
  371. return [
  372. ["<?\n", true],
  373. ["<?\n?>", true],
  374. [" <?\n", false],
  375. ["<?\n?> ", false],
  376. ["<?\n?><?\n", false],
  377. ["<?\n?><?php\n", false],
  378. ["<?\n?><?=' ';\n", false],
  379. ["<?php\n?><?\n", false],
  380. ["<?=' '\n?><?\n", false],
  381. ];
  382. }
  383. /**
  384. * @dataProvider provideShortOpenTagEchoMonolithicPhpDetection
  385. *
  386. * @param string $source
  387. * @param bool $monolithic
  388. */
  389. public function testShortOpenTagEchoMonolithicPhpDetection($source, $monolithic)
  390. {
  391. $tokens = Tokens::fromCode($source);
  392. $this->assertSame($monolithic, $tokens->isMonolithicPhp());
  393. }
  394. public function provideShortOpenTagEchoMonolithicPhpDetection()
  395. {
  396. return [
  397. ["<?=' ';\n", true],
  398. ["<?=' '?>", true],
  399. [" <?=' ';\n", false],
  400. ["<?=' '?> ", false],
  401. ["<?php\n?><?=' ';\n", false],
  402. ["<?=' '\n?><?php\n", false],
  403. ["<?=' '\n?><?=' ';\n", false],
  404. ];
  405. }
  406. public function testTokenKindsFound()
  407. {
  408. $code = <<<'EOF'
  409. <?php
  410. class Foo
  411. {
  412. public $foo;
  413. }
  414. if (!function_exists('bar')) {
  415. function bar()
  416. {
  417. return 'bar';
  418. }
  419. }
  420. EOF;
  421. $tokens = Tokens::fromCode($code);
  422. $this->assertTrue($tokens->isTokenKindFound(T_CLASS));
  423. $this->assertTrue($tokens->isTokenKindFound(T_RETURN));
  424. $this->assertFalse($tokens->isTokenKindFound(T_INTERFACE));
  425. $this->assertFalse($tokens->isTokenKindFound(T_ARRAY));
  426. $this->assertTrue($tokens->isAllTokenKindsFound([T_CLASS, T_RETURN]));
  427. $this->assertFalse($tokens->isAllTokenKindsFound([T_CLASS, T_INTERFACE]));
  428. $this->assertTrue($tokens->isAnyTokenKindsFound([T_CLASS, T_RETURN]));
  429. $this->assertTrue($tokens->isAnyTokenKindsFound([T_CLASS, T_INTERFACE]));
  430. $this->assertFalse($tokens->isAnyTokenKindsFound([T_INTERFACE, T_ARRAY]));
  431. }
  432. public function testFindGivenKind()
  433. {
  434. $source = <<<'PHP'
  435. <?php
  436. class FooBar
  437. {
  438. public function foo()
  439. {
  440. return 'bar';
  441. }
  442. public function bar()
  443. {
  444. return 'foo';
  445. }
  446. }
  447. PHP;
  448. $tokens = Tokens::fromCode($source);
  449. /** @var Token[] $found */
  450. $found = $tokens->findGivenKind(T_CLASS);
  451. $this->assertInternalType('array', $found);
  452. $this->assertCount(1, $found);
  453. $this->assertArrayHasKey(1, $found);
  454. $this->assertSame(T_CLASS, $found[1]->getId());
  455. /** @var array $found */
  456. $found = $tokens->findGivenKind([T_CLASS, T_FUNCTION]);
  457. $this->assertCount(2, $found);
  458. $this->assertArrayHasKey(T_CLASS, $found);
  459. $this->assertInternalType('array', $found[T_CLASS]);
  460. $this->assertCount(1, $found[T_CLASS]);
  461. $this->assertArrayHasKey(1, $found[T_CLASS]);
  462. $this->assertSame(T_CLASS, $found[T_CLASS][1]->getId());
  463. $this->assertArrayHasKey(T_FUNCTION, $found);
  464. $this->assertInternalType('array', $found[T_FUNCTION]);
  465. $this->assertCount(2, $found[T_FUNCTION]);
  466. $this->assertArrayHasKey(9, $found[T_FUNCTION]);
  467. $this->assertSame(T_FUNCTION, $found[T_FUNCTION][9]->getId());
  468. $this->assertArrayHasKey(26, $found[T_FUNCTION]);
  469. $this->assertSame(T_FUNCTION, $found[T_FUNCTION][26]->getId());
  470. // test offset and limits of the search
  471. $found = $tokens->findGivenKind([T_CLASS, T_FUNCTION], 10);
  472. $this->assertCount(0, $found[T_CLASS]);
  473. $this->assertCount(1, $found[T_FUNCTION]);
  474. $this->assertArrayHasKey(26, $found[T_FUNCTION]);
  475. $found = $tokens->findGivenKind([T_CLASS, T_FUNCTION], 2, 10);
  476. $this->assertCount(0, $found[T_CLASS]);
  477. $this->assertCount(1, $found[T_FUNCTION]);
  478. $this->assertArrayHasKey(9, $found[T_FUNCTION]);
  479. }
  480. /**
  481. * @param string $source
  482. * @param Token[] $expected tokens
  483. * @param int[] $indexes to clear
  484. *
  485. * @dataProvider getClearTokenAndMergeSurroundingWhitespaceCases
  486. */
  487. public function testClearTokenAndMergeSurroundingWhitespace($source, array $indexes, array $expected)
  488. {
  489. $this->doTestClearTokens($source, $indexes, $expected);
  490. if (count($indexes) > 1) {
  491. $this->doTestClearTokens($source, array_reverse($indexes), $expected);
  492. }
  493. }
  494. public function getClearTokenAndMergeSurroundingWhitespaceCases()
  495. {
  496. $clearToken = new Token([null, '']);
  497. $clearToken->clear();
  498. return [
  499. [
  500. '<?php if($a){}else{}',
  501. [7, 8, 9],
  502. [
  503. new Token([T_OPEN_TAG, '<?php ']),
  504. new Token([T_IF, 'if']),
  505. new Token('('),
  506. new Token([T_VARIABLE, '$a']),
  507. new Token(')'),
  508. new Token('{'),
  509. new Token('}'),
  510. $clearToken,
  511. $clearToken,
  512. $clearToken,
  513. ],
  514. ],
  515. [
  516. '<?php $a;/**/;',
  517. [2],
  518. [
  519. // <?php $a /**/;
  520. new Token([T_OPEN_TAG, '<?php ']),
  521. new Token([T_VARIABLE, '$a']),
  522. $clearToken,
  523. new Token([T_COMMENT, '/**/']),
  524. new Token(';'),
  525. ],
  526. ],
  527. [
  528. '<?php ; ; ;',
  529. [3],
  530. [
  531. // <?php ; ;
  532. new Token([T_OPEN_TAG, '<?php ']),
  533. new Token(';'),
  534. new Token([T_WHITESPACE, ' ']),
  535. $clearToken,
  536. $clearToken,
  537. new Token(';'),
  538. ],
  539. ],
  540. [
  541. '<?php ; ; ;',
  542. [1, 5],
  543. [
  544. // <?php ;
  545. new Token([T_OPEN_TAG, '<?php ']),
  546. new Token([T_WHITESPACE, ' ']),
  547. $clearToken,
  548. new Token(';'),
  549. new Token([T_WHITESPACE, ' ']),
  550. $clearToken,
  551. ],
  552. ],
  553. [
  554. '<?php ; ; ;',
  555. [1, 3],
  556. [
  557. // <?php ;
  558. new Token([T_OPEN_TAG, '<?php ']),
  559. new Token([T_WHITESPACE, ' ']),
  560. $clearToken,
  561. $clearToken,
  562. $clearToken,
  563. new Token(';'),
  564. ],
  565. ],
  566. [
  567. '<?php ; ; ;',
  568. [1],
  569. [
  570. // <?php ; ;
  571. new Token([T_OPEN_TAG, '<?php ']),
  572. new Token([T_WHITESPACE, ' ']),
  573. $clearToken,
  574. new Token(';'),
  575. new Token([T_WHITESPACE, ' ']),
  576. new Token(';'),
  577. ],
  578. ],
  579. ];
  580. }
  581. /**
  582. * @param int $expectedIndex
  583. * @param int $direction
  584. * @param int $index
  585. * @param array $findTokens
  586. * @param bool $caseSensitive
  587. *
  588. * @dataProvider provideTokenOfKindSiblingCases
  589. */
  590. public function testTokenOfKindSibling(
  591. $expectedIndex,
  592. $direction,
  593. $index,
  594. array $findTokens,
  595. $caseSensitive = true
  596. ) {
  597. $source =
  598. '<?php
  599. $a = function ($b) {
  600. return $b;
  601. };
  602. echo $a(1);
  603. // test
  604. return 123;';
  605. Tokens::clearCache();
  606. $tokens = Tokens::fromCode($source);
  607. if (1 === $direction) {
  608. $this->assertSame($expectedIndex, $tokens->getNextTokenOfKind($index, $findTokens, $caseSensitive));
  609. } else {
  610. $this->assertSame($expectedIndex, $tokens->getPrevTokenOfKind($index, $findTokens, $caseSensitive));
  611. }
  612. $this->assertSame($expectedIndex, $tokens->getTokenOfKindSibling($index, $direction, $findTokens, $caseSensitive));
  613. }
  614. public function provideTokenOfKindSiblingCases()
  615. {
  616. return [
  617. // find next cases
  618. [
  619. 35, 1, 34, [';'],
  620. ],
  621. [
  622. 14, 1, 0, [[T_RETURN]],
  623. ],
  624. [
  625. 32, 1, 14, [[T_RETURN]],
  626. ],
  627. [
  628. 6, 1, 0, [[T_RETURN], [T_FUNCTION]],
  629. ],
  630. // find previous cases
  631. [
  632. 14, -1, 32, [[T_RETURN], [T_FUNCTION]],
  633. ],
  634. [
  635. 6, -1, 7, [[T_FUNCTION]],
  636. ],
  637. [
  638. null, -1, 6, [[T_FUNCTION]],
  639. ],
  640. ];
  641. }
  642. /**
  643. * @param int $expectedIndex
  644. * @param string $source
  645. * @param int $type
  646. * @param int $searchIndex
  647. *
  648. * @dataProvider provideFindBlockEndCases
  649. */
  650. public function testFindBlockEnd($expectedIndex, $source, $type, $searchIndex)
  651. {
  652. $this->assertFindBlockEnd($expectedIndex, $source, $type, $searchIndex);
  653. }
  654. public function provideFindBlockEndCases()
  655. {
  656. return [
  657. [4, '<?php ${$bar};', Tokens::BLOCK_TYPE_DYNAMIC_VAR_BRACE, 2],
  658. [4, '<?php test(1);', Tokens::BLOCK_TYPE_PARENTHESIS_BRACE, 2],
  659. [4, '<?php $a{1};', Tokens::BLOCK_TYPE_ARRAY_INDEX_CURLY_BRACE, 2],
  660. [4, '<?php $a[1];', Tokens::BLOCK_TYPE_INDEX_SQUARE_BRACE, 2],
  661. [6, '<?php [1, "foo"];', Tokens::BLOCK_TYPE_ARRAY_SQUARE_BRACE, 1],
  662. [5, '<?php $foo->{$bar};', Tokens::BLOCK_TYPE_DYNAMIC_PROP_BRACE, 3],
  663. [4, '<?php list($a) = $b;', Tokens::BLOCK_TYPE_PARENTHESIS_BRACE, 2],
  664. [6, '<?php if($a){}?>', Tokens::BLOCK_TYPE_CURLY_BRACE, 5],
  665. ];
  666. }
  667. /**
  668. * @param int $expectedIndex
  669. * @param string $source
  670. * @param int $type
  671. * @param int $searchIndex
  672. *
  673. * @requires PHP 7.1
  674. * @dataProvider provideFindBlockEndCases71
  675. */
  676. public function testFindBlockEnd71($expectedIndex, $source, $type, $searchIndex)
  677. {
  678. $this->assertFindBlockEnd($expectedIndex, $source, $type, $searchIndex);
  679. }
  680. public function provideFindBlockEndCases71()
  681. {
  682. return [
  683. [10, '<?php use a\{ClassA, ClassB};', Tokens::BLOCK_TYPE_GROUP_IMPORT_BRACE, 5],
  684. [3, '<?php [$a] = $array;', Tokens::BLOCK_TYPE_DESTRUCTURING_SQUARE_BRACE, 1],
  685. ];
  686. }
  687. public function testFindBlockEndInvalidType()
  688. {
  689. $this->setExpectedExceptionRegExp(
  690. \InvalidArgumentException::class,
  691. '/^Invalid param type: -1\.$/'
  692. );
  693. Tokens::clearCache();
  694. $tokens = Tokens::fromCode('<?php ');
  695. $tokens->findBlockEnd(-1, 0);
  696. }
  697. public function testFindBlockEndInvalidStart()
  698. {
  699. $this->setExpectedExceptionRegExp(
  700. \InvalidArgumentException::class,
  701. '/^Invalid param \$startIndex - not a proper block start\.$/'
  702. );
  703. Tokens::clearCache();
  704. $tokens = Tokens::fromCode('<?php ');
  705. $tokens->findBlockEnd(Tokens::BLOCK_TYPE_DYNAMIC_VAR_BRACE, 0);
  706. }
  707. public function testParsingWithHHError()
  708. {
  709. if (!defined('HHVM_VERSION')) {
  710. $this->markTestSkipped('Skip tests for PHP compiler when running on non HHVM compiler.');
  711. }
  712. $this->setExpectedException(\ParseError::class);
  713. Tokens::fromCode('<?php# this will cause T_HH_ERROR');
  714. }
  715. public function testEmptyTokens()
  716. {
  717. $code = '';
  718. $tokens = Tokens::fromCode($code);
  719. $this->assertCount(0, $tokens);
  720. $this->assertFalse($tokens->isTokenKindFound(T_OPEN_TAG));
  721. }
  722. public function testEmptyTokensMultiple()
  723. {
  724. $code = '';
  725. $tokens = Tokens::fromCode($code);
  726. $tokens->insertAt(0, new Token([T_WHITESPACE, ' ']));
  727. $this->assertCount(1, $tokens);
  728. $this->assertFalse($tokens->isTokenKindFound(T_OPEN_TAG));
  729. $tokens2 = Tokens::fromCode($code);
  730. $this->assertCount(0, $tokens2);
  731. $this->assertFalse($tokens->isTokenKindFound(T_OPEN_TAG));
  732. }
  733. public function testFromArray()
  734. {
  735. $code = '<?php echo 1;';
  736. $tokens1 = Tokens::fromCode($code);
  737. $tokens2 = Tokens::fromArray($tokens1->toArray());
  738. $this->assertTrue($tokens1->isTokenKindFound(T_OPEN_TAG));
  739. $this->assertTrue($tokens2->isTokenKindFound(T_OPEN_TAG));
  740. $this->assertSame($tokens1->getCodeHash(), $tokens2->getCodeHash());
  741. }
  742. public function testFromArrayEmpty()
  743. {
  744. $tokens = Tokens::fromArray([]);
  745. $this->assertFalse($tokens->isTokenKindFound(T_OPEN_TAG));
  746. }
  747. public function testClone()
  748. {
  749. $code = '<?php echo 1;';
  750. $tokens = Tokens::fromCode($code);
  751. $tokensClone = clone $tokens;
  752. $this->assertTrue($tokens->isTokenKindFound(T_OPEN_TAG));
  753. $this->assertTrue($tokensClone->isTokenKindFound(T_OPEN_TAG));
  754. $count = count($tokens);
  755. $this->assertCount($count, $tokensClone);
  756. for ($i = 0; $i < $count; ++$i) {
  757. $this->assertTrue($tokens[$i]->equals($tokensClone[$i]));
  758. $this->assertNotSame($tokens[$i], $tokensClone[$i]);
  759. }
  760. }
  761. /**
  762. * @param int $expectedIndex
  763. * @param string $source
  764. * @param int $type
  765. * @param int $searchIndex
  766. */
  767. public function assertFindBlockEnd($expectedIndex, $source, $type, $searchIndex)
  768. {
  769. Tokens::clearCache();
  770. $tokens = Tokens::fromCode($source);
  771. $this->assertSame($expectedIndex, $tokens->findBlockEnd($type, $searchIndex, true));
  772. $this->assertSame($searchIndex, $tokens->findBlockEnd($type, $expectedIndex, false));
  773. $detectedType = Tokens::detectBlockType($tokens[$searchIndex]);
  774. $this->assertInternalType('array', $detectedType);
  775. $this->assertArrayHasKey('type', $detectedType);
  776. $this->assertArrayHasKey('isStart', $detectedType);
  777. $this->assertSame($type, $detectedType['type']);
  778. $this->assertTrue($detectedType['isStart']);
  779. $detectedType = Tokens::detectBlockType($tokens[$expectedIndex]);
  780. $this->assertInternalType('array', $detectedType);
  781. $this->assertArrayHasKey('type', $detectedType);
  782. $this->assertArrayHasKey('isStart', $detectedType);
  783. $this->assertSame($type, $detectedType['type']);
  784. $this->assertFalse($detectedType['isStart']);
  785. }
  786. /**
  787. * @param string $expected valid PHP code
  788. * @param string $input valid PHP code
  789. * @param int $index token index
  790. * @param int $offset
  791. * @param string $whiteSpace white space
  792. *
  793. * @dataProvider provideEnsureWhitespaceAtIndexCases
  794. */
  795. public function testEnsureWhitespaceAtIndex($expected, $input, $index, $offset, $whiteSpace)
  796. {
  797. $tokens = Tokens::fromCode($input);
  798. $tokens->ensureWhitespaceAtIndex($index, $offset, $whiteSpace);
  799. $this->assertTokens(Tokens::fromCode($expected), $tokens);
  800. }
  801. public function provideEnsureWhitespaceAtIndexCases()
  802. {
  803. return [
  804. [
  805. '<?php $a. $b;',
  806. '<?php $a.$b;',
  807. 2,
  808. 1,
  809. ' ',
  810. ],
  811. [
  812. '<?php $a .$b;',
  813. '<?php $a.$b;',
  814. 2,
  815. 0,
  816. ' ',
  817. ],
  818. [
  819. "<?php\r\n",
  820. '<?php ',
  821. 0,
  822. 1,
  823. "\r\n",
  824. ],
  825. [
  826. '<?php $a.$b;',
  827. '<?php $a.$b;',
  828. 2,
  829. -1,
  830. ' ',
  831. ],
  832. [
  833. "<?php\t ",
  834. "<?php\n",
  835. 0,
  836. 1,
  837. "\t ",
  838. ],
  839. [
  840. '<?php ',
  841. '<?php ',
  842. 0,
  843. 1,
  844. ' ',
  845. ],
  846. [
  847. "<?php\n",
  848. '<?php ',
  849. 0,
  850. 1,
  851. "\n",
  852. ],
  853. [
  854. "<?php\t",
  855. '<?php ',
  856. 0,
  857. 1,
  858. "\t",
  859. ],
  860. [
  861. '<?php
  862. //
  863. echo $a;',
  864. '<?php
  865. //
  866. echo $a;',
  867. 2,
  868. 1,
  869. "\n ",
  870. ],
  871. [
  872. '<?php
  873. echo $a;',
  874. '<?php
  875. echo $a;',
  876. 0,
  877. 1,
  878. "\n ",
  879. ],
  880. [
  881. '<?php
  882. echo $a;',
  883. '<?php echo $a;',
  884. 0,
  885. 1,
  886. "\n",
  887. ],
  888. [
  889. "<?php\techo \$a;",
  890. '<?php echo $a;',
  891. 0,
  892. 1,
  893. "\t",
  894. ],
  895. ];
  896. }
  897. /**
  898. * @param null|Token[] $expected
  899. * @param null|Token[] $input
  900. */
  901. private function assertEqualsTokensArray(array $expected = null, array $input = null)
  902. {
  903. if (null === $expected) {
  904. $this->assertNull($input);
  905. return;
  906. } elseif (null === $input) {
  907. $this->fail('While "input" is <null>, "expected" is not.');
  908. }
  909. $this->assertSame(array_keys($expected), array_keys($input), 'Both arrays need to have same keys.');
  910. foreach ($expected as $index => $expectedToken) {
  911. $this->assertTrue(
  912. $expectedToken->equals($input[$index]),
  913. sprintf('The token at index %d should be %s, got %s', $index, $expectedToken->toJson(), $input[$index]->toJson())
  914. );
  915. }
  916. }
  917. /**
  918. * @param string $source
  919. * @param int[] $indexes
  920. * @param Token[] $expected
  921. */
  922. private function doTestClearTokens($source, array $indexes, array $expected)
  923. {
  924. Tokens::clearCache();
  925. $tokens = Tokens::fromCode($source);
  926. foreach ($indexes as $index) {
  927. $tokens->clearTokenAndMergeSurroundingWhitespace($index);
  928. }
  929. $this->assertSame(count($expected), $tokens->count());
  930. foreach ($expected as $index => $expectedToken) {
  931. $token = $tokens[$index];
  932. $expectedPrototype = $expectedToken->getPrototype();
  933. if (is_array($expectedPrototype)) {
  934. unset($expectedPrototype[2]); // don't compare token lines as our token mutations don't deal with line numbers
  935. }
  936. $this->assertTrue($token->equals($expectedPrototype), sprintf('The token at index %d should be %s, got %s', $index, json_encode($expectedPrototype), $token->toJson()));
  937. }
  938. }
  939. }