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

keradus / PHP-CS-Fixer / 17328452173

29 Aug 2025 03:54PM UTC coverage: 94.693% (+0.01%) from 94.683%
17328452173

push

github

keradus
Merge branch 'test' of github.com:keradus/PHP-CS-Fixer into test

19 of 32 new or added lines in 6 files covered. (59.38%)

33 existing lines in 4 files now uncovered.

28318 of 29905 relevant lines covered (94.69%)

45.61 hits per line

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

90.22
/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: value-of<self::PATH_MODE_VALUES>,
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
 * @no-named-arguments Parameter names are not covered by the backward compatibility promise.
80
 */
81
final class ConfigurationResolver
82
{
83
    public const PATH_MODE_OVERRIDE = 'override';
84
    public const PATH_MODE_INTERSECTION = 'intersection';
85
    public const PATH_MODE_VALUES = [
86
        self::PATH_MODE_OVERRIDE,
87
        self::PATH_MODE_INTERSECTION,
88
    ];
89

90
    public const BOOL_YES = 'yes';
91
    public const BOOL_NO = 'no';
92
    public const BOOL_VALUES = [
93
        self::BOOL_YES,
94
        self::BOOL_NO,
95
    ];
96

97
    /**
98
     * @TODO v4: this is no longer needed due to `MARKER-multi-paths-vs-only-cwd-config`
99
     */
100
    private ?string $deprecatedNestedConfigDir = null;
101

102
    private ?bool $allowRisky = null;
103

104
    private ?ConfigInterface $config = null;
105

106
    private ?string $configFile = null;
107

108
    private string $cwd;
109

110
    private ConfigInterface $defaultConfig;
111

112
    private ?ReporterInterface $reporter = null;
113

114
    private ?bool $isStdIn = null;
115

116
    private ?bool $isDryRun = null;
117

118
    /**
119
     * @var null|list<FixerInterface>
120
     */
121
    private ?array $fixers = null;
122

123
    private ?bool $configFinderIsOverridden = null;
124

125
    private ToolInfoInterface $toolInfo;
126

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

148
    private ?string $cacheFile = null;
149

150
    private ?CacheManagerInterface $cacheManager = null;
151

152
    private ?DifferInterface $differ = null;
153

154
    private ?Directory $directory = null;
155

156
    /**
157
     * @var null|iterable<\SplFileInfo>
158
     */
159
    private ?iterable $finder = null;
160

161
    private ?string $format = null;
162

163
    private ?Linter $linter = null;
164

165
    /**
166
     * @var null|list<string>
167
     */
168
    private ?array $path = null;
169

170
    /**
171
     * @var null|ProgressOutputType::*
172
     */
173
    private $progress;
174

175
    private ?RuleSet $ruleSet = null;
176

177
    private ?bool $usingCache = null;
178

179
    private ?bool $isUnsupportedPhpVersionAllowed = null;
180

181
    private ?FixerFactory $fixerFactory = null;
182

183
    /**
184
     * @param array<string, mixed> $options
185
     */
186
    public function __construct(
187
        ConfigInterface $config,
188
        array $options,
189
        string $cwd,
190
        ToolInfoInterface $toolInfo
191
    ) {
192
        $this->defaultConfig = $config;
126✔
193
        $this->cwd = $cwd;
126✔
194
        $this->toolInfo = $toolInfo;
126✔
195

196
        foreach ($options as $name => $value) {
126✔
197
            $this->setOption($name, $value);
102✔
198
        }
199
    }
200

201
    public function getCacheFile(): ?string
202
    {
203
        if (!$this->getUsingCache()) {
10✔
204
            return null;
4✔
205
        }
206

207
        if (null === $this->cacheFile) {
6✔
208
            if (null === $this->options['cache-file']) {
6✔
209
                $this->cacheFile = $this->getConfig()->getCacheFile();
4✔
210
            } else {
211
                $this->cacheFile = $this->options['cache-file'];
2✔
212
            }
213
        }
214

215
        return $this->cacheFile;
6✔
216
    }
217

218
    public function getCacheManager(): CacheManagerInterface
219
    {
220
        if (null === $this->cacheManager) {
1✔
221
            $cacheFile = $this->getCacheFile();
1✔
222

223
            if (null === $cacheFile) {
1✔
224
                $this->cacheManager = new NullCacheManager();
1✔
225
            } else {
226
                $this->cacheManager = new FileCacheManager(
×
227
                    new FileHandler($cacheFile),
×
228
                    new Signature(
×
229
                        \PHP_VERSION,
×
230
                        $this->toolInfo->getVersion(),
×
231
                        $this->getConfig()->getIndent(),
×
232
                        $this->getConfig()->getLineEnding(),
×
233
                        $this->getRules()
×
234
                    ),
×
235
                    $this->isDryRun(),
×
236
                    $this->getDirectory()
×
237
                );
×
238
            }
239
        }
240

241
        return $this->cacheManager;
1✔
242
    }
243

244
    public function getConfig(): ConfigInterface
245
    {
246
        if (null === $this->config) {
79✔
247
            foreach ($this->computeConfigFiles() as $configFile) {
79✔
248
                if (!file_exists($configFile)) {
78✔
249
                    continue;
63✔
250
                }
251

252
                $configFileBasename = basename($configFile);
20✔
253

254
                /** @TODO v4 drop handling (triggering error) for v2 config names */
255
                $deprecatedConfigs = [
20✔
256
                    '.php_cs' => '.php-cs-fixer.php',
20✔
257
                    '.php_cs.dist' => '.php-cs-fixer.dist.php',
20✔
258
                ];
20✔
259

260
                if (isset($deprecatedConfigs[$configFileBasename])) {
20✔
261
                    throw new InvalidConfigurationException("Configuration file `{$configFileBasename}` is outdated, rename to `{$deprecatedConfigs[$configFileBasename]}`.");
×
262
                }
263

264
                if (null !== $this->deprecatedNestedConfigDir && str_starts_with($configFile, $this->deprecatedNestedConfigDir)) {
20✔
265
                    // @TODO v4: when removing, remove also TODO with `MARKER-multi-paths-vs-only-cwd-config`
266
                    Utils::triggerDeprecation(
7✔
267
                        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✔
268
                    );
7✔
269
                }
270

271
                $this->config = self::separatedContextLessInclude($configFile);
20✔
272
                $this->configFile = $configFile;
19✔
273

274
                break;
19✔
275
            }
276

277
            if (null === $this->config) {
77✔
278
                $this->config = $this->defaultConfig;
58✔
279
            }
280
        }
281

282
        return $this->config;
77✔
283
    }
284

285
    public function getParallelConfig(): ParallelConfig
286
    {
287
        $config = $this->getConfig();
3✔
288

289
        return true !== $this->options['sequential'] && $config instanceof ParallelAwareConfigInterface
3✔
290
            ? $config->getParallelConfig()
2✔
291
            : ParallelConfigFactory::sequential();
3✔
292
    }
293

294
    public function getConfigFile(): ?string
295
    {
296
        if (null === $this->configFile) {
19✔
297
            $this->getConfig();
14✔
298
        }
299

300
        return $this->configFile;
19✔
301
    }
302

303
    public function getDiffer(): DifferInterface
304
    {
305
        if (null === $this->differ) {
4✔
306
            $this->differ = (true === $this->options['diff']) ? new UnifiedDiffer() : new NullDiffer();
4✔
307
        }
308

309
        return $this->differ;
4✔
310
    }
311

312
    public function getDirectory(): DirectoryInterface
313
    {
314
        if (null === $this->directory) {
4✔
315
            $path = $this->getCacheFile();
4✔
316
            if (null === $path) {
4✔
317
                $absolutePath = $this->cwd;
1✔
318
            } else {
319
                $filesystem = new Filesystem();
3✔
320

321
                $absolutePath = $filesystem->isAbsolutePath($path)
3✔
322
                    ? $path
2✔
323
                    : $this->cwd.\DIRECTORY_SEPARATOR.$path;
1✔
324
                $absolutePath = \dirname($absolutePath);
3✔
325
            }
326

327
            $this->directory = new Directory($absolutePath);
4✔
328
        }
329

330
        return $this->directory;
4✔
331
    }
332

333
    /**
334
     * @return list<FixerInterface>
335
     */
336
    public function getFixers(): array
337
    {
338
        if (null === $this->fixers) {
5✔
339
            $this->fixers = $this->createFixerFactory()
5✔
340
                ->useRuleSet($this->getRuleSet())
5✔
341
                ->setWhitespacesConfig(new WhitespacesFixerConfig($this->config->getIndent(), $this->config->getLineEnding()))
5✔
342
                ->getFixers()
5✔
343
            ;
5✔
344

345
            if (false === $this->getRiskyAllowed()) {
5✔
346
                $riskyFixers = array_map(
3✔
347
                    static fn (FixerInterface $fixer): string => $fixer->getName(),
3✔
348
                    array_filter(
3✔
349
                        $this->fixers,
3✔
350
                        static fn (FixerInterface $fixer): bool => $fixer->isRisky()
3✔
351
                    )
3✔
352
                );
3✔
353

354
                if (\count($riskyFixers) > 0) {
3✔
355
                    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)));
×
356
                }
357
            }
