• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

keradus / PHP-CS-Fixer / 13074053291

31 Jan 2025 01:12PM UTC coverage: 94.959% (+0.005%) from 94.954%
13074053291

push

github

web-flow
deps: bump maglnet/composer-require-checker from 4.14.0 to 4.15.0 in /dev-tools (#8406)

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

27840 of 29318 relevant lines covered (94.96%)

43.14 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

91.79
/src/Console/ConfigurationResolver.php
1
<?php
2

3
declare(strict_types=1);
4

5
/*
6
 * This file is part of PHP CS Fixer.
7
 *
8
 * (c) Fabien Potencier <fabien@symfony.com>
9
 *     Dariusz Rumiński <dariusz.ruminski@gmail.com>
10
 *
11
 * This source file is subject to the MIT license that is bundled
12
 * with this source code in the file LICENSE.
13
 */
14

15
namespace PhpCsFixer\Console;
16

17
use PhpCsFixer\Cache\CacheManagerInterface;
18
use PhpCsFixer\Cache\Directory;
19
use PhpCsFixer\Cache\DirectoryInterface;
20
use PhpCsFixer\Cache\FileCacheManager;
21
use PhpCsFixer\Cache\FileHandler;
22
use PhpCsFixer\Cache\NullCacheManager;
23
use PhpCsFixer\Cache\Signature;
24
use PhpCsFixer\ConfigInterface;
25
use PhpCsFixer\ConfigurationException\InvalidConfigurationException;
26
use PhpCsFixer\Console\Output\Progress\ProgressOutputType;
27
use PhpCsFixer\Console\Report\FixReport\ReporterFactory;
28
use PhpCsFixer\Console\Report\FixReport\ReporterInterface;
29
use PhpCsFixer\Differ\DifferInterface;
30
use PhpCsFixer\Differ\NullDiffer;
31
use PhpCsFixer\Differ\UnifiedDiffer;
32
use PhpCsFixer\Finder;
33
use PhpCsFixer\Fixer\DeprecatedFixerInterface;
34
use PhpCsFixer\Fixer\FixerInterface;
35
use PhpCsFixer\FixerFactory;
36
use PhpCsFixer\Linter\Linter;
37
use PhpCsFixer\Linter\LinterInterface;
38
use PhpCsFixer\ParallelAwareConfigInterface;
39
use PhpCsFixer\RuleSet\RuleSet;
40
use PhpCsFixer\RuleSet\RuleSetInterface;
41
use PhpCsFixer\Runner\Parallel\ParallelConfig;
42
use PhpCsFixer\Runner\Parallel\ParallelConfigFactory;
43
use PhpCsFixer\StdinFileInfo;
44
use PhpCsFixer\ToolInfoInterface;
45
use PhpCsFixer\Utils;
46
use PhpCsFixer\WhitespacesFixerConfig;
47
use PhpCsFixer\WordMatcher;
48
use Symfony\Component\Filesystem\Filesystem;
49
use Symfony\Component\Finder\Finder as SymfonyFinder;
50

51
/**
52
 * The resolver that resolves configuration to use by command line options and config.
53
 *
54
 * @author Fabien Potencier <fabien@symfony.com>
55
 * @author Katsuhiro Ogawa <ko.fivestar@gmail.com>
56
 * @author Dariusz Rumiński <dariusz.ruminski@gmail.com>
57
 *
58
 * @internal
59
 *
60
 * @phpstan-type _Options array{
61
 *      allow-risky: null|string,
62
 *      cache-file: null|string,
63
 *      config: null|string,
64
 *      diff: null|string,
65
 *      dry-run: null|bool,
66
 *      format: null|string,
67
 *      path: list<string>,
68
 *      path-mode: self::PATH_MODE_*,
69
 *      rules: null|string,
70
 *      sequential: null|string,
71
 *      show-progress: null|string,
72
 *      stop-on-violation: null|bool,
73
 *      using-cache: null|string,
74
 *      verbosity: null|string,
75
 *  }
76
 */
77
final class ConfigurationResolver
78
{
79
    public const PATH_MODE_OVERRIDE = 'override';
80
    public const PATH_MODE_INTERSECTION = 'intersection';
81

82
    private ?bool $allowRisky = null;
83

84
    private ?ConfigInterface $config = null;
85

86
    private ?string $configFile = null;
87

88
    private string $cwd;
89

90
    private ConfigInterface $defaultConfig;
91

92
    private ?ReporterInterface $reporter = null;
93

94
    private ?bool $isStdIn = null;
95

96
    private ?bool $isDryRun = null;
97

98
    /**
99
     * @var null|list<FixerInterface>
100
     */
101
    private ?array $fixers = null;
102

103
    private ?bool $configFinderIsOverridden = null;
104

105
    private ToolInfoInterface $toolInfo;
106

107
    /**
108
     * @var _Options
109
     */
110
    private array $options = [
111
        'allow-risky' => null,
112
        'cache-file' => null,
113
        'config' => null,
114
        'diff' => null,
115
        'dry-run' => null,
116
        'format' => null,
117
        'path' => [],
118
        'path-mode' => self::PATH_MODE_OVERRIDE,
119
        'rules' => null,
120
        'sequential' => null,
121
        'show-progress' => null,
122
        'stop-on-violation' => null,
123
        'using-cache' => null,
124
        'verbosity' => null,
125
    ];
126

127
    private ?string $cacheFile = null;
128

129
    private ?CacheManagerInterface $cacheManager = null;
130

131
    private ?DifferInterface $differ = null;
132

133
    private ?Directory $directory = null;
134

135
    /**
136
     * @var null|iterable<\SplFileInfo>
137
     */
138
    private ?iterable $finder = null;
139

140
    private ?string $format = null;
141

142
    private ?Linter $linter = null;
143

144
    /**
145
     * @var null|list<string>
146
     */
147
    private ?array $path = null;
148

149
    /**
150
     * @var null|ProgressOutputType::*
151
     */
152
    private $progress;
153

154
    private ?RuleSet $ruleSet = null;
155

156
    private ?bool $usingCache = null;
157

158
    private ?FixerFactory $fixerFactory = null;
159

160
    /**
161
     * @param array<string, mixed> $options
162
     */
163
    public function __construct(
164
        ConfigInterface $config,
165
        array $options,
166
        string $cwd,
167
        ToolInfoInterface $toolInfo
168
    ) {
169
        $this->defaultConfig = $config;
121✔
170
        $this->cwd = $cwd;
121✔
171
        $this->toolInfo = $toolInfo;
121✔
172

173
        foreach ($options as $name => $value) {
121✔
174
            $this->setOption($name, $value);
97✔
175
        }
176
    }
177

178
    public function getCacheFile(): ?string
179
    {
180
        if (!$this->getUsingCache()) {
10✔
181
            return null;
4✔
182
        }
183

184
        if (null === $this->cacheFile) {
6✔
185
            if (null === $this->options['cache-file']) {
6✔
186
                $this->cacheFile = $this->getConfig()->getCacheFile();
4✔
187
            } else {
188
                $this->cacheFile = $this->options['cache-file'];
2✔
189
            }
190
        }
191

192
        return $this->cacheFile;
6✔
193
    }
194

195
    public function getCacheManager(): CacheManagerInterface
196
    {
197
        if (null === $this->cacheManager) {
1✔
198
            $cacheFile = $this->getCacheFile();
1✔
199

200
            if (null === $cacheFile) {
1✔
201
                $this->cacheManager = new NullCacheManager();
1✔
202
            } else {
203
                $this->cacheManager = new FileCacheManager(
×
204
                    new FileHandler($cacheFile),
×
205
                    new Signature(
×
206
                        PHP_VERSION,
×
207
                        $this->toolInfo->getVersion(),
×
208
                        $this->getConfig()->getIndent(),
×
209
                        $this->getConfig()->getLineEnding(),
×
210
                        $this->getRules()
×
211
                    ),
×
212
                    $this->isDryRun(),
×
213
                    $this->getDirectory()
×
214
                );
×
215
            }
216
        }
217

218
        return $this->cacheManager;
1✔
219
    }
220

221
    public function getConfig(): ConfigInterface
222
    {
223
        if (null === $this->config) {
79✔
224
            foreach ($this->computeConfigFiles() as $configFile) {
79✔
225
                if (!file_exists($configFile)) {
78✔
226
                    continue;
63✔
227
                }
228

229
                $configFileBasename = basename($configFile);
20✔
230
                $deprecatedConfigs = [
20✔
231
                    '.php_cs' => '.php-cs-fixer.php',
20✔
232
                    '.php_cs.dist' => '.php-cs-fixer.dist.php',
20✔
233
                ];
20✔
234

235
                if (isset($deprecatedConfigs[$configFileBasename])) {
20✔
236
                    throw new InvalidConfigurationException("Configuration file `{$configFileBasename}` is outdated, rename to `{$deprecatedConfigs[$configFileBasename]}`.");
×
237
                }
238

239
                $this->config = self::separatedContextLessInclude($configFile);
20✔
240
                $this->configFile = $configFile;
19✔
241

242
                break;
19✔
243
            }
244

245
            if (null === $this->config) {
77✔
246
                $this->config = $this->defaultConfig;
58✔
247
            }
248
        }
249

250
        return $this->config;
77✔
251
    }
252

253
    public function getParallelConfig(): ParallelConfig
254
    {
255
        $config = $this->getConfig();
3✔
256

257
        return true !== $this->options['sequential'] && $config instanceof ParallelAwareConfigInterface
3✔
258
            ? $config->getParallelConfig()
2✔
259
            : ParallelConfigFactory::sequential();
3✔
260
    }
261

262
    public function getConfigFile(): ?string
263
    {
264
        if (null === $this->configFile) {
19✔
265
            $this->getConfig();
14✔
266
        }
267

268
        return $this->configFile;
19✔
269
    }
270

271
    public function getDiffer(): DifferInterface
272
    {
273
        if (null === $this->differ) {
4✔
274
            $this->differ = (true === $this->options['diff']) ? new UnifiedDiffer() : new NullDiffer();
4✔
275
        }
276

277
        return $this->differ;
4✔
278
    }
279

280
    public function getDirectory(): DirectoryInterface
281
    {
282
        if (null === $this->directory) {
4✔
283
            $path = $this->getCacheFile();
4✔
284
            if (null === $path) {
4✔
285
                $absolutePath = $this->cwd;
1✔
286
            } else {
287
                $filesystem = new Filesystem();
3✔
288

289
                $absolutePath = $filesystem->isAbsolutePath($path)
3✔
290
                    ? $path
2✔
291
                    : $this->cwd.\DIRECTORY_SEPARATOR.$path;
1✔
292
                $absolutePath = \dirname($absolutePath);
3✔
293
            }
294

295
            $this->directory = new Directory($absolutePath);
4✔
296
        }
297

298
        return $this->directory;
4✔
299
    }
300

301
    /**
302
     * @return list<FixerInterface>
303
     */
304
    public function getFixers(): array
305
    {
306
        if (null === $this->fixers) {
5✔
307
            $this->fixers = $this->createFixerFactory()
5✔
308
                ->useRuleSet($this->getRuleSet())
5✔
309
                ->setWhitespacesConfig(new WhitespacesFixerConfig($this->config->getIndent(), $this->config->getLineEnding()))
5✔
310
                ->getFixers()
5✔
311
            ;
5✔
312

313
            if (false === $this->getRiskyAllowed()) {
5✔
314
                $riskyFixers = array_map(
3✔
315
                    static fn (FixerInterface $fixer): string => $fixer->getName(),
3✔
316
                    array_filter(
3✔
317
                        $this->fixers,
3✔
318
                        static fn (FixerInterface $fixer): bool => $fixer->isRisky()
3✔
319
                    )
3✔
320
                );
3✔
321

322
                if (\count($riskyFixers) > 0) {
3✔
323
                    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)));
×
324
                }
325
            }
326
        }
