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

keradus / PHP-CS-Fixer / 17253322895

26 Aug 2025 11:52PM UTC coverage: 94.753% (+0.008%) from 94.745%
17253322895

push

github

keradus
add to git-blame-ignore-revs

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

97.7
/src/Fixer/ControlStructure/SwitchContinueToBreakFixer.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\ControlStructure;
16

17
use PhpCsFixer\AbstractFixer;
18
use PhpCsFixer\FixerDefinition\CodeSample;
19
use PhpCsFixer\FixerDefinition\FixerDefinition;
20
use PhpCsFixer\FixerDefinition\FixerDefinitionInterface;
21
use PhpCsFixer\Preg;
22
use PhpCsFixer\Tokenizer\Token;
23
use PhpCsFixer\Tokenizer\Tokens;
24

25
/**
26
 * @no-named-arguments Parameter names are not covered by the backward compatibility promise.
27
 */
28
final class SwitchContinueToBreakFixer extends AbstractFixer
29
{
30
    /**
31
     * @var list<int>
32
     */
33
    private array $switchLevels = [];
34

35
    public function getDefinition(): FixerDefinitionInterface
36
    {
37
        return new FixerDefinition(
3✔
38
            'Switch case must not be ended with `continue` but with `break`.',
3✔
39
            [
3✔
40
                new CodeSample(
3✔
41
                    '<?php
3✔
42
switch ($foo) {
43
    case 1:
44
        continue;
45
}
46
'
3✔
47
                ),
3✔
48
                new CodeSample(
3✔
49
                    '<?php
3✔
50
switch ($foo) {
51
    case 1:
52
        while($bar) {
53
            do {
54
                continue 3;
55
            } while(false);
56

57
            if ($foo + 1 > 3) {
58
                continue;
59
            }
60

61
            continue 2;
62
        }
63
}
64
'
3✔
65
                ),
3✔
66
            ]
3✔
67
        );
3✔
68
    }
69

70
    /**
71
     * {@inheritdoc}
72
     *
73
     * Must run after NoAlternativeSyntaxFixer.
74
     */
75
    public function getPriority(): int
76
    {
77
        return 0;
1✔
78
    }
79

80
    public function isCandidate(Tokens $tokens): bool
81
    {
82
        return $tokens->isAllTokenKindsFound([\T_SWITCH, \T_CONTINUE]) && !$tokens->hasAlternativeSyntax();
13✔
83
    }
84

85
    protected function applyFix(\SplFileInfo $file, Tokens $tokens): void
86
    {
87
        $count = \count($tokens);
11✔
88

89
        for ($index = 1; $index < $count - 1; ++$index) {
11✔
90
            $index = $this->doFix($tokens, $index, 0, false);
11✔
91
        }
92
    }
93

94
    /**
95
     * @param int $depth >= 0
96
     */
97
    private function doFix(Tokens $tokens, int $index, int $depth, bool $isInSwitch): int
98
    {
99
        $token = $tokens[$index];
11✔
100

101
        if ($token->isGivenKind([\T_FOREACH, \T_FOR, \T_WHILE])) {
11✔
102
            // go to first `(`, go to its close ')', go to first of '{', ';', '? >'
103
            $index = $tokens->getNextTokenOfKind($index, ['(']);
9✔
104
            $index = $tokens->getNextTokenOfKind($index, [')']);
9✔
105
            $index = $tokens->getNextTokenOfKind($index, ['{', ';', [\T_CLOSE_TAG]]);
9✔
106

107
            if (!$tokens[$index]->equals('{')) {
9✔
108
                return $index;
4✔
109
            }
110

111
            return $this->fixInLoop($tokens, $index, $depth + 1);
7✔
112
        }
113

114
        if ($token->isGivenKind(\T_DO)) {
11✔
115
            return $this->fixInLoop($tokens, $tokens->getNextTokenOfKind($index, ['{']), $depth + 1);
3✔
116
        }
117

118
        if ($token->isGivenKind(\T_SWITCH)) {
11✔
119
            return $this->fixInSwitch($tokens, $index, $depth + 1);
11✔
120
        }
121

122
        if ($token->isGivenKind(\T_CONTINUE)) {
11✔
123
            return $this->fixContinueWhenActsAsBreak($tokens, $index, $isInSwitch, $depth);
11✔
124
        }
125

126
        return $index;
11✔
127
    }
128

129
    private function fixInSwitch(Tokens $tokens, int $switchIndex, int $depth): int
130
    {
131
        $this->switchLevels[] = $depth;
11✔
132

133
        // figure out where the switch starts
134
        $openIndex = $tokens->getNextTokenOfKind($switchIndex, ['{']);
11✔
135

136
        // figure out where the switch ends
137
        $closeIndex = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_CURLY_BRACE, $openIndex);
11✔
138

139
        for ($index = $openIndex + 1; $index < $closeIndex; ++$index) {
11✔
140
            $index = $this->doFix($tokens, $index, $depth, true);
11✔
141
        }
142

143
        array_pop($this->switchLevels);
11✔
144

145
        return $closeIndex;
11✔
146
    }
