Browse Source

Merge branch 'master' into 3.0

* master:
  GroupImportFixer - introduction
  DoctrineAnnotationSpacesFixer - fix for typed properties
  revert some unneeded exclusions
  Do not allow assignments in if statements
  Symfony's finder already ignores vcs and dot files by default
  ClassKeywordRemoveFixer - fix for fully qualified
  LambdaNotUsedImportFixer - add heredoc test

# Conflicts:
#	.travis.yml
#	README.rst
#	composer.json
SpacePossum 4 years ago
parent
commit
b50efd9260

+ 4 - 0
README.rst

@@ -790,6 +790,10 @@ Choose from the list of available rules:
   - ``import_functions`` (``false``, ``null``, ``true``): whether to import, not import or
     ignore global functions; defaults to ``null``
 
+* **group_import**
+
+  There MUST be group use for the same namespaces.
+
 * **header_comment**
 
   Add, replace or remove header comment.

+ 2 - 0
phpmd.xml

@@ -18,6 +18,8 @@
 
     <rule ref="rulesets/naming.xml/ConstantNamingConventions" />
 
+    <rule ref="rulesets/cleancode.xml/IfStatementAssignment" />
+
     <rule ref="../../../../../mi-schi/phpmd-extension/rulesets/cleancode.xml/DataStructureMethods" />
     <rule ref="../../../../../mi-schi/phpmd-extension/rulesets/cleancode.xml/SwitchStatement" />
 

+ 2 - 1
src/AbstractDoctrineAnnotationFixer.php

@@ -16,6 +16,7 @@ use PhpCsFixer\Doctrine\Annotation\Tokens as DoctrineAnnotationTokens;
 use PhpCsFixer\Fixer\ConfigurableFixerInterface;
 use PhpCsFixer\FixerConfiguration\FixerConfigurationResolver;
 use PhpCsFixer\FixerConfiguration\FixerOptionBuilder;
+use PhpCsFixer\Tokenizer\CT;
 use PhpCsFixer\Tokenizer\Token as PhpToken;
 use PhpCsFixer\Tokenizer\Tokens as PhpTokens;
 use PhpCsFixer\Tokenizer\TokensAnalyzer;
@@ -212,7 +213,7 @@ abstract class AbstractDoctrineAnnotationFixer extends AbstractFixer implements
             return true;
         }
 
-        while ($tokens[$index]->isGivenKind([T_PUBLIC, T_PROTECTED, T_PRIVATE, T_FINAL, T_ABSTRACT])) {
+        while ($tokens[$index]->isGivenKind([T_PUBLIC, T_PROTECTED, T_PRIVATE, T_FINAL, T_ABSTRACT, T_NS_SEPARATOR, T_STRING, CT::T_NULLABLE_TYPE])) {
             $index = $tokens->getNextMeaningfulToken($index);
         }
 

+ 4 - 3
src/Console/ConfigurationResolver.php

@@ -578,10 +578,11 @@ final class ConfigurationResolver
             $configDir = $this->cwd;
         } elseif (1 < \count($path)) {
             throw new InvalidConfigurationException('For multiple paths config parameter is required.');
-        } elseif (is_file($path[0]) && $dirName = pathinfo($path[0], PATHINFO_DIRNAME)) {
-            $configDir = $dirName;
-        } else {
+        } elseif (!is_file($path[0])) {
             $configDir = $path[0];
+        } else {
+            $dirName = pathinfo($path[0], PATHINFO_DIRNAME);
+            $configDir = $dirName ?: $path[0];
         }
 
         $candidates = [

+ 0 - 2
src/Finder.php

@@ -27,8 +27,6 @@ class Finder extends BaseFinder
         $this
             ->files()
             ->name('*.php')
-            ->ignoreDotFiles(true)
-            ->ignoreVCS(true)
             ->exclude('vendor')
         ;
     }

+ 3 - 1
src/Fixer/Alias/PowToExponentiationFixer.php

@@ -184,7 +184,9 @@ final class PowToExponentiationFixer extends AbstractFunctionReferenceFixer
                 continue;
             }
 
