Skip to content
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
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;
Expand All @@ -21,8 +20,7 @@
* Every argument of a Symfony attribute must be on a standalone line, to ease git diffs when arguments change.
*
* Only a fixed allowlist of Symfony attributes is 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 self::SYMFONY_ATTRIBUTE_CLASSES.
* Attributes are matched by their short name (#[AsCommand] or #[\Symfony\...\AsCommand]). See self::SYMFONY_ATTRIBUTE_SHORT_NAMES.
*
* @see \Symplify\CodingStandard\Tests\Fixer\Spacing\StandaloneLineSymfonyAttributeParamFixer\StandaloneLineSymfonyAttributeParamFixerTest
*/
Expand All @@ -33,29 +31,28 @@ final class StandaloneLineSymfonyAttributeParamFixer extends AbstractSymplifyFix
/**
* @var string[]
*/
private const array SYMFONY_ATTRIBUTE_CLASSES = [
'Symfony\Component\Console\Attribute\AsCommand',
'Symfony\Component\Routing\Attribute\Route',
'Symfony\Component\DependencyInjection\Attribute\Autowire',
'Symfony\Component\DependencyInjection\Attribute\AutowireIterator',
'Symfony\Component\DependencyInjection\Attribute\AutowireLocator',
'Symfony\Component\DependencyInjection\Attribute\AsAlias',
'Symfony\Component\DependencyInjection\Attribute\AsDecorator',
'Symfony\Component\DependencyInjection\Attribute\AsTaggedItem',
'Symfony\Component\DependencyInjection\Attribute\When',
'Symfony\Component\EventDispatcher\Attribute\AsEventListener',
'Symfony\Component\Messenger\Attribute\AsMessageHandler',
'Symfony\Component\HttpKernel\Attribute\AsController',
'Symfony\Component\HttpKernel\Attribute\MapRequestPayload',
'Symfony\Component\HttpKernel\Attribute\MapQueryParameter',
'Symfony\Component\HttpKernel\Attribute\MapQueryString',
'Symfony\Component\HttpKernel\Attribute\MapEntity',
'Symfony\Component\Security\Http\Attribute\IsGranted',
private const array SYMFONY_ATTRIBUTE_SHORT_NAMES = [
'AsCommand',
'Route',
'Autowire',
'AutowireIterator',
'AutowireLocator',
'AsAlias',
'AsDecorator',
'AsTaggedItem',
'When',
'AsEventListener',
'AsMessageHandler',
'AsController',
'MapRequestPayload',
'MapQueryParameter',
'MapQueryString',
'MapEntity',
'IsGranted',
];

public function __construct(
private readonly TokensNewliner $tokensNewliner,
private readonly NamespaceUsesAnalyzer $namespaceUsesAnalyzer
private readonly TokensNewliner $tokensNewliner
) {
}

Expand Down Expand Up @@ -95,8 +92,6 @@ 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 */
Expand All @@ -118,7 +113,7 @@ public function fix(SplFileInfo $fileInfo, Tokens $tokens): void
continue;
}

if (! $this->isSymfonyAttribute($tokens, $openBracketPosition, $shortNameToFullName)) {
if (! $this->isSymfonyAttribute($tokens, $openBracketPosition)) {
continue;
}

Expand All @@ -135,59 +130,22 @@ public function fix(SplFileInfo $fileInfo, Tokens $tokens): void

/**
* @param Tokens<Token> $tokens
* @param array<string, string> $shortNameToFullName
*/
private function isSymfonyAttribute(Tokens $tokens, int $openBracketPosition, array $shortNameToFullName): bool
private function isSymfonyAttribute(Tokens $tokens, int $openBracketPosition): bool
{
$attributeName = $this->resolveAttributeName($tokens, $openBracketPosition);

// fully-qualified name, e.g. #[\Symfony\...\AsCommand]
if (str_starts_with($attributeName, '\\')) {
return in_array(ltrim($attributeName, '\\'), self::SYMFONY_ATTRIBUTE_CLASSES, true);
// the attribute short name is the T_STRING right before its "(", e.g. "AsCommand"
$previousPosition = $tokens->getPrevMeaningfulToken($openBracketPosition);
if ($previousPosition === null) {
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 && in_array($fullName, self::SYMFONY_ATTRIBUTE_CLASSES, true);
}

/**
* Reads the attribute name written right before its "(", e.g. "\Symfony\...\AsCommand" or "AsCommand".
*
* @param Tokens<Token> $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, T_NS_SEPARATOR])) {
break;
}

$attributeName = $token->getContent() . $attributeName;
}

return $attributeName;
}

/**
* @param Tokens<Token> $tokens
* @return array<string, string>
*/
private function resolveShortNameToFullName(Tokens $tokens): array
{
$shortNameToFullName = [];
/** @var Token $previousToken */
$previousToken = $tokens[$previousPosition];

foreach ($this->namespaceUsesAnalyzer->getDeclarationsFromTokens($tokens) as $namespaceUseAnalysis) {
$shortNameToFullName[$namespaceUseAnalysis->getShortName()] = $namespaceUseAnalysis->getFullName();
if (! $previousToken->isGivenKind(T_STRING)) {
return false;
}

return $shortNameToFullName;
return in_array($previousToken->getContent(), self::SYMFONY_ATTRIBUTE_SHORT_NAMES, true);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?php

namespace Symplify\CodingStandard\Tests\Fixer\Spacing\StandaloneLineSymfonyAttributeParamFixer\Fixture;

final class FqnAutowireAttribute
{
public function __construct(
#[\Symfony\Component\DependencyInjection\Attribute\Autowire(service: 'some.service', lazy: true)]
private readonly object $service
) {
}
}

?>
-----
<?php

namespace Symplify\CodingStandard\Tests\Fixer\Spacing\StandaloneLineSymfonyAttributeParamFixer\Fixture;

final class FqnAutowireAttribute
{
public function __construct(
#[\Symfony\Component\DependencyInjection\Attribute\Autowire(
service: 'some.service',
lazy: true
)]
private readonly object $service
) {
}
}

?>
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<?php

namespace Symplify\CodingStandard\Tests\Fixer\Spacing\StandaloneLineSymfonyAttributeParamFixer\Fixture;

final class FqnRouteAttribute
{
#[\Symfony\Component\Routing\Attribute\Route(path: '/some', name: 'some_route')]
public function __invoke()
{
}
}

?>
-----
<?php

namespace Symplify\CodingStandard\Tests\Fixer\Spacing\StandaloneLineSymfonyAttributeParamFixer\Fixture;

final class FqnRouteAttribute
{
#[\Symfony\Component\Routing\Attribute\Route(
path: '/some',
name: 'some_route'
)]
public function __invoke()
{
}
}

?>
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<?php

namespace Symplify\CodingStandard\Tests\Fixer\Spacing\StandaloneLineSymfonyAttributeParamFixer\Fixture;

use Symfony\Component\DependencyInjection\Attribute\Autowire;

final class ImportedAutowireAttribute
{
public function __construct(
#[Autowire(service: 'some.service', lazy: true)]
private readonly object $service
) {
}
}

?>
-----
<?php

namespace Symplify\CodingStandard\Tests\Fixer\Spacing\StandaloneLineSymfonyAttributeParamFixer\Fixture;

use Symfony\Component\DependencyInjection\Attribute\Autowire;

final class ImportedAutowireAttribute
{
public function __construct(
#[Autowire(
service: 'some.service',
lazy: true
)]
private readonly object $service
) {
}
}

?>
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<?php

namespace Symplify\CodingStandard\Tests\Fixer\Spacing\StandaloneLineSymfonyAttributeParamFixer\Fixture;

use Symfony\Component\Routing\Attribute\Route;

final class ImportedRouteAttribute
{
#[Route(path: '/some', name: 'some_route')]
public function __invoke()
{
}
}

?>
-----
<?php

namespace Symplify\CodingStandard\Tests\Fixer\Spacing\StandaloneLineSymfonyAttributeParamFixer\Fixture;

use Symfony\Component\Routing\Attribute\Route;

final class ImportedRouteAttribute
{
#[Route(
path: '/some',
name: 'some_route'
)]
public function __invoke()
{
}
}

?>
Loading