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

keradus / PHP-CS-Fixer / 17377459942

01 Sep 2025 12:19PM UTC coverage: 94.684% (-0.009%) from 94.693%
17377459942

push

github

web-flow
chore: `Tokens::offsetSet` - explicit validation of input (#9004)

1 of 5 new or added lines in 1 file covered. (20.0%)

306 existing lines in 60 files now uncovered.

28390 of 29984 relevant lines covered (94.68%)

45.5 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
                        <?php
43
                        switch ($foo) {
44
                            case 1:
45
                                continue;
46
                        }
47

48
                        PHP
3✔
49
                ),
3✔
50
                new CodeSample(
3✔
51
                    <<<'PHP'
3✔
52
                        <?php
53
                        switch ($foo) {
54
                            case 1:
55
                                while($bar) {
56
                                    do {
57
                                        continue 3;
58
                                    } while(false);
59

60
                                    if ($foo + 1 > 3) {
61
                                        continue;
62
                                    }
63

64
                                    continue 2;
65
                                }
66
                        }
67

68
                        PHP
3✔
69
                ),
3✔
70
            ]
3✔
71
        );
3✔
72
    }
73

74
    /**
75
     * {@inheritdoc}
76
     *
77
     * Must run after NoAlternativeSyntaxFixer.
78
     */
79
    public function getPriority(): int
80
    {
81
        return 0;
1✔
82
    }
83

84
    public function isCandidate(Tokens $tokens): bool
85
    {
86
        return $tokens->isAllTokenKindsFound([\T_SWITCH, \T_CONTINUE]) && !$tokens->hasAlternativeSyntax();
13✔
87
    }
88

89
    protected function applyFix(\SplFileInfo $file, Tokens $tokens): void
90
    {
91
        $count = \count($tokens);
11✔
92

93
        for ($index = 1; $index < $count - 1; ++$index) {
11✔
94
            $index = $this->doFix($tokens, $index, 0, false);
11✔
95
        }
96
    }
97

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

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

111
            if (!$tokens[$index]->equals('{')) {
9✔
112
                return $index;
4✔
113
            }
114

115
            return $this->fixInLoop($tokens, $index, $depth + 1);
7✔
116
        }
117

118
        if ($token->isGivenKind(\T_DO)) {
11✔
119
            return $this->fixInLoop($tokens, $tokens->getNextTokenOfKind($index, ['{']), $depth + 1);
3✔
120
        }
121

122
        if ($token->isGivenKind(\T_SWITCH)) {
11✔
123
            return $this->fixInSwitch($tokens, $index, $depth + 1);
11✔
124
        }
125

126
        if ($token->isGivenKind(\T_CONTINUE)) {
11✔
127
            return $this->fixContinueWhenActsAsBreak($tokens, $index, $isInSwitch, $depth);
11✔
128
        }
129

130
        return $index;
11✔
131
    }
132

133
    private function fixInSwitch(Tokens $tokens, int $switchIndex, int $depth): int
134
    {
135
        $this->switchLevels[] = $depth;
11✔
136

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

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

143
        for ($index = $openIndex + 1; $index < $closeIndex; ++$index) {
11✔
144
            $index = $this->doFix($tokens, $index, $depth, true);
11✔
145
        }
146

147
        array_pop($this->switchLevels);
11✔
148

149
        return $closeIndex;
11✔
150
    }
151

152
    private function fixInLoop(Tokens $tokens, int $openIndex, int $depth): int
153
    {
154
        $openCount = 1;
8✔
155

156
        while (true) {
8✔
157
            ++$openIndex;
8✔
158
            $token = $tokens[$openIndex];
8✔
159

160
            if ($token->equals('{')) {
8✔
161
                ++$openCount;
2✔
162

163
                continue;
2✔
164
            }
165

166
            if ($token->equals('}')) {
8✔
167
                --$openCount;
8✔
168

169
                if (0 === $openCount) {
8✔
170
                    break;
8✔
171
                }
172

173
                continue;
2✔
174
            }
175

176
            $openIndex = $this->doFix($tokens, $openIndex, $depth, false);
8✔
177
        }
178

179
        return $openIndex;
8✔
180
    }
181

182
    private function fixContinueWhenActsAsBreak(Tokens $tokens, int $continueIndex, bool $isInSwitch, int $depth): int
183
    {
184
        $followingContinueIndex = $tokens->getNextMeaningfulToken($continueIndex);
11✔
185
        $followingContinueToken = $tokens[$followingContinueIndex];
11✔
186

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

190
            return $followingContinueIndex;
6✔
191
        }
192

193
        if (!$followingContinueToken->isGivenKind(\T_LNUMBER)) {
8✔
194
            return $followingContinueIndex;
5✔
195
        }
196

197
        $afterFollowingContinueIndex = $tokens->getNextMeaningfulToken($followingContinueIndex);
6✔
198

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

203
        // check if continue {jump} targets a switch statement and if so fix it
204

205
        $jump = $followingContinueToken->getContent();
5✔
206
        $jump = str_replace('_', '', $jump); // support for numeric_literal_separator
5✔
207

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

220
        if ($jump > \PHP_INT_MAX) {
5✔
UNCOV
221
            return $afterFollowingContinueIndex; // cannot process value, ignore
×
222
        }
223

224
        $jump = (int) $jump;
5✔
225

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

229
            return $afterFollowingContinueIndex;
2✔
230
        }
231

232
        $jumpDestination = $depth - $jump + 1;
5✔
233

234
        if (\in_array($jumpDestination, $this->switchLevels, true)) {
5✔
235
            $this->replaceContinueWithBreakToken($tokens, $continueIndex);
4✔
236

237
            return $afterFollowingContinueIndex;
4✔
238
        }
239

240
        return $afterFollowingContinueIndex;
2✔
241
    }
242

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