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

PHPCSStandards / PHP_CodeSniffer / 20876860641

10 Jan 2026 10:20AM UTC coverage: 78.904% (+0.001%) from 78.903%
20876860641

Pull #1358

github

web-flow
Merge b7cba758e into cb3ecb950
Pull Request #1358: PSR2 WrongOpenercase with colon and bracket is unclear

6 of 7 new or added lines in 1 file covered. (85.71%)

6 existing lines in 1 file now uncovered.

19831 of 25133 relevant lines covered (78.9%)

98.94 hits per line

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

95.6
/src/Standards/PSR2/Sniffs/ControlStructures/SwitchDeclarationSniff.php
1
<?php
2
/**
3
 * Ensures all switch statements are defined correctly.
4
 *
5
 * @author    Greg Sherwood <gsherwood@squiz.net>
6
 * @copyright 2006-2023 Squiz Pty Ltd (ABN 77 084 670 600)
7
 * @copyright 2023 PHPCSStandards and contributors
8
 * @license   https://github.com/PHPCSStandards/PHP_CodeSniffer/blob/HEAD/licence.txt BSD Licence
9
 */
10

11
namespace PHP_CodeSniffer\Standards\PSR2\Sniffs\ControlStructures;
12

13
use PHP_CodeSniffer\Files\File;
14
use PHP_CodeSniffer\Sniffs\Sniff;
15
use PHP_CodeSniffer\Util\Tokens;
16

