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

keradus / PHP-CS-Fixer / 19958239208

05 Dec 2025 09:13AM UTC coverage: 93.181% (-1.0%) from 94.158%
19958239208

push

github

keradus
chore: .php-cs-fixer.dist.php - remove no longer needed rule, 'expectedDeprecation' annotation does not exist for long time

28928 of 31045 relevant lines covered (93.18%)

44.49 hits per line

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

89.76
/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\CustomRulesetsAwareConfigInterface;
30
use PhpCsFixer\Differ\DifferInterface;
31
use PhpCsFixer\Differ\NullDiffer;
32
use PhpCsFixer\Differ\UnifiedDiffer;
33
use PhpCsFixer\Finder;
34
use PhpCsFixer\Fixer\DeprecatedFixerInterface;
35
use PhpCsFixer\Fixer\FixerInterface;
36
use PhpCsFixer\FixerFactory;
37
use PhpCsFixer\Future;
38
use PhpCsFixer\Linter\Linter;
39
use PhpCsFixer\Linter\LinterInterface;
40
use PhpCsFixer\ParallelAwareConfigInterface;
41
use PhpCsFixer\RuleSet\RuleSet;
42
use PhpCsFixer\RuleSet\RuleSetInterface;
43
use PhpCsFixer\RuleSet\RuleSets;
44
use PhpCsFixer\Runner\Parallel\ParallelConfig;
45
use PhpCsFixer\Runner\Parallel\ParallelConfigFactory;
46
use PhpCsFixer\StdinFileInfo;
47
use PhpCsFixer\ToolInfoInterface;
48
use PhpCsFixer\UnsupportedPhpVersionAllowedConfigInterface;
49
use PhpCsFixer\Utils;
50
use PhpCsFixer\WhitespacesFixerConfig;
51
use PhpCsFixer\WordMatcher;
52
use Symfony\Component\Filesystem\Filesystem;
53
use Symfony\Component\Finder\Finder as SymfonyFinder;
54

55
/**
56
 * The resolver that resolves configuration to use by command line options and config.
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: value-of<self::PATH_MODE_VALUES>,
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
 *      allow-unsupported-php-version: null|bool,
75
 *      verbosity: null|string,
76
 *  }
77
 *
78
 * @author Fabien Potencier <fabien@symfony.com>
79
 * @author Katsuhiro Ogawa <ko.fivestar@gmail.com>
80
 * @author Dariusz Rumiński <dariusz.ruminski@gmail.com>
81
 *
82
 * @no-named-arguments Parameter names are not covered by the backward compatibility promise.
83
 */