-            if (null !== $blockType = Tokens::detectBlockType($tokens[$i])) {
+            $blockType = Tokens::detectBlockType($tokens[$i]);
+
+            if (null !== $blockType) {
                 $i = $tokens->findBlockEnd($blockType['type'], $i);
 
                 continue;

+ 271 - 0
src/Fixer/Import/GroupImportFixer.php

@@ -0,0 +1,271 @@
+<?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\Import;
+
+use PhpCsFixer\AbstractFixer;
+use PhpCsFixer\FixerDefinition\FixerDefinition;
+use PhpCsFixer\FixerDefinition\VersionSpecification;
+use PhpCsFixer\FixerDefinition\VersionSpecificCodeSample;
+use PhpCsFixer\Tokenizer\Analyzer\Analysis\NamespaceUseAnalysis;
+use PhpCsFixer\Tokenizer\Analyzer\NamespaceUsesAnalyzer;
+use PhpCsFixer\Tokenizer\CT;
+use PhpCsFixer\Tokenizer\Token;
+use PhpCsFixer\Tokenizer\Tokens;
+
+/**
+ * @author Volodymyr Kupriienko <vldmr.kuprienko@gmail.com>
+ */
+final class GroupImportFixer extends AbstractFixer
+{
+    /**
+     * {@inheritdoc}
+     */
+    public function getDefinition()
+    {
+        return new FixerDefinition(
+            'There MUST be group use for the same namespaces.',
+            [
+                new VersionSpecificCodeSample(
+                    "<?php\nuse Foo\\Bar;\nuse Foo\\Baz;\n",
+                    new VersionSpecification(70000)
+                ),
+            ]
+        );
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function isCandidate(Tokens $tokens)
+    {
+        return \PHP_VERSION_ID >= 70000 && $tokens->isTokenKindFound(T_USE);
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    protected function applyFix(\SplFileInfo $file, Tokens $tokens)
+    {
+        $useWithSameNamespaces = $this->getSameNamespaces($tokens);
+
+        if ([] === $useWithSameNamespaces) {
+            return;
+        }
+
+        $this->removeSingleUseStatements($useWithSameNamespaces, $tokens);
+        $this->addGroupUseStatements($useWithSameNamespaces, $tokens);
+    }
+
+    /**
+     * Gets namespace use analyzers with same namespaces.
+     *
+     * @return NamespaceUseAnalysis[]
+     */
+    private function getSameNamespaces(Tokens $tokens)
+    {
+        $useDeclarations = (new NamespaceUsesAnalyzer())->getDeclarationsFromTokens($tokens);
+
+        if (0 === \count($useDeclarations)) {
+            return [];
+        }
+
+        $allNamespaces = array_map(
+            function (NamespaceUseAnalysis $useDeclaration) {
+                return $this->getNamespaceNameWithSlash($useDeclaration);
+            },
+            $useDeclarations
+        );
+
+        $sameNamespaces = array_filter(array_count_values($allNamespaces), function ($count) {
+            return $count > 1;
+        });
+        $sameNamespaces = array_keys($sameNamespaces);
+
+        $sameNamespaceAnalysis = array_filter($useDeclarations, function ($useDeclaration) use ($sameNamespaces) {
+            $namespaceName = $this->getNamespaceNameWithSlash($useDeclaration);
+
+            return \in_array($namespaceName, $sameNamespaces, true);
+        });
+
+        sort($sameNamespaceAnalysis);
+
+        return $sameNamespaceAnalysis;
+    }
+
+    /**
+     * @param NamespaceUseAnalysis[] $statements
+     */
+    private function removeSingleUseStatements(array $statements, Tokens $tokens)
+    {
+        foreach ($statements as $useDeclaration) {
+            $index = $useDeclaration->getStartIndex();
+            $endIndex = $useDeclaration->getEndIndex();
+
+            $useStatementTokens = [T_USE, T_WHITESPACE, T_STRING, T_NS_SEPARATOR, T_AS, CT::T_CONST_IMPORT, CT::T_FUNCTION_IMPORT];
+
+            while ($index !== $endIndex) {
+                if ($tokens[$index]->isGivenKind($useStatementTokens)) {
+                    $tokens->clearAt($index);
+                }
+
+                ++$index;
+            }
+
+            if (isset($tokens[$index]) && $tokens[$index]->equals(';')) {
+                $tokens->clearAt($index);
+            }
+
+            ++$index;
+
+            if (isset($tokens[$index]) && $tokens[$index]->isGivenKind(T_WHITESPACE)) {
+                $tokens->clearAt($index);
+            }
+        }
+    }
+
+    /**
+     * @param NamespaceUseAnalysis[] $statements
+     */
+    private function addGroupUseStatements(array $statements, Tokens $tokens)
+    {
+        $currentNamespace = '';
+        $insertIndex = \array_slice($statements, -1)[0]->getEndIndex();
+
+        while ($tokens[$insertIndex]->isGivenKind([T_COMMENT, T_DOC_COMMENT])) {
+            ++$insertIndex;
+        }
+
+        foreach ($statements as $index => $useDeclaration) {
+            $namespace = $this->getNamespaceNameWithSlash($useDeclaration);
+
+            if ($currentNamespace !== $namespace) {
+                if ($index > 1) {
+                    ++$insertIndex;
+                }
+
+                $currentNamespace = $namespace;
+                $insertIndex += $this->createNewGroup($tokens, $insertIndex, $useDeclaration, $currentNamespace);
+            } else {
+                $newTokens = [
+                    new Token(','),
+                    new Token([T_WHITESPACE, ' ']),
+                ];
+
+                if ($useDeclaration->isAliased()) {
+                    $tokens->insertAt($insertIndex + 1, $newTokens);
+                    $insertIndex += \count($newTokens);
+                    $newTokens = [];
+
+                    $insertIndex += $this->insertToGroupUseWithAlias($tokens, $insertIndex + 1, $useDeclaration);
+                }
+
+                $newTokens[] = new Token([T_STRING, $useDeclaration->getShortName()]);
+
+                if (!isset($statements[$index + 1]) || $this->getNamespaceNameWithSlash($statements[$index + 1]) !== $currentNamespace) {
+                    $newTokens[] = new Token([CT::T_GROUP_IMPORT_BRACE_CLOSE, '}']);
+                    $newTokens[] = new Token(';');
+                    $newTokens[] = new Token([T_WHITESPACE, "\n"]);
+                }
+
+                $tokens->insertAt($insertIndex + 1, $newTokens);
+                $insertIndex += \count($newTokens);
+            }
+        }
+    }
+
+    /**
+     * @return string
+     */
+    private function getNamespaceNameWithSlash(NamespaceUseAnalysis $useDeclaration)
+    {
+        return substr($useDeclaration->getFullName(), 0, strripos($useDeclaration->getFullName(), '\\') + 1);
+    }
+
+    /**
+     * Insert use with alias to the group.
+     *
+     * @param int $insertIndex
+     *
+     * @return int
+     */
+    private function insertToGroupUseWithAlias(Tokens $tokens, $insertIndex, NamespaceUseAnalysis $useDeclaration)
+    {
+        $newTokens = [
+            new Token([T_STRING, substr($useDeclaration->getFullName(), strripos($useDeclaration->getFullName(), '\\') + 1)]),
+            new Token([T_WHITESPACE, ' ']),
+            new Token([T_AS, 'as']),
+            new Token([T_WHITESPACE, ' ']),
+        ];
+
+        $tokens->insertAt($insertIndex, $newTokens);
+
+        return \count($newTokens);
+    }
+
+    /**
+     * Creates new use statement group.
+     *
+     * @param int    $insertIndex
+     * @param string $currentNamespace
+     *
+     * @return int
+     */
+    private function createNewGroup(Tokens $tokens, $insertIndex, NamespaceUseAnalysis $useDeclaration, $currentNamespace)
+    {
+        $insertedTokens = 0;
+
+        if (\count($tokens) === $insertIndex) {
+            $tokens->setSize($insertIndex + 1);
+        }
+
+        $newTokens = [
+            new Token([T_USE, 'use']),
+            new Token([T_WHITESPACE, ' ']),
+        ];
+
+        if ($useDeclaration->isFunction() || $useDeclaration->isConstant()) {
+            $importStatementParams = $useDeclaration->isFunction()
+                ? [CT::T_FUNCTION_IMPORT, 'function']
+                : [CT::T_CONST_IMPORT, 'const'];
+
+            $newTokens[] = new Token($importStatementParams);
+            $newTokens[] = new Token([T_WHITESPACE, ' ']);
+        }
+
+        $namespaceParts = array_filter(explode('\\', $currentNamespace));
+
+        foreach ($namespaceParts as $part) {
+            $newTokens[] = new Token([T_STRING, $part]);
+            $newTokens[] = new Token([T_NS_SEPARATOR, '\\']);
+        }
+
+        $newTokens[] = new Token([CT::T_GROUP_IMPORT_BRACE_OPEN, '{']);
+
+        $newTokensCount = \count($newTokens);
+        $tokens->insertAt($insertIndex, $newTokens);
+        $insertedTokens += $newTokensCount;
+
+        $insertIndex += $newTokensCount;
+
+        if ($useDeclaration->isAliased()) {
+            $inserted = $this->insertToGroupUseWithAlias($tokens, $insertIndex + 1, $useDeclaration);
+            $insertedTokens += $inserted;
+            $insertIndex += $inserted;
+        }
+
+        $tokens->insertAt($insertIndex + 1, new Token([T_STRING, $useDeclaration->getShortName()]));
+        ++$insertedTokens;
+
+        return $insertedTokens;
+    }
+}

+ 20 - 16
src/Fixer/LanguageConstruct/ClassKeywordRemoveFixer.php

@@ -159,10 +159,10 @@ $className = Baz::class;
     }
 
     /**
-     * @param string $namespace
+     * @param string $namespacePrefix
      * @param int    $classIndex
      */
-    private function replaceClassKeyword(Tokens $tokens, $namespace, $classIndex)
+    private function replaceClassKeyword(Tokens $tokens, $namespacePrefix, $classIndex)
     {
         $classEndIndex = $tokens->getPrevMeaningfulToken($classIndex);
         $classEndIndex = $tokens->getPrevMeaningfulToken($classEndIndex);
@@ -189,20 +189,24 @@ $className = Baz::class;
         );
 
         $classImport = false;
-        foreach ($this->imports as $alias => $import) {
-            if ($classString === $alias) {
-                $classImport = $import;
+        if ($tokens[$classBeginIndex]->isGivenKind(T_NS_SEPARATOR)) {
+            $namespacePrefix = '';
+        } else {
+            foreach ($this->imports as $alias => $import) {
+                if ($classString === $alias) {
+                    $classImport = $import;
 
-                break;
-            }
+                    break;
+                }
 
-            $classStringArray = explode('\\', $classString);
-            $namespaceToTest = $classStringArray[0];
+                $classStringArray = explode('\\', $classString);
+                $namespaceToTest = $classStringArray[0];
 
-            if (0 === strcmp($namespaceToTest, substr($import, -\strlen($namespaceToTest)))) {
-                $classImport = $import;
+                if (0 === strcmp($namespaceToTest, substr($import, -\strlen($namespaceToTest)))) {
+                    $classImport = $import;
 
-                break;
+                    break;
+                }
             }
         }
 
@@ -214,21 +218,21 @@ $className = Baz::class;
 
         $tokens->insertAt($classBeginIndex, new Token([
             T_CONSTANT_ENCAPSED_STRING,
-            "'".$this->makeClassFQN($namespace, $classImport, $classString)."'",
+            "'".$this->makeClassFQN($namespacePrefix, $classImport, $classString)."'",
         ]));
     }
 
     /**
-     * @param string       $namespace
+     * @param string       $namespacePrefix
      * @param false|string $classImport
      * @param string       $classString
      *
      * @return string
      */
-    private function makeClassFQN($namespace, $classImport, $classString)
+    private function makeClassFQN($namespacePrefix, $classImport, $classString)
     {
         if (false === $classImport) {
-            return ('' !== $namespace ? ($namespace.'\\') : '').$classString;
+            return ('' !== $namespacePrefix ? ($namespacePrefix.'\\') : '').$classString;
         }
 
         $classStringArray = explode('\\', $classString);

+ 1 - 0
src/FixerFactory.php

@@ -219,6 +219,7 @@ final class FixerFactory
     {
         static $conflictMap = [
             'no_blank_lines_before_namespace' => ['single_blank_line_before_namespace'],
+            'single_import_per_statement' => ['group_import'],
         ];
 
         $fixerName = $fixer->getName();

+ 38 - 0
tests/Fixer/DoctrineAnnotation/DoctrineAnnotationSpacesFixerTest.php

@@ -1533,4 +1533,42 @@ final class DoctrineAnnotationSpacesFixerTest extends AbstractDoctrineAnnotation
  */'],
         ]);
     }
+
+    /**
+     * @param string $element
+     *
+     * @requires PHP 7.4
+     * @dataProvider provideElementDiscoveringCases
+     */
+    public function testElementDiscovering($element)
+    {
+        $this->doTest(
+            sprintf('<?php
+                class Foo
+                {
+                    /**
+                     * @Foo(foo="foo")
+                     */
+                    %s
+                }
+            ', $element),
+            sprintf('<?php
+                class Foo
+                {
+                    /**
+                     * @Foo(foo = "foo")
+                     */
+                    %s
+                }
+            ', $element)
+        );
+    }
+
+    public static function provideElementDiscoveringCases()
+    {
+        yield ['private $foo;'];
+        yield ['private string $foo;'];
+        yield ['private Foo\Bar $foo;'];
+        yield ['private ?Foo\Bar $foo;'];
+    }
 }

Some files were not shown because too many files changed in this diff