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

keradus / PHP-CS-Fixer / 17642402866

10 Sep 2025 10:21AM UTC coverage: 94.687% (-0.006%) from 94.693%
17642402866

push

github

web-flow
feat: `symfony` ruleset: Add `@const` to `phpdoc_no_alias_tag` (#9016)

10 of 10 new or added lines in 2 files covered. (100.0%)

14 existing lines in 2 files now uncovered.

28406 of 30000 relevant lines covered (94.69%)

45.48 hits per line

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

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

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

49
    private FullDiffer $differ;
50

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

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

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

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

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

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

77

78
                Description
79
                -----------
80

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

85
        $deprecationDescription = '';
5✔
86

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

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

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

103
        $experimentalDescription = '';
5✔
104

105
        if ($fixer instanceof ExperimentalFixerInterface) {
5✔
106
            $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✔
107
            $experimentalDescription = <<<RST
1✔
108

109
                This rule is experimental
110
                ~~~~~~~~~~~~~~~~~~~~~~~~~
111

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

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

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

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

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

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

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

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

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

153

154
                Configuration
155
                -------------
156
                RST;
157

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

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

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

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

171
                if ($option instanceof AliasedFixerOption) {
3✔
UNCOV
172
                    $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.";
×
173
                }
174

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

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

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

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

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

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

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

214

215
                Examples
216
                --------
217
                RST;
218

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

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

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

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

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

245

246
                Rule sets
247
                ---------
248

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

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

256
                \assert(isset($this->ruleSetDefinitions[$set]));
2✔
257
                $ruleSetDescription = $this->ruleSetDefinitions[$set];
2✔
258
                $deprecatedDesc = ($ruleSetDescription instanceof DeprecatedRuleSetDescriptionInterface) ? ' *(deprecated)*' : '';
2✔
259

260
                $configInfo = (null !== $config)
2✔
261
                    ? " with config:\n\n  ``".Utils::toString($config)."``\n"
1✔
262
                    : '';
2✔
263

264
                $doc .= <<<RST
2✔
265
                    - `{$set} <./../../ruleSets{$ruleSetPath}>`_{$deprecatedDesc}{$configInfo}\n
2✔
266
                    RST;
2✔
267
            }
268

269
            $doc = trim($doc);
2✔
270
        }
271

272
        $reflectionObject = new \ReflectionObject($fixer);
5✔
273
        $className = str_replace('\\', '\\\\', $reflectionObject->getName());
5✔
274
        $fileName = $reflectionObject->getFileName();
5✔
275
        $fileName = str_replace('\\', '/', $fileName);
5✔
276
        $fileName = substr($fileName, strrpos($fileName, '/src/Fixer/') + 1);
5✔
277
        $fileName = "`{$className} <./../../../{$fileName}>`_";
5✔
278

279
        $testFileName = Preg::replace('~.*\K/src/(?=Fixer/)~', '/tests/', $fileName);
5✔
280
        $testFileName = Preg::replace('~PhpCsFixer\\\\\\\\\K(?=Fixer\\\\\\\)~', 'Tests\\\\\\\\', $testFileName);
5✔
281
        $testFileName = Preg::replace('~(?= <|\.php>)~', 'Test', $testFileName);
5✔
282

283
        $doc .= <<<RST
5✔
284

285

286
            References
287
            ----------
288

289
            - Fixer class: {$fileName}
5✔
290
            - Test class: {$testFileName}
5✔
291

292
            The test class defines officially supported behaviour. Each test case is a part of our backward compatibility promise.
293
            RST;
5✔
294

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

297
        return "{$doc}\n";
5✔
298
    }
299

300
    /**
301
     * @internal
302
     *
303
     * @return array<string, null|array<string, mixed>>
304
     */
305
    public static function getSetsOfRule(string $ruleName): array
306
    {
307
        $ruleSetConfigs = [];
5✔
308

309
        foreach (RuleSets::getSetDefinitionNames() as $set) {
5✔
310
            $ruleSet = new RuleSet([$set => true]);
5✔
311

312
            if ($ruleSet->hasRule($ruleName)) {
5✔
313
                $ruleSetConfigs[$set] = $ruleSet->getRuleConfiguration($ruleName);
2✔
314
            }
315
        }
316

317
        return $ruleSetConfigs;
5✔
318
    }