84
final class ConfigurationResolver
85
{
86
    public const IGNORE_CONFIG_FILE = '-';
87

88
    public const PATH_MODE_OVERRIDE = 'override';
89
    public const PATH_MODE_INTERSECTION = 'intersection';
90
    public const PATH_MODE_VALUES = [
91
        self::PATH_MODE_OVERRIDE,
92
        self::PATH_MODE_INTERSECTION,
93
    ];
94

95
    public const BOOL_YES = 'yes';
96
    public const BOOL_NO = 'no';
97
    public const BOOL_VALUES = [
98
        self::BOOL_YES,
99
        self::BOOL_NO,
100
    ];
101

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

107
    private ?bool $allowRisky = null;
108

109
    private ?ConfigInterface $config = null;
110

111
    private ?string $configFile = null;
112

113
    private string $cwd;
114

115
    private ConfigInterface $defaultConfig;
116

117
    private ?ReporterInterface $reporter = null;
118

119
    private ?bool $isStdIn = null;
120

121
    private ?bool $isDryRun = null;
122

123
    /**
124
     * @var null|list<FixerInterface>
125
     */
126
    private ?array $fixers = null;
127

128
    private ?bool $configFinderIsOverridden = null;
129

130
    private ?bool $configRulesAreOverridden = null;
131

132
    private ToolInfoInterface $toolInfo;
133

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

155
    private ?string $cacheFile = null;
156

157
    private ?CacheManagerInterface $cacheManager = null;
158

159
    private ?DifferInterface $differ = null;
160

161
    private ?Directory $directory = null;
162

163
    /**
164
     * @var null|iterable<\SplFileInfo>
165
     */
166
    private ?iterable $finder = null;
167

168
    private ?string $format = null;
169

170
    private ?Linter $linter = null;
171

172
    /**
173
     * @var null|list<string>
174
     */
175
    private ?array $path = null;
176

177
    /**
178
     * @var null|ProgressOutputType::*
179
     */
180
    private $progress;
181

182
    private ?RuleSet $ruleSet = null;
183

184
    private ?bool $usingCache = null;
185

186
    private ?bool $isUnsupportedPhpVersionAllowed = null;
187

188
    private ?FixerFactory $fixerFactory = null;
189

190
    /**
191
     * @param array<string, mixed> $options
192
     */
193
    public function __construct(
194
        ConfigInterface $config,
195
        array $options,
196
        string $cwd,
197
        ToolInfoInterface $toolInfo
198
    ) {
199
        $this->defaultConfig = $config;
127✔
200
        $this->cwd = $cwd;
127✔
201
        $this->toolInfo = $toolInfo;
127✔
202

203
        foreach ($options as $name => $value) {
127✔
204
            $this->setOption($name, $value);
103✔
205
        }
206
    }
207

208
    public function getCacheFile(): ?string
209
    {
210
        if (!$this->getUsingCache()) {
10✔
211
            return null;
4✔
212
        }
213

214
        if (null === $this->cacheFile) {
6✔
215
            if (null === $this->options['cache-file']) {
6✔
216
                $this->cacheFile = $this->getConfig()->getCacheFile();
4✔
217
            } else {
218
                $this->cacheFile = $this->options['cache-file'];
2✔
219
            }
220
        }
221

222
        return $this->cacheFile;
6✔
223
    }
224

225
    public function getCacheManager(): CacheManagerInterface
226
    {
227
        if (null === $this->cacheManager) {
1✔
228
            $cacheFile = $this->getCacheFile();
1✔
229

230
            if (null === $cacheFile) {
1✔
231
                $this->cacheManager = new NullCacheManager();
1✔
232
            } else {
233
                $this->cacheManager = new FileCacheManager(
×
234
                    new FileHandler($cacheFile),
×
235
                    new Signature(
×
236
                        \PHP_VERSION,
×
237
                        $this->toolInfo->getVersion(),
×
238
                        $this->getConfig()->getIndent(),
×
239
                        $this->getConfig()->getLineEnding(),
×
240
                        $this->getRules()
×
241
                    ),
×
242
                    $this->isDryRun(),
×
243
                    $this->getDirectory()
×
244
                );
×
245
            }
246
        }
247

248
        return $this->cacheManager;
1✔
249
    }
250

251
    public function getConfig(): ConfigInterface
252
    {
253
        if (null === $this->config) {
80✔
254
            foreach ($this->computeConfigFiles() as $configFile) {
80✔
255
                if (!file_exists($configFile)) {
77✔
256
                    continue;
62✔
257
                }
258

259
                $configFileBasename = basename($configFile);
20✔
260

261
                /** @TODO v4 drop handling (triggering error) for v2 config names */
262
                $deprecatedConfigs = [
20✔
263
                    '.php_cs' => '.php-cs-fixer.php',
20✔
264
                    '.php_cs.dist' => '.php-cs-fixer.dist.php',
20✔
265
                ];
20✔
266

267
                if (isset($deprecatedConfigs[$configFileBasename])) {
20✔
268
                    throw new InvalidConfigurationException("Configuration file `{$configFileBasename}` is outdated, rename to `{$deprecatedConfigs[$configFileBasename]}`.");
×
269
                }
270

271
                if (null !== $this->deprecatedNestedConfigDir && str_starts_with($configFile, $this->deprecatedNestedConfigDir)) {
20✔
272
                    // @TODO v4: when removing, remove also TODO with `MARKER-multi-paths-vs-only-cwd-config`
273
                    Future::triggerDeprecation(
7✔
274
                        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✔
275
                    );
7✔
276
                }
277

278
                $this->config = self::separatedContextLessInclude($configFile);
20✔
279
                $this->configFile = $configFile;
19✔
280

281
                break;
19✔
282
            }
283

284
            if (null === $this->config) {
78✔
285
                $this->config = $this->defaultConfig;
59✔
286
            }
287

288
            if ($this->config instanceof CustomRulesetsAwareConfigInterface) {
78✔
289
                foreach ($this->config->getCustomRuleSets() as $ruleSet) {
78✔
290
                    RuleSets::registerCustomRuleSet($ruleSet);
1✔
291
                }
292
            }
293
        }
294

295
        return $this->config;
78✔
296
    }
297

298
    public function getParallelConfig(): ParallelConfig
299
    {
300
        $config = $this->getConfig();
3✔
301

302
        return true !== $this->options['sequential'] && $config instanceof ParallelAwareConfigInterface
3✔
303
            ? $config->getParallelConfig()
2✔
304
            : ParallelConfigFactory::sequential();
3✔
305
    }
306

307
    public function getConfigFile(): ?string
308
    {
309
        if (null === $this->configFile) {
19✔
310
            $this->getConfig();
14✔
311
        }
312

313
        return $this->configFile;
19✔
314
    }
315

316
    public function getDiffer(): DifferInterface
317
    {
318
        if (null === $this->differ) {
4✔
319
            $this->differ = (true === $this->options['diff']) ? new UnifiedDiffer() : new NullDiffer();
4✔
320
        }
321

322
        return $this->differ;
4✔
323
    }
324

325
    public function getDirectory(): DirectoryInterface
326
    {
327
        if (null === $this->directory) {
4✔
328
            $path = $this->getCacheFile();
4✔
329
            if (null === $path) {
4✔
330
                $absolutePath = $this->cwd;
1✔
331
            } else {
332
                $filesystem = new Filesystem();
3✔
333

334
                $absolutePath = $filesystem->isAbsolutePath($path)
3✔
335
                    ? $path
2✔
336
                    : $this->cwd.\DIRECTORY_SEPARATOR.$path;
1✔
337
                $absolutePath = \dirname($absolutePath);
3✔
338
            }
339

340
            $this->directory = new Directory($absolutePath);
4✔
341
        }
342

343
        return $this->directory;
4✔
344
    }
345

346
    /**
347
     * @return list<FixerInterface>
348
     */
349
    public function getFixers(): array
350
    {
351
        if (null === $this->fixers) {
5✔
352
            $this->fixers = $this->createFixerFactory()
5✔
353
                ->useRuleSet($this->getRuleSet())
5✔
354
                ->setWhitespacesConfig(new WhitespacesFixerConfig($this->config->getIndent(), $this->config->getLineEnding()))
5✔
355
                ->getFixers()
5✔
356
            ;
5✔
357

358
            if (false === $this->getRiskyAllowed()) {
5✔
359
                $riskyFixers = array_map(
3✔
360
                    static fn (FixerInterface $fixer): string => $fixer->getName(),
3✔
361
                    array_values(array_filter(
3✔
362
                        $this->fixers,
3✔
363
                        static fn (FixerInterface $fixer): bool => $fixer->isRisky()
3✔
364
                    ))
3✔
365
                );
3✔
366

367
                if (\count($riskyFixers) > 0) {
3✔
368
                    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)));
×
369
                }
370
            }
