123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325 |
- <?php
- declare(strict_types=1);
- /*
- * This file is part of PHP CS Fixer.
- *
- * (c) Fabien Potencier <fabien@symfony.com>
- * Dariusz Rumiński <dariusz.ruminski@gmail.com>
- *
- * This source file is subject to the MIT license that is bundled
- * with this source code in the file LICENSE.
- */
- namespace PhpCsFixer\Fixer\AttributeNotation;
- use PhpCsFixer\AbstractFixer;
- use PhpCsFixer\ConfigurationException\InvalidFixerConfigurationException;
- use PhpCsFixer\Fixer\ConfigurableFixerInterface;
- use PhpCsFixer\Fixer\ConfigurableFixerTrait;
- use PhpCsFixer\FixerConfiguration\FixerConfigurationResolver;
- use PhpCsFixer\FixerConfiguration\FixerConfigurationResolverInterface;
- use PhpCsFixer\FixerConfiguration\FixerOptionBuilder;
- use PhpCsFixer\FixerDefinition\FixerDefinition;
- use PhpCsFixer\FixerDefinition\FixerDefinitionInterface;
- use PhpCsFixer\FixerDefinition\VersionSpecification;
- use PhpCsFixer\FixerDefinition\VersionSpecificCodeSample;
- use PhpCsFixer\Tokenizer\Analyzer\Analysis\AttributeAnalysis;
- use PhpCsFixer\Tokenizer\Analyzer\Analysis\NamespaceAnalysis;
- use PhpCsFixer\Tokenizer\Analyzer\Analysis\NamespaceUseAnalysis;
- use PhpCsFixer\Tokenizer\Analyzer\AttributeAnalyzer;
- use PhpCsFixer\Tokenizer\Analyzer\NamespacesAnalyzer;
- use PhpCsFixer\Tokenizer\Analyzer\NamespaceUsesAnalyzer;
- use PhpCsFixer\Tokenizer\Token;
- use PhpCsFixer\Tokenizer\Tokens;
- use Symfony\Component\OptionsResolver\Options;
- /**
- * @author HypeMC <hypemc@gmail.com>
- *
- * @phpstan-import-type _AttributeItems from AttributeAnalysis
- *
- * @implements ConfigurableFixerInterface<_AutogeneratedInputConfiguration, _AutogeneratedComputedConfiguration>
- *
- * @phpstan-type _AutogeneratedInputConfiguration array{
- * order?: list<string>,
- * sort_algorithm?: 'alpha'|'custom'
- * }
- * @phpstan-type _AutogeneratedComputedConfiguration array{
- * order: array<string, int>,
- * sort_algorithm: 'alpha'|'custom'
- * }
- */
- final class OrderedAttributesFixer extends AbstractFixer implements ConfigurableFixerInterface
- {
- /** @use ConfigurableFixerTrait<_AutogeneratedInputConfiguration, _AutogeneratedComputedConfiguration> */
- use ConfigurableFixerTrait;
- public const ORDER_ALPHA = 'alpha';
- public const ORDER_CUSTOM = 'custom';
- private const SUPPORTED_SORT_ALGORITHMS = [
- self::ORDER_ALPHA,
- self::ORDER_CUSTOM,
- ];
- public function getDefinition(): FixerDefinitionInterface
- {
- return new FixerDefinition(
- 'Sorts attributes using the configured sort algorithm.',
- [
- new VersionSpecificCodeSample(
- <<<'EOL'
- <?php
- #[Foo]
- #[Bar(3)]
- #[Qux(new Bar(5))]
- #[Corge(a: 'test')]
- class Sample1 {}
- #[
- Foo,
- Bar(3),
- Qux(new Bar(5)),
- Corge(a: 'test'),
- ]
- class Sample2 {}
- EOL,
- new VersionSpecification(8_00_00),
- ),
- new VersionSpecificCodeSample(
- <<<'EOL'
- <?php
- use A\B\Foo;
- use A\B\Bar as BarAlias;
- use A\B as AB;
- #[Foo]
- #[BarAlias(3)]
- #[AB\Qux(new Bar(5))]
- #[\A\B\Corge(a: 'test')]
- class Sample1 {}
- EOL,
- new VersionSpecification(8_00_00),
- ['sort_algorithm' => self::ORDER_CUSTOM, 'order' => ['A\B\Qux', 'A\B\Bar', 'A\B\Corge']],
- ),
- ],
- );
- }
- /**
- * {@inheritdoc}
- *
- * Must run after FullyQualifiedStrictTypesFixer.
- */
- public function getPriority(): int
- {
- return 0;
- }
- public function isCandidate(Tokens $tokens): bool
- {
- return \defined('T_ATTRIBUTE') && $tokens->isTokenKindFound(T_ATTRIBUTE);
- }
- protected function createConfigurationDefinition(): FixerConfigurationResolverInterface
- {
- $fixerName = $this->getName();
- return new FixerConfigurationResolver([
- (new FixerOptionBuilder('sort_algorithm', 'How the attributes should be sorted.'))
- ->setAllowedValues(self::SUPPORTED_SORT_ALGORITHMS)
- ->setDefault(self::ORDER_ALPHA)
- ->setNormalizer(static function (Options $options, string $value) use ($fixerName): string {
- if (self::ORDER_CUSTOM === $value && [] === $options['order']) {
- throw new InvalidFixerConfigurationException(
- $fixerName,
- 'The custom order strategy requires providing `order` option with a list of attributes\'s FQNs.'
- );
- }
- return $value;
- })
- ->getOption(),
- (new FixerOptionBuilder('order', 'A list of FQCNs of attributes defining the desired order used when custom sorting algorithm is configured.'))
- ->setAllowedTypes(['string[]'])
- ->setDefault([])
- ->setNormalizer(static function (Options $options, array $value) use ($fixerName): array {
- if ($value !== array_unique($value)) {
- throw new InvalidFixerConfigurationException($fixerName, 'The list includes attributes that are not unique.');
- }
- return array_flip(array_values(
- array_map(static fn (string $attribute): string => ltrim($attribute, '\\'), $value),
- ));
- })
- ->getOption(),
- ]);
- }
- protected function applyFix(\SplFileInfo $file, Tokens $tokens): void
- {
- $index = 0;
- while (null !== $index = $tokens->getNextTokenOfKind($index, [[T_ATTRIBUTE]])) {
- /** @var list<array{name: string, start: int, end: int}> $elements */
- $elements = array_map(function (AttributeAnalysis $attributeAnalysis) use ($tokens): array {
- return [
- 'name' => $this->sortAttributes($tokens, $attributeAnalysis->getStartIndex(), $attributeAnalysis->getAttributes()),
- 'start' => $attributeAnalysis->getStartIndex(),
- 'end' => $attributeAnalysis->getEndIndex(),
- ];
- }, AttributeAnalyzer::collect($tokens, $index));
- $endIndex = end($elements)['end'];
- try {
- if (1 === \count($elements)) {
- continue;
- }
- $sortedElements = $this->sortElements($elements);
- if ($elements === $sortedElements) {
- continue;
- }
- $this->sortTokens($tokens, $index, $endIndex, $sortedElements);
- } finally {
- $index = $endIndex;
- }
- }
- }
- /**
- * @param _AttributeItems $attributes
- */
- private function sortAttributes(Tokens $tokens, int $index, array $attributes): string
- {
- if (1 === \count($attributes)) {
- return $this->getAttributeName($tokens, $attributes[0]['name'], $attributes[0]['start']);
- }
- foreach ($attributes as &$attribute) {
- $attribute['name'] = $this->getAttributeName($tokens, $attribute['name'], $attribute['start']);
- }
- $sortedElements = $this->sortElements($attributes);
- if ($attributes === $sortedElements) {
- return $attributes[0]['name'];
- }
- $this->sortTokens($tokens, $index + 1, end($attributes)['end'], $sortedElements, new Token(','));
- return $sortedElements[0]['name'];
- }
- private function getAttributeName(Tokens $tokens, string $name, int $index): string
- {
- if (self::ORDER_CUSTOM === $this->configuration['sort_algorithm']) {
- $name = $this->determineAttributeFullyQualifiedName($tokens, $name, $index);
- }
- return ltrim($name, '\\');
- }
- private function determineAttributeFullyQualifiedName(Tokens $tokens, string $name, int $index): string
- {
- if ('\\' === $name[0]) {
- return $name;
- }
- if (!$tokens[$index]->isGivenKind([T_STRING, T_NS_SEPARATOR])) {
- $index = $tokens->getNextTokenOfKind($index, [[T_STRING], [T_NS_SEPARATOR]]);
- }
- [$namespaceAnalysis, $namespaceUseAnalyses] = $this->collectNamespaceAnalysis($tokens, $index);
- $namespace = $namespaceAnalysis->getFullName();
- $firstTokenOfName = $tokens[$index]->getContent();
- $namespaceUseAnalysis = $namespaceUseAnalyses[$firstTokenOfName] ?? false;
- if ($namespaceUseAnalysis instanceof NamespaceUseAnalysis) {
- $namespace = $namespaceUseAnalysis->getFullName();
- if ($name === $firstTokenOfName) {
- return $namespace;
- }
- $name = substr(strstr($name, '\\'), 1);
- }
- return $namespace.'\\'.$name;
- }
- /**
- * @param list<array{name: string, start: int, end: int}> $elements
- *
- * @return list<array{name: string, start: int, end: int}>
- */
- private function sortElements(array $elements): array
- {
- usort($elements, function (array $a, array $b): int {
- $sortAlgorithm = $this->configuration['sort_algorithm'];
- if (self::ORDER_ALPHA === $sortAlgorithm) {
- return $a['name'] <=> $b['name'];
- }
- if (self::ORDER_CUSTOM === $sortAlgorithm) {
- return
- ($this->configuration['order'][$a['name']] ?? PHP_INT_MAX)
- <=>
- ($this->configuration['order'][$b['name']] ?? PHP_INT_MAX);
- }
- throw new \InvalidArgumentException(\sprintf('Invalid sort algorithm "%s" provided.', $sortAlgorithm));
- });
- return $elements;
- }
- /**
- * @param list<array{name: string, start: int, end: int}> $elements
- */
- private function sortTokens(Tokens $tokens, int $startIndex, int $endIndex, array $elements, ?Token $delimiter = null): void
- {
- $replaceTokens = [];
- foreach ($elements as $pos => $element) {
- for ($i = $element['start']; $i <= $element['end']; ++$i) {
- $replaceTokens[] = clone $tokens[$i];
- }
- if (null !== $delimiter && $pos !== \count($elements) - 1) {
- $replaceTokens[] = clone $delimiter;
- }
- }
- $tokens->overrideRange($startIndex, $endIndex, $replaceTokens);
- }
- /**
- * @return array{NamespaceAnalysis, array<string, NamespaceUseAnalysis>}
- */
- private function collectNamespaceAnalysis(Tokens $tokens, int $startIndex): array
- {
- $namespaceAnalysis = (new NamespacesAnalyzer())->getNamespaceAt($tokens, $startIndex);
- $namespaceUseAnalyses = (new NamespaceUsesAnalyzer())->getDeclarationsInNamespace($tokens, $namespaceAnalysis);
- $uses = [];
- foreach ($namespaceUseAnalyses as $use) {
- if (!$use->isClass()) {
- continue;
- }
- $uses[$use->getShortName()] = $use;
- }
- return [$namespaceAnalysis, $uses];
- }
- }
|