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

keradus / PHP-CS-Fixer / 24023612215

06 Apr 2026 07:44AM UTC coverage: 92.938% (+0.01%) from 92.928%
24023612215

push

github

keradus
refactor: ConfigurableFixerTemplateFixer - move handling example file from fixing logic to definition

2 of 3 new or added lines in 1 file covered. (66.67%)

261 existing lines in 11 files now uncovered.

29268 of 31492 relevant lines covered (92.94%)

43.97 hits per line

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

88.29
/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\Config\NullRuleCustomisationPolicy;
25
use PhpCsFixer\Config\RuleCustomisationPolicyAwareConfigInterface;
26
use PhpCsFixer\Config\RuleCustomisationPolicyInterface;
27
use PhpCsFixer\ConfigInterface;
28
use PhpCsFixer\ConfigurationException\InvalidConfigurationException;
29
use PhpCsFixer\Console\Output\Progress\ProgressOutputType;
30
use PhpCsFixer\Console\Report\FixReport\ReporterFactory;
31
use PhpCsFixer\Console\Report\FixReport\ReporterInterface;
32
use PhpCsFixer\CustomRulesetsAwareConfigInterface;
33
use PhpCsFixer\Differ\DifferInterface;
34
use PhpCsFixer\Differ\NullDiffer;
35
use PhpCsFixer\Differ\UnifiedDiffer;
36
use PhpCsFixer\Finder;
37
use PhpCsFixer\Fixer\DeprecatedFixerInterface;
38
use PhpCsFixer\Fixer\FixerInterface;
39
use PhpCsFixer\FixerFactory;
40
use PhpCsFixer\Future;
41
use PhpCsFixer\Linter\Linter;
42
use PhpCsFixer\Linter\LinterInterface;
43
use PhpCsFixer\ParallelAwareConfigInterface;
44
use PhpCsFixer\RuleSet\RuleSet;
45
use PhpCsFixer\RuleSet\RuleSetInterface;
46
use PhpCsFixer\RuleSet\RuleSets;
47
use PhpCsFixer\Runner\Parallel\ParallelConfig;
48
use PhpCsFixer\Runner\Parallel\ParallelConfigFactory;
49
use PhpCsFixer\StdinFileInfo;
50
use PhpCsFixer\ToolInfoInterface;
51
use PhpCsFixer\UnsupportedPhpVersionAllowedConfigInterface;
52
use PhpCsFixer\Utils;
53
use PhpCsFixer\WhitespacesFixerConfig;
54
use PhpCsFixer\WordMatcher;
55
use Symfony\Component\Filesystem\Filesystem;
56
use Symfony\Component\Finder\Finder as SymfonyFinder;
57

58
/**
59
 * The resolver that resolves configuration to use by command line options and config.
60
 *
61
 * @internal
62
 *
63
 * @phpstan-type _Options array{
64
 *      allow-risky: null|string,
65
 *      cache-file: null|string,
66
 *      config: null|string,
67
 *      diff: null|string,
68
 *      dry-run: null|bool,
69
 *      format: null|string,
70
 *      path: list<string>,
71
 *      path-mode: value-of<self::PATH_MODE_VALUES>,
72
 *      rules: null|string,
73
 *      sequential: null|string,
74
 *      show-progress: null|string,
75
 *      stop-on-violation: null|bool,
76
 *      using-cache: null|string,
77
 *      allow-unsupported-php-version: null|bool,
78
 *      verbosity: null|string,
79
 *  }
80
 *
81
 * @author Fabien Potencier <fabien@symfony.com>
82
 * @author Katsuhiro Ogawa <ko.fivestar@gmail.com>
83
 * @author Dariusz Rumiński <dariusz.ruminski@gmail.com>
84
 *
85
 * @no-named-arguments Parameter names are not covered by the backward compatibility promise.
86
 */