371
        }
372

373
        return $this->fixers;
5✔
374
    }
375

376
    public function getLinter(): LinterInterface
377
    {
378
        if (null === $this->linter) {
1✔
379
            $this->linter = new Linter();
1✔
380
        }
381

382
        return $this->linter;
1✔
383
    }
384

385
    /**
386
     * Returns path.
387
     *
388
     * @return list<string>
389
     */
390
    public function getPath(): array
391
    {
392
        if (null === $this->path) {
94✔
393
            $filesystem = new Filesystem();
94✔
394
            $cwd = $this->cwd;
94✔
395

396
            if (1 === \count($this->options['path']) && '-' === $this->options['path'][0]) {
94✔
397
                $this->path = $this->options['path'];
×
398
            } else {
399
                $this->path = array_map(
94✔
400
                    static function (string $rawPath) use ($cwd, $filesystem): string {
94✔
401
                        $path = trim($rawPath);
46✔
402

403
                        if ('' === $path) {
46✔
404
                            throw new InvalidConfigurationException("Invalid path: \"{$rawPath}\".");
6✔
405
                        }
406

407
                        $absolutePath = $filesystem->isAbsolutePath($path)
42✔
408
                            ? $path
37✔
409
                            : $cwd.\DIRECTORY_SEPARATOR.$path;
5✔
410

411
                        if (!file_exists($absolutePath)) {
42✔
412
                            throw new InvalidConfigurationException(\sprintf(
5✔
413
                                'The path "%s" is not readable.',
5✔
414
                                $path
5✔
415
                            ));
5✔
416
                        }
417

418
                        return $absolutePath;
37✔
419
                    },
94✔
420
                    $this->options['path']
94✔
421
                );
94✔
422
            }
423
        }
424

425
        return $this->path;
83✔
426
    }
427

428
    /**
429
     * @return ProgressOutputType::*
430
     *
431
     * @throws InvalidConfigurationException
432
     */
433
    public function getProgressType(): string
434
    {
435
        if (null === $this->progress) {
13✔
436
            if ('txt' === $this->resolveFormat()) {
13✔
437
                $progressType = $this->options['show-progress'];
11✔
438

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

451
                $this->progress = $progressType;
10✔
452
            } else {
453
                $this->progress = ProgressOutputType::NONE;
2✔
454
            }
455
        }
456

457
        return $this->progress;
12✔
458
    }
