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

keradus / PHP-CS-Fixer / 17319949156

29 Aug 2025 09:20AM UTC coverage: 94.696% (-0.05%) from 94.744%
17319949156

push

github

keradus
CS

28333 of 29920 relevant lines covered (94.7%)

45.63 hits per line

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

92.54
/src/Tokenizer/Analyzer/ControlCaseStructuresAnalyzer.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\Tokenizer\Analyzer;
16

17
use PhpCsFixer\Tokenizer\Analyzer\Analysis\AbstractControlCaseStructuresAnalysis;
18
use PhpCsFixer\Tokenizer\Analyzer\Analysis\CaseAnalysis;
19
use PhpCsFixer\Tokenizer\Analyzer\Analysis\DefaultAnalysis;
20
use PhpCsFixer\Tokenizer\Analyzer\Analysis\EnumAnalysis;
21
use PhpCsFixer\Tokenizer\Analyzer\Analysis\MatchAnalysis;
22
use PhpCsFixer\Tokenizer\Analyzer\Analysis\SwitchAnalysis;
23
use PhpCsFixer\Tokenizer\FCT;
24
use PhpCsFixer\Tokenizer\Tokens;
25

26
/**
27
 * @no-named-arguments Parameter names are not covered by the backward compatibility promise.
28
 */