327

328
        return $this->fixers;
5✔
329
    }
330

331
    public function getLinter(): LinterInterface
332
    {
333
        if (null === $this->linter) {
1✔
334
            $this->linter = new Linter();
1✔
335
        }
336

337
        return $this->linter;
1✔
338
    }
339

340
    /**
341
     * Returns path.
342
     *
343
     * @return list<string>
344
     */
345
    public function getPath(): array
346
    {
347
        if (null === $this->path) {
93✔
348
            $filesystem = new Filesystem();
93✔
349
            $cwd = $this->cwd;
93✔
350

351
            if (1 === \count($this->options['path']) && '-' === $this->options['path'][0]) {
93✔
352
                $this->path = $this->options['path'];
×
353
            } else {
354
                $this->path = array_map(
93✔
355
                    static function (string $rawPath) use ($cwd, $filesystem): string {
93✔
356
                        $path = trim($rawPath);
46✔
357

358
                        if ('' === $path) {
46✔
359
                            throw new InvalidConfigurationException("Invalid path: \"{$rawPath}\".");
6✔
360
                        }
361

362
                        $absolutePath = $filesystem->isAbsolutePath($path)
42✔
363
                            ? $path
37✔
364
                            : $cwd.\DIRECTORY_SEPARATOR.$path;
5✔
365

366
                        if (!file_exists($absolutePath)) {
42✔
367
                            throw new InvalidConfigurationException(\sprintf(
5✔
368
                                'The path "%s" is not readable.',
5✔
369
                                $path
5✔
370
                            ));
5✔
371
                        }
372

373
                        return $absolutePath;
37✔
374
                    },
93✔
375
                    $this->options['path']
93✔
376
                );
93✔
377
            }
378
        }
379

380
        return $this->path;
82✔
381
    }
