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

keradus / PHP-CS-Fixer / 16715837382

04 Aug 2025 06:34AM UTC coverage: 94.728% (+0.01%) from 94.716%
16715837382

push

github

keradus
chore: switch to official checkstyle.xsd

28212 of 29782 relevant lines covered (94.73%)

45.91 hits per line

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

89.9
/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\UnsupportedPhpVersionAllowedConfigInterface;
46
use PhpCsFixer\Utils;
47
use PhpCsFixer\WhitespacesFixerConfig;
48
use PhpCsFixer\WordMatcher;
49
use Symfony\Component\Filesystem\Filesystem;
50
use Symfony\Component\Finder\Finder as SymfonyFinder;
51

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

84
    private ?bool $allowRisky = null;
85

86
    private ?ConfigInterface $config = null;
87

88
    private ?string $configFile = null;
89

90
    private string $cwd;
91

92
    private ConfigInterface $defaultConfig;
93

94
    private ?ReporterInterface $reporter = null;
95

96
    private ?bool $isStdIn = null;
97

98
    private ?bool $isDryRun = null;
99

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

105
    private ?bool $configFinderIsOverridden = null;
106

107
    private ToolInfoInterface $toolInfo;
108

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

130
    private ?string $cacheFile = null;
131

132
    private ?CacheManagerInterface $cacheManager = null;
133

134
    private ?DifferInterface $differ = null;
135

136
    private ?Directory $directory = null;
137

138
    /**
139
     * @var null|iterable<\SplFileInfo>
140
     */
141
    private ?iterable $finder = null;
142

143
    private ?string $format = null;
144

145
    private ?Linter $linter = null;
146

147
    /**
148
     * @var null|list<string>
149
     */
150
    private ?array $path = null;
151

152
    /**
153
     * @var null|ProgressOutputType::*
154
     */
155
    private $progress;
156

157
    private ?RuleSet $ruleSet = null;
158

159
    private ?bool $usingCache = null;
160

161
    private ?bool $isUnsupportedPhpVersionAllowed = null;
162

163
    private ?FixerFactory $fixerFactory = null;
164

165
    /**
166
     * @param array<string, mixed> $options
167
     */
168
    public function __construct(
169
        ConfigInterface $config,
170
        array $options,
171
        string $cwd,
172
        ToolInfoInterface $toolInfo
173
    ) {
174
        $this->defaultConfig = $config;
126✔
175
        $this->cwd = $cwd;
126✔
176
        $this->toolInfo = $toolInfo;
126✔
177

178
        foreach ($options as $name => $value) {
126✔
179
            $this->setOption($name, $value);
102✔
180
        }
181
    }
182

183
    public function getCacheFile(): ?string
184
    {
185
        if (!$this->getUsingCache()) {
10✔
186
            return null;
4✔
187
        }
188

189
        if (null === $this->cacheFile) {
6✔
190
            if (null === $this->options['cache-file']) {
6✔
191
                $this->cacheFile = $this->getConfig()->getCacheFile();
4✔
192
            } else {
193
                $this->cacheFile = $this->options['cache-file'];
2✔
194
            }
195
        }
196

197
        return $this->cacheFile;
6✔
198
    }
199

200
    public function getCacheManager(): CacheManagerInterface
