FixerFactory.php 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241
  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;
  13. use PhpCsFixer\ConfigurationException\InvalidFixerConfigurationException;
  14. use PhpCsFixer\Fixer\ConfigurableFixerInterface;
  15. use PhpCsFixer\Fixer\FixerInterface;
  16. use PhpCsFixer\Fixer\WhitespacesAwareFixerInterface;
  17. use PhpCsFixer\RuleSet\RuleSetInterface;
  18. use Symfony\Component\Finder\Finder as SymfonyFinder;
  19. use Symfony\Component\Finder\SplFileInfo;
  20. /**
  21. * Class provides a way to create a group of fixers.
  22. *
  23. * Fixers may be registered (made the factory aware of them) by
  24. * registering a custom fixer and default, built in fixers.
  25. * Then, one can attach Config instance to fixer instances.
  26. *
  27. * Finally factory creates a ready to use group of fixers.
  28. *
  29. * @author Dariusz Rumiński <dariusz.ruminski@gmail.com>
  30. *
  31. * @internal
  32. */
  33. final class FixerFactory
  34. {
  35. private FixerNameValidator $nameValidator;
  36. /**
  37. * @var list<FixerInterface>
  38. */
  39. private array $fixers = [];
  40. /**
  41. * @var array<string, FixerInterface>
  42. */
  43. private array $fixersByName = [];
  44. public function __construct()
  45. {
  46. $this->nameValidator = new FixerNameValidator();
  47. }
  48. public function setWhitespacesConfig(WhitespacesFixerConfig $config): self
  49. {
  50. foreach ($this->fixers as $fixer) {
  51. if ($fixer instanceof WhitespacesAwareFixerInterface) {
  52. $fixer->setWhitespacesConfig($config);
  53. }
  54. }
  55. return $this;
  56. }
  57. /**
  58. * @return list<FixerInterface>
  59. */
  60. public function getFixers(): array
  61. {
  62. $this->fixers = Utils::sortFixers($this->fixers);
  63. return $this->fixers;
  64. }
  65. /**
  66. * @return $this
  67. */
  68. public function registerBuiltInFixers(): self
  69. {
  70. static $builtInFixers = null;
  71. if (null === $builtInFixers) {
  72. /** @var list<class-string<FixerInterface>> */
  73. $builtInFixers = [];
  74. /** @var SplFileInfo $file */
  75. foreach (SymfonyFinder::create()->files()->in(__DIR__.'/Fixer')->name('*Fixer.php')->depth(1) as $file) {
  76. $relativeNamespace = $file->getRelativePath();
  77. $fixerClass = 'PhpCsFixer\\Fixer\\'.('' !== $relativeNamespace ? $relativeNamespace.'\\' : '').$file->getBasename('.php');
  78. $builtInFixers[] = $fixerClass;
  79. }
  80. }
  81. foreach ($builtInFixers as $class) {
  82. /** @var FixerInterface */
  83. $fixer = new $class();
  84. $this->registerFixer($fixer, false);
  85. }
  86. return $this;
  87. }
  88. /**
  89. * @param FixerInterface[] $fixers
  90. *
  91. * @return $this
  92. */
  93. public function registerCustomFixers(iterable $fixers): self
  94. {
  95. foreach ($fixers as $fixer) {
  96. $this->registerFixer($fixer, true);
  97. }
  98. return $this;
  99. }
  100. /**
  101. * @return $this
  102. */
  103. public function registerFixer(FixerInterface $fixer, bool $isCustom): self
  104. {
  105. $name = $fixer->getName();
  106. if (isset($this->fixersByName[$name])) {
  107. throw new \UnexpectedValueException(sprintf('Fixer named "%s" is already registered.', $name));
  108. }
  109. if (!$this->nameValidator->isValid($name, $isCustom)) {
  110. throw new \UnexpectedValueException(sprintf('Fixer named "%s" has invalid name.', $name));
  111. }
  112. $this->fixers[] = $fixer;
  113. $this->fixersByName[$name] = $fixer;
  114. return $this;
  115. }
  116. /**
  117. * Apply RuleSet on fixers to filter out all unwanted fixers.
  118. *
  119. * @return $this
  120. */
  121. public function useRuleSet(RuleSetInterface $ruleSet): self
  122. {
  123. $fixers = [];
  124. $fixersByName = [];
  125. $fixerConflicts = [];
  126. $fixerNames = array_keys($ruleSet->getRules());
  127. foreach ($fixerNames as $name) {
  128. if (!\array_key_exists($name, $this->fixersByName)) {
  129. throw new \UnexpectedValueException(sprintf('Rule "%s" does not exist.', $name));
  130. }
  131. $fixer = $this->fixersByName[$name];
  132. $config = $ruleSet->getRuleConfiguration($name);
  133. if (null !== $config) {
  134. if ($fixer instanceof ConfigurableFixerInterface) {
  135. if (\count($config) < 1) {
  136. throw new InvalidFixerConfigurationException($fixer->getName(), 'Configuration must be an array and may not be empty.');
  137. }
  138. $fixer->configure($config);
  139. } else {
  140. throw new InvalidFixerConfigurationException($fixer->getName(), 'Is not configurable.');
  141. }
  142. }
  143. $fixers[] = $fixer;
  144. $fixersByName[$name] = $fixer;
  145. $conflicts = array_intersect($this->getFixersConflicts($fixer), $fixerNames);
  146. if (\count($conflicts) > 0) {
  147. $fixerConflicts[$name] = $conflicts;
  148. }
  149. }
  150. if (\count($fixerConflicts) > 0) {
  151. throw new \UnexpectedValueException($this->generateConflictMessage($fixerConflicts));
  152. }
  153. $this->fixers = $fixers;
  154. $this->fixersByName = $fixersByName;
  155. return $this;
  156. }
  157. /**
  158. * Check if fixer exists.
  159. */
  160. public function hasRule(string $name): bool
  161. {
  162. return isset($this->fixersByName[$name]);
  163. }
  164. /**
  165. * @return list<string>
  166. */
  167. private function getFixersConflicts(FixerInterface $fixer): array
  168. {
  169. static $conflictMap = [
  170. 'blank_lines_before_namespace' => [
  171. 'no_blank_lines_before_namespace',
  172. 'single_blank_line_before_namespace',
  173. ],
  174. 'no_blank_lines_before_namespace' => ['single_blank_line_before_namespace'],
  175. 'single_import_per_statement' => ['group_import'],
  176. ];
  177. $fixerName = $fixer->getName();
  178. return \array_key_exists($fixerName, $conflictMap) ? $conflictMap[$fixerName] : [];
  179. }
  180. /**
  181. * @param array<string, string[]> $fixerConflicts
  182. */
  183. private function generateConflictMessage(array $fixerConflicts): string
  184. {
  185. $message = 'Rule contains conflicting fixers:';
  186. $report = [];
  187. foreach ($fixerConflicts as $fixer => $fixers) {
  188. // filter mutual conflicts
  189. $report[$fixer] = array_filter(
  190. $fixers,
  191. static fn (string $candidate): bool => !\array_key_exists($candidate, $report) || !\in_array($fixer, $report[$candidate], true)
  192. );
  193. if (\count($report[$fixer]) > 0) {
  194. $message .= sprintf("\n- \"%s\" with %s", $fixer, Utils::naturalLanguageJoin($report[$fixer]));
  195. }
  196. }
  197. return $message;
  198. }
  199. }