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

keradus / PHP-CS-Fixer / 17319949156

29 Aug 2025 09:20AM UTC coverage: 94.696% (-0.05%) from 94.744%
17319949156

push

github

keradus
CS

28333 of 29920 relevant lines covered (94.7%)

45.63 hits per line

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

92.82
/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\RuleSet;
31
use PhpCsFixer\RuleSet\RuleSets;
32
use PhpCsFixer\StdinFileInfo;
33
use PhpCsFixer\Tokenizer\Tokens;
34
use PhpCsFixer\Utils;
35

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

47
    private FullDiffer $differ;
48

49
    public function __construct(DocumentationLocator $locator)
50
    {
51
        $this->locator = $locator;
6✔
52
        $this->differ = new FullDiffer();
6✔
53
    }
54

55
    public function generateFixerDocumentation(FixerInterface $fixer): string
56
    {
57
        $name = $fixer->getName();
5✔
58
        $title = "Rule ``{$name}``";
5✔
59
        $titleLine = str_repeat('=', \strlen($title));
5✔
60
        $doc = "{$titleLine}\n{$title}\n{$titleLine}";
5✔
61

62
        $definition = $fixer->getDefinition();
5✔
63
        $doc .= "\n\n".RstUtils::toRst($definition->getSummary());
5✔
64

65
        $description = $definition->getDescription();
5✔
66

67
        if (null !== $description) {
5✔
68
            $description = RstUtils::toRst($description);
2✔
69
            $doc .= <<<RST
2✔
70

71

72
                Description
73
                -----------
74

75
                {$description}
2✔
76
                RST;
2✔
77
        }
78

79
        $deprecationDescription = '';
5✔
80

81
        if ($fixer instanceof DeprecatedFixerInterface) {
5✔
82
            $deprecationDescription = <<<'RST'
1✔
83

84
                This rule is deprecated and will be removed in the next major version
85
                ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
86
                RST;
1✔
87
            $alternatives = $fixer->getSuccessorsNames();
1✔
88

89
            if (0 !== \count($alternatives)) {
1✔
90
                $deprecationDescription .= RstUtils::toRst(\sprintf(
1✔
91
                    "\n\nYou should use %s instead.",
1✔
92
                    Utils::naturalLanguageJoinWithBackticks($alternatives)
1✔
93
                ), 0);
1✔
94
            }
95
        }
96

97
        $experimentalDescription = '';
5✔
98

99
        if ($fixer instanceof ExperimentalFixerInterface) {
5✔
100
            $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✔
101
            $experimentalDescription = <<<RST
1✔
102

103
                This rule is experimental
104
                ~~~~~~~~~~~~~~~~~~~~~~~~~
105

106
                {$experimentalDescriptionRaw}
1✔
107
                RST;
1✔
108
        }
109

110
        $riskyDescription = '';
5✔
111
        $riskyDescriptionRaw = $definition->getRiskyDescription();
5✔
112

113
        if (null !== $riskyDescriptionRaw) {
5✔
114
            $riskyDescriptionRaw = RstUtils::toRst($riskyDescriptionRaw, 0);
2✔
115
            $riskyDescription = <<<RST
2✔
116

117
                Using this rule is risky
118
                ~~~~~~~~~~~~~~~~~~~~~~~~
119

120
                {$riskyDescriptionRaw}
2✔
121
                RST;
2✔
122
        }
123

124
        if ('' !== $deprecationDescription || '' !== $riskyDescription) {
5✔
125
            $warningsHeader = 'Warning';
3✔
126

127
            if ('' !== $deprecationDescription && '' !== $riskyDescription) {
3✔
128
                $warningsHeader = 'Warnings';
×
129
            }
130

131
            $warningsHeaderLine = str_repeat('-', \strlen($warningsHeader));
3✔
132
            $doc .= "\n\n".implode("\n", array_filter(
3✔
133
                [
3✔
134
                    $warningsHeader,
3✔
135
                    $warningsHeaderLine,
3✔
136
                    $deprecationDescription,
3✔
137
                    $experimentalDescription,
3✔
138
                    $riskyDescription,
3✔
139
                ],
3✔
140
                static fn (string $text): bool => '' !== $text
3✔
141
            ));
3✔
142
        }
143

144
        if ($fixer instanceof ConfigurableFixerInterface) {
5✔
145
            $doc .= <<<'RST'
146

147

148
                Configuration
149
                -------------
150
                RST;
151

152
            $configurationDefinition = $fixer->getConfigurationDefinition();
3✔
153

154
            foreach ($configurationDefinition->getOptions() as $option) {
3✔
155
                $optionInfo = "``{$option->getName()}``";
3✔
156
                $optionInfo .= "\n".str_repeat('~', \strlen($optionInfo));
3✔
157

158
                if ($option instanceof DeprecatedFixerOptionInterface) {
3✔
159
                    $deprecationMessage = RstUtils::toRst($option->getDeprecationMessage());
×
160
                    $optionInfo .= "\n\n.. warning:: This option is deprecated and will be removed in the next major version. {$deprecationMessage}";
×
161
                }
162

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

165
                if ($option instanceof AliasedFixerOption) {
3✔
166
                    $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.";
×
167
                }
168

169
                $allowed = HelpCommand::getDisplayableAllowedValues($option);
3✔
170

171
                if (null === $allowed) {
3✔
172
                    $allowedKind = 'Allowed types';
2✔
173
                    $allowedTypes = $option->getAllowedTypes();
2✔
174
                    if (null !== $allowedTypes) {
2✔
175
                        $allowed = array_map(
2✔
176
                            static fn (string $value): string => '``'.Utils::convertArrayTypeToList($value).'``',
2✔
177
                            $allowedTypes,
2✔
178
                        );
2✔
179
                    }
180
                } else {
181
                    $allowedKind = 'Allowed values';
3✔
182
                    $allowed = array_map(static fn ($value): string => $value instanceof AllowedValueSubset
3✔
183
                        ? 'a subset of ``'.Utils::toString($value->getAllowedValues()).'``'
1✔
184
                        : '``'.Utils::toString($value).'``', $allowed);
3✔
185
                }
186

187
                if (null !== $allowed) {
3✔
188
                    $allowed = Utils::naturalLanguageJoin($allowed, '');
3✔
189
                    $optionInfo .= "\n\n{$allowedKind}: {$allowed}";
3✔
190
                }
191

192
                if ($option->hasDefault()) {
3✔
193
                    $default = Utils::toString($option->getDefault());
3✔
194
                    $optionInfo .= "\n\nDefault value: ``{$default}``";
3✔
195
                } else {
196
                    $optionInfo .= "\n\nThis option is required.";
1✔
197
                }
198

199
                $doc .= "\n\n{$optionInfo}";
3✔
200
            }
201
        }
202

203
        $samples = $definition->getCodeSamples();
5✔
204

205
        if (0 !== \count($samples)) {
5✔
206
            $doc .= <<<'RST'
207

208

209
                Examples
210
                --------
211
                RST;
212

213
            foreach ($samples as $index => $sample) {
5✔
214
                $title = \sprintf('Example #%d', $index + 1);
5✔
215
                $titleLine = str_repeat('~', \strlen($title));
5✔
216
                $doc .= "\n\n{$title}\n{$titleLine}";
5✔
217

218
                if ($fixer instanceof ConfigurableFixerInterface) {
5✔
219
                    if (null === $sample->getConfiguration()) {
3✔
220
                        $doc .= "\n\n*Default* configuration.";
2✔
221
                    } else {
222
                        $doc .= \sprintf(
3✔
223
                            "\n\nWith configuration: ``%s``.",
3✔
224
                            Utils::toString($sample->getConfiguration())
3✔
225
                        );
3✔
226
                    }
227
                }
228

229
                $doc .= "\n".$this->generateSampleDiff($fixer, $sample, $index + 1, $name);
5✔
230
            }
231
        }
232

233
        $ruleSetConfigs = self::getSetsOfRule($name);
5✔
234

235
        if ([] !== $ruleSetConfigs) {
5✔
236
            $plural = 1 !== \count($ruleSetConfigs) ? 's' : '';
2✔
237
            $doc .= <<<RST
2✔
238

239

240
                Rule sets
241
                ---------
242

243
                The rule is part of the following rule set{$plural}:\n\n
2✔
244
                RST;
2✔
245

246
            foreach ($ruleSetConfigs as $set => $config) {
2✔
247
                $ruleSetPath = $this->locator->getRuleSetsDocumentationFilePath($set);
2✔
248
                $ruleSetPath = substr($ruleSetPath, strrpos($ruleSetPath, '/'));
2✔
249

250
                $configInfo = (null !== $config)
2✔
251
                    ? " with config:\n\n  ``".Utils::toString($config)."``\n"
1✔
252
                    : '';
2✔
253

254
                $doc .= <<<RST
2✔
255
                    - `{$set} <./../../ruleSets{$ruleSetPath}>`_{$configInfo}\n
2✔
256
                    RST;
2✔
257
            }
258

259
            $doc = trim($doc);
2✔
260
        }
261

262
        $reflectionObject = new \ReflectionObject($fixer);
5✔
263
        $className = str_replace('\\', '\\\\', $reflectionObject->getName());
5✔
264
        $fileName = $reflectionObject->getFileName();
5✔
265
        $fileName = str_replace('\\', '/', $fileName);
5✔
266
        $fileName = substr($fileName, strrpos($fileName, '/src/Fixer/') + 1);
5✔
267
        $fileName = "`{$className} <./../../../{$fileName}>`_";
5✔
268

269
        $testFileName = Preg::replace('~.*\K/src/(?=Fixer/)~', '/tests/', $fileName);
5✔
270
        $testFileName = Preg::replace('~PhpCsFixer\\\\\\\\\K(?=Fixer\\\\\\\)~', 'Tests\\\\\\\\', $testFileName);
5✔
271
        $testFileName = Preg::replace('~(?= <|\.php>)~', 'Test', $testFileName);
5✔
272

273
        $doc .= <<<RST
5✔
274

275

276
            References
277
            ----------
278

279
            - Fixer class: {$fileName}
5✔
280
            - Test class: {$testFileName}
5✔
281

282
            The test class defines officially supported behaviour. Each test case is a part of our backward compatibility promise.
283
            RST;
5✔
284

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

287
        return "{$doc}\n";
5✔
288
    }