382

383
    /**
384
     * @return ProgressOutputType::*
385
     *
386
     * @throws InvalidConfigurationException
387
     */
388
    public function getProgressType(): string
389
    {
390
        if (null === $this->progress) {
13✔
391
            if ('txt' === $this->getFormat()) {
13✔
392
                $progressType = $this->options['show-progress'];
11✔
393

394
                if (null === $progressType) {
11✔
395
                    $progressType = $this->getConfig()->getHideProgress()
4✔
396
                        ? ProgressOutputType::NONE
2✔
397
                        : ProgressOutputType::BAR;
2✔
398
                } elseif (!\in_array($progressType, ProgressOutputType::all(), true)) {
7✔
399
                    throw new InvalidConfigurationException(\sprintf(
1✔
400
                        'The progress type "%s" is not defined, supported are %s.',
1✔
401
                        $progressType,
1✔
402
                        Utils::naturalLanguageJoin(ProgressOutputType::all())
1✔
403
                    ));
1✔
404
                }
405

406
                $this->progress = $progressType;
10✔
407
            } else {
408
                $this->progress = ProgressOutputType::NONE;
2✔
409
            }
410
        }
411

412
        return $this->progress;
12✔
413
    }
414

415
    public function getReporter(): ReporterInterface
416
    {
417
        if (null === $this->reporter) {
3✔
418
            $reporterFactory = new ReporterFactory();
3✔
419
            $reporterFactory->registerBuiltInReporters();
3✔
420

421
            $format = $this->getFormat();
3✔
422

423
            try {
424
                $this->reporter = $reporterFactory->getReporter($format);
3✔
425
            } catch (\UnexpectedValueException $e) {
1✔
426
                $formats = $reporterFactory->getFormats();
1✔
427
                sort($formats);
1✔
428

429
                throw new InvalidConfigurationException(\sprintf('The format "%s" is not defined, supported are %s.', $format, Utils::naturalLanguageJoin($formats)));
1✔
430
            }
431
        }
432

433
        return $this->reporter;
2✔
434
    }
