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

keradus / PHP-CS-Fixer / 24023665044

02 Apr 2026 09:33PM UTC coverage: 93.056% (+0.1%) from 92.938%
24023665044

push

github

web-flow
chore: add tests for `BracesPositionFixer` (#9522)

29548 of 31753 relevant lines covered (93.06%)

43.98 hits per line

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

58.9
/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
    /**
75
     * @var ?list<string>
76
     */
77
    private ?array $setNames = null;
78

79
    private FixerFactory $fixerFactory;
80

81
    /**
82
     * @var null|array<string, FixerInterface>
83
     */
84
    private ?array $fixers = null;
85

86
    public function __construct(?FixerFactory $fixerFactory = null)
87
    {
88
        parent::__construct('describe');
15✔
89
        $this->setDescription('Describe rule / ruleset.');
15✔
90

91
        if (null === $fixerFactory) {
15✔
92
            $fixerFactory = new FixerFactory();
15✔
93
            $fixerFactory->registerBuiltInFixers();
15✔
94
        }
95

96
        $this->fixerFactory = $fixerFactory;
15✔
97
    }
98

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

111
    protected function execute(InputInterface $input, OutputInterface $output): int
112
    {
113
        if ($output instanceof ConsoleOutputInterface) {
15✔
114
            $stdErr = $output->getErrorOutput();
×
115
            $stdErr->writeln(Application::getAboutWithRuntime(true));
×
116
        }
117

118
        $resolver = new ConfigurationResolver(
15✔
119
            new Config(),
15✔
120
            ['config' => $input->getOption('config')],
15✔
121
            getcwd(), // @phpstan-ignore argument.type
15✔
122
            new ToolInfo(),
15✔
123
        );
15✔
124

125
        $this->fixerFactory->registerCustomFixers($resolver->getConfig()->getCustomFixers());
15✔
126

127
        /** @var ?string $name */
128
        $name = $input->getArgument('name');
15✔
129
        $expand = $input->getOption('expand');
15✔
130
        $format = $input->getOption('format');
15✔
131

132
        if (null === $name) {
15✔
133
            if (false === $input->isInteractive()) {
1✔
134
                throw new RuntimeException('Not enough arguments (missing: "name") when not running interactively.');
1✔
135
            }
136

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

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

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

172
        try {
173
            if (str_starts_with($name, '@')) {
14✔
174
                $this->describeSet($input, $output, $name, $resolver);
2✔
175

176
                return 0;
1✔
177
            }
178

179
            $this->describeRule($output, $name);
12✔
180
        } catch (DescribeNameNotFoundException $e) {
3✔
181
            $matcher = new WordMatcher(
3✔
182
                'set' === $e->getType() ? $this->getSetNames() : array_keys($this->getFixers()),
3✔
183
            );
3✔
184

185
            $alternative = $matcher->match($name);
3✔
186

187
            $this->describeList($output, $e->getType());
3✔
188

189
            throw new \InvalidArgumentException(\sprintf(
3✔
190
                '%s "%s" not found.%s',
3✔
191
                ucfirst($e->getType()),
3✔
192
                $name,
3✔
193
                null === $alternative ? '' : ' Did you mean "'.$alternative.'"?',
3✔
194
            ));
3✔
195
        }
196

197
        return 0;
10✔
198
    }
199

200
    private function describeRule(OutputInterface $output, string $name): void
201
    {
202
        $fixers = $this->getFixers();
12✔
203

204
        if (!isset($fixers[$name])) {
12✔
205
            throw new DescribeNameNotFoundException($name, 'rule');
2✔
206
        }
207

208
        $fixer = $fixers[$name];
10✔
209

210
        $definition = $fixer->getDefinition();
10✔
211

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

215
        if ($output->getVerbosity() >= OutputInterface::VERBOSITY_VERBOSE) {
10✔
216
            $output->writeln(\sprintf('Fixer class: <comment>%s</comment>.', \get_class($fixer)));
1✔
217
            $output->writeln('');
1✔
218
        }
219

220
        $output->writeln($definition->getSummary());
10✔
221

222
        $description = $definition->getDescription();
10✔
223

224
        if (null !== $description) {
10✔
225
            $output->writeln($description);
7✔
226
        }
227

228
        $output->writeln('');
10✔
229

230
        $tags = DocumentationTagGenerator::analyseRule($fixer);
10✔
231

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

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

250
            if (null !== $tagDescription) {
4✔
251
                $tagDescription = Preg::replace('/(`[^`]+`)/', '<info>$1</info>', $tagDescription);
4✔
252
                $output->writeln($tagDescription);
4✔
253
            }
254

255
            $output->writeln('');
4✔
256
        }
257

258
        if ($fixer instanceof ConfigurableFixerInterface) {
10✔
259
            $configurationDefinition = $fixer->getConfigurationDefinition();
4✔
260
            $options = $configurationDefinition->getOptions();
4✔
261

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

264
            foreach ($options as $option) {
4✔
265
                $line = '* <info>'.OutputFormatter::escape($option->getName()).'</info>';
4✔
266
                $allowed = HelpCommand::getDisplayableAllowedValues($option);
4✔
267

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

282
                if (null !== $allowed) {
4✔
283
                    $line .= ' ('.Utils::naturalLanguageJoin($allowed, '').')';
4✔
284
                }
285

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

289
                if ($option->hasDefault()) {
4✔
290
                    $line .= \sprintf(
4✔
291
                        'defaults to <comment>%s</comment>',
4✔
292
                        Utils::toString($option->getDefault()),
4✔
293
                    );
4✔
294
                } else {
295
                    $line .= '<comment>required</comment>';
×
296
                }
297

298
                if ($option instanceof DeprecatedFixerOption) {
4✔
299
                    $line .= '. <error>DEPRECATED</error>: '.Preg::replace(
3✔
300
                        '/(`.+?`)/',
3✔
301
                        '<info>$1</info>',
3✔
302
                        OutputFormatter::escape(lcfirst($option->getDeprecationMessage())),
3✔
303
                    );
3✔
304
                }
305

306
                if ($option instanceof AliasedFixerOption) {
4✔
307
                    $line .= '; <error>DEPRECATED</error> alias: <comment>'.$option->getAlias().'</comment>';
3✔
308
                }
309

310
                $output->writeln($line);
4✔
311
            }
312

313
            $output->writeln('');
4✔
314
        }
315

316
        $codeSamples = array_filter($definition->getCodeSamples(), static function (CodeSampleInterface $codeSample): bool {
10✔
317
            if ($codeSample instanceof VersionSpecificCodeSampleInterface) {
8✔
318
                return $codeSample->isSuitableFor(\PHP_VERSION_ID);
2✔
319
            }
320

321
            return true;
7✔
322
        });
10✔
323

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

337
            $differ = new FullDiffer();
7✔
338
            $diffFormatter = new DiffConsoleFormatter(
7✔
339
                $output->isDecorated(),
7✔
340
                \sprintf(
7✔
341
                    '<comment>   ---------- begin diff ----------</comment>%s%%s%s<comment>   ----------- end diff -----------</comment>',
7✔
342
                    \PHP_EOL,
7✔
343
                    \PHP_EOL,
7✔
344
                ),
7✔
345
            );
7✔
346

347
            foreach ($codeSamples as $index => $codeSample) {
7✔
348
                $old = $codeSample->getCode();
7✔
349
                $tokens = Tokens::fromCode($old);
7✔
350

351
                $configuration = $codeSample->getConfiguration();
7✔
352

353
                if ($fixer instanceof ConfigurableFixerInterface) {
7✔
354
                    $fixer->configure($configuration ?? []);
4✔
355
                }
356

357
                $file = $codeSample instanceof FileSpecificCodeSampleInterface
7✔
358
                    ? $codeSample->getSplFileInfo()
×
359
                    : new StdinFileInfo();
7✔
360

361
                $fixer->fix($file, $tokens);
7✔
362

363
                $diff = $differ->diff($old, $tokens->generateCode());
7✔
364

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

375
                $output->writeln([$diffFormatter->format($diff, '   %s'), '']);
7✔
376
            }
377
        }
378

379
        $ruleSetConfigs = FixerDocumentGenerator::getSetsOfRule($name);
10✔
380

381
        if ([] !== $ruleSetConfigs) {
10✔
382
            ksort($ruleSetConfigs);
1✔
383
            $plural = 1 !== \count($ruleSetConfigs) ? 's' : '';
1✔
384
            $output->writeln("The fixer is part of the following rule set{$plural}:");
1✔
385

386
            $ruleSetDefinitions = RuleSets::getSetDefinitions();
1✔
387

388
            foreach ($ruleSetConfigs as $set => $config) {
1✔
389
                \assert(isset($ruleSetDefinitions[$set]));
1✔
390
                $ruleSetDefinition = $ruleSetDefinitions[$set];
1✔
391

392
                if ($ruleSetDefinition instanceof AutomaticRuleSetDefinitionInterface) {
1✔
393
                    continue;
×
394
                }
395

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

404
            $output->writeln('');
1✔
405
        }
406
    }
407

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

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

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

455
        $ruleSetDefinitions = RuleSets::getSetDefinitions();
1✔
456
        $ruleSetDefinition = $aliasedRuleSetDefinition ?? $ruleSetDefinitions[$name];
1✔
457
        $fixers = $this->getFixers();
1✔
458

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

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

468
        $output->writeln($this->replaceRstLinks($ruleSetDefinition->getDescription()));
1✔
469
        $output->writeln('');
1✔
470

471
        $tags = DocumentationTagGenerator::analyseRuleSet($ruleSetDefinition);
1✔
472

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

486
            $output->writeln(\sprintf('<error>%s</error>', $tag->title));
×
487
            $tagDescription = $tag->description;
×
488

489
            if (null !== $tagDescription) {
×
490
                $tagDescription = Preg::replace('/(`[^`]+`)/', '<info>$1</info>', $tagDescription);
×
491
                $output->writeln($tagDescription);
×
492
            }
493

494
            $output->writeln('');
×
495
        }
