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 @@ -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[]
*/
Expand Down Expand Up @@ -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;
}

Expand All @@ -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);
}
Expand All @@ -131,21 +145,57 @@ public function fix(SplFileInfo $fileInfo, Tokens $tokens): void
/**
* @param Tokens<Token> $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<Token> $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;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?php

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

#[\Symfony\Component\Console\Attribute\AsCommand(name: 'app:some')]
final class AsCommandSingleArgument
{
}

?>
-----
<?php

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

#[\Symfony\Component\Console\Attribute\AsCommand(
name: 'app:some'
)]
final class AsCommandSingleArgument
{
}

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

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

use Symfony\Component\Routing\Attribute\Route;

final class SkipSingleArgument
{
#[Route(path: '/some')]
public function __invoke()
{
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?php

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

use Symfony\Component\DependencyInjection\Attribute\Autowire;

final class SkipSingleArgumentWithNestedCommas
{
public function __construct(
#[Autowire(value: ['first', 'second', 'third'])]
private readonly array $values
) {
}
}
Loading