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

keradus / PHP-CS-Fixer / 17253231013

26 Aug 2025 11:46PM UTC coverage: 94.753% (+0.01%) from 94.741%
17253231013

push

github

keradus
chore: fix CS, somehow it missed the CI of .php-cs-fixer.well-defined-arrays.php

28316 of 29884 relevant lines covered (94.75%)

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

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

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

197
                $doc .= "\n\n{$optionInfo}";
3✔
198
            }
199
        }
200

201
        $samples = $definition->getCodeSamples();
5✔
202

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

206

207
                Examples
208
                --------
209
                RST;
210

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

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

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

231
        $ruleSetConfigs = self::getSetsOfRule($name);
5✔
232

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

237

238
                Rule sets
239
                ---------
240

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

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

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

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

257
            $doc = trim($doc);
2✔
258
        }
259

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

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

271
        $doc .= <<<RST
5✔
272

273

274
            References
275
            ----------
276

277
            - Fixer class: {$fileName}
5✔
278
            - Test class: {$testFileName}
5✔
279

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

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

285
        return "{$doc}\n";
5✔
286
    }
287

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

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

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

305
        return $ruleSetConfigs;
5✔
306
    }
307

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

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

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

327
        $currentGroup = null;
1✔
328

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

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

337
                $currentGroup = $group;
1✔
338
            }
339

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

342
            $attributes = [];
1✔
343

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

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

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

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

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

362
            $documentation .= <<<RST
1✔
363

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

366
                  {$summary}
1✔
367
                RST;
1✔
368
        }
369

370
        return "{$documentation}\n";
1✔
371
    }
372

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

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

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

386
            $error = <<<RST
×
387

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

393
            return \sprintf($error, \PHP_VERSION);
×
394
        }
395

396
        $old = $sample->getCode();
5✔
397

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

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

407
        $fixer->fix($file, $tokens);
5✔
408

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

416
        return <<<RST
5✔
417

418
            .. code-block:: diff
419

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