Skip to content
This repository has been archived by the owner on Dec 3, 2023. It is now read-only.

Commit

Permalink
[CodingStandard] Add DoctrineAnnotationNestedBracketsFixer (#3396)
Browse files Browse the repository at this point in the history
  • Loading branch information
TomasVotruba authored Jul 10, 2021
1 parent 01ee14f commit 16df9f4
Show file tree
Hide file tree
Showing 23 changed files with 481 additions and 17 deletions.
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@
"ondram/ci-detector": "^4.1",
"phpunit/phpunit": "^9.5",
"psr/log": "^1.1",
"rector/rector": "dev-main#ecda10a",
"rector/rector": "dev-main#54dcc7b",
"symfony/doctrine-bridge": "^5.3",
"symfony/framework-bundle": "^5.3",
"symfony/security-bundle": "^5.3",
Expand Down
6 changes: 6 additions & 0 deletions ecs.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

use PhpCsFixer\Fixer\PhpUnit\PhpUnitStrictFixer;
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
use Symplify\CodingStandard\Fixer\Annotation\DoctrineAnnotationNestedBracketsFixer;
use Symplify\CodingStandard\Fixer\LineLength\LineLengthFixer;
use Symplify\EasyCodingStandard\ValueObject\Option;
use Symplify\EasyCodingStandard\ValueObject\Set\SetList;
Expand All @@ -12,6 +13,11 @@
$services = $containerConfigurator->services();
$services->set(LineLengthFixer::class);

$services->set(DoctrineAnnotationNestedBracketsFixer::class)
->call('configure', [[
DoctrineAnnotationNestedBracketsFixer::ANNOTATION_CLASSES => ['Doctrine\ORM\JoinColumns'],
]]);

$containerConfigurator->import(SetList::CLEAN_CODE);
$containerConfigurator->import(SetList::SYMPLIFY);
$containerConfigurator->import(SetList::COMMON);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ final class SimpleCallableNodeTraverser
/**
* @param Node|Node[]|null $nodes
*/
public function traverseNodesWithCallable($nodes, callable $callable): void
public function traverseNodesWithCallable(Node | array | null $nodes, callable $callable): void
{
if ($nodes === null) {
return;
Expand Down
2 changes: 2 additions & 0 deletions packages/coding-standard/config/config.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
declare(strict_types=1);

use PhpCsFixer\Tokenizer\Analyzer\FunctionsAnalyzer;
use PhpCsFixer\Tokenizer\Analyzer\NamespaceUsesAnalyzer;
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
use Symplify\EasyCodingStandard\Caching\ChangedFilesDetector;
use Symplify\PackageBuilder\Reflection\PrivatesAccessor;
Expand All @@ -24,6 +25,7 @@
__DIR__ . '/../src/ValueObject',
]);

$services->set(NamespaceUsesAnalyzer::class);
$services->set(FunctionsAnalyzer::class);
$services->set(PrivatesAccessor::class);

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
<?php

declare(strict_types=1);

namespace Symplify\CodingStandard\Fixer\Annotation;

use Doctrine\Common\Annotations\DocLexer;
use PhpCsFixer\Doctrine\Annotation\Token as DoctrineAnnotationToken;
use PhpCsFixer\Doctrine\Annotation\Tokens as DoctrineAnnotationTokens;
use PhpCsFixer\FixerDefinition\FixerDefinition;
use PhpCsFixer\FixerDefinition\FixerDefinitionInterface;
use PhpCsFixer\Tokenizer\Analyzer\NamespaceUsesAnalyzer;
use PhpCsFixer\Tokenizer\Token;
use PhpCsFixer\Tokenizer\Tokens;
use SplFileInfo;
use Symplify\CodingStandard\Fixer\AbstractSymplifyFixer;
use Symplify\CodingStandard\TokenAnalyzer\DoctrineAnnotationElementAnalyzer;
use Symplify\CodingStandard\TokenAnalyzer\DoctrineAnnotationNameResolver;
use Symplify\RuleDocGenerator\Contract\ConfigurableRuleInterface;
use Symplify\RuleDocGenerator\Contract\DocumentedRuleInterface;
use Symplify\RuleDocGenerator\ValueObject\CodeSample\ConfiguredCodeSample;
use Symplify\RuleDocGenerator\ValueObject\RuleDefinition;
use Webmozart\Assert\Assert;

final class DoctrineAnnotationNestedBracketsFixer extends AbstractSymplifyFixer implements ConfigurableRuleInterface, DocumentedRuleInterface
{
/**
* @var string
*/
public const ANNOTATION_CLASSES = 'annotation_classes';

/**
* @var string
*/
private const ERROR_MESSAGE = 'Adds nested curly brackets to defined annotations, see https://github.com/doctrine/annotations/issues/418';

/**
* @var string[]
*/
private array $annotationClasses = [];

public function __construct(
private DoctrineAnnotationElementAnalyzer $doctrineAnnotationElementAnalyzer,
private DoctrineAnnotationNameResolver $doctrineAnnotationNameResolver,
private NamespaceUsesAnalyzer $namespaceUsesAnalyzer
) {
}

public function getDefinition(): FixerDefinitionInterface
{
return new FixerDefinition(self::ERROR_MESSAGE, []);
}

public function getRuleDefinition(): RuleDefinition
{
return new RuleDefinition(self::ERROR_MESSAGE, [
new ConfiguredCodeSample(
<<<'CODE_SAMPLE'
/**
* @MainAnnotation(
* @NestedAnnotation(),
* @NestedAnnotation(),
* )
*/
CODE_SAMPLE
,
<<<'CODE_SAMPLE'
/**
* @MainAnnotation({
* @NestedAnnotation(),
* @NestedAnnotation(),
* })
*/
CODE_SAMPLE
,
[
self::ANNOTATION_CLASSES => ['MainAnnotation'],
]
),
]);
}

/**
* @param array<string, string[]> $configuration
*/
public function configure(array $configuration): void
{
$annotationsClasses = $configuration[self::ANNOTATION_CLASSES] ?? [];
Assert::isArray($annotationsClasses);
Assert::allString($annotationsClasses);

$this->annotationClasses = $annotationsClasses;
}

/**
* @param Tokens<Token> $tokens
*/
public function isCandidate(Tokens $tokens): bool
{
return $tokens->isTokenKindFound(T_DOC_COMMENT);
}

/**
* @param Tokens<Token> $tokens
*/
public function fix(SplFileInfo $fileInfo, Tokens $tokens): void
{
$useDeclarations = $this->namespaceUsesAnalyzer->getDeclarationsFromTokens($tokens);

// fetch indexes one time, this is safe as we never add or remove a token during fixing

/** @var Token[] $docCommentTokens */
$docCommentTokens = $tokens->findGivenKind(T_DOC_COMMENT);
foreach ($docCommentTokens as $index => $docCommentToken) {
if (! $this->doctrineAnnotationElementAnalyzer->detect($tokens, $index)) {
continue;
}

$doctrineAnnotationTokens = DoctrineAnnotationTokens::createFromDocComment($docCommentToken, []);
$this->fixAnnotations($doctrineAnnotationTokens, $useDeclarations);

$tokens[$index] = new Token([T_DOC_COMMENT, $doctrineAnnotationTokens->getCode()]);
}
}

/**
* @param DoctrineAnnotationTokens<DoctrineAnnotationToken> $doctrineAnnotationTokens
*/
private function fixAnnotations(DoctrineAnnotationTokens $doctrineAnnotationTokens, $useDeclarations): void
{
foreach ($doctrineAnnotationTokens as $index => $token) {
$isAtToken = $doctrineAnnotationTokens[$index]->isType(DocLexer::T_AT);
if (! $isAtToken) {
continue;
}

$annotationName = $this->doctrineAnnotationNameResolver->resolveName(
$doctrineAnnotationTokens,
$index,
$useDeclarations
);
if ($annotationName === null) {
continue;
}

if (! in_array($annotationName, $this->annotationClasses, true)) {
continue;
}

$closingBraceIndex = $doctrineAnnotationTokens->getAnnotationEnd($index);
if ($closingBraceIndex === null) {
continue;
}

$braceIndex = $doctrineAnnotationTokens->getNextMeaningfulToken($index + 1);
if ($braceIndex === null) {
continue;
}

/** @var DoctrineAnnotationToken $braceToken */
$braceToken = $doctrineAnnotationTokens[$braceIndex];
if (! $this->doctrineAnnotationElementAnalyzer->isOpeningBracketFollowedByAnnotation(
$braceToken,
$doctrineAnnotationTokens,
$braceIndex
)) {
continue;
}

// add closing brace
$doctrineAnnotationTokens->insertAt(
$closingBraceIndex,
new DoctrineAnnotationToken(DocLexer::T_OPEN_CURLY_BRACES, '}')
);

// add opening brace
$doctrineAnnotationTokens->insertAt(
$braceIndex + 1,
new DoctrineAnnotationToken(DocLexer::T_OPEN_CURLY_BRACES, '{')
);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@
use Symplify\RuleDocGenerator\ValueObject\RuleDefinition;

/**
* @see https://softwareengineering.stackexchange.com/a/394288/148956
* @deprecated This rule is seriously buggy. Don't use it. We're working on a better replacement
* @see https://github.com/symplify/symplify/issues/3395
*
* @see \Symplify\CodingStandard\Tests\Fixer\Commenting\RemoveCommentedCodeFixer\RemoveCommentedCodeFixerTest
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,9 @@ public function fix(SplFileInfo $file, Tokens $tokens): void
}
}

/**
* @param array<string, int> $configuration
*/
public function configure(array $configuration): void
{
$this->lineLength = $configuration[self::LINE_LENGTH] ?? self::DEFAULT_LINE_LENGHT;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
<?php

declare(strict_types=1);

namespace Symplify\CodingStandard\TokenAnalyzer;

use Doctrine\Common\Annotations\DocLexer;
use PhpCsFixer\Doctrine\Annotation\Token;
use PhpCsFixer\Doctrine\Annotation\Tokens as DoctrineAnnotationTokens;
use PhpCsFixer\Tokenizer\CT;
use PhpCsFixer\Tokenizer\Tokens;
use PhpCsFixer\Tokenizer\TokensAnalyzer;

/**
* Copied from \PhpCsFixer\AbstractDoctrineAnnotationFixer::nextElementAcceptsDoctrineAnnotations() so it can be used as
* a normal service
*/
final class DoctrineAnnotationElementAnalyzer
{
/**
* @param Tokens<\PhpCsFixer\Tokenizer\Token> $tokens
*/
public function detect(Tokens $tokens, int $index): bool
{
$tokensAnalyzer = new TokensAnalyzer($tokens);
$classyElements = $tokensAnalyzer->getClassyElements();

do {
$index = $tokens->getNextMeaningfulToken($index);

if ($index === null) {
return false;
}
} while ($tokens[$index]->isGivenKind([T_ABSTRACT, T_FINAL]));

if ($tokens[$index]->isClassy()) {
return true;
}

while ($tokens[$index]->isGivenKind(
[T_PUBLIC, T_PROTECTED, T_PRIVATE, T_FINAL, T_ABSTRACT, T_NS_SEPARATOR, T_STRING, CT::T_NULLABLE_TYPE]
)) {
$index = $tokens->getNextMeaningfulToken($index);
if (! is_int($index)) {
return false;
}
}

return isset($classyElements[$index]);
}

/**
* We look for "(@SomeAnnotation"
*
* @param DoctrineAnnotationTokens<Token> $doctrineAnnotationTokens
*/
public function isOpeningBracketFollowedByAnnotation(
Token $token,
DoctrineAnnotationTokens $doctrineAnnotationTokens,
int $braceIndex
): bool {
// should be "("
$isNextOpenParenthesis = $token->isType(DocLexer::T_OPEN_PARENTHESIS);
if (! $isNextOpenParenthesis) {
return false;
}

$nextTokenIndex = $doctrineAnnotationTokens->getNextMeaningfulToken($braceIndex);
if ($nextTokenIndex === null) {
return false;
}

/** @var Token $nextToken */
$nextToken = $doctrineAnnotationTokens[$nextTokenIndex];

// next token must be nested annotation, we don't care otherwise
return $nextToken->isType(DocLexer::T_AT);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<?php

declare(strict_types=1);

namespace Symplify\CodingStandard\TokenAnalyzer;

use Doctrine\Common\Annotations\DocLexer;
use PhpCsFixer\Doctrine\Annotation\Token;
use PhpCsFixer\Doctrine\Annotation\Tokens;
use PhpCsFixer\Tokenizer\Analyzer\Analysis\NamespaceUseAnalysis;

final class DoctrineAnnotationNameResolver
{
/**
* @param Tokens<Token> $tokens
* @param NamespaceUseAnalysis[] $namespaceUseAnalyses
*/
public function resolveName(Tokens $tokens, int $index, array $namespaceUseAnalyses): ?string
{
$openParenthesisPosition = $tokens->getNextTokenOfType(DocLexer::T_OPEN_PARENTHESIS, $index);
if ($openParenthesisPosition === null) {
return null;
}

$annotationShortName = '';

for ($i = $index + 1; $i < $openParenthesisPosition; ++$i) {
/** @var Token $currentToken */
$currentToken = $tokens[$i];
$annotationShortName .= $currentToken->getContent();
}

if ($annotationShortName === '') {
return null;
}

foreach ($namespaceUseAnalyses as $namespaceUseAnalysis) {
if ($namespaceUseAnalysis->getShortName() === $annotationShortName) {
return $namespaceUseAnalysis->getFullName();
}
}

return $annotationShortName;
}
}
Loading

0 comments on commit 16df9f4

Please sign in to comment.