201
    {
202
        if (null === $this->cacheManager) {
1✔
203
            $cacheFile = $this->getCacheFile();
1✔
204

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

223
        return $this->cacheManager;
1✔
224
    }
225

226
    public function getConfig(): ConfigInterface
227
    {
228
        if (null === $this->config) {
79✔
229
            foreach ($this->computeConfigFiles() as $configFile) {
79✔
230
                if (!file_exists($configFile)) {
78✔
231
                    continue;
63✔
232
                }
233

234
                $configFileBasename = basename($configFile);
20✔
235

236
                /** @TODO v4 drop handling (triggering error) for v2 config names */
237
                $deprecatedConfigs = [
20✔
238
                    '.php_cs' => '.php-cs-fixer.php',
20✔
239
                    '.php_cs.dist' => '.php-cs-fixer.dist.php',
20✔
240
                ];
20✔
241

242
                if (isset($deprecatedConfigs[$configFileBasename])) {
20✔
243
                    throw new InvalidConfigurationException("Configuration file `{$configFileBasename}` is outdated, rename to `{$deprecatedConfigs[$configFileBasename]}`.");
×
244
                }
245

246
                $this->config = self::separatedContextLessInclude($configFile);
20✔
247
                $this->configFile = $configFile;
19✔
248

249
                break;
19✔
250
            }
251

252
            if (null === $this->config) {
77✔
253
                $this->config = $this->defaultConfig;
58✔
254
            }
255
        }
256

257
        return $this->config;
77✔
258
    }
259

260
    public function getParallelConfig(): ParallelConfig
261
    {
262
        $config = $this->getConfig();
3✔
263

264
        return true !== $this->options['sequential'] && $config instanceof ParallelAwareConfigInterface
3✔
265
            ? $config->getParallelConfig()
2✔
266
            : ParallelConfigFactory::sequential();
3✔
267
    }
268

269
    public function getConfigFile(): ?string
270
    {
271
        if (null === $this->configFile) {
19✔
272
            $this->getConfig();
14✔
273
        }
274

275
        return $this->configFile;
19✔
276
    }
277

278
    public function getDiffer(): DifferInterface
279
    {
280
        if (null === $this->differ) {
4✔
281
            $this->differ = (true === $this->options['diff']) ? new UnifiedDiffer() : new NullDiffer();
4✔
282
        }
283

284
        return $this->differ;
4✔
285
    }
286

287
    public function getDirectory(): DirectoryInterface
288
    {
289
        if (null === $this->directory) {
4✔
290
            $path = $this->getCacheFile();
4✔
291
            if (null === $path) {
4✔
292
                $absolutePath = $this->cwd;
1✔
293
            } else {
294
                $filesystem = new Filesystem();
3✔
295

296
                $absolutePath = $filesystem->isAbsolutePath($path)
3✔
297
                    ? $path
2✔
298
                    : $this->cwd.\DIRECTORY_SEPARATOR.$path;
1✔
299
                $absolutePath = \dirname($absolutePath);
3✔
300
            }
301

302
            $this->directory = new Directory($absolutePath);
4✔
303
        }
304

305
        return $this->directory;
4✔
306
    }
307

308
    /**
309
     * @return list<FixerInterface>
310
     */
311
    public function getFixers(): array
312
    {
313
        if (null === $this->fixers) {
5✔
314
            $this->fixers = $this->createFixerFactory()
5✔
315
                ->useRuleSet($this->getRuleSet())
5✔
316
                ->setWhitespacesConfig(new WhitespacesFixerConfig($this->config->getIndent(), $this->config->getLineEnding()))
5✔
317
                ->getFixers()
5✔
318
            ;
5✔
319

320
            if (false === $this->getRiskyAllowed()) {
5✔
321
                $riskyFixers = array_map(
3✔
322
                    static fn (FixerInterface $fixer): string => $fixer->getName(),
3✔
323
                    array_filter(
3✔
324
                        $this->fixers,
3✔
325
                        static fn (FixerInterface $fixer): bool => $fixer->isRisky()
3✔
326
                    )
3✔
327
                );
3✔
328

329
                if (\count($riskyFixers) > 0) {
3✔
330
                    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)));
×
331
                }
332
            }
333
        }
334

335
        return $this->fixers;
5✔
336
    }
337

338
    public function getLinter(): LinterInterface
339
    {
340
        if (null === $this->linter) {
1✔
341
            $this->linter = new Linter();
1✔
342
        }
343

344
        return $this->linter;
1✔
345
    }
346

347
    /**
348
     * Returns path.
349
     *
350
     * @return list<string>
351
     */
352
    public function getPath(): array