289

290
    /**
291
     * @internal
292
     *
293
     * @return array<string, null|array<string, mixed>>
294
     */
295
    public static function getSetsOfRule(string $ruleName): array
296
    {
297
        $ruleSetConfigs = [];
5✔
298

299
        foreach (RuleSets::getSetDefinitionNames() as $set) {
5✔
300
            $ruleSet = new RuleSet([$set => true]);
5✔
301

302
            if ($ruleSet->hasRule($ruleName)) {
5✔
303
                $ruleSetConfigs[$set] = $ruleSet->getRuleConfiguration($ruleName);
2✔
304
            }
305
        }
306

307
        return $ruleSetConfigs;
5✔
308
    }
309

310
    /**
311
     * @param list<FixerInterface> $fixers
312
     */
313
    public function generateFixersDocumentationIndex(array $fixers): string
314
    {
315
        $overrideGroups = [
1✔
316
            'PhpUnit' => 'PHPUnit',
1✔
317
            'PhpTag' => 'PHP Tag',
1✔
318
            'Phpdoc' => 'PHPDoc',
1✔
319
        ];
1✔
320

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

323
        $documentation = <<<'RST'
1✔
324
            =======================
325
            List of Available Rules
326
            =======================
327
            RST;
1✔
328

329
        $currentGroup = null;
1✔
330

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

335
            if ($group !== $currentGroup) {
1✔
336
                $underline = str_repeat('-', \strlen($group));
1✔
337
                $documentation .= "\n\n{$group}\n{$underline}\n";
1✔
338

339
                $currentGroup = $group;
1✔
340
            }
341

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

344
            $attributes = [];
1✔
345

346
            if ($fixer instanceof DeprecatedFixerInterface) {
1✔
347
                $attributes[] = 'deprecated';
1✔
348
            }
349

350
            if ($fixer instanceof ExperimentalFixerInterface) {
1✔
351
                $attributes[] = 'experimental';
1✔
352
            }
353

354
            if ($fixer->isRisky()) {
1✔
355
                $attributes[] = 'risky';
1✔
356
            }
357

358
            $attributes = 0 === \count($attributes)
1✔
359
                ? ''
1✔
360
                : ' *('.implode(', ', $attributes).')*';
1✔
361

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

364
            $documentation .= <<<RST
1✔
365

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

368
                  {$summary}
1✔
369
                RST;
1✔
370
        }
