TokensTest.php 46 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592
  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\Tests\Tokenizer;
  13. use PhpCsFixer\Tests\Test\Assert\AssertTokensTrait;
  14. use PhpCsFixer\Tests\TestCase;
  15. use PhpCsFixer\Tokenizer\Token;
  16. use PhpCsFixer\Tokenizer\Tokens;
  17. /**
  18. * @author Dariusz Rumiński <dariusz.ruminski@gmail.com>
  19. *
  20. * @internal
  21. *
  22. * @covers \PhpCsFixer\Tokenizer\Tokens
  23. */
  24. final class TokensTest extends TestCase
  25. {
  26. use AssertTokensTrait;
  27. public function testReadFromCacheAfterClearing(): void
  28. {
  29. $code = '<?php echo 1;';
  30. $tokens = Tokens::fromCode($code);
  31. $countBefore = $tokens->count();
  32. for ($i = 0; $i < $countBefore; ++$i) {
  33. $tokens->clearAt($i);
  34. }
  35. $tokens = Tokens::fromCode($code);
  36. static::assertSame($countBefore, $tokens->count());
  37. }
  38. /**
  39. * @param Token[] $sequence
  40. * @param null|array|bool $caseSensitive
  41. *
  42. * @dataProvider provideFindSequenceCases
  43. */
  44. public function testFindSequence(
  45. string $source,
  46. ?array $expected,
  47. array $sequence,
  48. int $start = 0,
  49. int $end = null,
  50. $caseSensitive = true
  51. ): void {
  52. $tokens = Tokens::fromCode($source);
  53. static::assertEqualsTokensArray(
  54. $expected,
  55. $tokens->findSequence(
  56. $sequence,
  57. $start,
  58. $end,
  59. $caseSensitive
  60. )
  61. );
  62. }
  63. public function provideFindSequenceCases(): array
  64. {
  65. return [
  66. [
  67. '<?php $x = 1;',
  68. null,
  69. [
  70. new Token(';'),
  71. ],
  72. 7,
  73. ],
  74. [
  75. '<?php $x = 2;',
  76. null,
  77. [
  78. [T_OPEN_TAG],
  79. [T_VARIABLE, '$y'],
  80. ],
  81. ],
  82. [
  83. '<?php $x = 3;',
  84. [
  85. 0 => new Token([T_OPEN_TAG, '<?php ']),
  86. 1 => new Token([T_VARIABLE, '$x']),
  87. ],
  88. [
  89. [T_OPEN_TAG],
  90. [T_VARIABLE, '$x'],
  91. ],
  92. ],
  93. [
  94. '<?php $x = 4;',
  95. [
  96. 3 => new Token('='),
  97. 5 => new Token([T_LNUMBER, '4']),
  98. 6 => new Token(';'),
  99. ],
  100. [
  101. '=',
  102. [T_LNUMBER, '4'],
  103. ';',
  104. ],
  105. ],
  106. [
  107. '<?php $x = 5;',
  108. [
  109. 0 => new Token([T_OPEN_TAG, '<?php ']),
  110. 1 => new Token([T_VARIABLE, '$x']),
  111. ],
  112. [
  113. [T_OPEN_TAG],
  114. [T_VARIABLE, '$x'],
  115. ],
  116. 0,
  117. ],
  118. [
  119. '<?php $x = 6;',
  120. null,
  121. [
  122. [T_OPEN_TAG],
  123. [T_VARIABLE, '$x'],
  124. ],
  125. 1,
  126. ],
  127. [
  128. '<?php $x = 7;',
  129. [
  130. 3 => new Token('='),
  131. 5 => new Token([T_LNUMBER, '7']),
  132. 6 => new Token(';'),
  133. ],
  134. [
  135. '=',
  136. [T_LNUMBER, '7'],
  137. ';',
  138. ],
  139. 3,
  140. 6,
  141. ],
  142. [
  143. '<?php $x = 8;',
  144. null,
  145. [
  146. '=',
  147. [T_LNUMBER, '8'],
  148. ';',
  149. ],
  150. 4,
  151. 6,
  152. ],
  153. [
  154. '<?php $x = 9;',
  155. null,
  156. [
  157. '=',
  158. [T_LNUMBER, '9'],
  159. ';',
  160. ],
  161. 3,
  162. 5,
  163. ],
  164. [
  165. '<?php $x = 10;',
  166. [
  167. 0 => new Token([T_OPEN_TAG, '<?php ']),
  168. 1 => new Token([T_VARIABLE, '$x']),
  169. ],
  170. [
  171. [T_OPEN_TAG],
  172. [T_VARIABLE, '$x'],
  173. ],
  174. 0,
  175. 1,
  176. true,
  177. ],
  178. [
  179. '<?php $x = 11;',
  180. null,
  181. [
  182. [T_OPEN_TAG],
  183. [T_VARIABLE, '$X'],
  184. ],
  185. 0,
  186. 1,
  187. true,
  188. ],
  189. [
  190. '<?php $x = 12;',
  191. null,
  192. [
  193. [T_OPEN_TAG],
  194. [T_VARIABLE, '$X'],
  195. ],
  196. 0,
  197. 1,
  198. [1 => true],
  199. ],
  200. [
  201. '<?php $x = 13;',
  202. [
  203. 0 => new Token([T_OPEN_TAG, '<?php ']),
  204. 1 => new Token([T_VARIABLE, '$x']),
  205. ],
  206. [
  207. [T_OPEN_TAG],
  208. [T_VARIABLE, '$X'],
  209. ],
  210. 0,
  211. 1,
  212. false,
  213. ],
  214. [
  215. '<?php $x = 14;',
  216. [
  217. 0 => new Token([T_OPEN_TAG, '<?php ']),
  218. 1 => new Token([T_VARIABLE, '$x']),
  219. ],
  220. [
  221. [T_OPEN_TAG],
  222. [T_VARIABLE, '$X'],
  223. ],
  224. 0,
  225. 1,
  226. [1 => false],
  227. ],
  228. [
  229. '<?php $x = 15;',
  230. [
  231. 0 => new Token([T_OPEN_TAG, '<?php ']),
  232. 1 => new Token([T_VARIABLE, '$x']),
  233. ],
  234. [
  235. [T_OPEN_TAG],
  236. [T_VARIABLE, '$X'],
  237. ],
  238. 0,
  239. 1,
  240. [1 => false],
  241. ],
  242. [
  243. '<?php $x = 16;',
  244. null,
  245. [
  246. [T_OPEN_TAG],
  247. [T_VARIABLE, '$X'],
  248. ],
  249. 0,
  250. 1,
  251. [2 => false],
  252. ],
  253. [
  254. '<?php $x = 17;',
  255. null,
  256. [
  257. [T_VARIABLE, '$X'],
  258. '=',
  259. ],
  260. 0,
  261. 10,
  262. ],
  263. ];
  264. }
  265. /**
  266. * @dataProvider provideFindSequenceExceptionCases
  267. */
  268. public function testFindSequenceException(string $message, array $sequence): void
  269. {
  270. $this->expectException(\InvalidArgumentException::class);
  271. $this->expectExceptionMessage($message);
  272. $tokens = Tokens::fromCode('<?php $x = 1;');
  273. $tokens->findSequence($sequence);
  274. }
  275. public function provideFindSequenceExceptionCases(): array
  276. {
  277. $emptyToken = new Token('');
  278. return [
  279. ['Invalid sequence.', []],
  280. [
  281. 'Non-meaningful token at position: "0".',
  282. [[T_WHITESPACE, ' ']],
  283. ],
  284. [
  285. 'Non-meaningful token at position: "1".',
  286. ['{', [T_COMMENT, '// Foo'], '}'],
  287. ],
  288. [
  289. 'Non-meaningful (empty) token at position: "2".',
  290. ['{', '!', $emptyToken, '}'],
  291. ],
  292. ];
  293. }
  294. public function testClearRange(): void
  295. {
  296. $source = <<<'PHP'
  297. <?php
  298. class FooBar
  299. {
  300. public function foo()
  301. {
  302. return 'bar';
  303. }
  304. public function bar()
  305. {
  306. return 'foo';
  307. }
  308. }
  309. PHP;
  310. $tokens = Tokens::fromCode($source);
  311. [$fooIndex, $barIndex] = array_keys($tokens->findGivenKind(T_PUBLIC));
  312. $tokens->clearRange($fooIndex, $barIndex - 1);
  313. $newPublicIndexes = array_keys($tokens->findGivenKind(T_PUBLIC));
  314. static::assertSame($barIndex, reset($newPublicIndexes));
  315. for ($i = $fooIndex; $i < $barIndex; ++$i) {
  316. static::assertTrue($tokens[$i]->isWhitespace());
  317. }
  318. }
  319. /**
  320. * @dataProvider provideMonolithicPhpDetectionCases
  321. */
  322. public function testMonolithicPhpDetection(bool $isMonolithic, string $source): void
  323. {
  324. $tokens = Tokens::fromCode($source);
  325. static::assertSame($isMonolithic, $tokens->isMonolithicPhp());
  326. }
  327. public function provideMonolithicPhpDetectionCases(): iterable
  328. {
  329. yield [true, "<?php\n"];
  330. yield [true, "<?php\n?>"];
  331. yield [false, ''];
  332. yield [false, ' '];
  333. yield [false, "#!/usr/bin/env php\n<?php\n"];
  334. yield [false, " <?php\n"];
  335. yield [false, "<?php\n?> "];
  336. yield [false, "<?php\n?><?php\n"];
  337. // short open tag
  338. yield [(bool) ini_get('short_open_tag'), "<?\n"];
  339. yield [(bool) ini_get('short_open_tag'), "<?\n?>"];
  340. yield [false, " <?\n"];
  341. yield [false, "<?\n?> "];
  342. yield [false, "<?\n?><?\n"];
  343. yield [false, "<?\n?><?php\n"];
  344. yield [false, "<?\n?><?=' ';\n"];
  345. yield [false, "<?php\n?><?\n"];
  346. yield [false, "<?=' '\n?><?\n"];
  347. // short open tag echo
  348. yield [true, "<?=' ';\n"];
  349. yield [true, "<?=' '?>"];
  350. yield [false, " <?=' ';\n"];
  351. yield [false, "<?=' '?> "];
  352. yield [false, "<?php\n?><?=' ';\n"];
  353. yield [false, "<?=' '\n?><?php\n"];
  354. yield [false, "<?=' '\n?><?=' ';\n"];
  355. }
  356. public function testTokenKindsFound(): void
  357. {
  358. $code = <<<'EOF'
  359. <?php
  360. class Foo
  361. {
  362. public $foo;
  363. }
  364. if (!function_exists('bar')) {
  365. function bar()
  366. {
  367. return 'bar';
  368. }
  369. }
  370. EOF;
  371. $tokens = Tokens::fromCode($code);
  372. static::assertTrue($tokens->isTokenKindFound(T_CLASS));
  373. static::assertTrue($tokens->isTokenKindFound(T_RETURN));
  374. static::assertFalse($tokens->isTokenKindFound(T_INTERFACE));
  375. static::assertFalse($tokens->isTokenKindFound(T_ARRAY));
  376. static::assertTrue($tokens->isAllTokenKindsFound([T_CLASS, T_RETURN]));
  377. static::assertFalse($tokens->isAllTokenKindsFound([T_CLASS, T_INTERFACE]));
  378. static::assertTrue($tokens->isAnyTokenKindsFound([T_CLASS, T_RETURN]));
  379. static::assertTrue($tokens->isAnyTokenKindsFound([T_CLASS, T_INTERFACE]));
  380. static::assertFalse($tokens->isAnyTokenKindsFound([T_INTERFACE, T_ARRAY]));
  381. }
  382. public function testFindGivenKind(): void
  383. {
  384. $source = <<<'PHP'
  385. <?php
  386. class FooBar
  387. {
  388. public function foo()
  389. {
  390. return 'bar';
  391. }
  392. public function bar()
  393. {
  394. return 'foo';
  395. }
  396. }
  397. PHP;
  398. $tokens = Tokens::fromCode($source);
  399. /** @var Token[] $found */
  400. $found = $tokens->findGivenKind(T_CLASS);
  401. static::assertCount(1, $found);
  402. static::assertArrayHasKey(1, $found);
  403. static::assertSame(T_CLASS, $found[1]->getId());
  404. $found = $tokens->findGivenKind([T_CLASS, T_FUNCTION]);
  405. static::assertCount(2, $found);
  406. static::assertArrayHasKey(T_CLASS, $found);
  407. static::assertIsArray($found[T_CLASS]);
  408. static::assertCount(1, $found[T_CLASS]);
  409. static::assertArrayHasKey(1, $found[T_CLASS]);
  410. static::assertSame(T_CLASS, $found[T_CLASS][1]->getId());
  411. static::assertArrayHasKey(T_FUNCTION, $found);
  412. static::assertIsArray($found[T_FUNCTION]);
  413. static::assertCount(2, $found[T_FUNCTION]);
  414. static::assertArrayHasKey(9, $found[T_FUNCTION]);
  415. static::assertSame(T_FUNCTION, $found[T_FUNCTION][9]->getId());
  416. static::assertArrayHasKey(26, $found[T_FUNCTION]);
  417. static::assertSame(T_FUNCTION, $found[T_FUNCTION][26]->getId());
  418. // test offset and limits of the search
  419. $found = $tokens->findGivenKind([T_CLASS, T_FUNCTION], 10);
  420. static::assertCount(0, $found[T_CLASS]);
  421. static::assertCount(1, $found[T_FUNCTION]);
  422. static::assertArrayHasKey(26, $found[T_FUNCTION]);
  423. $found = $tokens->findGivenKind([T_CLASS, T_FUNCTION], 2, 10);
  424. static::assertCount(0, $found[T_CLASS]);
  425. static::assertCount(1, $found[T_FUNCTION]);
  426. static::assertArrayHasKey(9, $found[T_FUNCTION]);
  427. }
  428. /**
  429. * @param Token[] $expected tokens
  430. * @param int[] $indexes to clear
  431. *
  432. * @dataProvider provideGetClearTokenAndMergeSurroundingWhitespaceCases
  433. */
  434. public function testClearTokenAndMergeSurroundingWhitespace(string $source, array $indexes, array $expected): void
  435. {
  436. $this->doTestClearTokens($source, $indexes, $expected);
  437. if (\count($indexes) > 1) {
  438. $this->doTestClearTokens($source, array_reverse($indexes), $expected);
  439. }
  440. }
  441. public function provideGetClearTokenAndMergeSurroundingWhitespaceCases(): array
  442. {
  443. $clearToken = new Token('');
  444. return [
  445. [
  446. '<?php if($a){}else{}',
  447. [7, 8, 9],
  448. [
  449. new Token([T_OPEN_TAG, '<?php ']),
  450. new Token([T_IF, 'if']),
  451. new Token('('),
  452. new Token([T_VARIABLE, '$a']),
  453. new Token(')'),
  454. new Token('{'),
  455. new Token('}'),
  456. $clearToken,
  457. $clearToken,
  458. $clearToken,
  459. ],
  460. ],
  461. [
  462. '<?php $a;/**/;',
  463. [2],
  464. [
  465. // <?php $a /**/;
  466. new Token([T_OPEN_TAG, '<?php ']),
  467. new Token([T_VARIABLE, '$a']),
  468. $clearToken,
  469. new Token([T_COMMENT, '/**/']),
  470. new Token(';'),
  471. ],
  472. ],
  473. [
  474. '<?php ; ; ;',
  475. [3],
  476. [
  477. // <?php ; ;
  478. new Token([T_OPEN_TAG, '<?php ']),
  479. new Token(';'),
  480. new Token([T_WHITESPACE, ' ']),
  481. $clearToken,
  482. $clearToken,
  483. new Token(';'),
  484. ],
  485. ],
  486. [
  487. '<?php ; ; ;',
  488. [1, 5],
  489. [
  490. // <?php ;
  491. new Token([T_OPEN_TAG, '<?php ']),
  492. new Token([T_WHITESPACE, ' ']),
  493. $clearToken,
  494. new Token(';'),
  495. new Token([T_WHITESPACE, ' ']),
  496. $clearToken,
  497. ],
  498. ],
  499. [
  500. '<?php ; ; ;',
  501. [1, 3],
  502. [
  503. // <?php ;
  504. new Token([T_OPEN_TAG, '<?php ']),
  505. new Token([T_WHITESPACE, ' ']),
  506. $clearToken,
  507. $clearToken,
  508. $clearToken,
  509. new Token(';'),
  510. ],
  511. ],
  512. [
  513. '<?php ; ; ;',
  514. [1],
  515. [
  516. // <?php ; ;
  517. new Token([T_OPEN_TAG, '<?php ']),
  518. new Token([T_WHITESPACE, ' ']),
  519. $clearToken,
  520. new Token(';'),
  521. new Token([T_WHITESPACE, ' ']),
  522. new Token(';'),
  523. ],
  524. ],
  525. ];
  526. }
  527. /**
  528. * @param ?int $expectedIndex
  529. *
  530. * @dataProvider provideTokenOfKindSiblingCases
  531. */
  532. public function testTokenOfKindSibling(
  533. ?int $expectedIndex,
  534. int $direction,
  535. int $index,
  536. array $findTokens,
  537. bool $caseSensitive = true
  538. ): void {
  539. $source =
  540. '<?php
  541. $a = function ($b) {
  542. return $b;
  543. };
  544. echo $a(1);
  545. // test
  546. return 123;';
  547. Tokens::clearCache();
  548. $tokens = Tokens::fromCode($source);
  549. if (1 === $direction) {
  550. static::assertSame($expectedIndex, $tokens->getNextTokenOfKind($index, $findTokens, $caseSensitive));
  551. } else {
  552. static::assertSame($expectedIndex, $tokens->getPrevTokenOfKind($index, $findTokens, $caseSensitive));
  553. }
  554. static::assertSame($expectedIndex, $tokens->getTokenOfKindSibling($index, $direction, $findTokens, $caseSensitive));
  555. }
  556. public function provideTokenOfKindSiblingCases(): array
  557. {
  558. return [
  559. // find next cases
  560. [
  561. 35, 1, 34, [';'],
  562. ],
  563. [
  564. 14, 1, 0, [[T_RETURN]],
  565. ],
  566. [
  567. 32, 1, 14, [[T_RETURN]],
  568. ],
  569. [
  570. 6, 1, 0, [[T_RETURN], [T_FUNCTION]],
  571. ],
  572. // find previous cases
  573. [
  574. 14, -1, 32, [[T_RETURN], [T_FUNCTION]],
  575. ],
  576. [
  577. 6, -1, 7, [[T_FUNCTION]],
  578. ],
  579. [
  580. null, -1, 6, [[T_FUNCTION]],
  581. ],
  582. ];
  583. }
  584. /**
  585. * @dataProvider provideFindBlockEndCases
  586. */
  587. public function testFindBlockEnd(int $expectedIndex, string $source, int $type, int $searchIndex): void
  588. {
  589. static::assertFindBlockEnd($expectedIndex, $source, $type, $searchIndex);
  590. }
  591. public function provideFindBlockEndCases(): array
  592. {
  593. return [
  594. [4, '<?php ${$bar};', Tokens::BLOCK_TYPE_DYNAMIC_VAR_BRACE, 2],
  595. [4, '<?php test(1);', Tokens::BLOCK_TYPE_PARENTHESIS_BRACE, 2],
  596. [4, '<?php $a{1};', Tokens::BLOCK_TYPE_ARRAY_INDEX_CURLY_BRACE, 2],
  597. [4, '<?php $a[1];', Tokens::BLOCK_TYPE_INDEX_SQUARE_BRACE, 2],
  598. [6, '<?php [1, "foo"];', Tokens::BLOCK_TYPE_ARRAY_SQUARE_BRACE, 1],
  599. [5, '<?php $foo->{$bar};', Tokens::BLOCK_TYPE_DYNAMIC_PROP_BRACE, 3],
  600. [4, '<?php list($a) = $b;', Tokens::BLOCK_TYPE_PARENTHESIS_BRACE, 2],
  601. [6, '<?php if($a){}?>', Tokens::BLOCK_TYPE_CURLY_BRACE, 5],
  602. [11, '<?php $foo = (new Foo());', Tokens::BLOCK_TYPE_BRACE_CLASS_INSTANTIATION, 5],
  603. [10, '<?php $object->{"set_{$name}"}(42);', Tokens::BLOCK_TYPE_DYNAMIC_PROP_BRACE, 3],
  604. [19, '<?php $foo = (new class () implements Foo {});', Tokens::BLOCK_TYPE_BRACE_CLASS_INSTANTIATION, 5],
  605. [10, '<?php use a\{ClassA, ClassB};', Tokens::BLOCK_TYPE_GROUP_IMPORT_BRACE, 5],
  606. [3, '<?php [$a] = $array;', Tokens::BLOCK_TYPE_DESTRUCTURING_SQUARE_BRACE, 1],
  607. ];
  608. }
  609. /**
  610. * @requires PHP 8.0
  611. * @dataProvider provideFindBlockEnd80Cases
  612. */
  613. public function testFindBlockEnd80(int $expectedIndex, string $source, int $type, int $searchIndex): void
  614. {
  615. static::assertFindBlockEnd($expectedIndex, $source, $type, $searchIndex);
  616. }
  617. public function provideFindBlockEnd80Cases(): array
  618. {
  619. return [
  620. [
  621. 9,
  622. '<?php class Foo {
  623. #[Required]
  624. public $bar;
  625. }',
  626. Tokens::BLOCK_TYPE_ATTRIBUTE,
  627. 7,
  628. ],
  629. ];
  630. }
  631. public function testFindBlockEndInvalidType(): void
  632. {
  633. $this->expectException(\InvalidArgumentException::class);
  634. $this->expectExceptionMessageMatches('/^Invalid param type: "-1"\.$/');
  635. Tokens::clearCache();
  636. $tokens = Tokens::fromCode('<?php ');
  637. $tokens->findBlockEnd(-1, 0);
  638. }
  639. public function testFindBlockEndInvalidStart(): void
  640. {
  641. $this->expectException(\InvalidArgumentException::class);
  642. $this->expectExceptionMessageMatches('/^Invalid param \$startIndex - not a proper block "start"\.$/');
  643. Tokens::clearCache();
  644. $tokens = Tokens::fromCode('<?php ');
  645. $tokens->findBlockEnd(Tokens::BLOCK_TYPE_DYNAMIC_VAR_BRACE, 0);
  646. }
  647. public function testFindBlockEndCalledMultipleTimes(): void
  648. {
  649. Tokens::clearCache();
  650. $tokens = Tokens::fromCode('<?php foo(1, 2);');
  651. static::assertSame(7, $tokens->findBlockEnd(Tokens::BLOCK_TYPE_PARENTHESIS_BRACE, 2));
  652. $this->expectException(\InvalidArgumentException::class);
  653. $this->expectExceptionMessageMatches('/^Invalid param \$startIndex - not a proper block "start"\.$/');
  654. $tokens->findBlockEnd(Tokens::BLOCK_TYPE_PARENTHESIS_BRACE, 7);
  655. }
  656. public function testFindBlockStartEdgeCalledMultipleTimes(): void
  657. {
  658. Tokens::clearCache();
  659. $tokens = Tokens::fromCode('<?php foo(1, 2);');
  660. static::assertSame(2, $tokens->findBlockStart(Tokens::BLOCK_TYPE_PARENTHESIS_BRACE, 7));
  661. $this->expectException(\InvalidArgumentException::class);
  662. $this->expectExceptionMessageMatches('/^Invalid param \$startIndex - not a proper block "end"\.$/');
  663. $tokens->findBlockStart(Tokens::BLOCK_TYPE_PARENTHESIS_BRACE, 2);
  664. }
  665. public function testEmptyTokens(): void
  666. {
  667. $code = '';
  668. $tokens = Tokens::fromCode($code);
  669. static::assertCount(0, $tokens);
  670. static::assertFalse($tokens->isTokenKindFound(T_OPEN_TAG));
  671. }
  672. public function testEmptyTokensMultiple(): void
  673. {
  674. $code = '';
  675. $tokens = Tokens::fromCode($code);
  676. static::assertFalse($tokens->isChanged());
  677. $tokens->insertAt(0, new Token([T_WHITESPACE, ' ']));
  678. static::assertCount(1, $tokens);
  679. static::assertFalse($tokens->isTokenKindFound(T_OPEN_TAG));
  680. static::assertTrue($tokens->isChanged());
  681. $tokens2 = Tokens::fromCode($code);
  682. static::assertCount(0, $tokens2);
  683. static::assertFalse($tokens->isTokenKindFound(T_OPEN_TAG));
  684. }
  685. public function testFromArray(): void
  686. {
  687. $code = '<?php echo 1;';
  688. $tokens1 = Tokens::fromCode($code);
  689. $tokens2 = Tokens::fromArray($tokens1->toArray());
  690. static::assertTrue($tokens1->isTokenKindFound(T_OPEN_TAG));
  691. static::assertTrue($tokens2->isTokenKindFound(T_OPEN_TAG));
  692. static::assertSame($tokens1->getCodeHash(), $tokens2->getCodeHash());
  693. }
  694. public function testFromArrayEmpty(): void
  695. {
  696. $tokens = Tokens::fromArray([]);
  697. static::assertFalse($tokens->isTokenKindFound(T_OPEN_TAG));
  698. }
  699. /**
  700. * @dataProvider provideIsEmptyCases
  701. */
  702. public function testIsEmpty(Token $token, bool $isEmpty): void
  703. {
  704. $tokens = Tokens::fromArray([$token]);
  705. Tokens::clearCache();
  706. static::assertSame($isEmpty, $tokens->isEmptyAt(0), $token->toJson());
  707. }
  708. public function provideIsEmptyCases(): array
  709. {
  710. return [
  711. [new Token(''), true],
  712. [new Token('('), false],
  713. [new Token([T_WHITESPACE, ' ']), false],
  714. ];
  715. }
  716. public function testClone(): void
  717. {
  718. $code = '<?php echo 1;';
  719. $tokens = Tokens::fromCode($code);
  720. $tokensClone = clone $tokens;
  721. static::assertTrue($tokens->isTokenKindFound(T_OPEN_TAG));
  722. static::assertTrue($tokensClone->isTokenKindFound(T_OPEN_TAG));
  723. $count = \count($tokens);
  724. static::assertCount($count, $tokensClone);
  725. for ($i = 0; $i < $count; ++$i) {
  726. static::assertTrue($tokens[$i]->equals($tokensClone[$i]));
  727. static::assertNotSame($tokens[$i], $tokensClone[$i]);
  728. }
  729. }
  730. /**
  731. * @dataProvider provideEnsureWhitespaceAtIndexCases
  732. */
  733. public function testEnsureWhitespaceAtIndex(string $expected, string $input, int $index, int $offset, string $whiteSpace): void
  734. {
  735. $tokens = Tokens::fromCode($input);
  736. $tokens->ensureWhitespaceAtIndex($index, $offset, $whiteSpace);
  737. $tokens->clearEmptyTokens();
  738. static::assertTokens(Tokens::fromCode($expected), $tokens);
  739. }
  740. public function provideEnsureWhitespaceAtIndexCases(): array
  741. {
  742. return [
  743. [
  744. '<?php echo 1;',
  745. '<?php echo 1;',
  746. 1,
  747. 1,
  748. ' ',
  749. ],
  750. [
  751. '<?php echo 7;',
  752. '<?php echo 7;',
  753. 1,
  754. 1,
  755. ' ',
  756. ],
  757. [
  758. '<?php ',
  759. '<?php ',
  760. 1,
  761. 1,
  762. ' ',
  763. ],
  764. [
  765. '<?php $a. $b;',
  766. '<?php $a.$b;',
  767. 2,
  768. 1,
  769. ' ',
  770. ],
  771. [
  772. '<?php $a .$b;',
  773. '<?php $a.$b;',
  774. 2,
  775. 0,
  776. ' ',
  777. ],
  778. [
  779. "<?php\r\n",
  780. '<?php ',
  781. 0,
  782. 1,
  783. "\r\n",
  784. ],
  785. [
  786. '<?php $a.$b;',
  787. '<?php $a.$b;',
  788. 2,
  789. -1,
  790. ' ',
  791. ],
  792. [
  793. "<?php\t ",
  794. "<?php\n",
  795. 0,
  796. 1,
  797. "\t ",
  798. ],
  799. [
  800. '<?php ',
  801. '<?php ',
  802. 0,
  803. 1,
  804. ' ',
  805. ],
  806. [
  807. "<?php\n",
  808. '<?php ',
  809. 0,
  810. 1,
  811. "\n",
  812. ],
  813. [
  814. "<?php\t",
  815. '<?php ',
  816. 0,
  817. 1,
  818. "\t",
  819. ],
  820. [
  821. '<?php
  822. //
  823. echo $a;',
  824. '<?php
  825. //
  826. echo $a;',
  827. 2,
  828. 1,
  829. "\n ",
  830. ],
  831. [
  832. '<?php
  833. echo $a;',
  834. '<?php
  835. echo $a;',
  836. 0,
  837. 1,
  838. "\n ",
  839. ],
  840. [
  841. '<?php
  842. echo $a;',
  843. '<?php echo $a;',
  844. 0,
  845. 1,
  846. "\n",
  847. ],
  848. [
  849. "<?php\techo \$a;",
  850. '<?php echo $a;',
  851. 0,
  852. 1,
  853. "\t",
  854. ],
  855. ];
  856. }
  857. public function testAssertTokensAfterChanging(): void
  858. {
  859. $template =
  860. '<?php class SomeClass {
  861. %s//
  862. public function __construct($name)
  863. {
  864. $this->name = $name;
  865. }
  866. }';
  867. $tokens = Tokens::fromCode(sprintf($template, ''));
  868. $commentIndex = $tokens->getNextTokenOfKind(0, [[T_COMMENT]]);
  869. $tokens->insertAt(
  870. $commentIndex,
  871. [
  872. new Token([T_PRIVATE, 'private']),
  873. new Token([T_WHITESPACE, ' ']),
  874. new Token([T_VARIABLE, '$name']),
  875. new Token(';'),
  876. ]
  877. );
  878. static::assertTrue($tokens->isChanged());
  879. $expected = Tokens::fromCode(sprintf($template, 'private $name;'));
  880. static::assertFalse($expected->isChanged());
  881. static::assertTokens($expected, $tokens);
  882. }
  883. /**
  884. * @dataProvider provideRemoveLeadingWhitespaceCases
  885. */
  886. public function testRemoveLeadingWhitespace(int $index, ?string $whitespaces, string $expected, string $input = null): void
  887. {
  888. Tokens::clearCache();
  889. $tokens = Tokens::fromCode($input ?? $expected);
  890. $tokens->removeLeadingWhitespace($index, $whitespaces);
  891. static::assertSame($expected, $tokens->generateCode());
  892. }
  893. public function provideRemoveLeadingWhitespaceCases(): array
  894. {
  895. return [
  896. [
  897. 7,
  898. null,
  899. "<?php echo 1;//\necho 2;",
  900. ],
  901. [
  902. 7,
  903. null,
  904. "<?php echo 1;//\necho 2;",
  905. "<?php echo 1;//\n echo 2;",
  906. ],
  907. [
  908. 7,
  909. null,
  910. "<?php echo 1;//\r\necho 2;",
  911. "<?php echo 1;//\r\n echo 2;",
  912. ],
  913. [
  914. 7,
  915. " \t",
  916. "<?php echo 1;//\n//",
  917. "<?php echo 1;//\n //",
  918. ],
  919. [
  920. 6,
  921. "\t ",
  922. '<?php echo 1;//',
  923. "<?php echo 1;\t \t \t //",
  924. ],
  925. [
  926. 8,
  927. null,
  928. '<?php $a = 1;//',
  929. '<?php $a = 1; //',
  930. ],
  931. [
  932. 6,
  933. null,
  934. '<?php echo 1;echo 2;',
  935. "<?php echo 1; \n \n \n \necho 2;",
  936. ],
  937. [
  938. 8,
  939. null,
  940. "<?php echo 1; // 1\necho 2;",
  941. "<?php echo 1; // 1\n \n \n \necho 2;",
  942. ],
  943. ];
  944. }
  945. /**
  946. * @dataProvider provideRemoveTrailingWhitespaceCases
  947. */
  948. public function testRemoveTrailingWhitespace(int $index, ?string $whitespaces, string $expected, string $input = null): void
  949. {
  950. Tokens::clearCache();
  951. $tokens = Tokens::fromCode($input ?? $expected);
  952. $tokens->removeTrailingWhitespace($index, $whitespaces);
  953. static::assertSame($expected, $tokens->generateCode());
  954. }
  955. public function provideRemoveTrailingWhitespaceCases(): \Generator
  956. {
  957. $leadingCases = $this->provideRemoveLeadingWhitespaceCases();
  958. foreach ($leadingCases as $leadingCase) {
  959. $leadingCase[0] -= 2;
  960. yield $leadingCase;
  961. }
  962. }
  963. public function testRemovingLeadingWhitespaceWithEmptyTokenInCollection(): void
  964. {
  965. $code = "<?php\n /* I will be removed */MY_INDEX_IS_THREE;foo();";
  966. $tokens = Tokens::fromCode($code);
  967. $tokens->clearAt(2);
  968. $tokens->removeLeadingWhitespace(3);
  969. $tokens->clearEmptyTokens();
  970. static::assertTokens(Tokens::fromCode("<?php\nMY_INDEX_IS_THREE;foo();"), $tokens);
  971. }
  972. public function testRemovingTrailingWhitespaceWithEmptyTokenInCollection(): void
  973. {
  974. $code = "<?php\nMY_INDEX_IS_ONE/* I will be removed */ ;foo();";
  975. $tokens = Tokens::fromCode($code);
  976. $tokens->clearAt(2);
  977. $tokens->removeTrailingWhitespace(1);
  978. $tokens->clearEmptyTokens();
  979. static::assertTokens(Tokens::fromCode("<?php\nMY_INDEX_IS_ONE;foo();"), $tokens);
  980. }
  981. /**
  982. * Action that begins with the word "remove" should not change the size of collection.
  983. */
  984. public function testRemovingLeadingWhitespaceWillNotIncreaseTokensCount(): void
  985. {
  986. $tokens = Tokens::fromCode('<?php
  987. // Foo
  988. $bar;');
  989. $originalCount = $tokens->count();
  990. $tokens->removeLeadingWhitespace(4);
  991. static::assertSame($originalCount, $tokens->count());
  992. static::assertSame(
  993. '<?php
  994. // Foo
  995. $bar;',
  996. $tokens->generateCode()
  997. );
  998. }
  999. /**
  1000. * Action that begins with the word "remove" should not change the size of collection.
  1001. */
  1002. public function testRemovingTrailingWhitespaceWillNotIncreaseTokensCount(): void
  1003. {
  1004. $tokens = Tokens::fromCode('<?php
  1005. // Foo
  1006. $bar;');
  1007. $originalCount = $tokens->count();
  1008. $tokens->removeTrailingWhitespace(2);
  1009. static::assertSame($originalCount, $tokens->count());
  1010. static::assertSame(
  1011. '<?php
  1012. // Foo
  1013. $bar;',
  1014. $tokens->generateCode()
  1015. );
  1016. }
  1017. /**
  1018. * @dataProvider provideDetectBlockTypeCases
  1019. */
  1020. public function testDetectBlockType(?array $expected, string $code, int $index): void
  1021. {
  1022. $tokens = Tokens::fromCode($code);
  1023. static::assertSame($expected, Tokens::detectBlockType($tokens[$index]));
  1024. }
  1025. public function provideDetectBlockTypeCases(): \Generator
  1026. {
  1027. yield [
  1028. [
  1029. 'type' => Tokens::BLOCK_TYPE_CURLY_BRACE,
  1030. 'isStart' => true,
  1031. ],
  1032. '<?php { echo 1; }',
  1033. 1,
  1034. ];
  1035. yield [
  1036. null,
  1037. '<?php { echo 1;}',
  1038. 0,
  1039. ];
  1040. }
  1041. public function testOverrideRangeTokens(): void
  1042. {
  1043. $expected = [
  1044. new Token([T_OPEN_TAG, '<?php ']),
  1045. new Token([T_FUNCTION, 'function']),
  1046. new Token([T_WHITESPACE, ' ']),
  1047. new Token([T_STRING, 'foo']),
  1048. new Token('('),
  1049. new Token([T_ARRAY, 'array']),
  1050. new Token([T_WHITESPACE, ' ']),
  1051. new Token([T_VARIABLE, '$bar']),
  1052. new Token(')'),
  1053. new Token('{'),
  1054. new Token('}'),
  1055. ];
  1056. $code = '<?php function foo(array $bar){}';
  1057. $indexStart = 5;
  1058. $indexEnd = 5;
  1059. $items = Tokens::fromArray([new Token([T_ARRAY, 'array'])]);
  1060. $tokens = Tokens::fromCode($code);
  1061. $tokens->overrideRange($indexStart, $indexEnd, $items);
  1062. $tokens->clearEmptyTokens();
  1063. self::assertTokens(Tokens::fromArray($expected), $tokens);
  1064. }
  1065. /**
  1066. * @param int $indexStart start overriding index
  1067. * @param int $indexEnd end overriding index
  1068. * @param array<Token> $items tokens to insert
  1069. *
  1070. * @dataProvider provideOverrideRangeCases
  1071. */
  1072. public function testOverrideRange(array $expected, string $code, int $indexStart, int $indexEnd, array $items): void
  1073. {
  1074. $tokens = Tokens::fromCode($code);
  1075. $tokens->overrideRange($indexStart, $indexEnd, $items);
  1076. $tokens->clearEmptyTokens();
  1077. self::assertTokens(Tokens::fromArray($expected), $tokens);
  1078. }
  1079. public function provideOverrideRangeCases(): \Generator
  1080. {
  1081. // typically done by transformers, here we test the reverse
  1082. yield 'override different tokens but same content' => [
  1083. [
  1084. new Token([T_OPEN_TAG, '<?php ']),
  1085. new Token([T_FUNCTION, 'function']),
  1086. new Token([T_WHITESPACE, ' ']),
  1087. new Token([T_STRING, 'foo']),
  1088. new Token('('),
  1089. new Token([T_ARRAY, 'array']),
  1090. new Token([T_WHITESPACE, ' ']),
  1091. new Token([T_VARIABLE, '$bar']),
  1092. new Token(')'),
  1093. new Token('{'),
  1094. new Token('}'),
  1095. ],
  1096. '<?php function foo(array $bar){}',
  1097. 5,
  1098. 5,
  1099. [new Token([T_ARRAY, 'array'])],
  1100. ];
  1101. yield 'add more item than in range' => [
  1102. [
  1103. new Token([T_OPEN_TAG, "<?php\n"]),
  1104. new Token([T_COMMENT, '// test']),
  1105. new Token([T_WHITESPACE, "\n"]),
  1106. new Token([T_COMMENT, '// test']),
  1107. new Token([T_WHITESPACE, "\n"]),
  1108. new Token([T_COMMENT, '// test']),
  1109. new Token([T_WHITESPACE, "\n"]),
  1110. ],
  1111. "<?php\n#comment",
  1112. 1,
  1113. 1,
  1114. [
  1115. new Token([T_COMMENT, '// test']),
  1116. new Token([T_WHITESPACE, "\n"]),
  1117. new Token([T_COMMENT, '// test']),
  1118. new Token([T_WHITESPACE, "\n"]),
  1119. new Token([T_COMMENT, '// test']),
  1120. new Token([T_WHITESPACE, "\n"]),
  1121. ],
  1122. ];
  1123. yield [
  1124. [
  1125. new Token([T_OPEN_TAG, "<?php\n"]),
  1126. new Token([T_COMMENT, '#comment1']),
  1127. new Token([T_WHITESPACE, "\n"]),
  1128. new Token([T_COMMENT, '// test 1']),
  1129. new Token([T_WHITESPACE, "\n"]),
  1130. new Token([T_COMMENT, '#comment5']),
  1131. new Token([T_WHITESPACE, "\n"]),
  1132. new Token([T_COMMENT, '#comment6']),
  1133. ],
  1134. "<?php\n#comment1\n#comment2\n#comment3\n#comment4\n#comment5\n#comment6",
  1135. 3,
  1136. 7,
  1137. [
  1138. new Token([T_COMMENT, '// test 1']),
  1139. ],
  1140. ];
  1141. yield [
  1142. [
  1143. new Token([T_OPEN_TAG, "<?php\n"]),
  1144. new Token([T_COMMENT, '// test']),
  1145. ],
  1146. "<?php\n#comment1\n#comment2\n#comment3\n#comment4\n#comment5\n#comment6\n#comment7",
  1147. 1,
  1148. 13,
  1149. [
  1150. new Token([T_COMMENT, '// test']),
  1151. ],
  1152. ];
  1153. yield [
  1154. [
  1155. new Token([T_OPEN_TAG, "<?php\n"]),
  1156. new Token([T_COMMENT, '// test']),
  1157. ],
  1158. "<?php\n#comment",
  1159. 1,
  1160. 1,
  1161. [
  1162. new Token([T_COMMENT, '// test']),
  1163. ],
  1164. ];
  1165. }
  1166. public function testInitialChangedState(): void
  1167. {
  1168. $tokens = Tokens::fromCode("<?php\n");
  1169. static::assertFalse($tokens->isChanged());
  1170. $tokens = Tokens::fromArray(
  1171. [
  1172. new Token([T_OPEN_TAG, "<?php\n"]),
  1173. new Token([T_STRING, 'Foo']),
  1174. new Token(';'),
  1175. ]
  1176. );
  1177. static::assertFalse($tokens->isChanged());
  1178. }
  1179. /**
  1180. * @dataProvider provideGetMeaningfulTokenSiblingCases
  1181. */
  1182. public function testGetMeaningfulTokenSibling(?int $expectIndex, int $index, int $direction, string $source): void
  1183. {
  1184. Tokens::clearCache();
  1185. $tokens = Tokens::fromCode($source);
  1186. static::assertSame($expectIndex, $tokens->getMeaningfulTokenSibling($index, $direction));
  1187. }
  1188. public function provideGetMeaningfulTokenSiblingCases(): \Generator
  1189. {
  1190. yield [null, 0, 1, '<?php '];
  1191. yield [null, 1, 1, '<?php /**/ /**/ /**/ /**/#'];
  1192. for ($i = 0; $i < 3; ++$i) {
  1193. yield '>'.$i => [3, $i, 1, '<?php /**/ foo();'];
  1194. }
  1195. yield '>>' => [4, 3, 1, '<?php /**/ foo();'];
  1196. yield '@ end' => [null, 6, 1, '<?php /**/ foo();'];
  1197. yield 'over end' => [null, 888, 1, '<?php /**/ foo();'];
  1198. yield [0, 3, -1, '<?php /**/ foo();'];
  1199. yield [4, 5, -1, '<?php /**/ foo();'];
  1200. yield [5, 6, -1, '<?php /**/ foo();'];
  1201. yield [null, 0, -1, '<?php /**/ foo();'];
  1202. }
  1203. /**
  1204. * @dataProvider provideInsertSlicesAtMultiplePlacesCases
  1205. *
  1206. * @param array<int, Token> $slices
  1207. */
  1208. public function testInsertSlicesAtMultiplePlaces(string $expected, array $slices): void
  1209. {
  1210. $input = <<<'EOF'
  1211. <?php
  1212. $after = get_class($after);
  1213. $before = get_class($before);
  1214. EOF;
  1215. $tokens = Tokens::fromCode($input);
  1216. $tokens->insertSlices([
  1217. 16 => $slices,
  1218. 6 => $slices,
  1219. ]);
  1220. static::assertTokens(Tokens::fromCode($expected), $tokens);
  1221. }
  1222. public function provideInsertSlicesAtMultiplePlacesCases(): \Generator
  1223. {
  1224. yield 'one slice count' => [
  1225. <<<'EOF'
  1226. <?php
  1227. $after = /*foo*/get_class($after);
  1228. $before = /*foo*/get_class($before);
  1229. EOF
  1230. ,
  1231. [new Token([T_COMMENT, '/*foo*/'])],
  1232. ];
  1233. yield 'two slice count' => [
  1234. <<<'EOF'
  1235. <?php
  1236. $after = (string) get_class($after);
  1237. $before = (string) get_class($before);
  1238. EOF
  1239. ,
  1240. [new Token([T_STRING_CAST, '(string)']), new Token([T_WHITESPACE, ' '])],
  1241. ];
  1242. yield 'three slice count' => [
  1243. <<<'EOF'
  1244. <?php
  1245. $after = !(bool) get_class($after);
  1246. $before = !(bool) get_class($before);
  1247. EOF
  1248. ,
  1249. [new Token('!'), new Token([T_BOOL_CAST, '(bool)']), new Token([T_WHITESPACE, ' '])],
  1250. ];
  1251. }
  1252. public function testInsertSlicesChangesState(): void
  1253. {
  1254. $tokens = Tokens::fromCode('<?php echo 1234567890;');
  1255. static::assertFalse($tokens->isChanged());
  1256. static::assertFalse($tokens->isTokenKindFound(T_COMMENT));
  1257. static::assertSame(5, $tokens->getSize());
  1258. $tokens->insertSlices([1 => new Token([T_COMMENT, '/* comment */'])]);
  1259. static::assertTrue($tokens->isChanged());
  1260. static::assertTrue($tokens->isTokenKindFound(T_COMMENT));
  1261. static::assertSame(6, $tokens->getSize());
  1262. }
  1263. /**
  1264. * @dataProvider provideInsertSlicesCases
  1265. */
  1266. public function testInsertSlices(Tokens $expected, Tokens $tokens, array $slices): void
  1267. {
  1268. $tokens->insertSlices($slices);
  1269. static::assertTokens($expected, $tokens);
  1270. }
  1271. public function provideInsertSlicesCases(): iterable
  1272. {
  1273. // basic insert of single token at 3 different locations including appending as new token
  1274. $template = "<?php\n%s\n/* single token test header */%s\necho 1;\n%s";
  1275. $commentContent = '/* test */';
  1276. $commentToken = new Token([T_COMMENT, $commentContent]);
  1277. $from = Tokens::fromCode(sprintf($template, '', '', ''));
  1278. yield 'single insert @ 1' => [
  1279. Tokens::fromCode(sprintf($template, $commentContent, '', '')),
  1280. clone $from,
  1281. [1 => $commentToken],
  1282. ];
  1283. yield 'single insert @ 3' => [
  1284. Tokens::fromCode(sprintf($template, '', $commentContent, '')),
  1285. clone $from,
  1286. [3 => Tokens::fromArray([$commentToken])],
  1287. ];
  1288. yield 'single insert @ 9' => [
  1289. Tokens::fromCode(sprintf($template, '', '', $commentContent)),
  1290. clone $from,
  1291. [9 => [$commentToken]],
  1292. ];
  1293. // basic tests for single token, array of that token and tokens object with that token
  1294. $openTagToken = new Token([T_OPEN_TAG, "<?php\n"]);
  1295. $expected = Tokens::fromArray([$openTagToken]);
  1296. $slices = [
  1297. [0 => $openTagToken],
  1298. [0 => [clone $openTagToken]],
  1299. [0 => clone Tokens::fromArray([$openTagToken])],
  1300. ];
  1301. foreach ($slices as $i => $slice) {
  1302. yield 'insert open tag @ 0 into empty collection '.$i => [$expected, new Tokens(), $slice];
  1303. }
  1304. // test insert lists of tokens, index out of order
  1305. $setOne = [
  1306. new Token([T_ECHO, 'echo']),
  1307. new Token([T_WHITESPACE, ' ']),
  1308. new Token([T_CONSTANT_ENCAPSED_STRING, '"new"']),
  1309. new Token(';'),
  1310. ];
  1311. $setTwo = [
  1312. new Token([T_WHITESPACE, ' ']),
  1313. new Token([T_COMMENT, '/* new comment */']),
  1314. ];
  1315. $setThree = Tokens::fromArray([
  1316. new Token([T_VARIABLE, '$new']),
  1317. new Token([T_WHITESPACE, ' ']),
  1318. new Token('='),
  1319. new Token([T_WHITESPACE, ' ']),
  1320. new Token([T_LNUMBER, '8899']),
  1321. new Token(';'),
  1322. new Token([T_WHITESPACE, "\n"]),
  1323. ]);
  1324. $template = "<?php\n%s\n/* header */%s\necho 789;\n%s";
  1325. $expected = Tokens::fromCode(
  1326. sprintf(
  1327. $template,
  1328. 'echo "new";',
  1329. ' /* new comment */',
  1330. "\$new = 8899;\n"
  1331. )
  1332. );
  1333. $from = Tokens::fromCode(sprintf($template, '', '', ''));
  1334. yield 'insert 3 token collections' => [$expected, $from, [9 => $setThree, 1 => $setOne, 3 => $setTwo]];
  1335. $sets = [];
  1336. for ($j = 0; $j < 4; ++$j) {
  1337. $set = ['tokens' => [], 'content' => ''];
  1338. for ($i = 0; $i < 10; ++$i) {
  1339. $content = sprintf('/* new %d|%s */', $j, $i);
  1340. $set['tokens'][] = new Token([T_COMMENT, $content]);
  1341. $set['content'] .= $content;
  1342. }
  1343. $sets[$j] = $set;
  1344. }
  1345. yield 'overlapping inserts of bunch of comments ' => [
  1346. Tokens::fromCode(sprintf("<?php\n%s/* line 1 */\n%s/* line 2 */\n%s/* line 3 */%s", $sets[0]['content'], $sets[1]['content'], $sets[2]['content'], $sets[3]['content'])),
  1347. Tokens::fromCode("<?php\n/* line 1 */\n/* line 2 */\n/* line 3 */"),
  1348. [1 => $sets[0]['tokens'], 3 => $sets[1]['tokens'], 5 => $sets[2]['tokens'], 6 => $sets[3]['tokens']],
  1349. ];
  1350. }
  1351. private static function assertFindBlockEnd(int $expectedIndex, string $source, int $type, int $searchIndex): void
  1352. {
  1353. Tokens::clearCache();
  1354. $tokens = Tokens::fromCode($source);
  1355. static::assertSame($expectedIndex, $tokens->findBlockEnd($type, $searchIndex));
  1356. static::assertSame($searchIndex, $tokens->findBlockStart($type, $expectedIndex));
  1357. $detectedType = Tokens::detectBlockType($tokens[$searchIndex]);
  1358. static::assertIsArray($detectedType);
  1359. static::assertArrayHasKey('type', $detectedType);
  1360. static::assertArrayHasKey('isStart', $detectedType);
  1361. static::assertSame($type, $detectedType['type']);
  1362. static::assertTrue($detectedType['isStart']);
  1363. $detectedType = Tokens::detectBlockType($tokens[$expectedIndex]);
  1364. static::assertIsArray($detectedType);
  1365. static::assertArrayHasKey('type', $detectedType);
  1366. static::assertArrayHasKey('isStart', $detectedType);
  1367. static::assertSame($type, $detectedType['type']);
  1368. static::assertFalse($detectedType['isStart']);
  1369. }
  1370. /**
  1371. * @param null|Token[] $expected
  1372. * @param null|Token[] $input
  1373. */
  1374. private static function assertEqualsTokensArray(array $expected = null, array $input = null): void
  1375. {
  1376. if (null === $expected) {
  1377. static::assertNull($input);
  1378. return;
  1379. }
  1380. if (null === $input) {
  1381. static::fail('While "input" is <null>, "expected" is not.');
  1382. }
  1383. static::assertSame(array_keys($expected), array_keys($input), 'Both arrays need to have same keys.');
  1384. foreach ($expected as $index => $expectedToken) {
  1385. static::assertTrue(
  1386. $expectedToken->equals($input[$index]),
  1387. sprintf('The token at index %d should be %s, got %s', $index, $expectedToken->toJson(), $input[$index]->toJson())
  1388. );
  1389. }
  1390. }
  1391. /**
  1392. * @param int[] $indexes
  1393. * @param Token[] $expected
  1394. */
  1395. private function doTestClearTokens(string $source, array $indexes, array $expected): void
  1396. {
  1397. Tokens::clearCache();
  1398. $tokens = Tokens::fromCode($source);
  1399. foreach ($indexes as $index) {
  1400. $tokens->clearTokenAndMergeSurroundingWhitespace($index);
  1401. }
  1402. static::assertSame(\count($expected), $tokens->count());
  1403. foreach ($expected as $index => $expectedToken) {
  1404. $token = $tokens[$index];
  1405. $expectedPrototype = $expectedToken->getPrototype();
  1406. if (\is_array($expectedPrototype)) {
  1407. unset($expectedPrototype[2]); // don't compare token lines as our token mutations don't deal with line numbers
  1408. }
  1409. static::assertTrue($token->equals($expectedPrototype), sprintf('The token at index %d should be %s, got %s', $index, json_encode($expectedPrototype), $token->toJson()));
  1410. }
  1411. }
  1412. }