• 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

59.05
/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\DocumentationTag;
23
use PhpCsFixer\Documentation\DocumentationTagGenerator;
24
use PhpCsFixer\Documentation\DocumentationTagType;
25
use PhpCsFixer\Documentation\FixerDocumentGenerator;
26
use PhpCsFixer\Fixer\ConfigurableFixerInterface;
27
use PhpCsFixer\Fixer\FixerInterface;
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\AutomaticRuleSetDefinitionInterface;
38
use PhpCsFixer\RuleSet\DeprecatedRuleSetDefinitionInterface;
39
use PhpCsFixer\RuleSet\RuleSet;
40
use PhpCsFixer\RuleSet\RuleSetDefinitionInterface;
41
use PhpCsFixer\RuleSet\RuleSets;
42
use PhpCsFixer\StdinFileInfo;
43
use PhpCsFixer\Tokenizer\Tokens;
44
use PhpCsFixer\ToolInfo;
45
use PhpCsFixer\Utils;
46
use PhpCsFixer\WordMatcher;
47
use Symfony\Component\Console\Attribute\AsCommand;
48
use Symfony\Component\Console\Command\Command;
49
use Symfony\Component\Console\Exception\RuntimeException;
50
use Symfony\Component\Console\Formatter\OutputFormatter;
51
use Symfony\Component\Console\Helper\TreeHelper;
52
use Symfony\Component\Console\Helper\TreeNode;
53
use Symfony\Component\Console\Input\ArrayInput;
54
use Symfony\Component\Console\Input\InputArgument;
55
use Symfony\Component\Console\Input\InputInterface;
56
use Symfony\Component\Console\Input\InputOption;
57
use Symfony\Component\Console\Output\ConsoleOutputInterface;
58
use Symfony\Component\Console\Output\OutputInterface;
59
use Symfony\Component\Console\Style\SymfonyStyle;
60

61
/**
62
 * @author Dariusz Rumiński <dariusz.ruminski@gmail.com>
63
 *
64
 * @internal
65
 *
66
 * @no-named-arguments Parameter names are not covered by the backward compatibility promise.
67
 */
