DescribeCommand::configure()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 9
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 4
c 1
b 0
f 0
dl 0
loc 9
rs 10
cc 1
nc 1
nop 0
1
<?php
2
3
declare(strict_types=1);
4
5
/*
6
 * This file is part of PHP CS Fixer.
7
 *
8
 * (c) Fabien Potencier <[email protected]>
9
 *     Dariusz Rumiński <[email protected]>
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\Differ\DiffConsoleFormatter;
18
use PhpCsFixer\Differ\FullDiffer;
19
use PhpCsFixer\Fixer\ConfigurableFixerInterface;
20
use PhpCsFixer\Fixer\DeprecatedFixerInterface;
21
use PhpCsFixer\Fixer\FixerInterface;
22
use PhpCsFixer\FixerConfiguration\AliasedFixerOption;
23
use PhpCsFixer\FixerConfiguration\AllowedValueSubset;
24
use PhpCsFixer\FixerConfiguration\DeprecatedFixerOption;
25
use PhpCsFixer\FixerDefinition\CodeSampleInterface;
26
use PhpCsFixer\FixerDefinition\FileSpecificCodeSampleInterface;
27
use PhpCsFixer\FixerDefinition\VersionSpecificCodeSampleInterface;
28
use PhpCsFixer\FixerFactory;
29
use PhpCsFixer\Preg;
30
use PhpCsFixer\RuleSet\RuleSets;
31
use PhpCsFixer\StdinFileInfo;
32
use PhpCsFixer\Tokenizer\Tokens;
33
use PhpCsFixer\Utils;
34
use PhpCsFixer\WordMatcher;
35
use Symfony\Component\Console\Command\Command;
36
use Symfony\Component\Console\Formatter\OutputFormatter;
37
use Symfony\Component\Console\Input\InputArgument;
38
use Symfony\Component\Console\Input\InputInterface;
39
use Symfony\Component\Console\Output\ConsoleOutputInterface;
40
use Symfony\Component\Console\Output\OutputInterface;
41
42
/**
43
 * @author Dariusz Rumiński <[email protected]>
44
 * @author SpacePossum
45
 *
46
 * @internal
47
 */
