* Dariusz RumiƄski * * This source file is subject to the MIT license that is bundled * with this source code in the file LICENSE. */ namespace PhpCsFixer\Tests\Tokenizer\Analyzer; use PhpCsFixer\Tests\TestCase; use PhpCsFixer\Tokenizer\Analyzer\Analysis\AbstractControlCaseStructuresAnalysis; use PhpCsFixer\Tokenizer\Analyzer\Analysis\CaseAnalysis; use PhpCsFixer\Tokenizer\Analyzer\Analysis\DefaultAnalysis; use PhpCsFixer\Tokenizer\Analyzer\Analysis\EnumAnalysis; use PhpCsFixer\Tokenizer\Analyzer\Analysis\MatchAnalysis; use PhpCsFixer\Tokenizer\Analyzer\Analysis\SwitchAnalysis; use PhpCsFixer\Tokenizer\Analyzer\ControlCaseStructuresAnalyzer; use PhpCsFixer\Tokenizer\Tokens; /** * @covers \PhpCsFixer\Tokenizer\Analyzer\ControlCaseStructuresAnalyzer * * @internal */ final class ControlCaseStructuresAnalyzerTest extends TestCase { /** * @param array $expectedAnalyses * * @dataProvider provideFindControlStructuresCases */ public function testFindControlStructures(array $expectedAnalyses, string $source): void { $tokens = Tokens::fromCode($source); $analyses = iterator_to_array(ControlCaseStructuresAnalyzer::findControlStructures($tokens, [T_SWITCH])); self::assertCount(\count($expectedAnalyses), $analyses); foreach ($expectedAnalyses as $index => $expectedAnalysis) { self::assertAnalysis($expectedAnalysis, $analyses[$index]); } } public static function provideFindControlStructuresCases(): iterable { yield 'two cases' => [ [1 => new SwitchAnalysis(1, 7, 46, [new CaseAnalysis(9, 12), new CaseAnalysis(36, 39)], null)], ' [ [1 => new SwitchAnalysis(1, 7, 34, [new CaseAnalysis(9, 12), new CaseAnalysis(19, 22), new CaseAnalysis(24, 27)], null)], ' [ [ 1 => new SwitchAnalysis( 1, 7, 132, [ new CaseAnalysis(17, 22), new CaseAnalysis(29, 40), new CaseAnalysis(47, 53), new CaseAnalysis(60, 71), new CaseAnalysis(78, 125), ], new DefaultAnalysis(9, 10) ), ], 'bar: return 4; case $a->$b::$c->${$d}->${$e}::foo(function ($x) { return $x * 2 + 2; })->$g::$h: return 5; }', ]; yield 'two case and default' => [ [1 => new SwitchAnalysis(1, 7, 38, [new CaseAnalysis(9, 12), new CaseAnalysis(19, 22)], new DefaultAnalysis(29, 30))], ' [ [1 => new SwitchAnalysis(1, 7, 38, [new CaseAnalysis(9, 12), new CaseAnalysis(19, 22)], new DefaultAnalysis(29, 30))], ' [ [1 => new SwitchAnalysis(1, 7, 39, [new CaseAnalysis(9, 22), new CaseAnalysis(29, 32)], null)], ' [ [ 1 => new SwitchAnalysis(1, 7, 67, [new CaseAnalysis(9, 12), new CaseAnalysis(57, 60)], null), 14 => new SwitchAnalysis(14, 20, 52, [new CaseAnalysis(22, 25), new CaseAnalysis(32, 35), new CaseAnalysis(42, 45)], null), ], ' [ [ 1 => new SwitchAnalysis(1, 7, 98, [new CaseAnalysis(9, 81), new CaseAnalysis(88, 91)], null), 25 => new SwitchAnalysis(25, 31, 63, [new CaseAnalysis(33, 36), new CaseAnalysis(43, 46), new CaseAnalysis(53, 56)], null), ], ' [ [1 => new SwitchAnalysis(1, 7, 30, [new CaseAnalysis(9, 12), new CaseAnalysis(19, 22)], null)], ' [ [1 => new SwitchAnalysis(1, 7, 31, [new CaseAnalysis(9, 12), new CaseAnalysis(19, 22)], null)], '', ]; yield 'alternative syntax nested' => [ [ 1 => new SwitchAnalysis(1, 7, 69, [new CaseAnalysis(9, 12), new CaseAnalysis(58, 61)], null), 14 => new SwitchAnalysis(14, 20, 53, [new CaseAnalysis(22, 25), new CaseAnalysis(32, 35), new CaseAnalysis(42, 45)], null), ], ' [ [ 1 => new SwitchAnalysis(1, 7, 69, [new CaseAnalysis(9, 12), new CaseAnalysis(58, 61)], null), 14 => new SwitchAnalysis(14, 20, 53, [new CaseAnalysis(22, 25), new CaseAnalysis(32, 35), new CaseAnalysis(42, 45)], null), ], ' [ [ 1 => new SwitchAnalysis(1, 7, 70, [new CaseAnalysis(9, 12), new CaseAnalysis(58, 61)], null), 14 => new SwitchAnalysis(14, 20, 53, [new CaseAnalysis(22, 25), new CaseAnalysis(32, 35), new CaseAnalysis(42, 45)], null), ], ' new SwitchAnalysis( 1, 6, 22, [ new CaseAnalysis(8, 11), ], new DefaultAnalysis(16, 17) ), ]; $code = ' [$expected, $code]; $code = str_replace('case 1:', 'case 1;', $code); $code = str_replace('default:', 'DEFAULT;', $code); yield 'case ;' => [$expected, $code]; yield 'no default, comments' => [ [ 1 => new SwitchAnalysis( 1, 6, 18, [ new CaseAnalysis(8, 12), ], null ), ], ' [ [ 2 => new SwitchAnalysis( 2, 8, 27, [ new CaseAnalysis(10, 22), ], null ), ], ' [ [ 1 => new SwitchAnalysis( 1, 8, 55, [ new CaseAnalysis(10, 13), new CaseAnalysis(18, 21), new CaseAnalysis(47, 50), ], null ), 23 => new SwitchAnalysis( 23, 30, 42, [ new CaseAnalysis(32, 35), ], null ), ], ' [ [ 3 => new SwitchAnalysis( 3, 8, 32, [ new CaseAnalysis(10, 13), ], null ), ], '', ]; yield [ [], ' [ [1 => new SwitchAnalysis(1, 7, 43, [new CaseAnalysis(9, 12), new CaseAnalysis(33, 36)], null)], ' [ [1 => new SwitchAnalysis(1, 7, 43, [new CaseAnalysis(9, 12), new CaseAnalysis(33, 36)], null)], ' $expectedAnalyses * @param list $types * * @requires PHP 8.1 * * @dataProvider provideFindControlStructuresPhp81Cases */ public function testFindControlStructuresPhp81(array $expectedAnalyses, string $source, array $types): void { $tokens = Tokens::fromCode($source); $analyses = iterator_to_array(ControlCaseStructuresAnalyzer::findControlStructures($tokens, $types)); self::assertCount(\count($expectedAnalyses), $analyses); foreach ($expectedAnalyses as $index => $expectedAnalysis) { self::assertAnalysis($expectedAnalysis, $analyses[$index]); } } public static function provideFindControlStructuresPhp81Cases(): iterable { $switchAnalysis = new SwitchAnalysis(1, 6, 26, [new CaseAnalysis(8, 11)], new DefaultAnalysis(18, 19)); $enumAnalysis = new EnumAnalysis(28, 35, 51, [new CaseAnalysis(37, 41), new CaseAnalysis(46, 49)]); $matchAnalysis = new MatchAnalysis(57, 63, 98, new DefaultAnalysis(89, 91)); $code = ' foo(), 3, 4 => bar(), default => baz(), }; '; yield [ [ 1 => $switchAnalysis, ], $code, [T_SWITCH], ]; 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 yield [ [ 1 => $switchAnalysis, 28 => $enumAnalysis, ], $code, [T_SWITCH, T_ENUM], ]; } 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 yield [ [ 57 => $matchAnalysis, ], $code, [T_MATCH], ]; } } public function testNoSupportedControlStructure(): void { $tokens = Tokens::fromCode(' 0){ echo 1; }'); $this->expectException(\InvalidArgumentException::class); $this->expectExceptionMessage(\sprintf('Unexpected type "%d".', T_IF)); // we use `iterator_to_array` to ensure generator is consumed and it has possibility to raise exception iterator_to_array(ControlCaseStructuresAnalyzer::findControlStructures($tokens, [T_IF])); } private static function assertAnalysis(AbstractControlCaseStructuresAnalysis $expectedAnalysis, AbstractControlCaseStructuresAnalysis $analysis): void { self::assertSame($expectedAnalysis->getIndex(), $analysis->getIndex(), 'index'); self::assertSame($expectedAnalysis->getOpenIndex(), $analysis->getOpenIndex(), 'open index'); self::assertSame($expectedAnalysis->getCloseIndex(), $analysis->getCloseIndex(), 'close index'); self::assertInstanceOf(\get_class($expectedAnalysis), $analysis); if ($expectedAnalysis instanceof MatchAnalysis || $expectedAnalysis instanceof SwitchAnalysis) { $expectedDefault = $expectedAnalysis->getDefaultAnalysis(); $actualDefault = $analysis->getDefaultAnalysis(); // @phpstan-ignore-line already type checked against expected if (null === $expectedDefault) { self::assertNull($actualDefault, 'default not null'); } else { self::assertSame($expectedDefault->getIndex(), $actualDefault->getIndex(), 'default index'); self::assertSame($expectedDefault->getColonIndex(), $actualDefault->getColonIndex(), 'default colon index'); } } if ($expectedAnalysis instanceof EnumAnalysis || $expectedAnalysis instanceof SwitchAnalysis) { $expectedCases = $expectedAnalysis->getCases(); $actualCases = $analysis->getCases(); // @phpstan-ignore-line already type checked against expected self::assertCount(\count($expectedCases), $actualCases); foreach ($expectedCases as $i => $expectedCase) { self::assertSame($expectedCase->getIndex(), $actualCases[$i]->getIndex(), 'case index'); self::assertSame($expectedCase->getColonIndex(), $actualCases[$i]->getColonIndex(), 'case colon index'); } } self::assertSame( serialize($expectedAnalysis), serialize($analysis) ); } }