87
final class ConfigurationResolver
88
{
89
    public const IGNORE_CONFIG_FILE = '-';
90

91
    public const PATH_MODE_OVERRIDE = 'override';
92
    public const PATH_MODE_INTERSECTION = 'intersection';
93
    public const PATH_MODE_VALUES = [
94
        self::PATH_MODE_OVERRIDE,
95
        self::PATH_MODE_INTERSECTION,
96
    ];
97

98
    public const BOOL_YES = 'yes';
99
    public const BOOL_NO = 'no';
100
    public const BOOL_VALUES = [
101
        self::BOOL_YES,
102
        self::BOOL_NO,
103
    ];
104

105
    /**
106
     * @TODO v4: this is no longer needed due to `MARKER-multi-paths-vs-only-cwd-config`
107
     */
108
    private ?string $deprecatedNestedConfigDir = null;
109

110
    private ?bool $allowRisky = null;
111

112
    private ?ConfigInterface $config = null;
113

114
    private ?string $configFile = null;
115

116
    private string $cwd;
117

118
    private ConfigInterface $defaultConfig;
119

120
    private ?ReporterInterface $reporter = null;
121

122
    private ?bool $isStdIn = null;
123

124
    private ?bool $isDryRun = null;
125

126
    /**
127
     * @var null|list<FixerInterface>
128
     */
129
    private ?array $fixers = null;
130

131
    private ?bool $configFinderIsOverridden = null;
132

133
    private ?bool $configRulesAreOverridden = null;
134

135
    private ToolInfoInterface $toolInfo;
136

137
    /**
138
     * @var _Options
139
     */
140
    private array $options = [
141
        'allow-risky' => null,
142
        'cache-file' => null,
143
        'config' => null,
144
        'diff' => null,
145
        'dry-run' => null,
146
        'format' => null,
147
        'path' => [],
148
        'path-mode' => self::PATH_MODE_OVERRIDE,
149
        'rules' => null,
150
        'sequential' => null,
151
        'show-progress' => null,
152
        'stop-on-violation' => null,
153
        'using-cache' => null,
154
        'allow-unsupported-php-version' => null,
155
        'verbosity' => null,
156
    ];
157

158
    private ?string $cacheFile = null;
159

160
    private ?CacheManagerInterface $cacheManager = null;
161

162
    private ?DifferInterface $differ = null;
163

164
    private ?Directory $directory = null;
165

166
    /**
167
     * @var null|iterable<\SplFileInfo>
168
     */
169
    private ?iterable $finder = null;
170

171
    private ?string $format = null;
172

173
    private ?Linter $linter = null;
174

175
    /**
176
     * @var null|list<string>
177
     */
178
    private ?array $path = null;
179

180
    /**
181
     * @var null|ProgressOutputType::*
182
     */
183
    private $progress;
184

185
    private ?RuleSet $ruleSet = null;
186

187
    private ?bool $usingCache = null;
188

189
    private ?bool $isUnsupportedPhpVersionAllowed = null;
190

191
    private ?RuleCustomisationPolicyInterface $ruleCustomisationPolicy = null;
192

193
    private ?FixerFactory $fixerFactory = null;
194

195
    /**
196
     * @param array<string, mixed> $options
197
     */
198
    public function __construct(
199
        ConfigInterface $config,
200
        array $options,
201
        string $cwd,
202
        ToolInfoInterface $toolInfo
203
    ) {
204
        $this->defaultConfig = $config;
127✔
205
        $this->cwd = $cwd;
127✔
206
        $this->toolInfo = $toolInfo;
127✔
207

208
        foreach ($options as $name => $value) {
127✔
209
            $this->setOption($name, $value);
103✔
210
        }
211
    }
212

213
    public function getCacheFile(): ?string
214
    {
215
        if (!$this->getUsingCache()) {
10✔
216
            return null;
4✔
217
        }
218

219
        if (null === $this->cacheFile) {
6✔
220
            if (null === $this->options['cache-file']) {
6✔
221
                $this->cacheFile = $this->getConfig()->getCacheFile();
4✔
222
            } else {
223
                $this->cacheFile = $this->options['cache-file'];
2✔
224
            }
225
        }
226

227
        return $this->cacheFile;
6✔
228
    }
229

230
    public function getCacheManager(): CacheManagerInterface
231
    {
232
        if (null === $this->cacheManager) {
1✔
233
            $cacheFile = $this->getCacheFile();
1✔
234

235
            if (null === $cacheFile) {
1✔
236
                $this->cacheManager = new NullCacheManager();
1✔
237
            } else {
238
                $this->cacheManager = new FileCacheManager(
×
239
                    new FileHandler($cacheFile),
×
240
                    new Signature(
×
241
                        \PHP_VERSION,
×
242
                        $this->toolInfo->getVersion(),
×
243
                        $this->getConfig()->getIndent(),
×
244
                        $this->getConfig()->getLineEnding(),
×
245
                        $this->getRules(),
×
246
                        $this->getRuleCustomisationPolicy()->getPolicyVersionForCache(),
×
247
                    ),
×
248
                    $this->isDryRun(),
×
249
                    $this->getDirectory(),
×
250
                );
×
251
            }
252
        }
253

254
        return $this->cacheManager;
1✔
255
    }
256

257
    public function getConfig(): ConfigInterface
258
    {
259
        if (null === $this->config) {
80✔
260
            foreach ($this->computeConfigFiles() as $configFile) {
80✔
261
                if (!file_exists($configFile)) {
77✔
262
                    continue;
62✔
263
                }
264

265
                $configFileBasename = basename($configFile);
20✔
266

267
                /** @TODO v4 drop handling (triggering error) for v2 config names */
268
                $deprecatedConfigs = [
20✔
269
                    '.php_cs' => '.php-cs-fixer.php',
20✔
270
                    '.php_cs.dist' => '.php-cs-fixer.dist.php',
20✔
271
                ];
20✔
272

273
                if (isset($deprecatedConfigs[$configFileBasename])) {
20✔
274
                    throw new InvalidConfigurationException("Configuration file `{$configFileBasename}` is outdated, rename to `{$deprecatedConfigs[$configFileBasename]}`.");
×
275
                }
276

277
                if (null !== $this->deprecatedNestedConfigDir && str_starts_with($configFile, $this->deprecatedNestedConfigDir)) {
20✔
278
                    // @TODO v4: when removing, remove also TODO with `MARKER-multi-paths-vs-only-cwd-config`
279
                    Future::triggerDeprecation(
7✔
280
                        new InvalidConfigurationException("Configuration file `{$configFile}` is picked as file inside passed `path` CLI argument. This will be ignored in the future and only config file in `cwd` will be picked. Please use `config` CLI option instead if you want to keep current behaviour."),
7✔
281
                    );
7✔
282
                }
283

284
                $this->config = self::separatedContextLessInclude($configFile);
20✔
285
                $this->configFile = $configFile;
19✔
286

287
                break;
19✔
288
            }
289

290
            if (null === $this->config) {
78✔
291
                $this->config = $this->defaultConfig;
59✔
292
            }
293

294
            if ($this->config instanceof CustomRulesetsAwareConfigInterface) {
78✔
295
                foreach ($this->config->getCustomRuleSets() as $ruleSet) {
78✔
296
                    RuleSets::registerCustomRuleSet($ruleSet);
1✔
297
                }
298
            }
299
        }
300

301
        return $this->config;
78✔
302
    }
303

304
    public function getParallelConfig(): ParallelConfig
305
    {
306
        $config = $this->getConfig();
3✔
307

308
        return true !== $this->options['sequential'] && $config instanceof ParallelAwareConfigInterface
3✔
309
            ? $config->getParallelConfig()
2✔
310
            : ParallelConfigFactory::sequential();
3✔
311
    }
312

313
    public function getConfigFile(): ?string
314
    {
315
        if (null === $this->configFile) {
19✔
316
            $this->getConfig();
14✔
317
        }
318

319
        return $this->configFile;
19✔
320
    }
321

322
    public function getDiffer(): DifferInterface
323
    {
324
        if (null === $this->differ) {
4✔
325
            $this->differ = (true === $this->options['diff']) ? new UnifiedDiffer() : new NullDiffer();
4✔
326
        }
327

328
        return $this->differ;
4✔
329
    }
330

331
    public function getDirectory(): DirectoryInterface
332
    {
333
        if (null === $this->directory) {
4✔
334
            $path = $this->getCacheFile();
4✔
335
            if (null === $path) {
4✔
336
                $absolutePath = $this->cwd;
1✔
337
            } else {
338
                $filesystem = new Filesystem();
3✔
339

340
                $absolutePath = $filesystem->isAbsolutePath($path)
3✔
341
                    ? $path
2✔
342
                    : $this->cwd.\DIRECTORY_SEPARATOR.$path;
1✔
343
                $absolutePath = \dirname($absolutePath);
3✔
344
            }
345

346
            $this->directory = new Directory($absolutePath);
4✔
347
        }
348

349
        return $this->directory;
4✔
350
    }
351

352
    /**
353
     * @return list<FixerInterface>
354
     */
355
    public function getFixers(): array
356
    {
357
        if (null === $this->fixers) {
5✔
358
            $this->fixers = $this->createFixerFactory()
5✔
359
                ->useRuleSet($this->getRuleSet())
5✔
360
                ->setWhitespacesConfig(new WhitespacesFixerConfig($this->config->getIndent(), $this->config->getLineEnding()))
5✔
361
                ->getFixers()
5✔
362
            ;
5✔
363

364
            if (false === $this->getRiskyAllowed()) {
5✔
365
                $riskyFixers = array_map(
3✔
366
                    static fn (FixerInterface $fixer): string => $fixer->getName(),
3✔
367
                    array_values(array_filter(
3✔
368
                        $this->fixers,
3✔
369
                        static fn (FixerInterface $fixer): bool => $fixer->isRisky(),
3✔
370
                    )),
3✔
371
                );
3✔
372

373
                if (\count($riskyFixers) > 0) {
3✔
UNCOV
374
                    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)));
×
375
                }
376
            }