496

497
        if ('tree' === $input->getOption('format')) {
1✔
498
            $this->describeSetContentAsTree($output, $ruleSetDefinition, $ruleSetDefinitions, $fixers);
×
499
        } else {
500
            $this->describeSetContentAsTxt($output, $ruleSetDefinition, $ruleSetDefinitions, $fixers);
1✔
501
        }
502
    }
503

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

518
        $node = new TreeNode($ruleSetDefinition->getName().$extra);
×
519

520
        $rules = $ruleSetDefinition->getRules();
×
521
        $rulesKeys = array_keys($rules);
×
522
        natcasesort($rulesKeys);
×
523

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

548
        return $node;
×
549
    }
550

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

562
        $root = $this->createTreeNode($ruleSetDefinition, $ruleSetDefinitions, $fixers);
×
563
        $tree = TreeHelper::createTree($io, $root);
×
564
        $tree->render();
×
565
    }
566

567
    /**
568
     * @param array<string, RuleSetDefinitionInterface> $ruleSetDefinitions
569
     * @param array<string, FixerInterface>             $fixers
570
     */
571
    private function describeSetContentAsTxt(OutputInterface $output, RuleSetDefinitionInterface $ruleSetDefinition, array $ruleSetDefinitions, array $fixers): void
572
    {
573
        $help = '';
1✔
574

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

591
                continue;
×
592
            }
