ControlCaseStructuresAnalyzerTest.php 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445
  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\Analyzer;
  13. use PhpCsFixer\Tests\TestCase;
  14. use PhpCsFixer\Tokenizer\Analyzer\Analysis\AbstractControlCaseStructuresAnalysis;
  15. use PhpCsFixer\Tokenizer\Analyzer\Analysis\CaseAnalysis;
  16. use PhpCsFixer\Tokenizer\Analyzer\Analysis\DefaultAnalysis;
  17. use PhpCsFixer\Tokenizer\Analyzer\Analysis\EnumAnalysis;
  18. use PhpCsFixer\Tokenizer\Analyzer\Analysis\MatchAnalysis;
  19. use PhpCsFixer\Tokenizer\Analyzer\Analysis\SwitchAnalysis;
  20. use PhpCsFixer\Tokenizer\Analyzer\ControlCaseStructuresAnalyzer;
  21. use PhpCsFixer\Tokenizer\Tokens;
  22. /**
  23. * @covers \PhpCsFixer\Tokenizer\Analyzer\ControlCaseStructuresAnalyzer
  24. *
  25. * @internal
  26. */
  27. final class ControlCaseStructuresAnalyzerTest extends TestCase
  28. {
  29. /**
  30. * @param array<int, AbstractControlCaseStructuresAnalysis> $expectedAnalyses
  31. *
  32. * @dataProvider provideFindControlStructuresCases
  33. */
  34. public function testFindControlStructures(array $expectedAnalyses, string $source): void
  35. {
  36. $tokens = Tokens::fromCode($source);
  37. $analyses = iterator_to_array(ControlCaseStructuresAnalyzer::findControlStructures($tokens, [T_SWITCH]));
  38. self::assertCount(\count($expectedAnalyses), $analyses);
  39. foreach ($expectedAnalyses as $index => $expectedAnalysis) {
  40. self::assertAnalysis($expectedAnalysis, $analyses[$index]);
  41. }
  42. }
  43. public static function provideFindControlStructuresCases(): iterable
  44. {
  45. yield 'two cases' => [
  46. [1 => new SwitchAnalysis(1, 7, 46, [new CaseAnalysis(9, 12), new CaseAnalysis(36, 39)], null)],
  47. '<?php switch ($foo) {
  48. case 1: $x = bar() ? 1 : 0; return true;
  49. case 2: return false;
  50. }',
  51. ];
  52. yield 'case without code' => [
  53. [1 => new SwitchAnalysis(1, 7, 34, [new CaseAnalysis(9, 12), new CaseAnalysis(19, 22), new CaseAnalysis(24, 27)], null)],
  54. '<?php switch ($foo) {
  55. case 1: return true;
  56. case 2:
  57. case 3: return false;
  58. }',
  59. ];
  60. yield 'advanced cases' => [
  61. [
  62. 1 => new SwitchAnalysis(
  63. 1,
  64. 7,
  65. 132,
  66. [
  67. new CaseAnalysis(17, 22),
  68. new CaseAnalysis(29, 40),
  69. new CaseAnalysis(47, 53),
  70. new CaseAnalysis(60, 71),
  71. new CaseAnalysis(78, 125),
  72. ],
  73. new DefaultAnalysis(9, 10)
  74. ),
  75. ],
  76. '<?php switch (true) {
  77. default: return 0;
  78. case ("a"): return 1;
  79. case [1, 2, 3]: return 2;
  80. case getValue($foo): return 3;
  81. case getValue2($foo)["key"]->bar: return 4;
  82. case $a->$b::$c->${$d}->${$e}::foo(function ($x) { return $x * 2 + 2; })->$g::$h: return 5;
  83. }',
  84. ];
  85. yield 'two case and default' => [
  86. [1 => new SwitchAnalysis(1, 7, 38, [new CaseAnalysis(9, 12), new CaseAnalysis(19, 22)], new DefaultAnalysis(29, 30))],
  87. '<?php switch ($foo) { case 10: return true; case 100: return false; default: return -1; }',
  88. ];
  89. yield 'two case and default with semicolon instead of colon' => [
  90. [1 => new SwitchAnalysis(1, 7, 38, [new CaseAnalysis(9, 12), new CaseAnalysis(19, 22)], new DefaultAnalysis(29, 30))],
  91. '<?php switch ($foo) { case 10; return true; case 100; return false; default; return -1; }',
  92. ];
  93. yield 'ternary operator in case' => [
  94. [1 => new SwitchAnalysis(1, 7, 39, [new CaseAnalysis(9, 22), new CaseAnalysis(29, 32)], null)],
  95. '<?php switch ($foo) { case ($bar ? 10 : 20): return true; case 100: return false; }',
  96. ];
  97. yield 'nested switch' => [
  98. [
  99. 1 => new SwitchAnalysis(1, 7, 67, [new CaseAnalysis(9, 12), new CaseAnalysis(57, 60)], null),
  100. 14 => new SwitchAnalysis(14, 20, 52, [new CaseAnalysis(22, 25), new CaseAnalysis(32, 35), new CaseAnalysis(42, 45)], null),
  101. ],
  102. '<?php switch ($foo) { case 10:
  103. switch ($bar) { case "a": return "b"; case "c": return "d"; case "e": return "f"; }
  104. return;
  105. case 100: return false; }',
  106. ];
  107. yield 'switch in case' => [
  108. [
  109. 1 => new SwitchAnalysis(1, 7, 98, [new CaseAnalysis(9, 81), new CaseAnalysis(88, 91)], null),
  110. 25 => new SwitchAnalysis(25, 31, 63, [new CaseAnalysis(33, 36), new CaseAnalysis(43, 46), new CaseAnalysis(53, 56)], null),
  111. ],
  112. '<?php
  113. switch ($foo) {
  114. case (
  115. array_sum(array_map(function ($x) { switch ($bar) { case "a": return "b"; case "c": return "d"; case "e": return "f"; } }, [1, 2, 3]))
  116. ):
  117. return true;
  118. case 100:
  119. return false;
  120. }
  121. ',
  122. ];
  123. yield 'alternative syntax' => [
  124. [1 => new SwitchAnalysis(1, 7, 30, [new CaseAnalysis(9, 12), new CaseAnalysis(19, 22)], null)],
  125. '<?php switch ($foo) : case 10: return true; case 100: return false; endswitch;',
  126. ];
  127. yield 'alternative syntax with closing tag' => [
  128. [1 => new SwitchAnalysis(1, 7, 31, [new CaseAnalysis(9, 12), new CaseAnalysis(19, 22)], null)],
  129. '<?php switch ($foo) : case 10: return true; case 100: return false; endswitch ?>',
  130. ];
  131. yield 'alternative syntax nested' => [
  132. [
  133. 1 => new SwitchAnalysis(1, 7, 69, [new CaseAnalysis(9, 12), new CaseAnalysis(58, 61)], null),
  134. 14 => new SwitchAnalysis(14, 20, 53, [new CaseAnalysis(22, 25), new CaseAnalysis(32, 35), new CaseAnalysis(42, 45)], null),
  135. ],
  136. '<?php switch ($foo) : case 10:
  137. switch ($bar) : case "a": return "b"; case "c": return "d"; case "e": return "f"; endswitch;
  138. return;
  139. case 100: return false; endswitch;',
  140. ];
  141. yield 'alternative syntax nested with mixed colon/semicolon' => [
  142. [
  143. 1 => new SwitchAnalysis(1, 7, 69, [new CaseAnalysis(9, 12), new CaseAnalysis(58, 61)], null),
  144. 14 => new SwitchAnalysis(14, 20, 53, [new CaseAnalysis(22, 25), new CaseAnalysis(32, 35), new CaseAnalysis(42, 45)], null),
  145. ],
  146. '<?php switch ($foo) : case 10;
  147. switch ($bar) : case "a": return "b"; case "c"; return "d"; case "e": return "f"; endswitch;
  148. return;
  149. case 100: return false; endswitch;',
  150. ];
  151. yield 'alternative syntax nested with closing tab and mixed colon/semicolon' => [
  152. [
  153. 1 => new SwitchAnalysis(1, 7, 70, [new CaseAnalysis(9, 12), new CaseAnalysis(58, 61)], null),
  154. 14 => new SwitchAnalysis(14, 20, 53, [new CaseAnalysis(22, 25), new CaseAnalysis(32, 35), new CaseAnalysis(42, 45)], null),
  155. ],
  156. '<?php switch ($foo) : case 10;
  157. switch ($bar) : case "a": return "b"; case "c"; return "d"; case "e": return "f"; endswitch;
  158. return;
  159. case 100: return false; endswitch ?> <?php echo 1;',
  160. 1,
  161. ];
  162. $expected = [
  163. 1 => new SwitchAnalysis(
  164. 1,
  165. 6,
  166. 22,
  167. [
  168. new CaseAnalysis(8, 11),
  169. ],
  170. new DefaultAnalysis(16, 17)
  171. ),
  172. ];
  173. $code = '<?php switch($a) {
  174. case 1:
  175. break;
  176. default:
  177. break;
  178. }';
  179. yield 'case :' => [$expected, $code];
  180. $code = str_replace('case 1:', 'case 1;', $code);
  181. $code = str_replace('default:', 'DEFAULT;', $code);
  182. yield 'case ;' => [$expected, $code];
  183. yield 'no default, comments' => [
  184. [
  185. 1 => new SwitchAnalysis(
  186. 1,
  187. 6,
  188. 18,
  189. [
  190. new CaseAnalysis(8, 12),
  191. ],
  192. null
  193. ),
  194. ],
  195. '<?php switch($a) {
  196. case 1/* 1 */:
  197. break;
  198. /* 2 */}',
  199. ];
  200. yield 'ternary case' => [
  201. [
  202. 2 => new SwitchAnalysis(
  203. 2,
  204. 8,
  205. 27,
  206. [
  207. new CaseAnalysis(10, 22),
  208. ],
  209. null
  210. ),
  211. ],
  212. '<?php
  213. switch ($a) {
  214. case $b ? "c" : "d" ;
  215. break;
  216. }',
  217. ];
  218. yield 'nested' => [
  219. [
  220. 1 => new SwitchAnalysis(
  221. 1,
  222. 8,
  223. 55,
  224. [
  225. new CaseAnalysis(10, 13),
  226. new CaseAnalysis(18, 21),
  227. new CaseAnalysis(47, 50),
  228. ],
  229. null
  230. ),
  231. 23 => new SwitchAnalysis(
  232. 23,
  233. 30,
  234. 42,
  235. [
  236. new CaseAnalysis(32, 35),
  237. ],
  238. null
  239. ),
  240. ],
  241. '<?php
  242. switch(foo()) {
  243. CASE 1:
  244. break;
  245. case 2:
  246. switch(bar()) {
  247. case 1:
  248. echo 1;
  249. }
  250. break;
  251. case 3:
  252. break;
  253. }
  254. ',
  255. ];
  256. yield 'alternative syntax 2' => [
  257. [
  258. 3 => new SwitchAnalysis(
  259. 3,
  260. 8,
  261. 32,
  262. [
  263. new CaseAnalysis(10, 13),
  264. ],
  265. null
  266. ),
  267. ],
  268. '<?php /* */ switch ($foo):
  269. case 1:
  270. $foo = new class {};
  271. break;
  272. endswitch ?>',
  273. ];
  274. yield [
  275. [],
  276. '<?php',
  277. ];
  278. yield 'function with return type' => [
  279. [1 => new SwitchAnalysis(1, 7, 43, [new CaseAnalysis(9, 12), new CaseAnalysis(33, 36)], null)],
  280. '<?php switch ($foo) { case 10: function foo($x): int {}; return true; case 100: return false; }',
  281. ];
  282. yield 'function with nullable parameter' => [
  283. [1 => new SwitchAnalysis(1, 7, 43, [new CaseAnalysis(9, 12), new CaseAnalysis(33, 36)], null)],
  284. '<?php switch ($foo) { case 10: function foo(?int $x) {}; return true; case 100: return false; }',
  285. ];
  286. }
  287. /**
  288. * @param array<int, AbstractControlCaseStructuresAnalysis> $expectedAnalyses
  289. * @param list<int> $types
  290. *
  291. * @requires PHP 8.1
  292. *
  293. * @dataProvider provideFindControlStructuresPhp81Cases
  294. */
  295. public function testFindControlStructuresPhp81(array $expectedAnalyses, string $source, array $types): void
  296. {
  297. $tokens = Tokens::fromCode($source);
  298. $analyses = iterator_to_array(ControlCaseStructuresAnalyzer::findControlStructures($tokens, $types));
  299. self::assertCount(\count($expectedAnalyses), $analyses);
  300. foreach ($expectedAnalyses as $index => $expectedAnalysis) {
  301. self::assertAnalysis($expectedAnalysis, $analyses[$index]);
  302. }
  303. }
  304. public static function provideFindControlStructuresPhp81Cases(): iterable
  305. {
  306. $switchAnalysis = new SwitchAnalysis(1, 6, 26, [new CaseAnalysis(8, 11)], new DefaultAnalysis(18, 19));
  307. $enumAnalysis = new EnumAnalysis(28, 35, 51, [new CaseAnalysis(37, 41), new CaseAnalysis(46, 49)]);
  308. $matchAnalysis = new MatchAnalysis(57, 63, 98, new DefaultAnalysis(89, 91));
  309. $code = '<?php
  310. switch($a) {
  311. case 1:
  312. echo 2;
  313. default:
  314. echo 1;
  315. }
  316. enum Suit: string {
  317. case Hearts = "foo";
  318. case Hearts2;
  319. }
  320. $expressionResult = match ($condition) {
  321. 1, 2 => foo(),
  322. 3, 4 => bar(),
  323. default => baz(),
  324. };
  325. ';
  326. yield [
  327. [
  328. 1 => $switchAnalysis,
  329. ],
  330. $code,
  331. [T_SWITCH],
  332. ];
  333. if (\defined('T_ENUM')) { // @TODO: drop condition when PHP 8.1+ is required - sadly PHPUnit still calls the provider even if requires condition is not matched
  334. yield [
  335. [
  336. 1 => $switchAnalysis,
  337. 28 => $enumAnalysis,
  338. ],
  339. $code,
  340. [T_SWITCH, T_ENUM],
  341. ];
  342. }
  343. if (\defined('T_MATCH')) { // @TODO: drop condition when PHP 8.0+ is required - sadly PHPUnit still calls the provider even if requires condition is not matched
  344. yield [
  345. [
  346. 57 => $matchAnalysis,
  347. ],
  348. $code,
  349. [T_MATCH],
  350. ];
  351. }
  352. }
  353. public function testNoSupportedControlStructure(): void
  354. {
  355. $tokens = Tokens::fromCode('<?php if(time() > 0){ echo 1; }');
  356. $this->expectException(\InvalidArgumentException::class);
  357. $this->expectExceptionMessage(\sprintf('Unexpected type "%d".', T_IF));
  358. // we use `iterator_to_array` to ensure generator is consumed and it has possibility to raise exception
  359. iterator_to_array(ControlCaseStructuresAnalyzer::findControlStructures($tokens, [T_IF]));
  360. }
  361. private static function assertAnalysis(AbstractControlCaseStructuresAnalysis $expectedAnalysis, AbstractControlCaseStructuresAnalysis $analysis): void
  362. {
  363. self::assertSame($expectedAnalysis->getIndex(), $analysis->getIndex(), 'index');
  364. self::assertSame($expectedAnalysis->getOpenIndex(), $analysis->getOpenIndex(), 'open index');
  365. self::assertSame($expectedAnalysis->getCloseIndex(), $analysis->getCloseIndex(), 'close index');
  366. self::assertInstanceOf(\get_class($expectedAnalysis), $analysis);
  367. if ($expectedAnalysis instanceof MatchAnalysis || $expectedAnalysis instanceof SwitchAnalysis) {
  368. $expectedDefault = $expectedAnalysis->getDefaultAnalysis();
  369. $actualDefault = $analysis->getDefaultAnalysis(); // @phpstan-ignore-line already type checked against expected
  370. if (null === $expectedDefault) {
  371. self::assertNull($actualDefault, 'default not null');
  372. } else {
  373. self::assertSame($expectedDefault->getIndex(), $actualDefault->getIndex(), 'default index');
  374. self::assertSame($expectedDefault->getColonIndex(), $actualDefault->getColonIndex(), 'default colon index');
  375. }
  376. }
  377. if ($expectedAnalysis instanceof EnumAnalysis || $expectedAnalysis instanceof SwitchAnalysis) {
  378. $expectedCases = $expectedAnalysis->getCases();
  379. $actualCases = $analysis->getCases(); // @phpstan-ignore-line already type checked against expected
  380. self::assertCount(\count($expectedCases), $actualCases);
  381. foreach ($expectedCases as $i => $expectedCase) {
  382. self::assertSame($expectedCase->getIndex(), $actualCases[$i]->getIndex(), 'case index');
  383. self::assertSame($expectedCase->getColonIndex(), $actualCases[$i]->getColonIndex(), 'case colon index');
  384. }
  385. }
  386. self::assertSame(
  387. serialize($expectedAnalysis),
  388. serialize($analysis)
  389. );
  390. }
  391. }