377
        }
378

379
        return $this->fixers;
5✔
380
    }
381

382
    public function getLinter(): LinterInterface
383
    {
384
        if (null === $this->linter) {
1✔
385
            $this->linter = new Linter();
1✔
386
        }
387

388
        return $this->linter;
1✔
389
    }
390

391
    /**
392
     * Returns path.
393
     *
394
     * @return list<string>
395
     */
396
    public function getPath(): array
397
    {
398
        if (null === $this->path) {
94✔
399
            $filesystem = new Filesystem();
94✔
400
            $cwd = $this->cwd;
94✔
401

402
            if (1 === \count($this->options['path']) && '-' === $this->options['path'][0]) {
94✔
UNCOV
403
                $this->path = $this->options['path'];
×
404
            } else {
405
                $this->path = array_map(
94✔
406
                    static function (string $rawPath) use ($cwd, $filesystem): string {
94✔
407
                        $path = trim($rawPath);
46✔
408

409
                        if ('' === $path) {
46✔
410
                            throw new InvalidConfigurationException("Invalid path: \"{$rawPath}\".");
6✔
411
                        }
412

413
                        $absolutePath = $filesystem->isAbsolutePath($path)
42✔
414
                            ? $path
37✔
415
                            : $cwd.\DIRECTORY_SEPARATOR.$path;
5✔
416

417
                        if (!file_exists($absolutePath)) {
42✔
418
                            throw new InvalidConfigurationException(\sprintf(
5✔
419
                                'The path "%s" is not readable.',
5✔
420
                                $path,
5✔
421
                            ));
5✔
422
                        }
423

424
                        return $absolutePath;
37✔
425
                    },
94✔
426
                    $this->options['path'],
94✔
427
                );
94✔
428
            }
429
        }
430

431
        return $this->path;
83✔
432
    }