353
    {
354
        if (null === $this->path) {
93✔
355
            $filesystem = new Filesystem();
93✔
356
            $cwd = $this->cwd;
93✔
357

358
            if (1 === \count($this->options['path']) && '-' === $this->options['path'][0]) {
93✔
359
                $this->path = $this->options['path'];
×
360
            } else {
361
                $this->path = array_map(
93✔
362
                    static function (string $rawPath) use ($cwd, $filesystem): string {
93✔
363
                        $path = trim($rawPath);
46✔
364

365
                        if ('' === $path) {
46✔
366
                            throw new InvalidConfigurationException("Invalid path: \"{$rawPath}\".");
6✔
367
                        }
368

369
                        $absolutePath = $filesystem->isAbsolutePath($path)
42✔
370
                            ? $path
37✔
371
                            : $cwd.\DIRECTORY_SEPARATOR.$path;
5✔
372

373
                        if (!file_exists($absolutePath)) {
42✔
374
                            throw new InvalidConfigurationException(\sprintf(
5✔
375
                                'The path "%s" is not readable.',
5✔
376
                                $path
5✔
377
                            ));
5✔
378
                        }
379

380
                        return $absolutePath;
37✔
381
                    },
93✔
382
                    $this->options['path']
93✔
383
                );
93✔
384
            }
385
        }
386

387
        return $this->path;
82✔
388
    }
389

390
    /**
391
     * @return ProgressOutputType::*
392
     *
393
     * @throws InvalidConfigurationException
394
     */
395
    public function getProgressType(): string
396
    {
397
        if (null === $this->progress) {
13✔
398
            if ('txt' === $this->resolveFormat()) {
13✔
399
                $progressType = $this->options['show-progress'];
11✔
400

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

413
                $this->progress = $progressType;
10✔
414
            } else {
415
                $this->progress = ProgressOutputType::NONE;
2✔
416
            }
417
        }
418

419
        return $this->progress;
12✔
420
    }
421

422
    public function getReporter(): ReporterInterface
423
    {
424
        if (null === $this->reporter) {
8✔
425
            $reporterFactory = new ReporterFactory();
8✔
426
            $reporterFactory->registerBuiltInReporters();
8✔
427

428
            $format = $this->resolveFormat();
8✔
429

430
            try {
431
                $this->reporter = $reporterFactory->getReporter($format);
8✔
432
            } catch (\UnexpectedValueException $e) {
1✔
433
                $formats = $reporterFactory->getFormats();
1✔
434
                sort($formats);
1✔
435

436
                throw new InvalidConfigurationException(\sprintf('The format "%s" is not defined, supported are %s.', $format, Utils::naturalLanguageJoin($formats)));
1✔
437
            }
438
        }
439

440
        return $this->reporter;
7✔
441
    }
442

443
    public function getRiskyAllowed(): bool
444
    {
445
        if (null === $this->allowRisky) {
18✔
446
            if (null === $this->options['allow-risky']) {
18✔
447
                $this->allowRisky = $this->getConfig()->getRiskyAllowed();
10✔
448
            } else {
449
                $this->allowRisky = $this->resolveOptionBooleanValue('allow-risky');
8✔
450
            }
451
        }
452

453
        return $this->allowRisky;
17✔
454
    }
455

456
    /**
457
     * Returns rules.
458
     *
459
     * @return array<string, array<string, mixed>|bool>
460
     */
461
    public function getRules(): array
462
    {
463
        return $this->getRuleSet()->getRules();
11✔
464
    }
465

466
    public function getUsingCache(): bool
467
    {
468
        if (null === $this->usingCache) {
22✔
469
            if (null === $this->options['using-cache']) {
22✔
470
                $this->usingCache = $this->getConfig()->getUsingCache();
17✔
471
            } else {
472
                $this->usingCache = $this->resolveOptionBooleanValue('using-cache');
5✔
473
            }
474
        }
475

476
        $this->usingCache = $this->usingCache && $this->isCachingAllowedForRuntime();
22✔
477

478
        return $this->usingCache;
22✔
479
    }
