Skip to content

Commit

Permalink
feat: Introduce general_attribute_remove fixer (#8339)
Browse files Browse the repository at this point in the history
  • Loading branch information
raffaelecarelle authored Jan 8, 2025
1 parent 87df022 commit f2f92d2
Show file tree
Hide file tree
Showing 18 changed files with 727 additions and 88 deletions.
6 changes: 0 additions & 6 deletions dev-tools/phpstan/baseline.php
Original file line number Diff line number Diff line change
Expand Up @@ -775,12 +775,6 @@
'count' => 1,
'path' => __DIR__ . '/../../src/Fixer/ArrayNotation/YieldFromArrayToYieldsFixer.php',
];
$ignoreErrors[] = [
'message' => '#^Parameter \\#1 \\$string of function substr expects string, string\\|false given\\.$#',
'identifier' => 'argument.type',
'count' => 1,
'path' => __DIR__ . '/../../src/Fixer/AttributeNotation/OrderedAttributesFixer.php',
];
$ignoreErrors[] = [
'message' => '#^Offset 0 might not exist on list\\<string\\>\\.$#',
'identifier' => 'offsetAccess.notFound',
Expand Down
56 changes: 56 additions & 0 deletions doc/rules/attribute_notation/general_attribute_remove.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
=================================
Rule ``general_attribute_remove``
=================================

Removes configured attributes by their respective FQN.

Configuration
-------------

``attributes``
~~~~~~~~~~~~~~

List of FQNs of attributes for removal.

Allowed types: ``list<string>``

Default value: ``[]``

Examples
--------

Example #1
~~~~~~~~~~

With configuration: ``['attributes' => ['\\A\\B\\Foo']]``.

.. code-block:: diff
--- Original
+++ New
<?php
-#[\A\B\Foo]
function foo() {}
Example #2
~~~~~~~~~~

With configuration: ``['attributes' => ['\\A\\B\\Foo', 'A\\B\\Bar']]``.

.. code-block:: diff
--- Original
+++ New
<?php
use A\B\Bar as BarAlias;
-#[\A\B\Foo]
-#[BarAlias]
function foo() {}
References
----------

- Fixer class: `PhpCsFixer\\Fixer\\AttributeNotation\\GeneralAttributeRemoveFixer <./../../../src/Fixer/AttributeNotation/GeneralAttributeRemoveFixer.php>`_
- Test class: `PhpCsFixer\\Tests\\Fixer\\AttributeNotation\\GeneralAttributeRemoveFixerTest <./../../../tests/Fixer/AttributeNotation/GeneralAttributeRemoveFixerTest.php>`_

The test class defines officially supported behaviour. Each test case is a part of our backward compatibility promise.
11 changes: 7 additions & 4 deletions doc/rules/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,9 @@ Attribute Notation
- `attribute_empty_parentheses <./attribute_notation/attribute_empty_parentheses.rst>`_

PHP attributes declared without arguments must (not) be followed by empty parentheses.
- `general_attribute_remove <./attribute_notation/general_attribute_remove.rst>`_

Removes configured attributes by their respective FQN.
- `ordered_attributes <./attribute_notation/ordered_attributes.rst>`_

Sorts attributes using the configured sort algorithm.
Expand Down Expand Up @@ -715,7 +718,7 @@ PHPDoc
Each line of multi-line DocComments must have an asterisk [PSR-5] and must be aligned with the first one.
- `general_phpdoc_annotation_remove <./phpdoc/general_phpdoc_annotation_remove.rst>`_

Configured annotations should be omitted from PHPDoc.
Removes configured annotations from PHPDoc.
- `general_phpdoc_tag_rename <./phpdoc/general_phpdoc_tag_rename.rst>`_

Renames PHPDoc tags.
Expand Down Expand Up @@ -754,16 +757,16 @@ PHPDoc
PHPDoc ``list`` type must be used instead of ``array`` without a key.
- `phpdoc_no_access <./phpdoc/phpdoc_no_access.rst>`_

``@access`` annotations should be omitted from PHPDoc.
``@access`` annotations must be removed from PHPDoc.
- `phpdoc_no_alias_tag <./phpdoc/phpdoc_no_alias_tag.rst>`_

No alias PHPDoc tags should be used.
- `phpdoc_no_empty_return <./phpdoc/phpdoc_no_empty_return.rst>`_

``@return void`` and ``@return null`` annotations should be omitted from PHPDoc.
``@return void`` and ``@return null`` annotations must be removed from PHPDoc.
- `phpdoc_no_package <./phpdoc/phpdoc_no_package.rst>`_

``@package`` and ``@subpackage`` annotations should be omitted from PHPDoc.
``@package`` and ``@subpackage`` annotations must be removed from PHPDoc.
- `phpdoc_no_useless_inheritdoc <./phpdoc/phpdoc_no_useless_inheritdoc.rst>`_

Classy that does not inherit must not have ``@inheritdoc`` tags.
Expand Down
2 changes: 1 addition & 1 deletion doc/rules/phpdoc/general_phpdoc_annotation_remove.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
Rule ``general_phpdoc_annotation_remove``
=========================================

Configured annotations should be omitted from PHPDoc.
Removes configured annotations from PHPDoc.

Configuration
-------------
Expand Down
2 changes: 1 addition & 1 deletion doc/rules/phpdoc/phpdoc_no_access.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
Rule ``phpdoc_no_access``
=========================

``@access`` annotations should be omitted from PHPDoc.
``@access`` annotations must be removed from PHPDoc.

Examples
--------
Expand Down
2 changes: 1 addition & 1 deletion doc/rules/phpdoc/phpdoc_no_empty_return.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
Rule ``phpdoc_no_empty_return``
===============================

``@return void`` and ``@return null`` annotations should be omitted from PHPDoc.
``@return void`` and ``@return null`` annotations must be removed from PHPDoc.

Examples
--------
Expand Down
2 changes: 1 addition & 1 deletion doc/rules/phpdoc/phpdoc_no_package.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
Rule ``phpdoc_no_package``
==========================

``@package`` and ``@subpackage`` annotations should be omitted from PHPDoc.
``@package`` and ``@subpackage`` annotations must be removed from PHPDoc.

Examples
--------
Expand Down
142 changes: 142 additions & 0 deletions src/Fixer/AttributeNotation/GeneralAttributeRemoveFixer.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
<?php

declare(strict_types=1);

/*
* This file is part of PHP CS Fixer.
*
* (c) Fabien Potencier <[email protected]>
* Dariusz Rumiński <[email protected]>
*
* This source file is subject to the MIT license that is bundled
* with this source code in the file LICENSE.
*/

namespace PhpCsFixer\Fixer\AttributeNotation;

use PhpCsFixer\AbstractFixer;
use PhpCsFixer\Fixer\ConfigurableFixerInterface;
use PhpCsFixer\Fixer\ConfigurableFixerTrait;
use PhpCsFixer\FixerConfiguration\FixerConfigurationResolver;
use PhpCsFixer\FixerConfiguration\FixerConfigurationResolverInterface;
use PhpCsFixer\FixerConfiguration\FixerOptionBuilder;
use PhpCsFixer\FixerDefinition\CodeSample;
use PhpCsFixer\FixerDefinition\FixerDefinition;
use PhpCsFixer\FixerDefinition\FixerDefinitionInterface;
use PhpCsFixer\Tokenizer\Analyzer\Analysis\AttributeAnalysis;
use PhpCsFixer\Tokenizer\Analyzer\AttributeAnalyzer;
use PhpCsFixer\Tokenizer\Tokens;

/**
* @author Raffaele Carelle <[email protected]>
*
* @implements ConfigurableFixerInterface<_AutogeneratedInputConfiguration, _AutogeneratedComputedConfiguration>
*
* @phpstan-import-type _AttributeItems from AttributeAnalysis
* @phpstan-import-type _AttributeItem from AttributeAnalysis
*
* @phpstan-type _AutogeneratedInputConfiguration array{
* attributes?: list<string>
* }
* @phpstan-type _AutogeneratedComputedConfiguration array{
* attributes: list<string>
* }
*/
final class GeneralAttributeRemoveFixer extends AbstractFixer implements ConfigurableFixerInterface
{
/** @use ConfigurableFixerTrait<_AutogeneratedInputConfiguration, _AutogeneratedComputedConfiguration> */
use ConfigurableFixerTrait;

public function getDefinition(): FixerDefinitionInterface
{
return new FixerDefinition(
'Removes configured attributes by their respective FQN.',
[
new CodeSample(
'<?php
#[\A\B\Foo]
function foo() {}
',
['attributes' => ['\A\B\Foo']]
),
new CodeSample(
'<?php
use A\B\Bar as BarAlias;
#[\A\B\Foo]
#[BarAlias]
function foo() {}
',
['attributes' => ['\A\B\Foo', 'A\B\Bar']]
),
]
);
}

public function getPriority(): int
{
return 0;
}

public function isCandidate(Tokens $tokens): bool
{
return \defined('T_ATTRIBUTE') && $tokens->isTokenKindFound(T_ATTRIBUTE);
}

protected function applyFix(\SplFileInfo $file, Tokens $tokens): void
{
if (0 === \count($this->configuration['attributes'])) {
return;
}

$index = 0;

while (null !== $index = $tokens->getNextTokenOfKind($index, [[T_ATTRIBUTE]])) {
$attributeAnalysis = AttributeAnalyzer::collectOne($tokens, $index);

$endIndex = $attributeAnalysis->getEndIndex();

$removedCount = 0;
foreach ($attributeAnalysis->getAttributes() as $element) {
$fullname = AttributeAnalyzer::determineAttributeFullyQualifiedName($tokens, $element['name'], $element['start']);

if (!\in_array($fullname, $this->configuration['attributes'], true)) {
continue;
}

$tokens->clearRange($element['start'], $element['end']);
++$removedCount;

$siblingIndex = $tokens->getNonEmptySibling($element['end'], 1);

// Clear element comma
if (',' === $tokens[$siblingIndex]->getContent()) {
$tokens->clearAt($siblingIndex);
}
}

// Clear whole attribute if all are removed (multiline attribute case)
if (\count($attributeAnalysis->getAttributes()) === $removedCount) {
$tokens->clearRange($attributeAnalysis->getStartIndex(), $attributeAnalysis->getEndIndex());
}

// Clear trailing comma
$tokenIndex = $tokens->getMeaningfulTokenSibling($attributeAnalysis->getClosingBracketIndex(), -1);
if (',' === $tokens[$tokenIndex]->getContent()) {
$tokens->clearAt($tokenIndex);
}

$index = $endIndex;
}
}

protected function createConfigurationDefinition(): FixerConfigurationResolverInterface
{
return new FixerConfigurationResolver([
(new FixerOptionBuilder('attributes', 'List of FQNs of attributes for removal.'))
->setAllowedTypes(['string[]'])
->setDefault([])
->getOption(),
]);
}
}
54 changes: 1 addition & 53 deletions src/Fixer/AttributeNotation/OrderedAttributesFixer.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,7 @@
use PhpCsFixer\FixerDefinition\VersionSpecification;
use PhpCsFixer\FixerDefinition\VersionSpecificCodeSample;
use PhpCsFixer\Tokenizer\Analyzer\Analysis\AttributeAnalysis;
use PhpCsFixer\Tokenizer\Analyzer\Analysis\NamespaceAnalysis;
use PhpCsFixer\Tokenizer\Analyzer\Analysis\NamespaceUseAnalysis;
use PhpCsFixer\Tokenizer\Analyzer\AttributeAnalyzer;
use PhpCsFixer\Tokenizer\Analyzer\NamespacesAnalyzer;
use PhpCsFixer\Tokenizer\Analyzer\NamespaceUsesAnalyzer;
use PhpCsFixer\Tokenizer\Token;
use PhpCsFixer\Tokenizer\Tokens;
use Symfony\Component\OptionsResolver\Options;
Expand Down Expand Up @@ -221,40 +217,12 @@ private function sortAttributes(Tokens $tokens, int $index, array $attributes):
private function getAttributeName(Tokens $tokens, string $name, int $index): string
{
if (self::ORDER_CUSTOM === $this->configuration['sort_algorithm']) {
$name = $this->determineAttributeFullyQualifiedName($tokens, $name, $index);
$name = AttributeAnalyzer::determineAttributeFullyQualifiedName($tokens, $name, $index);
}

return ltrim($name, '\\');
}

private function determineAttributeFullyQualifiedName(Tokens $tokens, string $name, int $index): string
{
if ('\\' === $name[0]) {
return $name;
}

if (!$tokens[$index]->isGivenKind([T_STRING, T_NS_SEPARATOR])) {
$index = $tokens->getNextTokenOfKind($index, [[T_STRING], [T_NS_SEPARATOR]]);
}

[$namespaceAnalysis, $namespaceUseAnalyses] = $this->collectNamespaceAnalysis($tokens, $index);
$namespace = $namespaceAnalysis->getFullName();
$firstTokenOfName = $tokens[$index]->getContent();
$namespaceUseAnalysis = $namespaceUseAnalyses[$firstTokenOfName] ?? false;

if ($namespaceUseAnalysis instanceof NamespaceUseAnalysis) {
$namespace = $namespaceUseAnalysis->getFullName();

if ($name === $firstTokenOfName) {
return $namespace;
}

$name = substr(strstr($name, '\\'), 1);
}

return $namespace.'\\'.$name;
}

/**
* @param _AttributeItems $elements
*
Expand Down Expand Up @@ -300,24 +268,4 @@ private function sortTokens(Tokens $tokens, int $startIndex, int $endIndex, arra

$tokens->overrideRange($startIndex, $endIndex, $replaceTokens);
}

/**
* @return array{NamespaceAnalysis, array<string, NamespaceUseAnalysis>}
*/
private function collectNamespaceAnalysis(Tokens $tokens, int $startIndex): array
{
$namespaceAnalysis = (new NamespacesAnalyzer())->getNamespaceAt($tokens, $startIndex);
$namespaceUseAnalyses = (new NamespaceUsesAnalyzer())->getDeclarationsInNamespace($tokens, $namespaceAnalysis);

$uses = [];
foreach ($namespaceUseAnalyses as $use) {
if (!$use->isClass()) {
continue;
}

$uses[$use->getShortName()] = $use;
}

return [$namespaceAnalysis, $uses];
}
}
Loading

0 comments on commit f2f92d2

Please sign in to comment.