459

460
    public function getReporter(): ReporterInterface
461
    {
462
        if (null === $this->reporter) {
8✔
463
            $reporterFactory = new ReporterFactory();
8✔
464
            $reporterFactory->registerBuiltInReporters();
8✔
465

466
            $format = $this->resolveFormat();
8✔
467

468
            try {
469
                $this->reporter = $reporterFactory->getReporter($format);
8✔
470
            } catch (\UnexpectedValueException $e) {
1✔
471
                $formats = $reporterFactory->getFormats();
1✔
472
                sort($formats);
1✔
473

474
                throw new InvalidConfigurationException(\sprintf('The format "%s" is not defined, supported are %s.', $format, Utils::naturalLanguageJoin($formats)));
1✔
475
            }
476
        }
477

478
        return $this->reporter;
7✔
479
    }
480

481
    public function getRiskyAllowed(): bool
482
    {
483
        if (null === $this->allowRisky) {
18✔
484
            if (null === $this->options['allow-risky']) {
18✔
485
                $this->allowRisky = $this->getConfig()->getRiskyAllowed();
10✔
486
            } else {
487
                $this->allowRisky = $this->resolveOptionBooleanValue('allow-risky');
8✔
488
            }
489
        }
490

491
        return $this->allowRisky;
17✔
492
    }
493

494
    /**
495
     * Returns rules.
496
     *
497
     * @return array<string, array<string, mixed>|bool>
498
     */
499
    public function getRules(): array
500
    {
501
        return $this->getRuleSet()->getRules();
11✔
502
    }
503

504
    public function getUsingCache(): bool
505
    {
506
        if (null === $this->usingCache) {
22✔
507
            if (null === $this->options['using-cache']) {
22✔
508
                $this->usingCache = $this->getConfig()->getUsingCache();
17✔
509
            } else {
510
                $this->usingCache = $this->resolveOptionBooleanValue('using-cache');
5✔
511
            }
512
        }
513

514
        $this->usingCache = $this->usingCache && $this->isCachingAllowedForRuntime();
22✔
515

516
        return $this->usingCache;
22✔
517
    }
518

519
    public function getUnsupportedPhpVersionAllowed(): bool
520
    {
521
        if (null === $this->isUnsupportedPhpVersionAllowed) {
×
522
            if (null === $this->options['allow-unsupported-php-version']) {
×
523
                $config = $this->getConfig();
×
524
                $this->isUnsupportedPhpVersionAllowed = $config instanceof UnsupportedPhpVersionAllowedConfigInterface
×
525
                    ? $config->getUnsupportedPhpVersionAllowed()
×
526
                    : false;
×
527
            } else {
528
                $this->isUnsupportedPhpVersionAllowed = $this->resolveOptionBooleanValue('allow-unsupported-php-version');
×
529
            }
530
        }
531

532
        return $this->isUnsupportedPhpVersionAllowed;
×
533
    }
534

535
    /**
536
     * @return iterable<\SplFileInfo>
537
     */
538
    public function getFinder(): iterable
539
    {
540
        if (null === $this->finder) {
31✔
541
            $this->finder = $this->resolveFinder();
31✔
542
        }
543

544
        return $this->finder;
26✔
545
    }
546

547
    /**
548
     * Returns dry-run flag.
549
     */
550
    public function isDryRun(): bool
551
    {
552
        if (null === $this->isDryRun) {
4✔
553
            if ($this->isStdIn()) {
4✔
554
                // Can't write to STDIN
555
                $this->isDryRun = true;
1✔
556
            } else {
557
                $this->isDryRun = $this->options['dry-run'];
3✔
558
            }
559
        }
560

561
        return $this->isDryRun;
4✔
562
    }
563

564
    public function shouldStopOnViolation(): bool
565
    {
566
        return $this->options['stop-on-violation'];
1✔
567
    }
568

569
    public function configFinderIsOverridden(): bool
570
    {
571
        if (null === $this->configFinderIsOverridden) {
7✔
572
            $this->resolveFinder();
7✔
573
        }
574

575
        return $this->configFinderIsOverridden;
7✔
576
    }
577

578
    public function configRulesAreOverridden(): bool
579
    {
580
        if (null === $this->configRulesAreOverridden) {
×
581
            $this->parseRules();
×
582
        }
583

584
        return $this->configRulesAreOverridden;
×
585
    }
586