358
        }
359

360
        return $this->fixers;
5✔
361
    }
362

363
    public function getLinter(): LinterInterface
364
    {
365
        if (null === $this->linter) {
1✔
366
            $this->linter = new Linter();
1✔
367
        }
368

369
        return $this->linter;
1✔
370
    }
371

372
    /**
373
     * Returns path.
374
     *
375
     * @return list<string>
376
     */
377
    public function getPath(): array
378
    {
379
        if (null === $this->path) {
93✔
380
            $filesystem = new Filesystem();
93✔
381
            $cwd = $this->cwd;
93✔
382

383
            if (1 === \count($this->options['path']) && '-' === $this->options['path'][0]) {
93✔
384
                $this->path = $this->options['path'];
×
385
            } else {
386
                $this->path = array_map(
93✔
387
                    static function (string $rawPath) use ($cwd, $filesystem): string {
93✔
388
                        $path = trim($rawPath);
46✔
389

390
                        if ('' === $path) {
46✔
391
                            throw new InvalidConfigurationException("Invalid path: \"{$rawPath}\".");
6✔
392
                        }
393

394
                        $absolutePath = $filesystem->isAbsolutePath($path)
42✔
395
                            ? $path
37✔
396
                            : $cwd.\DIRECTORY_SEPARATOR.$path;
5✔
397

398
                        if (!file_exists($absolutePath)) {
42✔
399
                            throw new InvalidConfigurationException(\sprintf(
5✔
400
                                'The path "%s" is not readable.',
5✔
401
                                $path
5✔
402
                            ));
5✔
403
                        }
404

405
                        return $absolutePath;
37✔
406
                    },
93✔
407
                    $this->options['path']
93✔
408
                );
93✔
409
            }
410
        }
411

412
        return $this->path;
82✔
413
    }