29
final class ControlCaseStructuresAnalyzer
30
{
31
    private const SUPPORTED_TYPES_WITH_CASE_OR_DEFAULT = [
32
        \T_SWITCH,
33
        FCT::T_MATCH,
34
        FCT::T_ENUM,
35
    ];
36

37
    /**
38
     * @param list<int> $types Token types of interest of which analyzes must be returned
39
     *
40
     * @return \Generator<int, AbstractControlCaseStructuresAnalysis>
41
     */
42
    public static function findControlStructures(Tokens $tokens, array $types): \Generator
43
    {
44
        if (\count($types) < 1) {
26✔
45
            return; // quick skip
×
46
        }
47

48
        foreach ($types as $type) {
26✔
49
            if (!\in_array($type, self::SUPPORTED_TYPES_WITH_CASE_OR_DEFAULT, true)) {
26✔
50
                throw new \InvalidArgumentException(\sprintf('Unexpected type "%d".', $type));
1✔
51
            }
52
        }
53

54
        if (!$tokens->isAnyTokenKindsFound($types)) {
25✔
55
            return; // quick skip
1✔
56
        }
57

58
        $depth = -1;
24✔
59

60
        /**
61
         * @var list<array{
62
         *     kind: null|int,
63
         *     index: int,
64
         *     brace_count: int,
65
         *     cases: list<array{index: int, open: int}>,
66
         *     default: null|array{index: int, open: int},
67
         *     alternative_syntax: bool,
68
         * }> $stack
69
         */
70
        $stack = [];
24✔
71
        $isTypeOfInterest = false;
24✔
72

73
        foreach ($tokens as $index => $token) {
24✔
74
            if ($token->isGivenKind(self::SUPPORTED_TYPES_WITH_CASE_OR_DEFAULT)) {
24✔
75
                ++$depth;
24✔
76

77
                $stack[$depth] = [
24✔
78
                    'kind' => $token->getId(),
24✔
79
                    'index' => $index,
24✔
80
                    'brace_count' => 0,
24✔
81
                    'cases' => [],
24✔
82
                    'default' => null,
24✔
83
                    'alternative_syntax' => false,
24✔
84
                ];
24✔
85

86
                $isTypeOfInterest = \in_array($stack[$depth]['kind'], $types, true);
24✔
87

88
                if ($token->isGivenKind(\T_SWITCH)) {
24✔
89
                    $index = $tokens->getNextMeaningfulToken($index);
24✔
90
                    $index = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_PARENTHESIS_BRACE, $index);
24✔
91

92
                    $stack[$depth]['open'] = $tokens->getNextMeaningfulToken($index);
24✔
93
                    $stack[$depth]['alternative_syntax'] = $tokens[$stack[$depth]['open']]->equals(':');
24✔
94
                } elseif ($token->isGivenKind(FCT::T_MATCH)) {
3✔
95
                    $index = $tokens->getNextMeaningfulToken($index);
3✔
96
                    $index = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_PARENTHESIS_BRACE, $index);
3✔
97

98
                    $stack[$depth]['open'] = $tokens->getNextMeaningfulToken($index);
3✔
99
                } elseif ($token->isGivenKind(FCT::T_ENUM)) {
3✔
100
                    $stack[$depth]['open'] = $tokens->getNextTokenOfKind($index, ['{']);
3✔
101
                }
102

103
                continue;
24✔
104
            }
105

106
            if ($depth < 0) {
24✔
107
                continue;
24✔
108
            }
109

110
            if ($token->equals('{')) {
24✔
111
                ++$stack[$depth]['brace_count'];
19✔
112

113
                continue;
19✔
114
            }
115

116
            if ($token->equals('}')) {
24✔
117
                --$stack[$depth]['brace_count'];
19✔
118

119
                if (0 === $stack[$depth]['brace_count']) {
19✔
120
                    if ($stack[$depth]['alternative_syntax']) {
19✔
121
                        continue;
1✔
122
                    }
123

124
                    if ($isTypeOfInterest) {
18✔
125
                        $stack[$depth]['end'] = $index;
18✔
126

127
                        yield $stack[$depth]['index'] => self::buildControlCaseStructureAnalysis($stack[$depth]);
18✔
128
                    }
129

130
                    array_pop($stack);
18✔
131
                    --$depth;
18✔
132

133
                    if ($depth < -1) { // @phpstan-ignore-line
18✔
134
                        throw new \RuntimeException('Analysis depth count failure.');
×
135
                    }
136

137
                    if (isset($stack[$depth]['kind'])) {
18✔
138
                        $isTypeOfInterest = \in_array($stack[$depth]['kind'], $types, true);
3✔
139
                    }
140
                }
141

142
                continue;
18✔
143
            }
144

145
            if ($tokens[$index]->isGivenKind(\T_ENDSWITCH)) {
24✔
146
                if (!$stack[$depth]['alternative_syntax']) {
6✔
147
                    throw new \RuntimeException('Analysis syntax failure, unexpected "T_ENDSWITCH".');
×
148
                }
149

150
                if (\T_SWITCH !== $stack[$depth]['kind']) {
6✔
151
                    throw new \RuntimeException('Analysis type failure, unexpected "T_ENDSWITCH".');
×
152
                }
153

154
                if (0 !== $stack[$depth]['brace_count']) {
6✔
155
                    throw new \RuntimeException('Analysis count failure, unexpected "T_ENDSWITCH".');
×
156
                }
157

158
                $index = $tokens->getNextTokenOfKind($index, [';', [\T_CLOSE_TAG]]);
6✔
159

160
                if ($isTypeOfInterest) {
6✔
161
                    $stack[$depth]['end'] = $index;
6✔
162

163
                    yield $stack[$depth]['index'] => self::buildControlCaseStructureAnalysis($stack[$depth]);
6✔
164
                }
165

166
                array_pop($stack);
6✔
167
                --$depth;
6✔
168

169
                if ($depth < -1) { // @phpstan-ignore-line
6✔
170
                    throw new \RuntimeException('Analysis depth count failure ("T_ENDSWITCH").');
×
171
                }
172

173
                if (isset($stack[$depth]['kind'])) {
6✔
174
                    $isTypeOfInterest = \in_array($stack[$depth]['kind'], $types, true);
3✔
175
                }
176
            }
177

178
            if (!$isTypeOfInterest) {
24✔
179
                continue; // don't bother to analyze stuff that caller is not interested in
3✔
180
            }
181

182
            if ($token->isGivenKind(\T_CASE)) {
24✔
183
                $stack[$depth]['cases'][] = ['index' => $index, 'open' => self::findCaseOpen($tokens, $stack[$depth]['kind'], $index)];
23✔
184
            } elseif ($token->isGivenKind(\T_DEFAULT)) {
24✔
185
                if (null !== $stack[$depth]['default']) {
8✔
186
                    throw new \RuntimeException('Analysis multiple "default" found.');
×
187
                }
188

189
                $stack[$depth]['default'] = ['index' => $index, 'open' => self::findDefaultOpen($tokens, $stack[$depth]['kind'], $index)];
8✔
190
            }
191
        }
