* 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 */ final class ClassDefinitionFixerTest extends AbstractFixerTestCase { public function testConfigureDefaultToFalse(): void { $defaultConfig = [ 'multi_line_extends_each_single_line' => false, 'single_item_single_line' => false, 'single_line' => false, 'space_before_parenthesis' => false, 'inline_constructor_arguments' => true, ]; $fixer = new ClassDefinitionFixer(); $fixer->configure($defaultConfig); self::assertConfigurationSame($defaultConfig, $fixer); $fixer->configure([]); self::assertConfigurationSame($defaultConfig, $fixer); } /** * @param string $expected PHP source code * @param string $input PHP source code * @param array $config * * @dataProvider provideFixingAnonymousClassesCases */ public function testFixingAnonymousClasses(string $expected, string $input, array $config = []): void { $this->fixer->configure($config); $this->doTest($expected, $input); } /** * @dataProvider provideFixingClassesCases */ public function testFixingClasses(string $expected, string $input): void { $this->fixer->configure([]); $this->doTest($expected, $input); } /** * @param array $config * * @dataProvider provideFixingClassesWithConfigCases */ public function testFixingClassesWithConfig(string $expected, string $input, array $config): void { $this->fixer->configure($config); $this->doTest($expected, $input); } /** * @dataProvider provideFixingInterfacesCases */ public function testFixingInterfaces(string $expected, string $input): void { $this->fixer->configure([]); $this->doTest($expected, $input); } /** * @dataProvider provideFixingTraitsCases */ public function testFixingTraits(string $expected, string $input): void { $this->fixer->configure([]); $this->doTest($expected, $input); } public function testInvalidConfigurationKey(): void { $this->expectException(InvalidFixerConfigurationException::class); $this->expectExceptionMessageMatches( '/^\[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"\.$/' ); $fixer = new ClassDefinitionFixer(); $fixer->configure(['a' => false]); } public function testInvalidConfigurationValueType(): void { $this->expectException(InvalidFixerConfigurationException::class); $this->expectExceptionMessageMatches( '/^\[class_definition\] Invalid configuration: The option "single_line" with value "z" is expected to be of type "bool", but is of type "string"\.$/' ); $fixer = new ClassDefinitionFixer(); $fixer->configure(['single_line' => 'z']); } public static function provideFixingAnonymousClassesCases(): 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], ]; } public static function provideFixingClassesCases(): iterable { return array_merge( self::provideClassyCases('class'), self::provideClassyExtendingCases('class'), self::provideClassyImplementsCases() ); } public static function provideFixingClassesWithConfigCases(): iterable { yield [ " 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, ], ]; } public static function provideFixingInterfacesCases(): iterable { yield from array_merge( self::provideClassyCases('interface'), self::provideClassyExtendingCases('interface') ); yield [ ' $expected * * @dataProvider provideClassyDefinitionInfoCases */ public function testClassyDefinitionInfo(string $source, array $expected): void { Tokens::clearCache(); $tokens = Tokens::fromCode($source); $method = new \ReflectionMethod($this->fixer, 'getClassyDefinitionInfo'); $method->setAccessible(true); $result = $method->invoke($this->fixer, $tokens, $expected['classy']); 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], ]; if (\PHP_VERSION_ID < 8_00_00) { $multiLine = true; $code = 'test(); }'; } else { $multiLine = false; $code = 'test(); }'; } yield [ $code, 'numberOfImplements', ['start' => 36, 'numberOfImplements' => 2, 'multiLine' => $multiLine], ]; yield [ " 12, 'numberOfExtends' => 1, 'multiLine' => true], ]; yield [ " 16, 'numberOfImplements' => 2, 'multiLine' => false], ]; yield [ " 12, 'numberOfExtends' => 1, 'multiLine' => true], ]; } /** * @dataProvider provideFixCases */ public function testFix(string $expected, ?string $input = null): void { $this->fixer->configure([]); $this->doTest($expected, $input); } public static function provideFixCases(): iterable { yield [ '', '', ]; yield [ 'fixer->setWhitespacesConfig(new WhitespacesFixerConfig("\t", "\r\n")); $this->fixer->configure([]); $this->doTest($expected, $input); } public static function provideMessyWhitespacesCases(): iterable { yield [ "doTest($expected, $input); } public static function provideFix80Cases(): iterable { yield 'anonymous class, single attribute' => [ ' [ 'doTest($expected, $input); } public static function provideFix81Cases(): iterable { yield [ "doTest($expected, $input); } public static function provideFix82Cases(): iterable { yield 'final readonly works' => [ ' [ ' [ 'doTest($expected, $input); } public static function provideFix83Cases(): iterable { yield 'anonymous class, readonly, missing spacing' => [ ' [ ' [ ' [ ' $expected */ private static function assertConfigurationSame(array $expected, ClassDefinitionFixer $fixer): void { $reflectionProperty = new \ReflectionProperty($fixer, 'configuration'); $reflectionProperty->setAccessible(true); self::assertSame($expected, $reflectionProperty->getValue($fixer)); } /** * @param array $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())); $method = new \ReflectionMethod($this->fixer, 'getClassyInheritanceInfo'); $method->setAccessible(true); $result = $method->invoke($this->fixer, $tokens, $expected['start'], $label); self::assertSame($expected, $result); } private static function provideClassyCases(string $classy): array { return [ [ sprintf(" [ " [ " [ '