• 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

92.54
/src/Documentation/FixerDocumentGenerator.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\Documentation;
16

17
use PhpCsFixer\Console\Command\HelpCommand;
18
use PhpCsFixer\Differ\FullDiffer;
19
use PhpCsFixer\Fixer\ConfigurableFixerInterface;
20
use PhpCsFixer\Fixer\DeprecatedFixerInterface;
21
use PhpCsFixer\Fixer\ExperimentalFixerInterface;
22
use PhpCsFixer\Fixer\FixerInterface;
23
use PhpCsFixer\FixerConfiguration\AliasedFixerOption;
24
use PhpCsFixer\FixerConfiguration\AllowedValueSubset;
25
use PhpCsFixer\FixerConfiguration\DeprecatedFixerOptionInterface;
26
use PhpCsFixer\FixerDefinition\CodeSampleInterface;
27
use PhpCsFixer\FixerDefinition\FileSpecificCodeSampleInterface;
28
use PhpCsFixer\FixerDefinition\VersionSpecificCodeSampleInterface;
29
use PhpCsFixer\Preg;
30
use PhpCsFixer\RuleSet\AutomaticRuleSetDescriptionInterface;
31
use PhpCsFixer\RuleSet\DeprecatedRuleSetDescriptionInterface;
32
use PhpCsFixer\RuleSet\RuleSet;
33
use PhpCsFixer\RuleSet\RuleSetDescriptionInterface;
34
use PhpCsFixer\RuleSet\RuleSets;
35
use PhpCsFixer\StdinFileInfo;
36
use PhpCsFixer\Tokenizer\Tokens;
37
use PhpCsFixer\Utils;
38

39
/**
40
 * @readonly
41
 *
42
 * @internal
43
 *
44
 * @no-named-arguments Parameter names are not covered by the backward compatibility promise.
45
 */
