FixerFactory.php 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282
  1. <?php
  2. /*
  3. * This file is part of PHP CS Fixer.
  4. *
  5. * (c) Fabien Potencier <fabien@symfony.com>
  6. * Dariusz Rumiński <dariusz.ruminski@gmail.com>
  7. *
  8. * This source file is subject to the MIT license that is bundled
  9. * with this source code in the file LICENSE.
  10. */
  11. namespace PhpCsFixer;
  12. use PhpCsFixer\ConfigurationException\InvalidFixerConfigurationException;
  13. use PhpCsFixer\Fixer\ConfigurableFixerInterface;
  14. use PhpCsFixer\Fixer\FixerInterface;
  15. use PhpCsFixer\Fixer\WhitespacesAwareFixerInterface;
  16. use Symfony\Component\Finder\Finder as SymfonyFinder;
  17. /**
  18. * Class provides a way to create a group of fixers.
  19. *
  20. * Fixers may be registered (made the factory aware of them) by
  21. * registering a custom fixer and default, built in fixers.
  22. * Then, one can attach Config instance to fixer instances.
  23. *
  24. * Finally factory creates a ready to use group of fixers.
  25. *
  26. * @author Dariusz Rumiński <dariusz.ruminski@gmail.com>
  27. *
  28. * @internal
  29. */
  30. final class FixerFactory
  31. {
  32. /**
  33. * @var FixerNameValidator
  34. */
  35. private $nameValidator;
  36. /**
  37. * @var FixerInterface[]
  38. */
  39. private $fixers = [];
  40. /**
  41. * @var FixerInterface[] Associative array of fixers with names as keys
  42. */
  43. private $fixersByName = [];
  44. public function __construct()
  45. {
  46. $this->nameValidator = new FixerNameValidator();
  47. }
  48. /**
  49. * Create instance.
  50. *
  51. * @return FixerFactory
  52. */
  53. public static function create()
  54. {
  55. return new self();
  56. }
  57. public function setWhitespacesConfig(WhitespacesFixerConfig $config)
  58. {
  59. foreach ($this->fixers as $fixer) {
  60. if ($fixer instanceof WhitespacesAwareFixerInterface) {
  61. $fixer->setWhitespacesConfig($config);
  62. }
  63. }
  64. return $this;
  65. }
  66. /**
  67. * @return FixerInterface[]
  68. */
  69. public function getFixers()
  70. {
  71. $this->sortFixers();
  72. return $this->fixers;
  73. }
  74. /**
  75. * @return $this
  76. */
  77. public function registerBuiltInFixers()
  78. {
  79. static $builtInFixers = null;
  80. if (null === $builtInFixers) {
  81. $builtInFixers = [];
  82. foreach (SymfonyFinder::create()->files()->in(__DIR__.'/Fixer') as $file) {
  83. $relativeNamespace = $file->getRelativePath();
  84. $fixerClass = 'PhpCsFixer\\Fixer\\'.($relativeNamespace ? $relativeNamespace.'\\' : '').$file->getBasename('.php');
  85. if ('Fixer' === substr($fixerClass, -5)) {
  86. $builtInFixers[] = $fixerClass;
  87. }
  88. }
  89. }
  90. foreach ($builtInFixers as $class) {
  91. $this->registerFixer(new $class(), false);
  92. }
  93. return $this;
  94. }
  95. /**
  96. * @param FixerInterface[] $fixers
  97. *
  98. * @return $this
  99. */
  100. public function registerCustomFixers(array $fixers)
  101. {
  102. foreach ($fixers as $fixer) {
  103. $this->registerFixer($fixer, true);
  104. }
  105. return $this;
  106. }
  107. /**
  108. * @param FixerInterface $fixer
  109. * @param bool $isCustom
  110. *
  111. * @return $this
  112. */
  113. public function registerFixer(FixerInterface $fixer, $isCustom)
  114. {
  115. $name = $fixer->getName();
  116. if (isset($this->fixersByName[$name])) {
  117. throw new \UnexpectedValueException(sprintf('Fixer named "%s" is already registered.', $name));
  118. }
  119. if (!$this->nameValidator->isValid($name, $isCustom)) {
  120. throw new \UnexpectedValueException(sprintf('Fixer named "%s" has invalid name.', $name));
  121. }
  122. $this->fixers[] = $fixer;
  123. $this->fixersByName[$name] = $fixer;
  124. return $this;
  125. }
  126. /**
  127. * Apply RuleSet on fixers to filter out all unwanted fixers.
  128. *
  129. * @param RuleSetInterface $ruleSet
  130. *
  131. * @return $this
  132. */
  133. public function useRuleSet(RuleSetInterface $ruleSet)
  134. {
  135. $fixers = [];
  136. $fixersByName = [];
  137. $fixerConflicts = [];
  138. $fixerNames = array_keys($ruleSet->getRules());
  139. foreach ($fixerNames as $name) {
  140. if (!array_key_exists($name, $this->fixersByName)) {
  141. throw new \UnexpectedValueException(sprintf('Rule "%s" does not exist.', $name));
  142. }
  143. $fixer = $this->fixersByName[$name];
  144. $config = $ruleSet->getRuleConfiguration($name);
  145. if (null !== $config) {
  146. if ($fixer instanceof ConfigurableFixerInterface) {
  147. if (!is_array($config) || !count($config)) {
  148. throw new InvalidFixerConfigurationException($fixer->getName(), 'Configuration must be an array and may not be empty.');
  149. }
  150. $fixer->configure($config);
  151. } else {
  152. throw new InvalidFixerConfigurationException($fixer->getName(), 'Is not configurable.');
  153. }
  154. }
  155. $fixers[] = $fixer;
  156. $fixersByName[$name] = $fixer;
  157. $conflicts = array_intersect($this->getFixersConflicts($fixer), $fixerNames);
  158. if (count($conflicts) > 0) {
  159. $fixerConflicts[$name] = $conflicts;
  160. }
  161. }
  162. if (count($fixerConflicts) > 0) {
  163. throw new \UnexpectedValueException($this->generateConflictMessage($fixerConflicts));
  164. }
  165. $this->fixers = $fixers;
  166. $this->fixersByName = $fixersByName;
  167. return $this;
  168. }
  169. /**
  170. * Check if fixer exists.
  171. *
  172. * @param string $name
  173. *
  174. * @return bool
  175. */
  176. public function hasRule($name)
  177. {
  178. return isset($this->fixersByName[$name]);
  179. }
  180. /**
  181. * Sort fixers by their priorities.
  182. *
  183. * @return $this
  184. */
  185. private function sortFixers()
  186. {
  187. // Schwartzian transform is used to improve the efficiency and avoid
  188. // `usort(): Array was modified by the user comparison function` warning for mocked objects.
  189. $data = array_map(function (FixerInterface $fixer) {
  190. return [$fixer, $fixer->getPriority()];
  191. }, $this->fixers);
  192. usort($data, function (array $a, array $b) {
  193. return Utils::cmpInt($b[1], $a[1]);
  194. });
  195. $this->fixers = array_map(function (array $item) {
  196. return $item[0];
  197. }, $data);
  198. return $this;
  199. }
  200. /**
  201. * @param FixerInterface $fixer
  202. *
  203. * @return string[]|null
  204. */
  205. private function getFixersConflicts(FixerInterface $fixer)
  206. {
  207. static $conflictMap = [
  208. 'no_blank_lines_before_namespace' => ['single_blank_line_before_namespace'],
  209. ];
  210. $fixerName = $fixer->getName();
  211. return array_key_exists($fixerName, $conflictMap) ? $conflictMap[$fixerName] : [];
  212. }
  213. /**
  214. * @param array<string, string[]> $fixerConflicts
  215. *
  216. * @return string
  217. */
  218. private function generateConflictMessage(array $fixerConflicts)
  219. {
  220. $message = 'Rule contains conflicting fixers:';
  221. $report = [];
  222. foreach ($fixerConflicts as $fixer => $fixers) {
  223. // filter mutual conflicts
  224. $report[$fixer] = array_filter(
  225. $fixers,
  226. function ($candidate) use ($report, $fixer) {
  227. return !array_key_exists($candidate, $report) || !in_array($fixer, $report[$candidate], true);
  228. }
  229. );
  230. if (count($report[$fixer]) > 0) {
  231. $message .= sprintf("\n- \"%s\" with \"%s\"", $fixer, implode('", "', $report[$fixer]));
  232. }
  233. }
  234. return $message;
  235. }
  236. }