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

keradus / PHP-CS-Fixer / 16253708939

13 Jul 2025 09:37PM UTC coverage: 94.807% (+0.001%) from 94.806%
16253708939

push

github

web-flow
Merge branch 'master' into native_constant_invocation__CS

2259 of 2334 new or added lines in 338 files covered. (96.79%)

7 existing lines in 5 files now uncovered.

28260 of 29808 relevant lines covered (94.81%)

45.39 hits per line

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

92.71
/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
final class FixerDocumentGenerator
42
{
43
    private DocumentationLocator $locator;
44

45
    private FullDiffer $differ;
46

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

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

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

63
        $description = $definition->getDescription();
5✔
64

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

69

70
                Description
71
                -----------
72

73
                {$description}
2✔
74
                RST;
2✔
75
        }
76

77
        $deprecationDescription = '';
5✔
78

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

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

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

95
        $experimentalDescription = '';
5✔
96

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

101
                This rule is experimental
102
                ~~~~~~~~~~~~~~~~~~~~~~~~~
103

104
                {$experimentalDescriptionRaw}
1✔
105
                RST;
1✔
106
        }
107

108
        $riskyDescription = '';
5✔
109
        $riskyDescriptionRaw = $definition->getRiskyDescription();
5✔
110

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

115
                Using this rule is risky
116
                ~~~~~~~~~~~~~~~~~~~~~~~~
117

118
                {$riskyDescriptionRaw}
2✔
119
                RST;
2✔
120
        }
121

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

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

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

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

145

146
                Configuration
147
                -------------
148
                RST;
149

150
            $configurationDefinition = $fixer->getConfigurationDefinition();
3✔
151

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

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

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

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

167
                $allowed = HelpCommand::getDisplayableAllowedValues($option);
3✔
168

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

182
                $allowed = Utils::naturalLanguageJoin($allowed, '');
3✔
183
                $optionInfo .= "\n\n{$allowedKind}: {$allowed}";
3✔
184

185
                if ($option->hasDefault()) {
3✔
186
                    $default = Utils::toString($option->getDefault());
3✔
187
                    $optionInfo .= "\n\nDefault value: ``{$default}``";
3✔
188
                } else {
189
                    $optionInfo .= "\n\nThis option is required.";
1✔
190
                }
191

192
                $doc .= "\n\n{$optionInfo}";
3✔
193
            }
194
        }
195

196
        $samples = $definition->getCodeSamples();
5✔
197

198
        if (0 !== \count($samples)) {
5✔
199
            $doc .= <<<'RST'
200

201

202
                Examples
203
                --------
204
                RST;
205

206
            foreach ($samples as $index => $sample) {
5✔
207
                $title = \sprintf('Example #%d', $index + 1);
5✔
208
                $titleLine = str_repeat('~', \strlen($title));
5✔
209
                $doc .= "\n\n{$title}\n{$titleLine}";
5✔
210

211
                if ($fixer instanceof ConfigurableFixerInterface) {
5✔
212
                    if (null === $sample->getConfiguration()) {
3✔
213
                        $doc .= "\n\n*Default* configuration.";
2✔
214
                    } else {
215
                        $doc .= \sprintf(
3✔
216
                            "\n\nWith configuration: ``%s``.",
3✔
217
                            Utils::toString($sample->getConfiguration())
3✔
218
                        );
3✔
219
                    }
220
                }
221

222
                $doc .= "\n".$this->generateSampleDiff($fixer, $sample, $index + 1, $name);
5✔
223
            }
224
        }
225

226
        $ruleSetConfigs = self::getSetsOfRule($name);
5✔
227

228
        if ([] !== $ruleSetConfigs) {
5✔
229
            $plural = 1 !== \count($ruleSetConfigs) ? 's' : '';
2✔
230
            $doc .= <<<RST
2✔
231

232

233
                Rule sets
234
                ---------
235

236
                The rule is part of the following rule set{$plural}:\n\n
2✔
237
                RST;
2✔
238

239
            foreach ($ruleSetConfigs as $set => $config) {
2✔
240
                $ruleSetPath = $this->locator->getRuleSetsDocumentationFilePath($set);
2✔
241
                $ruleSetPath = substr($ruleSetPath, strrpos($ruleSetPath, '/'));
2✔
242

243
                $configInfo = (null !== $config)
2✔
244
                    ? " with config:\n\n  ``".Utils::toString($config)."``\n"
1✔
245
                    : '';
2✔
246

247
                $doc .= <<<RST
2✔
248
                    - `{$set} <./../../ruleSets{$ruleSetPath}>`_{$configInfo}\n
2✔
249
                    RST;
2✔
250
            }
251

252
            $doc = trim($doc);
2✔
253
        }
254

255
        $reflectionObject = new \ReflectionObject($fixer);
5✔
256
        $className = str_replace('\\', '\\\\', $reflectionObject->getName());
5✔
257
        $fileName = $reflectionObject->getFileName();
5✔
258
        $fileName = str_replace('\\', '/', $fileName);
5✔
259
        $fileName = substr($fileName, strrpos($fileName, '/src/Fixer/') + 1);
5✔
260
        $fileName = "`{$className} <./../../../{$fileName}>`_";
5✔
261

262
        $testFileName = Preg::replace('~.*\K/src/(?=Fixer/)~', '/tests/', $fileName);