147

148
    private function fixInLoop(Tokens $tokens, int $openIndex, int $depth): int
149
    {
150
        $openCount = 1;
8✔
151

152
        while (true) {
8✔
153
            ++$openIndex;
8✔
154
            $token = $tokens[$openIndex];
8✔
155

156
            if ($token->equals('{')) {
8✔
157
                ++$openCount;
2✔
158

159
                continue;
2✔
160
            }
161

162
            if ($token->equals('}')) {
8✔
163
                --$openCount;
8✔
164

165
                if (0 === $openCount) {
8✔
166
                    break;
8✔
167
                }
168

169
                continue;
2✔
170
            }
171

172
            $openIndex = $this->doFix($tokens, $openIndex, $depth, false);
8✔
173
        }
174

175
        return $openIndex;
8✔
176
    }
177

178
    private function fixContinueWhenActsAsBreak(Tokens $tokens, int $continueIndex, bool $isInSwitch, int $depth): int
179
    {
180
        $followingContinueIndex = $tokens->getNextMeaningfulToken($continueIndex);
11✔
181
        $followingContinueToken = $tokens[$followingContinueIndex];
11✔
182

183
        if ($isInSwitch && $followingContinueToken->equals(';')) {
11✔
184
            $this->replaceContinueWithBreakToken($tokens, $continueIndex); // short continue 1 notation
6✔
185

186
            return $followingContinueIndex;
6✔
187
        }
188

189
        if (!$followingContinueToken->isGivenKind(\T_LNUMBER)) {
8✔
190
            return $followingContinueIndex;
5✔
191
        }
192

193
        $afterFollowingContinueIndex = $tokens->getNextMeaningfulToken($followingContinueIndex);
6✔
194

195
        if (!$tokens[$afterFollowingContinueIndex]->equals(';')) {
6✔
196
            return $afterFollowingContinueIndex; // if next not is `;` return without fixing, for example `continue 1 ? ><?php + $a;`
1✔
197
        }
198

199
        // check if continue {jump} targets a switch statement and if so fix it
200

201
        $jump = $followingContinueToken->getContent();
5✔
202
        $jump = str_replace('_', '', $jump); // support for numeric_literal_separator
5✔
203

204
        if (\strlen($jump) > 2 && 'x' === $jump[1]) {
5✔
205
            $jump = hexdec($jump); // hexadecimal - 0x1
1✔
206
        } elseif (\strlen($jump) > 2 && 'b' === $jump[1]) {
5✔
207
            $jump = bindec($jump); // binary - 0b1
1✔
208
        } elseif (\strlen($jump) > 1 && '0' === $jump[0]) {
5✔
209
            $jump = octdec($jump); // octal 01
1✔
210
        } elseif (Preg::match('#^\d+$#', $jump)) { // positive int
4✔
211
            $jump = (float) $jump; // cast to float, might be a number bigger than PHP max. int value
4✔
212
        } else {
213
            return $afterFollowingContinueIndex; // cannot process value, ignore
×
214
        }
215

216
        if ($jump > \PHP_INT_MAX) {
5✔
217
            return $afterFollowingContinueIndex; // cannot process value, ignore
×
218
        }
219

220
        $jump = (int) $jump;
5✔
221

222
        if ($isInSwitch && (1 === $jump || 0 === $jump)) {
5✔
223
            $this->replaceContinueWithBreakToken($tokens, $continueIndex); // long continue 0/1 notation
2✔
224

225
            return $afterFollowingContinueIndex;
2✔
226
        }
227

228
        $jumpDestination = $depth - $jump + 1;
5✔
229

230
        if (\in_array($jumpDestination, $this->switchLevels, true)) {
5✔
231
            $this->replaceContinueWithBreakToken($tokens, $continueIndex);
4✔
232

233
            return $afterFollowingContinueIndex;
4✔
234
        }
235

236
        return $afterFollowingContinueIndex;
2✔
237
    }
238

239
    private function replaceContinueWithBreakToken(Tokens $tokens, int $index): void
240
    {
241
        $tokens[$index] = new Token([\T_BREAK, 'break']);
10✔
242
    }
243
}
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