414

415
    /**
416
     * @return ProgressOutputType::*
417
     *
418
     * @throws InvalidConfigurationException
419
     */
420
    public function getProgressType(): string
421
    {
422
        if (null === $this->progress) {
13✔
423
            if ('txt' === $this->resolveFormat()) {
13✔
424
                $progressType = $this->options['show-progress'];
11✔
425

426
                if (null === $progressType) {
11✔
427
                    $progressType = $this->getConfig()->getHideProgress()
4✔
428
                        ? ProgressOutputType::NONE
2✔
429
                        : ProgressOutputType::BAR;
2✔
430
                } elseif (!\in_array($progressType, ProgressOutputType::all(), true)) {
7✔
431
                    throw new InvalidConfigurationException(\sprintf(
1✔
432
                        'The progress type "%s" is not defined, supported are %s.',
1✔
433
                        $progressType,
1✔
434
                        Utils::naturalLanguageJoin(ProgressOutputType::all())
1✔
435
                    ));
1✔
436
                }
437

438
                $this->progress = $progressType;
10✔
439
            } else {
440
                $this->progress = ProgressOutputType::NONE;
2✔
441
            }
442
        }
443

444
        return $this->progress;
12✔
445
    }
446

447
    public function getReporter(): ReporterInterface
448
    {
449
        if (null === $this->reporter) {
8✔
450
            $reporterFactory = new ReporterFactory();
8✔
451
            $reporterFactory->registerBuiltInReporters();
8✔
452

453
            $format = $this->resolveFormat();
8✔
454

455
            try {
456
                $this->reporter = $reporterFactory->getReporter($format);
8✔
457
            } catch (\UnexpectedValueException $e) {
1✔
458
                $formats = $reporterFactory->getFormats();
1✔
459
                sort($formats);
1✔
460

461
                throw new InvalidConfigurationException(\sprintf('The format "%s" is not defined, supported are %s.', $format, Utils::naturalLanguageJoin($formats)));
1✔
462
            }
463
        }
464

465
        return $this->reporter;
7✔
466
    }