5✔
263
        $testFileName = Preg::replace('~PhpCsFixer\\\\\\\\\K(?=Fixer\\\\\\\)~', 'Tests\\\\\\\\', $testFileName);
5✔
264
        $testFileName = Preg::replace('~(?= <|\.php>)~', 'Test', $testFileName);
5✔
265

266
        $doc .= <<<RST
5✔
267

268

269
            References
270
            ----------
271

272
            - Fixer class: {$fileName}
5✔
273
            - Test class: {$testFileName}
5✔
274

275
            The test class defines officially supported behaviour. Each test case is a part of our backward compatibility promise.
276
            RST;
5✔
277

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

280
        return "{$doc}\n";
5✔
281
    }
282

283
    /**
284
     * @internal
285
     *
286
     * @return array<string, null|array<string, mixed>>
287
     */
288
    public static function getSetsOfRule(string $ruleName): array
289
    {
290
        $ruleSetConfigs = [];
5✔
291

292
        foreach (RuleSets::getSetDefinitionNames() as $set) {
5✔
293
            $ruleSet = new RuleSet([$set => true]);
5✔
294

295
            if ($ruleSet->hasRule($ruleName)) {
5✔
296
                $ruleSetConfigs[$set] = $ruleSet->getRuleConfiguration($ruleName);
2✔
297
            }
298
        }
299

300
        return $ruleSetConfigs;
5✔
301
    }
302

303
    /**
304
     * @param list<FixerInterface> $fixers
305
     */
306
    public function generateFixersDocumentationIndex(array $fixers): string
307
    {
308
        $overrideGroups = [
1✔
309
            'PhpUnit' => 'PHPUnit',
1✔
310
            'PhpTag' => 'PHP Tag',
1✔
311
            'Phpdoc' => 'PHPDoc',
1✔
312
        ];
1✔
313

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

316
        $documentation = <<<'RST'
1✔
317
            =======================
318
            List of Available Rules
319
            =======================
320
            RST;
1✔
321

322
        $currentGroup = null;
1✔
323

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

328
            if ($group !== $currentGroup) {
1✔
329
                $underline = str_repeat('-', \strlen($group));
1✔
330
                $documentation .= "\n\n{$group}\n{$underline}\n";
1✔
331

332
                $currentGroup = $group;
1✔
333
            }
334

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

337
            $attributes = [];
1✔
338

339
            if ($fixer instanceof DeprecatedFixerInterface) {
1✔
340
                $attributes[] = 'deprecated';
1✔
341
            }
342

343
            if ($fixer instanceof ExperimentalFixerInterface) {
1✔
344
                $attributes[] = 'experimental';
1✔
345
            }
346

347
            if ($fixer->isRisky()) {
1✔
348
                $attributes[] = 'risky';
1✔
349
            }
350

351
            $attributes = 0 === \count($attributes)
1✔
352
                ? ''
1✔
353
                : ' *('.implode(', ', $attributes).')*';
1✔
354

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

357
            $documentation .= <<<RST
1✔
358

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

361
                  {$summary}
1✔
362
                RST;
1✔
363
        }
364

365
        return "{$documentation}\n";
1✔
366
    }
367

368
    private function generateSampleDiff(FixerInterface $fixer, CodeSampleInterface $sample, int $sampleNumber, string $ruleName): string
369
    {
370
        if ($sample instanceof VersionSpecificCodeSampleInterface && !$sample->isSuitableFor(\PHP_VERSION_ID)) {
5✔
371
            $existingFile = @file_get_contents($this->locator->getFixerDocumentationFilePath($fixer));
×
372

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

376
                if (isset($matches['diff'])) {
×
377
                    return $matches['diff'];
×
378
                }
379
            }
380

381
            $error = <<<RST
×
382

383
                .. error::
384
                   Cannot generate diff for code sample #{$sampleNumber} of rule {$ruleName}:
×
385
                   the sample is not suitable for current version of PHP (%s).
386
                RST;
×
387

NEW
388
            return \sprintf($error, \PHP_VERSION);
×
389
        }
390

391
        $old = $sample->getCode();
5✔
392

393
        $tokens = Tokens::fromCode($old);
5✔
394
        $file = $sample instanceof FileSpecificCodeSampleInterface
5✔
395
            ? $sample->getSplFileInfo()
×
396
            : new StdinFileInfo();
5✔
397

398
        if ($fixer instanceof ConfigurableFixerInterface) {
5✔
399
            $fixer->configure($sample->getConfiguration() ?? []);
3✔
400
        }
401

402
        $fixer->fix($file, $tokens);
5✔
403

404
        $diff = $this->differ->diff($old, $tokens->generateCode());
5✔
405
        $diff = Preg::replace('/@@[ \+\-\d,]+@@\n/', '', $diff);
5✔
406
        $diff = Preg::replace('/\r/', '^M', $diff);
5✔
407
        $diff = Preg::replace('/^ $/m', '', $diff);
5✔
408
        $diff = Preg::replace('/\n$/', '', $diff);
5✔
409
        $diff = RstUtils::indent($diff, 3);
5✔
410

411
        return <<<RST
5✔
412

413
            .. code-block:: diff
414

415
               {$diff}
5✔
416
            RST;
5✔
417
    }
418
}
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