433

434
    /**
435
     * @return ProgressOutputType::*
436
     *
437
     * @throws InvalidConfigurationException
438
     */
439
    public function getProgressType(): string
440
    {
441
        if (null === $this->progress) {
13✔
442
            if ('txt' === $this->resolveFormat()) {
13✔
443
                $progressType = $this->options['show-progress'];
11✔
444

445
                if (null === $progressType) {
11✔
446
                    $progressType = $this->getConfig()->getHideProgress()
4✔
447
                        ? ProgressOutputType::NONE
2✔
448
                        : ProgressOutputType::BAR;
2✔
449
                } elseif (!\in_array($progressType, ProgressOutputType::all(), true)) {
7✔
450
                    throw new InvalidConfigurationException(\sprintf(
1✔
451
                        'The progress type "%s" is not defined, supported are %s.',
1✔
452
                        $progressType,
1✔
453
                        Utils::naturalLanguageJoin(ProgressOutputType::all()),
1✔
454
                    ));
1✔
455
                }
456

457
                $this->progress = $progressType;
10✔
458
            } else {
459
                $this->progress = ProgressOutputType::NONE;
2✔
460
            }
461
        }
462

463
        return $this->progress;
12✔
464
    }
465

466
    public function getReporter(): ReporterInterface
467
    {
468
        if (null === $this->reporter) {
8✔
469
            $reporterFactory = new ReporterFactory();
8✔
470
            $reporterFactory->registerBuiltInReporters();
8✔
471

472
            $format = $this->resolveFormat();
8✔
473

474
            try {
475
                $this->reporter = $reporterFactory->getReporter($format);
8✔
476
            } catch (\UnexpectedValueException $e) {
1✔
477
                $formats = $reporterFactory->getFormats();
1✔
478
                sort($formats);
1✔
479

480
                throw new InvalidConfigurationException(\sprintf('The format "%s" is not defined, supported are %s.', $format, Utils::naturalLanguageJoin($formats)));
1✔
481
            }
482
        }
483

484
        return $this->reporter;
7✔
485
    }
486

487
    public function getRiskyAllowed(): bool
488
    {
489
        if (null === $this->allowRisky) {
18✔
490
            if (null === $this->options['allow-risky']) {
18✔
491
                $this->allowRisky = $this->getConfig()->getRiskyAllowed();
10✔
492
            } else {
493
                $this->allowRisky = $this->resolveOptionBooleanValue('allow-risky');
8✔
494
            }
495
        }
496

497
        return $this->allowRisky;
17✔
498
    }
499

500
    /**
501
     * Returns rules.
502
     *
503
     * @return array<string, array<string, mixed>|bool>
504
     */
505
    public function getRules(): array
506
    {
507
        return $this->getRuleSet()->getRules();
11✔
508
    }
509

510
    public function getUsingCache(): bool
511
    {
512
        if (null === $this->usingCache) {
22✔
513
            if (null === $this->options['using-cache']) {
22✔
514
                $this->usingCache = $this->getConfig()->getUsingCache();
17✔
515
            } else {
516
                $this->usingCache = $this->resolveOptionBooleanValue('using-cache');
5✔
517
            }
518
        }
519

520
        $this->usingCache = $this->usingCache && $this->isCachingAllowedForRuntime();
22✔
521

522
        return $this->usingCache;
22✔
523
    }
524

525
    public function getUnsupportedPhpVersionAllowed(): bool
526
    {
UNCOV
527
        if (null === $this->isUnsupportedPhpVersionAllowed) {
×
528
            if (null === $this->options['allow-unsupported-php-version']) {
×
UNCOV
529
                $config = $this->getConfig();
×
UNCOV
530
                $this->isUnsupportedPhpVersionAllowed = $config instanceof UnsupportedPhpVersionAllowedConfigInterface
×
UNCOV
531
                    ? $config->getUnsupportedPhpVersionAllowed()
×
UNCOV
532
                    : false;
×
533
            } else {
534
                $this->isUnsupportedPhpVersionAllowed = $this->resolveOptionBooleanValue('allow-unsupported-php-version');
×
535
            }
536
        }
537

538
        return $this->isUnsupportedPhpVersionAllowed;
×
539
    }
540

541
    public function getRuleCustomisationPolicy(): RuleCustomisationPolicyInterface
542
    {
UNCOV
543
        if (null === $this->ruleCustomisationPolicy) {
×
UNCOV
544
            $config = $this->getConfig();
×
UNCOV
545
            if ($config instanceof RuleCustomisationPolicyAwareConfigInterface) {
×
UNCOV
546
                $this->ruleCustomisationPolicy = $config->getRuleCustomisationPolicy();
×
547
            }
UNCOV
548
            $this->ruleCustomisationPolicy ??= new NullRuleCustomisationPolicy();
×
549
        }
550

UNCOV
551
        return $this->ruleCustomisationPolicy;
×
552
    }
553

554
    /**
555
     * @return iterable<\SplFileInfo>
556
     */
557
    public function getFinder(): iterable