435

436
    public function getRiskyAllowed(): bool
437
    {
438
        if (null === $this->allowRisky) {
18✔
439
            if (null === $this->options['allow-risky']) {
18✔
440
                $this->allowRisky = $this->getConfig()->getRiskyAllowed();
10✔
441
            } else {
442
                $this->allowRisky = $this->resolveOptionBooleanValue('allow-risky');
8✔
443
            }
444
        }
445

446
        return $this->allowRisky;
17✔
447
    }
448

449
    /**
450
     * Returns rules.
451
     *
452
     * @return array<string, array<string, mixed>|bool>
453
     */
454
    public function getRules(): array
455
    {
456
        return $this->getRuleSet()->getRules();
11✔
457
    }
458

459
    public function getUsingCache(): bool
460
    {
461
        if (null === $this->usingCache) {
22✔
462
            if (null === $this->options['using-cache']) {
22✔
463
                $this->usingCache = $this->getConfig()->getUsingCache();
17✔
464
            } else {
465
                $this->usingCache = $this->resolveOptionBooleanValue('using-cache');
5✔
466
            }
467
        }
468

469
        $this->usingCache = $this->usingCache && $this->isCachingAllowedForRuntime();
22✔
470

471
        return $this->usingCache;
22✔
472
    }
473

474
    /**
475
     * @return iterable<\SplFileInfo>
476
     */
477
    public function getFinder(): iterable
478
    {
479
        if (null === $this->finder) {
31✔
480
            $this->finder = $this->resolveFinder();
31✔
481
        }
482

483
        return $this->finder;
26✔
484
    }
485

486
    /**
487
     * Returns dry-run flag.
488
     */
489
    public function isDryRun(): bool
490
    {
491
        if (null === $this->isDryRun) {
4✔
492
            if ($this->isStdIn()) {
4✔
493
                // Can't write to STDIN
494
                $this->isDryRun = true;
1✔
495
            } else {
496
                $this->isDryRun = $this->options['dry-run'];
3✔
497
            }
498
        }
499

500
        return $this->isDryRun;
4✔
501
    }
502

503
    public function shouldStopOnViolation(): bool
504
    {
505
        return $this->options['stop-on-violation'];
1✔
506
    }
