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

PHPCSStandards / PHP_CodeSniffer / 14312206404

07 Apr 2025 02:43PM UTC coverage: 78.707% (-0.001%) from 78.708%
14312206404

Pull #904

github

web-flow
Merge 386f8fa91 into 99818768c
Pull Request #904: Tests/Tokenizer: use markers for the `testSwitchDefault()` test

24855 of 31579 relevant lines covered (78.71%)

201.06 hits per line

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

96.73
/src/Standards/Squiz/Sniffs/PHP/NonExecutableCodeSniff.php
1
<?php
2
/**
3
 * Warns about code that can never been executed.
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\Squiz\Sniffs\PHP;
11

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

16
class NonExecutableCodeSniff implements Sniff
17
{
18

19
    /**
20
     * Tokens for terminating expressions, which can be used inline.
21
     *
22
     * This is in contrast to terminating statements, which cannot be used inline
23
     * and would result in a parse error (which is not the concern of this sniff).
24
     *
25
     * `throw` can be used as an expression since PHP 8.0.
26
     * {@link https://wiki.php.net/rfc/throw_expression}
27
     *
28
     * @var array
29
     */
30
    private $expressionTokens = [
31
        T_EXIT  => T_EXIT,
32
        T_THROW => T_THROW,
33
    ];
34

35

36
    /**
37
     * Returns an array of tokens this test wants to listen for.
38
     *
39
     * @return array<int|string>
40
     */
41
    public function register()
9✔
42
    {
43
        return [
3✔
44
            T_BREAK,
9✔
45
            T_CONTINUE,
9✔
46
            T_RETURN,
9✔
47
            T_THROW,
9✔
48
            T_EXIT,
9✔
49
            T_GOTO,
9✔
50
        ];
6✔
51

52
    }//end register()
53

54

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

68
        $prev = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($stackPtr - 1), null, true);
9✔
69

70
        // Tokens which can be used in inline expressions need special handling.
71
        if (isset($this->expressionTokens[$tokens[$stackPtr]['code']]) === true) {
9✔
72
            // If this token is preceded by a logical operator, it only relates to one line
73
            // and should be ignored. For example: fopen() or die().
74
            // Note: There is one exception: throw expressions can not be used with xor.
75
            if (isset(Tokens::$booleanOperators[$tokens[$prev]['code']]) === true
9✔
76
                && ($tokens[$stackPtr]['code'] === T_THROW && $tokens[$prev]['code'] === T_LOGICAL_XOR) === false
9✔
77
            ) {
3✔
78
                return;
9✔
79
            }
80

81
            // Expressions are allowed in the `else` clause of ternaries.
82
            if ($tokens[$prev]['code'] === T_INLINE_THEN || $tokens[$prev]['code'] === T_INLINE_ELSE) {
9✔
83
                return;
9✔
84
            }
85

86
            // Expressions are allowed with PHP 7.0+ null coalesce and PHP 7.4+ null coalesce equals.
87
            if ($tokens[$prev]['code'] === T_COALESCE || $tokens[$prev]['code'] === T_COALESCE_EQUAL) {
9✔
88
                return;
9✔
89
            }
90

91
            // Expressions are allowed in arrow functions.
92
            if ($tokens[$prev]['code'] === T_FN_ARROW) {
9✔
93
                return;
9✔
94
            }
95
        }//end if
3✔
96

97
        // This token may be part of an inline condition.
98
        // If we find a closing parenthesis that belongs to a condition,
99
        // or an "else", we should ignore this token.
100
        if ($tokens[$prev]['code'] === T_ELSE
9✔
101
            || (isset($tokens[$prev]['parenthesis_owner']) === true
9✔
102
            && ($tokens[$tokens[$prev]['parenthesis_owner']]['code'] === T_IF
9✔
103
            || $tokens[$tokens[$prev]['parenthesis_owner']]['code'] === T_ELSEIF))
9✔
104
        ) {
3✔
105
            return;
9✔
106
        }
107