558
    {
559
        if (null === $this->finder) {
31✔
560
            $this->finder = $this->resolveFinder();
31✔
561
        }
562

563
        return $this->finder;
26✔
564
    }
565

566
    /**
567
     * Returns dry-run flag.
568
     */
569
    public function isDryRun(): bool
570
    {
571
        if (null === $this->isDryRun) {
4✔
572
            if ($this->isStdIn()) {
4✔
573
                // Can't write to STDIN
574
                $this->isDryRun = true;
1✔
575
            } else {
576
                $this->isDryRun = $this->options['dry-run'];
3✔
577
            }
578
        }
579

580
        return $this->isDryRun;
4✔
581
    }
582

583
    public function shouldStopOnViolation(): bool
584
    {
585
        return $this->options['stop-on-violation'];
1✔
586
    }
587

588
    public function configFinderIsOverridden(): bool
589
    {
590
        if (null === $this->configFinderIsOverridden) {
7✔
591
            $this->resolveFinder();
7✔
592
        }
593

594
        return $this->configFinderIsOverridden;
7✔
595
    }
596

597
    public function configRulesAreOverridden(): bool
598
    {
UNCOV
599
        if (null === $this->configRulesAreOverridden) {
×
UNCOV
600
            $this->parseRules();
×
601
        }
602

UNCOV
603
        return $this->configRulesAreOverridden;
×
604
    }
605

606
    /**
607
     * Compute file candidates for config file.
608
     *
609
     * @TODO v4: don't offer configs from passed `path` CLI argument
610
     *
611
     * @return list<string>
612
     */
613
    private function computeConfigFiles(): array
614
    {
615
        $configFile = $this->options['config'];
80✔
616

617
        if (self::IGNORE_CONFIG_FILE === $configFile) {
80✔
618
            return [];
2✔
619
        }
620

621
        if (null !== $configFile) {
78✔
622
            if (false === file_exists($configFile) || false === is_readable($configFile)) {
11✔
UNCOV
623
                throw new InvalidConfigurationException(\sprintf('Cannot read config file "%s".', $configFile));
×
624
            }
625

626
            return [$configFile];
11✔
627
        }
628

629
        $path = $this->getPath();
67✔
630

631
        if ($this->isStdIn() || 0 === \count($path)) {
67✔
632
            $configDir = $this->cwd;
45✔
633
        } elseif (1 < \count($path)) {
22✔
634
            // @TODO v4: this is no longer needed due to `MARKER-multi-paths-vs-only-cwd-config`
635
            throw new InvalidConfigurationException('For multiple paths config parameter is required.');
1✔
636
        } elseif (!is_file($path[0])) {
21✔
637
            $configDir = $path[0];
11✔
638
        } else {
639
            $dirName = pathinfo($path[0], \PATHINFO_DIRNAME);
10✔
640
            $configDir = is_dir($dirName) ? $dirName : $path[0];
10✔
641
        }
642

643
        $candidates = [
66✔
644
            $configDir.\DIRECTORY_SEPARATOR.'.php-cs-fixer.php',
66✔
645
            $configDir.\DIRECTORY_SEPARATOR.'.php-cs-fixer.dist.php',
66✔
646

647
            // @TODO v4 drop handling (triggering error) for v2 config names
648
            $configDir.\DIRECTORY_SEPARATOR.'.php_cs', // old v2 config, present here only to throw nice error message later
66✔
649
            $configDir.\DIRECTORY_SEPARATOR.'.php_cs.dist', // old v2 config, present here only to throw nice error message later
66✔
650
        ];
66✔
651

652
        if ($configDir !== $this->cwd) {
66✔
653
            $candidates[] = $this->cwd.\DIRECTORY_SEPARATOR.'.php-cs-fixer.php';
21✔
654
            $candidates[] = $this->cwd.\DIRECTORY_SEPARATOR.'.php-cs-fixer.dist.php';
21✔
655

656
            // @TODO v4 drop handling (triggering error) for v2 config names
657
            $candidates[] = $this->cwd.\DIRECTORY_SEPARATOR.'.php_cs'; // old v2 config, present here only to throw nice error message later
21✔
658
            $candidates[] = $this->cwd.\DIRECTORY_SEPARATOR.'.php_cs.dist'; // old v2 config, present here only to throw nice error message later
21✔
659

660
            $this->deprecatedNestedConfigDir = $configDir;
21✔
661
        }
662

663
        return $candidates;
66✔
664
    }
665

666
    private function createFixerFactory(): FixerFactory
667
    {
668
        if (null === $this->fixerFactory) {
15✔
669
            $fixerFactory = new FixerFactory();
15✔
670
            $fixerFactory->registerBuiltInFixers();
15✔
671
            $fixerFactory->registerCustomFixers($this->getConfig()->getCustomFixers());
15✔
672

673
            $this->fixerFactory = $fixerFactory;
15✔
674
        }
675

676
        return $this->fixerFactory;
15✔
677
    }
678

679
    private function resolveFormat(): string