467

468
    public function getRiskyAllowed(): bool
469
    {
470
        if (null === $this->allowRisky) {
18✔
471
            if (null === $this->options['allow-risky']) {
18✔
472
                $this->allowRisky = $this->getConfig()->getRiskyAllowed();
10✔
473
            } else {
474
                $this->allowRisky = $this->resolveOptionBooleanValue('allow-risky');
8✔
475
            }
476
        }
477

478
        return $this->allowRisky;
17✔
479
    }
480

481
    /**
482
     * Returns rules.
483
     *
484
     * @return array<string, array<string, mixed>|bool>
485
     */
486
    public function getRules(): array
487
    {
488
        return $this->getRuleSet()->getRules();
11✔
489
    }
490

491
    public function getUsingCache(): bool
492
    {
493
        if (null === $this->usingCache) {
22✔
494
            if (null === $this->options['using-cache']) {
22✔
495
                $this->usingCache = $this->getConfig()->getUsingCache();
17✔
496
            } else {
497
                $this->usingCache = $this->resolveOptionBooleanValue('using-cache');
5✔
498
            }
499
        }
500

501
        $this->usingCache = $this->usingCache && $this->isCachingAllowedForRuntime();
22✔
502

503
        return $this->usingCache;
22✔
504
    }
505

506
    public function getUnsupportedPhpVersionAllowed(): bool
507
    {
508
        if (null === $this->isUnsupportedPhpVersionAllowed) {
×
509
            if (null === $this->options['allow-unsupported-php-version']) {
×
510
                $config = $this->getConfig();
×
511
                $this->isUnsupportedPhpVersionAllowed = $config instanceof UnsupportedPhpVersionAllowedConfigInterface
×
512
                    ? $config->getUnsupportedPhpVersionAllowed()
×
513
                    : false;
×
514
            } else {
515
                $this->isUnsupportedPhpVersionAllowed = $this->resolveOptionBooleanValue('allow-unsupported-php-version');
×
516
            }
517
        }
518

519
        return $this->isUnsupportedPhpVersionAllowed;
×
520
    }
521

522
    /**
523
     * @return iterable<\SplFileInfo>
524
     */
525
    public function getFinder(): iterable
526
    {
527
        if (null === $this->finder) {
31✔
528
            $this->finder = $this->resolveFinder();
31✔
529
        }
530

531
        return $this->finder;
26✔
532
    }
533

534
    /**
535
     * Returns dry-run flag.
536
     */
537
    public function isDryRun(): bool
538
    {
539
        if (null === $this->isDryRun) {
4✔
540
            if ($this->isStdIn()) {
4✔
541
                // Can't write to STDIN
542
                $this->isDryRun = true;
1✔
543
            } else {
544
                $this->isDryRun = $this->options['dry-run'];
3✔
545
            }
546
        }
547

548
        return $this->isDryRun;
4✔
549
    }
550

551
    public function shouldStopOnViolation(): bool
552
    {
553
        return $this->options['stop-on-violation'];
1✔
554
    }
555

556
    public function configFinderIsOverridden(): bool
557
    {
558
        if (null === $this->configFinderIsOverridden) {
7✔
559
            $this->resolveFinder();
7✔
560
        }
561

562
        return $this->configFinderIsOverridden;
7✔
563
    }
564

565
    /**
566
     * Compute file candidates for config file.
567
     *
568
     * @TODO v4: don't offer configs from passed `path` CLI argument
569
     *
570
     * @return list<string>
571
     */
572
    private function computeConfigFiles(): array