68
#[AsCommand(name: 'describe', description: 'Describe rule / ruleset.')]
69
final class DescribeCommand extends Command
70
{
71
    private const SET_ALIAS_TO_DESCRIBE_CONFIG = '@';
72
    private const SET_ALIAS_TO_DESCRIBE_RULES_WITHOUT_SET = '@-';
73

74
    /** @TODO PHP 8.0 - remove the property */
75
    protected static $defaultName = 'describe';
76

77
    /** @TODO PHP 8.0 - remove the property */
78
    protected static $defaultDescription = 'Describe rule / ruleset.';
79

80
    /**
81
     * @var ?list<string>
82
     */
83
    private ?array $setNames = null;
84

85
    private FixerFactory $fixerFactory;
86

87
    /**
88
     * @var null|array<string, FixerInterface>
89
     */
90
    private ?array $fixers = null;
91

92
    public function __construct(?FixerFactory $fixerFactory = null)
93
    {
94
        parent::__construct();
15✔
95

96
        if (null === $fixerFactory) {
15✔
97
            $fixerFactory = new FixerFactory();
15✔
98
            $fixerFactory->registerBuiltInFixers();
15✔
99
        }
100

101
        $this->fixerFactory = $fixerFactory;
15✔
102
    }
103

104
    protected function configure(): void
105
    {
106
        $this->setDefinition(
15✔
107
            [
15✔
108
                new InputArgument('name', InputArgument::OPTIONAL, 'Name of rule / set.', null, fn () => array_merge($this->getSetNames(), array_keys($this->getFixers()))),
15✔
109
                new InputOption('config', '', InputOption::VALUE_REQUIRED, 'The path to a .php-cs-fixer.php file.'),
15✔
110
                new InputOption('expand', '', InputOption::VALUE_NONE, 'Shall nested sets be expanded into nested rules.'),
15✔
111
                new InputOption('format', '', InputOption::VALUE_REQUIRED, 'To output results in other formats (txt, tree).', 'txt', ['txt', 'tree']),
15✔
112
            ]
15✔
113
        );
15✔
114
    }
115

116
    protected function execute(InputInterface $input, OutputInterface $output): int
117
    {
118
        if ($output instanceof ConsoleOutputInterface) {
15✔
119
            $stdErr = $output->getErrorOutput();
×
120
            $stdErr->writeln(Application::getAboutWithRuntime(true));
×
121
        }
122

123
        $resolver = new ConfigurationResolver(
15✔
124
            new Config(),
15✔
125
            ['config' => $input->getOption('config')],
15✔
126
            getcwd(), // @phpstan-ignore argument.type
15✔
127
            new ToolInfo()
15✔
128
        );
15✔
129

130
        $this->fixerFactory->registerCustomFixers($resolver->getConfig()->getCustomFixers());
15✔
131

132
        /** @var ?string $name */
133
        $name = $input->getArgument('name');
15✔
134
        $expand = $input->getOption('expand');
15✔
135
        $format = $input->getOption('format');
15✔
136

137
        if (null === $name) {
15✔
138
            if (false === $input->isInteractive()) {
1✔
139
                throw new RuntimeException('Not enough arguments (missing: "name") when not running interactively.');
1✔
140
            }
141

142
            $io = new SymfonyStyle($input, $output);
×
143
            $shallDescribeConfigInUse = 'yes' === $io->choice(
×
144
                'Do you want to describe used configuration? (alias:`@`',
×
145
                ['yes', 'no'],
×
146
                'yes',
×
147
            );
×
148
            if ($shallDescribeConfigInUse) {
×
149
                $name = self::SET_ALIAS_TO_DESCRIBE_CONFIG;
×
150
            } else {
151
                $name = $io->choice(
×
152
                    'Please select rule / set to describe',
×
153
                    array_merge($this->getSetNames(), array_keys($this->getFixers()))
×
154
                );
×
155
            }
156
        }
157

158
        if ('tree' === $format) {
14✔
159
            if (!str_starts_with($name, '@')) {
×
160
                throw new \InvalidArgumentException(
×
161
                    'The "--format=tree" option is available only when describing a set (name starting with "@").',
×
162
                );
×
163
            }
164
            if (!class_exists(TreeHelper::class)) {
×
165
                throw new \RuntimeException('The "--format=tree" option requires symfony/console 7.3+.');
×
166
            }
167
        }
168

169
        if (!str_starts_with($name, '@')) {
14✔
170
            if (true === $expand) {
12✔
171
                throw new \InvalidArgumentException(
×
172
                    'The "--expand" option is available only when describing a set (name starting with "@").',
×
173
                );
×
174
            }
175
        }
176

177
        try {
178
            if (str_starts_with($name, '@')) {
14✔
179
                $this->describeSet($input, $output, $name, $resolver);
2✔
180

181
                return 0;
1✔
182
            }
183

184
            $this->describeRule($output, $name);
12✔
185
        } catch (DescribeNameNotFoundException $e) {
3✔
186
            $matcher = new WordMatcher(
3✔
187
                'set' === $e->getType() ? $this->getSetNames() : array_keys($this->getFixers())
3✔
188
            );
3✔
189

190
            $alternative = $matcher->match($name);
3✔
191

192
            $this->describeList($output, $e->getType());
3✔
193

194
            throw new \InvalidArgumentException(\sprintf(
3✔
195
                '%s "%s" not found.%s',
3✔
196
                ucfirst($e->getType()),
3✔
197
                $name,
3✔
198
                null === $alternative ? '' : ' Did you mean "'.$alternative.'"?'
3✔
199
            ));
3✔
200
        }
201

202
        return 0;
10✔
203
    }
204

205
    private function describeRule(OutputInterface $output, string $name): void
206
    {
207
        $fixers = $this->getFixers();
12✔
208

209
        if (!isset($fixers[$name])) {
12✔
210
            throw new DescribeNameNotFoundException($name, 'rule');
2✔
211
        }
212

213
        $fixer = $fixers[$name];
10✔
214

215
        $definition = $fixer->getDefinition();
10✔
216

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

220
        if ($output->getVerbosity() >= OutputInterface::VERBOSITY_VERBOSE) {
10✔
221
            $output->writeln(\sprintf('Fixer class: <comment>%s</comment>.', \get_class($fixer)));
1✔
222
            $output->writeln('');
1✔
223
        }
224

225
        $output->writeln($definition->getSummary());
10✔
226

227
        $description = $definition->getDescription();
10✔
228

229
        if (null !== $description) {
10✔
230
            $output->writeln($description);
7✔
231
        }
232

233
        $output->writeln('');
10✔
234

235
        $tags = DocumentationTagGenerator::analyseRule($fixer);
10✔
236

237
        foreach ($tags as $tag) {
10✔
238
            if (DocumentationTagType::DEPRECATED === $tag->type) {
5✔
239
                Future::triggerDeprecation(new \RuntimeException(str_replace(
3✔
240
                    '`',
3✔
241
                    '"',
3✔
242
                    \sprintf(
3✔
243
                        '%s%s',
3✔
244
                        str_replace('This rule', \sprintf('Rule "%s"', $name), $tag->title),
3✔
245
                        null !== $tag->description ? '. '.$tag->description : '',
3✔
246
                    ),
3✔
247
                )));
3✔
248
            } elseif (DocumentationTagType::CONFIGURABLE === $tag->type) {
5✔
249
                continue; // skip, handled later
4✔
250
            }
251

252
            $output->writeln(\sprintf('<error>%s</error>', $tag->title));
4✔
253
            $tagDescription = $tag->description;
4✔
254

255
            if (null !== $tagDescription) {
4✔
256
                $tagDescription = Preg::replace('/(`[^`]+`)/', '<info>$1</info>', $tagDescription);
4✔
257
                $output->writeln($tagDescription);
4✔
258
            }
259

260
            $output->writeln('');
4✔
261
        }
262

263
        if ($fixer instanceof ConfigurableFixerInterface) {
10✔
264
            $configurationDefinition = $fixer->getConfigurationDefinition();
4✔
265
            $options = $configurationDefinition->getOptions();
4✔
266

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

269
            foreach ($options as $option) {
4✔
270
                $line = '* <info>'.OutputFormatter::escape($option->getName()).'</info>';
4✔
271
                $allowed = HelpCommand::getDisplayableAllowedValues($option);
4✔
272

273
                if (null === $allowed) {
4✔
274
                    $allowedTypes = $option->getAllowedTypes();
4✔
275
                    if (null !== $allowedTypes) {
4✔
276
                        $allowed = array_map(
4✔
277
                            static fn (string $type): string => '<comment>'.$type.'</comment>',
4✔
278
                            $allowedTypes,
4✔
279
                        );
4✔
280
                    }
281
                } else {
282
                    $allowed = array_map(static fn ($value): string => $value instanceof AllowedValueSubset
4✔
283
                        ? 'a subset of <comment>'.Utils::toString($value->getAllowedValues()).'</comment>'
3✔
284
                        : '<comment>'.Utils::toString($value).'</comment>', $allowed);
4✔
285
                }
286

287
                if (null !== $allowed) {
4✔
288
                    $line .= ' ('.Utils::naturalLanguageJoin($allowed, '').')';
4✔
289
                }
290

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

294
                if ($option->hasDefault()) {
4✔
295
                    $line .= \sprintf(
4✔
296
                        'defaults to <comment>%s</comment>',
4✔
297
                        Utils::toString($option->getDefault())
4✔
298
                    );
4✔
299
                } else {
300
                    $line .= '<comment>required</comment>';
×
301
                }
302

303
                if ($option instanceof DeprecatedFixerOption) {
4✔
304
                    $line .= '. <error>DEPRECATED</error>: '.Preg::replace(
3✔
305
                        '/(`.+?`)/',
3✔
306
                        '<info>$1</info>',
3✔
307
                        OutputFormatter::escape(lcfirst($option->getDeprecationMessage()))
3✔
308
                    );
3✔
309
                }
310

311
                if ($option instanceof AliasedFixerOption) {
4✔
312
                    $line .= '; <error>DEPRECATED</error> alias: <comment>'.$option->getAlias().'</comment>';
3✔
313
                }
314

315
                $output->writeln($line);
4✔
316
            }
317

318
            $output->writeln('');
4✔
319
        }
320

321
        $codeSamples = array_filter($definition->getCodeSamples(), static function (CodeSampleInterface $codeSample): bool {
10✔
322
            if ($codeSample instanceof VersionSpecificCodeSampleInterface) {
8✔
323
                return $codeSample->isSuitableFor(\PHP_VERSION_ID);
2✔
324
            }
325

326
            return true;
7✔
327
        });
10✔
328

329
        if (0 === \count($definition->getCodeSamples())) {
10✔
330
            $output->writeln([
2✔
331
                'Fixing examples are not available for this rule.',
2✔
332
                '',
2✔
333
            ]);
2✔
334
        } elseif (0 === \count($codeSamples)) {
8✔
335
            $output->writeln([
1✔
336
                'Fixing examples <error>cannot be</error> demonstrated on the current PHP version.',
1✔
337
                '',
1✔
338
            ]);
1✔
339
        } else {
340
            $output->writeln('Fixing examples:');
7✔
341

342
            $differ = new FullDiffer();
7✔
343
            $diffFormatter = new DiffConsoleFormatter(
7✔
344
                $output->isDecorated(),
7✔
345
                \sprintf(
7✔
346
                    '<comment>   ---------- begin diff ----------</comment>%s%%s%s<comment>   ----------- end diff -----------</comment>',
7✔
347
                    \PHP_EOL,
7✔
348
                    \PHP_EOL
7✔
349
                )
7✔
350
            );
7✔
351

352
            foreach ($codeSamples as $index => $codeSample) {
7✔
353
                $old = $codeSample->getCode();
7✔
354
                $tokens = Tokens::fromCode($old);
7✔
355

356
                $configuration = $codeSample->getConfiguration();
7✔
357

358
                if ($fixer instanceof ConfigurableFixerInterface) {
7✔
359
                    $fixer->configure($configuration ?? []);
4✔
360
                }
361

362
                $file = $codeSample instanceof FileSpecificCodeSampleInterface
7✔
363
                    ? $codeSample->getSplFileInfo()
×
364
                    : new StdinFileInfo();
7✔
365

366
                $fixer->fix($file, $tokens);
7✔
367

368
                $diff = $differ->diff($old, $tokens->generateCode());
7✔
369

370
                if ($fixer instanceof ConfigurableFixerInterface) {
7✔
371
                    if (null === $configuration) {
4✔
372
                        $output->writeln(\sprintf(' * Example #%d. Fixing with the <comment>default</comment> configuration.', $index + 1));
4✔
373
                    } else {
374
                        $output->writeln(\sprintf(' * Example #%d. Fixing with configuration: <comment>%s</comment>.', $index + 1, Utils::toString($codeSample->getConfiguration())));
4✔
375
                    }
376
                } else {
377
                    $output->writeln(\sprintf(' * Example #%d.', $index + 1));
3✔
378
                }
379

380
                $output->writeln([$diffFormatter->format($diff, '   %s'), '']);
7✔
381
            }
382
        }
383

384
        $ruleSetConfigs = FixerDocumentGenerator::getSetsOfRule($name);
10✔
385

386
        if ([] !== $ruleSetConfigs) {
10✔
387
            ksort($ruleSetConfigs);
1✔
388
            $plural = 1 !== \count($ruleSetConfigs) ? 's' : '';
1✔
389
            $output->writeln("The fixer is part of the following rule set{$plural}:");
1✔
390

391
            $ruleSetDefinitions = RuleSets::getSetDefinitions();
1✔
392

393
            foreach ($ruleSetConfigs as $set => $config) {
1✔
394
                \assert(isset($ruleSetDefinitions[$set]));
1✔
395
                $ruleSetDefinition = $ruleSetDefinitions[$set];
1✔
396

397
                if ($ruleSetDefinition instanceof AutomaticRuleSetDefinitionInterface) {
1✔
398
                    continue;
1✔
399
                }
400

401
                $deprecatedDesc = ($ruleSetDefinition instanceof DeprecatedRuleSetDefinitionInterface) ? ' *(deprecated)*' : '';
1✔
402
                if (null !== $config) {
1✔
403
                    $output->writeln(\sprintf('* <info>%s</info> with config: <comment>%s</comment>', $set.$deprecatedDesc, Utils::toString($config)));
1✔
404
                } else {
405
                    $output->writeln(\sprintf('* <info>%s</info> with <comment>default</comment> config', $set.$deprecatedDesc));
1✔
406
                }
407
            }
408

409
            $output->writeln('');
1✔
410
        }
411
    }
412

413
    private function describeSet(InputInterface $input, OutputInterface $output, string $name, ConfigurationResolver $resolver): void
414
    {
415
        if (
416
            !\in_array($name, [self::SET_ALIAS_TO_DESCRIBE_CONFIG, self::SET_ALIAS_TO_DESCRIBE_RULES_WITHOUT_SET], true)
2✔
417
            && !\in_array($name, $this->getSetNames(), true)) {
2✔
418
            throw new DescribeNameNotFoundException($name, 'set');
1✔
419
        }
420

421
        if (self::SET_ALIAS_TO_DESCRIBE_CONFIG === $name) {
1✔
422
            $aliasedRuleSetDefinition = $this->createRuleSetDefinition(
×
423
                null,
×
424
                [],
×
425
                [
×
426
                    'getDescription' => null === $resolver->getConfigFile() ? 'Default rules, no config file.' : 'Rules defined in used config.',
×
427
                    'getName' => \sprintf('@ - %s', $resolver->getConfig()->getName()),
×
428
                    'getRules' => $resolver->getConfig()->getRules(),
×
429
                    'isRisky' => $resolver->getRiskyAllowed(),
×
430
                ]
×
431
            );
×
432
        } elseif (self::SET_ALIAS_TO_DESCRIBE_RULES_WITHOUT_SET === $name) {
1✔
433
            $rulesWithoutSet = array_filter(
×
434
                $this->getFixers(),
×
435
                static fn (string $name): bool => [] === FixerDocumentGenerator::getSetsOfRule($name),
×
436
                \ARRAY_FILTER_USE_KEY
×
437
            );
×
438

439
            $aliasedRuleSetDefinition = $this->createRuleSetDefinition(
×
440
                null,
×
441
                [],
×
442
                [
×
443
                    'getDescription' => 'Rules that are not part of any set.',
×
444
                    'getName' => '@- - rules without set',
×
445
                    'getRules' => array_combine(
×
446
                        array_map(
×
447
                            static fn (FixerInterface $fixer): string => $fixer->getName(),
×
448
                            $rulesWithoutSet,
×
449
                        ),
×
450
                        array_fill(0, \count($rulesWithoutSet), true),
×
451
                    ),
×
452
                    'isRisky' => array_any(
×
453
                        $rulesWithoutSet,
×
454
                        static fn (FixerInterface $fixer): bool => $fixer->isRisky(),
×
455
                    ),
×
456
                ]
×
457
            );
×
458
        }
459

460
        $ruleSetDefinitions = RuleSets::getSetDefinitions();
1✔
461
        $ruleSetDefinition = $aliasedRuleSetDefinition ?? $ruleSetDefinitions[$name];
1✔
462
        $fixers = $this->getFixers();
1✔
463

464
        if (true === $input->getOption('expand')) {
1✔
465
            $ruleSetDefinition = $this->createRuleSetDefinition($ruleSetDefinition, ['expand'], []);
×
466
        } else {
467
            $output->writeln("You may the '--expand' option to see nested sets expanded into nested rules.");
1✔
468
        }
469

470
        $output->writeln(\sprintf('<fg=blue>Description of the <info>`%s`</info> set.</>', $ruleSetDefinition->getName()));
1✔
471
        $output->writeln('');
1✔
472

473
        $output->writeln($this->replaceRstLinks($ruleSetDefinition->getDescription()));
1✔
474
        $output->writeln('');
1✔
475

476
        $tags = DocumentationTagGenerator::analyseRuleSet($ruleSetDefinition);
1✔
477

478
        foreach ($tags as $tag) {
1✔
479
            if (DocumentationTagType::DEPRECATED === $tag->type) {
×
480
                Future::triggerDeprecation(new \RuntimeException(str_replace(
×
481
                    '`',
×
482
                    '"',
×
483
                    \sprintf(
×
484
                        '%s%s',
×
485
                        str_replace('This rule set', \sprintf('Rule set "%s"', $name), $tag->title),
×
486
                        null !== $tag->description ? '. '.$tag->description : '',
×
487
                    ),
×
488
                )));
×
489
            }
490

491
            $output->writeln(\sprintf('<error>%s</error>', $tag->title));
×
492
            $tagDescription = $tag->description;
×
493

494
            if (null !== $tagDescription) {
×
495
                $tagDescription = Preg::replace('/(`[^`]+`)/', '<info>$1</info>', $tagDescription);
×
496
                $output->writeln($tagDescription);
×
497
            }
498

499
            $output->writeln('');
×
500
        }
501

502
        if ('tree' === $input->getOption('format')) {
1✔
503
            $this->describeSetContentAsTree($output, $ruleSetDefinition, $ruleSetDefinitions, $fixers);
×
504
        } else {
505
            $this->describeSetContentAsTxt($output, $ruleSetDefinition, $ruleSetDefinitions, $fixers);
1✔
506
        }
507
    }
508

509
    /**
510
     * @param array<string, RuleSetDefinitionInterface> $ruleSetDefinitions
511
     * @param array<string, FixerInterface>             $fixers
512
     */
513
    private function createTreeNode(RuleSetDefinitionInterface $ruleSetDefinition, array $ruleSetDefinitions, array $fixers): TreeNode
514
    {
515
        $tags = DocumentationTagGenerator::analyseRuleSet($ruleSetDefinition);
×
516
        $extra = [] !== $tags
×
517
            ? ' '.implode(' ', array_map(
×
518
                static fn (DocumentationTag $tag): string => "<error>{$tag->type}</error>",
×
519
                $tags,
×
520
            ))
×
521
            : '';
×
522

523
        $node = new TreeNode($ruleSetDefinition->getName().$extra);
×
524

525
        $rules = $ruleSetDefinition->getRules();
×
526
        $rulesKeys = array_keys($rules);
×
527
        natcasesort($rulesKeys);
×
528

529
        foreach ($rulesKeys as $rule) {
×
530
            \assert(isset($rules[$rule]));
×
531
            $config = $rules[$rule];
×
532
            if (str_starts_with($rule, '@')) {
×
533
                $child = $this->createTreeNode($ruleSetDefinitions[$rule], $ruleSetDefinitions, $fixers);
×
534
            } else {
535
                $fixer = $fixers[$rule];
×
536
                $tags = DocumentationTagGenerator::analyseRule($fixer);
×
537
                $extra = [] !== $tags
×
538
                    ? ' '.implode(' ', array_map(
×
539
                        static fn (DocumentationTag $tag): string => "<error>{$tag->type}</error>",
×
540
                        $tags,
×
541
                    ))
×
542
                    : '';
×
543
                if (false === $config) {
×
544
                    $extra = \sprintf('    | <error>Configuration: %s</>', Utils::toString($config));
×
545
                } elseif (true !== $config) {
×
546
                    $extra = \sprintf('    | <comment>Configuration: %s</>', Utils::toString($config));
×
547
                }
548
                $child = new TreeNode($rule.$extra);
×
549
            }
550
            $node->addChild($child);
×
551
        }
552

553
        return $node;
×
554
    }
555

556
    /**
557
     * @param array<string, RuleSetDefinitionInterface> $ruleSetDefinitions
558
     * @param array<string, FixerInterface>             $fixers
559
     */
560
    private function describeSetContentAsTree(OutputInterface $output, RuleSetDefinitionInterface $ruleSetDefinition, array $ruleSetDefinitions, array $fixers): void
561
    {
562
        $io = new SymfonyStyle(
×
563
            new ArrayInput([]),
×
564
            $output
×
565
        );
×
566

567
        $root = $this->createTreeNode($ruleSetDefinition, $ruleSetDefinitions, $fixers);
×
568
        $tree = TreeHelper::createTree($io, $root);
×
569
        $tree->render();
×
570
    }
571

572
    /**
573
     * @param array<string, RuleSetDefinitionInterface> $ruleSetDefinitions
574
     * @param array<string, FixerInterface>             $fixers
575
     */
576
    private function describeSetContentAsTxt(OutputInterface $output, RuleSetDefinitionInterface $ruleSetDefinition, array $ruleSetDefinitions, array $fixers): void
577
    {
578
        $help = '';
1✔
579

580
        foreach ($ruleSetDefinition->getRules() as $rule => $config) {
1✔
581
            if (str_starts_with($rule, '@')) {
1✔
582
                \assert(isset($ruleSetDefinitions[$rule]));
×
583
                $set = $ruleSetDefinitions[$rule];
×
584
                $tags = DocumentationTagGenerator::analyseRuleSet($set);
×
585
                $help .= \sprintf(
×
586
                    " * <info>%s</info>%s%s\n   | %s\n\n",
×
587
                    $rule,
×
588
                    [] !== $tags ? ' ' : '',
×
589
                    implode(' ', array_map(
×
590
                        static fn (DocumentationTag $tag): string => "<error>{$tag->type}</error>",
×
591
                        $tags,
×
592
                    )),
×
593
                    $this->replaceRstLinks($set->getDescription())
×
594
                );
×
595

596
                continue;
×
597
            }
598

599
            \assert(isset($fixers[$rule]));
1✔
600
            $fixer = $fixers[$rule];
1✔
601
            $tags = DocumentationTagGenerator::analyseRule($fixer);
1✔
602

603
            $definition = $fixer->getDefinition();
1✔
604
            $help .= \sprintf(
1✔
605
                " * <info>%s</info>%s%s\n   | %s\n%s\n",
1✔
606
                $rule,
1✔
607
                [] !== $tags ? ' ' : '',
1✔
608
                implode(' ', array_map(
1✔
609
                    static fn (DocumentationTag $tag): string => "<error>{$tag->type}</error>",
1✔
610
                    $tags,
1✔
611
                )),
1✔
612
                $definition->getSummary(),
1✔
613
                true !== $config ? \sprintf("   <comment>| Configuration: %s</comment>\n", Utils::toString($config)) : ''
1✔
614
            );
1✔
615
        }
616

617
        $output->write($help);
1✔
618
    }
619

620
    /**
621
     * @return array<string, FixerInterface>
622
     */
623
    private function getFixers(): array
624
    {
625
        if (null !== $this->fixers) {
13✔
626
            return $this->fixers;
2✔
627
        }
628

629
        $fixers = [];
13✔
630

631
        foreach ($this->fixerFactory->getFixers() as $fixer) {
13✔
632
            $fixers[$fixer->getName()] = $fixer;
12✔
633
        }
634

635
        $this->fixers = $fixers;
13✔
636
        ksort($this->fixers);
13✔
637

638
        return $this->fixers;
13✔
639
    }
640

641
    /**
642
     * @return list<string>
643
     */
644
    private function getSetNames(): array
645
    {
646
        if (null !== $this->setNames) {
2✔
647
            return $this->setNames;
1✔
648
        }
649

650
        $this->setNames = RuleSets::getSetDefinitionNames();
2✔
651

652
        return $this->setNames;
2✔
653
    }
654

655
    /**
656
     * @param string $type 'rule'|'set'
657
     */
658
    private function describeList(OutputInterface $output, string $type): void
659
    {
660
        if ($output->getVerbosity() < OutputInterface::VERBOSITY_VERBOSE) {
3✔
661
            return;
3✔
662
        }
663

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

667
            $items = $this->getSetNames();
×
668
            foreach ($items as $item) {
×
669
                $output->writeln(\sprintf('* <info>%s</info>', $item));
×
670
            }
671
        }
672

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

676
            $items = array_keys($this->getFixers());
×
677
            foreach ($items as $item) {
×
678
                $output->writeln(\sprintf('* <info>%s</info>', $item));
×
679
            }
680
        }