680
    {
681
        if (null === $this->format) {
19✔
682
            $formatCandidate = $this->options['format'] ?? $this->getConfig()->getFormat();
19✔
683
            $parts = explode(',', $formatCandidate);
19✔
684

685
            if (\count($parts) > 2) {
19✔
UNCOV
686
                throw new InvalidConfigurationException(\sprintf('The format "%s" is invalid.', $formatCandidate));
×
687
            }
688

689
            $this->format = $parts[0];
19✔
690

691
            if ('@auto' === $this->format) {
19✔
692
                $this->format = $parts[1] ?? 'txt';
3✔
693

694
                if (filter_var(getenv('GITLAB_CI'), \FILTER_VALIDATE_BOOL)) {
3✔
695
                    $this->format = 'gitlab';
1✔
696
                }
697
            }
698
        }
699

700
        return $this->format;
19✔
701
    }
702

703
    private function getRuleSet(): RuleSetInterface
704
    {
705
        if (null === $this->ruleSet) {
16✔
706
            $rules = $this->parseRules();
16✔
707
            $this->validateRules($rules);
15✔
708

709
            $this->ruleSet = new RuleSet($rules);
10✔
710
        }
711

712
        return $this->ruleSet;
10✔
713
    }
714

715
    private function isStdIn(): bool
716
    {
717
        if (null === $this->isStdIn) {
87✔
718
            $this->isStdIn = 1 === \count($this->options['path']) && '-' === $this->options['path'][0];
87✔
719
        }
720

721
        return $this->isStdIn;
87✔
722
    }
723

724
    /**
725
     * @template T
726
     *
727
     * @param iterable<T> $iterable
728
     *
729
     * @return \Traversable<T>
730
     */
731
    private function iterableToTraversable(iterable $iterable): \Traversable
732
    {
733
        return \is_array($iterable) ? new \ArrayIterator($iterable) : $iterable;
25✔
734
    }
735

736
    /**
737
     * @return array<string, mixed>
738
     */
739
    private function parseRules(): array
740
    {
741
        $this->configRulesAreOverridden = null !== $this->options['rules'];
16✔
742

743
        if (null === $this->options['rules']) {
16✔
744
            $this->configRulesAreOverridden = false;
7✔
745

746
            return $this->getConfig()->getRules();
7✔
747
        }
748

749
        $rules = trim($this->options['rules']);
9✔
750
        if ('' === $rules) {
9✔
751
            throw new InvalidConfigurationException('Empty rules value is not allowed.');
1✔
752
        }
753

754
        if (str_starts_with($rules, '{')) {
8✔
755
            try {
UNCOV
756
                return json_decode($rules, true, 512, \JSON_THROW_ON_ERROR);
×
UNCOV
757
            } catch (\JsonException $e) {
×
UNCOV
758
                throw new InvalidConfigurationException(\sprintf('Invalid JSON rules input: "%s".', $e->getMessage()));
×
759
            }
760
        }
761

762
        $rules = [];
8✔
763

764
        foreach (explode(',', $this->options['rules']) as $rule) {
8✔
765
            $rule = trim($rule);
8✔
766

767
            if ('' === $rule) {
8✔
UNCOV
768
                throw new InvalidConfigurationException('Empty rule name is not allowed.');
×
769
            }
770

771
            if (str_starts_with($rule, '-')) {
8✔
772
                $rules[substr($rule, 1)] = false;
2✔
773
            } else {
774
                $rules[$rule] = true;
8✔
775
            }
776
        }
777

778
        $this->configRulesAreOverridden = true;
8✔
779

780
        return $rules;
8✔
781
    }
782

783
    /**
784
     * @param array<string, mixed> $rules
785
     *
786
     * @throws InvalidConfigurationException
787
     */
788
    private function validateRules(array $rules): void