587
    /**
588
     * Compute file candidates for config file.
589
     *
590
     * @TODO v4: don't offer configs from passed `path` CLI argument
591
     *
592
     * @return list<string>
593
     */
594
    private function computeConfigFiles(): array
595
    {
596
        $configFile = $this->options['config'];
80✔
597

598
        if (self::IGNORE_CONFIG_FILE === $configFile) {
80✔
599
            return [];
2✔
600
        }
601

602
        if (null !== $configFile) {
78✔
603
            if (false === file_exists($configFile) || false === is_readable($configFile)) {
11✔
604
                throw new InvalidConfigurationException(\sprintf('Cannot read config file "%s".', $configFile));
×
605
            }
606

607
            return [$configFile];
11✔
608
        }
609

610
        $path = $this->getPath();
67✔
611

612
        if ($this->isStdIn() || 0 === \count($path)) {
67✔
613
            $configDir = $this->cwd;
45✔
614
        } elseif (1 < \count($path)) {
22✔
615
            // @TODO v4: this is no longer needed due to `MARKER-multi-paths-vs-only-cwd-config`
616
            throw new InvalidConfigurationException('For multiple paths config parameter is required.');
1✔
617
        } elseif (!is_file($path[0])) {
21✔
618
            $configDir = $path[0];
11✔
619
        } else {
620
            $dirName = pathinfo($path[0], \PATHINFO_DIRNAME);
10✔
621
            $configDir = is_dir($dirName) ? $dirName : $path[0];
10✔
622
        }
623

624
        $candidates = [
66✔
625
            $configDir.\DIRECTORY_SEPARATOR.'.php-cs-fixer.php',
66✔
626
            $configDir.\DIRECTORY_SEPARATOR.'.php-cs-fixer.dist.php',
66✔
627

628
            // @TODO v4 drop handling (triggering error) for v2 config names
629
            $configDir.\DIRECTORY_SEPARATOR.'.php_cs', // old v2 config, present here only to throw nice error message later
66✔
630
            $configDir.\DIRECTORY_SEPARATOR.'.php_cs.dist', // old v2 config, present here only to throw nice error message later
66✔
631
        ];
66✔
632

633
        if ($configDir !== $this->cwd) {
66✔
634
            $candidates[] = $this->cwd.\DIRECTORY_SEPARATOR.'.php-cs-fixer.php';
21✔
635
            $candidates[] = $this->cwd.\DIRECTORY_SEPARATOR.'.php-cs-fixer.dist.php';
21✔
636

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

641
            $this->deprecatedNestedConfigDir = $configDir;
21✔
642
        }
643

644
        return $candidates;
66✔
645
    }
646

647
    private function createFixerFactory(): FixerFactory
648
    {
649
        if (null === $this->fixerFactory) {
15✔
650
            $fixerFactory = new FixerFactory();
15✔
651
            $fixerFactory->registerBuiltInFixers();
15✔
652
            $fixerFactory->registerCustomFixers($this->getConfig()->getCustomFixers());
15✔
653

654
            $this->fixerFactory = $fixerFactory;
15✔
655
        }
656

657
        return $this->fixerFactory;
15✔
658
    }
659

660
    private function resolveFormat(): string
661
    {
662
        if (null === $this->format) {
19✔
663
            $formatCandidate = $this->options['format'] ?? $this->getConfig()->getFormat();
19✔
664
            $parts = explode(',', $formatCandidate);
19✔
665

666
            if (\count($parts) > 2) {
19✔
667
                throw new InvalidConfigurationException(\sprintf('The format "%s" is invalid.', $formatCandidate));
×
668
            }
669

670
            $this->format = $parts[0];
19✔
671

672
            if ('@auto' === $this->format) {
19✔
673
                $this->format = $parts[1] ?? 'txt';
3✔
674

675
                if (filter_var(getenv('GITLAB_CI'), \FILTER_VALIDATE_BOOL)) {
3✔
676
                    $this->format = 'gitlab';
1✔
677
                }
678
            }
679
        }
680

681
        return $this->format;
19✔
682
    }
683

684
    private function getRuleSet(): RuleSetInterface
685
    {
686
        if (null === $this->ruleSet) {
16✔
687
            $rules = $this->parseRules();
16✔
688
            $this->validateRules($rules);
15✔
689

690
            $this->ruleSet = new RuleSet($rules);
10✔
691
        }
692

693
        return $this->ruleSet;
10✔
694
    }
695

696
    private function isStdIn(): bool
697
    {
698
        if (null === $this->isStdIn) {
87✔
699
            $this->isStdIn = 1 === \count($this->options['path']) && '-' === $this->options['path'][0];
87✔
700
        }
701

702
        return $this->isStdIn;
87✔
703
    }