507

508
    public function configFinderIsOverridden(): bool
509
    {
510
        if (null === $this->configFinderIsOverridden) {
7✔
511
            $this->resolveFinder();
7✔
512
        }
513

514
        return $this->configFinderIsOverridden;
7✔
515
    }
516

517
    /**
518
     * Compute file candidates for config file.
519
     *
520
     * @return list<string>
521
     */
522
    private function computeConfigFiles(): array
523
    {
524
        $configFile = $this->options['config'];
79✔
525

526
        if (null !== $configFile) {
79✔
527
            if (false === file_exists($configFile) || false === is_readable($configFile)) {
11✔
528
                throw new InvalidConfigurationException(\sprintf('Cannot read config file "%s".', $configFile));
×
529
            }
530

531
            return [$configFile];
11✔
532
        }
533

534
        $path = $this->getPath();
68✔
535

536
        if ($this->isStdIn() || 0 === \count($path)) {
68✔
537
            $configDir = $this->cwd;
45✔
538
        } elseif (1 < \count($path)) {
23✔
539
            throw new InvalidConfigurationException('For multiple paths config parameter is required.');
1✔
540
        } elseif (!is_file($path[0])) {
22✔
541
            $configDir = $path[0];
12✔
542
        } else {
543
            $dirName = pathinfo($path[0], PATHINFO_DIRNAME);
10✔
544
            $configDir = is_dir($dirName) ? $dirName : $path[0];
10✔
545
        }
546

547
        $candidates = [
67✔
548
            $configDir.\DIRECTORY_SEPARATOR.'.php-cs-fixer.php',
67✔
549
            $configDir.\DIRECTORY_SEPARATOR.'.php-cs-fixer.dist.php',
67✔
550
            $configDir.\DIRECTORY_SEPARATOR.'.php_cs', // old v2 config, present here only to throw nice error message later
67✔
551
            $configDir.\DIRECTORY_SEPARATOR.'.php_cs.dist', // old v2 config, present here only to throw nice error message later
67✔
552
        ];
67✔
553

554
        if ($configDir !== $this->cwd) {
67✔
555
            $candidates[] = $this->cwd.\DIRECTORY_SEPARATOR.'.php-cs-fixer.php';
22✔
556
            $candidates[] = $this->cwd.\DIRECTORY_SEPARATOR.'.php-cs-fixer.dist.php';
22✔
557
            $candidates[] = $this->cwd.\DIRECTORY_SEPARATOR.'.php_cs'; // old v2 config, present here only to throw nice error message later
22✔
558
            $candidates[] = $this->cwd.\DIRECTORY_SEPARATOR.'.php_cs.dist'; // old v2 config, present here only to throw nice error message later
22✔
559
        }
560

561
        return $candidates;
67✔
562
    }
563

564
    private function createFixerFactory(): FixerFactory
565
    {
566
        if (null === $this->fixerFactory) {
15✔
567
            $fixerFactory = new FixerFactory();
15✔
568
            $fixerFactory->registerBuiltInFixers();
15✔
569
            $fixerFactory->registerCustomFixers($this->getConfig()->getCustomFixers());
15✔
570

571
            $this->fixerFactory = $fixerFactory;
15✔
572
        }
573

574
        return $this->fixerFactory;
15✔
575
    }
576

577
    private function getFormat(): string
578
    {
579
        if (null === $this->format) {
14✔
580
            $this->format = $this->options['format'] ?? $this->getConfig()->getFormat();
14✔
581
        }
582

583
        return $this->format;
14✔
584
    }
585

586
    private function getRuleSet(): RuleSetInterface
587
    {
588
        if (null === $this->ruleSet) {
16✔
589
            $rules = $this->parseRules();
16✔
590
            $this->validateRules($rules);
15✔
591

592
            $this->ruleSet = new RuleSet($rules);
10✔
593
        }
594

595
        return $this->ruleSet;
10✔
596
    }
597

598
    private function isStdIn(): bool
599
    {
600
        if (null === $this->isStdIn) {
86✔
601
            $this->isStdIn = 1 === \count($this->options['path']) && '-' === $this->options['path'][0];
86✔
602
        }
603

604
        return $this->isStdIn;
86✔
605
    }
606

607
    /**
608
     * @template T
609
     *
610
     * @param iterable<T> $iterable
611
     *
612
     * @return \Traversable<T>
613
     */
614
    private function iterableToTraversable(iterable $iterable): \Traversable
