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

PHPCSStandards / PHP_CodeSniffer / 16808529167

07 Aug 2025 03:14PM UTC coverage: 78.557% (+0.1%) from 78.441%
16808529167

Pull #1126

github

web-flow
Merge 98b197449 into 501c14723
Pull Request #1126: fix: Check that the result of pcntl_waitpid is the PID of a managed process

0 of 1 new or added line in 1 file covered. (0.0%)

93 existing lines in 4 files now uncovered.

25260 of 32155 relevant lines covered (78.56%)

69.67 hits per line

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

96.8
/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-2015 Squiz Pty Ltd (ABN 77 084 670 600)
7
 * @license   https://github.com/PHPCSStandards/PHP_CodeSniffer/blob/master/licence.txt BSD Licence
8
 */
9

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

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

16
class SwitchDeclarationSniff implements Sniff
17
{
18

19
    /**
20
     * The number of spaces code should be indented.
21
     *
22
     * @var integer
23
     */
24
    public $indent = 4;
25

26

27
    /**
28
     * Returns an array of tokens this test wants to listen for.
29
     *
30
     * @return array<int|string>
31
     */
32
    public function register()
3✔
33
    {
34
        return [T_SWITCH];
3✔
35

36
    }//end register()
37

38

39
    /**
40
     * Processes this test, when one of its tokens is encountered.
41
     *
42
     * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned.
43
     * @param int                         $stackPtr  The position of the current token in the
44
     *                                               stack passed in $tokens.
45
     *
46
     * @return void
47
     */
48
    public function process(File $phpcsFile, $stackPtr)
3✔
49
    {
50
        $tokens = $phpcsFile->getTokens();
3✔
51

52
        // We can't process SWITCH statements unless we know where they start and end.
53
        if (isset($tokens[$stackPtr]['scope_opener']) === false
3✔
54
            || isset($tokens[$stackPtr]['scope_closer']) === false
3✔
55
        ) {
1✔
56
            return;
×
57
        }
58

59
        $switch        = $tokens[$stackPtr];
3✔
60
        $nextCase      = $stackPtr;
3✔
61
        $caseAlignment = ($switch['column'] + $this->indent);
3✔
62

63
        while (($nextCase = $this->findNextCase($phpcsFile, ($nextCase + 1), $switch['scope_closer'])) !== false) {
3✔
64
            if ($tokens[$nextCase]['code'] === T_DEFAULT) {
3✔
65
                $type = 'default';
3✔
66
            } else {
1✔
67
                $type = 'case';
3✔
68
            }
69

70
            if ($tokens[$nextCase]['content'] !== strtolower($tokens[$nextCase]['content'])) {
3✔
71
                $expected = strtolower($tokens[$nextCase]['content']);
3✔
72
                $error    = strtoupper($type).' keyword must be lowercase; expected "%s" but found "%s"';
3✔
73
                $data     = [
1✔
74
                    $expected,
3✔
75
                    $tokens[$nextCase]['content'],
3✔
76
                ];
2✔
77

78
                $fix = $phpcsFile->addFixableError($error, $nextCase, $type.'NotLower', $data);
3✔
79
                if ($fix === true) {
3✔
80
                    $phpcsFile->fixer->replaceToken($nextCase, $expected);
3✔
81
                }
1✔
82
            }
1✔
83

84
            if ($type === 'case'
2✔
85
                && ($tokens[($nextCase + 1)]['code'] !== T_WHITESPACE
3✔
86
                || $tokens[($nextCase + 1)]['content'] !== ' ')
3✔
87
            ) {
1✔
88
                $error = 'CASE keyword must be followed by a single space';
3✔
89
                $fix   = $phpcsFile->addFixableError($error, $nextCase, 'SpacingAfterCase');
3✔
90
                if ($fix === true) {
3✔
91
                    if ($tokens[($nextCase + 1)]['code'] !== T_WHITESPACE) {
3✔
92
                        $phpcsFile->fixer->addContent($nextCase, ' ');
3✔
93
                    } else {
1✔
94
                        $phpcsFile->fixer->replaceToken(($nextCase + 1), ' ');
3✔
95
                    }
96
                }
1✔
97
            }
1✔
98

99
            $opener     = $tokens[$nextCase]['scope_opener'];
3✔
100
            $nextCloser = $tokens[$nextCase]['scope_closer'];
3✔
101
            if ($tokens[$opener]['code'] === T_COLON) {
3✔
102
                if ($tokens[($opener - 1)]['code'] === T_WHITESPACE) {
3✔
103
                    $error = 'There must be no space before the colon in a '.strtoupper($type).' statement';
3✔
104
                    $fix   = $phpcsFile->addFixableError($error, $nextCase, 'SpaceBeforeColon'.strtoupper($type));
3✔
105
                    if ($fix === true) {
3✔
106
                        $phpcsFile->fixer->replaceToken(($opener - 1), '');
3✔
107
                    }
1✔
108
                }
1✔
109

110
                for ($next = ($opener + 1); $next < $nextCloser; $next++) {
3✔
111
                    if (isset(Tokens::$emptyTokens[$tokens[$next]['code']]) === false
3✔
112
                        || (isset(Tokens::$commentTokens[$tokens[$next]['code']]) === true
3✔
113
                        && $tokens[$next]['line'] !== $tokens[$opener]['line'])
3✔
114
                    ) {
1✔
115
                        break;
3✔
116
                    }
117
                }
1✔
118

119
                if ($tokens[$next]['line'] !== ($tokens[$opener]['line'] + 1)) {
3✔
120
                    $error = 'The '.strtoupper($type).' body must start on the line following the statement';
3✔
121
                    $fix   = $phpcsFile->addFixableError($error, $nextCase, 'BodyOnNextLine'.strtoupper($type));
3✔
122
                    if ($fix === true) {
3✔
123
                        if ($tokens[$next]['line'] === $tokens[$opener]['line']) {
3✔
124
                            $padding = str_repeat(' ', ($caseAlignment + $this->indent - 1));
3✔
125
                            $phpcsFile->fixer->addContentBefore($next, $phpcsFile->eolChar.$padding);
3✔
126
                        } else {
1✔
127
                            $phpcsFile->fixer->beginChangeset();
3✔
128
                            for ($i = ($opener + 1); $i < $next; $i++) {
3✔
129
                                if ($tokens[$i]['line'] === $tokens[$opener]['line']) {
3✔
130
                                    // Ignore trailing comments.
131
                                    continue;
3✔
132
                                }
133

134
                                if ($tokens[$i]['line'] === $tokens[$next]['line']) {
3✔
135
                                    break;
3✔
136
                                }
137

138
                                $phpcsFile->fixer->replaceToken($i, '');
3✔
139
                            }
1✔
140

141
                            $phpcsFile->fixer->endChangeset();
3✔
142
                        }
143
                    }//end if
1✔
144
                }//end if
1✔
145

146
                if ($tokens[$nextCloser]['scope_condition'] === $nextCase) {
3✔
147
                    // Only need to check some things once, even if the
148
                    // closer is shared between multiple case statements, or even
149
                    // the default case.
150
                    $prev = $phpcsFile->findPrevious(T_WHITESPACE, ($nextCloser - 1), $nextCase, true);
3✔
151
                    if ($tokens[$prev]['line'] === $tokens[$nextCloser]['line']) {
3✔
152
                        $error = 'Terminating statement must be on a line by itself';
3✔
153
                        $fix   = $phpcsFile->addFixableError($error, $nextCloser, 'BreakNotNewLine');
3✔
154
                        if ($fix === true) {
3✔
155
                            $phpcsFile->fixer->addNewline($prev);
3✔
156
                            $phpcsFile->fixer->replaceToken($nextCloser, trim($tokens[$nextCloser]['content']));
3✔
157
                        }
1✔
158
                    } else {
1✔
159
                        $diff = ($tokens[$nextCase]['column'] + $this->indent - $tokens[$nextCloser]['column']);
3✔
160
                        if ($diff !== 0) {
3✔
161
                            $error = 'Terminating statement must be indented to the same level as the CASE body';
3✔
162
                            $fix   = $phpcsFile->addFixableError($error, $nextCloser, 'BreakIndent');
3✔
163
                            if ($fix === true) {
3✔
164
                                if ($diff > 0) {
3✔
165
                                    $phpcsFile->fixer->addContentBefore($nextCloser, str_repeat(' ', $diff));
3✔
166
                                } else {
1✔
167
                                    $phpcsFile->fixer->substrToken(($nextCloser - 1), 0, $diff);
3✔
168
                                }
169
                            }
1✔
170
                        }
1✔
171
                    }//end if
172
                }//end if
1✔
173
            } else {
1✔
174
                $error = strtoupper($type).' statements must be defined using a colon';
3✔
175
                if ($tokens[$opener]['code'] === T_SEMICOLON) {
3✔
176
                    $fix = $phpcsFile->addFixableError($error, $nextCase, 'WrongOpener'.$type);
3✔
177
                    if ($fix === true) {
3✔
178
                        $phpcsFile->fixer->replaceToken($opener, ':');
3✔
179
                    }
1✔
180
                } else {
1✔
181
                    // Probably a case/default statement with colon + curly braces.
182
                    $phpcsFile->addError($error, $nextCase, 'WrongOpener'.$type);
3✔
183
                }
184
            }//end if
185

186
            // We only want cases from here on in.
187
            if ($type !== 'case') {
3✔
188
                continue;
3✔
189
            }
190

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

193
            if ($tokens[$nextCode]['code'] !== T_CASE && $tokens[$nextCode]['code'] !== T_DEFAULT) {
3✔
194
                // This case statement has content. If the next case or default comes
195
                // before the closer, it means we don't have an obvious terminating
196
                // statement and need to make some more effort to find one. If we
197
                // don't, we do need a comment.
198
                $nextCode = $this->findNextCase($phpcsFile, ($opener + 1), $nextCloser);
3✔
199
                if ($nextCode !== false) {
3✔
200
                    $prevCode = $phpcsFile->findPrevious(T_WHITESPACE, ($nextCode - 1), $nextCase, true);
3✔
201
                    if (isset(Tokens::$commentTokens[$tokens[$prevCode]['code']]) === false
3✔
202
                        && $this->findNestedTerminator($phpcsFile, ($opener + 1), $nextCode) === false
3✔
203
                    ) {
1✔
204
                        $error = 'There must be a comment when fall-through is intentional in a non-empty case body';
3✔
205
                        $phpcsFile->addError($error, $nextCase, 'TerminatingComment');
3✔
206
                    }
1✔
207
                }
1✔
208
            }
1✔
209
        }//end while
1✔
210

211
    }//end process()
2✔
212

213

214
    /**
215
     * Find the next CASE or DEFAULT statement from a point in the file.
216
     *
217
     * Note that nested switches are ignored.
218
     *
219
     * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned.
220
     * @param int                         $stackPtr  The position to start looking at.
221
     * @param int                         $end       The position to stop looking at.
222
     *
223
     * @return int|false
224
     */
225
    private function findNextCase($phpcsFile, $stackPtr, $end)
3✔
226
    {
227
        $tokens = $phpcsFile->getTokens();
3✔
228
        while (($stackPtr = $phpcsFile->findNext([T_CASE, T_DEFAULT, T_SWITCH], $stackPtr, $end)) !== false) {
3✔
229
            // Skip nested SWITCH statements; they are handled on their own.
230
            if ($tokens[$stackPtr]['code'] === T_SWITCH) {
3✔
231
                $stackPtr = $tokens[$stackPtr]['scope_closer'];
3✔
232
                continue;
3✔
233
            }
234

235
            break;
3✔
236
        }
237

238
        return $stackPtr;
3✔
239

240
    }//end findNextCase()
241

242

243
    /**
244
     * Returns the position of the nested terminating statement.
245
     *
246
     * Returns false if no terminating statement was found.
247
     *
248
     * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned.
249
     * @param int                         $stackPtr  The position to start looking at.
250
     * @param int                         $end       The position to stop looking at.
251
     *
252
     * @return int|bool
253
     */
254
    private function findNestedTerminator($phpcsFile, $stackPtr, $end)
3✔
255
    {
256
        $tokens = $phpcsFile->getTokens();
3✔
257

258
        $lastToken = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($end - 1), $stackPtr, true);
3✔
259
        if ($lastToken === false) {
3✔
UNCOV
260
            return false;
×
261
        }
262

263
        if ($tokens[$lastToken]['code'] === T_CLOSE_CURLY_BRACKET) {
3✔
264
            // We found a closing curly bracket and want to check if its block
265
            // belongs to a SWITCH, IF, ELSEIF or ELSE, TRY, CATCH OR FINALLY clause.
266
            // If yes, we continue searching for a terminating statement within that
267
            // block. Note that we have to make sure that every block of
268
            // the entire if/else/switch statement has a terminating statement.
269
            // For a try/catch/finally statement, either the finally block has
270
            // to have a terminating statement or every try/catch block has to have one.
271
            $currentCloser = $lastToken;
3✔
272
            $hasElseBlock  = false;
3✔
273
            $hasCatchWithoutTerminator = false;
3✔
274
            do {
275
                $scopeOpener = $tokens[$currentCloser]['scope_opener'];
3✔
276
                $scopeCloser = $tokens[$currentCloser]['scope_closer'];
3✔
277

278
                $prevToken = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($scopeOpener - 1), $stackPtr, true);
3✔
279
                if ($prevToken === false) {
3✔
UNCOV
280
                    return false;
×
281
                }
282

283
                // SWITCH, IF, ELSEIF, CATCH clauses possess a condition we have to account for.
284
                if ($tokens[$prevToken]['code'] === T_CLOSE_PARENTHESIS) {
3✔
285
                    $prevToken = $tokens[$prevToken]['parenthesis_owner'];
3✔
286
                }
1✔
287

288
                if ($tokens[$prevToken]['code'] === T_IF) {
3✔
289
                    // If we have not encountered an ELSE clause by now, we cannot
290
                    // be sure that the whole statement terminates in every case.
291
                    if ($hasElseBlock === false) {
3✔
292
                        return false;
3✔
293
                    }
294

295
                    return $this->findNestedTerminator($phpcsFile, ($scopeOpener + 1), $scopeCloser);
3✔
296
                } else if ($tokens[$prevToken]['code'] === T_ELSEIF
3✔
297
                    || $tokens[$prevToken]['code'] === T_ELSE
3✔
298
                ) {
1✔
299
                    // If we find a terminating statement within this block,
300
                    // we continue with the previous ELSEIF or IF clause.
301
                    $hasTerminator = $this->findNestedTerminator($phpcsFile, ($scopeOpener + 1), $scopeCloser);
3✔
302
                    if ($hasTerminator === false) {
3✔
303
                        return false;
3✔
304
                    }
305

306
                    $currentCloser = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($prevToken - 1), $stackPtr, true);
3✔
307
                    if ($tokens[$prevToken]['code'] === T_ELSE) {
3✔
308
                        $hasElseBlock = true;
3✔
309
                    }
1✔
310
                } else if ($tokens[$prevToken]['code'] === T_FINALLY) {
3✔
311
                    // If we find a terminating statement within this block,
312
                    // the whole try/catch/finally statement is covered.
313
                    $hasTerminator = $this->findNestedTerminator($phpcsFile, ($scopeOpener + 1), $scopeCloser);
3✔
314
                    if ($hasTerminator !== false) {
3✔
315
                        return $hasTerminator;
3✔
316
                    }
317

318
                    // Otherwise, we continue with the previous TRY or CATCH clause.
319
                    $currentCloser = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($prevToken - 1), $stackPtr, true);
3✔
320
                } else if ($tokens[$prevToken]['code'] === T_TRY) {
3✔
321
                    // If we've seen CATCH blocks without terminator statement and
322
                    // have not seen a FINALLY *with* a terminator statement, we
323
                    // don't even need to bother checking the TRY.
324
                    if ($hasCatchWithoutTerminator === true) {
3✔
325
                        return false;
3✔
326
                    }
327

328
                    return $this->findNestedTerminator($phpcsFile, ($scopeOpener + 1), $scopeCloser);
3✔
329
                } else if ($tokens[$prevToken]['code'] === T_CATCH) {
3✔
330
                    // Keep track of seen catch statements without terminating statement,
331
                    // but don't bow out yet as there may still be a FINALLY clause
332
                    // with a terminating statement before the CATCH.
333
                    $hasTerminator = $this->findNestedTerminator($phpcsFile, ($scopeOpener + 1), $scopeCloser);
3✔
334
                    if ($hasTerminator === false) {
3✔
335
                        $hasCatchWithoutTerminator = true;
3✔
336
                    }
1✔
337

338
                    $currentCloser = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($prevToken - 1), $stackPtr, true);
