1<?php
2/**
3 * Squiz_Sniffs_Formatting_OperationBracketSniff.
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_Formatting_OperationBracketSniff.
18 *
19 * Tests that all arithmetic operations are bracketed.
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 Squiz_Sniffs_Formatting_OperatorBracketSniff 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    /**
45     * Returns an array of tokens this test wants to listen for.
46     *
47     * @return array
48     */
49    public function register()
50    {
51        return PHP_CodeSniffer_Tokens::$operators;
52
53    }//end register()
54
55
56    /**
57     * Processes this test, when one of its tokens is encountered.
58     *
59     * @param PHP_CodeSniffer_File $phpcsFile The file being scanned.
60     * @param int                  $stackPtr  The position of the current token in the
61     *                                        stack passed in $tokens.
62     *
63     * @return void
64     */
65    public function process(PHP_CodeSniffer_File $phpcsFile, $stackPtr)
66    {
67        $tokens = $phpcsFile->getTokens();
68
69        if ($phpcsFile->tokenizerType === 'JS' && $tokens[$stackPtr]['code'] === T_PLUS) {
70            // JavaScript uses the plus operator for string concatenation as well
71            // so we cannot accurately determine if it is a string concat or addition.
72            // So just ignore it.
73            return;
74        }
75
76        // If the & is a reference, then we don't want to check for brackets.
77        if ($tokens[$stackPtr]['code'] === T_BITWISE_AND && $phpcsFile->isReference($stackPtr) === true) {
78            return;
79        }
80
81        // There is one instance where brackets aren't needed, which involves
82        // the minus sign being used to assign a negative number to a variable.
83        if ($tokens[$stackPtr]['code'] === T_MINUS) {
84            // Check to see if we are trying to return -n.
85            $prev = $phpcsFile->findPrevious(PHP_CodeSniffer_Tokens::$emptyTokens, ($stackPtr - 1), null, true);
86            if ($tokens[$prev]['code'] === T_RETURN) {
87                return;
88            }
89
90            $number = $phpcsFile->findNext(T_WHITESPACE, ($stackPtr + 1), null, true);
91            if ($tokens[$number]['code'] === T_LNUMBER || $tokens[$number]['code'] === T_DNUMBER) {
92                $previous = $phpcsFile->findPrevious(T_WHITESPACE, ($stackPtr - 1), null, true);
93                if ($previous !== false) {
94                    $isAssignment = in_array($tokens[$previous]['code'], PHP_CodeSniffer_Tokens::$assignmentTokens);
95                    $isEquality   = in_array($tokens[$previous]['code'], PHP_CodeSniffer_Tokens::$equalityTokens);
96                    $isComparison = in_array($tokens[$previous]['code'], PHP_CodeSniffer_Tokens::$comparisonTokens);
97                    if ($isAssignment === true || $isEquality === true || $isComparison === true) {
98                        // This is a negative assignment or comparison.
99                        // We need to check that the minus and the number are
100                        // adjacent.
101                        if (($number - $stackPtr) !== 1) {
102                            $error = 'No space allowed between minus sign and number';
103                            $phpcsFile->addError($error, $stackPtr, 'SpacingAfterMinus');
104                        }
105
106                        return;
107                    }
108                }
109            }
110        }//end if
111
112        $previousToken = $phpcsFile->findPrevious(T_WHITESPACE, ($stackPtr - 1), null, true, null, true);
113        if ($previousToken !== false) {
114            // A list of tokens that indicate that the token is not
115            // part of an arithmetic operation.
116            $invalidTokens = array(
117                              T_COMMA,
118                              T_COLON,
119                              T_OPEN_PARENTHESIS,
120                              T_OPEN_SQUARE_BRACKET,
121                              T_OPEN_SHORT_ARRAY,
122                              T_CASE,
123                             );
124
125            if (in_array($tokens[$previousToken]['code'], $invalidTokens) === true) {
126                return;
127            }
128        }
129
130        // Tokens that are allowed inside a bracketed operation.
131        $allowed = array(
132                    T_VARIABLE,
133                    T_LNUMBER,
134                    T_DNUMBER,
135                    T_STRING,
136                    T_WHITESPACE,
137                    T_NS_SEPARATOR,
138                    T_THIS,
139                    T_SELF,
140                    T_OBJECT_OPERATOR,
141                    T_DOUBLE_COLON,
142                    T_OPEN_SQUARE_BRACKET,
143                    T_CLOSE_SQUARE_BRACKET,
144                    T_MODULUS,
145                    T_NONE,
146                   );
147
148        $allowed += PHP_CodeSniffer_Tokens::$operators;
149
150        $lastBracket = false;
151        if (isset($tokens[$stackPtr]['nested_parenthesis']) === true) {
152            $parenthesis = array_reverse($tokens[$stackPtr]['nested_parenthesis'], true);
153            foreach ($parenthesis as $bracket => $endBracket) {
154                $prevToken = $phpcsFile->findPrevious(T_WHITESPACE, ($bracket - 1), null, true);
155                $prevCode  = $tokens[$prevToken]['code'];
156
157                if ($prevCode === T_ISSET) {
158                    // This operation is inside an isset() call, but has
159                    // no bracket of it's own.
160                    break;
161                }
162
163                if ($prevCode === T_STRING || $prevCode === T_SWITCH) {
164                    // We allow simple operations to not be bracketed.
165                    // For example, ceil($one / $two).
166                    for ($prev = ($stackPtr - 1); $prev > $bracket; $prev--) {
167                        if (in_array($tokens[$prev]['code'], $allowed) === true) {
168                            continue;
169                        }
170
171                        if ($tokens[$prev]['code'] === T_CLOSE_PARENTHESIS) {
172                            $prev = $tokens[$prev]['parenthesis_opener'];
173                        } else {
174                            break;
175                        }
176                    }
177
178                    if ($prev !== $bracket) {
179                        break;
180                    }
181
182                    for ($next = ($stackPtr + 1); $next < $endBracket; $next++) {
183                        if (in_array($tokens[$next]['code'], $allowed) === true) {
184                            continue;
185                        }
186
187                        if ($tokens[$next]['code'] === T_OPEN_PARENTHESIS) {
188                            $next = $tokens[$next]['parenthesis_closer'];
189                        } else {
190                            break;
191                        }
192                    }
193
194                    if ($next !== $endBracket) {
195                        break;
196                    }
197                }//end if
198
199                if (in_array($prevCode, PHP_CodeSniffer_Tokens::$scopeOpeners) === true) {
200                    // This operation is inside a control structure like FOREACH
201                    // or IF, but has no bracket of it's own.
202                    // The only control structure allowed to do this is SWITCH.
203                    if ($prevCode !== T_SWITCH) {
204                        break;
205                    }
206                }
207
208                if ($prevCode === T_OPEN_PARENTHESIS) {
209                    // These are two open parenthesis in a row. If the current
210                    // one doesn't enclose the operator, go to the previous one.
211                    if ($endBracket < $stackPtr) {
212                        continue;
213                    }
214                }
215
216                $lastBracket = $bracket;
217                break;
218            }//end foreach
219        }//end if
220
221        if ($lastBracket === false) {
222            // It is not in a bracketed statement at all.
223            $this->addMissingBracketsError($phpcsFile, $stackPtr);
224            return;
225        } else if ($tokens[$lastBracket]['parenthesis_closer'] < $stackPtr) {
226            // There are a set of brackets in front of it that don't include it.
227            $this->addMissingBracketsError($phpcsFile, $stackPtr);
228            return;
229        } else {
230            // We are enclosed in a set of bracket, so the last thing to
231            // check is that we are not also enclosed in square brackets
232            // like this: ($array[$index + 1]), which is invalid.
233            $brackets = array(
234                         T_OPEN_SQUARE_BRACKET,
235                         T_CLOSE_SQUARE_BRACKET,
236                        );
237
238            $squareBracket = $phpcsFile->findPrevious($brackets, ($stackPtr - 1), $lastBracket);
239            if ($squareBracket !== false && $tokens[$squareBracket]['code'] === T_OPEN_SQUARE_BRACKET) {
240                $closeSquareBracket = $phpcsFile->findNext($brackets, ($stackPtr + 1));
241                if ($closeSquareBracket !== false && $tokens[$closeSquareBracket]['code'] === T_CLOSE_SQUARE_BRACKET) {
242                    $this->addMissingBracketsError($phpcsFile, $stackPtr);
243                }
244            }
245
246            return;
247        }//end if
248
249        $lastAssignment = $phpcsFile->findPrevious(PHP_CodeSniffer_Tokens::$assignmentTokens, $stackPtr, null, false, null, true);
250        if ($lastAssignment !== false && $lastAssignment > $lastBracket) {
251            $this->addMissingBracketsError($phpcsFile, $stackPtr);
252        }
253
254    }//end process()
255
256
257    /**
258     * Add and fix the missing brackets error.
259     *
260     * @param PHP_CodeSniffer_File $phpcsFile The file being scanned.
261     * @param int                  $stackPtr  The position of the current token in the
262     *                                        stack passed in $tokens.
263     *
264     * @return void
265     */
266    public function addMissingBracketsError(PHP_CodeSniffer_File $phpcsFile, $stackPtr)
267    {
268        $error = 'Arithmetic operation must be bracketed';
269        $fix   = $phpcsFile->addFixableError($error, $stackPtr, 'MissingBrackets');
270
271        if ($fix === false) {
272            return;
273        }
274
275        $tokens = $phpcsFile->getTokens();
276
277        $allowed = array(
278                    T_VARIABLE        => true,
279                    T_LNUMBER         => true,
280                    T_DNUMBER         => true,
281                    T_STRING          => true,
282                    T_WHITESPACE      => true,
283                    T_THIS            => true,
284                    T_SELF            => true,
285                    T_OBJECT_OPERATOR => true,
286                    T_DOUBLE_COLON    => true,
287                    T_MODULUS         => true,
288                    T_ISSET           => true,
289                    T_ARRAY           => true,
290                    T_NONE            => true,
291                   );
292
293        // Find the first token in the expression.
294        for ($before = ($stackPtr - 1); $before > 0; $before--) {
295            // Special case for plus operators because we can't tell if they are used
296            // for addition or string contact. So assume string concat to be safe.
297            if ($phpcsFile->tokenizerType === 'JS' && $tokens[$before]['code'] === T_PLUS) {
298                break;
299            }
300
301            if (isset(PHP_CodeSniffer_Tokens::$emptyTokens[$tokens[$before]['code']]) === true
302                || isset(PHP_CodeSniffer_Tokens::$operators[$tokens[$before]['code']]) === true
303                || isset(PHP_CodeSniffer_Tokens::$castTokens[$tokens[$before]['code']]) === true
304                || isset($allowed[$tokens[$before]['code']]) === true
305            ) {
306                continue;
307            }
308
309            if ($tokens[$before]['code'] === T_CLOSE_PARENTHESIS) {
310                $before = $tokens[$before]['parenthesis_opener'];
311                continue;
312            }
313
314            if ($tokens[$before]['code'] === T_CLOSE_SQUARE_BRACKET) {
315                $before = $tokens[$before]['bracket_opener'];
316                continue;
317            }
318
319            break;
320        }//end for
321
322        $before = $phpcsFile->findNext(PHP_CodeSniffer_Tokens::$emptyTokens, ($before + 1), null, true);
323
324        // Find the last token in the expression.
325        for ($after = ($stackPtr + 1); $after < $phpcsFile->numTokens; $after++) {
326            // Special case for plus operators because we can't tell if they are used
327            // for addition or string contact. So assume string concat to be safe.
328            if ($phpcsFile->tokenizerType === 'JS' && $tokens[$after]['code'] === T_PLUS) {
329                break;
330            }
331
332            if (isset(PHP_CodeSniffer_Tokens::$emptyTokens[$tokens[$after]['code']]) === true
333                || isset(PHP_CodeSniffer_Tokens::$operators[$tokens[$after]['code']]) === true
334                || isset(PHP_CodeSniffer_Tokens::$castTokens[$tokens[$after]['code']]) === true
335                || isset($allowed[$tokens[$after]['code']]) === true
336            ) {
337                continue;
338            }
339
340            if ($tokens[$after]['code'] === T_OPEN_PARENTHESIS) {
341                $after = $tokens[$after]['parenthesis_closer'];
342                continue;
343            }
344
345            if ($tokens[$after]['code'] === T_OPEN_SQUARE_BRACKET) {
346                $after = $tokens[$after]['bracket_closer'];
347                continue;
348            }
349
350            break;
351        }//end for
352
353        $after = $phpcsFile->findPrevious(PHP_CodeSniffer_Tokens::$emptyTokens, ($after - 1), null, true);
354
355        // Can only fix this error if both tokens are available for fixing.
356        // Adding one bracket without the other will create parse errors.
357        $phpcsFile->fixer->beginChangeset();
358        $phpcsFile->fixer->replaceToken($before, '('.$tokens[$before]['content']);
359        $phpcsFile->fixer->replaceToken($after, $tokens[$after]['content'].')');
360        $phpcsFile->fixer->endChangeset();
361
362    }//end addMissingBracketsError()
363
364
365}//end class
366