17
class SwitchDeclarationSniff implements Sniff
18
{
19

20
    /**
21
     * Tokens which can terminate a "case".
22
     *
23
     * @var array<int|string, int|string>
24
     */
25
    private const CASE_TERMINATING_TOKENS = [
26
        T_RETURN   => T_RETURN,
27
        T_BREAK    => T_BREAK,
28
        T_CONTINUE => T_CONTINUE,
29
        T_THROW    => T_THROW,
30
        T_EXIT     => T_EXIT,
31
        T_GOTO     => T_GOTO,
32
    ];
33

34
    /**
35
     * The number of spaces code should be indented.
36
     *
37
     * @var integer
38
     */
39
    public $indent = 4;
40

41

42
    /**
43
     * Returns an array of tokens this test wants to listen for.
44
     *
45
     * @return array<int|string>
46
     */
47
    public function register()
3✔
48
    {
49
        return [T_SWITCH];
3✔
50
    }
51

52

53
    /**
54
     * Processes this test, when one of its tokens is encountered.
55
     *
56
     * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned.
57
     * @param int                         $stackPtr  The position of the current token in the
58
     *                                               stack passed in $tokens.
59
     *
60
     * @return void
61
     */
62
    public function process(File $phpcsFile, int $stackPtr)
3✔
63
    {
64
        $tokens = $phpcsFile->getTokens();
3✔
65

66
        // We can't process SWITCH statements unless we know where they start and end.
67
        if (isset($tokens[$stackPtr]['scope_opener']) === false
3✔
68
            || isset($tokens[$stackPtr]['scope_closer']) === false
3✔
69
        ) {
70
            return;
×
71
        }
72

73
        $switch        = $tokens[$stackPtr];
3✔
74
        $nextCase      = $stackPtr;
3✔
75
        $caseAlignment = ($switch['column'] + $this->indent);
3✔
76

77
        while (($nextCase = $this->findNextCase($phpcsFile, ($nextCase + 1), $switch['scope_closer'])) !== false) {
3✔
78
            if ($tokens[$nextCase]['code'] === T_DEFAULT) {
3✔
79
                $type = 'default';
3✔
80
            } else {
81
                $type = 'case';
3✔
82
            }
83

84
            if ($tokens[$nextCase]['content'] !== strtolower($tokens[$nextCase]['content'])) {
3✔
85
                $expected = strtolower($tokens[$nextCase]['content']);
3✔
86
                $error    = strtoupper($type) . ' keyword must be lowercase; expected "%s" but found "%s"';
3✔
87
                $data     = [
2✔
88
                    $expected,
3✔
89
                    $tokens[$nextCase]['content'],
3✔
90
                ];
2✔
91

92
                $fix = $phpcsFile->addFixableError($error, $nextCase, $type . 'NotLower', $data);
3✔
93
                if ($fix === true) {
3✔
94
                    $phpcsFile->fixer->replaceToken($nextCase, $expected);
3✔
95
                }
96
            }
97

98
            if ($type === 'case'
3✔
99
                && ($tokens[($nextCase + 1)]['code'] !== T_WHITESPACE
3✔
100
                || $tokens[($nextCase + 1)]['content'] !== ' ')
3✔
101
            ) {
102
                $error = 'CASE keyword must be followed by a single space';
3✔
103
                $fix   = $phpcsFile->addFixableError($error, $nextCase, 'SpacingAfterCase');
3✔
104
                if ($fix === true) {
3✔
105
                    if ($tokens[($nextCase + 1)]['code'] !== T_WHITESPACE) {
3✔
106
                        $phpcsFile->fixer->addContent($nextCase, ' ');
3✔
107
                    } else {
108
                        $phpcsFile->fixer->replaceToken(($nextCase + 1), ' ');
3✔
109
                    }
110
                }
111
            }
112

113
            $opener     = $tokens[$nextCase]['scope_opener'];
3✔
114
            $nextCloser = $tokens[$nextCase]['scope_closer'];
3✔
115
            if ($tokens[$opener]['code'] === T_COLON) {
3✔
116
                if ($tokens[($opener - 1)]['code'] === T_WHITESPACE) {
3✔
117
                    $error = 'There must be no space before the colon in a ' . strtoupper($type) . ' statement';
3✔
118
                    $fix   = $phpcsFile->addFixableError($error, $nextCase, 'SpaceBeforeColon' . strtoupper($type));
3✔
119
                    if ($fix === true) {
3✔
120
                        $phpcsFile->fixer->replaceToken(($opener - 1), '');
3✔
121
                    }
122
                }
123

124
                for ($next = ($opener + 1); $next < $nextCloser; $next++) {
3✔
125
                    if (isset(Tokens::EMPTY_TOKENS[$tokens[$next]['code']]) === false
3✔
126
                        || (isset(Tokens::COMMENT_TOKENS[$tokens[$next]['code']]) === true
3✔
127
                        && $tokens[$next]['line'] !== $tokens[$opener]['line'])
3✔
128
                    ) {
129
                        break;
3✔
130
                    }
131
                }
132

133
                if ($tokens[$next]['line'] !== ($tokens[$opener]['line'] + 1)) {
3✔
134
                    $error = 'The ' . strtoupper($type) . ' body must start on the line following the statement';
3✔
135
                    $fix   = $phpcsFile->addFixableError($error, $nextCase, 'BodyOnNextLine' . strtoupper($type));
3✔
136
                    if ($fix === true) {
3✔
137
                        if ($tokens[$next]['line'] === $tokens[$opener]['line']) {
3✔
138
                            $padding = str_repeat(' ', ($caseAlignment + $this->indent - 1));
3✔
139
                            $phpcsFile->fixer->addContentBefore($next, $phpcsFile->eolChar . $padding);
3✔
140
                        } else {
141
                            $phpcsFile->fixer->beginChangeset();
3✔
142
                            for ($i = ($opener + 1); $i < $next; $i++) {
3✔
143
                                if ($tokens[$i]['line'] === $tokens[$opener]['line']) {
3✔
144
                                    // Ignore trailing comments.
145
                                    continue;
3✔
146
                                }
147

148
                                if ($tokens[$i]['line'] === $tokens[$next]['line']) {
3✔
149
                                    break;
3✔
150
                                }
151

152
                                $phpcsFile->fixer->replaceToken($i, '');
3✔
153
                            }
154

155
                            $phpcsFile->fixer->endChangeset();
3✔
156
                        }
157
                    }
158
                }
159

160
                if ($tokens[$nextCloser]['scope_condition'] === $nextCase) {
3✔
161
                    // Only need to check some things once, even if the
162
                    // closer is shared between multiple case statements, or even
163
                    // the default case.
164
                    $prev = $phpcsFile->findPrevious(T_WHITESPACE, ($nextCloser - 1), $nextCase, true);
3✔
165
                    if ($tokens[$prev]['line'] === $tokens[$nextCloser]['line']) {
3✔
166
                        $error = 'Terminating statement must be on a line by itself';
3✔
167
                        $fix   = $phpcsFile->addFixableError($error, $nextCloser, 'BreakNotNewLine');
3✔
168
                        if ($fix === true) {
3✔
169
                            $phpcsFile->fixer->addNewline($prev);
3✔
170
                            $phpcsFile->fixer->replaceToken($nextCloser, trim($tokens[$nextCloser]['content']));
3✔
171
                        }
172
                    } else {
173
                        $diff = ($tokens[$nextCase]['column'] + $this->indent - $tokens[$nextCloser]['column']);
3✔
174
                        if ($diff !== 0) {
3✔
175
                            $error = 'Terminating statement must be indented to the same level as the CASE body';
3✔
176
                            $fix   = $phpcsFile->addFixableError($error, $nextCloser, 'BreakIndent');
3✔
177
                            if ($fix === true) {
3✔
178
                                if ($diff > 0) {
3✔
179
                                    $phpcsFile->fixer->addContentBefore($nextCloser, str_repeat(' ', $diff));
3✔
180
                                } else {
181
                                    $phpcsFile->fixer->substrToken(($nextCloser - 1), 0, $diff);
3✔
182
                                }
183
                            }
184
                        }
185
                    }
186
                }
187
            } else {
188
                $error = strtoupper($type) . ' statements must be defined using a colon';
3✔
189
                if ($tokens[$opener]['code'] === T_SEMICOLON || $tokens[$opener]['code'] === T_CLOSE_TAG) {
3✔
190
                    $fix = $phpcsFile->addFixableError($error, $nextCase, 'WrongOpener' . $type);
3✔
191
                    if ($fix === true) {
3✔
192
                        if ($tokens[$opener]['code'] === T_SEMICOLON) {
3✔
193
                            $phpcsFile->fixer->replaceToken($opener, ':');
3✔
194
                        } else {
195
                            $prevNonEmpty = $phpcsFile->findPrevious(T_WHITESPACE, ($opener - 1), null, true);
3✔
196
                            $phpcsFile->fixer->addContent($prevNonEmpty, ':');
3✔
197
                        }
198
                    }
199
                } else {
200
                    // Only raise error if colon is followed by a curly brace.
201
                    $prevToken = $phpcsFile->findPrevious(T_WHITESPACE, ($opener - 1), $nextCase, true);
3✔
202
                    if ($prevToken !== false
3✔
203
                        && $tokens[$prevToken]['code'] === T_COLON
3✔
204
                        && $tokens[$opener]['code'] === T_OPEN_CURLY_BRACKET
3✔
205
                    ) {
206
                        $error = '%s statements must not use a braced block after the colon';
3✔
207
                        $phpcsFile->addError($error, $nextCase, 'CaseWithBlockScope', [strtoupper($type)]);
3✔
208
                    } else {
NEW
UNCOV
209
                        $phpcsFile->addError($error, $nextCase, 'WrongOpener' . $type);
×
210
                    }
211
                }
212
            }
213

214
            // We only want cases from here on in.
215
            if ($type !== 'case') {
3✔
216
                continue;
3✔
217
            }
218

219
            $nextCode = $phpcsFile->findNext(T_WHITESPACE, ($opener + 1), $nextCloser, true);
3✔
220

221
            if ($tokens[$nextCode]['code'] !== T_CASE && $tokens[$nextCode]['code'] !== T_DEFAULT) {
3✔
222
                // This case statement has content. If the next case or default comes
223
                // before the closer, it means we don't have an obvious terminating
224
                // statement and need to make some more effort to find one. If we
225
                // don't, we do need a comment.
226
                $nextCode = $this->findNextCase($phpcsFile, ($opener + 1), $nextCloser);
3✔
227
                if ($nextCode !== false) {
3✔
228
                    $prevCode = $phpcsFile->findPrevious(T_WHITESPACE, ($nextCode - 1), $nextCase, true);
3✔
229
                    if (isset(Tokens::COMMENT_TOKENS[$tokens[$prevCode]['code']]) === false
3✔
230
                        && $this->findNestedTerminator($phpcsFile, ($opener + 1), $nextCode) === false
3✔
231
                    ) {
232
                        $error = 'There must be a comment when fall-through is intentional in a non-empty case body';
3✔
233
                        $phpcsFile->addError($error, $nextCase, 'TerminatingComment');
3✔
234
                    }
235
                }
236
            }
237
        }
238
    }
1✔
239

240

241
    /**
242
     * Find the next CASE or DEFAULT statement from a point in the file.
243
     *
244
     * Note that nested switches are ignored.
245
     *
246
     * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned.
247
     * @param int                         $stackPtr  The position to start looking at.
248
     * @param int                         $end       The position to stop looking at.
249
     *
250
     * @return int|false
251
     */
252
    private function findNextCase(File $phpcsFile, int $stackPtr, int $end)
3✔
253
    {
254
        $tokens = $phpcsFile->getTokens();
3✔
255
        while (($stackPtr = $phpcsFile->findNext([T_CASE, T_DEFAULT, T_SWITCH], $stackPtr, $end)) !== false) {
3✔
256
            // Skip nested SWITCH statements; they are handled on their own.
257
            if ($tokens[$stackPtr]['code'] === T_SWITCH) {
3✔
258
                $stackPtr = $tokens[$stackPtr]['scope_closer'];
3✔
259
                continue;
3✔
260
            }
261

262
            break;
3✔
263
        }
264

265
        return $stackPtr;
3✔
266
    }
267

268

269
    /**
270
     * Returns the position of the nested terminating statement.
271
     *
272
     * Returns false if no terminating statement was found.
273
     *
274
     * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned.
275
     * @param int                         $stackPtr  The position to start looking at.
276
     * @param int                         $end       The position to stop looking at.
277
     *
278
     * @return int|bool
279
     */
280
    private function findNestedTerminator(File $phpcsFile, int $stackPtr, int $end)
3✔
281
    {
282
        $tokens = $phpcsFile->getTokens();
3✔
283

284
        $lastToken = $phpcsFile->findPrevious(Tokens::EMPTY_TOKENS, ($end - 1), $stackPtr, true);
3✔
285
        if ($lastToken === false) {
3✔
UNCOV
286
            return false;
×
287
        }
288

289
        if ($tokens[$lastToken]['code'] === T_CLOSE_CURLY_BRACKET) {
3✔
290
            // We found a closing curly bracket and want to check if its block
291
            // belongs to a SWITCH, IF, ELSEIF or ELSE, TRY, CATCH OR FINALLY clause.
292
            // If yes, we continue searching for a terminating statement within that
293
            // block. Note that we have to make sure that every block of
294
            // the entire if/else/switch statement has a terminating statement.
295
            // For a try/catch/finally statement, either the finally block has
296
            // to have a terminating statement or every try/catch block has to have one.
297
            $currentCloser = $lastToken;
3✔
298
            $hasElseBlock  = false;
3✔
299
            $hasCatchWithoutTerminator = false;
3✔
300
            do {
301
                $scopeOpener = $tokens[$currentCloser]['scope_opener'];
3✔
302
                $scopeCloser = $tokens[$currentCloser]['scope_closer'];
3✔
303

304
                $prevToken = $phpcsFile->findPrevious(Tokens::EMPTY_TOKENS, ($scopeOpener - 1), $stackPtr, true);
3✔
305
                if ($prevToken === false) {
3✔
UNCOV
306
                    return false;
×
307
                }
308

309
                // SWITCH, IF, ELSEIF, CATCH clauses possess a condition we have to account for.
310
                if ($tokens[$prevToken]['code'] === T_CLOSE_PARENTHESIS) {
3✔
311
                    $prevToken = $tokens[$prevToken]['parenthesis_owner'];
3✔
312
                }
313

314
                if ($tokens[$prevToken]['code'] === T_IF) {
3✔
315
                    // If we have not encountered an ELSE clause by now, we cannot
316
                    // be sure that the whole statement terminates in every case.
317
                    if ($hasElseBlock === false) {
3✔
318
                        return false;
3✔
319
                    }
320

321
                    return $this->findNestedTerminator($phpcsFile, ($scopeOpener + 1), $scopeCloser);
3✔
322
                } elseif ($tokens[$prevToken]['code'] === T_ELSEIF
3✔
323
                    || $tokens[$prevToken]['code'] === T_ELSE
3✔
324
                ) {
325
                    // If we find a terminating statement within this block,
326
                    // we continue with the previous ELSEIF or IF clause.
327
                    $hasTerminator = $this->findNestedTerminator($phpcsFile, ($scopeOpener + 1), $scopeCloser);
3✔
328
                    if ($hasTerminator === false) {
3✔
329
                        return false;
3✔
330
                    }
331

332
                    $currentCloser = $phpcsFile->findPrevious(Tokens::EMPTY_TOKENS, ($prevToken - 1), $stackPtr, true);
3✔
333
                    if ($tokens[$prevToken]['code'] === T_ELSE) {
3✔
334
                        $hasElseBlock = true;
3✔
335
                    }
336
                } elseif ($tokens[$prevToken]['code'] === T_FINALLY) {
3✔
337
                    // If we find a terminating statement within this block,
338
                    // the whole try/catch/finally statement is covered.
339
                    $hasTerminator = $this->findNestedTerminator($phpcsFile, ($scopeOpener + 1), $scopeCloser);
3✔
340
                    if ($hasTerminator !== false) {
3✔
341
                        return $hasTerminator;
3✔
342
                    }
343

344
                    // Otherwise, we continue with the previous TRY or CATCH clause.
345
                    $currentCloser = $phpcsFile->findPrevious(Tokens::EMPTY_TOKENS, ($prevToken - 1), $stackPtr, true);
3✔
346
                } elseif ($tokens[$prevToken]['code'] === T_TRY) {
3✔
347
                    // If we've seen CATCH blocks without terminator statement and
348
                    // have not seen a FINALLY *with* a terminator statement, we
349
                    // don't even need to bother checking the TRY.
350
                    if ($hasCatchWithoutTerminator === true) {
3✔
351
                        return false;
3✔
352
                    }
353

354
                    return $this->findNestedTerminator($phpcsFile, ($scopeOpener + 1), $scopeCloser);
3✔
355
                } elseif ($tokens[$prevToken]['code'] === T_CATCH) {
3✔
356
                    // Keep track of seen catch statements without terminating statement,
357
                    // but don't bow out yet as there may still be a FINALLY clause
358
                    // with a terminating statement before the CATCH.
359
                    $hasTerminator = $this->findNestedTerminator($phpcsFile, ($scopeOpener + 1), $scopeCloser);
3✔
360
                    if ($hasTerminator === false) {
3✔
361
                        $hasCatchWithoutTerminator = true;
3✔
362
                    }
363

364
                    $currentCloser = $phpcsFile->findPrevious(Tokens::EMPTY_TOKENS, ($prevToken - 1), $stackPtr, true);
3✔
365
                } elseif ($tokens[$prevToken]['code'] === T_SWITCH) {
3✔
366
                    $hasDefaultBlock = false;
3✔
367
                    $endOfSwitch     = $tokens[$prevToken]['scope_closer'];
3✔
368
                    $nextCase        = $prevToken;
3✔
369

370
                    // We look for a terminating statement within every blocks.
371
                    while (($nextCase = $this->findNextCase($phpcsFile, ($nextCase + 1), $endOfSwitch)) !== false) {
3✔
372
                        if ($tokens[$nextCase]['code'] === T_DEFAULT) {
3✔
373
                            $hasDefaultBlock = true;
3✔
374
                        }
375

376
                        $opener = $tokens[$nextCase]['scope_opener'];
3✔
377

378
                        $nextCode = $phpcsFile->findNext(Tokens::EMPTY_TOKENS, ($opener + 1), $endOfSwitch, true);
3✔
379
                        if ($tokens[$nextCode]['code'] === T_CASE || $tokens[$nextCode]['code'] === T_DEFAULT) {
3✔
380
                            // This case statement has no content, so skip it.
UNCOV
381
                            continue;
×
382
                        }
383

384
                        $endOfCase = $this->findNextCase($phpcsFile, ($opener + 1), $endOfSwitch);
3✔
385
                        if ($endOfCase === false) {
3✔
386
                            $endOfCase = $endOfSwitch;
3✔
387
                        }
388

389
                        $hasTerminator = $this->findNestedTerminator($phpcsFile, ($opener + 1), $endOfCase);
3✔
390
                        if ($hasTerminator === false) {
3✔
391
                            return false;
3✔
392
                        }
393
                    }
394

395
                    // If we have not encountered a DEFAULT block by now, we cannot
396
                    // be sure that the whole statement terminates in every case.
397
                    if ($hasDefaultBlock === false) {
3✔
398
                        return false;
×
399
                    }
400

401
                    return $hasTerminator;
3✔
402
                } else {
UNCOV
403
                    return false;
×
404
                }
405
            } while ($currentCloser !== false && $tokens[$currentCloser]['code'] === T_CLOSE_CURLY_BRACKET);
3✔
406

UNCOV
407
            return true;
×
408
        } elseif ($tokens[$lastToken]['code'] === T_SEMICOLON) {
3✔
409
            // We found the last statement of the CASE. Now we want to
410
            // check whether it is a terminating one.
411
            $terminator = $phpcsFile->findStartOfStatement(($lastToken - 1));
3✔
412
            if (isset(self::CASE_TERMINATING_TOKENS[$tokens[$terminator]['code']]) === true) {
3✔
413
                return $terminator;
3✔
414
            }
415
        }
416

417
        return false;
3✔
418
    }
419
}
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