This repository has been archived by the owner on Dec 3, 2023. It is now read-only.
-
-
Notifications
You must be signed in to change notification settings - Fork 190
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[CodingStandard] Add DoctrineAnnotationNestedBracketsFixer (#3396)
- Loading branch information
1 parent
01ee14f
commit 16df9f4
Showing
23 changed files
with
481 additions
and
17 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
183 changes: 183 additions & 0 deletions
183
packages/coding-standard/src/Fixer/Annotation/DoctrineAnnotationNestedBracketsFixer.php
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, '{') | ||
); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
79 changes: 79 additions & 0 deletions
79
packages/coding-standard/src/TokenAnalyzer/DoctrineAnnotationElementAnalyzer.php
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} |
45 changes: 45 additions & 0 deletions
45
packages/coding-standard/src/TokenAnalyzer/DoctrineAnnotationNameResolver.php
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} |
Oops, something went wrong.