371

372
        return "{$documentation}\n";
1✔
373
    }
374

375
    private function generateSampleDiff(FixerInterface $fixer, CodeSampleInterface $sample, int $sampleNumber, string $ruleName): string
376
    {
377
        if ($sample instanceof VersionSpecificCodeSampleInterface && !$sample->isSuitableFor(\PHP_VERSION_ID)) {
5✔
378
            $existingFile = @file_get_contents($this->locator->getFixerDocumentationFilePath($fixer));
×
379

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

383
                if (isset($matches['diff'])) {
×
384
                    return $matches['diff'];
×
385
                }
386
            }
387

388
            $error = <<<RST
×
389

390
                .. error::
391
                   Cannot generate diff for code sample #{$sampleNumber} of rule {$ruleName}:
×
392
                   the sample is not suitable for current version of PHP (%s).
393
                RST;
×
394

395
            return \sprintf($error, \PHP_VERSION);
×
396
        }
397

398
        $old = $sample->getCode();
5✔
399

400
        $tokens = Tokens::fromCode($old);
5✔
401
        $file = $sample instanceof FileSpecificCodeSampleInterface
5✔
402
            ? $sample->getSplFileInfo()
×
403
            : new StdinFileInfo();
5✔
404

405
        if ($fixer instanceof ConfigurableFixerInterface) {
5✔
406
            $fixer->configure($sample->getConfiguration() ?? []);
3✔
407
        }
408

409
        $fixer->fix($file, $tokens);
5✔
410

411
        $diff = $this->differ->diff($old, $tokens->generateCode());
5✔
412
        $diff = Preg::replace('/@@[ \+\-\d,]+@@\n/', '', $diff);
5✔
413
        $diff = Preg::replace('/\r/', '^M', $diff);
5✔
414
        $diff = Preg::replace('/^ $/m', '', $diff);
5✔
415
        $diff = Preg::replace('/\n$/', '', $diff);
5✔
416
        $diff = RstUtils::indent($diff, 3);
5✔
417

418
        return <<<RST
5✔
419

420
            .. code-block:: diff
421

422
               {$diff}
5✔
423
            RST;
5✔
424
    }
425
}
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