From 964b91d2fae2f96acc42b690d74211d746ffa33a Mon Sep 17 00:00:00 2001 From: Tomas Votruba Date: Wed, 22 Jul 2026 15:52:41 +0200 Subject: [PATCH] Recognize imported short-name Symfony attributes in StandaloneLineSymfonyAttributeParamFixer Resolve the attribute name via file use statements so short names like #[AsCommand(...)] imported from Symfony are handled, not only the fully-qualified #[\Symfony\...\AsCommand(...)] form. --- ...andaloneLineSymfonyAttributeParamFixer.php | 68 ++++++++++++++++--- .../imported_symfony_attribute.php.inc | 28 ++++++++ ...kip_imported_non_symfony_attribute.php.inc | 10 +++ 3 files changed, 97 insertions(+), 9 deletions(-) create mode 100644 packages/coding-standard/tests/Fixer/Spacing/StandaloneLineSymfonyAttributeParamFixer/Fixture/imported_symfony_attribute.php.inc create mode 100644 packages/coding-standard/tests/Fixer/Spacing/StandaloneLineSymfonyAttributeParamFixer/Fixture/skip_imported_non_symfony_attribute.php.inc diff --git a/packages/coding-standard/src/Fixer/Spacing/StandaloneLineSymfonyAttributeParamFixer.php b/packages/coding-standard/src/Fixer/Spacing/StandaloneLineSymfonyAttributeParamFixer.php index 29ed460bb1..b124f7cb3d 100644 --- a/packages/coding-standard/src/Fixer/Spacing/StandaloneLineSymfonyAttributeParamFixer.php +++ b/packages/coding-standard/src/Fixer/Spacing/StandaloneLineSymfonyAttributeParamFixer.php @@ -8,6 +8,7 @@ use PhpCsFixer\FixerDefinition\CodeSample; use PhpCsFixer\FixerDefinition\FixerDefinition; use PhpCsFixer\FixerDefinition\FixerDefinitionInterface; +use PhpCsFixer\Tokenizer\Analyzer\NamespaceUsesAnalyzer; use PhpCsFixer\Tokenizer\Token; use PhpCsFixer\Tokenizer\Tokens; use SplFileInfo; @@ -19,8 +20,8 @@ /** * Every argument of a Symfony attribute must be on a standalone line, to ease git diffs when arguments change. * - * Only attributes whose written name contains the "Symfony" namespace part are handled, so third-party attributes - * keep their original layout. + * Only Symfony attributes are handled, so third-party attributes keep their original layout. Both fully-qualified + * names (#[\Symfony\...\AsCommand]) and short names imported via a use statement (#[AsCommand]) are recognized. * * @see \Symplify\CodingStandard\Tests\Fixer\Spacing\StandaloneLineSymfonyAttributeParamFixer\StandaloneLineSymfonyAttributeParamFixerTest */ @@ -31,7 +32,8 @@ final class StandaloneLineSymfonyAttributeParamFixer extends AbstractSymplifyFix private const string SYMFONY_NAMESPACE_PART = 'Symfony'; public function __construct( - private readonly TokensNewliner $tokensNewliner + private readonly TokensNewliner $tokensNewliner, + private readonly NamespaceUsesAnalyzer $namespaceUsesAnalyzer ) { } @@ -71,6 +73,8 @@ public function isCandidate(Tokens $tokens): bool */ public function fix(SplFileInfo $fileInfo, Tokens $tokens): void { + $shortNameToFullName = $this->resolveShortNameToFullName($tokens); + // from the bottom up, as adding tokens shifts every position after them for ($position = count($tokens) - 1; $position >= 0; --$position) { /** @var Token $token */ @@ -92,7 +96,7 @@ public function fix(SplFileInfo $fileInfo, Tokens $tokens): void continue; } - if (! $this->isSymfonyAttribute($tokens, $position, $openBracketPosition)) { + if (! $this->isSymfonyAttribute($tokens, $openBracketPosition, $shortNameToFullName)) { continue; } @@ -109,18 +113,64 @@ public function fix(SplFileInfo $fileInfo, Tokens $tokens): void /** * @param Tokens $tokens + * @param array $shortNameToFullName */ - private function isSymfonyAttribute(Tokens $tokens, int $attributePosition, int $openBracketPosition): bool + private function isSymfonyAttribute(Tokens $tokens, int $openBracketPosition, array $shortNameToFullName): bool { - for ($index = $attributePosition + 1; $index < $openBracketPosition; ++$index) { + $attributeName = $this->resolveAttributeName($tokens, $openBracketPosition); + + // fully-qualified or partially-qualified Symfony name, e.g. #[\Symfony\...\AsCommand] + if (str_contains($attributeName, self::SYMFONY_NAMESPACE_PART)) { + return true; + } + + // fully-qualified but not Symfony + if (str_starts_with($attributeName, '\\')) { + return false; + } + + // short name imported via a use statement, e.g. #[AsCommand] with "use Symfony\...\AsCommand;" + $firstNamePart = explode('\\', $attributeName)[0]; + $fullName = $shortNameToFullName[$firstNamePart] ?? null; + + return $fullName !== null && str_contains($fullName, self::SYMFONY_NAMESPACE_PART); + } + + /** + * Reads the attribute name written right before its "(", e.g. "\Symfony\...\AsCommand" or "AsCommand". + * + * @param Tokens $tokens + */ + private function resolveAttributeName(Tokens $tokens, int $openBracketPosition): string + { + $attributeName = ''; + + for ($index = $openBracketPosition - 1; $index >= 0; --$index) { /** @var Token $token */ $token = $tokens[$index]; - if ($token->isGivenKind(T_STRING) && $token->getContent() === self::SYMFONY_NAMESPACE_PART) { - return true; + if (! $token->isGivenKind([T_STRING, T_NS_SEPARATOR])) { + break; } + + $attributeName = $token->getContent() . $attributeName; + } + + return $attributeName; + } + + /** + * @param Tokens $tokens + * @return array + */ + private function resolveShortNameToFullName(Tokens $tokens): array + { + $shortNameToFullName = []; + + foreach ($this->namespaceUsesAnalyzer->getDeclarationsFromTokens($tokens) as $namespaceUseAnalysis) { + $shortNameToFullName[$namespaceUseAnalysis->getShortName()] = $namespaceUseAnalysis->getFullName(); } - return false; + return $shortNameToFullName; } } diff --git a/packages/coding-standard/tests/Fixer/Spacing/StandaloneLineSymfonyAttributeParamFixer/Fixture/imported_symfony_attribute.php.inc b/packages/coding-standard/tests/Fixer/Spacing/StandaloneLineSymfonyAttributeParamFixer/Fixture/imported_symfony_attribute.php.inc new file mode 100644 index 0000000000..6f1cd461ce --- /dev/null +++ b/packages/coding-standard/tests/Fixer/Spacing/StandaloneLineSymfonyAttributeParamFixer/Fixture/imported_symfony_attribute.php.inc @@ -0,0 +1,28 @@ + +----- + diff --git a/packages/coding-standard/tests/Fixer/Spacing/StandaloneLineSymfonyAttributeParamFixer/Fixture/skip_imported_non_symfony_attribute.php.inc b/packages/coding-standard/tests/Fixer/Spacing/StandaloneLineSymfonyAttributeParamFixer/Fixture/skip_imported_non_symfony_attribute.php.inc new file mode 100644 index 0000000000..007ab8df9b --- /dev/null +++ b/packages/coding-standard/tests/Fixer/Spacing/StandaloneLineSymfonyAttributeParamFixer/Fixture/skip_imported_non_symfony_attribute.php.inc @@ -0,0 +1,10 @@ +