480

481
    public function getUnsupportedPhpVersionAllowed(): bool
482
    {
483
        if (null === $this->isUnsupportedPhpVersionAllowed) {
×
484
            if (null === $this->options['allow-unsupported-php-version']) {
×
485
                $config = $this->getConfig();
×
486
                $this->isUnsupportedPhpVersionAllowed = $config instanceof UnsupportedPhpVersionAllowedConfigInterface
×
487
                    ? $config->getUnsupportedPhpVersionAllowed()
×
488
                    : false;
×
489
            } else {
490
                $this->isUnsupportedPhpVersionAllowed = $this->resolveOptionBooleanValue('allow-unsupported-php-version');
×
491
            }
492
        }
493

494
        return $this->isUnsupportedPhpVersionAllowed;
×
495
    }
496

497
    /**
498
     * @return iterable<\SplFileInfo>
499
     */
500
    public function getFinder(): iterable
501
    {
502
        if (null === $this->finder) {
31✔
503
            $this->finder = $this->resolveFinder();
31✔
504
        }
505

506
        return $this->finder;
26✔
507
    }
508

509
    /**
510
     * Returns dry-run flag.
511
     */
512
    public function isDryRun(): bool
513
    {
514
        if (null === $this->isDryRun) {
4✔
515
            if ($this->isStdIn()) {
4✔
516
                // Can't write to STDIN
517
                $this->isDryRun = true;
1✔
518
            } else {
519
                $this->isDryRun = $this->options['dry-run'];
3✔
520
            }
521
        }
522

523
        return $this->isDryRun;
4✔
524
    }
525

526
    public function shouldStopOnViolation(): bool
527
    {
528
        return $this->options['stop-on-violation'];
1✔
529
    }
530

531
    public function configFinderIsOverridden(): bool
532
    {
533
        if (null === $this->configFinderIsOverridden) {
7✔
534
            $this->resolveFinder();
7✔
535
        }
536

537
        return $this->configFinderIsOverridden;
7✔
538
    }
539

540
    /**
541
     * Compute file candidates for config file.
542
     *
543
     * @return list<string>
544
     */
545
    private function computeConfigFiles(): array
546
    {
547
        $configFile = $this->options['config'];
79✔
548

549
        if (null !== $configFile) {
79✔
550
            if (false === file_exists($configFile) || false === is_readable($configFile)) {
11✔
551
                throw new InvalidConfigurationException(\sprintf('Cannot read config file "%s".', $configFile));
×
552
            }
553

554
            return [$configFile];
11✔
555
        }
556

557
        $path = $this->getPath();
68✔
558

559
        if ($this->isStdIn() || 0 === \count($path)) {
68✔
560
            $configDir = $this->cwd;
45✔
561
        } elseif (1 < \count($path)) {
23✔
562
            throw new InvalidConfigurationException('For multiple paths config parameter is required.');
1✔
563
        } elseif (!is_file($path[0])) {
22✔
564
            $configDir = $path[0];
12✔
565
        } else {
566
            $dirName = pathinfo($path[0], \PATHINFO_DIRNAME);
10✔
567
            $configDir = is_dir($dirName) ? $dirName : $path[0];
10✔
568
        }
569

570
        $candidates = [
67✔
571
            $configDir.\DIRECTORY_SEPARATOR.'.php-cs-fixer.php',
67✔
572
            $configDir.\DIRECTORY_SEPARATOR.'.php-cs-fixer.dist.php',
67✔
573

574
            // @TODO v4 drop handling (triggering error) for v2 config names
575
            $configDir.\DIRECTORY_SEPARATOR.'.php_cs', // old v2 config, present here only to throw nice error message later
67✔
576
            $configDir.\DIRECTORY_SEPARATOR.'.php_cs.dist', // old v2 config, present here only to throw nice error message later
67✔
577
        ];
67✔
578

579
        if ($configDir !== $this->cwd) {
67✔
580
            $candidates[] = $this->cwd.\DIRECTORY_SEPARATOR.'.php-cs-fixer.php';
22✔
581
            $candidates[] = $this->cwd.\DIRECTORY_SEPARATOR.'.php-cs-fixer.dist.php';
22✔
582

583
            // @TODO v4 drop handling (triggering error) for v2 config names
584
            $candidates[] = $this->cwd.\DIRECTORY_SEPARATOR.'.php_cs'; // old v2 config, present here only to throw nice error message later
22✔
585
            $candidates[] = $this->cwd.\DIRECTORY_SEPARATOR.'.php_cs.dist'; // old v2 config, present here only to throw nice error message later
22✔
586
        }
587

588
        return $candidates;
67✔
589
    }
