ConfigurationResolver.php 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973
  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\Console;
  13. use PhpCsFixer\Cache\CacheManagerInterface;
  14. use PhpCsFixer\Cache\Directory;
  15. use PhpCsFixer\Cache\DirectoryInterface;
  16. use PhpCsFixer\Cache\FileCacheManager;
  17. use PhpCsFixer\Cache\FileHandler;
  18. use PhpCsFixer\Cache\NullCacheManager;
  19. use PhpCsFixer\Cache\Signature;
  20. use PhpCsFixer\ConfigInterface;
  21. use PhpCsFixer\ConfigurationException\InvalidConfigurationException;
  22. use PhpCsFixer\Console\Output\Progress\ProgressOutputType;
  23. use PhpCsFixer\Console\Report\FixReport\ReporterFactory;
  24. use PhpCsFixer\Console\Report\FixReport\ReporterInterface;
  25. use PhpCsFixer\Differ\DifferInterface;
  26. use PhpCsFixer\Differ\NullDiffer;
  27. use PhpCsFixer\Differ\UnifiedDiffer;
  28. use PhpCsFixer\Finder;
  29. use PhpCsFixer\Fixer\DeprecatedFixerInterface;
  30. use PhpCsFixer\Fixer\FixerInterface;
  31. use PhpCsFixer\FixerFactory;
  32. use PhpCsFixer\Linter\Linter;
  33. use PhpCsFixer\Linter\LinterInterface;
  34. use PhpCsFixer\ParallelAwareConfigInterface;
  35. use PhpCsFixer\RuleSet\RuleSet;
  36. use PhpCsFixer\RuleSet\RuleSetInterface;
  37. use PhpCsFixer\Runner\Parallel\ParallelConfig;
  38. use PhpCsFixer\Runner\Parallel\ParallelConfigFactory;
  39. use PhpCsFixer\StdinFileInfo;
  40. use PhpCsFixer\ToolInfoInterface;
  41. use PhpCsFixer\Utils;
  42. use PhpCsFixer\WhitespacesFixerConfig;
  43. use PhpCsFixer\WordMatcher;
  44. use Symfony\Component\Filesystem\Filesystem;
  45. use Symfony\Component\Finder\Finder as SymfonyFinder;
  46. /**
  47. * The resolver that resolves configuration to use by command line options and config.
  48. *
  49. * @author Fabien Potencier <fabien@symfony.com>
  50. * @author Katsuhiro Ogawa <ko.fivestar@gmail.com>
  51. * @author Dariusz Rumiński <dariusz.ruminski@gmail.com>
  52. *
  53. * @internal
  54. */
  55. final class ConfigurationResolver
  56. {
  57. public const PATH_MODE_OVERRIDE = 'override';
  58. public const PATH_MODE_INTERSECTION = 'intersection';
  59. /**
  60. * @var null|bool
  61. */
  62. private $allowRisky;
  63. /**
  64. * @var null|ConfigInterface
  65. */
  66. private $config;
  67. /**
  68. * @var null|string
  69. */
  70. private $configFile;
  71. private string $cwd;
  72. private ConfigInterface $defaultConfig;
  73. /**
  74. * @var null|ReporterInterface
  75. */
  76. private $reporter;
  77. /**
  78. * @var null|bool
  79. */
  80. private $isStdIn;
  81. /**
  82. * @var null|bool
  83. */
  84. private $isDryRun;
  85. /**
  86. * @var null|list<FixerInterface>
  87. */
  88. private $fixers;
  89. /**
  90. * @var null|bool
  91. */
  92. private $configFinderIsOverridden;
  93. private ToolInfoInterface $toolInfo;
  94. /**
  95. * @var array<string, mixed>
  96. */
  97. private array $options = [
  98. 'allow-risky' => null,
  99. 'cache-file' => null,
  100. 'config' => null,
  101. 'diff' => null,
  102. 'dry-run' => null,
  103. 'format' => null,
  104. 'path' => [],
  105. 'path-mode' => self::PATH_MODE_OVERRIDE,
  106. 'rules' => null,
  107. 'sequential' => null,
  108. 'show-progress' => null,
  109. 'stop-on-violation' => null,
  110. 'using-cache' => null,
  111. 'verbosity' => null,
  112. ];
  113. /**
  114. * @var null|string
  115. */
  116. private $cacheFile;
  117. /**
  118. * @var null|CacheManagerInterface
  119. */
  120. private $cacheManager;
  121. /**
  122. * @var null|DifferInterface
  123. */
  124. private $differ;
  125. /**
  126. * @var null|Directory
  127. */
  128. private $directory;
  129. /**
  130. * @var null|iterable<\SplFileInfo>
  131. */
  132. private ?iterable $finder = null;
  133. private ?string $format = null;
  134. /**
  135. * @var null|Linter
  136. */
  137. private $linter;
  138. /**
  139. * @var null|list<string>
  140. */
  141. private ?array $path = null;
  142. /**
  143. * @var null|string
  144. */
  145. private $progress;
  146. /**
  147. * @var null|RuleSet
  148. */
  149. private $ruleSet;
  150. /**
  151. * @var null|bool
  152. */
  153. private $usingCache;
  154. /**
  155. * @var FixerFactory
  156. */
  157. private $fixerFactory;
  158. /**
  159. * @param array<string, mixed> $options
  160. */
  161. public function __construct(
  162. ConfigInterface $config,
  163. array $options,
  164. string $cwd,
  165. ToolInfoInterface $toolInfo
  166. ) {
  167. $this->defaultConfig = $config;
  168. $this->cwd = $cwd;
  169. $this->toolInfo = $toolInfo;
  170. foreach ($options as $name => $value) {
  171. $this->setOption($name, $value);
  172. }
  173. }
  174. public function getCacheFile(): ?string
  175. {
  176. if (!$this->getUsingCache()) {
  177. return null;
  178. }
  179. if (null === $this->cacheFile) {
  180. if (null === $this->options['cache-file']) {
  181. $this->cacheFile = $this->getConfig()->getCacheFile();
  182. } else {
  183. $this->cacheFile = $this->options['cache-file'];
  184. }
  185. }
  186. return $this->cacheFile;
  187. }
  188. public function getCacheManager(): CacheManagerInterface
  189. {
  190. if (null === $this->cacheManager) {
  191. $cacheFile = $this->getCacheFile();
  192. if (null === $cacheFile) {
  193. $this->cacheManager = new NullCacheManager();
  194. } else {
  195. $this->cacheManager = new FileCacheManager(
  196. new FileHandler($cacheFile),
  197. new Signature(
  198. PHP_VERSION,
  199. $this->toolInfo->getVersion(),
  200. $this->getConfig()->getIndent(),
  201. $this->getConfig()->getLineEnding(),
  202. $this->getRules()
  203. ),
  204. $this->isDryRun(),
  205. $this->getDirectory()
  206. );
  207. }
  208. }
  209. return $this->cacheManager;
  210. }
  211. public function getConfig(): ConfigInterface
  212. {
  213. if (null === $this->config) {
  214. foreach ($this->computeConfigFiles() as $configFile) {
  215. if (!file_exists($configFile)) {
  216. continue;
  217. }
  218. $configFileBasename = basename($configFile);
  219. $deprecatedConfigs = [
  220. '.php_cs' => '.php-cs-fixer.php',
  221. '.php_cs.dist' => '.php-cs-fixer.dist.php',
  222. ];
  223. if (isset($deprecatedConfigs[$configFileBasename])) {
  224. throw new InvalidConfigurationException("Configuration file `{$configFileBasename}` is outdated, rename to `{$deprecatedConfigs[$configFileBasename]}`.");
  225. }
  226. $this->config = self::separatedContextLessInclude($configFile);
  227. $this->configFile = $configFile;
  228. break;
  229. }
  230. if (null === $this->config) {
  231. $this->config = $this->defaultConfig;
  232. }
  233. }
  234. return $this->config;
  235. }
  236. public function getParallelConfig(): ParallelConfig
  237. {
  238. $config = $this->getConfig();
  239. return true !== $this->options['sequential'] && $config instanceof ParallelAwareConfigInterface
  240. ? $config->getParallelConfig()
  241. : ParallelConfigFactory::sequential();
  242. }
  243. public function getConfigFile(): ?string
  244. {
  245. if (null === $this->configFile) {
  246. $this->getConfig();
  247. }
  248. return $this->configFile;
  249. }
  250. public function getDiffer(): DifferInterface
  251. {
  252. if (null === $this->differ) {
  253. $this->differ = (true === $this->options['diff']) ? new UnifiedDiffer() : new NullDiffer();
  254. }
  255. return $this->differ;
  256. }
  257. public function getDirectory(): DirectoryInterface
  258. {
  259. if (null === $this->directory) {
  260. $path = $this->getCacheFile();
  261. if (null === $path) {
  262. $absolutePath = $this->cwd;
  263. } else {
  264. $filesystem = new Filesystem();
  265. $absolutePath = $filesystem->isAbsolutePath($path)
  266. ? $path
  267. : $this->cwd.\DIRECTORY_SEPARATOR.$path;
  268. $absolutePath = \dirname($absolutePath);
  269. }
  270. $this->directory = new Directory($absolutePath);
  271. }
  272. return $this->directory;
  273. }
  274. /**
  275. * @return list<FixerInterface>
  276. */
  277. public function getFixers(): array
  278. {
  279. if (null === $this->fixers) {
  280. $this->fixers = $this->createFixerFactory()
  281. ->useRuleSet($this->getRuleSet())
  282. ->setWhitespacesConfig(new WhitespacesFixerConfig($this->config->getIndent(), $this->config->getLineEnding()))
  283. ->getFixers()
  284. ;
  285. if (false === $this->getRiskyAllowed()) {
  286. $riskyFixers = array_map(
  287. static fn (FixerInterface $fixer): string => $fixer->getName(),
  288. array_filter(
  289. $this->fixers,
  290. static fn (FixerInterface $fixer): bool => $fixer->isRisky()
  291. )
  292. );
  293. if (\count($riskyFixers) > 0) {
  294. throw new InvalidConfigurationException(sprintf('The rules contain risky fixers (%s), but they are not allowed to run. Perhaps you forget to use --allow-risky=yes option?', Utils::naturalLanguageJoin($riskyFixers)));
  295. }
  296. }
  297. }
  298. return $this->fixers;
  299. }
  300. public function getLinter(): LinterInterface
  301. {
  302. if (null === $this->linter) {
  303. $this->linter = new Linter();
  304. }
  305. return $this->linter;
  306. }
  307. /**
  308. * Returns path.
  309. *
  310. * @return list<string>
  311. */
  312. public function getPath(): array
  313. {
  314. if (null === $this->path) {
  315. $filesystem = new Filesystem();
  316. $cwd = $this->cwd;
  317. if (1 === \count($this->options['path']) && '-' === $this->options['path'][0]) {
  318. $this->path = $this->options['path'];
  319. } else {
  320. $this->path = array_map(
  321. static function (string $rawPath) use ($cwd, $filesystem): string {
  322. $path = trim($rawPath);
  323. if ('' === $path) {
  324. throw new InvalidConfigurationException("Invalid path: \"{$rawPath}\".");
  325. }
  326. $absolutePath = $filesystem->isAbsolutePath($path)
  327. ? $path
  328. : $cwd.\DIRECTORY_SEPARATOR.$path;
  329. if (!file_exists($absolutePath)) {
  330. throw new InvalidConfigurationException(sprintf(
  331. 'The path "%s" is not readable.',
  332. $path
  333. ));
  334. }
  335. return $absolutePath;
  336. },
  337. $this->options['path']
  338. );
  339. }
  340. }
  341. return $this->path;
  342. }
  343. /**
  344. * @throws InvalidConfigurationException
  345. */
  346. public function getProgressType(): string
  347. {
  348. if (null === $this->progress) {
  349. if ('txt' === $this->getFormat()) {
  350. $progressType = $this->options['show-progress'];
  351. if (null === $progressType) {
  352. $progressType = $this->getConfig()->getHideProgress()
  353. ? ProgressOutputType::NONE
  354. : ProgressOutputType::BAR;
  355. } elseif (!\in_array($progressType, ProgressOutputType::all(), true)) {
  356. throw new InvalidConfigurationException(sprintf(
  357. 'The progress type "%s" is not defined, supported are %s.',
  358. $progressType,
  359. Utils::naturalLanguageJoin(ProgressOutputType::all())
  360. ));
  361. }
  362. $this->progress = $progressType;
  363. } else {
  364. $this->progress = ProgressOutputType::NONE;
  365. }
  366. }
  367. return $this->progress;
  368. }
  369. public function getReporter(): ReporterInterface
  370. {
  371. if (null === $this->reporter) {
  372. $reporterFactory = new ReporterFactory();
  373. $reporterFactory->registerBuiltInReporters();
  374. $format = $this->getFormat();
  375. try {
  376. $this->reporter = $reporterFactory->getReporter($format);
  377. } catch (\UnexpectedValueException $e) {
  378. $formats = $reporterFactory->getFormats();
  379. sort($formats);
  380. throw new InvalidConfigurationException(sprintf('The format "%s" is not defined, supported are %s.', $format, Utils::naturalLanguageJoin($formats)));
  381. }
  382. }
  383. return $this->reporter;
  384. }
  385. public function getRiskyAllowed(): bool
  386. {
  387. if (null === $this->allowRisky) {
  388. if (null === $this->options['allow-risky']) {
  389. $this->allowRisky = $this->getConfig()->getRiskyAllowed();
  390. } else {
  391. $this->allowRisky = $this->resolveOptionBooleanValue('allow-risky');
  392. }
  393. }
  394. return $this->allowRisky;
  395. }
  396. /**
  397. * Returns rules.
  398. *
  399. * @return array<string, array<string, mixed>|bool>
  400. */
  401. public function getRules(): array
  402. {
  403. return $this->getRuleSet()->getRules();
  404. }
  405. public function getUsingCache(): bool
  406. {
  407. if (null === $this->usingCache) {
  408. if (null === $this->options['using-cache']) {
  409. $this->usingCache = $this->getConfig()->getUsingCache();
  410. } else {
  411. $this->usingCache = $this->resolveOptionBooleanValue('using-cache');
  412. }
  413. }
  414. $this->usingCache = $this->usingCache && $this->isCachingAllowedForRuntime();
  415. return $this->usingCache;
  416. }
  417. /**
  418. * @return iterable<\SplFileInfo>
  419. */
  420. public function getFinder(): iterable
  421. {
  422. if (null === $this->finder) {
  423. $this->finder = $this->resolveFinder();
  424. }
  425. return $this->finder;
  426. }
  427. /**
  428. * Returns dry-run flag.
  429. */
  430. public function isDryRun(): bool
  431. {
  432. if (null === $this->isDryRun) {
  433. if ($this->isStdIn()) {
  434. // Can't write to STDIN
  435. $this->isDryRun = true;
  436. } else {
  437. $this->isDryRun = $this->options['dry-run'];
  438. }
  439. }
  440. return $this->isDryRun;
  441. }
  442. public function shouldStopOnViolation(): bool
  443. {
  444. return $this->options['stop-on-violation'];
  445. }
  446. public function configFinderIsOverridden(): bool
  447. {
  448. if (null === $this->configFinderIsOverridden) {
  449. $this->resolveFinder();
  450. }
  451. return $this->configFinderIsOverridden;
  452. }
  453. /**
  454. * Compute file candidates for config file.
  455. *
  456. * @return list<string>
  457. */
  458. private function computeConfigFiles(): array
  459. {
  460. $configFile = $this->options['config'];
  461. if (null !== $configFile) {
  462. if (false === file_exists($configFile) || false === is_readable($configFile)) {
  463. throw new InvalidConfigurationException(sprintf('Cannot read config file "%s".', $configFile));
  464. }
  465. return [$configFile];
  466. }
  467. $path = $this->getPath();
  468. if ($this->isStdIn() || 0 === \count($path)) {
  469. $configDir = $this->cwd;
  470. } elseif (1 < \count($path)) {
  471. throw new InvalidConfigurationException('For multiple paths config parameter is required.');
  472. } elseif (!is_file($path[0])) {
  473. $configDir = $path[0];
  474. } else {
  475. $dirName = pathinfo($path[0], PATHINFO_DIRNAME);
  476. $configDir = is_dir($dirName) ? $dirName : $path[0];
  477. }
  478. $candidates = [
  479. $configDir.\DIRECTORY_SEPARATOR.'.php-cs-fixer.php',
  480. $configDir.\DIRECTORY_SEPARATOR.'.php-cs-fixer.dist.php',
  481. $configDir.\DIRECTORY_SEPARATOR.'.php_cs', // old v2 config, present here only to throw nice error message later
  482. $configDir.\DIRECTORY_SEPARATOR.'.php_cs.dist', // old v2 config, present here only to throw nice error message later
  483. ];
  484. if ($configDir !== $this->cwd) {
  485. $candidates[] = $this->cwd.\DIRECTORY_SEPARATOR.'.php-cs-fixer.php';
  486. $candidates[] = $this->cwd.\DIRECTORY_SEPARATOR.'.php-cs-fixer.dist.php';
  487. $candidates[] = $this->cwd.\DIRECTORY_SEPARATOR.'.php_cs'; // old v2 config, present here only to throw nice error message later
  488. $candidates[] = $this->cwd.\DIRECTORY_SEPARATOR.'.php_cs.dist'; // old v2 config, present here only to throw nice error message later
  489. }
  490. return $candidates;
  491. }
  492. private function createFixerFactory(): FixerFactory
  493. {
  494. if (null === $this->fixerFactory) {
  495. $fixerFactory = new FixerFactory();
  496. $fixerFactory->registerBuiltInFixers();
  497. $fixerFactory->registerCustomFixers($this->getConfig()->getCustomFixers());
  498. $this->fixerFactory = $fixerFactory;
  499. }
  500. return $this->fixerFactory;
  501. }
  502. private function getFormat(): string
  503. {
  504. if (null === $this->format) {
  505. $this->format = $this->options['format'] ?? $this->getConfig()->getFormat();
  506. }
  507. return $this->format;
  508. }
  509. private function getRuleSet(): RuleSetInterface
  510. {
  511. if (null === $this->ruleSet) {
  512. $rules = $this->parseRules();
  513. $this->validateRules($rules);
  514. $this->ruleSet = new RuleSet($rules);
  515. }
  516. return $this->ruleSet;
  517. }
  518. private function isStdIn(): bool
  519. {
  520. if (null === $this->isStdIn) {
  521. $this->isStdIn = 1 === \count($this->options['path']) && '-' === $this->options['path'][0];
  522. }
  523. return $this->isStdIn;
  524. }
  525. /**
  526. * @template T
  527. *
  528. * @param iterable<T> $iterable
  529. *
  530. * @return \Traversable<T>
  531. */
  532. private function iterableToTraversable(iterable $iterable): \Traversable
  533. {
  534. return \is_array($iterable) ? new \ArrayIterator($iterable) : $iterable;
  535. }
  536. /**
  537. * @return array<string, mixed>
  538. */
  539. private function parseRules(): array
  540. {
  541. if (null === $this->options['rules']) {
  542. return $this->getConfig()->getRules();
  543. }
  544. $rules = trim($this->options['rules']);
  545. if ('' === $rules) {
  546. throw new InvalidConfigurationException('Empty rules value is not allowed.');
  547. }
  548. if (str_starts_with($rules, '{')) {
  549. $rules = json_decode($rules, true);
  550. if (JSON_ERROR_NONE !== json_last_error()) {
  551. throw new InvalidConfigurationException(sprintf('Invalid JSON rules input: "%s".', json_last_error_msg()));
  552. }
  553. return $rules;
  554. }
  555. $rules = [];
  556. foreach (explode(',', $this->options['rules']) as $rule) {
  557. $rule = trim($rule);
  558. if ('' === $rule) {
  559. throw new InvalidConfigurationException('Empty rule name is not allowed.');
  560. }
  561. if (str_starts_with($rule, '-')) {
  562. $rules[substr($rule, 1)] = false;
  563. } else {
  564. $rules[$rule] = true;
  565. }
  566. }
  567. return $rules;
  568. }
  569. /**
  570. * @param array<string, mixed> $rules
  571. *
  572. * @throws InvalidConfigurationException
  573. */
  574. private function validateRules(array $rules): void
  575. {
  576. /**
  577. * Create a ruleset that contains all configured rules, even when they originally have been disabled.
  578. *
  579. * @see RuleSet::resolveSet()
  580. */
  581. $ruleSet = [];
  582. foreach ($rules as $key => $value) {
  583. if (\is_int($key)) {
  584. throw new InvalidConfigurationException(sprintf('Missing value for "%s" rule/set.', $value));
  585. }
  586. $ruleSet[$key] = true;
  587. }
  588. $ruleSet = new RuleSet($ruleSet);
  589. $configuredFixers = array_keys($ruleSet->getRules());
  590. $fixers = $this->createFixerFactory()->getFixers();
  591. $availableFixers = array_map(static fn (FixerInterface $fixer): string => $fixer->getName(), $fixers);
  592. $unknownFixers = array_diff($configuredFixers, $availableFixers);
  593. if (\count($unknownFixers) > 0) {
  594. $renamedRules = [
  595. 'blank_line_before_return' => [
  596. 'new_name' => 'blank_line_before_statement',
  597. 'config' => ['statements' => ['return']],
  598. ],
  599. 'final_static_access' => [
  600. 'new_name' => 'self_static_accessor',
  601. ],
  602. 'hash_to_slash_comment' => [
  603. 'new_name' => 'single_line_comment_style',
  604. 'config' => ['comment_types' => ['hash']],
  605. ],
  606. 'lowercase_constants' => [
  607. 'new_name' => 'constant_case',
  608. 'config' => ['case' => 'lower'],
  609. ],
  610. 'no_extra_consecutive_blank_lines' => [
  611. 'new_name' => 'no_extra_blank_lines',
  612. ],
  613. 'no_multiline_whitespace_before_semicolons' => [
  614. 'new_name' => 'multiline_whitespace_before_semicolons',
  615. ],
  616. 'no_short_echo_tag' => [
  617. 'new_name' => 'echo_tag_syntax',
  618. 'config' => ['format' => 'long'],
  619. ],
  620. 'php_unit_ordered_covers' => [
  621. 'new_name' => 'phpdoc_order_by_value',
  622. 'config' => ['annotations' => ['covers']],
  623. ],
  624. 'phpdoc_inline_tag' => [
  625. 'new_name' => 'general_phpdoc_tag_rename, phpdoc_inline_tag_normalizer and phpdoc_tag_type',
  626. ],
  627. 'pre_increment' => [
  628. 'new_name' => 'increment_style',
  629. 'config' => ['style' => 'pre'],
  630. ],
  631. 'psr0' => [
  632. 'new_name' => 'psr_autoloading',
  633. 'config' => ['dir' => 'x'],
  634. ],
  635. 'psr4' => [
  636. 'new_name' => 'psr_autoloading',
  637. ],
  638. 'silenced_deprecation_error' => [
  639. 'new_name' => 'error_suppression',
  640. ],
  641. 'trailing_comma_in_multiline_array' => [
  642. 'new_name' => 'trailing_comma_in_multiline',
  643. 'config' => ['elements' => ['arrays']],
  644. ],
  645. ];
  646. $message = 'The rules contain unknown fixers: ';
  647. $hasOldRule = false;
  648. foreach ($unknownFixers as $unknownFixer) {
  649. if (isset($renamedRules[$unknownFixer])) { // Check if present as old renamed rule
  650. $hasOldRule = true;
  651. $message .= sprintf(
  652. '"%s" is renamed (did you mean "%s"?%s), ',
  653. $unknownFixer,
  654. $renamedRules[$unknownFixer]['new_name'],
  655. isset($renamedRules[$unknownFixer]['config']) ? ' (note: use configuration "'.Utils::toString($renamedRules[$unknownFixer]['config']).'")' : ''
  656. );
  657. } else { // Go to normal matcher if it is not a renamed rule
  658. $matcher = new WordMatcher($availableFixers);
  659. $alternative = $matcher->match($unknownFixer);
  660. $message .= sprintf(
  661. '"%s"%s, ',
  662. $unknownFixer,
  663. null === $alternative ? '' : ' (did you mean "'.$alternative.'"?)'
  664. );
  665. }
  666. }
  667. $message = substr($message, 0, -2).'.';
  668. if ($hasOldRule) {
  669. $message .= "\nFor more info about updating see: https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/blob/v3.0.0/UPGRADE-v3.md#renamed-ruless.";
  670. }
  671. throw new InvalidConfigurationException($message);
  672. }
  673. foreach ($fixers as $fixer) {
  674. $fixerName = $fixer->getName();
  675. if (isset($rules[$fixerName]) && $fixer instanceof DeprecatedFixerInterface) {
  676. $successors = $fixer->getSuccessorsNames();
  677. $messageEnd = [] === $successors
  678. ? sprintf(' and will be removed in version %d.0.', Application::getMajorVersion() + 1)
  679. : sprintf('. Use %s instead.', str_replace('`', '"', Utils::naturalLanguageJoinWithBackticks($successors)));
  680. Utils::triggerDeprecation(new \RuntimeException("Rule \"{$fixerName}\" is deprecated{$messageEnd}"));
  681. }
  682. }
  683. }
  684. /**
  685. * Apply path on config instance.
  686. *
  687. * @return iterable<\SplFileInfo>
  688. */
  689. private function resolveFinder(): iterable
  690. {
  691. $this->configFinderIsOverridden = false;
  692. if ($this->isStdIn()) {
  693. return new \ArrayIterator([new StdinFileInfo()]);
  694. }
  695. $modes = [self::PATH_MODE_OVERRIDE, self::PATH_MODE_INTERSECTION];
  696. if (!\in_array(
  697. $this->options['path-mode'],
  698. $modes,
  699. true
  700. )) {
  701. throw new InvalidConfigurationException(sprintf(
  702. 'The path-mode "%s" is not defined, supported are %s.',
  703. $this->options['path-mode'],
  704. Utils::naturalLanguageJoin($modes)
  705. ));
  706. }
  707. $isIntersectionPathMode = self::PATH_MODE_INTERSECTION === $this->options['path-mode'];
  708. $paths = array_map(
  709. static fn (string $path) => realpath($path),
  710. $this->getPath()
  711. );
  712. if (0 === \count($paths)) {
  713. if ($isIntersectionPathMode) {
  714. return new \ArrayIterator([]);
  715. }
  716. return $this->iterableToTraversable($this->getConfig()->getFinder());
  717. }
  718. $pathsByType = [
  719. 'file' => [],
  720. 'dir' => [],
  721. ];
  722. foreach ($paths as $path) {
  723. if (is_file($path)) {
  724. $pathsByType['file'][] = $path;
  725. } else {
  726. $pathsByType['dir'][] = $path.\DIRECTORY_SEPARATOR;
  727. }
  728. }
  729. $nestedFinder = null;
  730. $currentFinder = $this->iterableToTraversable($this->getConfig()->getFinder());
  731. try {
  732. $nestedFinder = $currentFinder instanceof \IteratorAggregate ? $currentFinder->getIterator() : $currentFinder;
  733. } catch (\Exception $e) {
  734. }
  735. if ($isIntersectionPathMode) {
  736. if (null === $nestedFinder) {
  737. throw new InvalidConfigurationException(
  738. 'Cannot create intersection with not-fully defined Finder in configuration file.'
  739. );
  740. }
  741. return new \CallbackFilterIterator(
  742. new \IteratorIterator($nestedFinder),
  743. static function (\SplFileInfo $current) use ($pathsByType): bool {
  744. $currentRealPath = $current->getRealPath();
  745. if (\in_array($currentRealPath, $pathsByType['file'], true)) {
  746. return true;
  747. }
  748. foreach ($pathsByType['dir'] as $path) {
  749. if (str_starts_with($currentRealPath, $path)) {
  750. return true;
  751. }
  752. }
  753. return false;
  754. }
  755. );
  756. }
  757. if (null !== $this->getConfigFile() && null !== $nestedFinder) {
  758. $this->configFinderIsOverridden = true;
  759. }
  760. if ($currentFinder instanceof SymfonyFinder && null === $nestedFinder) {
  761. // finder from configuration Symfony finder and it is not fully defined, we may fulfill it
  762. return $currentFinder->in($pathsByType['dir'])->append($pathsByType['file']);
  763. }
  764. return Finder::create()->in($pathsByType['dir'])->append($pathsByType['file']);
  765. }
  766. /**
  767. * Set option that will be resolved.
  768. *
  769. * @param mixed $value
  770. */
  771. private function setOption(string $name, $value): void
  772. {
  773. if (!\array_key_exists($name, $this->options)) {
  774. throw new InvalidConfigurationException(sprintf('Unknown option name: "%s".', $name));
  775. }
  776. $this->options[$name] = $value;
  777. }
  778. private function resolveOptionBooleanValue(string $optionName): bool
  779. {
  780. $value = $this->options[$optionName];
  781. if (!\is_string($value)) {
  782. throw new InvalidConfigurationException(sprintf('Expected boolean or string value for option "%s".', $optionName));
  783. }
  784. if ('yes' === $value) {
  785. return true;
  786. }
  787. if ('no' === $value) {
  788. return false;
  789. }
  790. throw new InvalidConfigurationException(sprintf('Expected "yes" or "no" for option "%s", got "%s".', $optionName, $value));
  791. }
  792. private static function separatedContextLessInclude(string $path): ConfigInterface
  793. {
  794. $config = include $path;
  795. // verify that the config has an instance of Config
  796. if (!$config instanceof ConfigInterface) {
  797. throw new InvalidConfigurationException(sprintf('The config file: "%s" does not return a "PhpCsFixer\ConfigInterface" instance. Got: "%s".', $path, \is_object($config) ? \get_class($config) : \gettype($config)));
  798. }
  799. return $config;
  800. }
  801. private function isCachingAllowedForRuntime(): bool
  802. {
  803. return $this->toolInfo->isInstalledAsPhar()
  804. || $this->toolInfo->isInstalledByComposer()
  805. || $this->toolInfo->isRunInsideDocker()
  806. || filter_var(getenv('PHP_CS_FIXER_ENFORCE_CACHE'), FILTER_VALIDATE_BOOL);
  807. }
  808. }