615
    {
616
        return \is_array($iterable) ? new \ArrayIterator($iterable) : $iterable;
25✔
617
    }
618

619
    /**
620
     * @return array<string, mixed>
621
     */
622
    private function parseRules(): array
623
    {
624
        if (null === $this->options['rules']) {
16✔
625
            return $this->getConfig()->getRules();
7✔
626
        }
627

628
        $rules = trim($this->options['rules']);
9✔
629
        if ('' === $rules) {
9✔
630
            throw new InvalidConfigurationException('Empty rules value is not allowed.');
1✔
631
        }
632

633
        if (str_starts_with($rules, '{')) {
8✔
634
            $rules = json_decode($rules, true);
×
635

636
            if (JSON_ERROR_NONE !== json_last_error()) {
×
637
                throw new InvalidConfigurationException(\sprintf('Invalid JSON rules input: "%s".', json_last_error_msg()));
×
638
            }
639

640
            return $rules;
×
641
        }
642

643
        $rules = [];
8✔
644

645
        foreach (explode(',', $this->options['rules']) as $rule) {
8✔
646
            $rule = trim($rule);
8✔
647

648
            if ('' === $rule) {
8✔
649
                throw new InvalidConfigurationException('Empty rule name is not allowed.');
×
650
            }
651

652
            if (str_starts_with($rule, '-')) {
8✔
653
                $rules[substr($rule, 1)] = false;
2✔
654
            } else {
655
                $rules[$rule] = true;
8✔
656
            }
657
        }
658

659
        return $rules;
8✔
660
    }
661

662
    /**
663
     * @param array<string, mixed> $rules
664
     *
665
     * @throws InvalidConfigurationException
666
     */
667
    private function validateRules(array $rules): void