590

591
    private function createFixerFactory(): FixerFactory
592
    {
593
        if (null === $this->fixerFactory) {
15✔
594
            $fixerFactory = new FixerFactory();
15✔
595
            $fixerFactory->registerBuiltInFixers();
15✔
596
            $fixerFactory->registerCustomFixers($this->getConfig()->getCustomFixers());
15✔
597

598
            $this->fixerFactory = $fixerFactory;
15✔
599
        }
600

601
        return $this->fixerFactory;
15✔
602
    }
603

604
    private function resolveFormat(): string
605
    {
606
        if (null === $this->format) {
19✔
607
            $formatCandidate = $this->options['format'] ?? $this->getConfig()->getFormat();
19✔
608
            $parts = explode(',', $formatCandidate);
19✔
609

610
            if (\count($parts) > 2) {
19✔
611
                throw new InvalidConfigurationException(\sprintf('The format "%s" is invalid.', $formatCandidate));
×
612
            }
613

614
            $this->format = $parts[0];
19✔
615

616
            if ('@auto' === $this->format) {
19✔
617
                $this->format = $parts[1] ?? 'txt';
3✔
618

619
                if (filter_var(getenv('GITLAB_CI'), \FILTER_VALIDATE_BOOL)) {
3✔
620
                    $this->format = 'gitlab';
1✔
621
                }
622
            }
623
        }
624

625
        return $this->format;
19✔
626
    }
627

628
    private function getRuleSet(): RuleSetInterface
629
    {
630
        if (null === $this->ruleSet) {
16✔
631
            $rules = $this->parseRules();
16✔
632
            $this->validateRules($rules);
15✔
633

634
            $this->ruleSet = new RuleSet($rules);
10✔
635
        }
636

637
        return $this->ruleSet;
10✔
638
    }
639

640
    private function isStdIn(): bool
641
    {
642
        if (null === $this->isStdIn) {
86✔
643
            $this->isStdIn = 1 === \count($this->options['path']) && '-' === $this->options['path'][0];
86✔
644
        }
645

646
        return $this->isStdIn;
86✔
647
    }
648

649
    /**
650
     * @template T
651
     *
652
     * @param iterable<T> $iterable
653
     *
654
     * @return \Traversable<T>
655
     */
656
    private function iterableToTraversable(iterable $iterable): \Traversable
657
    {
658
        return \is_array($iterable) ? new \ArrayIterator($iterable) : $iterable;
25✔
659
    }
660

661
    /**
662
     * @return array<string, mixed>
663
     */
664
    private function parseRules(): array
665
    {
666
        if (null === $this->options['rules']) {
16✔
667
            return $this->getConfig()->getRules();
7✔
668
        }
669

670
        $rules = trim($this->options['rules']);
9✔
671
        if ('' === $rules) {
9✔
672
            throw new InvalidConfigurationException('Empty rules value is not allowed.');
1✔
673
        }
674

675
        if (str_starts_with($rules, '{')) {
8✔
676
            $rules = json_decode($rules, true);
×
677

678
            if (\JSON_ERROR_NONE !== json_last_error()) {
×
679
                throw new InvalidConfigurationException(\sprintf('Invalid JSON rules input: "%s".', json_last_error_msg()));
×
680
            }
681

682
            return $rules;
×
683
        }
684

685
        $rules = [];
8✔
686

687
        foreach (explode(',', $this->options['rules']) as $rule) {
8✔
688
            $rule = trim($rule);
8✔
689

690
            if ('' === $rule) {
8✔
691
                throw new InvalidConfigurationException('Empty rule name is not allowed.');
×
692
            }
693

694
            if (str_starts_with($rule, '-')) {
8✔
695
                $rules[substr($rule, 1)] = false;
2✔
696
            } else {
697
                $rules[$rule] = true;
8✔
698
            }
699
        }
700

701
        return $rules;
8✔
702
    }
