TokensTest.php 51 KB

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