789
    {
790
        /**
791
         * Create a ruleset that contains all configured rules, even when they originally have been disabled.
792
         *
793
         * @see RuleSet::resolveSet()
794
         */
795
        $ruleSet = [];
15✔
796

797
        foreach ($rules as $key => $value) {
15✔
798
            if (\is_int($key)) {
15✔
UNCOV
799
                throw new InvalidConfigurationException(\sprintf('Missing value for "%s" rule/set.', $value));
×
800
            }
801

802
            $ruleSet[$key] = true;
15✔
803
        }
804

805
        $ruleSet = new RuleSet($ruleSet);
15✔
806

807
        $configuredFixers = array_keys($ruleSet->getRules());
15✔
808

809
        $fixers = $this->createFixerFactory()->getFixers();
15✔
810

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

813
        $unknownFixers = array_diff($configuredFixers, $availableFixers);
15✔
814

815
        if (\count($unknownFixers) > 0) {
15✔
816
            /**
817
             * @TODO v4: `renamedRulesFromV2ToV3` no longer needed
818
             * @TODO v3.99: decide how to handle v3 to v4 (where legacy rules are already removed)
819
             */
820
            $renamedRulesFromV2ToV3 = [
5✔
821
                'blank_line_before_return' => [
5✔
822
                    'new_name' => 'blank_line_before_statement',
5✔
823
                    'config' => ['statements' => ['return']],
5✔
824
                ],
5✔
825
                'final_static_access' => [
5✔
826
                    'new_name' => 'self_static_accessor',
5✔
827
                ],
5✔
828
                'hash_to_slash_comment' => [
5✔
829
                    'new_name' => 'single_line_comment_style',
5✔
830
                    'config' => ['comment_types' => ['hash']],
5✔
831
                ],
5✔
832
                'lowercase_constants' => [
5✔
833
                    'new_name' => 'constant_case',
5✔
834
                    'config' => ['case' => 'lower'],
5✔
835
                ],
5✔
836
                'no_extra_consecutive_blank_lines' => [
5✔
837
                    'new_name' => 'no_extra_blank_lines',
5✔
838
                ],
5✔
839
                'no_multiline_whitespace_before_semicolons' => [
5✔
840
                    'new_name' => 'multiline_whitespace_before_semicolons',
5✔
841
                ],
5✔
842
                'no_short_echo_tag' => [
5✔
843
                    'new_name' => 'echo_tag_syntax',
5✔
844
                    'config' => ['format' => 'long'],
5✔
845
                ],
5✔
846
                'php_unit_ordered_covers' => [
5✔
847
                    'new_name' => 'phpdoc_order_by_value',
5✔
848
                    'config' => ['annotations' => ['covers']],
5✔
849
                ],
5✔
850
                'phpdoc_inline_tag' => [
5✔
851
                    'new_name' => 'general_phpdoc_tag_rename, phpdoc_inline_tag_normalizer and phpdoc_tag_type',
5✔
852
                ],
5✔
853
                'pre_increment' => [
5✔
854
                    'new_name' => 'increment_style',
5✔
855
                    'config' => ['style' => 'pre'],
5✔
856
                ],
5✔
857
                'psr0' => [
5✔
858
                    'new_name' => 'psr_autoloading',
5✔
859
                    'config' => ['dir' => 'x'],
5✔
860
                ],
5✔
861
                'psr4' => [
5✔
862
                    'new_name' => 'psr_autoloading',
5✔
863
                ],
5✔
864
                'silenced_deprecation_error' => [
5✔
865
                    'new_name' => 'error_suppression',
5✔
866
                ],
5✔
867
                'trailing_comma_in_multiline_array' => [
5✔
868
                    'new_name' => 'trailing_comma_in_multiline',
5✔
869
                    'config' => ['elements' => ['arrays']],
5✔
870
                ],
5✔
871
            ];
5✔
872

873
            $message = 'The rules contain unknown fixers: ';
5✔
874
            $hasOldRule = false;
5✔
875

876
            foreach ($unknownFixers as $unknownFixer) {
5✔
877
                if (isset($renamedRulesFromV2ToV3[$unknownFixer])) { // Check if present as old renamed rule
5✔
878
                    $hasOldRule = true;
4✔
879
                    $message .= \sprintf(
4✔
880
                        '"%s" is renamed (did you mean "%s"?%s), ',
4✔
881
                        $unknownFixer,
4✔
882
                        $renamedRulesFromV2ToV3[$unknownFixer]['new_name'],
4✔
883
                        isset($renamedRulesFromV2ToV3[$unknownFixer]['config']) ? ' (note: use configuration "'.Utils::toString($renamedRulesFromV2ToV3[$unknownFixer]['config']).'")' : '',
4✔
884
                    );
4✔
885
                } else { // Go to normal matcher if it is not a renamed rule
886
                    $matcher = new WordMatcher($availableFixers);
2✔
887
                    $alternative = $matcher->match($unknownFixer);
2✔
888
                    $message .= \sprintf(
2✔
889
                        '"%s"%s, ',
2✔
890
                        $unknownFixer,
2✔
891
                        null === $alternative ? '' : ' (did you mean "'.$alternative.'"?)',
2✔
892
                    );
2✔
893
                }
894
            }
895

896
            $message = substr($message, 0, -2).'.';
5✔
897

898
            if ($hasOldRule) {
5✔
899
                $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✔
900
            }
901

902
            throw new InvalidConfigurationException($message);
5✔
903
        }
904

905
        foreach ($fixers as $fixer) {
10✔
906
            $fixerName = $fixer->getName();
10✔
907
            if (isset($rules[$fixerName]) && $fixer instanceof DeprecatedFixerInterface) {
10✔
908
                $successors = $fixer->getSuccessorsNames();
3✔
909
                $messageEnd = [] === $successors
3✔
UNCOV
910
                    ? \sprintf(' and will be removed in version %d.0.', Application::getMajorVersion() + 1)
×
911
                    : \sprintf('. Use %s instead.', str_replace('`', '"', Utils::naturalLanguageJoinWithBackticks($successors)));
3✔
912

913
                Future::triggerDeprecation(new \RuntimeException("Rule \"{$fixerName}\" is deprecated{$messageEnd}"));
3✔
914
            }
915
        }
916
    }
917

918
    /**
919
     * Apply path on config instance.
920
     *
921
     * @return iterable<\SplFileInfo>
922
     */
923
    private function resolveFinder(): iterable
