Browse Source

Merge branch '1.12'

Conflicts:
	README.rst
Dariusz Ruminski 8 years ago
parent
commit
72038e1815

+ 4 - 0
README.rst

@@ -219,6 +219,10 @@ Choose from the list of available fixers:
                          interfaces definition should
                          be one space.
 
+* **class_keyword_remove**
+                         Converts ::class keywords to
+                         FQCN strings.
+
 * **combine_consecutive_unsets**
                          Calling unset on multiple
                          items should be done in one

+ 208 - 0
src/Fixer/LanguageConstruct/ClassKeywordRemoveFixer.php

@@ -0,0 +1,208 @@
+<?php
+
+/*
+ * This file is part of PHP CS Fixer.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *     Dariusz Rumiński <dariusz.ruminski@gmail.com>
+ *
+ * This source file is subject to the MIT license that is bundled
+ * with this source code in the file LICENSE.
+ */
+
+namespace PhpCsFixer\Fixer\LanguageConstruct;
+
+use PhpCsFixer\AbstractFixer;
+use PhpCsFixer\Tokenizer\Token;
+use PhpCsFixer\Tokenizer\Tokens;
+use PhpCsFixer\Tokenizer\TokensAnalyzer;
+
+/**
+ * @author Sullivan Senechal <soullivaneuh@gmail.com>
+ */
+final class ClassKeywordRemoveFixer extends AbstractFixer
+{
+    /**
+     * {@inheritdoc}
+     */
+    public function isCandidate(Tokens $tokens)
+    {
+        return $tokens->isTokenKindFound(CT_CLASS_CONSTANT);
+    }
+
+    /**
+     * @var string[]
+     */
+    private $imports = array();
+
+    /**
+     * {@inheritdoc}
+     */
+    public function fix(\SplFileInfo $file, Tokens $tokens)
+    {
+        $this->replaceClassKeywords($tokens);
+    }
+
+    /**
+     * Replaces ::class keyword, namespace by namespace.
+     *
+     * It uses recursive method to get rid of token index changes.
+     *
+     * @param Tokens $tokens
+     * @param int    $namespaceNumber
+     */
+    private function replaceClassKeywords(Tokens $tokens, $namespaceNumber = -1)
+    {
+        $namespaceIndexes = array_keys($tokens->findGivenKind(T_NAMESPACE));
+
+        // Namespace blocks
+        if (!empty($namespaceIndexes) && isset($namespaceIndexes[$namespaceNumber])) {
+            $startIndex = $namespaceIndexes[$namespaceNumber];
+
+            $namespaceBlockStartIndex = $tokens->getNextTokenOfKind($startIndex, array(';', '{'));
+            $endIndex = $tokens[$namespaceBlockStartIndex]->equals('{')
+                ? $tokens->findBlockEnd(Tokens::BLOCK_TYPE_CURLY_BRACE, $namespaceBlockStartIndex)
+                : $tokens->getNextTokenOfKind($namespaceBlockStartIndex, array(T_NAMESPACE));
+            $endIndex = $endIndex ?: $tokens->count() - 1;
+        } elseif (-1 === $namespaceNumber) { // Out of any namespace block
+            $startIndex = 0;
+            $endIndex = !empty($namespaceIndexes) ? $namespaceIndexes[0] : $tokens->count() - 1;
+        } else {
+            return;
+        }
+
+        $this->storeImports($tokens, $startIndex, $endIndex);
+        $tokens->rewind();
+        $this->replaceClassKeywordsSection($tokens, $startIndex, $endIndex);
+        $this->replaceClassKeywords($tokens, $namespaceNumber + 1);
+    }
+
+    /**
+     * @param Tokens $tokens
+     */
+    private function storeImports(Tokens $tokens, $startIndex, $endIndex)
+    {
+        $tokensAnalyzer = new TokensAnalyzer($tokens);
+        $this->imports = array();
+
+        foreach ($tokensAnalyzer->getImportUseIndexes() as $index) {
+            if ($index < $startIndex || $index > $endIndex) {
+                continue;
+            }
+
+            $import = '';
+            while (($index = $tokens->getNextMeaningfulToken($index))) {
+                if ($tokens[$index]->equalsAny(array(';', '{')) || $tokens[$index]->isGivenKind(T_AS)) {
+                    break;
+                }
+
+                $import .= $tokens[$index]->getContent();
+            }
+
+            // Imports group (PHP 7 spec)
+            if ($tokens[$index]->equals('{')) {
+                $groupEndIndex = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_CURLY_BRACE, $index);
+                $groupImports = array_map(
+                    'trim',
+                    explode(',', $tokens->generatePartialCode($index + 1, $groupEndIndex - 1))
+                );
+                foreach ($groupImports as $groupImport) {
+                    $groupImportParts = array_map('trim', explode(' as ', $groupImport));
+                    if (2 === count($groupImportParts)) {
+                        $this->imports[$groupImportParts[1]] = $import.$groupImportParts[0];
+                    } else {
+                        $this->imports[] = $import.$groupImport;
+                    }
+                }
+            } elseif ($tokens[$index]->isGivenKind(T_AS)) {
+                $aliasIndex = $tokens->getNextMeaningfulToken($index);
+                $alias = $tokens[$aliasIndex]->getContent();
+                $this->imports[$alias] = $import;
+            } else {
+                $this->imports[] = $import;
+            }
+        }
+    }
+
+    /**
+     * @param Tokens $tokens
+     */
+    private function replaceClassKeywordsSection(Tokens $tokens, $startIndex, $endIndex)
+    {
+        $CTClassTokens = $tokens->findGivenKind(CT_CLASS_CONSTANT, $startIndex, $endIndex);
+        if (!empty($CTClassTokens)) {
+            $this->replaceClassKeyword($tokens, current(array_keys($CTClassTokens)));
+            $this->replaceClassKeywordsSection($tokens, $startIndex, $endIndex);
+        }
+    }
+
+    /**
+     * @param Tokens $tokens
+     * @param int    $classIndex
+     */
+    private function replaceClassKeyword(Tokens $tokens, $classIndex)
+    {
+        $classEndIndex = $classIndex - 2;
+        $classBeginIndex = $classEndIndex;
+        while ($tokens[--$classBeginIndex]->isGivenKind(array(T_NS_SEPARATOR, T_STRING)));
+        ++$classBeginIndex;
+        $classString = $tokens->generatePartialCode($classBeginIndex, $classEndIndex);
+
+        $classImport = false;
+        foreach ($this->imports as $alias => $import) {
+            if ($classString === $alias) {
+                $classImport = $import;
+                break;
+            }
+
+            $classStringArray = explode('\\', $classString);
+            $namespaceToTest = $classStringArray[0];
+
+            if (0 === strcmp($namespaceToTest, substr($import, -strlen($namespaceToTest)))) {
+                $classImport = $import;
+                break;
+            }
+        }
+
+        $tokens->clearRange($classBeginIndex, $classIndex);
+        $tokens->insertAt($classBeginIndex, new Token(array(
+            T_CONSTANT_ENCAPSED_STRING,
+            "'".$this->makeClassFQN($classImport, $classString)."'",
+        )));
+    }
+
+    /**
+     * @param string|false $classImport
+     * @param string       $classString
+     *
+     * @return string
+     */
+    private function makeClassFQN($classImport, $classString)
+    {
+        if (false === $classImport) {
+            return $classString;
+        }
+
+        $classStringArray = explode('\\', $classString);
+        $classStringLength = count($classStringArray);
+        $classImportArray = explode('\\', $classImport);
+        $classImportLength = count($classImportArray);
+
+        if (1 === $classStringLength) {
+            return $classImport;
+        }
+
+        return implode('\\', array_merge(
+            array_slice($classImportArray, 0, $classImportLength - $classStringLength + 1),
+            $classStringArray
+        ));
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function getDescription()
+    {
+        return 'Converts ::class keywords to FQCN strings.';
+    }
+}