48
final class DescribeCommand extends Command
49
{
50
    /**
51
     * @var string
52
     */
53
    protected static $defaultName = 'describe';
54
55
    /**
56
     * @var string[]
57
     */
58
    private $setNames;
59
60
    /**
61
     * @var FixerFactory
62
     */
63
    private $fixerFactory;
64
65
    /**
66
     * @var array<string, FixerInterface>
67
     */
68
    private $fixers;
69
70
    public function __construct(?FixerFactory $fixerFactory = null)
71
    {
72
        parent::__construct();
73
74
        if (null === $fixerFactory) {
75
            $fixerFactory = new FixerFactory();
76
            $fixerFactory->registerBuiltInFixers();
77
        }
78
79
        $this->fixerFactory = $fixerFactory;
80
    }
81
82
    /**
83
     * {@inheritdoc}
84
     */
85
    protected function configure(): void
86
    {
87
        $this
88
            ->setDefinition(
89
                [
90
                    new InputArgument('name', InputArgument::REQUIRED, 'Name of rule / set.'),
91
                ]
92
            )
93
            ->setDescription('Describe rule / ruleset.')
94
        ;
95
    }
96
97
    /**
98
     * {@inheritdoc}
99
     */
100
    protected function execute(InputInterface $input, OutputInterface $output): int
101
    {
102
        if (OutputInterface::VERBOSITY_VERBOSE <= $output->getVerbosity() && $output instanceof ConsoleOutputInterface) {
103
            $stdErr = $output->getErrorOutput();
104
            $stdErr->writeln($this->getApplication()->getLongVersion());
105
            $stdErr->writeln(sprintf('Runtime: <info>PHP %s</info>', PHP_VERSION));
106
        }
107
108
        $name = $input->getArgument('name');
109
110
        try {
111
            if ('@' === $name[0]) {
112
                $this->describeSet($output, $name);
113
114
                return 0;
115
            }
116
117
            $this->describeRule($output, $name);
118
        } catch (DescribeNameNotFoundException $e) {
119
            $matcher = new WordMatcher(
120
                'set' === $e->getType() ? $this->getSetNames() : array_keys($this->getFixers())
121
            );
122
123
            $alternative = $matcher->match($name);
124
125
            $this->describeList($output, $e->getType());
126
127
            throw new \InvalidArgumentException(sprintf(
128
                '%s "%s" not found.%s',
129
                ucfirst($e->getType()),
130
                $name,
131
                null === $alternative ? '' : ' Did you mean "'.$alternative.'"?'
132
            ));
133
        }
134
135
        return 0;
136
    }
137
138
    private function describeRule(OutputInterface $output, string $name): void
139
    {
140
        $fixers = $this->getFixers();
141
142
        if (!isset($fixers[$name])) {
143
            throw new DescribeNameNotFoundException($name, 'rule');
144
        }
145
146
        /** @var FixerInterface $fixer */
147
        $fixer = $fixers[$name];
148
149
        $definition = $fixer->getDefinition();
150
151
        $description = $definition->getSummary();
152
153
        if ($fixer instanceof DeprecatedFixerInterface) {
154
            $successors = $fixer->getSuccessorsNames();
155
            $message = [] === $successors
156
                ? 'will be removed on next major version'
157
                : sprintf('use %s instead', Utils::naturalLanguageJoinWithBackticks($successors));
158
            $message = Preg::replace('/(`.+?`)/', '<info>$1</info>', $message);
159
            $description .= sprintf(' <error>DEPRECATED</error>: %s.', $message);
160
        }
161
162
        $output->writeln(sprintf('<info>Description of</info> %s <info>rule</info>.', $name));
163
164
        if ($output->getVerbosity() >= OutputInterface::VERBOSITY_VERBOSE) {
165
            $output->writeln(sprintf('Fixer class: <comment>%s</comment>.', \get_class($fixer)));
166
        }
167
168
        $output->writeln($description);
169
170
        if ($definition->getDescription()) {
171
            $output->writeln($definition->getDescription());
172
        }
173
174
        $output->writeln('');
175
176
        if ($fixer->isRisky()) {
177
            $output->writeln('<error>Fixer applying this rule is risky.</error>');
178
179
            if ($definition->getRiskyDescription()) {
180
                $output->writeln($definition->getRiskyDescription());
181
            }
182
183
            $output->writeln('');
184
        }
185
186
        if ($fixer instanceof ConfigurableFixerInterface) {
187
            $configurationDefinition = $fixer->getConfigurationDefinition();
188
            $options = $configurationDefinition->getOptions();
189
190
            $output->writeln(sprintf('Fixer is configurable using following option%s:', 1 === \count($options) ? '' : 's'));
191
192
            foreach ($options as $option) {
193
                $line = '* <info>'.OutputFormatter::escape($option->getName()).'</info>';
194
                $allowed = HelpCommand::getDisplayableAllowedValues($option);
195
196
                if (null !== $allowed) {
197
                    foreach ($allowed as &$value) {
198
                        if ($value instanceof AllowedValueSubset) {
199
                            $value = 'a subset of <comment>'.HelpCommand::toString($value->getAllowedValues()).'</comment>';
200
                        } else {
201
                            $value = '<comment>'.HelpCommand::toString($value).'</comment>';
202
                        }
203
                    }
204
                } else {
205
                    $allowed = array_map(
206
                        static function (string $type) {
207
                            return '<comment>'.$type.'</comment>';
208
                        },
209
                        $option->getAllowedTypes()
0 ignored issues
show
Bug introduced by
It seems like $option->getAllowedTypes() can also be of type null; however, parameter $array of array_map() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

209
                        /** @scrutinizer ignore-type */ $option->getAllowedTypes()
Loading history...
210
                    );
211
                }
212
213
                if (null !== $allowed) {
214
                    $line .= ' ('.implode(', ', $allowed).')';
215
                }
216
217
                $description = Preg::replace('/(`.+?`)/', '<info>$1</info>', OutputFormatter::escape($option->getDescription()));
218
                $line .= ': '.lcfirst(Preg::replace('/\.$/', '', $description)).'; ';
219
220
                if ($option->hasDefault()) {
221
                    $line .= sprintf(
222
                        'defaults to <comment>%s</comment>',
223
                        HelpCommand::toString($option->getDefault())
224
                    );
225
                } else {
226
                    $line .= '<comment>required</comment>';
227
                }
228
229
                if ($option instanceof DeprecatedFixerOption) {
230
                    $line .= '. <error>DEPRECATED</error>: '.Preg::replace(
231
                        '/(`.+?`)/',
232
                        '<info>$1</info>',
233
                        OutputFormatter::escape(lcfirst($option->getDeprecationMessage()))
234
                    );
235
                }
236
237
                if ($option instanceof AliasedFixerOption) {
238
                    $line .= '; <error>DEPRECATED</error> alias: <comment>'.$option->getAlias().'</comment>';
239
                }
240
241
                $output->writeln($line);
242
            }
243
244
            $output->writeln('');
245
        }
246
247
        /** @var CodeSampleInterface[] $codeSamples */
248
        $codeSamples = array_filter($definition->getCodeSamples(), static function (CodeSampleInterface $codeSample) {
249
            if ($codeSample instanceof VersionSpecificCodeSampleInterface) {
250
                return $codeSample->isSuitableFor(\PHP_VERSION_ID);
251
            }
252
253
            return true;
254
        });
255
256
        if (!\count($codeSamples)) {
257
            $output->writeln([
258
                'Fixing examples can not be demonstrated on the current PHP version.',
259
                '',
260
            ]);
261
        } else {
262
            $output->writeln('Fixing examples:');
263
264
            $differ = new FullDiffer();
265
            $diffFormatter = new DiffConsoleFormatter(
266
                $output->isDecorated(),
267
                sprintf(
268
                    '<comment>   ---------- begin diff ----------</comment>%s%%s%s<comment>   ----------- end diff -----------</comment>',
269
                    PHP_EOL,
270
                    PHP_EOL
271
                )
272
            );
273
274
            foreach ($codeSamples as $index => $codeSample) {
275
                $old = $codeSample->getCode();
276
                $tokens = Tokens::fromCode($old);
277
278
                $configuration = $codeSample->getConfiguration();
279
280
                if ($fixer instanceof ConfigurableFixerInterface) {
281
                    $fixer->configure(null === $configuration ? [] : $configuration);
282
                }
283
284
                $file = $codeSample instanceof FileSpecificCodeSampleInterface
285
                    ? $codeSample->getSplFileInfo()
286
                    : new StdinFileInfo();
287
288
                $fixer->fix($file, $tokens);
289
290
                $diff = $differ->diff($old, $tokens->generateCode());
291
292
                if ($fixer instanceof ConfigurableFixerInterface) {
293
                    if (null === $configuration) {
294
                        $output->writeln(sprintf(' * Example #%d. Fixing with the <comment>default</comment> configuration.', $index + 1));
295
                    } else {
296
                        $output->writeln(sprintf(' * Example #%d. Fixing with configuration: <comment>%s</comment>.', $index + 1, HelpCommand::toString($codeSample->getConfiguration())));
297
                    }
298
                } else {
299
                    $output->writeln(sprintf(' * Example #%d.', $index + 1));
300
                }
301
302
                $output->writeln([$diffFormatter->format($diff, '   %s'), '']);
303
            }
304
        }
305
    }
306
307
    private function describeSet(OutputInterface $output, string $name): void
308
    {
309
        if (!\in_array($name, $this->getSetNames(), true)) {
310
            throw new DescribeNameNotFoundException($name, 'set');
311
        }
312
313
        $ruleSetDefinitions = RuleSets::getSetDefinitions();
314
        $fixers = $this->getFixers();
315
316
        $output->writeln(sprintf('<info>Description of the</info> %s <info>set.</info>', $ruleSetDefinitions[$name]->getName()));
317
        $output->writeln($this->replaceRstLinks($ruleSetDefinitions[$name]->getDescription()));
318
319
        if ($ruleSetDefinitions[$name]->isRisky()) {
320
            $output->writeln('This set contains <error>risky</error> rules.');
321
        }
322
323
        $output->writeln('');
324
325
        $help = '';
326
327
        foreach ($ruleSetDefinitions[$name]->getRules() as $rule => $config) {
328
            if ('@' === $rule[0]) {
329
                $set = $ruleSetDefinitions[$rule];
330
                $help .= sprintf(
331
                    " * <info>%s</info>%s\n   | %s\n\n",
332
                    $rule,
333
                    $set->isRisky() ? ' <error>risky</error>' : '',
334
                    $this->replaceRstLinks($set->getDescription())
335
                );
336
337
                continue;
338
            }
339
340
            /** @var FixerInterface $fixer */
341
            $fixer = $fixers[$rule];
342
343
            $definition = $fixer->getDefinition();
344
            $help .= sprintf(
345
                " * <info>%s</info>%s\n   | %s\n%s\n",
346
                $rule,
347
                $fixer->isRisky() ? ' <error>risky</error>' : '',
348
                $definition->getSummary(),
349
                true !== $config ? sprintf("   <comment>| Configuration: %s</comment>\n", HelpCommand::toString($config)) : ''
350
            );
351
        }
352
353
        $output->write($help);
354
    }
355
356
    /**
357
     * @return array<string, FixerInterface>
358
     */
359
    private function getFixers(): array
360
    {
361
        if (null !== $this->fixers) {
362
            return $this->fixers;
363
        }
364
365
        $fixers = [];
366
367
        foreach ($this->fixerFactory->getFixers() as $fixer) {
368
            $fixers[$fixer->getName()] = $fixer;
369
        }
370
371
        $this->fixers = $fixers;
372
        ksort($this->fixers);
0 ignored issues
show
Bug introduced by
$this->fixers of type void is incompatible with the type array expected by parameter $array of ksort(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

372
        ksort(/** @scrutinizer ignore-type */ $this->fixers);
Loading history...
373
374
        return $this->fixers;
375
    }
376
377
    /**
378
     * @return string[]
379
     */
380
    private function getSetNames(): array
381
    {
382
        if (null !== $this->setNames) {
383
            return $this->setNames;
384
        }
385
386
        $this->setNames = RuleSets::getSetDefinitionNames();
387
388
        return $this->setNames;
389
    }
390
391
    /**
392
     * @param string $type 'rule'|'set'
393
     */
394
    private function describeList(OutputInterface $output, string $type): void
395
    {
396
        if ($output->getVerbosity() >= OutputInterface::VERBOSITY_VERY_VERBOSE) {
397
            $describe = [
398
                'sets' => $this->getSetNames(),
399
                'rules' => $this->getFixers(),
400
            ];
401
        } elseif ($output->getVerbosity() >= OutputInterface::VERBOSITY_VERBOSE) {
402
            $describe = 'set' === $type ? ['sets' => $this->getSetNames()] : ['rules' => $this->getFixers()];
403
        } else {
404
            return;
405
        }
406
407
        /** @var string[] $items */
408
        foreach ($describe as $list => $items) {
409
            $output->writeln(sprintf('<comment>Defined %s:</comment>', $list));
410
411
            foreach ($items as $name => $item) {
412
                $output->writeln(sprintf('* <info>%s</info>', \is_string($name) ? $name : $item));
413
            }
414
        }
415
    }
416
417
    private function replaceRstLinks(string $content): string
418
    {
419
        return Preg::replaceCallback(
420
            '/(`[^<]+<[^>]+>`_)/',
421
            static function (array $matches) {
422
                return Preg::replaceCallback(
423
                    '/`(.*)<(.*)>`_/',
424
                    static function (array $matches) {
425
                        return $matches[1].'('.$matches[2].')';
426
                    },
427
                    $matches[1]
428
                );
429
            },
430
            $content
431
        );
432
    }
433
}
434