668
    {
669
        /**
670
         * Create a ruleset that contains all configured rules, even when they originally have been disabled.
671
         *
672
         * @see RuleSet::resolveSet()
673
         */
674
        $ruleSet = [];
15✔
675

676
        foreach ($rules as $key => $value) {
15✔
677
            if (\is_int($key)) {
15✔
678
                throw new InvalidConfigurationException(\sprintf('Missing value for "%s" rule/set.', $value));
×
679
            }
680

681
            $ruleSet[$key] = true;
15✔
682
        }
683

684
        $ruleSet = new RuleSet($ruleSet);
15✔
685

686
        $configuredFixers = array_keys($ruleSet->getRules());
15✔
687

688
        $fixers = $this->createFixerFactory()->getFixers();
15✔
689

690
        $availableFixers = array_map(static fn (FixerInterface $fixer): string => $fixer->getName(), $fixers);
15✔
691

692
        $unknownFixers = array_diff($configuredFixers, $availableFixers);
15✔
693

694
        if (\count($unknownFixers) > 0) {
15✔
695
            $renamedRules = [
5✔
696
                'blank_line_before_return' => [
5✔
697
                    'new_name' => 'blank_line_before_statement',
5✔
698
                    'config' => ['statements' => ['return']],
5✔
699
                ],
5✔
700
                'final_static_access' => [
5✔
701
                    'new_name' => 'self_static_accessor',
5✔
702
                ],
5✔
703
                'hash_to_slash_comment' => [
5✔
704
                    'new_name' => 'single_line_comment_style',
5✔
705
                    'config' => ['comment_types' => ['hash']],
5✔
706
                ],
5✔
707
                'lowercase_constants' => [
5✔
708
                    'new_name' => 'constant_case',
5✔
709
                    'config' => ['case' => 'lower'],
5✔
710
                ],
5✔
711
                'no_extra_consecutive_blank_lines' => [
5✔
712
                    'new_name' => 'no_extra_blank_lines',
5✔
713
                ],
5✔
714
                'no_multiline_whitespace_before_semicolons' => [
5✔
715
                    'new_name' => 'multiline_whitespace_before_semicolons',
5✔
716
                ],
5✔
717
                'no_short_echo_tag' => [
5✔
718
                    'new_name' => 'echo_tag_syntax',
5✔
719
                    'config' => ['format' => 'long'],
5✔
720
                ],
5✔
721
                'php_unit_ordered_covers' => [
5✔
722
                    'new_name' => 'phpdoc_order_by_value',
5✔
723
                    'config' => ['annotations' => ['covers']],
5✔
724
                ],
5✔
725
                'phpdoc_inline_tag' => [
5✔
726
                    'new_name' => 'general_phpdoc_tag_rename, phpdoc_inline_tag_normalizer and phpdoc_tag_type',
5✔
727
                ],
5✔
728
                'pre_increment' => [
5✔
729
                    'new_name' => 'increment_style',
5✔
730
                    'config' => ['style' => 'pre'],
5✔
731
                ],
5✔
732
                'psr0' => [
5✔
733
                    'new_name' => 'psr_autoloading',
5✔
734
                    'config' => ['dir' => 'x'],
5✔
735
                ],
5✔
736
                'psr4' => [
5✔
737
                    'new_name' => 'psr_autoloading',
5✔
738
                ],
5✔
739
                'silenced_deprecation_error' => [
5✔
740
                    'new_name' => 'error_suppression',
5✔
741
                ],
5✔
742
                'trailing_comma_in_multiline_array' => [
5✔
743
                    'new_name' => 'trailing_comma_in_multiline',
5✔
744
                    'config' => ['elements' => ['arrays']],
5✔
745
                ],
5✔
746
            ];
5✔
747

748
            $message = 'The rules contain unknown fixers: ';
5✔
749
            $hasOldRule = false;
5✔
750

751
            foreach ($unknownFixers as $unknownFixer) {
5✔
752
                if (isset($renamedRules[$unknownFixer])) { // Check if present as old renamed rule
5✔
753
                    $hasOldRule = true;
4✔
754
                    $message .= \sprintf(
4✔
755
                        '"%s" is renamed (did you mean "%s"?%s), ',
4✔
756
                        $unknownFixer,
4✔
757
                        $renamedRules[$unknownFixer]['new_name'],
4✔
758
                        isset($renamedRules[$unknownFixer]['config']) ? ' (note: use configuration "'.Utils::toString($renamedRules[$unknownFixer]['config']).'")' : ''
4✔
759
                    );
4✔
760
                } else { // Go to normal matcher if it is not a renamed rule
761
                    $matcher = new WordMatcher($availableFixers);
2✔
762
                    $alternative = $matcher->match($unknownFixer);
2✔
763
                    $message .= \sprintf(
2✔
764
                        '"%s"%s, ',
2✔
765
                        $unknownFixer,
2✔
766
                        null === $alternative ? '' : ' (did you mean "'.$alternative.'"?)'
2✔
767
                    );
2✔
768
                }
769
            }
770

771
            $message = substr($message, 0, -2).'.';
5✔
772

773
            if ($hasOldRule) {
5✔
774
                $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.";
4✔
775
            }
776

777
            throw new InvalidConfigurationException($message);
5✔
778
        }
779

780
        foreach ($fixers as $fixer) {
10✔
781
            $fixerName = $fixer->getName();
10✔
782
            if (isset($rules[$fixerName]) && $fixer instanceof DeprecatedFixerInterface) {
10✔
783
                $successors = $fixer->getSuccessorsNames();
3✔
784
                $messageEnd = [] === $successors
3✔
785
                    ? \sprintf(' and will be removed in version %d.0.', Application::getMajorVersion() + 1)
×
786
                    : \sprintf('. Use %s instead.', str_replace('`', '"', Utils::naturalLanguageJoinWithBackticks($successors)));
3✔
787

788
                Utils::triggerDeprecation(new \RuntimeException("Rule \"{$fixerName}\" is deprecated{$messageEnd}"));
3✔
789
            }
790
        }
791
    }
792

793
    /**
794
     * Apply path on config instance.
795
     *
796
     * @return iterable<\SplFileInfo>
797
     */
798
    private function resolveFinder(): iterable