+ 207 - 0
tests/Fixer/LanguageConstruct/ClassKeywordRemoveFixerTest.php

@@ -0,0 +1,207 @@
+<?php
+
+/*
+ * This file is part of PHP CS Fixer.
+ *
+ * (c) Fabien Potencier <fabien@symfony.com>
+ *     Dariusz Rumiński <dariusz.ruminski@gmail.com>
+ *
+ * This source file is subject to the MIT license that is bundled
+ * with this source code in the file LICENSE.
+ */
+
+namespace PhpCsFixer\Tests\Fixer\LanguageConstruct;
+
+use PhpCsFixer\Test\AbstractFixerTestCase;
+
+/**
+ * @author Sullivan Senechal <soullivaneuh@gmail.com>
+ *
+ * @internal
+ */
+final class ClassKeywordRemoveFixerTest extends AbstractFixerTestCase
+{
+    /**
+     * @dataProvider provideFixCases
+     */
+    public function testFix($expected, $input = null)
+    {
+        $this->doTest($expected, $input);
+    }
+
+    public function provideFixCases()
+    {
+        return array(
+            array(
+                "<?php
+                use Foo\Bar\Thing;
+
+                echo 'Foo\Bar\Thing';
+                ",
+                "<?php
+                use Foo\Bar\Thing;
+
+                echo Thing::class;
+                ",
+            ),
+            array(
+                "<?php
+                use Foo\Bar;
+            "."
+                echo 'Foo\Bar\Thing';
+                ",
+                "<?php
+                use Foo\Bar;
+            "."
+                echo Bar\Thing::class;
+                ",
+            ),
+            array(
+                "<?php
+                use Foo\Bar\Thing as Alias;
+
+                echo 'Foo\Bar\Thing';
+                ",
+                "<?php
+                use Foo\Bar\Thing as Alias;
+
+                echo Alias::class;
+                ",
+            ),
+            array(
+                "<?php
+                use Foo\Bar\Dummy;
+                use Foo\Bar\Thing as Alias;
+
+                echo 'Foo\Bar\Dummy';
+                echo 'Foo\Bar\Thing';
+                ",
+                "<?php
+                use Foo\Bar\Dummy;
+                use Foo\Bar\Thing as Alias;
+
+                echo Dummy::class;
+                echo Alias::class;
+                ",
+            ),
+            array(
+                "<?php
+                echo '\DateTime';
+                ",
+                "<?php
+                echo \DateTime::class;
+                ",
+            ),
+            array(
+                "<?php
+                echo 'Thing';
+                ",
+                '<?php
+                echo Thing::class;
+                ',
+            ),
+            array(
+                "<?php
+                class Foo {
+                    public function amazingFunction() {
+                        echo 'Thing';
+                    }
+                }
+                ",
+                '<?php
+                class Foo {
+                    public function amazingFunction() {
+                        echo Thing::class;
+                    }
+                }
+                ',
+            ),
+            array(
+                "<?php
+                namespace A\B;
+
+                use Foo\Bar;
+
+                echo 'Foo\Bar';
+                ",
+                '<?php
+                namespace A\B;
+
+                use Foo\Bar;
+
+                echo Bar::class;
+                ',
+            ),
+            array(
+                "<?php
+
+                namespace A\B {
+
+                    class D {
+
+                    }
+                }
+
+                namespace B\B {
+                    class D {
+
+                    }
+                }
+
+                namespace C {
+                    use A\B\D;
+                    var_dump('A\B\D');
+                }
+
+                namespace C1 {
+                    use B\B\D;
+                    var_dump('B\B\D');
+                }
+                ",
+                '<?php
+
+                namespace A\B {
+
+                    class D {
+
+                    }
+                }
+
+                namespace B\B {
+                    class D {
+
+                    }
+                }
+
+                namespace C {
+                    use A\B\D;
+                    var_dump(D::class);
+                }
+
+                namespace C1 {
+                    use B\B\D;
+                    var_dump(D::class);
+                }
+                ',
+            ),
+            array(
+                "<?php
+                use Foo\\Bar\{ClassA, ClassB, ClassC as C};
+                use function Foo\\Bar\{fn_a, fn_b, fn_c};
+                use const Foo\\Bar\{ConstA, ConstB, ConstC};
+
+                echo 'Foo\\Bar\ClassB';
+                echo 'Foo\\Bar\ClassC';
+                ",
+                '<?php
+                use Foo\Bar\{ClassA, ClassB, ClassC as C};
+                use function Foo\Bar\{fn_a, fn_b, fn_c};
+                use const Foo\Bar\{ConstA, ConstB, ConstC};
+
+                echo ClassB::class;
+                echo C::class;
+                ',
+            ),
+        );
+    }
+}

