1<?php
2/**
3 * Generic_Sniffs_ControlStructures_InlineControlStructureSniff.
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 * Generic_Sniffs_ControlStructures_InlineControlStructureSniff.
18 *
19 * Verifies that inline control statements are not present.
20 *
21 * @category  PHP
22 * @package   PHP_CodeSniffer
23 * @author    Greg Sherwood <gsherwood@squiz.net>
24 * @author    Marc McIntyre <mmcintyre@squiz.net>
25 * @copyright 2006-2014 Squiz Pty Ltd (ABN 77 084 670 600)
26 * @license   https://github.com/squizlabs/PHP_CodeSniffer/blob/master/licence.txt BSD Licence
27 * @version   Release: @package_version@
28 * @link      http://pear.php.net/package/PHP_CodeSniffer
29 */
30class Generic_Sniffs_ControlStructures_InlineControlStructureSniff implements PHP_CodeSniffer_Sniff
31{
32
33    /**
34     * A list of tokenizers this sniff supports.
35     *
36     * @var array
37     */
38    public $supportedTokenizers = array(
39                                   'PHP',
40                                   'JS',
41                                  );
42
43    /**
44     * If true, an error will be thrown; otherwise a warning.
45     *
46     * @var bool
47     */
48    public $error = true;
49
50
51    /**
52     * Returns an array of tokens this test wants to listen for.
53     *
54     * @return array
55     */
56    public function register()
57    {
58        return array(
59                T_IF,
60                T_ELSE,
61                T_ELSEIF,
62                T_FOREACH,
63                T_WHILE,
64                T_DO,
65                T_SWITCH,
66                T_FOR,
67               );
68
69    }//end register()
70
71
72    /**
73     * Processes this test, when one of its tokens is encountered.
74     *
75     * @param PHP_CodeSniffer_File $phpcsFile The file being scanned.
76     * @param int                  $stackPtr  The position of the current token in the
77     *                                        stack passed in $tokens.
78     *
79     * @return void
80     */
81    public function process(PHP_CodeSniffer_File $phpcsFile, $stackPtr)
82    {
83        $tokens = $phpcsFile->getTokens();
84
85        if (isset($tokens[$stackPtr]['scope_opener']) === true) {
86            $phpcsFile->recordMetric($stackPtr, 'Control structure defined inline', 'no');
87            return;
88        }
89
90        // Ignore the ELSE in ELSE IF. We'll process the IF part later.
91        if ($tokens[$stackPtr]['code'] === T_ELSE) {
92            $next = $phpcsFile->findNext(T_WHITESPACE, ($stackPtr + 1), null, true);
93            if ($tokens[$next]['code'] === T_IF) {
94                return;
95            }
96        }
97
98        if ($tokens[$stackPtr]['code'] === T_WHILE) {
99            // This could be from a DO WHILE, which doesn't have an opening brace.
100            $lastContent = $phpcsFile->findPrevious(T_WHITESPACE, ($stackPtr - 1), null, true);
101            if ($tokens[$lastContent]['code'] === T_CLOSE_CURLY_BRACKET) {
102                $brace = $tokens[$lastContent];
103                if (isset($brace['scope_condition']) === true) {
104                    $condition = $tokens[$brace['scope_condition']];
105                    if ($condition['code'] === T_DO) {
106                        return;
107                    }
108                }
109            }
110
111            // In Javascript DO WHILE loops without curly braces are legal. This
112            // is only valid if a single statement is present between the DO and
113            // the WHILE. We can detect this by checking only a single semicolon
114            // is present between them.
115            if ($phpcsFile->tokenizerType === 'JS') {
116                $lastDo        = $phpcsFile->findPrevious(T_DO, ($stackPtr - 1));
117                $lastSemicolon = $phpcsFile->findPrevious(T_SEMICOLON, ($stackPtr - 1));
118                if ($lastDo !== false && $lastSemicolon !== false && $lastDo < $lastSemicolon) {
119                    $precedingSemicolon = $phpcsFile->findPrevious(T_SEMICOLON, ($lastSemicolon - 1));
120                    if ($precedingSemicolon === false || $precedingSemicolon < $lastDo) {
121                        return;
122                    }
123                }
124            }
125        }//end if
126
127        // This is a control structure without an opening brace,
128        // so it is an inline statement.
129        if ($this->error === true) {
130            $fix = $phpcsFile->addFixableError('Inline control structures are not allowed', $stackPtr, 'NotAllowed');
131        } else {
132            $fix = $phpcsFile->addFixableWarning('Inline control structures are discouraged', $stackPtr, 'Discouraged');
133        }
134
135        $phpcsFile->recordMetric($stackPtr, 'Control structure defined inline', 'yes');
136
137        // Stop here if we are not fixing the error.
138        if ($fix !== true) {
139            return;
140        }
141
142        $phpcsFile->fixer->beginChangeset();
143        if (isset($tokens[$stackPtr]['parenthesis_closer']) === true) {
144            $closer = $tokens[$stackPtr]['parenthesis_closer'];
145        } else {
146            $closer = $stackPtr;
147        }
148
149        if ($tokens[($closer + 1)]['code'] === T_WHITESPACE
150            || $tokens[($closer + 1)]['code'] === T_SEMICOLON
151        ) {
152            $phpcsFile->fixer->addContent($closer, ' {');
153        } else {
154            $phpcsFile->fixer->addContent($closer, ' { ');
155        }
156
157        $fixableScopeOpeners = $this->register();
158
159        $lastNonEmpty = $closer;
160        for ($end = ($closer + 1); $end < $phpcsFile->numTokens; $end++) {
161            if ($tokens[$end]['code'] === T_SEMICOLON) {
162                break;
163            }
164
165            if ($tokens[$end]['code'] === T_CLOSE_TAG) {
166                $end = $lastNonEmpty;
167                break;
168            }
169
170            if (in_array($tokens[$end]['code'], $fixableScopeOpeners) === true
171                && isset($tokens[$end]['scope_opener']) === false
172            ) {
173                // The best way to fix nested inline scopes is middle-out.
174                // So skip this one. It will be detected and fixed on a future loop.
175                $phpcsFile->fixer->rollbackChangeset();
176                return;
177            }
178
179            if (isset($tokens[$end]['scope_opener']) === true) {
180                $type = $tokens[$end]['code'];
181                $end  = $tokens[$end]['scope_closer'];
182                if ($type === T_DO || $type === T_IF || $type === T_ELSEIF || $type === T_TRY) {
183                    $next = $phpcsFile->findNext(PHP_CodeSniffer_Tokens::$emptyTokens, ($end + 1), null, true);
184                    if ($next === false) {
185                        break;
186                    }
187
188                    $nextType = $tokens[$next]['code'];
189
190                    // Let additional conditions loop and find their ending.
191                    if (($type === T_IF
192                        || $type === T_ELSEIF)
193                        && ($nextType === T_ELSEIF
194                        || $nextType === T_ELSE)
195                    ) {
196                        continue;
197                    }
198
199                    // Account for DO... WHILE conditions.
200                    if ($type === T_DO && $nextType === T_WHILE) {
201                        $end = $phpcsFile->findNext(T_SEMICOLON, ($next + 1));
202                    }
203
204                    // Account for TRY... CATCH statements.
205                    if ($type === T_TRY && $nextType === T_CATCH) {
206                        $end = $tokens[$next]['scope_closer'];
207                    }
208                }//end if
209
210                if ($tokens[$end]['code'] !== T_END_HEREDOC
211                    && $tokens[$end]['code'] !== T_END_NOWDOC
212                ) {
213                    break;
214                }
215            }//end if
216
217            if (isset($tokens[$end]['parenthesis_closer']) === true) {
218                $end          = $tokens[$end]['parenthesis_closer'];
219                $lastNonEmpty = $end;
220                continue;
221            }
222
223            if ($tokens[$end]['code'] !== T_WHITESPACE) {
224                $lastNonEmpty = $end;
225            }
226        }//end for
227
228        if ($end === $phpcsFile->numTokens) {
229            $end = $lastNonEmpty;
230        }
231
232        $next = $phpcsFile->findNext(PHP_CodeSniffer_Tokens::$emptyTokens, ($end + 1), null, true);
233
234        if ($next === false || $tokens[$next]['line'] !== $tokens[$end]['line']) {
235            // Looks for completely empty statements.
236            $next = $phpcsFile->findNext(T_WHITESPACE, ($closer + 1), ($end + 1), true);
237
238            // Account for a comment on the end of the line.
239            for ($endLine = $end; $endLine < $phpcsFile->numTokens; $endLine++) {
240                if (isset($tokens[($endLine + 1)]) === false
241                    || $tokens[$endLine]['line'] !== $tokens[($endLine + 1)]['line']
242                ) {
243                    break;
244                }
245            }
246
247            if ($tokens[$endLine]['code'] !== T_COMMENT) {
248                $endLine = $end;
249            }
250        } else {
251            $next    = ($end + 1);
252            $endLine = $end;
253        }
254
255        if ($next !== $end) {
256            if ($endLine !== $end) {
257                $endToken     = $endLine;
258                $addedContent = '';
259            } else {
260                $endToken     = $end;
261                $addedContent = $phpcsFile->eolChar;
262
263                if ($tokens[$end]['code'] !== T_SEMICOLON
264                    && $tokens[$end]['code'] !== T_CLOSE_CURLY_BRACKET
265                ) {
266                    $phpcsFile->fixer->addContent($end, '; ');
267                }
268            }
269
270            $next = $phpcsFile->findNext(T_WHITESPACE, ($endToken + 1), null, true);
271            if ($next !== false
272                && ($tokens[$next]['code'] === T_ELSE
273                || $tokens[$next]['code'] === T_ELSEIF)
274            ) {
275                $phpcsFile->fixer->addContentBefore($next, '} ');
276            } else {
277                $indent = '';
278                for ($first = $stackPtr; $first > 0; $first--) {
279                    if ($first === 1
280                        || $tokens[($first - 1)]['line'] !== $tokens[$first]['line']
281                    ) {
282                        break;
283                    }
284                }
285
286                if ($tokens[$first]['code'] === T_WHITESPACE) {
287                    $indent = $tokens[$first]['content'];
288                } else if ($tokens[$first]['code'] === T_INLINE_HTML
289                    || $tokens[$first]['code'] === T_OPEN_TAG
290                ) {
291                    $addedContent = '';
292                }
293
294                $addedContent .= $indent.'}';
295                if ($next !== false && $tokens[$endToken]['code'] === T_COMMENT) {
296                    $addedContent .= $phpcsFile->eolChar;
297                }
298
299                $phpcsFile->fixer->addContent($endToken, $addedContent);
300            }//end if
301        } else {
302            if ($endLine !== $end) {
303                $phpcsFile->fixer->replaceToken($end, '');
304                $phpcsFile->fixer->addNewlineBefore($endLine);
305                $phpcsFile->fixer->addContent($endLine, '}');
306            } else {
307                $phpcsFile->fixer->replaceToken($end, '}');
308            }
309        }//end if
310
311        $phpcsFile->fixer->endChangeset();
312
313    }//end process()
314
315
316}//end class
317