681
    }
682

683
    private function replaceRstLinks(string $content): string
684
    {
685
        return Preg::replaceCallback(
1✔
686
            '/(`[^<]+<[^>]+>`_)/',
1✔
687
            static fn (array $matches) => Preg::replaceCallback(
1✔
688
                '/`(.*)<(.*)>`_/',
1✔
689
                static fn (array $matches): string => $matches[1].'('.$matches[2].')',
1✔
690
                $matches[1]
1✔
691
            ),
1✔
692
            $content
1✔
693
        );
1✔
694
    }
695

696
    /**
697
     * @param list<'expand'>                                                                                                        $adjustments
698
     * @param array{getDescription?: string, getName?: string, getRules?: array<string, array<string, mixed>|bool>, isRisky?: bool} $overrides
699
     */
700
    private function createRuleSetDefinition(?RuleSetDefinitionInterface $ruleSetDefinition, array $adjustments, array $overrides): RuleSetDefinitionInterface
701
    {
702
        return new class($ruleSetDefinition, $adjustments, $overrides) implements RuleSetDefinitionInterface {
×
703
            private ?RuleSetDefinitionInterface $original;
704

705
            /** @var list<'expand'> */
706
            private array $adjustments;
707

708
            /** @var array{getDescription?: string, getName?: string, getRules?: array<string, array<string, mixed>|bool>, isRisky?: bool} */
709
            private array $overrides;
710

711
            /**
712
             * @param list<'expand'>                                                                                                        $adjustments
713
             * @param array{getDescription?: string, getName?: string, getRules?: array<string, array<string, mixed>|bool>, isRisky?: bool} $overrides
714
             */
715
            public function __construct(
716
                ?RuleSetDefinitionInterface $original,
717
                array $adjustments,
718
                array $overrides
719
            ) {
720
                $this->original = $original;
×
721
                $this->adjustments = $adjustments;
×
722
                $this->overrides = $overrides;
×
723
            }
724

725
            public function getDescription(): string
726
            {
727
                return $this->overrides[__FUNCTION__]
×
728
                    ?? (null !== $this->original ? $this->original->{__FUNCTION__}() : 'unknown description'); // @phpstan-ignore method.dynamicName
×
729
            }
730

731
            public function getName(): string
732
            {
733
                $value = $this->overrides[__FUNCTION__]
×
734
                    ?? (null !== $this->original ? $this->original->{__FUNCTION__}() : 'unknown name'); // @phpstan-ignore method.dynamicName
×
735

736
                if (\in_array('expand', $this->adjustments, true)) {
×
737
                    $value .= ' (expanded)';
×
738
                }
739

740
                return $value;
×
741
            }
742

743
            public function getRules(): array
744
            {
745
                $value = $this->overrides[__FUNCTION__]
×
746
                    ?? (null !== $this->original ? $this->original->{__FUNCTION__}() : null); // @phpstan-ignore method.dynamicName
×
747

748
                if (null === $value) {
×
749
                    throw new \LogicException('Cannot get rules from unknown original rule set and missing overrides.');
×
750
                }
751

752
                if (\in_array('expand', $this->adjustments, true)) {
×
753
                    $value = (new RuleSet($value))->getRules();
×
754
                }
755

756
                return $value;
×
757
            }
758

759
            public function isRisky(): bool
760
            {
761
                $value = $this->overrides[__FUNCTION__]
×
762
                    ?? (null !== $this->original ? $this->original->{__FUNCTION__}() : null); // @phpstan-ignore method.dynamicName
×
763

764
                if (null === $value) {
×
765
                    throw new \LogicException('Cannot get isRisky from unknown original rule set and missing overrides.');
×
766
                }
767

768
                return $value;
×
769
            }
770
        };
×
771
    }
772
}
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