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

keradus / PHP-CS-Fixer / 19958239208

05 Dec 2025 09:13AM UTC coverage: 93.181% (-1.0%) from 94.158%
19958239208

push

github

keradus
chore: .php-cs-fixer.dist.php - remove no longer needed rule, 'expectedDeprecation' annotation does not exist for long time

28928 of 31045 relevant lines covered (93.18%)

44.49 hits per line

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

89.36
/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\FixerInterface;
21
use PhpCsFixer\FixerConfiguration\AliasedFixerOption;
22
use PhpCsFixer\FixerConfiguration\AllowedValueSubset;
23
use PhpCsFixer\FixerConfiguration\DeprecatedFixerOptionInterface;
24
use PhpCsFixer\FixerDefinition\CodeSampleInterface;
25
use PhpCsFixer\FixerDefinition\FileSpecificCodeSampleInterface;
26
use PhpCsFixer\FixerDefinition\VersionSpecificCodeSampleInterface;
27
use PhpCsFixer\Preg;
28
use PhpCsFixer\RuleSet\AutomaticRuleSetDefinitionInterface;
29
use PhpCsFixer\RuleSet\DeprecatedRuleSetDefinitionInterface;
30
use PhpCsFixer\RuleSet\RuleSet;
31
use PhpCsFixer\RuleSet\RuleSetDefinitionInterface;
32
use PhpCsFixer\RuleSet\RuleSets;
33
use PhpCsFixer\StdinFileInfo;
34
use PhpCsFixer\Tokenizer\Tokens;
35
use PhpCsFixer\Utils;
36

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

48
    private FullDiffer $differ;
49

50
    /** @var array<string, RuleSetDefinitionInterface> */
51
    private array $ruleSetDefinitions;
52

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

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

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

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

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

76

77
                Description
78
                -----------
79

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

84
        $header = static function (string $message, string $underline = '-'): string {
5✔
85
            $line = str_repeat($underline, \strlen($message));
5✔
86

87
            return "{$message}\n{$line}\n";
5✔
88
        };
5✔
89

90
        $tags = DocumentationTagGenerator::analyseRule($fixer);
5✔
91
        $warnings = array_map(
5✔
92
            static function (DocumentationTag $tag): string {
5✔
93
                $titleLine = str_repeat('~', \strlen($tag->title));
5✔
94

95
                return \sprintf(
5✔
96
                    "\n%s\n%s\n\n%s",
5✔
97
                    $tag->title,
5✔
98
                    $titleLine,
5✔
99
                    null === $tag->description ? '' : RstUtils::toRst($tag->description, 0)
5✔
100
                );
5✔
101
            },
5✔
102
            $tags
5✔
103
        );
5✔
104

105
        if ([] !== $warnings) {
5✔
106
            $warningsHeader = 1 === \count($warnings) ? 'Warning' : 'Warnings';
5✔
107

108
            $doc .= "\n\n".$header($warningsHeader).implode("\n", $warnings);
5✔
109
        }
110

111
        if ($fixer instanceof ConfigurableFixerInterface) {
5✔
112
            $doc .= <<<'RST'
113

114

115
                Configuration
116
                -------------
117
                RST;
118

119
            $configurationDefinition = $fixer->getConfigurationDefinition();
3✔
120

121
            foreach ($configurationDefinition->getOptions() as $option) {
3✔
122
                $optionInfo = "``{$option->getName()}``";
3✔
123
                $optionInfo .= "\n".str_repeat('~', \strlen($optionInfo));
3✔
124

125
                if ($option instanceof DeprecatedFixerOptionInterface) {
3✔
126
                    $deprecationMessage = RstUtils::toRst($option->getDeprecationMessage());
×
127
                    $optionInfo .= "\n\n.. warning:: This option is deprecated and will be removed in the next major version. {$deprecationMessage}";
×
128
                }
129

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

132
                if ($option instanceof AliasedFixerOption) {
3✔
133
                    $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.";
×
134
                }
135

136
                $allowed = HelpCommand::getDisplayableAllowedValues($option);
3✔
137

138
                if (null === $allowed) {
3✔
139
                    $allowedKind = 'Allowed types';
2✔
140
                    $allowedTypes = $option->getAllowedTypes();
2✔
141
                    if (null !== $allowedTypes) {
2✔
142
                        $allowed = array_map(
2✔
143
                            static fn (string $value): string => '``'.Utils::convertArrayTypeToList($value).'``',
2✔
144
                            $allowedTypes,
2✔
145
                        );
2✔
146
                    }
147
                } else {
148
                    $allowedKind = 'Allowed values';
3✔
149
                    $allowed = array_map(static fn ($value): string => $value instanceof AllowedValueSubset
3✔
150
                        ? 'a subset of ``'.Utils::toString($value->getAllowedValues()).'``'
1✔
151
                        : '``'.Utils::toString($value).'``', $allowed);
3✔
152
                }
153

154
                if (null !== $allowed) {
3✔
155
                    $allowed = Utils::naturalLanguageJoin($allowed, '');
3✔
156
                    $optionInfo .= "\n\n{$allowedKind}: {$allowed}";
3✔
157
                }
158

159
                if ($option->hasDefault()) {
3✔
160
                    $default = Utils::toString($option->getDefault());
3✔
161
                    $optionInfo .= "\n\nDefault value: ``{$default}``";
3✔
162
                } else {
163
                    $optionInfo .= "\n\nThis option is required.";
1✔
164
                }
165

166
                $doc .= "\n\n{$optionInfo}";
3✔
167
            }
168
        }