108
        if ($tokens[$stackPtr]['code'] === T_RETURN) {
9✔
109
            $next = $phpcsFile->findNext(Tokens::$emptyTokens, ($stackPtr + 1), null, true);
9✔
110
            if ($tokens[$next]['code'] === T_SEMICOLON) {
9✔
111
                $next = $phpcsFile->findNext(Tokens::$emptyTokens, ($next + 1), null, true);
9✔
112
                if ($tokens[$next]['code'] === T_CLOSE_CURLY_BRACKET) {
9✔
113
                    // If this is the closing brace of a function
114
                    // then this return statement doesn't return anything
115
                    // and is not required anyway.
116
                    $owner = $tokens[$next]['scope_condition'];
9✔
117
                    if ($tokens[$owner]['code'] === T_FUNCTION
9✔
118
                        || $tokens[$owner]['code'] === T_CLOSURE
9✔
119
                    ) {
3✔
120
                        $warning = 'Empty return statement not required here';
9✔
121
                        $phpcsFile->addWarning($warning, $stackPtr, 'ReturnNotRequired');
9✔
122
                        return;
9✔
123
                    }
124
                }
3✔
125
            }
3✔
126
        }
3✔
127

128
        if (isset($tokens[$stackPtr]['scope_opener']) === true) {
9✔
129
            $owner = $tokens[$stackPtr]['scope_condition'];
9✔
130
            if ($tokens[$owner]['code'] === T_CASE || $tokens[$owner]['code'] === T_DEFAULT) {
9✔
131
                // This token closes the scope of a CASE or DEFAULT statement
132
                // so any code between this statement and the next CASE, DEFAULT or
133
                // end of SWITCH token will not be executable.
134
                $end  = $phpcsFile->findEndOfStatement($stackPtr);
9✔
135
                $next = $phpcsFile->findNext(
9✔
136
                    [
3✔
137
                        T_CASE,
9✔
138
                        T_DEFAULT,
9✔
139
                        T_CLOSE_CURLY_BRACKET,
9✔
140
                        T_ENDSWITCH,
9✔
141
                    ],
6✔
142
                    ($end + 1)
9✔
143
                );
6✔
144

145
                if ($next !== false) {
9✔
146
                    $lastLine = $tokens[$end]['line'];
9✔
147
                    for ($i = ($stackPtr + 1); $i < $next; $i++) {
9✔
148
                        if (isset(Tokens::$emptyTokens[$tokens[$i]['code']]) === true) {
9✔
149
                            continue;
9✔
150
                        }
151

152
                        $line = $tokens[$i]['line'];
9✔
153
                        if ($line > $lastLine) {
9✔
154
                            $type    = substr($tokens[$stackPtr]['type'], 2);
9✔
155
                            $warning = 'Code after the %s statement on line %s cannot be executed';
9✔
156
                            $data    = [
3✔
157
                                $type,
9✔
158
                                $tokens[$stackPtr]['line'],
9✔
159
                            ];
6✔
160
                            $phpcsFile->addWarning($warning, $i, 'Unreachable', $data);
9✔
161
                            $lastLine = $line;
9✔
162
                        }
3✔
163
                    }
3✔
164
                }//end if
3✔
165

166
                // That's all we have to check for these types of statements.
167
                return;
9✔
168
            }//end if
169
        }//end if
170

171
        $ourConditions = array_keys($tokens[$stackPtr]['conditions']);
9✔
172

173
        if (empty($ourConditions) === false) {
9✔
174
            $condition = array_pop($ourConditions);
9✔
175

176
            if (isset($tokens[$condition]['scope_closer']) === false) {
9✔
177
                return;
×
178
            }
179

180
            // Special case for BREAK statements sitting directly inside SWITCH
181
            // statements. If we get to this point, we know the BREAK is not being
182
            // used to close a CASE statement, so it is most likely non-executable
183
            // code itself (as is the case when you put return; break; at the end of
184
            // a case). So we need to ignore this token.
185
            if ($tokens[$condition]['code'] === T_SWITCH
9✔
186
                && $tokens[$stackPtr]['code'] === T_BREAK
9✔
187
            ) {
3✔
188
                return;
9✔
189
            }
190

191
            $closer = $tokens[$condition]['scope_closer'];
9✔
192

193
            // If the closer for our condition is shared with other openers,
194
            // we will need to throw errors from this token to the next
195
            // shared opener (if there is one), not to the scope closer.
196
            $nextOpener = null;
9✔
197
            for ($i = ($stackPtr + 1); $i < $closer; $i++) {
9✔
198
                if (isset($tokens[$i]['scope_closer']) === true) {
9✔
199
                    if ($tokens[$i]['scope_closer'] === $closer) {
9✔
200
                        // We found an opener that shares the same
201
                        // closing token as us.
202
                        $nextOpener = $i;
×
203
                        break;
×
204
                    }
205
                }
3✔
206
            }//end for
3✔
207

208
            if ($nextOpener === null) {
9✔
209
                $end = $closer;
9✔
210
            } else {
3✔
211
                $end = ($nextOpener - 1);
3✔
212
            }
213
        } else {
3✔
214
            // This token is in the global scope.
215
            if ($tokens[$stackPtr]['code'] === T_BREAK) {
9✔
216
                return;
×
217
            }
218

219
            // Throw an error for all lines until the end of the file.
220
            $end = ($phpcsFile->numTokens - 1);
9✔
221
        }//end if