192
    }
193

194
    /**
195
     * @param array{
196
     *     kind: int,
197
     *     index: int,
198
     *     open: int,
199
     *     end: int,
200
     *     cases: list<array{index: int, open: int}>,
201
     *     default: null|array{index: int, open: int},
202
     * } $analysis
203
     */
204
    private static function buildControlCaseStructureAnalysis(array $analysis): AbstractControlCaseStructuresAnalysis
205
    {
206
        $default = null === $analysis['default']
24✔
207
            ? null
17✔
208
            : new DefaultAnalysis($analysis['default']['index'], $analysis['default']['open']);
8✔
209

210
        $cases = [];
24✔
211

212
        foreach ($analysis['cases'] as $case) {
24✔
213
            $cases[$case['index']] = new CaseAnalysis($case['index'], $case['open']);
23✔
214
        }
215

216
        sort($cases);
24✔
217

218
        if (\T_SWITCH === $analysis['kind']) {
24✔
219
            return new SwitchAnalysis(
23✔
220
                $analysis['index'],
23✔
221
                $analysis['open'],
23✔
222
                $analysis['end'],
23✔
223
                $cases,
23✔
224
                $default
23✔
225
            );
23✔
226
        }
227

228
        if (FCT::T_ENUM === $analysis['kind']) {
2✔
229
            return new EnumAnalysis(
1✔
230
                $analysis['index'],
1✔
231
                $analysis['open'],
1✔
232
                $analysis['end'],
1✔
233
                $cases
1✔
234
            );
1✔
235
        }
236

237
        if (FCT::T_MATCH === $analysis['kind']) {
1✔
238
            return new MatchAnalysis(
1✔
239
                $analysis['index'],
1✔
240
                $analysis['open'],
1✔
241
                $analysis['end'],
1✔
242
                $default
1✔
243
            );
1✔
244
        }
245

246
        throw new \InvalidArgumentException(\sprintf('Unexpected type "%d".', $analysis['kind']));
×
247
    }
248

249
    private static function findCaseOpen(Tokens $tokens, int $kind, int $index): int
250
    {
251
        if (\T_SWITCH === $kind) {
23✔
252
            $ternariesCount = 0;
23✔
253

254
            --$index;
23✔
255
            while (true) {
23✔
256
                ++$index;
23✔
257

258
                if ($tokens[$index]->equalsAny(['(', '{'])) { // skip constructs
23✔
259
                    $type = Tokens::detectBlockType($tokens[$index]);
3✔
260
                    $index = $tokens->findBlockEnd($type['type'], $index);
3✔
261

262
                    continue;
3✔
263
                }
264

265
                if ($tokens[$index]->equals('?')) {
23✔
266
                    ++$ternariesCount;
1✔
267

268
                    continue;
1✔
269
                }
270

271
                if ($tokens[$index]->equalsAny([':', ';'])) {
23✔
272
                    if (0 === $ternariesCount) {
23✔
273
                        break;
23✔
274
                    }
275

276
                    --$ternariesCount;
1✔
277
                }
278
            }
279

280
            return $index;
23✔
281
        }
282

283
        if (FCT::T_ENUM === $kind) {
1✔
284
            return $tokens->getNextTokenOfKind($index, ['=', ';']);
1✔
285
        }
286

287
        throw new \InvalidArgumentException(\sprintf('Unexpected case for type "%d".', $kind));
×
288
    }
289

290
    private static function findDefaultOpen(Tokens $tokens, int $kind, int $index): int
291
    {
292
        if (\T_SWITCH === $kind) {
8✔
293
            return $tokens->getNextTokenOfKind($index, [':', ';']);
7✔
294
        }
295

296
        if (FCT::T_MATCH === $kind) {
1✔
297
            return $tokens->getNextTokenOfKind($index, [[\T_DOUBLE_ARROW]]);
1✔
298
        }
299

300
        throw new \InvalidArgumentException(\sprintf('Unexpected default for type "%d".', $kind));
×
301
    }
302
}
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