OrderedAttributesFixer.php 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310
  1. <?php
  2. declare(strict_types=1);
  3. /*
  4. * This file is part of PHP CS Fixer.
  5. *
  6. * (c) Fabien Potencier <fabien@symfony.com>
  7. * Dariusz Rumiński <dariusz.ruminski@gmail.com>
  8. *
  9. * This source file is subject to the MIT license that is bundled
  10. * with this source code in the file LICENSE.
  11. */
  12. namespace PhpCsFixer\Fixer\AttributeNotation;
  13. use PhpCsFixer\AbstractFixer;
  14. use PhpCsFixer\ConfigurationException\InvalidFixerConfigurationException;
  15. use PhpCsFixer\Fixer\ConfigurableFixerInterface;
  16. use PhpCsFixer\FixerConfiguration\FixerConfigurationResolver;
  17. use PhpCsFixer\FixerConfiguration\FixerConfigurationResolverInterface;
  18. use PhpCsFixer\FixerConfiguration\FixerOptionBuilder;
  19. use PhpCsFixer\FixerDefinition\FixerDefinition;
  20. use PhpCsFixer\FixerDefinition\FixerDefinitionInterface;
  21. use PhpCsFixer\FixerDefinition\VersionSpecification;
  22. use PhpCsFixer\FixerDefinition\VersionSpecificCodeSample;
  23. use PhpCsFixer\Tokenizer\Analyzer\Analysis\AttributeAnalysis;
  24. use PhpCsFixer\Tokenizer\Analyzer\Analysis\NamespaceAnalysis;
  25. use PhpCsFixer\Tokenizer\Analyzer\Analysis\NamespaceUseAnalysis;
  26. use PhpCsFixer\Tokenizer\Analyzer\AttributeAnalyzer;
  27. use PhpCsFixer\Tokenizer\Analyzer\NamespacesAnalyzer;
  28. use PhpCsFixer\Tokenizer\Analyzer\NamespaceUsesAnalyzer;
  29. use PhpCsFixer\Tokenizer\Token;
  30. use PhpCsFixer\Tokenizer\Tokens;
  31. use Symfony\Component\OptionsResolver\Options;
  32. /**
  33. * @author HypeMC <hypemc@gmail.com>
  34. *
  35. * @phpstan-import-type _AttributeItems from AttributeAnalysis
  36. */
  37. final class OrderedAttributesFixer extends AbstractFixer implements ConfigurableFixerInterface
  38. {
  39. public const ORDER_ALPHA = 'alpha';
  40. public const ORDER_CUSTOM = 'custom';
  41. private const SUPPORTED_SORT_ALGORITHMS = [
  42. self::ORDER_ALPHA,
  43. self::ORDER_CUSTOM,
  44. ];
  45. public function getDefinition(): FixerDefinitionInterface
  46. {
  47. return new FixerDefinition(
  48. 'Sorts attributes using the configured sort algorithm.',
  49. [
  50. new VersionSpecificCodeSample(
  51. <<<'EOL'
  52. <?php
  53. #[Foo]
  54. #[Bar(3)]
  55. #[Qux(new Bar(5))]
  56. #[Corge(a: 'test')]
  57. class Sample1 {}
  58. #[
  59. Foo,
  60. Bar(3),
  61. Qux(new Bar(5)),
  62. Corge(a: 'test'),
  63. ]
  64. class Sample2 {}
  65. EOL,
  66. new VersionSpecification(8_00_00),
  67. ),
  68. new VersionSpecificCodeSample(
  69. <<<'EOL'
  70. <?php
  71. use A\B\Foo;
  72. use A\B\Bar as BarAlias;
  73. use A\B as AB;
  74. #[Foo]
  75. #[BarAlias(3)]
  76. #[AB\Qux(new Bar(5))]
  77. #[\A\B\Corge(a: 'test')]
  78. class Sample1 {}
  79. EOL,
  80. new VersionSpecification(8_00_00),
  81. ['sort_algorithm' => self::ORDER_CUSTOM, 'order' => ['A\B\Qux', 'A\B\Bar', 'A\B\Corge']],
  82. ),
  83. ],
  84. );
  85. }
  86. /**
  87. * {@inheritdoc}
  88. *
  89. * Must run after FullyQualifiedStrictTypesFixer.
  90. */
  91. public function getPriority(): int
  92. {
  93. return 0;
  94. }
  95. public function isCandidate(Tokens $tokens): bool
  96. {
  97. return \defined('T_ATTRIBUTE') && $tokens->isTokenKindFound(T_ATTRIBUTE);
  98. }
  99. protected function createConfigurationDefinition(): FixerConfigurationResolverInterface
  100. {
  101. $fixerName = $this->getName();
  102. return new FixerConfigurationResolver([
  103. (new FixerOptionBuilder('sort_algorithm', 'How the attributes should be sorted.'))
  104. ->setAllowedValues(self::SUPPORTED_SORT_ALGORITHMS)
  105. ->setDefault(self::ORDER_ALPHA)
  106. ->setNormalizer(static function (Options $options, string $value) use ($fixerName): string {
  107. if (self::ORDER_CUSTOM === $value && [] === $options['order']) {
  108. throw new InvalidFixerConfigurationException(
  109. $fixerName,
  110. 'The custom order strategy requires providing `order` option with a list of attributes\'s FQNs.'
  111. );
  112. }
  113. return $value;
  114. })
  115. ->getOption(),
  116. (new FixerOptionBuilder('order', 'A list of FQCNs of attributes defining the desired order used when custom sorting algorithm is configured.'))
  117. ->setAllowedTypes(['string[]'])
  118. ->setDefault([])
  119. ->setNormalizer(static function (Options $options, array $value) use ($fixerName): array {
  120. if ($value !== array_unique($value)) {
  121. throw new InvalidFixerConfigurationException($fixerName, 'The list includes attributes that are not unique.');
  122. }
  123. return array_flip(array_values(
  124. array_map(static fn (string $attribute): string => ltrim($attribute, '\\'), $value),
  125. ));
  126. })
  127. ->getOption(),
  128. ]);
  129. }
  130. protected function applyFix(\SplFileInfo $file, Tokens $tokens): void
  131. {
  132. $index = 0;
  133. while (null !== $index = $tokens->getNextTokenOfKind($index, [[T_ATTRIBUTE]])) {
  134. /** @var list<array{name: string, start: int, end: int}> $elements */
  135. $elements = array_map(function (AttributeAnalysis $attributeAnalysis) use ($tokens): array {
  136. return [
  137. 'name' => $this->sortAttributes($tokens, $attributeAnalysis->getStartIndex(), $attributeAnalysis->getAttributes()),
  138. 'start' => $attributeAnalysis->getStartIndex(),
  139. 'end' => $attributeAnalysis->getEndIndex(),
  140. ];
  141. }, AttributeAnalyzer::collect($tokens, $index));
  142. $endIndex = end($elements)['end'];
  143. try {
  144. if (1 === \count($elements)) {
  145. continue;
  146. }
  147. $sortedElements = $this->sortElements($elements);
  148. if ($elements === $sortedElements) {
  149. continue;
  150. }
  151. $this->sortTokens($tokens, $index, $endIndex, $sortedElements);
  152. } finally {
  153. $index = $endIndex;
  154. }
  155. }
  156. }
  157. /**
  158. * @param _AttributeItems $attributes
  159. */
  160. private function sortAttributes(Tokens $tokens, int $index, array $attributes): string
  161. {
  162. if (1 === \count($attributes)) {
  163. return $this->getAttributeName($tokens, $attributes[0]['name'], $attributes[0]['start']);
  164. }
  165. foreach ($attributes as &$attribute) {
  166. $attribute['name'] = $this->getAttributeName($tokens, $attribute['name'], $attribute['start']);
  167. }
  168. $sortedElements = $this->sortElements($attributes);
  169. if ($attributes === $sortedElements) {
  170. return $attributes[0]['name'];
  171. }
  172. $this->sortTokens($tokens, $index + 1, end($attributes)['end'], $sortedElements, new Token(','));
  173. return $sortedElements[0]['name'];
  174. }
  175. private function getAttributeName(Tokens $tokens, string $name, int $index): string
  176. {
  177. if (self::ORDER_CUSTOM === $this->configuration['sort_algorithm']) {
  178. $name = $this->determineAttributeFullyQualifiedName($tokens, $name, $index);
  179. }
  180. return ltrim($name, '\\');
  181. }
  182. private function determineAttributeFullyQualifiedName(Tokens $tokens, string $name, int $index): string
  183. {
  184. if ('\\' === $name[0]) {
  185. return $name;
  186. }
  187. if (!$tokens[$index]->isGivenKind([T_STRING, T_NS_SEPARATOR])) {
  188. $index = $tokens->getNextTokenOfKind($index, [[T_STRING], [T_NS_SEPARATOR]]);
  189. }
  190. [$namespaceAnalysis, $namespaceUseAnalyses] = $this->collectNamespaceAnalysis($tokens, $index);
  191. $namespace = $namespaceAnalysis->getFullName();
  192. $firstTokenOfName = $tokens[$index]->getContent();
  193. $namespaceUseAnalysis = $namespaceUseAnalyses[$firstTokenOfName] ?? false;
  194. if ($namespaceUseAnalysis instanceof NamespaceUseAnalysis) {
  195. $namespace = $namespaceUseAnalysis->getFullName();
  196. if ($name === $firstTokenOfName) {
  197. return $namespace;
  198. }
  199. $name = substr(strstr($name, '\\'), 1);
  200. }
  201. return $namespace.'\\'.$name;
  202. }
  203. /**
  204. * @param list<array{name: string, start: int, end: int}> $elements
  205. *
  206. * @return list<array{name: string, start: int, end: int}>
  207. */
  208. private function sortElements(array $elements): array
  209. {
  210. usort($elements, function (array $a, array $b): int {
  211. $sortAlgorithm = $this->configuration['sort_algorithm'];
  212. if (self::ORDER_ALPHA === $sortAlgorithm) {
  213. return $a['name'] <=> $b['name'];
  214. }
  215. if (self::ORDER_CUSTOM === $sortAlgorithm) {
  216. return
  217. ($this->configuration['order'][$a['name']] ?? PHP_INT_MAX)
  218. <=>
  219. ($this->configuration['order'][$b['name']] ?? PHP_INT_MAX);
  220. }
  221. throw new \InvalidArgumentException(sprintf('Invalid sort algorithm "%s" provided.', $sortAlgorithm));
  222. });
  223. return $elements;
  224. }
  225. /**
  226. * @param list<array{name: string, start: int, end: int}> $elements
  227. */
  228. private function sortTokens(Tokens $tokens, int $startIndex, int $endIndex, array $elements, ?Token $delimiter = null): void
  229. {
  230. $replaceTokens = [];
  231. foreach ($elements as $pos => $element) {
  232. for ($i = $element['start']; $i <= $element['end']; ++$i) {
  233. $replaceTokens[] = clone $tokens[$i];
  234. }
  235. if (null !== $delimiter && $pos !== \count($elements) - 1) {
  236. $replaceTokens[] = clone $delimiter;
  237. }
  238. }
  239. $tokens->overrideRange($startIndex, $endIndex, $replaceTokens);
  240. }
  241. /**
  242. * @return array{NamespaceAnalysis, array<string, NamespaceUseAnalysis>}
  243. */
  244. private function collectNamespaceAnalysis(Tokens $tokens, int $startIndex): array
  245. {
  246. $namespaceAnalysis = (new NamespacesAnalyzer())->getNamespaceAt($tokens, $startIndex);
  247. $namespaceUseAnalyses = (new NamespaceUsesAnalyzer())->getDeclarationsInNamespace($tokens, $namespaceAnalysis);
  248. $uses = [];
  249. foreach ($namespaceUseAnalyses as $use) {
  250. if (!$use->isClass()) {
  251. continue;
  252. }
  253. $uses[$use->getShortName()] = $use;
  254. }
  255. return [$namespaceAnalysis, $uses];
  256. }
  257. }