704

705
    /**
706
     * @template T
707
     *
708
     * @param iterable<T> $iterable
709
     *
710
     * @return \Traversable<T>
711
     */
712
    private function iterableToTraversable(iterable $iterable): \Traversable
713
    {
714
        return \is_array($iterable) ? new \ArrayIterator($iterable) : $iterable;
25✔
715
    }
716

717
    /**
718
     * @return array<string, mixed>
719
     */
720
    private function parseRules(): array
721
    {
722
        $this->configRulesAreOverridden = null !== $this->options['rules'];
16✔
723

724
        if (null === $this->options['rules']) {
16✔
725
            $this->configRulesAreOverridden = false;
7✔
726

727
            return $this->getConfig()->getRules();
7✔
728
        }
729

730
        $rules = trim($this->options['rules']);
9✔
731
        if ('' === $rules) {
9✔
732
            throw new InvalidConfigurationException('Empty rules value is not allowed.');
1✔
733
        }
734

735
        if (str_starts_with($rules, '{')) {
8✔
736
            try {
737
                return json_decode($rules, true, 512, \JSON_THROW_ON_ERROR);
×
738
            } catch (\JsonException $e) {
×
739
                throw new InvalidConfigurationException(\sprintf('Invalid JSON rules input: "%s".', $e->getMessage()));
×
740
            }
741
        }
742

743
        $rules = [];
8✔
744

745
        foreach (explode(',', $this->options['rules']) as $rule) {
8✔
746
            $rule = trim($rule);
8✔
747

748
            if ('' === $rule) {
8✔
749
                throw new InvalidConfigurationException('Empty rule name is not allowed.');
×
750
            }
751

752
            if (str_starts_with($rule, '-')) {
8✔
753
                $rules[substr($rule, 1)] = false;
2✔
754
            } else {
755
                $rules[$rule] = true;
8✔
756
            }
757
        }
758

759
        $this->configRulesAreOverridden = true;
8✔
760

761
        return $rules;
8✔
762
    }
763

764
    /**
765
     * @param array<string, mixed> $rules
766
     *
767
     * @throws InvalidConfigurationException
768
     */
769
    private function validateRules(array $rules): void
