* 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\Fixer\ClassNotation; use PhpCsFixer\ConfigurationException\InvalidFixerConfigurationException; use PhpCsFixer\Fixer\ClassNotation\ClassDefinitionFixer; use PhpCsFixer\Tests\Test\AbstractFixerTestCase; use PhpCsFixer\Tokenizer\Tokens; use PhpCsFixer\WhitespacesFixerConfig; /** * @internal * * @covers \PhpCsFixer\Fixer\ClassNotation\ClassDefinitionFixer * * @extends AbstractFixerTestCase<\PhpCsFixer\Fixer\ClassNotation\ClassDefinitionFixer> * * @phpstan-import-type _AutogeneratedInputConfiguration from \PhpCsFixer\Fixer\ClassNotation\ClassDefinitionFixer */ final class ClassDefinitionFixerTest extends AbstractFixerTestCase { public function testConfigure(): void { $defaultConfig = [ 'inline_constructor_arguments' => true, 'multi_line_extends_each_single_line' => false, 'single_item_single_line' => false, 'single_line' => false, 'space_before_parenthesis' => false, ]; $fixer = new ClassDefinitionFixer(); $fixer->configure($defaultConfig); self::assertConfigurationSame($defaultConfig, $fixer); $fixer->configure([]); self::assertConfigurationSame($defaultConfig, $fixer); } /** * @param _AutogeneratedInputConfiguration $config * * @dataProvider provideInvalidConfigurationCases */ public function testInvalidConfiguration(array $config, string $exceptionExpression): void { $this->expectException(InvalidFixerConfigurationException::class); $this->expectExceptionMessageMatches($exceptionExpression); $this->fixer->configure($config); } /** * @return iterable, string}> */ public static function provideInvalidConfigurationCases(): iterable { yield 'invalid configuration key' => [ ['a' => false], '/^\[class_definition\] Invalid configuration: The option "a" does not exist\. Defined options are: "inline_constructor_arguments", "multi_line_extends_each_single_line", "single_item_single_line", "single_line", "space_before_parenthesis"\.$/', ]; yield 'invalid configuration value' => [ ['single_line' => 'z'], '/^\[class_definition\] Invalid configuration: The option "single_line" with value "z" is expected to be of type "bool", but is of type "string"\.$/', ]; } /** * @param _AutogeneratedInputConfiguration $configuration * * @dataProvider provideFixCases */ public function testFix(string $expected, ?string $input = null, array $configuration = []): void { $this->fixer->configure($configuration); $this->doTest($expected, $input); } public static function provideFixCases(): iterable { yield [ ' true], ]; yield [ " true], ]; yield [ ' false], ]; yield [ ' false], ]; yield [ 'foo() , bar ( $a) ) {};', "foo() , bar ( \$a) ){};", ['inline_constructor_arguments' => false], ]; yield [ ' false], ]; yield [ 'prop) {};', 'prop ){};', ]; yield [ 'prop ) {};', 'prop ){};', ['inline_constructor_arguments' => false], ]; yield [ " false], ]; yield [ " false], ]; yield [ 'prop, $v[3], 4) {};', 'prop,$v[3], 4) {};', ]; yield 'PSR-12 Extends/Implements Parenthesis on the next line.' => [ ' [ ' [ ' true], ]; yield [ ' [ ' true], ]; yield 'space_before_parenthesis 2' => [ ' true], ]; yield 'space_before_parenthesis and inline_constructor_arguments' => [ 'bar()) ,baz() ) {};', 'bar()) ,baz() ) {};', ['space_before_parenthesis' => true, 'inline_constructor_arguments' => false], ]; yield 'single attribute on separate line' => [ <<<'EOF' [ <<<'EOF' [ <<<'EOF' [ <<<'EOF' [ <<<'EOF' [ <<<'EOF' true], ]; yield [ " true], ]; yield [ " false, 'single_item_single_line' => true], ]; yield [ " true], ]; yield [ " true], ]; yield [ " true], ]; yield [ " false, 'single_item_single_line' => true], ]; yield [ " false, 'single_item_single_line' => false, 'multi_line_extends_each_single_line' => true, ], ]; yield from self::provideClassyCases('interface'); yield from self::provideClassyExtendingCases('interface'); yield [ '', '', ]; yield [ ' $expected * * @dataProvider provideClassyDefinitionInfoCases */ public function testClassyDefinitionInfo(string $source, array $expected): void { Tokens::clearCache(); $tokens = Tokens::fromCode($source); $result = \Closure::bind(static fn (ClassDefinitionFixer $fixer): array => $fixer->getClassyDefinitionInfo($tokens, $expected['classy']), null, ClassDefinitionFixer::class)($this->fixer); ksort($expected); ksort($result); self::assertSame($expected, $result); } public static function provideClassyDefinitionInfoCases(): iterable { yield [ ' 1, 'classy' => 1, 'open' => 4, 'extends' => false, 'implements' => false, 'anonymousClass' => false, 'final' => false, 'abstract' => false, 'readonly' => false, ], ]; yield [ ' 1, 'classy' => 3, 'open' => 6, 'extends' => false, 'implements' => false, 'anonymousClass' => false, 'final' => 1, 'abstract' => false, 'readonly' => false, ], ]; yield [ ' 1, 'classy' => 5, 'open' => 8, 'extends' => false, 'implements' => false, 'anonymousClass' => false, 'final' => false, 'abstract' => 1, 'readonly' => false, ], ]; yield [ ' 1, 'classy' => 1, 'open' => 9, 'extends' => [ 'start' => 5, 'numberOfExtends' => 1, 'multiLine' => false, ], 'implements' => false, 'anonymousClass' => false, 'final' => false, 'abstract' => false, 'readonly' => false, ], ]; yield [ ' 1, 'classy' => 1, 'open' => 13, 'extends' => [ 'start' => 5, 'numberOfExtends' => 3, 'multiLine' => false, ], 'implements' => false, 'anonymousClass' => false, 'final' => false, 'abstract' => false, 'readonly' => false, ], ]; } /** * @param array $expected * * @dataProvider provideClassyInheritanceInfoCases */ public function testClassyInheritanceInfo(string $source, string $label, array $expected): void { $this->doTestClassyInheritanceInfo($source, $label, $expected); } public static function provideClassyInheritanceInfoCases(): iterable { yield '1' => [ ' 5, 'numberOfImplements' => 3, 'multiLine' => false], ]; yield '2' => [ ' 5, 'numberOfImplements' => 3, 'multiLine' => false], ]; yield '3' => [ ' 5, 'numberOfImplements' => 1, 'multiLine' => false], ]; yield '4' => [ " 5, 'numberOfImplements' => 2, 'multiLine' => true], ]; yield '5' => [ " 5, 'numberOfImplements' => 3, 'multiLine' => false], ]; yield [ 'test(); }', 'numberOfImplements', ['start' => 36, 'numberOfImplements' => 2, 'multiLine' => false], ]; yield [ " 12, 'numberOfExtends' => 1, 'multiLine' => true], ]; yield [ " 16, 'numberOfImplements' => 2, 'multiLine' => false], ]; yield [ " 12, 'numberOfExtends' => 1, 'multiLine' => true], ]; } /** * @param array $expected * * @dataProvider provideClassyInheritanceInfoPre80Cases * * @requires PHP <8.0 */ public function testClassyInheritanceInfoPre80(string $source, string $label, array $expected): void { $this->doTestClassyInheritanceInfo($source, $label, $expected); } public static function provideClassyInheritanceInfoPre80Cases(): iterable { yield [ 'test(); }', 'numberOfImplements', ['start' => 36, 'numberOfImplements' => 2, 'multiLine' => true], ]; } /** * @dataProvider provideWithWhitespacesConfigCases */ public function testWithWhitespacesConfig(string $expected, ?string $input = null): void { $this->fixer->setWhitespacesConfig(new WhitespacesFixerConfig("\t", "\r\n")); $this->doTest($expected, $input); } /** * @return iterable */ public static function provideWithWhitespacesConfigCases(): iterable { yield [ "doTest($expected, $input); } /** * @return iterable */ public static function provideFix80Cases(): iterable { yield 'anonymous class, single attribute' => [ ' [ 'doTest($expected, $input); } /** * @return iterable */ public static function provideFix81Cases(): iterable { yield [ "doTest($expected, $input); } /** * @return iterable */ public static function provideFix82Cases(): iterable { yield 'final readonly works' => [ ' [ ' [ 'doTest($expected, $input); } /** * @return iterable */ public static function provideFix83Cases(): iterable { yield 'anonymous class, readonly, missing spacing' => [ ' [ ' [ ' [ ' $expected */ private function doTestClassyInheritanceInfo(string $source, string $label, array $expected): void { Tokens::clearCache(); $tokens = Tokens::fromCode($source); self::assertTrue($tokens[$expected['start']]->isGivenKind([T_IMPLEMENTS, T_EXTENDS]), \sprintf('Token must be "implements" or "extends", got "%s".', $tokens[$expected['start']]->getContent())); $result = \Closure::bind(static fn (ClassDefinitionFixer $fixer): array => $fixer->getClassyInheritanceInfo($tokens, $expected['start'], $label), null, ClassDefinitionFixer::class)($this->fixer); self::assertSame($expected, $result); } /** * @param array $expected */ private static function assertConfigurationSame(array $expected, ClassDefinitionFixer $fixer): void { self::assertSame( $expected, \Closure::bind(static fn (ClassDefinitionFixer $fixer): array => $fixer->configuration, null, ClassDefinitionFixer::class)($fixer), ); } /** * @return iterable */ private static function provideClassyCases(string $classy): iterable { return [ [ \sprintf(" */ private static function provideClassyExtendingCases(string $classy): iterable { return [ [ \sprintf(" */ private static function provideClassyImplementsCases(): iterable { return [ [ ' [ " [ " [ '