319

320
    /**
321
     * @param list<FixerInterface> $fixers
322
     */
323
    public function generateFixersDocumentationIndex(array $fixers): string
324
    {
325
        $overrideGroups = [
1✔
326
            'PhpUnit' => 'PHPUnit',
1✔
327
            'PhpTag' => 'PHP Tag',
1✔
328
            'Phpdoc' => 'PHPDoc',
1✔
329
        ];
1✔
330

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

333
        $documentation = <<<'RST'
1✔
334
            =======================
335
            List of Available Rules
336
            =======================
337
            RST;
1✔
338

339
        $currentGroup = null;
1✔
340

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

345
            if ($group !== $currentGroup) {
1✔
346
                $underline = str_repeat('-', \strlen($group));
1✔
347
                $documentation .= "\n\n{$group}\n{$underline}\n";
1✔
348

349
                $currentGroup = $group;
1✔
350
            }
351

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

354
            $attributes = [];
1✔
355

356
            if ($fixer instanceof DeprecatedFixerInterface) {
1✔
357
                $attributes[] = 'deprecated';
1✔
358
            }
359

360
            if ($fixer instanceof ExperimentalFixerInterface) {
1✔
361
                $attributes[] = 'experimental';
1✔
362
            }
363

364
            if ($fixer->isRisky()) {
1✔
365
                $attributes[] = 'risky';
1✔
366
            }
367

368
            $attributes = 0 === \count($attributes)
1✔
369
                ? ''
1✔
370
                : ' *('.implode(', ', $attributes).')*';
1✔
371

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

374
            $documentation .= <<<RST
1✔
375

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

378
                  {$summary}
1✔
379
                RST;
1✔
380
        }
381

382
        return "{$documentation}\n";
1✔
383
    }
384

385
    private function generateSampleDiff(FixerInterface $fixer, CodeSampleInterface $sample, int $sampleNumber, string $ruleName): string
386
    {
387
        if ($sample instanceof VersionSpecificCodeSampleInterface && !$sample->isSuitableFor(\PHP_VERSION_ID)) {
5✔
388
            $existingFile = @file_get_contents($this->locator->getFixerDocumentationFilePath($fixer));
×
389

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

393
                if (isset($matches['diff'])) {
×
UNCOV
394
                    return $matches['diff'];
×
395
                }
396
            }
397

UNCOV
398
            $error = <<<RST
×
399

400
                .. error::
UNCOV
401
                   Cannot generate diff for code sample #{$sampleNumber} of rule {$ruleName}:
×
402
                   the sample is not suitable for current version of PHP (%s).
UNCOV
403
                RST;
×
404

UNCOV
405
            return \sprintf($error, \PHP_VERSION);
×
406
        }
407

408
        $old = $sample->getCode();
5✔
409

410
        $tokens = Tokens::fromCode($old);
5✔
411
        $file = $sample instanceof FileSpecificCodeSampleInterface
5✔
UNCOV
412
            ? $sample->getSplFileInfo()
×
413
            : new StdinFileInfo();
5✔
414

415
        if ($fixer instanceof ConfigurableFixerInterface) {
5✔
416
            $fixer->configure($sample->getConfiguration() ?? []);
3✔
417
        }
418

419
        $fixer->fix($file, $tokens);
5✔
420

421
        $diff = $this->differ->diff($old, $tokens->generateCode());
5✔
422
        $diff = Preg::replace('/@@[ \+\-\d,]+@@\n/', '', $diff);
5✔
423
        $diff = Preg::replace('/\r/', '^M', $diff);
5✔
424
        $diff = Preg::replace('/^ $/m', '', $diff);
5✔
425
        $diff = Preg::replace('/\n$/', '', $diff);
5✔
426
        $diff = RstUtils::indent($diff, 3);
5✔
427

428
        return <<<RST
5✔
429

430
            .. code-block:: diff
431

432
               {$diff}
5✔
433
            RST;
5✔
434
    }
435
}
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