Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

GetClassToClassKeywordFixer - introduction #5953

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
9 changes: 9 additions & 0 deletions doc/list.rst
Expand Up @@ -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.
Expand Down
1 change: 1 addition & 0 deletions doc/ruleSets/PHP80MigrationRisky.rst
Expand Up @@ -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:
Expand Down
3 changes: 3 additions & 0 deletions doc/rules/index.rst
Expand Up @@ -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``.
Expand Down
44 changes: 44 additions & 0 deletions 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.
Expand Up @@ -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
Expand Down
167 changes: 167 additions & 0 deletions 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);
}
}
Expand Up @@ -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
{
Expand Down
2 changes: 1 addition & 1 deletion src/Fixer/Whitespace/NoSpacesInsideParenthesisFixer.php
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions src/RuleSet/Sets/PHP80MigrationRiskySet.php
Expand Up @@ -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' => [
Expand Down
3 changes: 3 additions & 0 deletions tests/AutoReview/FixerFactoryTest.php
Expand Up @@ -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']],
Expand Down Expand Up @@ -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']],
Expand Down