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

PHPCSStandards / PHP_CodeSniffer / 14309561950

07 Apr 2025 12:46PM UTC coverage: 78.707% (-0.001%) from 78.708%
14309561950

Pull #904

github

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

24855 of 31579 relevant lines covered (78.71%)

67.02 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()
3✔
42
    {
43
        return [
1✔
44
            T_BREAK,
3✔
45
            T_CONTINUE,
3✔
46
            T_RETURN,
3✔
47
            T_THROW,
3✔
48
            T_EXIT,
3✔
49
            T_GOTO,
3✔
50
        ];
2✔
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)
3✔
65
    {
66
        $tokens = $phpcsFile->getTokens();
3✔
67

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

70
        // Tokens which can be used in inline expressions need special handling.
71
        if (isset($this->expressionTokens[$tokens[$stackPtr]['code']]) === true) {
3✔
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
3✔
76
                && ($tokens[$stackPtr]['code'] === T_THROW && $tokens[$prev]['code'] === T_LOGICAL_XOR) === false
3✔
77
            ) {
1✔
78
                return;
3✔
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) {
3✔
83
                return;
3✔
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) {
3✔
88
                return;
3✔
89
            }
90

91
            // Expressions are allowed in arrow functions.
92
            if ($tokens[$prev]['code'] === T_FN_ARROW) {
3✔
93
                return;
3✔
94
            }
95
        }//end if
1✔
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
3✔
101
            || (isset($tokens[$prev]['parenthesis_owner']) === true
3✔
102
            && ($tokens[$tokens[$prev]['parenthesis_owner']]['code'] === T_IF
3✔
103
            || $tokens[$tokens[$prev]['parenthesis_owner']]['code'] === T_ELSEIF))
3✔
104
        ) {
1✔
105
            return;
3✔
106
        }
107

108
        if ($tokens[$stackPtr]['code'] === T_RETURN) {
3✔
109
            $next = $phpcsFile->findNext(Tokens::$emptyTokens, ($stackPtr + 1), null, true);
3✔
110
            if ($tokens[$next]['code'] === T_SEMICOLON) {
3✔
111
                $next = $phpcsFile->findNext(Tokens::$emptyTokens, ($next + 1), null, true);
3✔
112
                if ($tokens[$next]['code'] === T_CLOSE_CURLY_BRACKET) {
3✔
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'];
3✔
117
                    if ($tokens[$owner]['code'] === T_FUNCTION
3✔
118
                        || $tokens[$owner]['code'] === T_CLOSURE
3✔
119
                    ) {
1✔
120
                        $warning = 'Empty return statement not required here';
3✔
121
                        $phpcsFile->addWarning($warning, $stackPtr, 'ReturnNotRequired');
3✔
122
                        return;
3✔
123
                    }
124
                }
1✔
125
            }
1✔
126
        }
1✔
127

128
        if (isset($tokens[$stackPtr]['scope_opener']) === true) {
3✔
129
            $owner = $tokens[$stackPtr]['scope_condition'];
3✔
130
            if ($tokens[$owner]['code'] === T_CASE || $tokens[$owner]['code'] === T_DEFAULT) {
3✔
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);
3✔
135
                $next = $phpcsFile->findNext(
3✔
136
                    [
1✔
137
                        T_CASE,
3✔
138
                        T_DEFAULT,
3✔
139
                        T_CLOSE_CURLY_BRACKET,
3✔
140
                        T_ENDSWITCH,
3✔
141
                    ],
2✔
142
                    ($end + 1)
3✔
143
                );
2✔
144

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

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

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

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

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

176
            if (isset($tokens[$condition]['scope_closer']) === false) {
3✔
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
3✔
186
                && $tokens[$stackPtr]['code'] === T_BREAK
3✔
187
            ) {
1✔
188
                return;
3✔
189
            }
190

191
            $closer = $tokens[$condition]['scope_closer'];
3✔
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;
3✔
197
            for ($i = ($stackPtr + 1); $i < $closer; $i++) {
3✔
198
                if (isset($tokens[$i]['scope_closer']) === true) {
3✔
199
                    if ($tokens[$i]['scope_closer'] === $closer) {
3✔
200
                        // We found an opener that shares the same
201
                        // closing token as us.
202
                        $nextOpener = $i;
×
203
                        break;
×
204
                    }
205
                }
1✔
206
            }//end for
1✔
207

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

219
            // Throw an error for all lines until the end of the file.
220
            $end = ($phpcsFile->numTokens - 1);
3✔
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++) {
3✔
226
            if ($start === $end) {
3✔
227
                break;
3✔
228
            }
229

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

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

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

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

253
        $lastLine = $tokens[$start]['line'];
3✔
254
        for ($i = ($start + 1); $i < $end; $i++) {
3✔
255
            if (isset(Tokens::$emptyTokens[$tokens[$i]['code']]) === true
3✔
256
                || isset(Tokens::$bracketTokens[$tokens[$i]['code']]) === true
3✔
257
                || $tokens[$i]['code'] === T_SEMICOLON
3✔
258
            ) {
1✔
259
                continue;
3✔
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
3✔
265
                || $tokens[$i]['code'] === T_FUNCTION
3✔
266
                || $tokens[$i]['code'] === T_CLOSURE
3✔
267
            ) {
1✔
268
                if (isset($tokens[$i]['scope_closer']) === false) {
3✔
269
                    // Parse error/Live coding.
270
                    return;
3✔
271
                }
272

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

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

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

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

300
    }//end process()
2✔
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