770
    {
771
        /**
772
         * Create a ruleset that contains all configured rules, even when they originally have been disabled.
773
         *
774
         * @see RuleSet::resolveSet()
775
         */
776
        $ruleSet = [];
15✔
777

778
        foreach ($rules as $key => $value) {
15✔
779
            if (\is_int($key)) {
15✔
780
                throw new InvalidConfigurationException(\sprintf('Missing value for "%s" rule/set.', $value));
×
781
            }
782

783
            $ruleSet[$key] = true;
15✔
784
        }
785

786
        $ruleSet = new RuleSet($ruleSet);
15✔
787

788
        $configuredFixers = array_keys($ruleSet->getRules());
15✔
789

790
        $fixers = $this->createFixerFactory()->getFixers();
15✔
791

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

794
        $unknownFixers = array_diff($configuredFixers, $availableFixers);
15✔
795

796
        if (\count($unknownFixers) > 0) {
15✔
797
            /**
798
             * @TODO v4: `renamedRulesFromV2ToV3` no longer needed
799
             * @TODO v3.99: decide how to handle v3 to v4 (where legacy rules are already removed)
800
             */
801
            $renamedRulesFromV2ToV3 = [
5✔
802
                'blank_line_before_return' => [
5✔
803
                    'new_name' => 'blank_line_before_statement',
5✔
804
                    'config' => ['statements' => ['return']],
5✔
805
                ],
5✔
806
                'final_static_access' => [
5✔
807
                    'new_name' => 'self_static_accessor',
5✔
808
                ],
5✔
809
                'hash_to_slash_comment' => [
5✔
810
                    'new_name' => 'single_line_comment_style',
5✔
811
                    'config' => ['comment_types' => ['hash']],
5✔
812
                ],
5✔
813
                'lowercase_constants' => [
5✔
814
                    'new_name' => 'constant_case',
5✔
815
                    'config' => ['case' => 'lower'],
5✔
816
                ],
5✔
817
                'no_extra_consecutive_blank_lines' => [
5✔
818
                    'new_name' => 'no_extra_blank_lines',
5✔
819
                ],
5✔
820
                'no_multiline_whitespace_before_semicolons' => [
5✔
821
                    'new_name' => 'multiline_whitespace_before_semicolons',
5✔
822
                ],
5✔
823
                'no_short_echo_tag' => [
5✔
824
                    'new_name' => 'echo_tag_syntax',
5✔
825
                    'config' => ['format' => 'long'],
5✔
826
                ],
5✔
827
                'php_unit_ordered_covers' => [
5✔
828
                    'new_name' => 'phpdoc_order_by_value',
5✔
829
                    'config' => ['annotations' => ['covers']],
5✔
830
                ],
5✔
831
                'phpdoc_inline_tag' => [
5✔
832
                    'new_name' => 'general_phpdoc_tag_rename, phpdoc_inline_tag_normalizer and phpdoc_tag_type',
5✔
833
                ],
5✔
834
                'pre_increment' => [
5✔
835
                    'new_name' => 'increment_style',
5✔
836
                    'config' => ['style' => 'pre'],
5✔
837
                ],
5✔
838
                'psr0' => [
5✔
839
                    'new_name' => 'psr_autoloading',
5✔
840
                    'config' => ['dir' => 'x'],
5✔
841
                ],
5✔
842
                'psr4' => [
5✔
843
                    'new_name' => 'psr_autoloading',
5✔
844
                ],
5✔
845
                'silenced_deprecation_error' => [
5✔
846
                    'new_name' => 'error_suppression',
5✔
847
                ],
5✔
848
                'trailing_comma_in_multiline_array' => [
5✔
849
                    'new_name' => 'trailing_comma_in_multiline',
5✔
850
                    'config' => ['elements' => ['arrays']],
5✔
851
                ],
5✔
852
            ];
5✔
853

854
            $message = 'The rules contain unknown fixers: ';
5✔
855
            $hasOldRule = false;
5✔
856

857
            foreach ($unknownFixers as $unknownFixer) {
5✔
858
                if (isset($renamedRulesFromV2ToV3[$unknownFixer])) { // Check if present as old renamed rule
5✔
859
                    $hasOldRule = true;
4✔
860
                    $message .= \sprintf(
4✔
861
                        '"%s" is renamed (did you mean "%s"?%s), ',
4✔
862
                        $unknownFixer,
4✔
863
                        $renamedRulesFromV2ToV3[$unknownFixer]['new_name'],
4✔
864
                        isset($renamedRulesFromV2ToV3[$unknownFixer]['config']) ? ' (note: use configuration "'.Utils::toString($renamedRulesFromV2ToV3[$unknownFixer]['config']).'")' : ''
4✔
865
                    );
4✔
866
                } else { // Go to normal matcher if it is not a renamed rule
867
                    $matcher = new WordMatcher($availableFixers);
2✔
868
                    $alternative = $matcher->match($unknownFixer);
2✔
869
                    $message .= \sprintf(
2✔
870
                        '"%s"%s, ',
2✔
871
                        $unknownFixer,
2✔
872
                        null === $alternative ? '' : ' (did you mean "'.$alternative.'"?)'
2✔
873
                    );
2✔
874
                }
875
            }
876

877
            $message = substr($message, 0, -2).'.';
5✔
878

879
            if ($hasOldRule) {
5✔
880
                $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✔
881
            }
882

883
            throw new InvalidConfigurationException($message);
5✔
884
        }
885

886
        foreach ($fixers as $fixer) {
10✔
887
            $fixerName = $fixer->getName();
10✔
888
            if (isset($rules[$fixerName]) && $fixer instanceof DeprecatedFixerInterface) {
10✔
889
                $successors = $fixer->getSuccessorsNames();
3✔
890
                $messageEnd = [] === $successors
3✔
891
                    ? \sprintf(' and will be removed in version %d.0.', Application::getMajorVersion() + 1)
×
892
                    : \sprintf('. Use %s instead.', str_replace('`', '"', Utils::naturalLanguageJoinWithBackticks($successors)));
3✔
893

894
                Future::triggerDeprecation(new \RuntimeException("Rule \"{$fixerName}\" is deprecated{$messageEnd}"));
3✔
895
            }
896
        }
897
    }
898

899
    /**
900
     * Apply path on config instance.
901
     *
902
     * @return iterable<\SplFileInfo>
903
     */
904
    private function resolveFinder(): iterable