573
    {
574
        $configFile = $this->options['config'];
79✔
575

576
        if (null !== $configFile) {
79✔
577
            if (false === file_exists($configFile) || false === is_readable($configFile)) {
11✔
578
                throw new InvalidConfigurationException(\sprintf('Cannot read config file "%s".', $configFile));
×
579
            }
580

581
            return [$configFile];
11✔
582
        }
583

584
        $path = $this->getPath();
68✔
585

586
        if ($this->isStdIn() || 0 === \count($path)) {
68✔
587
            $configDir = $this->cwd;
45✔
588
        } elseif (1 < \count($path)) {
23✔
589
            // @TODO v4: this is no longer needed due to `MARKER-multi-paths-vs-only-cwd-config`
590
            throw new InvalidConfigurationException('For multiple paths config parameter is required.');
1✔
591
        } elseif (!is_file($path[0])) {
22✔
592
            $configDir = $path[0];
12✔
593
        } else {
594
            $dirName = pathinfo($path[0], \PATHINFO_DIRNAME);
10✔
595
            $configDir = is_dir($dirName) ? $dirName : $path[0];
10✔
596
        }
597

598
        $candidates = [
67✔
599
            $configDir.\DIRECTORY_SEPARATOR.'.php-cs-fixer.php',
67✔
600
            $configDir.\DIRECTORY_SEPARATOR.'.php-cs-fixer.dist.php',
67✔
601

602
            // @TODO v4 drop handling (triggering error) for v2 config names
603
            $configDir.\DIRECTORY_SEPARATOR.'.php_cs', // old v2 config, present here only to throw nice error message later
67✔
604
            $configDir.\DIRECTORY_SEPARATOR.'.php_cs.dist', // old v2 config, present here only to throw nice error message later
67✔
605
        ];
67✔
606

607
        if ($configDir !== $this->cwd) {
67✔
608
            $candidates[] = $this->cwd.\DIRECTORY_SEPARATOR.'.php-cs-fixer.php';
22✔
609
            $candidates[] = $this->cwd.\DIRECTORY_SEPARATOR.'.php-cs-fixer.dist.php';
22✔
610

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

615
            $this->deprecatedNestedConfigDir = $configDir;
22✔
616
        }
617

618
        return $candidates;
67✔
619
    }
620

621
    private function createFixerFactory(): FixerFactory
622
    {
623
        if (null === $this->fixerFactory) {
15✔
624
            $fixerFactory = new FixerFactory();
15✔
625
            $fixerFactory->registerBuiltInFixers();
15✔
626
            $fixerFactory->registerCustomFixers($this->getConfig()->getCustomFixers());
15✔
627

628
            $this->fixerFactory = $fixerFactory;
15✔
629
        }
630

631
        return $this->fixerFactory;
15✔
632
    }
633

634
    private function resolveFormat(): string
635
    {
636
        if (null === $this->format) {
19✔
637
            $formatCandidate = $this->options['format'] ?? $this->getConfig()->getFormat();
19✔
638
            $parts = explode(',', $formatCandidate);
19✔
639

640
            if (\count($parts) > 2) {
19✔
641
                throw new InvalidConfigurationException(\sprintf('The format "%s" is invalid.', $formatCandidate));
×
642
            }
643

644
            $this->format = $parts[0];
19✔
645

646
            if ('@auto' === $this->format) {
19✔
647
                $this->format = $parts[1] ?? 'txt';
3✔
648

649
                if (filter_var(getenv('GITLAB_CI'), \FILTER_VALIDATE_BOOL)) {
3✔
650
                    $this->format = 'gitlab';
1✔
651
                }
652
            }
653
        }
654

655
        return $this->format;
19✔
656
    }
657

658
    private function getRuleSet(): RuleSetInterface
659
    {
660
        if (null === $this->ruleSet) {
16✔
661
            $rules = $this->parseRules();
16✔
662
            $this->validateRules($rules);
15✔
663

664
            $this->ruleSet = new RuleSet($rules);
10✔
665
        }
666

667
        return $this->ruleSet;
10✔
668
    }
669

670
    private function isStdIn(): bool
671
    {
672
        if (null === $this->isStdIn) {
86✔
673
            $this->isStdIn = 1 === \count($this->options['path']) && '-' === $this->options['path'][0];
86✔
674
        }
675

676
        return $this->isStdIn;
86✔
677
    }
678

679
    /**
680
     * @template T
681
     *
682
     * @param iterable<T> $iterable
683
     *
684
     * @return \Traversable<T>
685
     */
686
    private function iterableToTraversable(iterable $iterable): \Traversable
687
    {
688
        return \is_array($iterable) ? new \ArrayIterator($iterable) : $iterable;
25✔
689
    }
690