924
    {
925
        $this->configFinderIsOverridden = false;
31✔
926

927
        if ($this->isStdIn()) {
31✔
UNCOV
928
            return new \ArrayIterator([new StdinFileInfo()]);
×
929
        }
930

931
        if (!\in_array(
31✔
932
            $this->options['path-mode'],
31✔
933
            self::PATH_MODE_VALUES,
31✔
934
            true,
31✔
935
        )) {
31✔
UNCOV
936
            throw new InvalidConfigurationException(\sprintf(
×
UNCOV
937
                'The path-mode "%s" is not defined, supported are %s.',
×
UNCOV
938
                $this->options['path-mode'],
×
UNCOV
939
                Utils::naturalLanguageJoin(self::PATH_MODE_VALUES),
×
UNCOV
940
            ));
×
941
        }
942

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

945
        $paths = array_map(
31✔
946
            static fn (string $path): string => realpath($path), // @phpstan-ignore return.type
31✔
947
            $this->getPath(),
31✔
948
        );
31✔
949

950
        if (0 === \count($paths)) {
26✔
951
            if ($isIntersectionPathMode) {
5✔
952
                return new \ArrayIterator([]);
1✔
953
            }
954

955
            return $this->iterableToTraversable($this->getConfig()->getFinder());
4✔
956
        }
957

958
        $pathsByType = [
21✔
959
            'file' => [],
21✔
960
            'dir' => [],
21✔
961
        ];
21✔
962

963
        foreach ($paths as $path) {
21✔
964
            if (is_file($path)) {
21✔
965
                $pathsByType['file'][] = $path;
11✔
966
            } else {
967
                $pathsByType['dir'][] = $path.\DIRECTORY_SEPARATOR;
12✔
968
            }
969
        }
970

971
        $nestedFinder = null;
21✔
972
        $currentFinder = $this->iterableToTraversable($this->getConfig()->getFinder());
21✔
973

974
        try {
975
            $nestedFinder = $currentFinder instanceof \IteratorAggregate ? $currentFinder->getIterator() : $currentFinder;
21✔
976
        } catch (\Exception $e) {
4✔
977
        }
978

979
        if ($isIntersectionPathMode) {
21✔
980
            if (null === $nestedFinder) {
11✔
UNCOV
981
                throw new InvalidConfigurationException(
×
UNCOV
982
                    'Cannot create intersection with not-fully defined Finder in configuration file.',
×
UNCOV
983
                );
×
984
            }
985

986
            return new \CallbackFilterIterator(
11✔
987
                new \IteratorIterator($nestedFinder),
11✔
988
                static function (\SplFileInfo $current) use ($pathsByType): bool {
11✔
989
                    $currentRealPath = $current->getRealPath();
10✔
990

991
                    if (\in_array($currentRealPath, $pathsByType['file'], true)) {
10✔
992
                        return true;
3✔
993
                    }
994

995
                    foreach ($pathsByType['dir'] as $path) {
10✔
996
                        if (str_starts_with($currentRealPath, $path)) {
5✔
997
                            return true;
4✔
998
                        }
999
                    }
1000

1001
                    return false;
10✔
1002
                },
11✔
1003
            );
11✔
1004
        }
1005

1006
        if (null !== $this->getConfigFile() && null !== $nestedFinder) {
10✔
1007
            $this->configFinderIsOverridden = true;
3✔
1008
        }
1009

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

1015
        return Finder::create()->in($pathsByType['dir'])->append($pathsByType['file']);
6✔
1016
    }
1017

1018
    /**
1019
     * Set option that will be resolved.
1020
     *
1021
     * @param mixed $value
1022
     */
1023
    private function setOption(string $name, $value): void
1024
    {
1025
        if (!\array_key_exists($name, $this->options)) {
103✔
1026
            throw new InvalidConfigurationException(\sprintf('Unknown option name: "%s".', $name));
1✔
1027
        }
1028

1029
        $this->options[$name] = $value;
102✔
1030
    }
1031

1032
    /**
1033
     * @param key-of<_Options> $optionName
1034
     */
1035
    private function resolveOptionBooleanValue(string $optionName): bool
1036
    {
1037
        $value = $this->options[$optionName];
12✔
1038

1039
        if (self::BOOL_YES === $value) {
12✔
1040
            return true;
6✔
1041
        }
1042

1043
        if (self::BOOL_NO === $value) {
7✔
1044
            return false;
6✔
1045
        }
1046

1047
        throw new InvalidConfigurationException(\sprintf('Expected "%s" or "%s" for option "%s", got "%s".', self::BOOL_YES, self::BOOL_NO, $optionName, \is_object($value) ? \get_class($value) : (\is_scalar($value) ? $value : \gettype($value))));
1✔
1048
    }
1049

1050
    private static function separatedContextLessInclude(string $path): ConfigInterface
1051
    {
1052
        $config = include $path;
20✔
1053

1054
        // verify that the config has an instance of Config
1055
        if (!$config instanceof ConfigInterface) {
20✔
1056
            throw new InvalidConfigurationException(\sprintf('The config file: "%s" does not return a "%s" instance. Got: "%s".', $path, ConfigInterface::class, get_debug_type($config)));
1✔
1057
        }
1058

1059
        return $config;
19✔
1060
    }
1061

1062
    private function isCachingAllowedForRuntime(): bool
1063
    {
1064
        return $this->toolInfo->isInstalledAsPhar()
14✔
1065
            || $this->toolInfo->isInstalledByComposer()
14✔
1066
            || $this->toolInfo->isRunInsideDocker()
14✔
1067
            || filter_var(getenv('PHP_CS_FIXER_ENFORCE_CACHE'), \FILTER_VALIDATE_BOOL);
14✔
1068
    }
1069
}
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