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

PHP-CS-Fixer / PHP-CS-Fixer / 15273528065

27 May 2025 11:01AM UTC coverage: 94.859% (-0.06%) from 94.915%
15273528065

push

github

web-flow
DX: introduce `FCT` class for tokens not present in the lowest supported PHP version (#8706)

Co-authored-by: Dariusz Rumiński <dariusz.ruminski@gmail.com>

186 of 192 new or added lines in 52 files covered. (96.88%)

1 existing line in 1 file now uncovered.

28102 of 29625 relevant lines covered (94.86%)

45.33 hits per line

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

67.74
/src/Fixer/AbstractPhpUnitFixer.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\Fixer;
16

17
use PhpCsFixer\AbstractFixer;
18
use PhpCsFixer\DocBlock\DocBlock;
19
use PhpCsFixer\DocBlock\Line;
20
use PhpCsFixer\Indicator\PhpUnitTestCaseIndicator;
21
use PhpCsFixer\Tokenizer\Analyzer\Analysis\NamespaceUseAnalysis;
22
use PhpCsFixer\Tokenizer\Analyzer\AttributeAnalyzer;
23
use PhpCsFixer\Tokenizer\Analyzer\FullyQualifiedNameAnalyzer;
24
use PhpCsFixer\Tokenizer\Analyzer\FunctionsAnalyzer;
25
use PhpCsFixer\Tokenizer\Analyzer\WhitespacesAnalyzer;
26
use PhpCsFixer\Tokenizer\CT;
27
use PhpCsFixer\Tokenizer\FCT;
28
use PhpCsFixer\Tokenizer\Token;
29
use PhpCsFixer\Tokenizer\Tokens;
30

31
/**
32
 * @internal
33
 */
34
abstract class AbstractPhpUnitFixer extends AbstractFixer
35
{
36
    private const DOC_BLOCK_MODIFIERS = [T_PUBLIC, T_PROTECTED, T_PRIVATE, T_FINAL, T_ABSTRACT, T_COMMENT, FCT::T_ATTRIBUTE, FCT::T_READONLY];
37
    private const ATTRIBUTE_MODIFIERS = [T_FINAL, FCT::T_READONLY];
38

39
    public function isCandidate(Tokens $tokens): bool
40
    {
41
        return $tokens->isAllTokenKindsFound([T_CLASS, T_STRING]);
59✔
42
    }
43

44
    final protected function applyFix(\SplFileInfo $file, Tokens $tokens): void
45
    {
46
        $phpUnitTestCaseIndicator = new PhpUnitTestCaseIndicator();
59✔
47

48
        foreach ($phpUnitTestCaseIndicator->findPhpUnitClasses($tokens) as $indices) {
59✔
49
            $this->applyPhpUnitClassFix($tokens, $indices[0], $indices[1]);
56✔
50
        }
51
    }
52

53
    abstract protected function applyPhpUnitClassFix(Tokens $tokens, int $startIndex, int $endIndex): void;
54

55
    final protected function getDocBlockIndex(Tokens $tokens, int $index): int
56
    {
57
        do {
58
            $index = $tokens->getPrevNonWhitespace($index);
54✔
59

60
            if ($tokens[$index]->isGivenKind(CT::T_ATTRIBUTE_CLOSE)) {
54✔
61
                $index = $tokens->getPrevTokenOfKind($index, [[T_ATTRIBUTE]]);
28✔
62
            }
63
        } while ($tokens[$index]->isGivenKind(self::DOC_BLOCK_MODIFIERS));
54✔
64

65
        return $index;
54✔
66
    }
67

68
    /**
69
     * @param list<string>       $preventingAnnotations
70
     * @param list<class-string> $preventingAttributes
71
     */
72
    final protected function ensureIsDocBlockWithAnnotation(
73
        Tokens $tokens,
74
        int $index,
75
        string $annotation,
76
        array $preventingAnnotations,
77
        array $preventingAttributes
78
    ): void {
79
        $docBlockIndex = $this->getDocBlockIndex($tokens, $index);
54✔
80

81
        if (self::isPreventedByAttribute($tokens, $index, $preventingAttributes)) {
54✔
82
            return;
23✔
83
        }
84

85
        if ($this->isPHPDoc($tokens, $docBlockIndex)) {
31✔
86
            $this->updateDocBlockIfNeeded($tokens, $docBlockIndex, $annotation, $preventingAnnotations);
29✔
87
        } else {
88
            $this->createDocBlock($tokens, $docBlockIndex, $annotation);
16✔
89
        }
90
    }
91

92
    final protected function isPHPDoc(Tokens $tokens, int $index): bool
93
    {
94
        return $tokens[$index]->isGivenKind(T_DOC_COMMENT);
31✔
95
    }
96

97
    /**
98
     * @return iterable<array{
99
     *     index: int,
100
     *     loweredName: string,
101
     *     openBraceIndex: int,
102
     *     closeBraceIndex: int,
103
     * }>
104
     */
105
    protected function getPreviousAssertCall(Tokens $tokens, int $startIndex, int $endIndex): iterable
106
    {
107
        $functionsAnalyzer = new FunctionsAnalyzer();
×
108

109
        for ($index = $endIndex; $index > $startIndex; --$index) {
×
110
            $index = $tokens->getPrevTokenOfKind($index, [[T_STRING]]);
×
111

112
            if (null === $index) {
×
113
                return;
×
114
            }
115

116
            // test if "assert" something call
117
            $loweredContent = strtolower($tokens[$index]->getContent());
×
118

119
            if (!str_starts_with($loweredContent, 'assert')) {
×
120
                continue;
×
121
            }
122

123
            // test candidate for simple calls like: ([\]+'some fixable call'(...))
124
            $openBraceIndex = $tokens->getNextMeaningfulToken($index);
×
125

126
            if (!$tokens[$openBraceIndex]->equals('(')) {
×
127
                continue;
×
128
            }
129

130
            if (!$functionsAnalyzer->isTheSameClassCall($tokens, $index)) {
×
131
                continue;
×
132
            }
133

134
            yield [
×
135
                'index' => $index,
×
136
                'loweredName' => $loweredContent,
×
137
                'openBraceIndex' => $openBraceIndex,
×
138
                'closeBraceIndex' => $tokens->findBlockEnd(Tokens::BLOCK_TYPE_PARENTHESIS_BRACE, $openBraceIndex),
×
139
            ];
×
140
        }
141
    }
142

143
    final protected function isTestAttributePresent(Tokens $tokens, int $index): bool
144
    {
NEW
145
        $attributeIndex = $tokens->getPrevTokenOfKind($index, ['{', [FCT::T_ATTRIBUTE]]);
×
NEW
146
        if (!$tokens[$attributeIndex]->isGivenKind(FCT::T_ATTRIBUTE)) {
×
UNCOV
147
            return false;
×
148
        }
149

150
        $fullyQualifiedNameAnalyzer = new FullyQualifiedNameAnalyzer($tokens);
×
151

152
        foreach (AttributeAnalyzer::collect($tokens, $attributeIndex) as $attributeAnalysis) {
×
153
            foreach ($attributeAnalysis->getAttributes() as $attribute) {
×
154
                $attributeName = strtolower($fullyQualifiedNameAnalyzer->getFullyQualifiedName($attribute['name'], $attribute['start'], NamespaceUseAnalysis::TYPE_CLASS));
×
155
                if ('phpunit\framework\attributes\test' === $attributeName) {
×
156
                    return true;
×
157
                }
158
            }
159
        }
160

161
        return false;
×
162
    }
163

164
    private function createDocBlock(Tokens $tokens, int $docBlockIndex, string $annotation): void
165
    {
166
        $lineEnd = $this->whitespacesConfig->getLineEnding();
16✔
167
        $originalIndent = WhitespacesAnalyzer::detectIndent($tokens, $tokens->getNextNonWhitespace($docBlockIndex));
16✔
168
        $toInsert = [
16✔
169
            new Token([T_DOC_COMMENT, "/**{$lineEnd}{$originalIndent} * @{$annotation}{$lineEnd}{$originalIndent} */"]),
16✔
170
            new Token([T_WHITESPACE, $lineEnd.$originalIndent]),
16✔
171
        ];
16✔
172
        $index = $tokens->getNextMeaningfulToken($docBlockIndex);
16✔
173
        $tokens->insertAt($index, $toInsert);
16✔
174

175
        if (!$tokens[$index - 1]->isGivenKind(T_WHITESPACE)) {
16✔
176
            $extraNewLines = $this->whitespacesConfig->getLineEnding();
3✔
177

178
            if (!$tokens[$index - 1]->isGivenKind(T_OPEN_TAG)) {
3✔
179
                $extraNewLines .= $this->whitespacesConfig->getLineEnding();
1✔
180
            }
181

182
            $tokens->insertAt($index, [
3✔
183
                new Token([T_WHITESPACE, $extraNewLines.WhitespacesAnalyzer::detectIndent($tokens, $index)]),
3✔
184
            ]);
3✔
185
        }
186
    }
187

188
    /**
189
     * @param list<string> $preventingAnnotations
190
     */
191
    private function updateDocBlockIfNeeded(
192
        Tokens $tokens,
193
        int $docBlockIndex,
194
        string $annotation,
195
        array $preventingAnnotations
196
    ): void {
197
        $doc = new DocBlock($tokens[$docBlockIndex]->getContent());
29✔
198
        foreach ($preventingAnnotations as $preventingAnnotation) {
29✔
199
            if ([] !== $doc->getAnnotationsOfType($preventingAnnotation)) {
29✔
200
                return;
29✔
201
            }
202
        }
203
        $doc = $this->makeDocBlockMultiLineIfNeeded($doc, $tokens, $docBlockIndex, $annotation);
11✔
204

205
        $lines = $this->addInternalAnnotation($doc, $tokens, $docBlockIndex, $annotation);
11✔
206
        $lines = implode('', $lines);
11✔
207

208
        $tokens->getNamespaceDeclarations();
11✔
209
        $tokens[$docBlockIndex] = new Token([T_DOC_COMMENT, $lines]);
11✔
210
    }
211

212
    /**
213
     * @param list<class-string> $preventingAttributes
214
     */
215
    private static function isPreventedByAttribute(Tokens $tokens, int $index, array $preventingAttributes): bool
216
    {
217
        if ([] === $preventingAttributes) {
54✔
218
            return false;
×
219
        }
220

221
        do {
222
            $index = $tokens->getPrevMeaningfulToken($index);
54✔
223
        } while ($tokens[$index]->isGivenKind(self::ATTRIBUTE_MODIFIERS));
54✔
224
        if (!$tokens[$index]->isGivenKind(CT::T_ATTRIBUTE_CLOSE)) {
54✔
225
            return false;
26✔
226
        }
227
        $index = $tokens->findBlockStart(Tokens::BLOCK_TYPE_ATTRIBUTE, $index);
28✔
228

229
        $fullyQualifiedNameAnalyzer = new FullyQualifiedNameAnalyzer($tokens);
28✔
230

231
        foreach (AttributeAnalyzer::collect($tokens, $index) as $attributeAnalysis) {
28✔
232
            foreach ($attributeAnalysis->getAttributes() as $attribute) {
28✔
233
                if (\in_array(strtolower($fullyQualifiedNameAnalyzer->getFullyQualifiedName($attribute['name'], $attribute['start'], NamespaceUseAnalysis::TYPE_CLASS)), $preventingAttributes, true)) {
28✔
234
                    return true;
23✔
235
                }
236
            }
237
        }
238

239
        return false;
5✔
240
    }
241

242
    /**
243
     * @return list<Line>
244
     */
245
    private function addInternalAnnotation(DocBlock $docBlock, Tokens $tokens, int $docBlockIndex, string $annotation): array
246
    {
247
        $lines = $docBlock->getLines();
11✔
248
        $originalIndent = WhitespacesAnalyzer::detectIndent($tokens, $docBlockIndex);
11✔
249
        $lineEnd = $this->whitespacesConfig->getLineEnding();
11✔
250
        array_splice($lines, -1, 0, $originalIndent.' * @'.$annotation.$lineEnd);
11✔
251

252
        return $lines;
11✔
253
    }
254

255
    private function makeDocBlockMultiLineIfNeeded(DocBlock $doc, Tokens $tokens, int $docBlockIndex, string $annotation): DocBlock
256
    {
257
        $lines = $doc->getLines();
11✔
258
        if (1 === \count($lines) && [] === $doc->getAnnotationsOfType($annotation)) {
11✔
259
            $indent = WhitespacesAnalyzer::detectIndent($tokens, $tokens->getNextNonWhitespace($docBlockIndex));
2✔
260
            $doc->makeMultiLine($indent, $this->whitespacesConfig->getLineEnding());
2✔
261

262
            return $doc;
2✔
263
        }
264

265
        return $doc;
9✔
266
    }
267
}
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