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

Feature Request: Add MultilinePromotedPropertiesWithAttributesFixer #839

Open
patrickcurl opened this issue Nov 10, 2022 · 3 comments
Open
Labels

Comments

@patrickcurl
Copy link

Currently I'm only able to get :

public function __construct(
    #[Required,
    StringType,
    In(['primary', 'secondary'])]
    public string $type,

What I'd like it it to just be:

public function __construct(
    #[Required, StringType, In(['primary', 'secondary'])]
    public string $type,
)

or even:

public function __construct(
    #[
          Required, 
          StringType, 
          In(['primary', 'secondary'])
    ]
    public string $type,
)
@kubawerlos
Copy link
Owner

Hi @patrickcurl, what would you say for a fixer to make attribute single line, similar to single_line_throw?

@petski
Copy link

petski commented Jan 25, 2025

+1 for the "or even" writing style

@petski
Copy link

petski commented Feb 10, 2025

I implemented the "or even" style myself. See below.

I'd be happy to contribute this via a PR, if you're interested, @kubawerlos

<?php

declare(strict_types=1);

namespace App\CodeQuality\PhpCsFixer;

use Override;
use PhpCsFixer\AbstractFixer;
use PhpCsFixer\Fixer\Indentation;
use PhpCsFixer\Fixer\WhitespacesAwareFixerInterface;
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\CT;
use PhpCsFixer\Tokenizer\Token;
use PhpCsFixer\Tokenizer\Tokens;
use SplFileInfo;

/**
 * @phpstan-import-type _AttributeItems from AttributeAnalysis
 */
final class AttributeGroupFixer extends AbstractFixer implements WhitespacesAwareFixerInterface
{
    use Indentation;

    public const string NAME = 'App/attribute_group_fixer';

    /**
     * @param Tokens<Token> $tokens
     */
    #[Override()]
    public function isCandidate(Tokens $tokens): bool
    {
        return $tokens->isTokenKindFound(T_ATTRIBUTE);
    }

