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

keradus / PHP-CS-Fixer / 22042339290

15 Feb 2026 08:14PM UTC coverage: 92.957% (-0.2%) from 93.171%
22042339290

push

github

keradus
test: check PHP env in CI jobs

29302 of 31522 relevant lines covered (92.96%)

44.04 hits per line

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

86.41
/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\FixerConfiguration\FixerOptionInterface;
25
use PhpCsFixer\FixerDefinition\CodeSampleInterface;
26
use PhpCsFixer\FixerDefinition\FileSpecificCodeSampleInterface;
27
use PhpCsFixer\FixerDefinition\VersionSpecificCodeSampleInterface;
28
use PhpCsFixer\Future;
29
use PhpCsFixer\Preg;
30
use PhpCsFixer\RuleSet\AutomaticRuleSetDefinitionInterface;
31
use PhpCsFixer\RuleSet\DeprecatedRuleSetDefinitionInterface;
32
use PhpCsFixer\RuleSet\RuleSet;
33
use PhpCsFixer\RuleSet\RuleSetDefinitionInterface;
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, RuleSetDefinitionInterface> */
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
        $header = static function (string $message, string $underline = '-'): string {
5✔
87
            $line = str_repeat($underline, \strlen($message));
5✔
88

89
            return "{$message}\n{$line}\n";
5✔
90
        };
5✔
91

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

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

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

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

113
        if ($fixer instanceof ConfigurableFixerInterface) {
5✔
114
            $fixerInFutureMode = self::createFixerInFutureMode($fixer);
3✔
115

116
            $doc .= <<<'RST'
117

118

119
                Configuration
120
                -------------
121
                RST;
122

123
            $configurationDefinition = $fixer->getConfigurationDefinition();
3✔
124

125
            foreach ($configurationDefinition->getOptions() as $option) {
3✔
126
                $optionInfo = "``{$option->getName()}``";
3✔
127
                $optionInfo .= "\n".str_repeat('~', \strlen($optionInfo));
3✔
128

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

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

136
                if ($option instanceof AliasedFixerOption) {
3✔
137
                    $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.";
×
138
                }
139

140
                $allowed = HelpCommand::getDisplayableAllowedValues($option);
3✔
141

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

158
                if (null !== $allowed) {
3✔
159
                    $allowed = Utils::naturalLanguageJoin($allowed, '');
3✔
160
                    $optionInfo .= "\n\n{$allowedKind}: {$allowed}";
3✔
161
                }
162

163
                if ($option->hasDefault()) {
3✔
164
                    $optionInfo .= \sprintf("\n\nDefault value: ``%s``", Utils::toString($option->getDefault()));
3✔
165

166
                    $optionInFutureMode = array_find(
3✔
167
                        $fixerInFutureMode->getConfigurationDefinition()->getOptions(),
3✔
168
                        static fn (FixerOptionInterface $opt): bool => $option->getName() === $opt->getName(),
3✔
169
                    );
3✔
170
                    \assert(null !== $optionInFutureMode); // if rule exist in v3, shall exist in future mode too, as it does not remove options
3✔
171
                    if ($optionInFutureMode->getDefault() !== $option->getDefault()) {
3✔
172
                        $optionInfo .= \sprintf("\n\nDefault value (future-mode): ``%s``", Utils::toString($optionInFutureMode->getDefault()));
×
173
                    }
174
                } else {
175
                    $optionInfo .= "\n\nThis option is required.";
1✔
176
                }
177

178
                $doc .= "\n\n{$optionInfo}";
3✔
179
            }
180
        }
181

182
        $samples = $definition->getCodeSamples();
5✔
183

184
        if (0 !== \count($samples)) {
5✔
185
            $doc .= <<<'RST'
186

187

188
                Examples
189
                --------
190
                RST;
191

192
            foreach ($samples as $index => $sample) {
5✔
193
                $title = \sprintf('Example #%d', $index + 1);
5✔
194
                $titleLine = str_repeat('~', \strlen($title));
5✔
195
                $doc .= "\n\n{$title}\n{$titleLine}";
5✔
196

197
                if ($fixer instanceof ConfigurableFixerInterface) {
5✔
198
                    if (null === $sample->getConfiguration()) {
3✔
199
                        $doc .= "\n\n*Default* configuration.";
2✔
200
                    } else {
201
                        $doc .= \sprintf(
3✔
202
                            "\n\nWith configuration: ``%s``.",
3✔
203
                            Utils::toString($sample->getConfiguration()),
3✔
204
                        );
3✔
205
                    }
206
                }
207

208
                $doc .= "\n".$this->generateSampleDiff($fixer, $sample, $index + 1, $name);
5✔
209
            }
210
        }
211

212
        $ruleSetConfigs = self::getSetsOfRule($name);
5✔
213

214
        if ([] !== $ruleSetConfigs) {
5✔
215
            $plural = 1 !== \count($ruleSetConfigs) ? 's' : '';
2✔
216
            $doc .= <<<RST
2✔
217

218

219
                Rule sets
220
                ---------
221

222
                The rule is part of the following rule set{$plural}:\n\n
2✔
223
                RST;
2✔
224

225
            foreach ($ruleSetConfigs as $set => $config) {
2✔
226
                $ruleSetPath = $this->locator->getRuleSetsDocumentationFilePath($set);
2✔
227
                $ruleSetPath = substr($ruleSetPath, strrpos($ruleSetPath, '/'));
2✔
228

229
                \assert(isset($this->ruleSetDefinitions[$set]));
2✔
230
                $ruleSetDefinition = $this->ruleSetDefinitions[$set];
2✔
231

232
                if ($ruleSetDefinition instanceof AutomaticRuleSetDefinitionInterface) {
2✔
233
                    continue;
×
234
                }
235

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

238
                $configInfo = (null !== $config)
2✔
239
                    ? " with config:\n\n  ``".Utils::toString($config)."``\n"
1✔
240
                    : '';
2✔
241

242
                $doc .= <<<RST
2✔
243
                    - `{$set} <./../../ruleSets{$ruleSetPath}>`_{$deprecatedDesc}{$configInfo}\n
2✔
244
                    RST;
2✔
245
            }
246

247
            $doc = trim($doc);
2✔
248
        }
