Browse Source

Merge branch '1.12'

Conflicts:
	Symfony/CS/Tests/FixerTest.php
	Symfony/CS/Tests/Fixtures/Integration/misc/class_definition,trailing_spaces.test
	src/Fixer/ClassNotation/ClassDefinitionFixer.php
	tests/Fixer/ClassNotation/ClassDefinitionFixerTest.php
Dariusz Ruminski 8 years ago
parent
commit
25f24056cd

+ 223 - 109
src/Fixer/ClassNotation/ClassDefinitionFixer.php

@@ -13,8 +13,10 @@
 namespace PhpCsFixer\Fixer\ClassNotation;
 
 use PhpCsFixer\AbstractFixer;
+use PhpCsFixer\ConfigurationException\InvalidFixerConfigurationException;
 use PhpCsFixer\Tokenizer\Token;
 use PhpCsFixer\Tokenizer\Tokens;
+use PhpCsFixer\Tokenizer\TokensAnalyzer;
 
 /**
  * Fixer for part of the rules defined in PSR2 ¶4.1 Extends and Implements.
@@ -23,6 +25,51 @@ use PhpCsFixer\Tokenizer\Tokens;
  */
 final class ClassDefinitionFixer extends AbstractFixer
 {
+    /**
+     * @var array<string, bool>
+     */
+    private static $defaultConfig = array(
+        // put class declaration on one line
+        'singleLine' => false,
+        // if a classy extends or implements only one element than put it on the same line
+        'singleItemSingleLine' => false,
+        // if an interface extends multiple interfaces declared over multiple lines put each interface on its own line
+        'multiLineExtendsEachSingleLine' => false,
+    );
+
+    /**
+     * @var array
+     */
+    private $config;
+
+    /**
+     * @param array $configuration
+     *
+     * @throws InvalidFixerConfigurationException
+     */
+    public function configure(array $configuration = null)
+    {
+        if (null === $configuration || count($configuration) < 1) {
+            $this->config = self::$defaultConfig;
+
+            return;
+        }
+
+        $configuration = array_merge(self::$defaultConfig, $configuration);
+
+        foreach ($configuration as $item => $value) {
+            if (!array_key_exists($item, self::$defaultConfig)) {
+                throw new InvalidFixerConfigurationException('class_definition', sprintf('Unknown configuration item "%s", expected any of "%s".', $item, implode(', ', array_keys(self::$defaultConfig))));
+            }
+
+            if (!is_bool($value)) {
+                throw new InvalidFixerConfigurationException('class_definition', sprintf('Configuration value for item "%s" must be a bool, got "%s".', $item, is_object($value) ? get_class($value) : gettype($value)));
+            }
+        }
+
+        $this->config = $configuration;
+    }
+
     /**
      * {@inheritdoc}
      */
@@ -36,12 +83,11 @@ final class ClassDefinitionFixer extends AbstractFixer
      */
     public function fix(\SplFileInfo $file, Tokens $tokens)
     {
-        for ($index = $tokens->getSize() - 1; $index > 0; --$index) {
-            if (!$tokens[$index]->isClassy()) {
-                continue;
+        // -4, one for count to index, 3 because min. of tokens for a classy location.
+        for ($index = $tokens->getSize() - 4; $index > 0; --$index) {
+            if ($tokens[$index]->isClassy()) {
+                $this->fixClassyDefinition($tokens, $index);
             }
-
-            $this->fixClassDefinition($tokens, $index, $tokens->getNextTokenOfKind($index, array('{')));
         }
     }
 
@@ -54,169 +100,237 @@ final class ClassDefinitionFixer extends AbstractFixer
     }
 
     /**
-     * {@inheritdoc}
+     * @param Tokens $tokens
+     * @param int    $classyIndex Class definition token start index
      */
-    public function getPriority()
+    private function fixClassyDefinition(Tokens $tokens, $classyIndex)
     {
-        // should be run before the NoTrailingWhitespaceFixer
-        return 21;
+        $classDefInfo = $this->getClassyDefinitionInfo($tokens, $classyIndex);
+
+        // PSR: class definition open curly brace must go on a new line
+        $classDefInfo['open'] = $this->fixClassyDefinitionOpenSpacing($tokens, $classDefInfo['open']);
+
+        // PSR2 4.1 Lists of implements MAY be split across multiple lines, where each subsequent line is indented once.
+        // When doing so, the first item in the list MUST be on the next line, and there MUST be only one interface per line.
+        if (false !== $classDefInfo['implements']) {
+            $this->fixClassyDefinitionImplements(
+                $tokens,
+                $classDefInfo['open'],
+                $classDefInfo['implements']
+            );
+        }
+
+        if (false !== $classDefInfo['extends']) {
+            $this->fixClassyDefinitionExtends(
+                $tokens,
+                false === $classDefInfo['implements'] ? $classDefInfo['open'] : 1 + $classDefInfo['implements']['start'],
+                $classDefInfo['extends']
+            );
+        }
+
+        if ($classDefInfo['implements']) {
+            $end = $classDefInfo['implements']['start'];
+        } elseif ($classDefInfo['extends']) {
+            $end = $classDefInfo['extends']['start'];
+        } else {
+            $end = $tokens->getPrevNonWhitespace($classDefInfo['open']);
+        }
+
+        $tokensAnalyzer = new TokensAnalyzer($tokens);
+
+        // 4.1 The extends and implements keywords MUST be declared on the same line as the class name.
+        $this->makeClassyDefinitionSingleLine(
+            $tokens,
+            $tokensAnalyzer->isAnonymousClass($classyIndex) ? $tokens->getPrevMeaningfulToken($classyIndex) : $classDefInfo['start'],
+            $end
+        );
+    }
+
+    private function fixClassyDefinitionExtends(Tokens $tokens, $classOpenIndex, $classExtendsInfo)
+    {
+        $endIndex = $tokens->getPrevNonWhitespace($classOpenIndex);
+
+        if ($this->config['singleLine'] || false === $classExtendsInfo['multiLine']) {
+            $this->makeClassyDefinitionSingleLine($tokens, $classExtendsInfo['start'], $endIndex);
+        } elseif ($this->config['singleItemSingleLine'] && 1 === $classExtendsInfo['numberOfExtends']) {
+            $this->makeClassyDefinitionSingleLine($tokens, $classExtendsInfo['start'], $endIndex);
+        } elseif ($this->config['multiLineExtendsEachSingleLine'] && $classExtendsInfo['multiLine']) {
+            $this->makeClassyInheritancePartMultiLine($tokens, $classExtendsInfo['start'], $endIndex);
+        }
     }
 
     /**
      * @param Tokens $tokens
-     * @param int    $start      Class definition token start index
-     * @param int    $classyOpen Class definition token end index
+     * @param int    $classOpenIndex
+     * @param array  $classImplementsInfo
      */
-    private function fixClassDefinition(Tokens $tokens, $start, $classyOpen)
+    private function fixClassyDefinitionImplements(Tokens $tokens, $classOpenIndex, array $classImplementsInfo)
     {
-        // check if there is a `implements` part in the definition, since there are rules for it in PSR 2.
-        $implementsInfo = $this->getMultiLineInfo($tokens, $start, $classyOpen);
+        $endIndex = $tokens->getPrevNonWhitespace($classOpenIndex);
 
-        // 4.1 The extends and implements keywords MUST be declared on the same line as the class name.
-        if ($implementsInfo['numberOfInterfaces'] > 1 && $implementsInfo['multiLine']) {
-            $classyOpen += $this->ensureWhiteSpaceSeparation($tokens, $start, $implementsInfo['breakAt']);
-            $this->fixMultiLineImplements($tokens, $implementsInfo['breakAt'], $classyOpen);
+        if ($this->config['singleLine'] || false === $classImplementsInfo['multiLine']) {
+            $this->makeClassyDefinitionSingleLine($tokens, $classImplementsInfo['start'], $endIndex);
+        } elseif ($this->config['singleItemSingleLine'] && 1 === $classImplementsInfo['numberOfImplements']) {
+            $this->makeClassyDefinitionSingleLine($tokens, $classImplementsInfo['start'], $endIndex);
         } else {
-            $classyOpen -= $tokens[$classyOpen - 1]->isWhitespace() ? 2 : 1;
-            $this->ensureWhiteSpaceSeparation($tokens, $start, $classyOpen);
+            $this->makeClassyInheritancePartMultiLine($tokens, $classImplementsInfo['start'], $endIndex);
         }
     }
 
     /**
-     * Returns an array with `implements` data.
-     *
-     * Returns array:
-     * * int  'breakAt'            index of the Token of type T_IMPLEMENTS for the definition, or 0
-     * * int  'numberOfInterfaces'
-     * * bool 'multiLine'
+     * @param Tokens $tokens
+     * @param int    $openIndex
      *
+     * @return int
+     */
+    private function fixClassyDefinitionOpenSpacing(Tokens $tokens, $openIndex)
+    {
+        if (false !== strpos($tokens[$openIndex - 1]->getContent(), "\n")) {
+            return $openIndex;
+        }
+
+        if ($tokens[$openIndex - 1]->isWhitespace()) {
+            $tokens[$openIndex - 1]->setContent("\n");
+
+            return $openIndex;
+        }
+
+        $tokens->insertAt($openIndex, new Token(array(T_WHITESPACE, "\n")));
+
+        return $openIndex + 1;
+    }
+
+    /**
      * @param Tokens $tokens
-     * @param int    $start
-     * @param int    $classyOpen
+     * @param int    $classyIndex
      *
      * @return array
      */
-    private function getMultiLineInfo(Tokens $tokens, $start, $classyOpen)
+    private function getClassyDefinitionInfo(Tokens $tokens, $classyIndex)
     {
-        $implementsInfo = array('breakAt' => 0, 'numberOfInterfaces' => 0, 'multiLine' => false);
-        $breakAtToken = $tokens->findGivenKind($tokens[$start]->isGivenKind(T_INTERFACE) ? T_EXTENDS : T_IMPLEMENTS, $start, $classyOpen);
-        if (count($breakAtToken) < 1) {
-            return $implementsInfo;
-        }
+        $openIndex = $tokens->getNextTokenOfKind($classyIndex, array('{'));
+        $prev = $tokens->getPrevMeaningfulToken($classyIndex);
+        $startIndex = $tokens[$prev]->isGivenKind(array(T_FINAL, T_ABSTRACT)) ? $prev : $classyIndex;
 
-        $implementsInfo['breakAt'] = key($breakAtToken);
-        $classyOpen = $tokens->getPrevNonWhitespace($classyOpen);
-        for ($j = $implementsInfo['breakAt'] + 1; $j < $classyOpen; ++$j) {
-            if ($tokens[$j]->isGivenKind(T_STRING)) {
-                ++$implementsInfo['numberOfInterfaces'];
-                continue;
-            }
+        $extends = false;
+        $implements = false;
+        if (!(defined('T_TRAIT') && $tokens[$classyIndex]->isGivenKind(T_TRAIT))) {
+            $extends = $tokens->findGivenKind(T_EXTENDS, $classyIndex, $openIndex);
+            $extends = count($extends) ? $this->getClassyInheritanceInfo($tokens, key($extends), $openIndex, 'numberOfExtends') : false;
 
-            if (!$implementsInfo['multiLine'] && ($tokens[$j]->isWhitespace() || $tokens[$j]->isComment()) && false !== strpos($tokens[$j]->getContent(), "\n")) {
-                $implementsInfo['multiLine'] = true;
+            if (!$tokens[$classyIndex]->isGivenKind(T_INTERFACE)) {
+                $implements = $tokens->findGivenKind(T_IMPLEMENTS, $classyIndex, $openIndex);
+                $implements = count($implements) ? $this->getClassyInheritanceInfo($tokens, key($implements), $openIndex, 'numberOfImplements') : false;
             }
         }
 
-        return $implementsInfo;
+        return array(
+            'start' => $startIndex,
+            'classy' => $classyIndex,
+            'open' => $openIndex,
+            'extends' => $extends,
+            'implements' => $implements,
+        );
     }
 
     /**
-     * Fix spacing between lines following `implements`.
-     *
-     * PSR2 4.1 Lists of implements MAY be split across multiple lines, where each subsequent line is indented once.
-     * When doing so, the first item in the list MUST be on the next line, and there MUST be only one interface per line.
-     *
      * @param Tokens $tokens
-     * @param int    $breakAt
-     * @param int    $classyOpen
+     * @param int    $implementsIndex
+     * @param int    $openIndex
+     * @param string $label
+     *
+     * @return array
      */
-    private function fixMultiLineImplements(Tokens $tokens, $breakAt, $classyOpen)
+    private function getClassyInheritanceInfo(Tokens $tokens, $implementsIndex, $openIndex, $label)
     {
-        // implements should be followed by a line break, but we allow a comments before that,
-        // the lines after 'implements' are always build up as (comment|whitespace)*T_STRING{1}(comment|whitespace)*','
-        // after fixing it must be (whitespace indent)(comment)*T_STRING{1}(comment)*','
-        for ($index = $classyOpen - 1; $index > $breakAt - 1; --$index) {
-            if ($tokens[$index]->isWhitespace()) {
-                if ($tokens[$index + 1]->equals(',')) {
-                    $tokens[$index]->clear();
-                } elseif (
-                    $tokens[$index + 1]->isComment()
-                    && ' ' !== $tokens[$index]->getContent()
-                    && !($tokens[$index - 1]->isComment() && "\n" === substr($tokens[$index]->getContent(), 0, 1))
-                ) {
-                    $tokens[$index]->setContent(' ');
-                }
+        $implementsInfo = array('start' => $implementsIndex, $label => 1, 'multiLine' => false);
+        $lastMeaningFul = $tokens->getPrevMeaningfulToken($openIndex);
+        for ($i = $implementsIndex; $i < $lastMeaningFul; ++$i) {
+            if ($tokens[$i]->equals(',')) {
+                ++$implementsInfo[$label];
+
+                continue;
             }
 
-            if ($tokens[$index]->isGivenKind(T_STRING)) {
-                $index = $this->ensureOnNewLine($tokens, $index);
+            if (!$implementsInfo['multiLine'] && false !== strpos($tokens[$i]->getContent(), "\n")) {
+                $implementsInfo['multiLine'] = true;
             }
         }
+
+        return $implementsInfo;
     }
 
     /**
-     * Make sure the tokens are separated by a single space.
-     *
      * @param Tokens $tokens
-     * @param int    $start
-     * @param int    $end
-     *
-     * @return int number tokens inserted by the method before the end token
+     * @param int    $startIndex
+     * @param int    $endIndex
      */
-    private function ensureWhiteSpaceSeparation(Tokens $tokens, $start, $end)
+    private function makeClassyDefinitionSingleLine(Tokens $tokens, $startIndex, $endIndex)
     {
-        $insertCount = 0;
-        for ($i = $end; $i > $start; --$i) {
+        for ($i = $endIndex - 1; $i >= $startIndex; --$i) {
             if ($tokens[$i]->isWhitespace()) {
-                $content = $tokens[$i]->getContent();
                 if (
-                    ' ' !== $content
-                    && !($tokens[$i - 1]->isComment() && "\n" === $content[0])
+                    $tokens[$i + 1]->equalsAny(array(',', '(', ')'))
+                    || $tokens[$i - 1]->equals('(')
+                ) {
+                    $tokens[$i]->clear();
+                } elseif (
+                    !$tokens[$i + 1]->isComment()
+                    && !($tokens[$i - 1]->isGivenKind(T_COMMENT) && '//' === substr($tokens[$i - 1]->getContent(), 0, 2))
                 ) {
                     $tokens[$i]->setContent(' ');
                 }
-                continue;
-            }
 
-            if ($tokens[$i - 1]->isWhitespace() || "\n" === substr($tokens[$i - 1]->getContent(), -1)) {
+                --$i;
                 continue;
             }
 
-            if ($tokens[$i - 1]->isComment() || $tokens[$i]->isComment()) {
-                $tokens->insertAt($i, new Token(array(T_WHITESPACE, ' ')));
-                ++$insertCount;
-                continue;
+            if (
+                !$tokens[$i + 1]->equalsAny(array(',', '(', ')', array(T_NS_SEPARATOR)))
+                && !$tokens[$i]->equalsAny(array('(', array(T_NS_SEPARATOR)))
+                && false === strpos($tokens[$i]->getContent(), "\n")
+            ) {
+                $tokens->insertAt($i + 1, new Token(array(T_WHITESPACE, ' ')));
             }
         }
-
-        return $insertCount;
     }
 
-    private function ensureOnNewLine(Tokens $tokens, $index)
+    /**
+     * @param Tokens $tokens
+     * @param int    $startIndex
+     * @param int    $endIndex
+     */
+    private function makeClassyInheritancePartMultiLine(Tokens $tokens, $startIndex, $endIndex)
     {
-        // while not whitespace and not comment go back
-        for (--$index; $index > 0; --$index) {
-            if (!$tokens[$index]->isGivenKind(array(T_NS_SEPARATOR, T_STRING))) {
-                break;
-            }
-        }
+        for ($i = $endIndex; $i > $startIndex; --$i) {
+            $previousInterfaceImplementingIndex = $tokens->getPrevTokenOfKind($i, array(',', array(T_IMPLEMENTS), array(T_EXTENDS)));
+            $breakAtIndex = $tokens->getNextMeaningfulToken($previousInterfaceImplementingIndex);
+            // make the part of a ',' or 'implements' single line
+            $this->makeClassyDefinitionSingleLine(
+                $tokens,
+                $breakAtIndex,
+                $i
+            );
 
-        if ("\n" === substr($tokens[$index]->getContent(), -1)) {
-            return $index;
-        }
+            // make sure the part is on its own line
+            $isOnOwnLine = false;
+            for ($j = $breakAtIndex; $j > $previousInterfaceImplementingIndex; --$j) {
+                if (false !== strpos($tokens[$j]->getContent(), "\n")) {
+                    $isOnOwnLine = true;
 
-        if (!$tokens[$index]->isWhitespace()) {
-            $tokens->insertAt($index + 1, new Token(array(T_WHITESPACE, "\n")));
+                    break;
+                }
+            }
 
-            return $index;
-        }
+            if (!$isOnOwnLine) {
+                if ($tokens[$breakAtIndex - 1]->isWhitespace()) {
+                    $tokens[$breakAtIndex - 1]->setContent("\n");
+                } else {
+                    $tokens->insertAt($breakAtIndex, new Token(array(T_WHITESPACE, "\n")));
+                }
+            }
 
-        if (false !== strpos($tokens[$index]->getContent(), "\n")) {
-            return $index;
+            $i = $previousInterfaceImplementingIndex + 1;
         }
-
-        $tokens[$index]->setContent($tokens[$index]->getContent()."\n");
-
-        return $index;
     }
 }

+ 610 - 164
tests/Fixer/ClassNotation/ClassDefinitionFixerTest.php

@@ -12,230 +12,413 @@
 
 namespace PhpCsFixer\Tests\Fixer\ClassNotation;
 
+use PhpCsFixer\Fixer\ClassNotation\ClassDefinitionFixer;
 use PhpCsFixer\Test\AbstractFixerTestCase;
+use PhpCsFixer\Tokenizer\Tokens;
 
 /**
  * @internal
  */
 final class ClassDefinitionFixerTest extends AbstractFixerTestCase
 {
+    private static $defaultTestConfig = array(
+        'singleLine' => false,
+        'singleItemSingleLine' => false,
+        'multiLineExtendsEachSingleLine' => false,
+    );
+
+    public function testConfigureDefaultToNull()
+    {
+        $fixer = new ClassDefinitionFixer();
+        $fixer->configure(self::$defaultTestConfig);
+        $fixer->configure(null);
+
+        $defaultConfigProperty = new \ReflectionProperty('PhpCsFixer\Fixer\ClassNotation\ClassDefinitionFixer', 'defaultConfig');
+        $defaultConfigProperty->setAccessible(true);
+
+        $this->assertAttributeSame($defaultConfigProperty->getValue(), 'config', $fixer);
+    }
+
     /**
-     * @dataProvider provideCases
+     * @param string $expected PHP source code
+     * @param string $input    PHP source code
+     *
+     * @dataProvider provideAnonymousClassesCases
+     *
+     * @requires PHP 7.0
      */
-    public function testFix($expected, $input = null)
+    public function testFixingAnonymousClasses($expected, $input)
     {
         $this->doTest($expected, $input);
     }
 
-    public function provideCases()
+    /**
+     * @param string $expected PHP source code
+     * @param string $input    PHP source code
+     *
+     * @dataProvider provideClassesCases
+     */
+    public function testFixingClasses($expected, $input)
+    {
+        $this->doTest($expected, $input);
+    }
+
+    /**
+     * @param string              $expected PHP source code
+     * @param string              $input    PHP source code
+     * @param array<string, bool> $config
+     *
+     * @dataProvider provideClassesWithConfigCases
+     */
+    public function testFixingClassesWithConfig($expected, $input, array $config)
+    {
+        $fixer = $this->getFixer();
+        $fixer->configure($config);
+
+        $this->doTest($expected, $input, null, $fixer);
+    }
+
+    /**
+     * @param string $expected PHP source code
+     * @param string $input    PHP source code
+     *
+     * @dataProvider provideInterfacesCases
+     */
+    public function testFixingInterfaces($expected, $input)
+    {
+        $this->doTest($expected, $input);
+    }
+
+    /**
+     * @param string $expected PHP source code
+     * @param string $input    PHP source code
+     *
+     * @dataProvider provideTraitsCases
+     */
+    public function testFixingTraits($expected, $input)
+    {
+        if (!defined('T_TRAIT')) {
+            $this->markTestSkipped('Test requires traits.');
+        }
+
+        $this->doTest($expected, $input);
+    }
+
+    /**
+     * @expectedException \PhpCsFixer\ConfigurationException\InvalidFixerConfigurationException
+     * @expectedExceptionMessageRegExp /^\[class_definition\] Unknown configuration item "a", expected any of "singleLine, singleItemSingleLine, multiLineExtendsEachSingleLine".$/
+     */
+    public function testInvalidConfigurationKey()
+    {
+        $fixer = new ClassDefinitionFixer();
+        $fixer->configure(array('a' => false));
+    }
+
+    /**
+     * @expectedException \PhpCsFixer\ConfigurationException\InvalidFixerConfigurationException
+     * @expectedExceptionMessageRegExp /^\[class_definition\] Configuration value for item "singleLine" must be a bool, got "string".$/
+     */
+    public function testInvalidConfigurationValueType()
+    {
+        $fixer = new ClassDefinitionFixer();
+        $fixer->configure(array('singleLine' => 'z'));
+    }
+
+    public function provideAnonymousClassesCases()
     {
         return array(
             array(
-                '<?php
-class Aaa implements
-    \RFb,
-    \Fcc, '.'
-\GFddZz
-{
-}',
-                '<?php
-class Aaa implements
-    \RFb,
-    \Fcc, \GFddZz
-{
-}',
+                "<?php \$a = new class\n{};",
+                '<?php $a = new class{};',
             ),
             array(
-                '<?php
-class Aaa implements
-    PhpCsFixer\Tests\Fixer,
-\RFb,
-    \Fcc1, '.'
-\GFdd
-{
-}',
-                '<?php
-class Aaa implements
-    PhpCsFixer\Tests\Fixer,\RFb,
-    \Fcc1, \GFdd
-{
-}',
+                "<?php \$a = new class()\n{};",
+                "<?php \$a = new\n class  (  ){};",
             ),
             array(
-                '<?php
-interface Test extends /*a*/ /*b*/
-TestInterface1, /* test */
-    TestInterface2, // test
-    '.'
+                "<?php \$a = new class(10, 1, /**/ 2)\n{};",
+                '<?php $a = new class(  10, 1,/**/2  ){};',
+            ),
+            array(
+                "<?php \$a = new class(10)\n{};",
+                '<?php $a = new    class(10){};',
+            ),
+            array(
+                "<?php \$a = new class(10) extends SomeClass implements SomeInterface, D\n{};",
+                "<?php \$a = new    class(10)     extends\nSomeClass\timplements    SomeInterface, D {};",
+            ),
+        );
+    }
 
-// test
-TestInterface3, /**/     '.'
-TestInterface4,
-      TestInterface5, /**/
-TestInterface6    {}',
-                '<?php
-interface Test
-extends
-  /*a*/    /*b*/TestInterface1   ,  /* test */
-    TestInterface2   ,   // test
-    '.'
+    public function provideClassesCases()
+    {
+        return array_merge(
+            $this->provideClassyCases('class'),
+            $this->provideClassyExtendingCases('class'),
+            $this->provideClassyImplementsCases()
+        );
+    }
 
-// test
-TestInterface3, /**/     TestInterface4   ,
-      TestInterface5    ,     '.'
-        /**/TestInterface6    {}',
+    public function provideClassesWithConfigCases()
+    {
+        return array(
+            array(
+                "<?php class configA implements B, C\n{}",
+                "<?php class configA implements\nB, C{}",
+                array('singleLine' => true),
             ),
             array(
-                '<?php
-class Test extends TestInterface8 implements /*a*/ /*b*/
-TestInterface1, /* test */
-    TestInterface2, // test
+                "<?php class configA1 extends B\n{}",
+                "<?php class configA1\n extends\nB{}",
+                array('singleLine' => true),
+            ),
+            array(
+                "<?php class configA1a extends B\n{}",
+                "<?php class configA1a\n extends\nB{}",
+                array('singleLine' => false, 'singleItemSingleLine' => true),
+            ),
+            array(
+                "<?php class configA2 extends D implements B, C\n{}",
+                "<?php class configA2 extends D implements\nB,\nC{}",
+                array('singleLine' => true),
+            ),
+            array(
+                "<?php class configA3 extends D implements B, C\n{}",
+                "<?php class configA3\n extends\nD\n\t implements\nB,\nC{}",
+                array('singleLine' => true),
+            ),
+            array(
+                "<?php class configA4 extends D implements B, //\nC\n{}",
+                "<?php class configA4\n extends\nD\n\t implements\nB,//\nC{}",
+                array('singleLine' => true),
+            ),
+            array(
+                "<?php class configA5 implements A\n{}",
+                "<?php class configA5 implements\nA{}",
+                array('singleLine' => false, 'singleItemSingleLine' => true),
+            ),
+            array(
+                "<?php interface TestWithMultiExtendsMultiLine extends\nA,\nAb,\nC,\nD\n{}",
+                "<?php interface TestWithMultiExtendsMultiLine extends A,\nAb,C,D\n{}",
+                array(
+                    'singleLine' => false,
+                    'singleItemSingleLine' => false,
+                    'multiLineExtendsEachSingleLine' => true,
+                ),
+            ),
+        );
+    }
+
+    public function provideInterfacesCases()
+    {
+        $cases = array_merge(
+            $this->provideClassyCases('interface'),
+            $this->provideClassyExtendingCases('interface')
+        );
+
+        $cases[] = array(
+    '<?php
+interface Test extends
+  /*a*/    /*b*/TestInterface1   , \A\B\C  ,  /* test */
+    TestInterface2   ,   // test
     '.'
 
-// test
-TestInterface3, /**/     '.'
-TestInterface4,
-      TestInterface5, /**/
-TestInterface6
-{
-}',
-                '<?php
-class Test
+// Note: PSR does not have a rule for multiple extends
+TestInterface3, /**/     TestInterface4   ,
+      TestInterface5    ,     '.'
+        /**/TestInterface65
+{}
+            ',
+    '<?php
+interface Test
 extends
-    TestInterface8
-  implements  /*a*/    /*b*/TestInterface1   ,  /* test */
+  /*a*/    /*b*/TestInterface1   , \A\B\C  ,  /* test */
     TestInterface2   ,   // test
     '.'
 
-// test
+// Note: PSR does not have a rule for multiple extends
 TestInterface3, /**/     TestInterface4   ,
-      TestInterface5    ,    '.'
-        /**/TestInterface6
-{
-}',
-            ),
-            array(
-                '<?php
-class /**/ Test123 extends /**/ \RuntimeException implements TestZ{
-}',
-                '<?php
-class/**/Test123
-extends  /**/        \RuntimeException    implements
+      TestInterface5    ,     '.'
+        /**/TestInterface65    {}
+            ',
+        );
 
-TestZ{
-}',
-            ),
-            array(
-                '<?php
-class /**/ Test125 //aaa
-extends /*
+        return $cases;
+    }
 
-*/
-//
-\Exception //
-{}',
-                '<?php
-class/**/Test125 //aaa
-extends  /*
+    public function provideTraitsCases()
+    {
+        return $this->provideClassyCases('trait');
+    }
 
-*/
-//
-\Exception        //
-{}',
+    /**
+     * @param string $source   PHP source code
+     * @param array  $expected
+     *
+     * @dataProvider provideClassyDefinitionInfoCases
+     */
+    public function testClassyDefinitionInfo($source, array $expected)
+    {
+        Tokens::clearCache();
+        $tokens = Tokens::fromCode($source);
+
+        $fixer = $this->getFixer();
+        $method = new \ReflectionMethod($fixer, 'getClassyDefinitionInfo');
+        $method->setAccessible(true);
+
+        $result = $method->invoke($fixer, $tokens, $expected['classy']);
+
+        $this->assertSame($expected, $result);
+    }
+
+    public function provideClassyDefinitionInfoCases()
+    {
+        return array(
+            array(
+                '<?php class A{}',
+                array(
+                    'start' => 1,
+                    'classy' => 1,
+                    'open' => 4,
+                    'extends' => false,
+                    'implements' => false,
+                ),
             ),
             array(
-                '<?php
-class Test124 extends \Exception {}',
-                '<?php
-class
-Test124
-
-extends
-\Exception {}',
+                '<?php final class A{}',
+                array(
+                    'start' => 1,
+                    'classy' => 3,
+                    'open' => 6,
+                    'extends' => false,
+                    'implements' => false,
+                ),
             ),
             array(
-                '<?php
-class Aaa implements Fbb, Ccc
-{
-}',
+                '<?php abstract /**/ class A{}',
+                array(
+                    'start' => 1,
+                    'classy' => 5,
+                    'open' => 8,
+                    'extends' => false,
+                    'implements' => false,
+                ),
             ),
             array(
-                '<?php
-    class Aaa implements Ebb, Ccc
-    {
-    }',
+                '<?php class A extends B {}',
+                array(
+                    'start' => 1,
+                    'classy' => 1,
+                    'open' => 9,
+                    'extends' => array(
+                            'start' => 5,
+                            'numberOfExtends' => 1,
+                            'multiLine' => false,
+                        ),
+                    'implements' => false,
+                ),
             ),
             array(
-                '<?php
-class Aaa implements \Dbb, Ccc
-{
-}',
+                '<?php interface A extends B,C,D {}',
+                array(
+                    'start' => 1,
+                    'classy' => 1,
+                    'open' => 13,
+                    'extends' => array(
+                            'start' => 5,
+                            'numberOfExtends' => 3,
+                            'multiLine' => false,
+                        ),
+                    'implements' => false,
+                ),
             ),
+        );
+    }
+
+    /**
+     * @param string $source         PHP source code
+     * @param int    $classOpenIndex classy curly brace open index
+     * @param array  $expected
+     *
+     * @dataProvider provideClassyImplementsInfoCases
+     */
+    public function testClassyInheritanceInfo($source, $classOpenIndex, $label, array $expected)
+    {
+        Tokens::clearCache();
+        $tokens = Tokens::fromCode($source);
+
+        $fixer = $this->getFixer();
+        $method = new \ReflectionMethod($fixer, 'getClassyInheritanceInfo');
+        $method->setAccessible(true);
+
+        $result = $method->invoke($fixer, $tokens, $expected['start'], $classOpenIndex, $label);
+
+        $this->assertSame($expected, $result);
+    }
+
+    public function provideClassyImplementsInfoCases()
+    {
+        return array(
             array(
                 '<?php
-class Aaa implements Cbb, \Ccc
+class X11 implements    Z   , T,R
 {
 }',
+                15,
+                'numberOfImplements',
+                array('start' => 5, 'numberOfImplements' => 3, 'multiLine' => false),
             ),
             array(
                 '<?php
-class Aaa implements \CFb, \Ccc
+class X10 implements    Z   , T,R    //
 {
 }',
+                16,
+                'numberOfImplements',
+                array('start' => 5, 'numberOfImplements' => 3, 'multiLine' => false),
             ),
             array(
-                '<?php
-if (1) {
-    class IndentedClass
-    {
-    }
-}',
+                '<?php class A implements B {}',
+                9,
+                'numberOfImplements',
+                array('start' => 5, 'numberOfImplements' => 1, 'multiLine' => false),
             ),
             array(
-                '<?php
-namespace {
-    class IndentedNameSpacedClass
-    {
-    }
-}',
+                "<?php class A implements B,\n C{}",
+                11,
+                'numberOfImplements',
+                array('start' => 5, 'numberOfImplements' => 2, 'multiLine' => true),
+            ),
+            array(
+                "<?php class A implements Z\\C\\B,C,D  {\n\n\n}",
+                17,
+                'numberOfImplements',
+                array('start' => 5, 'numberOfImplements' => 3, 'multiLine' => false),
             ),
             array(
                 '<?php
-class Aaa implements
-    \CFb,
-    \Ccc,
-    \CFdd
-{
-}', ),
-        );
-    }
-
-    /**
-     * @dataProvider provide54Cases
-     * @requires PHP 5.4
-     */
-    public function testFix54($expected, $input = null)
-    {
-        $this->doTest($expected, $input);
-    }
+namespace A {
+    interface C {}
+}
 
-    public function provide54Cases()
-    {
-        return array(
-            array(
-            '<?php
-trait traitTest
-{}
+namespace {
+    class B{}
 
-trait /**/ traitTest2 //
-/**/ {}',
-            '<?php
-trait
-   traitTest
-{}
+    class A extends //
+        B     implements /*  */ \A
+        \C, Z{
+        public function test()
+        {
+            echo 1;
+        }
+    }
 
-trait/**/traitTest2//
-/**/ {}',
+    $a = new A();
+    $a->test();
+}',
+                48,
+                'numberOfImplements',
+                array('start' => 36, 'numberOfImplements' => 2, 'multiLine' => true),
             ),
         );
     }
@@ -256,7 +439,7 @@ trait/**/traitTest2//
             '<?php
 $a = new class implements
     \RFb,
-    \Fcc, '.'
+    \Fcc,
 \GFddZz
 {
 };',
@@ -271,7 +454,7 @@ $a = new class implements
             '<?php
 $a = new class implements
     \RFb,
-    \Fcc, '.'
+    \Fcc,
 \GFddZz
 {
 }?>',
@@ -284,4 +467,267 @@ $a = new class implements
             ),
         );
     }
+
+    protected function getFixerConfiguration()
+    {
+        return self::$defaultTestConfig;
+    }
+
+    private function provideClassyCases($classy)
+    {
+        return array(
+            array(
+                sprintf("<?php %s A\n{}", $classy),
+                sprintf('<?php %s    A   {}', $classy),
+            ),
+            array(
+                sprintf("<?php %s B\n{}", $classy),
+                sprintf('<?php %s    B{}', $classy),
+            ),
+            array(
+                sprintf("<?php %s C\n{}", $classy),
+                sprintf("<?php %s\n\tC{}", $classy),
+            ),
+            array(
+                sprintf("<?php %s D //\n{}", $classy),
+                sprintf("<?php %s    D//\n{}", $classy),
+            ),
+            array(
+                sprintf("<?php %s /**/ E //\n{}", $classy),
+                sprintf("<?php %s/**/E//\n{}", $classy),
+            ),
+            array(
+                sprintf(
+                    "<?php
+%s A
+{}
+
+%s /**/ B //
+/**/\n{}", $classy, $classy
+                ),
+                sprintf(
+                    '<?php
+%s
+   A
+{}
+
+%s/**/B//
+/**/ {}', $classy, $classy
+                ),
+            ),
+            array(
+                sprintf('<?php
+namespace {
+    %s IndentedNameSpacedClass
+{
+    }
+}', $classy
+                ),
+                sprintf('<?php
+namespace {
+    %s IndentedNameSpacedClass    {
+    }
+}', $classy
+                ),
+            ),
+        );
+    }
+
+    private function provideClassyExtendingCases($classy)
+    {
+        return array(
+            array(
+                sprintf("<?php %s AE0 extends B\n{}", $classy),
+                sprintf('<?php %s    AE0    extends B    {}', $classy),
+            ),
+            array(
+                sprintf("<?php %s /**/ AE1 /**/ extends /**/ B /**/\n{}", $classy),
+                sprintf('<?php %s/**/AE1/**/extends/**/B/**/{}', $classy),
+            ),
+            array(
+                sprintf("<?php %s /*%s*/ AE2 extends\nB\n{}", $classy, $classy),
+                sprintf("<?php %s /*%s*/ AE2 extends\nB{}", $classy, $classy),
+            ),
+            array(
+                sprintf('<?php
+%s Test124 extends
+\Exception
+{}', $classy),
+                sprintf('<?php
+%s
+Test124
+
+extends
+\Exception {}', $classy),
+            ),
+        );
+    }
+
+    private function provideClassyImplementsCases()
+    {
+        return array(
+            array(
+                "<?php class E implements B\n{}",
+                "<?php class    E   \nimplements     B       \t{}",
+            ),
+            array(
+                "<?php abstract class F extends B implements C\n{}",
+                '<?php abstract    class    F    extends     B    implements C {}',
+            ),
+            array(
+                "<?php abstract class G extends       //
+B /*  */ implements C\n{}",
+                '<?php abstract    class     G     extends       //
+B/*  */implements C{}',
+            ),
+            array(
+                '<?php
+class Aaa IMPLEMENTS
+    \RFb,
+    \Fcc,
+\GFddZz
+{
+}',
+                '<?php
+class Aaa IMPLEMENTS
+    \RFb,
+    \Fcc, \GFddZz
+{
+}',
+            ),
+            array(
+                '<?php
+class        //
+X            //
+extends      //
+Y            //
+implements   //
+Z,       //
+U            //
+{}           //',
+                '<?php
+class        //
+X            //
+extends      //
+Y            //
+implements   //
+Z    ,       //
+U            //
+{}           //',
+            ),
+            array(
+                '<?php
+class Aaa implements
+    PhpCsFixer\Tests\Fixer,
+\RFb,
+    \Fcc1,
+\GFdd
+{
+}',
+                '<?php
+class Aaa implements
+    PhpCsFixer\Tests\Fixer,\RFb,
+    \Fcc1, \GFdd
+{
+}',
+            ),
+            array(
+                '<?php
+class /**/ Test123 EXtends  /**/ \RuntimeException implements
+TestZ
+{
+}',
+                '<?php
+class/**/Test123
+EXtends  /**/        \RuntimeException    implements
+TestZ
+{
+}',
+            ),
+            array(
+                '<?php
+    class Aaa implements Ebb, \Ccc
+    {
+    }',
+                '<?php
+    class Aaa    implements    Ebb,    \Ccc
+    {
+    }',
+            ),
+            array(
+                '<?php
+class X2 IMPLEMENTS
+Z, //
+U,
+D
+{
+}',
+                '<?php
+class X2 IMPLEMENTS
+Z    , //
+U, D
+{
+}',
+            ),
+            array(
+                '<?php
+                    class VeryLongClassNameWithLotsOfLetters extends AnotherVeryLongClassName implements
+    VeryLongInterfaceNameThatIDontWantOnTheSameLine
+{
+}',
+                '<?php
+                    class      VeryLongClassNameWithLotsOfLetters    extends AnotherVeryLongClassName implements
+    VeryLongInterfaceNameThatIDontWantOnTheSameLine
+{
+}',
+            ),
+            array(
+                '<?php
+class /**/ Test125 //aaa
+extends  /*
+
+*/
+//
+\Exception        //
+{}',
+                '<?php
+class/**/Test125 //aaa
+extends  /*
+
+*/
+//
+\Exception        //
+{}',
+            ),
+            array(
+                '<?php
+class Test extends TestInterface8 implements      /*a*/      /*b*/
+TestInterface1,  /* test */
+    TestInterface2,   // test
+    '.'
+
+// test
+TestInterface3, /**/
+TestInterface4,
+      TestInterface5,    '.'
+        /**/TestInterface6c
+{
+}',
+                '<?php
+class Test
+extends
+    TestInterface8
+  implements      /*a*/      /*b*/TestInterface1   ,  /* test */
+    TestInterface2   ,   // test
+    '.'
+
+// test
+TestInterface3, /**/     TestInterface4   ,
+      TestInterface5    ,    '.'
+        /**/TestInterface6c
+{
+}',
+            ),
+        );
+    }
 }

+ 0 - 1
tests/FixerFactoryTest.php

@@ -190,7 +190,6 @@ final class FixerFactoryTest extends \PHPUnit_Framework_TestCase
         }
 
         $cases = array(
-            array($fixers['class_definition'], $fixers['no_trailing_whitespace']), // tested also in: class_definition,no_trailing_whitespace.test
             array($fixers['concat_without_spaces'], $fixers['concat_with_spaces']),
             array($fixers['elseif'], $fixers['braces']),
             array($fixers['method_separation'], $fixers['braces']),

+ 26 - 0
tests/Fixtures/Integration/misc/class_definition,no_trailing_whitespace.test

@@ -0,0 +1,26 @@
+--TEST--
+Integration of fixers: class_definition, no_trailing_whitespace
+--RULESET--
+{
+    "class_definition": true,
+    "no_trailing_whitespace": true
+}
+--SETTINGS--
+{"checkPriority": false}
+--EXPECT--
+<?php
+class Aaa implements
+    Symfony\CS\Tests\Fixer,
+\RFb,
+    \Fcc1,
+\GFdd
+{
+}
+
+--INPUT--
+<?php
+class Aaa implements
+    Symfony\CS\Tests\Fixer,\RFb,
+    \Fcc1, \GFdd
+{
+}

+ 0 - 21
tests/Fixtures/Integration/priority/class_definition,no_trailing_whitespace.test

@@ -1,21 +0,0 @@
---TEST--
-Integration of fixers: class_definition, no_trailing_whitespace.
---RULESET--
-{"class_definition": true, "no_trailing_whitespace": true}
---EXPECT--
-<?php
-class Aaa implements
-    PhpCsFixer\Tests\Fixer,
-\RFb,
-    \Fcc1,
-\GFdd
-{
-}
-
---INPUT--
-<?php
-class Aaa implements
-    PhpCsFixer\Tests\Fixer,\RFb,
-    \Fcc1, \GFdd
-{
-}