* Dariusz RumiƄski * * 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\NamespaceUseAnalysis; use PhpCsFixer\Tokenizer\Analyzer\AttributeAnalyzer; use PhpCsFixer\Tokenizer\Analyzer\FullyQualifiedNameAnalyzer; use PhpCsFixer\Tokenizer\FCT; use PhpCsFixer\Tokenizer\Token; use PhpCsFixer\Tokenizer\Tokens; use Symfony\Component\OptionsResolver\Options; /** * @phpstan-import-type _AttributeItems from AttributeAnalysis * * @phpstan-type _AutogeneratedInputConfiguration array{ * order?: list, * sort_algorithm?: 'alpha'|'custom', * } * @phpstan-type _AutogeneratedComputedConfiguration array{ * order: array, * sort_algorithm: 'alpha'|'custom', * } * * @implements ConfigurableFixerInterface<_AutogeneratedInputConfiguration, _AutogeneratedComputedConfiguration> * * @author HypeMC */ 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' 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 $tokens->isTokenKindFound(FCT::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 { $fullyQualifiedNameAnalyzer = new FullyQualifiedNameAnalyzer($tokens); $index = 0; while (null !== $index = $tokens->getNextTokenOfKind($index, [[\T_ATTRIBUTE]])) { /** @var _AttributeItems $elements */ $elements = array_map(fn (AttributeAnalysis $attributeAnalysis): array => [ 'name' => $this->sortAttributes($fullyQualifiedNameAnalyzer, $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(FullyQualifiedNameAnalyzer $fullyQualifiedNameAnalyzer, Tokens $tokens, int $index, array $attributes): string { if (1 === \count($attributes)) { return $this->getAttributeName($fullyQualifiedNameAnalyzer, $attributes[0]['name'], $attributes[0]['start']); } foreach ($attributes as &$attribute) { $attribute['name'] = $this->getAttributeName($fullyQualifiedNameAnalyzer, $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(FullyQualifiedNameAnalyzer $fullyQualifiedNameAnalyzer, string $name, int $index): string { if (self::ORDER_CUSTOM === $this->configuration['sort_algorithm']) { return $fullyQualifiedNameAnalyzer->getFullyQualifiedName($name, $index, NamespaceUseAnalysis::TYPE_CLASS); } return ltrim($name, '\\'); } /** * @param _AttributeItems $elements * * @return _AttributeItems */ 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 _AttributeItems $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); } }