222

223
        // Find the semicolon or closing PHP tag that ends this statement,
224
        // skipping nested statements like FOR loops and closures.
225
        for ($start = ($stackPtr + 1); $start < $phpcsFile->numTokens; $start++) {
9✔
226
            if ($start === $end) {
9✔
227
                break;
9✔
228
            }
229

230
            if (isset($tokens[$start]['parenthesis_closer']) === true
9✔
231
                && $tokens[$start]['code'] === T_OPEN_PARENTHESIS
9✔
232
            ) {
3✔
233
                $start = $tokens[$start]['parenthesis_closer'];
9✔
234
                continue;
9✔
235
            }
236

237
            if (isset($tokens[$start]['bracket_closer']) === true
9✔
238
                && $tokens[$start]['code'] === T_OPEN_CURLY_BRACKET
9✔
239
            ) {
3✔
240
                $start = $tokens[$start]['bracket_closer'];
9✔
241
                continue;
9✔
242
            }
243

244
            if ($tokens[$start]['code'] === T_SEMICOLON || $tokens[$start]['code'] === T_CLOSE_TAG) {
9✔
245
                break;
9✔
246
            }
247
        }//end for
3✔
248

249
        if (isset($tokens[$start]) === false) {
9✔
250
            return;
×
251
        }
252

253
        $lastLine = $tokens[$start]['line'];
9✔
254
        for ($i = ($start + 1); $i < $end; $i++) {
9✔
255
            if (isset(Tokens::$emptyTokens[$tokens[$i]['code']]) === true
9✔
256
                || isset(Tokens::$bracketTokens[$tokens[$i]['code']]) === true
9✔
257
                || $tokens[$i]['code'] === T_SEMICOLON
9✔
258
            ) {
3✔
259
                continue;
9✔
260
            }
261

262
            // Skip whole functions and classes/interfaces because they are not
263
            // technically executed code, but rather declarations that may be used.
264
            if (isset(Tokens::$ooScopeTokens[$tokens[$i]['code']]) === true
9✔
265
                || $tokens[$i]['code'] === T_FUNCTION
9✔
266
                || $tokens[$i]['code'] === T_CLOSURE
9✔
267
            ) {
3✔
268
                if (isset($tokens[$i]['scope_closer']) === false) {
9✔
269
                    // Parse error/Live coding.
270
                    return;
9✔
271
                }
272

273
                $i = $tokens[$i]['scope_closer'];
9✔
274
                continue;
9✔
275
            }
276

277
            // Skip HTML whitespace.
278
            if ($tokens[$i]['code'] === T_INLINE_HTML && trim($tokens[$i]['content']) === '') {
9✔
279
                continue;
9✔
280
            }
281

282
            // Skip PHP re-open tag (eg, after inline HTML).
283
            if ($tokens[$i]['code'] === T_OPEN_TAG) {
9✔
284
                continue;
9✔
285
            }
286

287
            $line = $tokens[$i]['line'];
9✔
288
            if ($line > $lastLine) {
9✔
289
                $type    = substr($tokens[$stackPtr]['type'], 2);
9✔
290
                $warning = 'Code after the %s statement on line %s cannot be executed';
9✔
291
                $data    = [
3✔
292
                    $type,
9✔
293
                    $tokens[$stackPtr]['line'],
9✔
294
                ];
6✔
295
                $phpcsFile->addWarning($warning, $i, 'Unreachable', $data);
9✔
296
                $lastLine = $line;
9✔
297
            }
3✔
298
        }//end for
3✔
299

300
    }//end process()
6✔
301

302

303
}//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

© 2025 Coveralls, Inc