From 3747c583711bd64c625786add1841170af91a767 Mon Sep 17 00:00:00 2001 From: Tomas Votruba Date: Wed, 22 Jul 2026 21:59:14 +0200 Subject: [PATCH] Break AsCommand always, other Symfony attributes only with 2+ arguments in StandaloneLineSymfonyAttributeParamFixer --- ...andaloneLineSymfonyAttributeParamFixer.php | 60 +++++++++++++++++-- .../as_command_single_argument.php.inc | 23 +++++++ .../Fixture/skip_single_argument.php.inc | 13 ++++ ...single_argument_with_nested_commas.php.inc | 14 +++++ 4 files changed, 105 insertions(+), 5 deletions(-) create mode 100644 packages/coding-standard/tests/Fixer/Spacing/StandaloneLineSymfonyAttributeParamFixer/Fixture/as_command_single_argument.php.inc create mode 100644 packages/coding-standard/tests/Fixer/Spacing/StandaloneLineSymfonyAttributeParamFixer/Fixture/skip_single_argument.php.inc create mode 100644 packages/coding-standard/tests/Fixer/Spacing/StandaloneLineSymfonyAttributeParamFixer/Fixture/skip_single_argument_with_nested_commas.php.inc diff --git a/packages/coding-standard/src/Fixer/Spacing/StandaloneLineSymfonyAttributeParamFixer.php b/packages/coding-standard/src/Fixer/Spacing/StandaloneLineSymfonyAttributeParamFixer.php index a84fc35c7e..d19ac222e9 100644 --- a/packages/coding-standard/src/Fixer/Spacing/StandaloneLineSymfonyAttributeParamFixer.php +++ b/packages/coding-standard/src/Fixer/Spacing/StandaloneLineSymfonyAttributeParamFixer.php @@ -22,12 +22,19 @@ * Only a fixed allowlist of Symfony attributes is handled, so third-party attributes keep their original layout. * Attributes are matched by their short name (#[AsCommand] or #[\Symfony\...\AsCommand]). See self::SYMFONY_ATTRIBUTE_SHORT_NAMES. * + * #[AsCommand] is always broken to standalone lines; every other attribute only when it has 2+ arguments. + * * @see \Symplify\CodingStandard\Tests\Fixer\Spacing\StandaloneLineSymfonyAttributeParamFixer\StandaloneLineSymfonyAttributeParamFixerTest */ final class StandaloneLineSymfonyAttributeParamFixer extends AbstractSymplifyFixer { private const string ERROR_MESSAGE = 'Symfony attribute argument should be on a standalone line to ease git diffs on change'; + /** + * Always broken to standalone lines, even with a single argument. + */ + private const string ALWAYS_ATTRIBUTE_SHORT_NAME = 'AsCommand'; + /** * @var string[] */ @@ -113,7 +120,8 @@ public function fix(SplFileInfo $fileInfo, Tokens $tokens): void continue; } - if (! $this->isSymfonyAttribute($tokens, $openBracketPosition)) { + $attributeShortName = $this->matchSymfonyAttributeShortName($tokens, $openBracketPosition); + if ($attributeShortName === null) { continue; } @@ -123,6 +131,12 @@ public function fix(SplFileInfo $fileInfo, Tokens $tokens): void continue; } + // AsCommand always breaks; other attributes only when they have 2+ arguments + if ($attributeShortName !== self::ALWAYS_ATTRIBUTE_SHORT_NAME + && $this->countArguments($tokens, $openBracketPosition, $closeBracketPosition) < 2) { + continue; + } + $blockInfo = new BlockInfo($openBracketPosition, $closeBracketPosition); $this->tokensNewliner->breakItems($blockInfo, $tokens, LineKind::CALLS); } @@ -131,21 +145,57 @@ public function fix(SplFileInfo $fileInfo, Tokens $tokens): void /** * @param Tokens $tokens */ - private function isSymfonyAttribute(Tokens $tokens, int $openBracketPosition): bool + private function matchSymfonyAttributeShortName(Tokens $tokens, int $openBracketPosition): ?string { // the attribute short name is the T_STRING right before its "(", e.g. "AsCommand" $previousPosition = $tokens->getPrevMeaningfulToken($openBracketPosition); if ($previousPosition === null) { - return false; + return null; } /** @var Token $previousToken */ $previousToken = $tokens[$previousPosition]; if (! $previousToken->isGivenKind(T_STRING)) { - return false; + return null; + } + + $shortName = $previousToken->getContent(); + if (! in_array($shortName, self::SYMFONY_ATTRIBUTE_SHORT_NAMES, true)) { + return null; + } + + return $shortName; + } + + /** + * @param Tokens $tokens + */ + private function countArguments(Tokens $tokens, int $openBracketPosition, int $closeBracketPosition): int + { + $argumentCount = 1; + $nestingLevel = 0; + + for ($position = $openBracketPosition + 1; $position < $closeBracketPosition; ++$position) { + /** @var Token $token */ + $token = $tokens[$position]; + + $content = $token->getContent(); + if (in_array($content, ['(', '['], true) || $token->isGivenKind(T_ATTRIBUTE)) { + ++$nestingLevel; + continue; + } + + if (in_array($content, [')', ']'], true)) { + --$nestingLevel; + continue; + } + + if ($nestingLevel === 0 && $content === ',') { + ++$argumentCount; + } } - return in_array($previousToken->getContent(), self::SYMFONY_ATTRIBUTE_SHORT_NAMES, true); + return $argumentCount; } } diff --git a/packages/coding-standard/tests/Fixer/Spacing/StandaloneLineSymfonyAttributeParamFixer/Fixture/as_command_single_argument.php.inc b/packages/coding-standard/tests/Fixer/Spacing/StandaloneLineSymfonyAttributeParamFixer/Fixture/as_command_single_argument.php.inc new file mode 100644 index 0000000000..31aad2edb3 --- /dev/null +++ b/packages/coding-standard/tests/Fixer/Spacing/StandaloneLineSymfonyAttributeParamFixer/Fixture/as_command_single_argument.php.inc @@ -0,0 +1,23 @@ + +----- + diff --git a/packages/coding-standard/tests/Fixer/Spacing/StandaloneLineSymfonyAttributeParamFixer/Fixture/skip_single_argument.php.inc b/packages/coding-standard/tests/Fixer/Spacing/StandaloneLineSymfonyAttributeParamFixer/Fixture/skip_single_argument.php.inc new file mode 100644 index 0000000000..c0bcfdb68e --- /dev/null +++ b/packages/coding-standard/tests/Fixer/Spacing/StandaloneLineSymfonyAttributeParamFixer/Fixture/skip_single_argument.php.inc @@ -0,0 +1,13 @@ +