691
    /**
692
     * @return array<string, mixed>
693
     */
694
    private function parseRules(): array
695
    {
696
        if (null === $this->options['rules']) {
16✔
697
            return $this->getConfig()->getRules();
7✔
698
        }
699

700
        $rules = trim($this->options['rules']);
9✔
701
        if ('' === $rules) {
9✔
702
            throw new InvalidConfigurationException('Empty rules value is not allowed.');
1✔
703
        }
704

705
        if (str_starts_with($rules, '{')) {
8✔
706
            try {
NEW
707
                return json_decode($rules, true, 512, \JSON_THROW_ON_ERROR);
×
NEW
708
            } catch (\JsonException $e) {
×
NEW
709
                throw new InvalidConfigurationException(\sprintf('Invalid JSON rules input: "%s".', $e->getMessage()));
×
710
            }
711
        }
712

713
        $rules = [];
8✔
714

715
        foreach (explode(',', $this->options['rules']) as $rule) {
8✔
716
            $rule = trim($rule);
8✔
717

718
            if ('' === $rule) {
8✔
UNCOV
719
                throw new InvalidConfigurationException('Empty rule name is not allowed.');
×
720
            }
721

722
            if (str_starts_with($rule, '-')) {
8✔
723
                $rules[substr($rule, 1)] = false;
2✔
724
            } else {
725
                $rules[$rule] = true;
8✔
726
            }
727
        }
728

729
        return $rules;
8✔
730
    }
731

732
    /**
733
     * @param array<string, mixed> $rules
734
     *
735
     * @throws InvalidConfigurationException
736
     */
737
    private function validateRules(array $rules): void