703

704
    /**
705
     * @param array<string, mixed> $rules
706
     *
707
     * @throws InvalidConfigurationException
708
     */
709
    private function validateRules(array $rules): void
710
    {
711
        /**
712
         * Create a ruleset that contains all configured rules, even when they originally have been disabled.
713
         *
714
         * @see RuleSet::resolveSet()
715
         */
716
        $ruleSet = [];
15✔
717

718
        foreach ($rules as $key => $value) {
15✔
719
            if (\is_int($key)) {
15✔
720
                throw new InvalidConfigurationException(\sprintf('Missing value for "%s" rule/set.', $value));
×
721
            }
722

723
            $ruleSet[$key] = true;
15✔
724
        }
725

726
        $ruleSet = new RuleSet($ruleSet);
15✔
727

728
        $configuredFixers = array_keys($ruleSet->getRules());
15✔
729

730
        $fixers = $this->createFixerFactory()->getFixers();
15✔
731

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

734
        $unknownFixers = array_diff($configuredFixers, $availableFixers);
15✔
735

736
        if (\count($unknownFixers) > 0) {
15✔
737
            $renamedRules = [
5✔
738
                'blank_line_before_return' => [
5✔
739
                    'new_name' => 'blank_line_before_statement',
5✔
740
                    'config' => ['statements' => ['return']],
5✔
741
                ],
5✔
742
                'final_static_access' => [
5✔
743
                    'new_name' => 'self_static_accessor',
5✔
744
                ],
5✔
745
                'hash_to_slash_comment' => [
5✔
746
                    'new_name' => 'single_line_comment_style',
5✔
747
                    'config' => ['comment_types' => ['hash']],
5✔
748
                ],
5✔
749
                'lowercase_constants' => [
5✔
750
                    'new_name' => 'constant_case',
5✔
751
                    'config' => ['case' => 'lower'],
5✔
752
                ],
5✔
753
                'no_extra_consecutive_blank_lines' => [
5✔
754
                    'new_name' => 'no_extra_blank_lines',
5✔
755
                ],
5✔
756
                'no_multiline_whitespace_before_semicolons' => [
5✔
757
                    'new_name' => 'multiline_whitespace_before_semicolons',
5✔
758
                ],
5✔
759
                'no_short_echo_tag' => [
5✔
760
                    'new_name' => 'echo_tag_syntax',
5✔
761
                    'config' => ['format' => 'long'],
5✔
762
                ],
5✔
763
                'php_unit_ordered_covers' => [
5✔
764
                    'new_name' => 'phpdoc_order_by_value',
5✔
765
                    'config' => ['annotations' => ['covers']],
5✔
766
                ],
5✔
767
                'phpdoc_inline_tag' => [
5✔
768
                    'new_name' => 'general_phpdoc_tag_rename, phpdoc_inline_tag_normalizer and phpdoc_tag_type',
5✔
769
                ],
5✔
770
                'pre_increment' => [
5✔
771
                    'new_name' => 'increment_style',
5✔
772
                    'config' => ['style' => 'pre'],
5✔
773
                ],
5✔
774
                'psr0' => [
5✔
775
                    'new_name' => 'psr_autoloading',
5✔
776
                    'config' => ['dir' => 'x'],
5✔
777
                ],
5✔
778
                'psr4' => [
5✔
779
                    'new_name' => 'psr_autoloading',
5✔
780
                ],
5✔
781
                'silenced_deprecation_error' => [
5✔
782
                    'new_name' => 'error_suppression',
5✔
783
                ],
5✔
784
                'trailing_comma_in_multiline_array' => [
5✔
785
                    'new_name' => 'trailing_comma_in_multiline',
5✔
786
                    'config' => ['elements' => ['arrays']],
5✔
787
                ],
5✔
788
            ];
5✔
789

790
            $message = 'The rules contain unknown fixers: ';
5✔
791
            $hasOldRule = false;
5✔
792

793
            foreach ($unknownFixers as $unknownFixer) {
5✔
794
                if (isset($renamedRules[$unknownFixer])) { // Check if present as old renamed rule
5✔
795
                    $hasOldRule = true;
4✔
796
                    $message .= \sprintf(
4✔
797
                        '"%s" is renamed (did you mean "%s"?%s), ',
4✔
798
                        $unknownFixer,
4✔
799
                        $renamedRules[$unknownFixer]['new_name'],
4✔
800
                        isset($renamedRules[$unknownFixer]['config']) ? ' (note: use configuration "'.Utils::toString($renamedRules[$unknownFixer]['config']).'")' : ''
4✔
801
                    );
4✔
802
                } else { // Go to normal matcher if it is not a renamed rule
803
                    $matcher = new WordMatcher($availableFixers);
2✔
804
                    $alternative = $matcher->match($unknownFixer);
2✔
805
                    $message .= \sprintf(
2✔
806
                        '"%s"%s, ',
2✔
807
                        $unknownFixer,
2✔
808
                        null === $alternative ? '' : ' (did you mean "'.$alternative.'"?)'
2✔
809
                    );
2✔
810
                }
811
            }
812

813
            $message = substr($message, 0, -2).'.';
5✔
814

815
            if ($hasOldRule) {
5✔
816
                $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✔
817
            }
818

819
            throw new InvalidConfigurationException($message);
5✔
820
        }
821

822
        foreach ($fixers as $fixer) {
10✔
823
            $fixerName = $fixer->getName();
10✔
824
            if (isset($rules[$fixerName]) && $fixer instanceof DeprecatedFixerInterface) {
10✔
825
                $successors = $fixer->getSuccessorsNames();
3✔
826
                $messageEnd = [] === $successors
3✔
827
                    ? \sprintf(' and will be removed in version %d.0.', Application::getMajorVersion() + 1)
×
828
                    : \sprintf('. Use %s instead.', str_replace('`', '"', Utils::naturalLanguageJoinWithBackticks($successors)));
3✔
829

830
                Utils::triggerDeprecation(new \RuntimeException("Rule \"{$fixerName}\" is deprecated{$messageEnd}"));
3✔
831
            }
832
        }
833
    }