905
    {
906
        $this->configFinderIsOverridden = false;
31✔
907

908
        if ($this->isStdIn()) {
31✔
909
            return new \ArrayIterator([new StdinFileInfo()]);
×
910
        }
911

912
        if (!\in_array(
31✔
913
            $this->options['path-mode'],
31✔
914
            self::PATH_MODE_VALUES,
31✔
915
            true
31✔
916
        )) {
31✔
917
            throw new InvalidConfigurationException(\sprintf(
×
918
                'The path-mode "%s" is not defined, supported are %s.',
×
919
                $this->options['path-mode'],
×
920
                Utils::naturalLanguageJoin(self::PATH_MODE_VALUES)
×
921
            ));
×
922
        }
923

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

926
        $paths = array_map(
31✔
927
            static fn (string $path): string => realpath($path), // @phpstan-ignore return.type
31✔
928
            $this->getPath()
31✔
929
        );
31✔
930

931
        if (0 === \count($paths)) {
26✔
932
            if ($isIntersectionPathMode) {
5✔
933
                return new \ArrayIterator([]);
1✔
934
            }
935

936
            return $this->iterableToTraversable($this->getConfig()->getFinder());
4✔
937
        }
938

939
        $pathsByType = [
21✔
940
            'file' => [],
21✔
941
            'dir' => [],
21✔
942
        ];
21✔
943

944
        foreach ($paths as $path) {
21✔
945
            if (is_file($path)) {
21✔
946
                $pathsByType['file'][] = $path;
11✔
947
            } else {
948
                $pathsByType['dir'][] = $path.\DIRECTORY_SEPARATOR;
12✔
949
            }
950
        }
951

952
        $nestedFinder = null;
21✔
953
        $currentFinder = $this->iterableToTraversable($this->getConfig()->getFinder());
21✔
954

955
        try {
956
            $nestedFinder = $currentFinder instanceof \IteratorAggregate ? $currentFinder->getIterator() : $currentFinder;
21✔
957
        } catch (\Exception $e) {
4✔
958
        }
959

960
        if ($isIntersectionPathMode) {
21✔
961
            if (null === $nestedFinder) {
11✔
962
                throw new InvalidConfigurationException(
×
963
                    'Cannot create intersection with not-fully defined Finder in configuration file.'
×
964
                );
×
965
            }
966

967
            return new \CallbackFilterIterator(
11✔
968
                new \IteratorIterator($nestedFinder),
11✔
969
                static function (\SplFileInfo $current) use ($pathsByType): bool {
11✔
970
                    $currentRealPath = $current->getRealPath();
10✔
971

972
                    if (\in_array($currentRealPath, $pathsByType['file'], true)) {
10✔
973
                        return true;
3✔
974
                    }
975

976
                    foreach ($pathsByType['dir'] as $path) {
10✔
977
                        if (str_starts_with($currentRealPath, $path)) {
5✔
978
                            return true;
4✔
979
                        }
980
                    }
981

982
                    return false;
10✔
983
                }
11✔
984
            );
11✔
985
        }
986

987
        if (null !== $this->getConfigFile() && null !== $nestedFinder) {
10✔
988
            $this->configFinderIsOverridden = true;
3✔
989
        }
990

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

996
        return Finder::create()->in($pathsByType['dir'])->append($pathsByType['file']);
6✔
997
    }
998

999
    /**
1000
     * Set option that will be resolved.
1001
     *
1002
     * @param mixed $value
1003
     */
1004
    private function setOption(string $name, $value): void
1005
    {
1006
        if (!\array_key_exists($name, $this->options)) {
103✔
1007
            throw new InvalidConfigurationException(\sprintf('Unknown option name: "%s".', $name));
1✔
1008
        }
1009

1010
        $this->options[$name] = $value;
102✔
1011
    }
1012

1013
    /**
1014
     * @param key-of<_Options> $optionName
1015
     */
1016
    private function resolveOptionBooleanValue(string $optionName): bool
1017
    {
1018
        $value = $this->options[$optionName];
12✔
1019

1020
        if (self::BOOL_YES === $value) {
12✔
1021
            return true;
6✔
1022
        }
1023

1024
        if (self::BOOL_NO === $value) {
7✔
1025
            return false;
6✔
1026
        }
1027

1028
        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✔
1029
    }
1030

1031
    private static function separatedContextLessInclude(string $path): ConfigInterface
1032
    {
1033
        $config = include $path;
20✔
1034

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

1040
        return $config;
19✔
1041
    }
1042

1043
    private function isCachingAllowedForRuntime(): bool
1044
    {
1045
        return $this->toolInfo->isInstalledAsPhar()
14✔
1046
            || $this->toolInfo->isInstalledByComposer()
14✔
1047
            || $this->toolInfo->isRunInsideDocker()
14✔
1048
            || filter_var(getenv('PHP_CS_FIXER_ENFORCE_CACHE'), \FILTER_VALIDATE_BOOL);
14✔
1049
    }
1050
}
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