Browse Source

feature #5953 GetClassToClassKeywordFixer - introduction (paulbalandan)

This PR was merged into the master branch.

Discussion
----------

GetClassToClassKeywordFixer - introduction

```
$ php php-cs-fixer describe get_class_to_class_keyword`

Description of get_class_to_class_keyword rule.
Replace `get_class` calls on object variables with class keyword syntax.

Fixer applying this rule is risky.
Risky if the `get_class` function is overridden.

Fixing examples:
 * Example #1.
   ---------- begin diff ----------
   --- Original
   +++ New
   @@ -1,2 +1,2 @@
    <?php
   -get_class($a);
   +$a::class;

   ----------- end diff -----------

 * Example #2.
   ---------- begin diff ----------
   --- Original
   +++ New
   @@ -1,4 +1,4 @@
    <?php

    $date = new \DateTimeImmutable();
   -$class = get_class($date);
   +$class = $date::class;

   ----------- end diff -----------
```

Commits
-------

ee541a6d3 GetClassToClassKeywordFixer - introduction
SpacePossum 3 years ago
parent
commit
00096f0126

+ 9 - 0
doc/list.rst

@@ -799,6 +799,15 @@ List of Available Rules
    Part of rule sets `@PhpCsFixer <./ruleSets/PhpCsFixer.rst>`_ `@Symfony <./ruleSets/Symfony.rst>`_
 
    `Source PhpCsFixer\\Fixer\\Phpdoc\\GeneralPhpdocTagRenameFixer <./../src/Fixer/Phpdoc/GeneralPhpdocTagRenameFixer.php>`_
+-  `get_class_to_class_keyword <./rules/language_construct/get_class_to_class_keyword.rst>`_
+
+   Replace ``get_class`` calls on object variables with class keyword syntax.
+
+   *warning risky* Risky if the ``get_class`` function is overridden.
+
+   Part of rule set `@PHP80Migration:risky <./ruleSets/PHP80MigrationRisky.rst>`_
+
+   `Source PhpCsFixer\\Fixer\\LanguageConstruct\\GetClassToClassKeywordFixer <./../src/Fixer/LanguageConstruct/GetClassToClassKeywordFixer.php>`_
 -  `global_namespace_import <./rules/import/global_namespace_import.rst>`_
 
    Imports or fully qualifies global classes/functions/constants.

+ 1 - 0
doc/ruleSets/PHP80MigrationRisky.rst

@@ -8,6 +8,7 @@ Rules
 -----
 
 - `@PHP74Migration:risky <./PHP74MigrationRisky.rst>`_
+- `get_class_to_class_keyword <./../rules/language_construct/get_class_to_class_keyword.rst>`_
 - `modernize_strpos <./../rules/alias/modernize_strpos.rst>`_
 - `no_alias_functions <./../rules/alias/no_alias_functions.rst>`_
   config:

+ 3 - 0
doc/rules/index.rst

@@ -429,6 +429,9 @@ Language Construct
 - `function_to_constant <./language_construct/function_to_constant.rst>`_ *(risky)*
 
   Replace core functions calls returning constants with the constants.
+- `get_class_to_class_keyword <./language_construct/get_class_to_class_keyword.rst>`_ *(risky)*
+
+  Replace ``get_class`` calls on object variables with class keyword syntax.
 - `is_null <./language_construct/is_null.rst>`_ *(risky)*
 
   Replaces ``is_null($var)`` expression with ``null === $var``.

+ 44 - 0
doc/rules/language_construct/get_class_to_class_keyword.rst

@@ -0,0 +1,44 @@
+===================================
+Rule ``get_class_to_class_keyword``
+===================================
+
+Replace ``get_class`` calls on object variables with class keyword syntax.
+
+.. warning:: Using this rule is risky.
+
+   Risky if the ``get_class`` function is overridden.
+
+Examples
+--------
+
+Example #1
+~~~~~~~~~~
+
+.. code-block:: diff
+
+   --- Original
+   +++ New
+    <?php
+   -get_class($a);
+   +$a::class;
+
+Example #2
+~~~~~~~~~~
+
+.. code-block:: diff
+
+   --- Original
+   +++ New
+    <?php
+
+    $date = new \DateTimeImmutable();
+   -$class = get_class($date);
+   +$class = $date::class;
+
+Rule sets
+---------
+
+The rule is part of the following rule set:
+
+@PHP80Migration:risky
+  Using the `@PHP80Migration:risky <./../../ruleSets/PHP80MigrationRisky.rst>`_ rule set will enable the ``get_class_to_class_keyword`` rule.

+ 1 - 1
src/Fixer/FunctionNotation/NoSpacesAfterFunctionNameFixer.php

@@ -43,7 +43,7 @@ final class NoSpacesAfterFunctionNameFixer extends AbstractFixer
     /**
      * {@inheritdoc}
      *
-     * Must run before FunctionToConstantFixer.
+     * Must run before FunctionToConstantFixer, GetClassToClassKeywordFixer.
      * Must run after PowToExponentiationFixer.
      */
     public function getPriority(): int

+ 167 - 0
src/Fixer/LanguageConstruct/GetClassToClassKeywordFixer.php

@@ -0,0 +1,167 @@
+<?php
+
+declare(strict_types=1);
+
+/*
+ * 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\FixerDefinition\FixerDefinition;
+use PhpCsFixer\FixerDefinition\FixerDefinitionInterface;
+use PhpCsFixer\FixerDefinition\VersionSpecification;
+use PhpCsFixer\FixerDefinition\VersionSpecificCodeSample;
+use PhpCsFixer\Tokenizer\Analyzer\FunctionsAnalyzer;
+use PhpCsFixer\Tokenizer\CT;
+use PhpCsFixer\Tokenizer\Token;
+use PhpCsFixer\Tokenizer\Tokens;
+
+/**
+ * @author John Paul E. Balandan, CPA <paulbalandan@gmail.com>
+ */
+final class GetClassToClassKeywordFixer extends AbstractFixer
+{
+    /**
+     * {@inheritdoc}
+     */
+    public function getDefinition(): FixerDefinitionInterface
+    {
+        return new FixerDefinition(
+            'Replace `get_class` calls on object variables with class keyword syntax.',
+            [
+                new VersionSpecificCodeSample(
+                    "<?php\nget_class(\$a);\n",
+                    new VersionSpecification(80000)
+                ),
+                new VersionSpecificCodeSample(
+                    "<?php\n\n\$date = new \\DateTimeImmutable();\n\$class = get_class(\$date);\n",
+                    new VersionSpecification(80000)
+                ),
+            ],
+            null,
+            'Risky if the `get_class` function is overridden.'
+        );
+    }
+
+    /**
+     * {@inheritdoc}
+     *
+     * Must run before MultilineWhitespaceBeforeSemicolonsFixer.
+     * Must run after NoSpacesAfterFunctionNameFixer, NoSpacesInsideParenthesisFixer.
+     */
+    public function getPriority(): int
+    {
+        return 1;
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function isCandidate(Tokens $tokens): bool
+    {
+        return \PHP_VERSION_ID >= 80000 && $tokens->isAllTokenKindsFound([T_STRING, T_VARIABLE]);
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    public function isRisky(): bool
+    {
+        return true;
+    }
+
+    /**
+     * {@inheritdoc}
+     */
+    protected function applyFix(\SplFileInfo $file, Tokens $tokens): void
+    {
+        $functionsAnalyzer = new FunctionsAnalyzer();
+        $indicesToClear = [];
+        $tokenSlices = [];
+
+        for ($index = $tokens->count() - 1; $index > 0; --$index) {
+            if (!$tokens[$index]->equals([T_STRING, 'get_class'], false)) {
+                continue;
+            }
+
+            if (!$functionsAnalyzer->isGlobalFunctionCall($tokens, $index)) {
+                continue;
+            }
+
+            $braceOpenIndex = $tokens->getNextMeaningfulToken($index);
+            $braceCloseIndex = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_PARENTHESIS_BRACE, $braceOpenIndex);
+
+            if ($braceCloseIndex === $tokens->getNextMeaningfulToken($braceOpenIndex)) {
+                continue; // get_class with no arguments
+            }
+
+            $meaningfulTokensCount = 0;
+            $variableTokensIndices = [];
+
+            for ($i = $braceOpenIndex + 1; $i < $braceCloseIndex; ++$i) {
+                if (!$tokens[$i]->equalsAny([[T_WHITESPACE], [T_COMMENT], [T_DOC_COMMENT], '(', ')'])) {
+                    ++$meaningfulTokensCount;
+                }
+
+                if (!$tokens[$i]->isGivenKind(T_VARIABLE)) {
+                    continue;
+                }
+
+                if ('$this' === strtolower($tokens[$i]->getContent())) {
+                    continue 2; // get_class($this)
+                }
+
+                $variableTokensIndices[] = $i;
+            }
+
+            if ($meaningfulTokensCount > 1 || 1 !== \count($variableTokensIndices)) {
+                continue; // argument contains more logic, or more arguments, or no variable argument
+            }
+
+            $indicesToClear[$index] = [$braceOpenIndex, current($variableTokensIndices), $braceCloseIndex];
+        }
+
+        foreach ($indicesToClear as $index => $items) {
+            $tokenSlices[$index] = $this->getReplacementTokenSlices($tokens, $items[1]);
+            $this->clearGetClassCall($tokens, $index, $items[0], $items[2]);
+        }
+
+        $tokens->insertSlices($tokenSlices);
+    }
+
+    private function getReplacementTokenSlices(Tokens $tokens, int $variableIndex): array
+    {
+        return [
+            new Token([T_VARIABLE, $tokens[$variableIndex]->getContent()]),
+            new Token([T_DOUBLE_COLON, '::']),
+            new Token([CT::T_CLASS_CONSTANT, 'class']),
+        ];
+    }
+
+    private function clearGetClassCall(Tokens $tokens, int $index, int $braceOpenIndex, int $braceCloseIndex): void
+    {
+        for ($i = $braceOpenIndex; $i <= $braceCloseIndex; ++$i) {
+            if ($tokens[$i]->isGivenKind([T_WHITESPACE, T_COMMENT, T_DOC_COMMENT])) {
+                continue;
+            }
+
+            $tokens->clearTokenAndMergeSurroundingWhitespace($i);
+        }
+
+        $prevIndex = $tokens->getPrevMeaningfulToken($index);
+
+        if ($tokens[$prevIndex]->isGivenKind(T_NS_SEPARATOR)) {
+            $tokens->clearAt($prevIndex);
+        }
+
+        $tokens->clearAt($index);
+    }
+}

+ 1 - 1
src/Fixer/Semicolon/MultilineWhitespaceBeforeSemicolonsFixer.php

@@ -76,7 +76,7 @@ function foo () {
      * {@inheritdoc}
      *
      * Must run before SpaceAfterSemicolonFixer.
-     * Must run after CombineConsecutiveIssetsFixer, NoEmptyStatementFixer, SimplifiedIfReturnFixer, SingleImportPerStatementFixer.
+     * Must run after CombineConsecutiveIssetsFixer, GetClassToClassKeywordFixer, NoEmptyStatementFixer, SimplifiedIfReturnFixer, SingleImportPerStatementFixer.
      */
     public function getPriority(): int
     {

+ 1 - 1
src/Fixer/Whitespace/NoSpacesInsideParenthesisFixer.php

@@ -50,7 +50,7 @@ function foo( \$bar, \$baz )
     /**
      * {@inheritdoc}
      *
-     * Must run before FunctionToConstantFixer, StringLengthToEmptyFixer.
+     * Must run before FunctionToConstantFixer, GetClassToClassKeywordFixer, StringLengthToEmptyFixer.
      * Must run after CombineConsecutiveIssetsFixer, CombineNestedDirnameFixer, LambdaNotUsedImportFixer, ModernizeStrposFixer, NoUselessSprintfFixer, PowToExponentiationFixer.
      */
     public function getPriority(): int

+ 1 - 0
src/RuleSet/Sets/PHP80MigrationRiskySet.php

@@ -25,6 +25,7 @@ final class PHP80MigrationRiskySet extends AbstractMigrationSetDescription
     {
         return [
             '@PHP74Migration:risky' => true,
+            'get_class_to_class_keyword' => true,
             'modernize_strpos' => true,
             'no_alias_functions' => [
                 'sets' => [

+ 3 - 0
tests/AutoReview/FixerFactoryTest.php

@@ -128,6 +128,7 @@ final class FixerFactoryTest extends TestCase
             [$fixers['general_phpdoc_annotation_remove'], $fixers['phpdoc_separation']],
             [$fixers['general_phpdoc_annotation_remove'], $fixers['phpdoc_trim']],
             [$fixers['general_phpdoc_tag_rename'], $fixers['phpdoc_add_missing_param_annotation']],
+            [$fixers['get_class_to_class_keyword'], $fixers['multiline_whitespace_before_semicolons']],
             [$fixers['global_namespace_import'], $fixers['no_unused_imports']],
             [$fixers['global_namespace_import'], $fixers['ordered_imports']],
             [$fixers['header_comment'], $fixers['single_line_comment_style']],
@@ -188,7 +189,9 @@ final class FixerFactoryTest extends TestCase
             [$fixers['no_php4_constructor'], $fixers['ordered_class_elements']],
             [$fixers['no_short_bool_cast'], $fixers['cast_spaces']],
             [$fixers['no_spaces_after_function_name'], $fixers['function_to_constant']],
+            [$fixers['no_spaces_after_function_name'], $fixers['get_class_to_class_keyword']],
             [$fixers['no_spaces_inside_parenthesis'], $fixers['function_to_constant']],
+            [$fixers['no_spaces_inside_parenthesis'], $fixers['get_class_to_class_keyword']],
             [$fixers['no_spaces_inside_parenthesis'], $fixers['string_length_to_empty']],
             [$fixers['no_superfluous_elseif'], $fixers['simplified_if_return']],
             [$fixers['no_superfluous_phpdoc_tags'], $fixers['no_empty_phpdoc']],

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