738
    {
739
        /**
740
         * Create a ruleset that contains all configured rules, even when they originally have been disabled.
741
         *
742
         * @see RuleSet::resolveSet()
743
         */
744
        $ruleSet = [];
15✔
745

746
        foreach ($rules as $key => $value) {
15✔
747
            if (\is_int($key)) {
15✔
UNCOV
748
                throw new InvalidConfigurationException(\sprintf('Missing value for "%s" rule/set.', $value));
×
749
            }
750

751
            $ruleSet[$key] = true;
15✔
752
        }
753

754
        $ruleSet = new RuleSet($ruleSet);
15✔
755

756
        $configuredFixers = array_keys($ruleSet->getRules());
15✔
757

758
        $fixers = $this->createFixerFactory()->getFixers();
15✔
759

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

762
        $unknownFixers = array_diff($configuredFixers, $availableFixers);
15✔
763

764
        if (\count($unknownFixers) > 0) {
15✔
765
            $renamedRules = [
5✔
766
                'blank_line_before_return' => [
5✔
767
                    'new_name' => 'blank_line_before_statement',
5✔
768
                    'config' => ['statements' => ['return']],
5✔
769
                ],
5✔
770
                'final_static_access' => [
5✔
771
                    'new_name' => 'self_static_accessor',
5✔
772
                ],
5✔
773
                'hash_to_slash_comment' => [
5✔
774
                    'new_name' => 'single_line_comment_style',
5✔
775
                    'config' => ['comment_types' => ['hash']],
5✔
776
                ],
5✔
777
                'lowercase_constants' => [
5✔
778
                    'new_name' => 'constant_case',
5✔
779
                    'config' => ['case' => 'lower'],
5✔
780
                ],
5✔
781
                'no_extra_consecutive_blank_lines' => [
5✔
782
                    'new_name' => 'no_extra_blank_lines',
5✔
783
                ],
5✔
784
                'no_multiline_whitespace_before_semicolons' => [
5✔
785
                    'new_name' => 'multiline_whitespace_before_semicolons',
5✔
786
                ],
5✔
787
                'no_short_echo_tag' => [
5✔
788
                    'new_name' => 'echo_tag_syntax',
5✔
789
                    'config' => ['format' => 'long'],
5✔
790
                ],
5✔
791
                'php_unit_ordered_covers' => [
5✔
792
                    'new_name' => 'phpdoc_order_by_value',
5✔
793
                    'config' => ['annotations' => ['covers']],
5✔
794
                ],
5✔
795
                'phpdoc_inline_tag' => [
5✔
796
                    'new_name' => 'general_phpdoc_tag_rename, phpdoc_inline_tag_normalizer and phpdoc_tag_type',
5✔
797
                ],
5✔
798
                'pre_increment' => [
5✔
799
                    'new_name' => 'increment_style',
5✔
800
                    'config' => ['style' => 'pre'],
5✔
801
                ],
5✔
802
                'psr0' => [
5✔
803
                    'new_name' => 'psr_autoloading',
5✔
804
                    'config' => ['dir' => 'x'],
5✔
805
                ],
5✔
806
                'psr4' => [
5✔
807
                    'new_name' => 'psr_autoloading',
5✔
808
                ],
5✔
809
                'silenced_deprecation_error' => [
5✔
810
                    'new_name' => 'error_suppression',
5✔
811
                ],
5✔
812
                'trailing_comma_in_multiline_array' => [
5✔
813
                    'new_name' => 'trailing_comma_in_multiline',
5✔
814
                    'config' => ['elements' => ['arrays']],
5✔
815
                ],
5✔
816
            ];
5✔
817

818
            $message = 'The rules contain unknown fixers: ';
5✔
819
            $hasOldRule = false;
5✔
820

821
            foreach ($unknownFixers as $unknownFixer) {
5✔
822
                if (isset($renamedRules[$unknownFixer])) { // Check if present as old renamed rule
5✔
823
                    $hasOldRule = true;
4✔
824
                    $message .= \sprintf(
4✔
825
                        '"%s" is renamed (did you mean "%s"?%s), ',
4✔
826
                        $unknownFixer,
4✔
827
                        $renamedRules[$unknownFixer]['new_name'],
4✔
828
                        isset($renamedRules[$unknownFixer]['config']) ? ' (note: use configuration "'.Utils::toString($renamedRules[$unknownFixer]['config']).'")' : ''
4✔
829
                    );
4✔
830
                } else { // Go to normal matcher if it is not a renamed rule
831
                    $matcher = new WordMatcher($availableFixers);
2✔
832
                    $alternative = $matcher->match($unknownFixer);
2✔
833
                    $message .= \sprintf(
2✔
834
                        '"%s"%s, ',
2✔
835
                        $unknownFixer,
2✔
836
                        null === $alternative ? '' : ' (did you mean "'.$alternative.'"?)'
2✔
837
                    );
2✔
838
                }
839
            }
840

841
            $message = substr($message, 0, -2).'.';
5✔
842

843
            if ($hasOldRule) {
5✔
844
                $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✔
845
            }
846

847
            throw new InvalidConfigurationException($message);
5✔
848
        }
849

850
        foreach ($fixers as $fixer) {
10✔
851
            $fixerName = $fixer->getName();
10✔
852
            if (isset($rules[$fixerName]) && $fixer instanceof DeprecatedFixerInterface) {
10✔
853
                $successors = $fixer->getSuccessorsNames();
3✔
854
                $messageEnd = [] === $successors
3✔
UNCOV
855
                    ? \sprintf(' and will be removed in version %d.0.', Application::getMajorVersion() + 1)
×
856
                    : \sprintf('. Use %s instead.', str_replace('`', '"', Utils::naturalLanguageJoinWithBackticks($successors)));
3✔
857

858
                Utils::triggerDeprecation(new \RuntimeException("Rule \"{$fixerName}\" is deprecated{$messageEnd}"));
3✔
859
            }
860
        }
861
    }
862

863
    /**
864
     * Apply path on config instance.
865
     *
866
     * @return iterable<\SplFileInfo>
867
     */
868
    private function resolveFinder(): iterable
