TokensTest.php 53 KB

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