593

594
            \assert(isset($fixers[$rule]));
1✔
595
            $fixer = $fixers[$rule];
1✔
596
            $tags = DocumentationTagGenerator::analyseRule($fixer);
1✔
597

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

612
        $output->write($help);
1✔
613
    }
614

615
    /**
616
     * @return array<string, FixerInterface>
617
     */
618
    private function getFixers(): array
619
    {
620
        if (null !== $this->fixers) {
13✔
621
            return $this->fixers;
2✔
622
        }
623

624
        $fixers = [];
13✔
625

626
        foreach ($this->fixerFactory->getFixers() as $fixer) {
13✔
627
            $fixers[$fixer->getName()] = $fixer;
12✔
628
        }
629

630
        $this->fixers = $fixers;
13✔
631
        ksort($this->fixers);
13✔
632

633
        return $this->fixers;
13✔
634
    }
635

636
    /**
637
     * @return list<string>
638
     */
639
    private function getSetNames(): array
640
    {
641
        if (null !== $this->setNames) {
2✔
642
            return $this->setNames;
1✔
643
        }
644

645
        $this->setNames = RuleSets::getSetDefinitionNames();
2✔
646

647
        return $this->setNames;
2✔
648
    }
649

650
    /**
651
     * @param string $type 'rule'|'set'
652
     */
653
    private function describeList(OutputInterface $output, string $type): void
654
    {
655
        if ($output->getVerbosity() < OutputInterface::VERBOSITY_VERBOSE) {
3✔
656
            return;
3✔
657
        }
658

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

662
            $items = $this->getSetNames();
×
663
            foreach ($items as $item) {
×
664
                $output->writeln(\sprintf('* <info>%s</info>', $item));
×
665
            }
666
        }
667

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

671
            $items = array_keys($this->getFixers());
×
672
            foreach ($items as $item) {
×
673
                $output->writeln(\sprintf('* <info>%s</info>', $item));
×
674
            }
675
        }
676
    }
677

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

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

700
            /** @var list<'expand'> */
701
            private array $adjustments;
702

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

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

720
            public function getDescription(): string
721
            {
722
                return $this->overrides[__FUNCTION__]
×
723
                    ?? (null !== $this->original ? $this->original->{__FUNCTION__}() : 'unknown description'); // @phpstan-ignore method.dynamicName
×
724
            }
725

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

731
                if (\in_array('expand', $this->adjustments, true)) {
×
732
                    $value .= ' (expanded)';
×
733
                }
734

735
                return $value;
×
736
            }
737

738
            public function getRules(): array
739
            {
740
                $value = $this->overrides[__FUNCTION__]
×
741
                    ?? (null !== $this->original ? $this->original->{__FUNCTION__}() : null); // @phpstan-ignore method.dynamicName
×
742

743
                if (null === $value) {
×
744
                    throw new \LogicException('Cannot get rules from unknown original rule set and missing overrides.');
×
745
                }
746

747
                if (\in_array('expand', $this->adjustments, true)) {
×
748
                    $value = (new RuleSet($value))->getRules();
×
749
                }
750

751
                return $value;
×
752
            }
753

754
            public function isRisky(): bool
755
            {
756
                $value = $this->overrides[__FUNCTION__]
×
757
                    ?? (null !== $this->original ? $this->original->{__FUNCTION__}() : null); // @phpstan-ignore method.dynamicName
×
758

759
                if (null === $value) {
×
760
                    throw new \LogicException('Cannot get isRisky from unknown original rule set and missing overrides.');
×
761
                }
762

763
                return $value;
×
764
            }
765
        };
×
766
    }
767
}
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