169

170
        $samples = $definition->getCodeSamples();
5✔
171

172
        if (0 !== \count($samples)) {
5✔
173
            $doc .= <<<'RST'
174

175

176
                Examples
177
                --------
178
                RST;
179

180
            foreach ($samples as $index => $sample) {
5✔
181
                $title = \sprintf('Example #%d', $index + 1);
5✔
182
                $titleLine = str_repeat('~', \strlen($title));
5✔
183
                $doc .= "\n\n{$title}\n{$titleLine}";
5✔
184

185
                if ($fixer instanceof ConfigurableFixerInterface) {
5✔
186
                    if (null === $sample->getConfiguration()) {
3✔
187
                        $doc .= "\n\n*Default* configuration.";
2✔
188
                    } else {
189
                        $doc .= \sprintf(
3✔
190
                            "\n\nWith configuration: ``%s``.",
3✔
191
                            Utils::toString($sample->getConfiguration())
3✔
192
                        );
3✔
193
                    }
194
                }
195

196
                $doc .= "\n".$this->generateSampleDiff($fixer, $sample, $index + 1, $name);
5✔
197
            }
198
        }
199

200
        $ruleSetConfigs = self::getSetsOfRule($name);
5✔
201

202
        if ([] !== $ruleSetConfigs) {
5✔
203
            $plural = 1 !== \count($ruleSetConfigs) ? 's' : '';
2✔
204
            $doc .= <<<RST
2✔
205

206

207
                Rule sets
208
                ---------
209

210
                The rule is part of the following rule set{$plural}:\n\n
2✔
211
                RST;
2✔
212

213
            foreach ($ruleSetConfigs as $set => $config) {
2✔
214
                $ruleSetPath = $this->locator->getRuleSetsDocumentationFilePath($set);
2✔
215
                $ruleSetPath = substr($ruleSetPath, strrpos($ruleSetPath, '/'));
2✔
216

217
                \assert(isset($this->ruleSetDefinitions[$set]));
2✔
218
                $ruleSetDefinition = $this->ruleSetDefinitions[$set];
2✔
219

220
                if ($ruleSetDefinition instanceof AutomaticRuleSetDefinitionInterface) {
2✔
221
                    continue;
1✔
222
                }
223

224
                $deprecatedDesc = ($ruleSetDefinition instanceof DeprecatedRuleSetDefinitionInterface) ? ' *(deprecated)*' : '';
2✔
225

226
                $configInfo = (null !== $config)
2✔
227
                    ? " with config:\n\n  ``".Utils::toString($config)."``\n"
1✔
228
                    : '';
2✔
229

230
                $doc .= <<<RST
2✔
231
                    - `{$set} <./../../ruleSets{$ruleSetPath}>`_{$deprecatedDesc}{$configInfo}\n
2✔
232
                    RST;
2✔
233
            }
234

235
            $doc = trim($doc);
2✔
236
        }
237

238
        $reflectionObject = new \ReflectionObject($fixer);
5✔
239
        $className = str_replace('\\', '\\\\', $reflectionObject->getName());
5✔
240
        $fileName = $reflectionObject->getFileName();
5✔
241
        $fileName = str_replace('\\', '/', $fileName);
5✔
242
        $fileName = substr($fileName, (int) strrpos($fileName, '/src/Fixer/') + 1);
5✔
243
        $fileName = "`{$className} <./../../../{$fileName}>`_";
5✔
244

245
        $testFileName = Preg::replace('~.*\K/src/(?=Fixer/)~', '/tests/', $fileName);
5✔
246
        $testFileName = Preg::replace('~PhpCsFixer\\\\\\\\\K(?=Fixer\\\\\\\)~', 'Tests\\\\\\\\', $testFileName);
5✔
247
        $testFileName = Preg::replace('~(?= <|\.php>)~', 'Test', $testFileName);
5✔
248

249
        $doc .= <<<RST
5✔
250

251

252
            References
253
            ----------
254

255
            - Fixer class: {$fileName}
5✔
256
            - Test class: {$testFileName}
5✔
257