799
    {
800
        $this->configFinderIsOverridden = false;
31✔
801

802
        if ($this->isStdIn()) {
31✔
803
            return new \ArrayIterator([new StdinFileInfo()]);
×
804
        }
805

806
        $modes = [self::PATH_MODE_OVERRIDE, self::PATH_MODE_INTERSECTION];
31✔
807

808
        if (!\in_array(
31✔
809
            $this->options['path-mode'],
31✔
810
            $modes,
31✔
811
            true
31✔
812
        )) {
31✔
813
            throw new InvalidConfigurationException(\sprintf(
×
814
                'The path-mode "%s" is not defined, supported are %s.',
×
815
                $this->options['path-mode'],
×
816
                Utils::naturalLanguageJoin($modes)
×
817
            ));
×
818
        }
819

820
        $isIntersectionPathMode = self::PATH_MODE_INTERSECTION === $this->options['path-mode'];
31✔
821

822
        $paths = array_map(
31✔
823
            static fn (string $path) => realpath($path),
31✔
824
            $this->getPath()
31✔
825
        );
31✔
826

827
        if (0 === \count($paths)) {
26✔
828
            if ($isIntersectionPathMode) {
5✔
829
                return new \ArrayIterator([]);
1✔
830
            }
831

832
            return $this->iterableToTraversable($this->getConfig()->getFinder());
4✔
833
        }
834

835
        $pathsByType = [
21✔
836
            'file' => [],
21✔
837
            'dir' => [],
21✔
838
        ];
21✔
839

840
        foreach ($paths as $path) {
21✔
841
            if (is_file($path)) {
21✔
842
                $pathsByType['file'][] = $path;
11✔
843
            } else {
844
                $pathsByType['dir'][] = $path.\DIRECTORY_SEPARATOR;
12✔
845
            }
846
        }
847

848
        $nestedFinder = null;
21✔
849
        $currentFinder = $this->iterableToTraversable($this->getConfig()->getFinder());
21✔
850

851
        try {
852
            $nestedFinder = $currentFinder instanceof \IteratorAggregate ? $currentFinder->getIterator() : $currentFinder;
21✔
853
        } catch (\Exception $e) {
4✔
854
        }
855

856
        if ($isIntersectionPathMode) {
21✔
857
            if (null === $nestedFinder) {
11✔
858
                throw new InvalidConfigurationException(
×
859
                    'Cannot create intersection with not-fully defined Finder in configuration file.'
×
860
                );
×
861
            }
862

863
            return new \CallbackFilterIterator(
11✔
864
                new \IteratorIterator($nestedFinder),
11✔
865
                static function (\SplFileInfo $current) use ($pathsByType): bool {
11✔
866
                    $currentRealPath = $current->getRealPath();
10✔
867

868
                    if (\in_array($currentRealPath, $pathsByType['file'], true)) {
10✔
869
                        return true;
3✔
870
                    }
871

872
                    foreach ($pathsByType['dir'] as $path) {
10✔
873
                        if (str_starts_with($currentRealPath, $path)) {
5✔
874
                            return true;
4✔
875
                        }
876
                    }
877

878
                    return false;
10✔
879
                }
11✔
880
            );
11✔
881
        }
882

883
        if (null !== $this->getConfigFile() && null !== $nestedFinder) {
10✔
884
            $this->configFinderIsOverridden = true;
3✔
885
        }
886

887
        if ($currentFinder instanceof SymfonyFinder && null === $nestedFinder) {
10✔
888
            // finder from configuration Symfony finder and it is not fully defined, we may fulfill it
889
            return $currentFinder->in($pathsByType['dir'])->append($pathsByType['file']);
4✔
890
        }
891

892
        return Finder::create()->in($pathsByType['dir'])->append($pathsByType['file']);
6✔
893
    }
894

895
    /**
896
     * Set option that will be resolved.
897
     *
898
     * @param mixed $value
899
     */
900
    private function setOption(string $name, $value): void
901
    {
902
        if (!\array_key_exists($name, $this->options)) {
97✔
903
            throw new InvalidConfigurationException(\sprintf('Unknown option name: "%s".', $name));
1✔
904
        }
905

906
        $this->options[$name] = $value;
96✔
907
    }
908

909
    /**
910
     * @param key-of<_Options> $optionName
911
     */
912
    private function resolveOptionBooleanValue(string $optionName): bool
913
    {
914
        $value = $this->options[$optionName];
12✔
915

916
        if ('yes' === $value) {
12✔
917
            return true;
6✔
918
        }
919

920
        if ('no' === $value) {
7✔
921
            return false;
6✔
922
        }
923

924
        throw new InvalidConfigurationException(\sprintf('Expected "yes" or "no" for option "%s", got "%s".', $optionName, \is_object($value) ? \get_class($value) : (\is_scalar($value) ? $value : \gettype($value))));
1✔
925
    }
926

927
    private static function separatedContextLessInclude(string $path): ConfigInterface
928
    {
929
        $config = include $path;
20✔
930

931
        // verify that the config has an instance of Config
932
        if (!$config instanceof ConfigInterface) {
20✔
933
            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)));
1✔
934
        }
935

936
        return $config;
19✔
937
    }
938

939
    private function isCachingAllowedForRuntime(): bool
940
    {
941
        return $this->toolInfo->isInstalledAsPhar()
14✔
942
            || $this->toolInfo->isInstalledByComposer()
14✔
943
            || $this->toolInfo->isRunInsideDocker()
14✔
944
            || filter_var(getenv('PHP_CS_FIXER_ENFORCE_CACHE'), FILTER_VALIDATE_BOOL);
14✔
945
    }
946
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc