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

keradus / PHP-CS-Fixer / 17899394524

21 Sep 2025 09:49PM UTC coverage: 94.396% (-0.2%) from 94.55%
17899394524

push

github

web-flow
refactor: introduce concept of AutomaticRuleSet (#9067)

99 of 150 new or added lines in 6 files covered. (66.0%)

4 existing lines in 2 files now uncovered.

28486 of 30177 relevant lines covered (94.4%)

45.35 hits per line

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

70.12
/src/Console/Command/DescribeCommand.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\Command;
16

17
use PhpCsFixer\Config;
18
use PhpCsFixer\Console\Application;
19
use PhpCsFixer\Console\ConfigurationResolver;
20
use PhpCsFixer\Differ\DiffConsoleFormatter;
21
use PhpCsFixer\Differ\FullDiffer;
22
use PhpCsFixer\Documentation\FixerDocumentGenerator;
23
use PhpCsFixer\Fixer\ConfigurableFixerInterface;
24
use PhpCsFixer\Fixer\DeprecatedFixerInterface;
25
use PhpCsFixer\Fixer\ExperimentalFixerInterface;
26
use PhpCsFixer\Fixer\FixerInterface;
27
use PhpCsFixer\Fixer\InternalFixerInterface;
28
use PhpCsFixer\FixerConfiguration\AliasedFixerOption;
29
use PhpCsFixer\FixerConfiguration\AllowedValueSubset;
30
use PhpCsFixer\FixerConfiguration\DeprecatedFixerOption;
31
use PhpCsFixer\FixerDefinition\CodeSampleInterface;
32
use PhpCsFixer\FixerDefinition\FileSpecificCodeSampleInterface;
33
use PhpCsFixer\FixerDefinition\VersionSpecificCodeSampleInterface;
34
use PhpCsFixer\FixerFactory;
35
use PhpCsFixer\Future;
36
use PhpCsFixer\Preg;
37
use PhpCsFixer\RuleSet\AutomaticRuleSetDescriptionInterface;
38
use PhpCsFixer\RuleSet\DeprecatedRuleSetDescriptionInterface;
39
use PhpCsFixer\RuleSet\RuleSets;
40
use PhpCsFixer\StdinFileInfo;
41
use PhpCsFixer\Tokenizer\Tokens;
42
use PhpCsFixer\ToolInfo;
43
use PhpCsFixer\Utils;
44
use PhpCsFixer\WordMatcher;
45
use Symfony\Component\Console\Attribute\AsCommand;
46
use Symfony\Component\Console\Command\Command;
47
use Symfony\Component\Console\Formatter\OutputFormatter;
48
use Symfony\Component\Console\Input\InputArgument;
49
use Symfony\Component\Console\Input\InputInterface;
50
use Symfony\Component\Console\Input\InputOption;
51
use Symfony\Component\Console\Output\ConsoleOutputInterface;
52
use Symfony\Component\Console\Output\OutputInterface;
53

54
/**
55
 * @author Dariusz Rumiński <dariusz.ruminski@gmail.com>
56
 *
57
 * @internal
58
 *
59
 * @no-named-arguments Parameter names are not covered by the backward compatibility promise.
60
 */
61
#[AsCommand(name: 'describe', description: 'Describe rule / ruleset.')]
62
final class DescribeCommand extends Command
63
{
64
    /** @TODO PHP 8.0 - remove the property */
65
    protected static $defaultName = 'describe';
66

67
    /** @TODO PHP 8.0 - remove the property */
68
    protected static $defaultDescription = 'Describe rule / ruleset.';
69

70
    /**
71
     * @var ?list<string>
72
     */
73
    private ?array $setNames = null;
74

75
    private FixerFactory $fixerFactory;
76

77
    /**
78
     * @var null|array<string, FixerInterface>
79
     */
80
    private ?array $fixers = null;
81

82
    public function __construct(?FixerFactory $fixerFactory = null)
83
    {
84
        parent::__construct();
14✔
85

86
        if (null === $fixerFactory) {
14✔
87
            $fixerFactory = new FixerFactory();
14✔
88
            $fixerFactory->registerBuiltInFixers();
14✔
89
        }
90

91
        $this->fixerFactory = $fixerFactory;
14✔
92
    }
93

94
    protected function configure(): void
95
    {
96
        $this->setDefinition(
14✔
97
            [
14✔
98
                new InputArgument('name', InputArgument::REQUIRED, 'Name of rule / set.', null, fn () => array_merge($this->getSetNames(), array_keys($this->getFixers()))),
14✔
99
                new InputOption('config', '', InputOption::VALUE_REQUIRED, 'The path to a .php-cs-fixer.php file.'),
14✔
100
            ]
14✔
101
        );
14✔
102
    }
103

104
    protected function execute(InputInterface $input, OutputInterface $output): int
105
    {
106
        if ($output instanceof ConsoleOutputInterface) {
13✔
107
            $stdErr = $output->getErrorOutput();
×
108
            $stdErr->writeln(Application::getAboutWithRuntime(true));
×
109
        }
110

111
        $resolver = new ConfigurationResolver(
13✔
112
            new Config(),
13✔
113
            ['config' => $input->getOption('config')],
13✔
114
            getcwd(),
13✔
115
            new ToolInfo()
13✔
116
        );
13✔
117

118
        $this->fixerFactory->registerCustomFixers($resolver->getConfig()->getCustomFixers());
13✔
119

120
        $name = $input->getArgument('name');
13✔
121

122
        try {
123
            if (str_starts_with($name, '@')) {
13✔
124
                $this->describeSet($output, $name);
1✔
125

126
                return 0;
×
127
            }
128

129
            $this->describeRule($output, $name);
12✔
130
        } catch (DescribeNameNotFoundException $e) {
3✔
131
            $matcher = new WordMatcher(
3✔
132
                'set' === $e->getType() ? $this->getSetNames() : array_keys($this->getFixers())
3✔
133
            );
3✔
134

135
            $alternative = $matcher->match($name);
3✔
136

137
            $this->describeList($output, $e->getType());
3✔
138

139
            throw new \InvalidArgumentException(\sprintf(
3✔
140
                '%s "%s" not found.%s',
3✔
141
                ucfirst($e->getType()),
3✔
142
                $name,
3✔
143
                null === $alternative ? '' : ' Did you mean "'.$alternative.'"?'
3✔
144
            ));
3✔
145
        }
146

147
        return 0;
10✔
148
    }
149

150
    private function describeRule(OutputInterface $output, string $name): void
151
    {
152
        $fixers = $this->getFixers();
12✔
153

154
        if (!isset($fixers[$name])) {
12✔
155
            throw new DescribeNameNotFoundException($name, 'rule');
2✔
156
        }
157

158
        $fixer = $fixers[$name];
10✔
159

160
        $definition = $fixer->getDefinition();
10✔
161

162
        $output->writeln(\sprintf('<fg=blue>Description of the <info>`%s`</info> rule.</>', $name));
10✔
163
        $output->writeln('');
10✔
164

165
        if ($output->getVerbosity() >= OutputInterface::VERBOSITY_VERBOSE) {
10✔
166
            $output->writeln(\sprintf('Fixer class: <comment>%s</comment>.', \get_class($fixer)));
1✔
167
            $output->writeln('');
1✔
168
        }
169

170
        if ($fixer instanceof DeprecatedFixerInterface) {
10✔
171
            $successors = $fixer->getSuccessorsNames();
3✔
172
            $message = [] === $successors
3✔
173
                ? \sprintf('it will be removed in version %d.0', Application::getMajorVersion() + 1)
×
174
                : \sprintf('use %s instead', Utils::naturalLanguageJoinWithBackticks($successors));
3✔
175

176
            $endMessage = '. '.ucfirst($message);
3✔
177
            Future::triggerDeprecation(new \RuntimeException(str_replace('`', '"', "Rule \"{$name}\" is deprecated{$endMessage}.")));
3✔
178
            $message = Preg::replace('/(`[^`]+`)/', '<info>$1</info>', $message);
3✔
179
            $output->writeln(\sprintf('<error>DEPRECATED</error>: %s.', $message));
3✔
180
            $output->writeln('');
3✔
181
        }
182

183
        $output->writeln($definition->getSummary());
10✔
184

185
        $description = $definition->getDescription();
10✔
186

187
        if (null !== $description) {
10✔
188
            $output->writeln($description);
7✔
189
        }
190

191
        $output->writeln('');
10✔
192

193
        if ($fixer instanceof ExperimentalFixerInterface) {
10✔
194
            $output->writeln('<error>Fixer applying this rule is EXPERIMENTAL.</error>.');
×
195
            $output->writeln('It is not covered with backward compatibility promise and may produce unstable or unexpected results.');
×
196

197
            $output->writeln('');
×
198
        }
199

200
        if ($fixer instanceof InternalFixerInterface) {
10✔
201
            $output->writeln('<error>Fixer applying this rule is INTERNAL.</error>.');
×
202
            $output->writeln('It is expected to be used only on PHP CS Fixer project itself.');
×
203

204
            $output->writeln('');
×
205
        }
206

207
        if ($fixer->isRisky()) {
10✔
208
            $output->writeln('<error>Fixer applying this rule is RISKY.</error>');
4✔
209

210
            $riskyDescription = $definition->getRiskyDescription();
4✔
211

212
            if (null !== $riskyDescription) {
4✔
213
                $output->writeln($riskyDescription);
3✔
214
            }
215

216
            $output->writeln('');
4✔
217
        }
218

219
        if ($fixer instanceof ConfigurableFixerInterface) {
10✔
220
            $configurationDefinition = $fixer->getConfigurationDefinition();
4✔
221
            $options = $configurationDefinition->getOptions();
4✔
222

223
            $output->writeln(\sprintf('Fixer is configurable using following option%s:', 1 === \count($options) ? '' : 's'));
4✔
224

225
            foreach ($options as $option) {
4✔
226
                $line = '* <info>'.OutputFormatter::escape($option->getName()).'</info>';
4✔
227
                $allowed = HelpCommand::getDisplayableAllowedValues($option);
4✔
228

229
                if (null === $allowed) {
4✔
230
                    $allowedTypes = $option->getAllowedTypes();
4✔
231
                    if (null !== $allowedTypes) {
4✔
232
                        $allowed = array_map(
4✔
233
                            static fn (string $type): string => '<comment>'.$type.'</comment>',
4✔
234
                            $allowedTypes,
4✔
235
                        );
4✔
236
                    }
237
                } else {
238
                    $allowed = array_map(static fn ($value): string => $value instanceof AllowedValueSubset
4✔
239
                        ? 'a subset of <comment>'.Utils::toString($value->getAllowedValues()).'</comment>'
3✔
240
                        : '<comment>'.Utils::toString($value).'</comment>', $allowed);
4✔
241
                }
242

243
                if (null !== $allowed) {
4✔
244
                    $line .= ' ('.Utils::naturalLanguageJoin($allowed, '').')';
4✔
245
                }
246

247
                $description = Preg::replace('/(`.+?`)/', '<info>$1</info>', OutputFormatter::escape($option->getDescription()));
4✔
248
                $line .= ': '.lcfirst(Preg::replace('/\.$/', '', $description)).'; ';
4✔
249

250
                if ($option->hasDefault()) {
4✔
251
                    $line .= \sprintf(
4✔
252
                        'defaults to <comment>%s</comment>',
4✔
253
                        Utils::toString($option->getDefault())
4✔
254
                    );
4✔
255
                } else {
256
                    $line .= '<comment>required</comment>';
×
257
                }
258

259
                if ($option instanceof DeprecatedFixerOption) {
4✔
260
                    $line .= '. <error>DEPRECATED</error>: '.Preg::replace(
3✔
261
                        '/(`.+?`)/',
3✔
262
                        '<info>$1</info>',
3✔
263
                        OutputFormatter::escape(lcfirst($option->getDeprecationMessage()))
3✔
264
                    );
3✔
265
                }
266

267
                if ($option instanceof AliasedFixerOption) {
4✔
268
                    $line .= '; <error>DEPRECATED</error> alias: <comment>'.$option->getAlias().'</comment>';
3✔
269
                }
270

271
                $output->writeln($line);
4✔
272
            }
273

274
            $output->writeln('');
4✔
275
        }
276

277
        $codeSamples = array_filter($definition->getCodeSamples(), static function (CodeSampleInterface $codeSample): bool {
10✔
278
            if ($codeSample instanceof VersionSpecificCodeSampleInterface) {
8✔
279
                return $codeSample->isSuitableFor(\PHP_VERSION_ID);
2✔
280
            }
281

282
            return true;
7✔
283
        });
10✔
284

285
        if (0 === \count($definition->getCodeSamples())) {
10✔
286
            $output->writeln([
2✔
287
                'Fixing examples are not available for this rule.',
2✔
288
                '',
2✔
289
            ]);
2✔
290
        } elseif (0 === \count($codeSamples)) {
8✔
291
            $output->writeln([
1✔
292
                'Fixing examples <error>cannot be</error> demonstrated on the current PHP version.',
1✔
293
                '',
1✔
294
            ]);
1✔
295
        } else {
296
            $output->writeln('Fixing examples:');
7✔
297

298
            $differ = new FullDiffer();
7✔
299
            $diffFormatter = new DiffConsoleFormatter(
7✔
300
                $output->isDecorated(),
7✔
301
                \sprintf(
7✔
302
                    '<comment>   ---------- begin diff ----------</comment>%s%%s%s<comment>   ----------- end diff -----------</comment>',
7✔
303
                    \PHP_EOL,
7✔
304
                    \PHP_EOL
7✔
305
                )
7✔
306
            );
7✔
307

308
            foreach ($codeSamples as $index => $codeSample) {
7✔
309
                $old = $codeSample->getCode();
7✔
310
                $tokens = Tokens::fromCode($old);
7✔
311

312
                $configuration = $codeSample->getConfiguration();
7✔
313

314
                if ($fixer instanceof ConfigurableFixerInterface) {
7✔
315
                    $fixer->configure($configuration ?? []);
4✔
316
                }
317

318
                $file = $codeSample instanceof FileSpecificCodeSampleInterface
7✔
319
                    ? $codeSample->getSplFileInfo()
×
320
                    : new StdinFileInfo();
7✔
321

322
                $fixer->fix($file, $tokens);
7✔
323

324
                $diff = $differ->diff($old, $tokens->generateCode());
7✔
325

326
                if ($fixer instanceof ConfigurableFixerInterface) {
7✔
327
                    if (null === $configuration) {
4✔
328
                        $output->writeln(\sprintf(' * Example #%d. Fixing with the <comment>default</comment> configuration.', $index + 1));
4✔
329
                    } else {
330
                        $output->writeln(\sprintf(' * Example #%d. Fixing with configuration: <comment>%s</comment>.', $index + 1, Utils::toString($codeSample->getConfiguration())));
4✔
331
                    }
332
                } else {
333
                    $output->writeln(\sprintf(' * Example #%d.', $index + 1));
3✔
334
                }
335

336
                $output->writeln([$diffFormatter->format($diff, '   %s'), '']);
7✔
337
            }
338
        }
339

340
        $ruleSetConfigs = FixerDocumentGenerator::getSetsOfRule($name);
10✔
341

342
        if ([] !== $ruleSetConfigs) {
10✔
343
            ksort($ruleSetConfigs);
1✔
344
            $plural = 1 !== \count($ruleSetConfigs) ? 's' : '';
1✔
345
            $output->writeln("The fixer is part of the following rule set{$plural}:");
1✔
346

347
            $ruleSetDefinitions = RuleSets::getSetDefinitions();
1✔
348

349
            foreach ($ruleSetConfigs as $set => $config) {
1✔
350
                \assert(isset($ruleSetDefinitions[$set]));
1✔
351
                $ruleSetDescription = $ruleSetDefinitions[$set];
1✔
352

353
                if ($ruleSetDescription instanceof AutomaticRuleSetDescriptionInterface) {
1✔
NEW
354
                    continue;
×
355
                }
356

357
                $deprecatedDesc = ($ruleSetDescription instanceof DeprecatedRuleSetDescriptionInterface) ? ' *(deprecated)*' : '';
1✔
358
                if (null !== $config) {
1✔
359
                    $output->writeln(\sprintf('* <info>%s</info> with config: <comment>%s</comment>', $set.$deprecatedDesc, Utils::toString($config)));
1✔
360
                } else {
361
                    $output->writeln(\sprintf('* <info>%s</info> with <comment>default</comment> config', $set.$deprecatedDesc));
1✔
362
                }
363
            }
364

365
            $output->writeln('');
1✔
366
        }
367
    }
368

369
    private function describeSet(OutputInterface $output, string $name): void
370
    {
371
        if (!\in_array($name, $this->getSetNames(), true)) {
1✔
372
            throw new DescribeNameNotFoundException($name, 'set');
1✔
373
        }
374

375
        $ruleSetDefinitions = RuleSets::getSetDefinitions();
×
376
        $ruleSetDescription = $ruleSetDefinitions[$name];
×
377
        $fixers = $this->getFixers();
×
378

379
        $output->writeln(\sprintf('<fg=blue>Description of the <info>`%s`</info> set.</>', $ruleSetDescription->getName()));
×
380
        $output->writeln('');
×
381

382
        $output->writeln($this->replaceRstLinks($ruleSetDescription->getDescription()));
×
383
        $output->writeln('');
×
384

385
        if ($ruleSetDescription instanceof DeprecatedRuleSetDescriptionInterface) {
×
386
            $successors = $ruleSetDescription->getSuccessorsNames();
×
387
            $message = [] === $successors
×
388
                ? \sprintf('it will be removed in version %d.0', Application::getMajorVersion() + 1)
×
389
                : \sprintf('use %s instead', Utils::naturalLanguageJoinWithBackticks($successors));
×
390

391
            Future::triggerDeprecation(new \RuntimeException(str_replace('`', '"', "Set \"{$name}\" is deprecated, {$message}.")));
×
392
            $message = Preg::replace('/(`[^`]+`)/', '<info>$1</info>', $message);
×
393
            $output->writeln(\sprintf('<error>DEPRECATED</error>: %s.', $message));
×
394
            $output->writeln('');
×
395
        }
396

397
        if ($ruleSetDescription->isRisky()) {
×
398
            $output->writeln('<error>This set contains risky rules.</error>');
×
399
            $output->writeln('');
×
400
        }
401

NEW
402
        if ($ruleSetDescription instanceof AutomaticRuleSetDescriptionInterface) {
×
NEW
403
            $output->writeln(AutomaticRuleSetDescriptionInterface::WARNING_MESSAGE_DECORATED);
×
NEW
404
            $output->writeln('');
×
405
        }
406

UNCOV
407
        $help = '';
×
408

409
        foreach ($ruleSetDescription->getRules() as $rule => $config) {
×
410
            if (str_starts_with($rule, '@')) {
×
411
                $set = $ruleSetDefinitions[$rule];
×
412
                $help .= \sprintf(
×
413
                    " * <info>%s</info>%s\n   | %s\n\n",
×
414
                    $rule,
×
415
                    $set->isRisky() ? ' <error>risky</error>' : '',
×
416
                    $this->replaceRstLinks($set->getDescription())
×
417
                );
×
418

419
                continue;
×
420
            }
421

422
            $fixer = $fixers[$rule];
×
423

424
            $definition = $fixer->getDefinition();
×
425
            $help .= \sprintf(
×
426
                " * <info>%s</info>%s\n   | %s\n%s\n",
×
427
                $rule,
×
428
                $fixer->isRisky() ? ' <error>risky</error>' : '',
×
429
                $definition->getSummary(),
×
430
                true !== $config ? \sprintf("   <comment>| Configuration: %s</comment>\n", Utils::toString($config)) : ''
×
431
            );
×
432
        }
433

434
        $output->write($help);
×
435
    }
436

437
    /**
438
     * @return array<string, FixerInterface>
439
     */
440
    private function getFixers(): array
441
    {
442
        if (null !== $this->fixers) {
12✔
443
            return $this->fixers;
2✔
444
        }
445

446
        $fixers = [];
12✔
447

448
        foreach ($this->fixerFactory->getFixers() as $fixer) {
12✔
449
            $fixers[$fixer->getName()] = $fixer;
12✔
450
        }
451

452
        $this->fixers = $fixers;
12✔
453
        ksort($this->fixers);
12✔
454

455
        return $this->fixers;
12✔
456
    }
457

458
    /**
459
     * @return list<string>
460
     */
461
    private function getSetNames(): array
462
    {
463
        if (null !== $this->setNames) {
1✔
464
            return $this->setNames;
1✔
465
        }
466

467
        $this->setNames = RuleSets::getSetDefinitionNames();
1✔
468

469
        return $this->setNames;
1✔
470
    }
471

472
    /**
473
     * @param string $type 'rule'|'set'
474
     */
475
    private function describeList(OutputInterface $output, string $type): void
476
    {
477
        if ($output->getVerbosity() < OutputInterface::VERBOSITY_VERBOSE) {
3✔
478
            return;
3✔
479
        }
480

481
        if ($output->getVerbosity() >= OutputInterface::VERBOSITY_VERY_VERBOSE || 'set' === $type) {
×
482
            $output->writeln('<comment>Defined sets:</comment>');
×
483

484
            $items = $this->getSetNames();
×
485
            foreach ($items as $item) {
×
486
                $output->writeln(\sprintf('* <info>%s</info>', $item));
×
487
            }
488
        }
489

490
        if ($output->getVerbosity() >= OutputInterface::VERBOSITY_VERY_VERBOSE || 'rule' === $type) {
×
491
            $output->writeln('<comment>Defined rules:</comment>');
×
492

493
            $items = array_keys($this->getFixers());
×
494
            foreach ($items as $item) {
×
495
                $output->writeln(\sprintf('* <info>%s</info>', $item));
×
496
            }
497
        }
498
    }
499

500
    private function replaceRstLinks(string $content): string
501
    {
502
        return Preg::replaceCallback(
×
503
            '/(`[^<]+<[^>]+>`_)/',
×
504
            static fn (array $matches) => Preg::replaceCallback(
×
505
                '/`(.*)<(.*)>`_/',
×
506
                static fn (array $matches): string => $matches[1].'('.$matches[2].')',
×
507
                $matches[1]
×
508
            ),
×
509
            $content
×
510
        );
×
511
    }
512
}
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