    #[Override()]
    public function getDefinition(): FixerDefinitionInterface
    {
        return new FixerDefinition(
            'Opinionated fixer for attribute groups.',
            [
                new CodeSample(
                    <<<PHP
                        <?php
                        #[ORM\Entity(repositoryClass: BarRepository::class)]
                        #[SoftDeleteable(hardDelete: false)]
                        class Bar
                        {
                            #[ApiProperty(identifier: true)]
                            #[Groups([
                                'read'
                            ])]
                            #[ORM\Id]
                            public function foo() {
                                return 'foo';
                            }

                            #[
                                ORM\Id(), ApiProperty(identifier: true),
                                Groups([
                                    'write'
                                ])
                            ]
                            public function bar() {
                                return 'bar';
                            }

                            #[
                            Groups(
                                [
                                    'read',
                                    'write'
                                ]
                            ),
                            ORM\Id,
                            ]
                            public function baz() {
                                return 'baz';
                            }
                        }

                        PHP,
                ),
            ],
        );
    }

    #[Override()]
    public function getPriority(): int
    {
        // Lower than StatementIndentationFixer
        return -4;
    }

    #[Override()]
    public function getName(): string
    {
        return self::NAME;
    }

    /**
     * @param Tokens<Token> $tokens
     */
    protected function applyFix(SplFileInfo $file, Tokens $tokens): void
    {
        $index = 0;

        while (null !== $index = $tokens->getNextTokenOfKind($index, [[T_ATTRIBUTE]])) {
            // Placeholders for data we need
            $openingBracketIndex = null;
            $closingBracketIndex = null;
            $attributes          = [];

            // Collect the data we need
            foreach (AttributeAnalyzer::collect($tokens, $index) as $attributeAnalysis) {
                if ($openingBracketIndex === null) {
                    $openingBracketIndex = $attributeAnalysis->getOpeningBracketIndex();
                }

                $closingBracketIndex = $attributeAnalysis->getClosingBracketIndex();
                $attributes          = array_merge($attributes, $attributeAnalysis->getAttributes());
            }

            // Save the fuzz when there are no attributes (quite unlikely, but better safe than sorry)
            if (count($attributes) === 0 || $openingBracketIndex === null || $closingBracketIndex === null) {
                ++$index;
                continue;
            }

            try {
                // Save the fuzz when there's only one attribute
                if (count($attributes) === 1) {
                    continue;
                }

                $desiredStyle = $this->desiredStyle($tokens, $openingBracketIndex, $attributes);
                $tokens->overrideRange($openingBracketIndex, $closingBracketIndex, $desiredStyle);
            } finally {
                $index = $closingBracketIndex;
            }
        }
    }

    /**
     * @param Tokens<Token>   $tokens
     * @param _AttributeItems $attributes
     */
    private function desiredStyle(
        Tokens $tokens,
        int $openingBracketIndex,
        array $attributes,
    ): Tokens {
        // Determine the indentation of the line of the opening bracket
        $lineIndentationAtOpeningBracket = $this->getLineIndentation($tokens, $openingBracketIndex);

        return Tokens::fromArray([
            new Token([T_ATTRIBUTE, '#[']),
            new Token([T_WHITESPACE, $this->whitespacesConfig->getLineEnding()]),
            ...array_reduce(
                $attributes,
                function (array $carry, array $attribute) use ($tokens, $lineIndentationAtOpeningBracket): array {
                    $trueStart = $tokens[$attribute['start']]->isWhitespace() ? $tokens->getNextNonWhitespace($attribute['start']) : $attribute['start'];

                    if ($trueStart === null) {
                        return $carry;
                    }

                    $lineIndentationAtStart = $this->getLineIndentation($tokens, $trueStart);

                    // Determine the code
                    $attributeBodyTokens = [];
                    for ($i = $trueStart; $i <= $attribute['end']; ++$i) {
                        $attributeBodyToken = clone $tokens[$i];

                        if ($attributeBodyToken->isWhitespace() && str_contains($attributeBodyToken->getContent(), $this->whitespacesConfig->getLineEnding())) {
                            $attributeBodyToken = new Token([
                                T_WHITESPACE,
                                $this->whitespacesConfig->getLineEnding() .
                                $lineIndentationAtOpeningBracket .
                                $this->whitespacesConfig->getIndent() .
                                (
                                    $i === $attribute['end'] ? '' : implode('', array_fill(0, strlen($this->getLineIndentation($tokens, $i + 1)) - strlen($lineIndentationAtStart), ' '))
                                ),
                            ]);
                        }

                        $attributeBodyTokens[] = $attributeBodyToken;
                    }

                    return array_merge(
                        $carry,
                        [
                            new Token([T_WHITESPACE, $lineIndentationAtOpeningBracket . $this->whitespacesConfig->getIndent()]),
                            ...$attributeBodyTokens,
                            new Token([T_STRING, ',']),
                            new Token([T_WHITESPACE, $this->whitespacesConfig->getLineEnding()]),
                        ],
                    );
                },
                [],
            ),
            ...($lineIndentationAtOpeningBracket !== '' ? [new Token([T_WHITESPACE, $lineIndentationAtOpeningBracket])] : []),
            new Token([CT::T_ATTRIBUTE_CLOSE, ']']),
        ]);
    }
}

Partial output of vendor/bin/php-cs-fixer describe App/attribute_group_fixer:

--- Original
+++ New
@@ -1,37 +1,42 @@
 <?php
-#[ORM\Entity(repositoryClass: BarRepository::class)]
-#[SoftDeleteable(hardDelete: false)]
+#[
+    ORM\Entity(repositoryClass: BarRepository::class),
+    SoftDeleteable(hardDelete: false),
+]
 class Bar
 {
-    #[ApiProperty(identifier: true)]
-    #[Groups([
-        'read'
-    ])]
-    #[ORM\Id]
+    #[
+        ApiProperty(identifier: true),
+        Groups([
+            'read'
+        ]),
+        ORM\Id,
+    ]
     public function foo() {
         return 'foo';
     }
 
     #[
-        ORM\Id(), ApiProperty(identifier: true),
+        ORM\Id(),
+        ApiProperty(identifier: true),
         Groups([
             'write'
-        ])
+        ]),
     ]
     public function bar() {
         return 'bar';
     }
 
     #[
-    Groups(
-        [
-            'read',
-            'write'
-        ]
-    ),
-    ORM\Id,
+        Groups(
+            [
+                'read',
+                'write'
+            ]
+        ),
+        ORM\Id,
     ]
     public function baz() {
         return 'baz';
     }
 }

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

3 participants