249

250
        $reflectionObject = new \ReflectionObject($fixer);
5✔
251
        $className = str_replace('\\', '\\\\', $reflectionObject->getName());
5✔
252
        $fileName = $reflectionObject->getFileName();
5✔
253
        $fileName = str_replace('\\', '/', $fileName);
5✔
254
        $fileName = substr($fileName, (int) strrpos($fileName, '/src/Fixer/') + 1);
5✔
255
        $fileName = "`{$className} <./../../../{$fileName}>`_";
5✔
256

257
        $testFileName = Preg::replace('~.*\K/src/(?=Fixer/)~', '/tests/', $fileName);
5✔
258
        $testFileName = Preg::replace('~PhpCsFixer\\\\\\\\\K(?=Fixer\\\\\\\)~', 'Tests\\\\\\\\', $testFileName);
5✔
259
        $testFileName = Preg::replace('~(?= <|\.php>)~', 'Test', $testFileName);
5✔
260

261
        $doc .= <<<RST
5✔
262

263

264
            References
265
            ----------
266

267
            - Fixer class: {$fileName}
5✔
268
            - Test class: {$testFileName}
5✔
269

270
            The test class defines officially supported behaviour. Each test case is a part of our backward compatibility promise.
271
            RST;
5✔
272

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

275
        return "{$doc}\n";
5✔
276
    }
277

278
    /**
279
     * @internal
280
     *
281
     * @return array<string, null|array<string, mixed>>
282
     */
283
    public static function getSetsOfRule(string $ruleName): array
284
    {
285
        static $ruleSetCache = null;
5✔
286

287
        if (null === $ruleSetCache) {
5✔
288
            $definitionNames = array_keys(
×
289
                array_filter(
×
290
                    RuleSets::getSetDefinitions(),
×
291
                    static fn (RuleSetDefinitionInterface $definition): bool => !$definition instanceof AutomaticRuleSetDefinitionInterface,
×
292
                ),
×
293
            );
×
294
            $ruleSetCache = array_combine(
×
295
                $definitionNames,
×
296
                array_map(
×
297
                    static fn (string $name): RuleSet => new RuleSet([$name => true]),
×
298
                    $definitionNames,
×
299
                ),
×
300
            );
×
301
        }
302

303
        $ruleSetConfigs = [];
5✔
304

305
        foreach ($ruleSetCache as $set => $ruleSet) {
5✔
306
            if ($ruleSet->hasRule($ruleName)) {
5✔
307
                $ruleSetConfigs[$set] = $ruleSet->getRuleConfiguration($ruleName);
2✔
308
            }
309
        }
310

311
        return $ruleSetConfigs;
5✔
312
    }
313

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

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

327
        $documentation = <<<'RST'
1✔
328
            =======================
329
            List of Available Rules
330
            =======================
331
            RST;
1✔
332

333
        $currentGroup = null;
1✔
334

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

339
            if ($group !== $currentGroup) {
1✔
340
                $underline = str_repeat('-', \strlen($group));
1✔
341
                $documentation .= "\n\n{$group}\n{$underline}\n";
1✔
342

343
                $currentGroup = $group;
1✔
344
            }
345

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

348
            $tags = array_map(
1✔
349
                static fn (DocumentationTag $tag): string => $tag->type,
1✔
350
                DocumentationTagGenerator::analyseRule($fixer),
1✔
351
            );
1✔
352

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

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

359
            $documentation .= <<<RST
1✔
360

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

363
                  {$summary}
1✔
364
                RST;
1✔
365
        }
366

367
        return "{$documentation}\n";
1✔
368
    }
369

370
    /**
371
     * @template T of FixerInterface
372
     *
373
     * @param T $fixer
374
     *
375
     * @return T
376
     */
377
    private static function createFixerInFutureMode(FixerInterface $fixer): FixerInterface
378
    {
379
        $object = Future::runWithEnforcedFutureMode(
3✔
380
            static fn () => (new \ReflectionObject($fixer))->newInstance(),
3✔
381
        );
3✔
382

383
        \assert($object instanceof $fixer);
3✔
384

385
        return $object;
3✔
386
    }
387

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

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

396
                if (isset($matches['diff'])) {
×
397
                    return $matches['diff'];
×
398
                }
399
            }
400

401
            $error = <<<RST
×
402

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

408
            return \sprintf($error, \PHP_VERSION);
×
409
        }
410

411
        $old = $sample->getCode();
5✔
412

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

418
        if ($fixer instanceof ConfigurableFixerInterface) {
5✔
419
            $fixer->configure($sample->getConfiguration() ?? []);
3✔
420
        }
421

422
        $fixer->fix($file, $tokens);
5✔
423

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

431
        return <<<RST
5✔
432

433
            .. code-block:: diff
434

435
               {$diff}
5✔
436
            RST;
5✔
437
    }
438
}
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