+ 1 - 0
tests/FixerFactoryTest.php

@@ -275,6 +275,7 @@ final class FixerFactoryTest extends \PHPUnit_Framework_TestCase
             array($fixers['declare_strict_types'], $fixers['single_blank_line_before_namespace']), // tested also in: declare_strict_types,single_blank_line_before_namespace.test
             array($fixers['short_array_syntax'], $fixers['unalign_equals']), // tested also in: short_array_syntax,unalign_equals.test
             array($fixers['short_array_syntax'], $fixers['ternary_operator_spaces']), // tested also in: short_array_syntax,ternary_operator_spaces.test
+            array($fixers['class_keyword_remove'], $fixers['no_unused_imports']), // tested also in: class_keyword_remove,no_unused_imports.test
         );
 
         // prepare bulk tests for phpdoc fixers to test that:

+ 14 - 0
tests/Fixtures/Integration/priority/class_keyword_remove,no_unused_imports.test

@@ -0,0 +1,14 @@
+--TEST--
+Integration of fixers: class_keyword_remove,no_unused_imports.
+--CONFIG--
+{"class_keyword_remove": true, "no_unused_imports": true}
+--EXPECT--
+<?php
+
+echo 'Foo\Bar\Thing';
+
+--INPUT--
+<?php
+use Foo\Bar\Thing;
+
+echo Thing::class;