869
    {
870
        $this->configFinderIsOverridden = false;
31✔
871

872
        if ($this->isStdIn()) {
31✔
UNCOV
873
            return new \ArrayIterator([new StdinFileInfo()]);
×
874
        }
875

876
        if (!\in_array(
31✔
877
            $this->options['path-mode'],
31✔
878
            self::PATH_MODE_VALUES,
31✔
879
            true
31✔
880
        )) {
31✔
881
            throw new InvalidConfigurationException(\sprintf(
×
882
                'The path-mode "%s" is not defined, supported are %s.',
×
883
                $this->options['path-mode'],
×
UNCOV
884
                Utils::naturalLanguageJoin(self::PATH_MODE_VALUES)
×
UNCOV
885
            ));
×
886
        }
887

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

890
        $paths = array_map(
31✔
891
            static fn (string $path) => realpath($path),
31✔
892
            $this->getPath()
31✔
893
        );
31✔
894

895
        if (0 === \count($paths)) {
26✔
896
            if ($isIntersectionPathMode) {
5✔
897
                return new \ArrayIterator([]);
1✔
898
            }
899

900
            return $this->iterableToTraversable($this->getConfig()->getFinder());
4✔
901
        }
902

903
        $pathsByType = [
21✔
904
            'file' => [],
21✔
905
            'dir' => [],
21✔
906
        ];
21✔
907

908
        foreach ($paths as $path) {
21✔
909
            if (is_file($path)) {
21✔
910
                $pathsByType['file'][] = $path;
11✔
911
            } else {
912
                $pathsByType['dir'][] = $path.\DIRECTORY_SEPARATOR;
12✔
913
            }
914
        }
915

916
        $nestedFinder = null;
21✔
917
        $currentFinder = $this->iterableToTraversable($this->getConfig()->getFinder());
21✔
918

919
        try {
920
            $nestedFinder = $currentFinder instanceof \IteratorAggregate ? $currentFinder->getIterator() : $currentFinder;
21✔
921
        } catch (\Exception $e) {
4✔
922
        }
923

924
        if ($isIntersectionPathMode) {
21✔
925
            if (null === $nestedFinder) {
11✔
926
                throw new InvalidConfigurationException(
×
UNCOV
927
                    'Cannot create intersection with not-fully defined Finder in configuration file.'
×
UNCOV
928
                );
×
929
            }
930

931
            return new \CallbackFilterIterator(
11✔
932
                new \IteratorIterator($nestedFinder),
11✔
933
                static function (\SplFileInfo $current) use ($pathsByType): bool {
11✔
934
                    $currentRealPath = $current->getRealPath();
10✔
935

936
                    if (\in_array($currentRealPath, $pathsByType['file'], true)) {
10✔
937
                        return true;
3✔
938
                    }
939

940
                    foreach ($pathsByType['dir'] as $path) {
10✔
941
                        if (str_starts_with($currentRealPath, $path)) {
5✔
942
                            return true;
4✔
943
                        }
944
                    }
945

946
                    return false;
10✔
947
                }
11✔
948
            );
11✔
949
        }
950

951
        if (null !== $this->getConfigFile() && null !== $nestedFinder) {
10✔
952
            $this->configFinderIsOverridden = true;
3✔
953
        }
954

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

960
        return Finder::create()->in($pathsByType['dir'])->append($pathsByType['file']);
6✔
961
    }
962

963
    /**
964
     * Set option that will be resolved.
965
     *
966
     * @param mixed $value
967
     */
968
    private function setOption(string $name, $value): void
969
    {
970
        if (!\array_key_exists($name, $this->options)) {
102✔
971
            throw new InvalidConfigurationException(\sprintf('Unknown option name: "%s".', $name));
1✔
972
        }
973

974
        $this->options[$name] = $value;
101✔
975
    }
976

977
    /**
978
     * @param key-of<_Options> $optionName
979
     */
980
    private function resolveOptionBooleanValue(string $optionName): bool
981
    {
982
        $value = $this->options[$optionName];
12✔
983

984
        if (self::BOOL_YES === $value) {
12✔
985
            return true;
6✔
986
        }
987

988
        if (self::BOOL_NO === $value) {
7✔
989
            return false;
6✔
990
        }
991

992
        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✔
993
    }
994

995
    private static function separatedContextLessInclude(string $path): ConfigInterface
996
    {
997
        $config = include $path;
20✔
998

999
        // verify that the config has an instance of Config
1000
        if (!$config instanceof ConfigInterface) {
20✔
1001
            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✔
1002
        }
1003

1004
        return $config;
19✔
1005
    }
1006

1007
    private function isCachingAllowedForRuntime(): bool
1008
    {
1009
        return $this->toolInfo->isInstalledAsPhar()
14✔
1010
            || $this->toolInfo->isInstalledByComposer()
14✔
1011
            || $this->toolInfo->isRunInsideDocker()
14✔
1012
            || filter_var(getenv('PHP_CS_FIXER_ENFORCE_CACHE'), \FILTER_VALIDATE_BOOL);
14✔
1013
    }
1014
}
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