258
            The test class defines officially supported behaviour. Each test case is a part of our backward compatibility promise.
259
            RST;
5✔
260

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

263
        return "{$doc}\n";
5✔
264
    }
265

266
    /**
267
     * @internal
268
     *
269
     * @return array<string, null|array<string, mixed>>
270
     */
271
    public static function getSetsOfRule(string $ruleName): array
272
    {
273
        static $ruleSetCache = null;
5✔
274

275
        if (null === $ruleSetCache) {
5✔
276
            $ruleSetCache = array_combine(
×
277
                RuleSets::getSetDefinitionNames(),
×
278
                array_map(
×
279
                    static fn (string $name): RuleSet => new RuleSet([$name => true]),
×
280
                    RuleSets::getSetDefinitionNames()
×
281
                )
×
282
            );
×
283
        }
284

285
        $ruleSetConfigs = [];
5✔
286

287
        foreach ($ruleSetCache as $set => $ruleSet) {
5✔
288
            if ($ruleSet->hasRule($ruleName)) {
5✔
289
                $ruleSetConfigs[$set] = $ruleSet->getRuleConfiguration($ruleName);
2✔
290
            }
291
        }
292

293
        return $ruleSetConfigs;
5✔
294
    }
295

296
    /**
297
     * @param list<FixerInterface> $fixers
298
     */
299
    public function generateFixersDocumentationIndex(array $fixers): string
300
    {
301
        $overrideGroups = [
1✔
302
            'PhpUnit' => 'PHPUnit',
1✔
303
            'PhpTag' => 'PHP Tag',
1✔
304
            'Phpdoc' => 'PHPDoc',
1✔
305
        ];
1✔
306

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

309
        $documentation = <<<'RST'
1✔
310
            =======================
311
            List of Available Rules
312
            =======================
313
            RST;
1✔
314

315
        $currentGroup = null;
1✔
316

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

321
            if ($group !== $currentGroup) {
1✔
322
                $underline = str_repeat('-', \strlen($group));
1✔
323
                $documentation .= "\n\n{$group}\n{$underline}\n";
1✔
324

325
                $currentGroup = $group;
1✔
326
            }
327

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

330
            $tags = array_map(
1✔
331
                static fn (DocumentationTag $tag): string => $tag->type,
1✔
332
                DocumentationTagGenerator::analyseRule($fixer)
1✔
333
            );
1✔
334

335
            $attributes = 0 === \count($tags)
1✔
336
                ? ''
1✔
337
                : ' *('.implode(', ', $tags).')*';
1✔
338

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

341
            $documentation .= <<<RST
1✔
342

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

345
                  {$summary}
1✔
346
                RST;
1✔
347
        }
348

349
        return "{$documentation}\n";
1✔
350
    }
351

352
    private function generateSampleDiff(FixerInterface $fixer, CodeSampleInterface $sample, int $sampleNumber, string $ruleName): string
353
    {
354
        if ($sample instanceof VersionSpecificCodeSampleInterface && !$sample->isSuitableFor(\PHP_VERSION_ID)) {
5✔
355
            $existingFile = @file_get_contents($this->locator->getFixerDocumentationFilePath($fixer));
×
356

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

360
                if (isset($matches['diff'])) {
×
361
                    return $matches['diff'];
×
362
                }
363
            }
364

365
            $error = <<<RST
×
366

367
                .. error::
368
                   Cannot generate diff for code sample #{$sampleNumber} of rule {$ruleName}:
×
369
                   the sample is not suitable for current version of PHP (%s).
370
                RST;
×
371

372
            return \sprintf($error, \PHP_VERSION);
×
373
        }
374

375
        $old = $sample->getCode();
5✔
376

377
        $tokens = Tokens::fromCode($old);
5✔
378
        $file = $sample instanceof FileSpecificCodeSampleInterface
5✔
379
            ? $sample->getSplFileInfo()
×
380
            : new StdinFileInfo();
5✔
381

382
        if ($fixer instanceof ConfigurableFixerInterface) {
5✔
383
            $fixer->configure($sample->getConfiguration() ?? []);
3✔
384
        }
385

386
        $fixer->fix($file, $tokens);
5✔
387

388
        $diff = $this->differ->diff($old, $tokens->generateCode());
5✔
389
        $diff = Preg::replace('/@@[ \+\-\d,]+@@\n/', '', $diff);
5✔
390
        $diff = Preg::replace('/\r/', '^M', $diff);
5✔
391
        $diff = Preg::replace('/^ $/m', '', $diff);
5✔
392
        $diff = Preg::replace('/\n$/', '', $diff);
5✔
393
        $diff = RstUtils::indent($diff, 3);
5✔
394

395
        return <<<RST
5✔
396

397
            .. code-block:: diff
398

399
               {$diff}
5✔
400
            RST;
5✔
401
    }
402
}
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