834

835
    /**
836
     * Apply path on config instance.
837
     *
838
     * @return iterable<\SplFileInfo>
839
     */
840
    private function resolveFinder(): iterable
841
    {
842
        $this->configFinderIsOverridden = false;
31✔
843

844
        if ($this->isStdIn()) {
31✔
845
            return new \ArrayIterator([new StdinFileInfo()]);
×
846
        }
847

848
        $modes = [self::PATH_MODE_OVERRIDE, self::PATH_MODE_INTERSECTION];
31✔
849

850
        if (!\in_array(
31✔
851
            $this->options['path-mode'],
31✔
852
            $modes,
31✔
853
            true
31✔
854
        )) {
31✔
855
            throw new InvalidConfigurationException(\sprintf(
×
856
                'The path-mode "%s" is not defined, supported are %s.',
×
857
                $this->options['path-mode'],
×
858
                Utils::naturalLanguageJoin($modes)
×
859
            ));
×
860
        }
861

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

864
        $paths = array_map(
31✔
865
            static fn (string $path) => realpath($path),
31✔
866
            $this->getPath()
31✔
867
        );
31✔
868

869
        if (0 === \count($paths)) {
26✔
870
            if ($isIntersectionPathMode) {
5✔
871
                return new \ArrayIterator([]);
1✔
872
            }
873

874
            return $this->iterableToTraversable($this->getConfig()->getFinder());
4✔
875
        }
876

877
        $pathsByType = [
21✔
878
            'file' => [],
21✔
879
            'dir' => [],
21✔
880
        ];
21✔
881

882
        foreach ($paths as $path) {
21✔
883
            if (is_file($path)) {
21✔
884
                $pathsByType['file'][] = $path;
11✔
885
            } else {
886
                $pathsByType['dir'][] = $path.\DIRECTORY_SEPARATOR;
12✔
887
            }
888
        }
889

890
        $nestedFinder = null;
21✔
891
        $currentFinder = $this->iterableToTraversable($this->getConfig()->getFinder());
21✔
892

893
        try {
894
            $nestedFinder = $currentFinder instanceof \IteratorAggregate ? $currentFinder->getIterator() : $currentFinder;
21✔
895
        } catch (\Exception $e) {
4✔
896
        }
897

898
        if ($isIntersectionPathMode) {
21✔
899
            if (null === $nestedFinder) {
11✔
900
                throw new InvalidConfigurationException(
×
901
                    'Cannot create intersection with not-fully defined Finder in configuration file.'
×
902
                );
×
903
            }
904

905
            return new \CallbackFilterIterator(
11✔
906
                new \IteratorIterator($nestedFinder),
11✔
907
                static function (\SplFileInfo $current) use ($pathsByType): bool {
11✔
908
                    $currentRealPath = $current->getRealPath();
10✔
909

910
                    if (\in_array($currentRealPath, $pathsByType['file'], true)) {
10✔
911
                        return true;
3✔
912
                    }
913

914
                    foreach ($pathsByType['dir'] as $path) {
10✔
915
                        if (str_starts_with($currentRealPath, $path)) {
5✔
916
                            return true;
4✔
917
                        }
918
                    }
919

920
                    return false;
10✔
921
                }
11✔
922
            );
11✔
923
        }
924

925
        if (null !== $this->getConfigFile() && null !== $nestedFinder) {
10✔
926
            $this->configFinderIsOverridden = true;
3✔
927
        }
928

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

934
        return Finder::create()->in($pathsByType['dir'])->append($pathsByType['file']);
6✔
935
    }