3✔
339
                } else if ($tokens[$prevToken]['code'] === T_SWITCH) {
3✔
340
                    $hasDefaultBlock = false;
3✔
341
                    $endOfSwitch     = $tokens[$prevToken]['scope_closer'];
3✔
342
                    $nextCase        = $prevToken;
3✔
343

344
                    // We look for a terminating statement within every blocks.
345
                    while (($nextCase = $this->findNextCase($phpcsFile, ($nextCase + 1), $endOfSwitch)) !== false) {
3✔
346
                        if ($tokens[$nextCase]['code'] === T_DEFAULT) {
3✔
347
                            $hasDefaultBlock = true;
3✔
348
                        }
1✔
349

350
                        $opener = $tokens[$nextCase]['scope_opener'];
3✔
351

352
                        $nextCode = $phpcsFile->findNext(Tokens::$emptyTokens, ($opener + 1), $endOfSwitch, true);
3✔
353
                        if ($tokens[$nextCode]['code'] === T_CASE || $tokens[$nextCode]['code'] === T_DEFAULT) {
3✔
354
                            // This case statement has no content, so skip it.
UNCOV
355
                            continue;
×
356
                        }
357

358
                        $endOfCase = $this->findNextCase($phpcsFile, ($opener + 1), $endOfSwitch);
3✔
359
                        if ($endOfCase === false) {
3✔
360
                            $endOfCase = $endOfSwitch;
3✔
361
                        }
1✔
362

363
                        $hasTerminator = $this->findNestedTerminator($phpcsFile, ($opener + 1), $endOfCase);
3✔
364
                        if ($hasTerminator === false) {
3✔
365
                            return false;
3✔
366
                        }
367
                    }//end while
1✔
368

369
                    // If we have not encountered a DEFAULT block by now, we cannot
370
                    // be sure that the whole statement terminates in every case.
371
                    if ($hasDefaultBlock === false) {
3✔
UNCOV
372
                        return false;
×
373
                    }
374

375
                    return $hasTerminator;
3✔
376
                } else {
UNCOV
377
                    return false;
×
378
                }//end if
379
            } while ($currentCloser !== false && $tokens[$currentCloser]['code'] === T_CLOSE_CURLY_BRACKET);
3✔
380

UNCOV
381
            return true;
×
382
        } else if ($tokens[$lastToken]['code'] === T_SEMICOLON) {
3✔
383
            // We found the last statement of the CASE. Now we want to
384
            // check whether it is a terminating one.
385
            $terminators = [
1✔
386
                T_RETURN   => T_RETURN,
3✔
387
                T_BREAK    => T_BREAK,
3✔
388
                T_CONTINUE => T_CONTINUE,
3✔
389
                T_THROW    => T_THROW,
3✔
390
                T_EXIT     => T_EXIT,
3✔
391
                T_GOTO     => T_GOTO,
3✔
392
            ];
2✔
393

394
            $terminator = $phpcsFile->findStartOfStatement(($lastToken - 1));
3✔
395
            if (isset($terminators[$tokens[$terminator]['code']]) === true) {
3✔
396
                return $terminator;
3✔
397
            }
398
        }//end if
1✔
399

400
        return false;
3✔
401

402
    }//end findNestedTerminator()
403

404

405
}//end class
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