46
final class FixerDocumentGenerator
47
{
48
    private DocumentationLocator $locator;
49

50
    private FullDiffer $differ;
51

52
    /** @var array<string, RuleSetDescriptionInterface> */
53
    private array $ruleSetDefinitions;
54

55
    public function __construct(DocumentationLocator $locator)
56
    {
57
        $this->locator = $locator;
6✔
58
        $this->differ = new FullDiffer();
6✔
59
        $this->ruleSetDefinitions = RuleSets::getSetDefinitions();
6✔
60
    }
61

62
    public function generateFixerDocumentation(FixerInterface $fixer): string
63
    {
64
        $name = $fixer->getName();
5✔
65
        $title = "Rule ``{$name}``";
5✔
66
        $titleLine = str_repeat('=', \strlen($title));
5✔
67
        $doc = "{$titleLine}\n{$title}\n{$titleLine}";
5✔
68

69
        $definition = $fixer->getDefinition();
5✔
70
        $doc .= "\n\n".RstUtils::toRst($definition->getSummary());
5✔
71

72
        $description = $definition->getDescription();
5✔
73

74
        if (null !== $description) {
5✔
75
            $description = RstUtils::toRst($description);
2✔
76
            $doc .= <<<RST
2✔
77

78

79
                Description
80
                -----------
81

82
                {$description}
2✔
83
                RST;
2✔
84
        }
85

86
        $deprecationDescription = '';
5✔
87

88
        if ($fixer instanceof DeprecatedFixerInterface) {
5✔
89
            $deprecationDescription = <<<'RST'
1✔
90

91
                This rule is deprecated and will be removed in the next major version
92
                ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
93
                RST;
1✔
94
            $alternatives = $fixer->getSuccessorsNames();
1✔
95

96
            if (0 !== \count($alternatives)) {
1✔
97
                $deprecationDescription .= RstUtils::toRst(\sprintf(
1✔
98
                    "\n\nYou should use %s instead.",
1✔
99
                    Utils::naturalLanguageJoinWithBackticks($alternatives)
1✔
100
                ), 0);
1✔
101
            }
102
        }
103

104
        $experimentalDescription = '';
5✔
105

106
        if ($fixer instanceof ExperimentalFixerInterface) {
5✔
107
            $experimentalDescriptionRaw = RstUtils::toRst('Rule is not covered with backward compatibility promise, use it at your own risk. Rule\'s behaviour may be changed at any point, including rule\'s name; its options\' names, availability and allowed values; its default configuration. Rule may be even removed without prior notice. Feel free to provide feedback and help with determining final state of the rule.', 0);
1✔
108
            $experimentalDescription = <<<RST
1✔
109

110
                This rule is experimental
111
                ~~~~~~~~~~~~~~~~~~~~~~~~~
112

113
                {$experimentalDescriptionRaw}
1✔
114
                RST;
1✔
115
        }
116

117
        $riskyDescription = '';
5✔
118
        $riskyDescriptionRaw = $definition->getRiskyDescription();
5✔
119

120
        if (null !== $riskyDescriptionRaw) {
5✔
121
            $riskyDescriptionRaw = RstUtils::toRst($riskyDescriptionRaw, 0);
2✔
122
            $riskyDescription = <<<RST
2✔
123

124
                Using this rule is risky
125
                ~~~~~~~~~~~~~~~~~~~~~~~~
126

127
                {$riskyDescriptionRaw}
2✔
128
                RST;
2✔
129
        }
130

131
        if ('' !== $deprecationDescription || '' !== $riskyDescription) {
5✔
132
            $warningsHeader = 'Warning';
3✔
133

134
            if ('' !== $deprecationDescription && '' !== $riskyDescription) {
3✔
135
                $warningsHeader = 'Warnings';
×
136
            }
137

138
            $warningsHeaderLine = str_repeat('-', \strlen($warningsHeader));
3✔
139
            $doc .= "\n\n".implode("\n", array_filter(
3✔
140
                [
3✔
141
                    $warningsHeader,
3✔
142
                    $warningsHeaderLine,
3✔
143
                    $deprecationDescription,
3✔
144
                    $experimentalDescription,
3✔
145
                    $riskyDescription,
3✔
146
                ],
3✔
147
                static fn (string $text): bool => '' !== $text
3✔
148
            ));
3✔
149
        }
150

151
        if ($fixer instanceof ConfigurableFixerInterface) {
5✔
152
            $doc .= <<<'RST'
153

154

155
                Configuration
156
                -------------
157
                RST;
158

159
            $configurationDefinition = $fixer->getConfigurationDefinition();
3✔
160

161
            foreach ($configurationDefinition->getOptions() as $option) {
3✔
162
                $optionInfo = "``{$option->getName()}``";
3✔
163
                $optionInfo .= "\n".str_repeat('~', \strlen($optionInfo));
3✔
164

165
                if ($option instanceof DeprecatedFixerOptionInterface) {
3✔
166
                    $deprecationMessage = RstUtils::toRst($option->getDeprecationMessage());
×
167
                    $optionInfo .= "\n\n.. warning:: This option is deprecated and will be removed in the next major version. {$deprecationMessage}";
×
168
                }
169

170
                $optionInfo .= "\n\n".RstUtils::toRst($option->getDescription());
3✔
171

172
                if ($option instanceof AliasedFixerOption) {
3✔
173
                    $optionInfo .= "\n\n.. note:: The previous name of this option was ``{$option->getAlias()}`` but it is now deprecated and will be removed in the next major version.";
×
174
                }
175

176
                $allowed = HelpCommand::getDisplayableAllowedValues($option);
3✔
177

178
                if (null === $allowed) {
3✔
179
                    $allowedKind = 'Allowed types';
2✔
180
                    $allowedTypes = $option->getAllowedTypes();
2✔
181
                    if (null !== $allowedTypes) {
2✔
182
                        $allowed = array_map(
2✔
183
                            static fn (string $value): string => '``'.Utils::convertArrayTypeToList($value).'``',
2✔
184
                            $allowedTypes,
2✔
185
                        );
2✔
186
                    }
187
                } else {
188
                    $allowedKind = 'Allowed values';
3✔
189
                    $allowed = array_map(static fn ($value): string => $value instanceof AllowedValueSubset
3✔
190
                        ? 'a subset of ``'.Utils::toString($value->getAllowedValues()).'``'
1✔
191
                        : '``'.Utils::toString($value).'``', $allowed);
3✔
192
                }
193

194
                if (null !== $allowed) {
3✔
195
                    $allowed = Utils::naturalLanguageJoin($allowed, '');
3✔
196
                    $optionInfo .= "\n\n{$allowedKind}: {$allowed}";
3✔
197
                }
198

199
                if ($option->hasDefault()) {
3✔
200
                    $default = Utils::toString($option->getDefault());
3✔
201
                    $optionInfo .= "\n\nDefault value: ``{$default}``";
3✔
202
                } else {
203
                    $optionInfo .= "\n\nThis option is required.";
1✔
204
                }
205

206
                $doc .= "\n\n{$optionInfo}";
3✔
207
            }
208
        }
209

210
        $samples = $definition->getCodeSamples();
5✔
211

212
        if (0 !== \count($samples)) {
5✔
213
            $doc .= <<<'RST'
214

215

216
                Examples
217
                --------
218
                RST;
219

220
            foreach ($samples as $index => $sample) {
5✔
221
                $title = \sprintf('Example #%d', $index + 1);
5✔
222
                $titleLine = str_repeat('~', \strlen($title));
5✔
223
                $doc .= "\n\n{$title}\n{$titleLine}";
5✔
224

225
                if ($fixer instanceof ConfigurableFixerInterface) {
5✔
226
                    if (null === $sample->getConfiguration()) {
3✔
227
                        $doc .= "\n\n*Default* configuration.";
2✔
228
                    } else {
229
                        $doc .= \sprintf(
3✔
230
                            "\n\nWith configuration: ``%s``.",
3✔
231
                            Utils::toString($sample->getConfiguration())
3✔
232
                        );
3✔
233
                    }
234
                }
235

236
                $doc .= "\n".$this->generateSampleDiff($fixer, $sample, $index + 1, $name);
5✔
237
            }
238
        }
239

240
        $ruleSetConfigs = self::getSetsOfRule($name);
5✔
241

242
        if ([] !== $ruleSetConfigs) {
5✔
243
            $plural = 1 !== \count($ruleSetConfigs) ? 's' : '';
2✔
244
            $doc .= <<<RST
2✔
245

246

247
                Rule sets
248
                ---------
249

250
                The rule is part of the following rule set{$plural}:\n\n
2✔
251
                RST;
2✔
252

253
            foreach ($ruleSetConfigs as $set => $config) {
2✔
254
                $ruleSetPath = $this->locator->getRuleSetsDocumentationFilePath($set);
2✔
255
                $ruleSetPath = substr($ruleSetPath, strrpos($ruleSetPath, '/'));
2✔
256

257
                \assert(isset($this->ruleSetDefinitions[$set]));
2✔
258
                $ruleSetDescription = $this->ruleSetDefinitions[$set];
2✔
259

260
                if ($ruleSetDescription instanceof AutomaticRuleSetDescriptionInterface) {
2✔
NEW
261
                    continue;
×
262
                }
263

264
                $deprecatedDesc = ($ruleSetDescription instanceof DeprecatedRuleSetDescriptionInterface) ? ' *(deprecated)*' : '';
2✔
265

266
                $configInfo = (null !== $config)
2✔
267
                    ? " with config:\n\n  ``".Utils::toString($config)."``\n"
1✔
268
                    : '';
2✔
269

270
                $doc .= <<<RST
2✔
271
                    - `{$set} <./../../ruleSets{$ruleSetPath}>`_{$deprecatedDesc}{$configInfo}\n
2✔
272
                    RST;
2✔
273
            }
274

275
            $doc = trim($doc);
2✔
276
        }
277

278
        $reflectionObject = new \ReflectionObject($fixer);
5✔
279
        $className = str_replace('\\', '\\\\', $reflectionObject->getName());
5✔
280
        $fileName = $reflectionObject->getFileName();
5✔
281
        $fileName = str_replace('\\', '/', $fileName);
5✔
282
        $fileName = substr($fileName, strrpos($fileName, '/src/Fixer/') + 1);
5✔
283
        $fileName = "`{$className} <./../../../{$fileName}>`_";
5✔
284

285
        $testFileName = Preg::replace('~.*\K/src/(?=Fixer/)~', '/tests/', $fileName);
5✔
286
        $testFileName = Preg::replace('~PhpCsFixer\\\\\\\\\K(?=Fixer\\\\\\\)~', 'Tests\\\\\\\\', $testFileName);
5✔
287
        $testFileName = Preg::replace('~(?= <|\.php>)~', 'Test', $testFileName);
5✔
288

289
        $doc .= <<<RST
5✔
290

291

292
            References
293
            ----------
294

295
            - Fixer class: {$fileName}
5✔
296
            - Test class: {$testFileName}
5✔
297

298
            The test class defines officially supported behaviour. Each test case is a part of our backward compatibility promise.
299
            RST;
5✔
300

301
        $doc = str_replace("\t", '<TAB>', $doc);
5✔
302

303
        return "{$doc}\n";
5✔
304
    }
305

306
    /**
307
     * @internal
308
     *
309
     * @return array<string, null|array<string, mixed>>
310
     */
311
    public static function getSetsOfRule(string $ruleName): array
312
    {
313
        $ruleSetConfigs = [];
5✔
314

315
        foreach (RuleSets::getSetDefinitionNames() as $set) {
5✔
316
            $ruleSet = new RuleSet([$set => true]);
5✔
317

318
            if ($ruleSet->hasRule($ruleName)) {
5✔
319
                $ruleSetConfigs[$set] = $ruleSet->getRuleConfiguration($ruleName);
2✔
320
            }
321
        }
322

323
        return $ruleSetConfigs;
5✔
324
    }
325

326
    /**
327
     * @param list<FixerInterface> $fixers
328
     */
329
    public function generateFixersDocumentationIndex(array $fixers): string
330
    {
331
        $overrideGroups = [
1✔
332
            'PhpUnit' => 'PHPUnit',
1✔
333
            'PhpTag' => 'PHP Tag',
1✔
334
            'Phpdoc' => 'PHPDoc',
1✔
335
        ];
1✔
336

337
        usort($fixers, static fn (FixerInterface $a, FixerInterface $b): int => \get_class($a) <=> \get_class($b));
1✔
338

339
        $documentation = <<<'RST'
1✔
340
            =======================
341
            List of Available Rules
342
            =======================
343
            RST;
1✔
344

345
        $currentGroup = null;
1✔
346

347
        foreach ($fixers as $fixer) {
1✔
348
            $namespace = Preg::replace('/^.*\\\(.+)\\\.+Fixer$/', '$1', \get_class($fixer));
1✔
349
            $group = $overrideGroups[$namespace] ?? Preg::replace('/(?<=[[:lower:]])(?=[[:upper:]])/', ' ', $namespace);
1✔
350

351
            if ($group !== $currentGroup) {
1✔
352
                $underline = str_repeat('-', \strlen($group));
1✔
353
                $documentation .= "\n\n{$group}\n{$underline}\n";
1✔
354

355
                $currentGroup = $group;
1✔
356
            }
357

358
            $path = './'.$this->locator->getFixerDocumentationFileRelativePath($fixer);
1✔
359

360
            $attributes = [];
1✔
361

362
            if ($fixer instanceof DeprecatedFixerInterface) {
1✔
363
                $attributes[] = 'deprecated';
1✔
364
            }
365

366
            if ($fixer instanceof ExperimentalFixerInterface) {
1✔
367
                $attributes[] = 'experimental';
1✔
368
            }
369

370
            if ($fixer->isRisky()) {
1✔
371
                $attributes[] = 'risky';
1✔
372
            }
373

374
            $attributes = 0 === \count($attributes)
1✔
375
                ? ''
1✔
376
                : ' *('.implode(', ', $attributes).')*';
1✔
377

378
            $summary = str_replace('`', '``', $fixer->getDefinition()->getSummary());
1✔
379

380
            $documentation .= <<<RST
1✔
381

382
                - `{$fixer->getName()} <{$path}>`_{$attributes}
1✔
383

384
                  {$summary}
1✔
385
                RST;
1✔
386
        }
387

388
        return "{$documentation}\n";
1✔
389
    }
390

391
    private function generateSampleDiff(FixerInterface $fixer, CodeSampleInterface $sample, int $sampleNumber, string $ruleName): string
392
    {
393
        if ($sample instanceof VersionSpecificCodeSampleInterface && !$sample->isSuitableFor(\PHP_VERSION_ID)) {
5✔
394
            $existingFile = @file_get_contents($this->locator->getFixerDocumentationFilePath($fixer));
×
395

396
            if (false !== $existingFile) {
×
397
                Preg::match("/\\RExample #{$sampleNumber}\\R.+?(?<diff>\\R\\.\\. code-block:: diff\\R\\R.*?)\\R(?:\\R\\S|$)/s", $existingFile, $matches);
×
398

399
                if (isset($matches['diff'])) {
×
400
                    return $matches['diff'];
×
401
                }
402
            }
403

404
            $error = <<<RST
×
405

406
                .. error::
407
                   Cannot generate diff for code sample #{$sampleNumber} of rule {$ruleName}:
×
408
                   the sample is not suitable for current version of PHP (%s).
409
                RST;
×
410

411
            return \sprintf($error, \PHP_VERSION);
×
412
        }
413

414
        $old = $sample->getCode();
5✔
415

416
        $tokens = Tokens::fromCode($old);
5✔
417
        $file = $sample instanceof FileSpecificCodeSampleInterface
5✔
418
            ? $sample->getSplFileInfo()
×
419
            : new StdinFileInfo();
5✔
420

421
        if ($fixer instanceof ConfigurableFixerInterface) {
5✔
422
            $fixer->configure($sample->getConfiguration() ?? []);
3✔
423
        }
424

425
        $fixer->fix($file, $tokens);
5✔
426

427
        $diff = $this->differ->diff($old, $tokens->generateCode());
5✔
428
        $diff = Preg::replace('/@@[ \+\-\d,]+@@\n/', '', $diff);
5✔
429
        $diff = Preg::replace('/\r/', '^M', $diff);
5✔
430
        $diff = Preg::replace('/^ $/m', '', $diff);
5✔
431
        $diff = Preg::replace('/\n$/', '', $diff);
5✔
432
        $diff = RstUtils::indent($diff, 3);
5✔
433

434
        return <<<RST
5✔
435

436
            .. code-block:: diff
437

438
               {$diff}
5✔
439
            RST;
5✔
440
    }
441
}
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