From de85002e3ec7b4a1fe6bea44d2cea11329e37a00 Mon Sep 17 00:00:00 2001 From: "John Paul E. Balandan, CPA" Date: Mon, 13 Sep 2021 20:54:58 +0800 Subject: [PATCH] GetClassToClassKeywordFixer - introduction --- doc/list.rst | 9 + doc/ruleSets/PHP80MigrationRisky.rst | 1 + doc/rules/index.rst | 3 + .../get_class_to_class_keyword.rst | 44 +++++ .../NoSpacesAfterFunctionNameFixer.php | 2 +- .../GetClassToClassKeywordFixer.php | 172 ++++++++++++++++++ ...ltilineWhitespaceBeforeSemicolonsFixer.php | 2 +- .../NoSpacesInsideParenthesisFixer.php | 2 +- src/RuleSet/Sets/PHP80MigrationRiskySet.php | 1 + tests/AutoReview/FixerFactoryTest.php | 3 + .../GetClassToClassKeywordFixerTest.php | 161 ++++++++++++++++ ...ultiline_whitespace_before_semicolons.test | 17 ++ ...ction_name,get_class_to_class_keyword.test | 17 ++ ...arenthesis,get_class_to_class_keyword.test | 17 ++ 14 files changed, 448 insertions(+), 3 deletions(-) create mode 100644 doc/rules/language_construct/get_class_to_class_keyword.rst create mode 100644 src/Fixer/LanguageConstruct/GetClassToClassKeywordFixer.php create mode 100644 tests/Fixer/LanguageConstruct/GetClassToClassKeywordFixerTest.php create mode 100644 tests/Fixtures/Integration/priority/get_class_to_class_keyword,multiline_whitespace_before_semicolons.test create mode 100644 tests/Fixtures/Integration/priority/no_spaces_after_function_name,get_class_to_class_keyword.test create mode 100644 tests/Fixtures/Integration/priority/no_spaces_inside_parenthesis,get_class_to_class_keyword.test diff --git a/doc/list.rst b/doc/list.rst index dd710c5ce12..51983bba6c2 100644 --- a/doc/list.rst +++ b/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. diff --git a/doc/ruleSets/PHP80MigrationRisky.rst b/doc/ruleSets/PHP80MigrationRisky.rst index d5b9f874b87..dc50e100494 100644 --- a/doc/ruleSets/PHP80MigrationRisky.rst +++ b/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: diff --git a/doc/rules/index.rst b/doc/rules/index.rst index bb171e04c10..8d1ad6618a3 100644 --- a/doc/rules/index.rst +++ b/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``. diff --git a/doc/rules/language_construct/get_class_to_class_keyword.rst b/doc/rules/language_construct/get_class_to_class_keyword.rst new file mode 100644 index 00000000000..c9a0674b92c --- /dev/null +++ b/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 + `_ rule set will enable the ``get_class_to_class_keyword`` rule. diff --git a/src/Fixer/FunctionNotation/NoSpacesAfterFunctionNameFixer.php b/src/Fixer/FunctionNotation/NoSpacesAfterFunctionNameFixer.php index 621a3b67683..24ef8469aee 100644 --- a/src/Fixer/FunctionNotation/NoSpacesAfterFunctionNameFixer.php +++ b/src/Fixer/FunctionNotation/NoSpacesAfterFunctionNameFixer.php @@ -43,7 +43,7 @@ public function getDefinition(): FixerDefinitionInterface /** * {@inheritdoc} * - * Must run before FunctionToConstantFixer. + * Must run before FunctionToConstantFixer, GetClassToClassKeywordFixer. * Must run after PowToExponentiationFixer. */ public function getPriority(): int diff --git a/src/Fixer/LanguageConstruct/GetClassToClassKeywordFixer.php b/src/Fixer/LanguageConstruct/GetClassToClassKeywordFixer.php new file mode 100644 index 00000000000..33a97db9ef1 --- /dev/null +++ b/src/Fixer/LanguageConstruct/GetClassToClassKeywordFixer.php @@ -0,0 +1,172 @@ + + * 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\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 + */ +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( + "= 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)) { + // get_class with no arguments + continue; + } + + $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())) { + // get_class($this) + continue 2; + } + + $variableTokensIndices[] = $i; + } + + if ($meaningfulTokensCount > 1 || 1 !== \count($variableTokensIndices)) { + // argument contains more logic, or more arguments, or no variable argument + continue; + } + + $indicesToClear[$index] = [$braceOpenIndex, current($variableTokensIndices), $braceCloseIndex]; + } + + foreach ($indicesToClear as $index => $items) { + $tokenSlices += $this->getReplacementTokenSlices($tokens, $index, $items[1]); + $this->clearGetClassCall($tokens, $index, $items[0], $items[2]); + } + + $tokens->insertSlices($tokenSlices); + } + + private function getReplacementTokenSlices(Tokens $tokens, int $index, int $variableIndex): array + { + return [ + $index => [ + 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); + } +} diff --git a/src/Fixer/Semicolon/MultilineWhitespaceBeforeSemicolonsFixer.php b/src/Fixer/Semicolon/MultilineWhitespaceBeforeSemicolonsFixer.php index a7b3d4767db..919f2df3388 100644 --- a/src/Fixer/Semicolon/MultilineWhitespaceBeforeSemicolonsFixer.php +++ b/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 { diff --git a/src/Fixer/Whitespace/NoSpacesInsideParenthesisFixer.php b/src/Fixer/Whitespace/NoSpacesInsideParenthesisFixer.php index 623af8132e5..0fb421a67b2 100644 --- a/src/Fixer/Whitespace/NoSpacesInsideParenthesisFixer.php +++ b/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 diff --git a/src/RuleSet/Sets/PHP80MigrationRiskySet.php b/src/RuleSet/Sets/PHP80MigrationRiskySet.php index e3daabf5284..4027b6ed8c6 100644 --- a/src/RuleSet/Sets/PHP80MigrationRiskySet.php +++ b/src/RuleSet/Sets/PHP80MigrationRiskySet.php @@ -25,6 +25,7 @@ public function getRules(): array { return [ '@PHP74Migration:risky' => true, + 'get_class_to_class_keyword' => true, 'modernize_strpos' => true, 'no_alias_functions' => [ 'sets' => [ diff --git a/tests/AutoReview/FixerFactoryTest.php b/tests/AutoReview/FixerFactoryTest.php index 70115b5ad91..15ba96e6da0 100644 --- a/tests/AutoReview/FixerFactoryTest.php +++ b/tests/AutoReview/FixerFactoryTest.php @@ -128,6 +128,7 @@ public function provideFixersPriorityCases(): array [$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 @@ public function provideFixersPriorityCases(): array [$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']], diff --git a/tests/Fixer/LanguageConstruct/GetClassToClassKeywordFixerTest.php b/tests/Fixer/LanguageConstruct/GetClassToClassKeywordFixerTest.php new file mode 100644 index 00000000000..f7d76fe62f1 --- /dev/null +++ b/tests/Fixer/LanguageConstruct/GetClassToClassKeywordFixerTest.php @@ -0,0 +1,161 @@ + + * 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\LanguageConstruct; + +use PhpCsFixer\Tests\Test\AbstractFixerTestCase; + +/** + * @author John Paul E. Balandan, CPA + * + * @internal + * + * @covers \PhpCsFixer\Fixer\LanguageConstruct\GetClassToClassKeywordFixer + * @requires PHP 8.0 + */ +final class GetClassToClassKeywordFixerTest extends AbstractFixerTestCase +{ + /** + * @dataProvider provideFixCases + */ + public function testFixCases(string $expected, ?string $input = null): void + { + $this->doTest($expected, $input); + } + + public function provideFixCases() + { + yield [ + ' +bar);', + ]; + + yield [ + '