1<?php
2/**
3 * Squiz_Sniffs_PHP_InnerFunctionsSniff.
4 *
5 * PHP version 5
6 *
7 * @category  PHP
8 * @package   PHP_CodeSniffer
9 * @author    Greg Sherwood <gsherwood@squiz.net>
10 * @author    Marc McIntyre <mmcintyre@squiz.net>
11 * @copyright 2006-2014 Squiz Pty Ltd (ABN 77 084 670 600)
12 * @license   https://github.com/squizlabs/PHP_CodeSniffer/blob/master/licence.txt BSD Licence
13 * @link      http://pear.php.net/package/PHP_CodeSniffer
14 */
15
16/**
17 * Squiz_Sniffs_PHP_NonExecutableCodeSniff.
18 *
19 * Warns about code that can never been executed. This happens when a function
20 * returns before the code, or a break ends execution of a statement etc.
21 *
22 * @category  PHP
23 * @package   PHP_CodeSniffer
24 * @author    Greg Sherwood <gsherwood@squiz.net>
25 * @author    Marc McIntyre <mmcintyre@squiz.net>
26 * @copyright 2006-2014 Squiz Pty Ltd (ABN 77 084 670 600)
27 * @license   https://github.com/squizlabs/PHP_CodeSniffer/blob/master/licence.txt BSD Licence
28 * @version   Release: @package_version@
29 * @link      http://pear.php.net/package/PHP_CodeSniffer
30 */
31class Squiz_Sniffs_PHP_NonExecutableCodeSniff implements PHP_CodeSniffer_Sniff
32{
33
34
35    /**
36     * Returns an array of tokens this test wants to listen for.
37     *
38     * @return array
39     */
40    public function register()
41    {
42        return array(
43                T_BREAK,
44                T_CONTINUE,
45                T_RETURN,
46                T_THROW,
47                T_EXIT,
48               );
49
50    }//end register()
51
52
53    /**
54     * Processes this test, when one of its tokens is encountered.
55     *
56     * @param PHP_CodeSniffer_File $phpcsFile The file being scanned.
57     * @param int                  $stackPtr  The position of the current token in
58     *                                        the stack passed in $tokens.
59     *
60     * @return void
61     */
62    public function process(PHP_CodeSniffer_File $phpcsFile, $stackPtr)
63    {
64        $tokens = $phpcsFile->getTokens();
65
66        // If this token is preceded with an "or", it only relates to one line
67        // and should be ignored. For example: fopen() or die().
68        $prev = $phpcsFile->findPrevious(PHP_CodeSniffer_Tokens::$emptyTokens, ($stackPtr - 1), null, true);
69        if ($tokens[$prev]['code'] === T_LOGICAL_OR || $tokens[$prev]['code'] === T_BOOLEAN_OR) {
70            return;
71        }
72
73        // Check if this token is actually part of a one-line IF or ELSE statement.
74        for ($i = ($stackPtr - 1); $i > 0; $i--) {
75            if ($tokens[$i]['code'] === T_CLOSE_PARENTHESIS) {
76                $i = $tokens[$i]['parenthesis_opener'];
77                continue;
78            } else if (isset(PHP_CodeSniffer_Tokens::$emptyTokens[$tokens[$i]['code']]) === true) {
79                continue;
80            }
81
82            break;
83        }
84
85        if ($tokens[$i]['code'] === T_IF
86            || $tokens[$i]['code'] === T_ELSE
87            || $tokens[$i]['code'] === T_ELSEIF
88        ) {
89            return;
90        }
91
92        if ($tokens[$stackPtr]['code'] === T_RETURN) {
93            $next = $phpcsFile->findNext(T_WHITESPACE, ($stackPtr + 1), null, true);
94            if ($tokens[$next]['code'] === T_SEMICOLON) {
95                $next = $phpcsFile->findNext(T_WHITESPACE, ($next + 1), null, true);
96                if ($tokens[$next]['code'] === T_CLOSE_CURLY_BRACKET) {
97                    // If this is the closing brace of a function
98                    // then this return statement doesn't return anything
99                    // and is not required anyway.
100                    $owner = $tokens[$next]['scope_condition'];
101                    if ($tokens[$owner]['code'] === T_FUNCTION) {
102                        $warning = 'Empty return statement not required here';
103                        $phpcsFile->addWarning($warning, $stackPtr, 'ReturnNotRequired');
104                        return;
105                    }
106                }
107            }
108        }
109
110        if (isset($tokens[$stackPtr]['scope_opener']) === true) {
111            $owner = $tokens[$stackPtr]['scope_condition'];
112            if ($tokens[$owner]['code'] === T_CASE || $tokens[$owner]['code'] === T_DEFAULT) {
113                // This token closes the scope of a CASE or DEFAULT statement
114                // so any code between this statement and the next CASE, DEFAULT or
115                // end of SWITCH token will not be executable.
116                $end  = $phpcsFile->findEndOfStatement($stackPtr);
117                $next = $phpcsFile->findNext(
118                    array(
119                     T_CASE,
120                     T_DEFAULT,
121                     T_CLOSE_CURLY_BRACKET,
122                    ),
123                    ($end + 1)
124                );
125
126                if ($next !== false) {
127                    $lastLine = $tokens[$end]['line'];
128                    for ($i = ($stackPtr + 1); $i < $next; $i++) {
129                        if (isset(PHP_CodeSniffer_Tokens::$emptyTokens[$tokens[$i]['code']]) === true) {
130                            continue;
131                        }
132
133                        $line = $tokens[$i]['line'];
134                        if ($line > $lastLine) {
135                            $type    = substr($tokens[$stackPtr]['type'], 2);
136                            $warning = 'Code after %s statement cannot be executed';
137                            $data    = array($type);
138                            $phpcsFile->addWarning($warning, $i, 'Unreachable', $data);
139                            $lastLine = $line;
140                        }
141                    }
142                }//end if
143
144                // That's all we have to check for these types of statements.
145                return;
146            }//end if
147        }//end if
148
149        // This token may be part of an inline condition.
150        // If we find a closing parenthesis that belongs to a condition
151        // we should ignore this token.
152        $prev = $phpcsFile->findPrevious(PHP_CodeSniffer_Tokens::$emptyTokens, ($stackPtr - 1), null, true);
153        if (isset($tokens[$prev]['parenthesis_owner']) === true) {
154            $owner  = $tokens[$prev]['parenthesis_owner'];
155            $ignore = array(
156                       T_IF     => true,
157                       T_ELSE   => true,
158                       T_ELSEIF => true,
159                      );
160            if (isset($ignore[$tokens[$owner]['code']]) === true) {
161                return;
162            }
163        }
164
165        $ourConditions = array_keys($tokens[$stackPtr]['conditions']);
166
167        if (empty($ourConditions) === false) {
168            $condition = array_pop($ourConditions);
169
170            if (isset($tokens[$condition]['scope_closer']) === false) {
171                return;
172            }
173
174            // Special case for BREAK statements sitting directly inside SWITCH
175            // statements. If we get to this point, we know the BREAK is not being
176            // used to close a CASE statement, so it is most likely non-executable
177            // code itself (as is the case when you put return; break; at the end of
178            // a case). So we need to ignore this token.
179            if ($tokens[$condition]['code'] === T_SWITCH
180                && $tokens[$stackPtr]['code'] === T_BREAK
181            ) {
182                return;
183            }
184
185            $closer = $tokens[$condition]['scope_closer'];
186
187            // If the closer for our condition is shared with other openers,
188            // we will need to throw errors from this token to the next
189            // shared opener (if there is one), not to the scope closer.
190            $nextOpener = null;
191            for ($i = ($stackPtr + 1); $i < $closer; $i++) {
192                if (isset($tokens[$i]['scope_closer']) === true) {
193                    if ($tokens[$i]['scope_closer'] === $closer) {
194                        // We found an opener that shares the same
195                        // closing token as us.
196                        $nextOpener = $i;
197                        break;
198                    }
199                }
200            }//end for
201
202            if ($nextOpener === null) {
203                $end = $closer;
204            } else {
205                $end = ($nextOpener - 1);
206            }
207        } else {
208            // This token is in the global scope.
209            if ($tokens[$stackPtr]['code'] === T_BREAK) {
210                return;
211            }
212
213            // Throw an error for all lines until the end of the file.
214            $end = ($phpcsFile->numTokens - 1);
215        }//end if
216
217        // Find the semicolon that ends this statement, skipping
218        // nested statements like FOR loops and closures.
219        for ($start = ($stackPtr + 1); $start < $phpcsFile->numTokens; $start++) {
220            if ($start === $end) {
221                break;
222            }
223
224            if ($tokens[$start]['code'] === T_OPEN_PARENTHESIS) {
225                $start = $tokens[$start]['parenthesis_closer'];
226                continue;
227            }
228
229            if ($tokens[$start]['code'] === T_OPEN_CURLY_BRACKET) {
230                $start = $tokens[$start]['bracket_closer'];
231                continue;
232            }
233
234            if ($tokens[$start]['code'] === T_SEMICOLON) {
235                break;
236            }
237        }//end for
238
239        $lastLine = $tokens[$start]['line'];
240        for ($i = ($start + 1); $i < $end; $i++) {
241            if (isset(PHP_CodeSniffer_Tokens::$emptyTokens[$tokens[$i]['code']]) === true
242                || isset(PHP_CodeSniffer_Tokens::$bracketTokens[$tokens[$i]['code']]) === true
243            ) {
244                continue;
245            }
246
247            // Skip whole functions and classes/interfaces because they are not
248            // technically executed code, but rather declarations that may be used.
249            if ($tokens[$i]['code'] === T_FUNCTION
250                || $tokens[$i]['code'] === T_CLASS
251                || $tokens[$i]['code'] === T_INTERFACE
252            ) {
253                $i = $tokens[$i]['scope_closer'];
254                continue;
255            }
256
257            $line = $tokens[$i]['line'];
258            if ($line > $lastLine) {
259                $type    = substr($tokens[$stackPtr]['type'], 2);
260                $warning = 'Code after %s statement cannot be executed';
261                $data    = array($type);
262                $phpcsFile->addWarning($warning, $i, 'Unreachable', $data);
263                $lastLine = $line;
264            }
265        }//end for
266
267    }//end process()
268
269
270}//end class
271