936

937
    /**
938
     * Set option that will be resolved.
939
     *
940
     * @param mixed $value
941
     */
942
    private function setOption(string $name, $value): void
943
    {
944
        if (!\array_key_exists($name, $this->options)) {
102✔
945
            throw new InvalidConfigurationException(\sprintf('Unknown option name: "%s".', $name));
1✔
946
        }
947

948
        $this->options[$name] = $value;
101✔
949
    }
950

951
    /**
952
     * @param key-of<_Options> $optionName
953
     */
954
    private function resolveOptionBooleanValue(string $optionName): bool
955
    {
956
        $value = $this->options[$optionName];
12✔
957

958
        if ('yes' === $value) {
12✔
959
            return true;
6✔
960
        }
961

962
        if ('no' === $value) {
7✔
963
            return false;
6✔
964
        }
965

966
        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✔
967
    }
968

969
    private static function separatedContextLessInclude(string $path): ConfigInterface
970
    {
971
        $config = include $path;
20✔
972

973
        // verify that the config has an instance of Config
974
        if (!$config instanceof ConfigInterface) {
20✔
975
            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✔
976
        }
977

978
        return $config;
19✔
979
    }
980

981
    private function isCachingAllowedForRuntime(): bool
982
    {
983
        return $this->toolInfo->isInstalledAsPhar()
14✔
984
            || $this->toolInfo->isInstalledByComposer()
14✔
985
            || $this->toolInfo->isRunInsideDocker()
14✔
986
            || filter_var(getenv('PHP_CS_FIXER_ENFORCE_CACHE'), \FILTER_VALIDATE_BOOL);
14✔
987
    }
988
}
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