1<?php
2/**
3 * Sniffs_Squiz_WhiteSpace_OperatorSpacingSniff.
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 * Sniffs_Squiz_WhiteSpace_OperatorSpacingSniff.
18 *
19 * Verifies that operators have valid spacing surrounding them.
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_WhiteSpace_OperatorSpacingSniff 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     * Allow newlines instead of spaces.
45     *
46     * @var boolean
47     */
48    public $ignoreNewlines = false;
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        $comparison = PHP_CodeSniffer_Tokens::$comparisonTokens;
59        $operators  = PHP_CodeSniffer_Tokens::$operators;
60        $assignment = PHP_CodeSniffer_Tokens::$assignmentTokens;
61        $inlineIf   = array(
62                       T_INLINE_THEN,
63                       T_INLINE_ELSE,
64                      );
65
66        return array_unique(
67            array_merge($comparison, $operators, $assignment, $inlineIf)
68        );
69
70    }//end register()
71
72
73    /**
74     * Processes this sniff, when one of its tokens is encountered.
75     *
76     * @param PHP_CodeSniffer_File $phpcsFile The current file being checked.
77     * @param int                  $stackPtr  The position of the current token in
78     *                                        the stack passed in $tokens.
79     *
80     * @return void
81     */
82    public function process(PHP_CodeSniffer_File $phpcsFile, $stackPtr)
83    {
84        $tokens = $phpcsFile->getTokens();
85
86        // Skip default values in function declarations.
87        // Skip declare statements.
88        if ($tokens[$stackPtr]['code'] === T_EQUAL
89            || $tokens[$stackPtr]['code'] === T_MINUS
90        ) {
91            if (isset($tokens[$stackPtr]['nested_parenthesis']) === true) {
92                $parenthesis = array_keys($tokens[$stackPtr]['nested_parenthesis']);
93                $bracket     = array_pop($parenthesis);
94                if (isset($tokens[$bracket]['parenthesis_owner']) === true) {
95                    $function = $tokens[$bracket]['parenthesis_owner'];
96                    if ($tokens[$function]['code'] === T_FUNCTION
97                        || $tokens[$function]['code'] === T_CLOSURE
98                        || $tokens[$function]['code'] === T_DECLARE
99                    ) {
100                        return;
101                    }
102                }
103            }
104        }
105
106        if ($tokens[$stackPtr]['code'] === T_EQUAL) {
107            // Skip for '=&' case.
108            if (isset($tokens[($stackPtr + 1)]) === true
109                && $tokens[($stackPtr + 1)]['code'] === T_BITWISE_AND
110            ) {
111                return;
112            }
113        }
114
115        // Skip short ternary such as: "$foo = $bar ?: true;".
116        if (($tokens[$stackPtr]['code'] === T_INLINE_THEN
117            && $tokens[($stackPtr + 1)]['code'] === T_INLINE_ELSE)
118            || ($tokens[($stackPtr - 1)]['code'] === T_INLINE_THEN
119            && $tokens[$stackPtr]['code'] === T_INLINE_ELSE)
120        ) {
121                return;
122        }
123
124        if ($tokens[$stackPtr]['code'] === T_BITWISE_AND) {
125            // If it's not a reference, then we expect one space either side of the
126            // bitwise operator.
127            if ($phpcsFile->isReference($stackPtr) === true) {
128                return;
129            }
130
131            // Check there is one space before the & operator.
132            if ($tokens[($stackPtr - 1)]['code'] !== T_WHITESPACE) {
133                $error = 'Expected 1 space before "&" operator; 0 found';
134                $fix   = $phpcsFile->addFixableError($error, $stackPtr, 'NoSpaceBeforeAmp');
135                if ($fix === true) {
136                    $phpcsFile->fixer->addContentBefore($stackPtr, ' ');
137                }
138
139                $phpcsFile->recordMetric($stackPtr, 'Space before operator', 0);
140            } else {
141                if ($tokens[($stackPtr - 2)]['line'] !== $tokens[$stackPtr]['line']) {
142                    $found = 'newline';
143                } else {
144                    $found = $tokens[($stackPtr - 1)]['length'];
145                }
146
147                $phpcsFile->recordMetric($stackPtr, 'Space before operator', $found);
148                if ($found !== 1
149                    && ($found !== 'newline' || $this->ignoreNewlines === false)
150                ) {
151                    $error = 'Expected 1 space before "&" operator; %s found';
152                    $data  = array($found);
153                    $fix   = $phpcsFile->addFixableError($error, $stackPtr, 'SpacingBeforeAmp', $data);
154                    if ($fix === true) {
155                        $phpcsFile->fixer->replaceToken(($stackPtr - 1), ' ');
156                    }
157                }
158            }//end if
159
160            // Check there is one space after the & operator.
161            if ($tokens[($stackPtr + 1)]['code'] !== T_WHITESPACE) {
162                $error = 'Expected 1 space after "&" operator; 0 found';
163                $fix   = $phpcsFile->addFixableError($error, $stackPtr, 'NoSpaceAfterAmp');
164                if ($fix === true) {
165                    $phpcsFile->fixer->addContent($stackPtr, ' ');
166                }
167
168                $phpcsFile->recordMetric($stackPtr, 'Space after operator', 0);
169            } else {
170                if ($tokens[($stackPtr + 2)]['line'] !== $tokens[$stackPtr]['line']) {
171                    $found = 'newline';
172                } else {
173                    $found = $tokens[($stackPtr + 1)]['length'];
174                }
175
176                $phpcsFile->recordMetric($stackPtr, 'Space after operator', $found);
177                if ($found !== 1
178                    && ($found !== 'newline' || $this->ignoreNewlines === false)
179                ) {
180                    $error = 'Expected 1 space after "&" operator; %s found';
181                    $data  = array($found);
182                    $fix   = $phpcsFile->addFixableError($error, $stackPtr, 'SpacingAfterAmp', $data);
183                    if ($fix === true) {
184                        $phpcsFile->fixer->replaceToken(($stackPtr + 1), ' ');
185                    }
186                }
187            }//end if
188
189            return;
190        }//end if
191
192        if ($tokens[$stackPtr]['code'] === T_MINUS || $tokens[$stackPtr]['code'] === T_PLUS) {
193            // Check minus spacing, but make sure we aren't just assigning
194            // a minus value or returning one.
195            $prev = $phpcsFile->findPrevious(T_WHITESPACE, ($stackPtr - 1), null, true);
196            if ($tokens[$prev]['code'] === T_RETURN) {
197                // Just returning a negative value; eg. (return -1).
198                return;
199            }
200
201            if (isset(PHP_CodeSniffer_Tokens::$operators[$tokens[$prev]['code']]) === true) {
202                // Just trying to operate on a negative value; eg. ($var * -1).
203                return;
204            }
205
206            if (isset(PHP_CodeSniffer_Tokens::$comparisonTokens[$tokens[$prev]['code']]) === true) {
207                // Just trying to compare a negative value; eg. ($var === -1).
208                return;
209            }
210
211            if (isset(PHP_CodeSniffer_Tokens::$booleanOperators[$tokens[$prev]['code']]) === true) {
212                // Just trying to compare a negative value; eg. ($var || -1 === $b).
213                return;
214            }
215
216            if (isset(PHP_CodeSniffer_Tokens::$assignmentTokens[$tokens[$prev]['code']]) === true) {
217                // Just trying to assign a negative value; eg. ($var = -1).
218                return;
219            }
220
221            // A list of tokens that indicate that the token is not
222            // part of an arithmetic operation.
223            $invalidTokens = array(
224                              T_COMMA               => true,
225                              T_OPEN_PARENTHESIS    => true,
226                              T_OPEN_SQUARE_BRACKET => true,
227                              T_OPEN_SHORT_ARRAY    => true,
228                              T_DOUBLE_ARROW        => true,
229                              T_COLON               => true,
230                              T_INLINE_THEN         => true,
231                              T_INLINE_ELSE         => true,
232                              T_CASE                => true,
233                             );
234
235            if (isset($invalidTokens[$tokens[$prev]['code']]) === true) {
236                // Just trying to use a negative value; eg. myFunction($var, -2).
237                return;
238            }
239        }//end if
240
241        $operator = $tokens[$stackPtr]['content'];
242
243        if ($tokens[($stackPtr - 1)]['code'] !== T_WHITESPACE) {
244            $error = "Expected 1 space before \"$operator\"; 0 found";
245            $fix   = $phpcsFile->addFixableError($error, $stackPtr, 'NoSpaceBefore');
246            if ($fix === true) {
247                $phpcsFile->fixer->addContentBefore($stackPtr, ' ');
248            }
249
250            $phpcsFile->recordMetric($stackPtr, 'Space before operator', 0);
251        } else if (isset(PHP_CodeSniffer_Tokens::$assignmentTokens[$tokens[$stackPtr]['code']]) === false) {
252            // Don't throw an error for assignments, because other standards allow
253            // multiple spaces there to align multiple assignments.
254            if ($tokens[($stackPtr - 2)]['line'] !== $tokens[$stackPtr]['line']) {
255                $found = 'newline';
256            } else {
257                $found = $tokens[($stackPtr - 1)]['length'];
258            }
259
260            $phpcsFile->recordMetric($stackPtr, 'Space before operator', $found);
261            if ($found !== 1
262                && ($found !== 'newline' || $this->ignoreNewlines === false)
263            ) {
264                $error = 'Expected 1 space before "%s"; %s found';
265                $data  = array(
266                          $operator,
267                          $found,
268                         );
269                $fix   = $phpcsFile->addFixableError($error, $stackPtr, 'SpacingBefore', $data);
270                if ($fix === true) {
271                    $phpcsFile->fixer->beginChangeset();
272                    if ($found === 'newline') {
273                        $i = ($stackPtr - 2);
274                        while ($tokens[$i]['code'] === T_WHITESPACE) {
275                            $phpcsFile->fixer->replaceToken($i, '');
276                            $i--;
277                        }
278                    }
279
280                    $phpcsFile->fixer->replaceToken(($stackPtr - 1), ' ');
281                    $phpcsFile->fixer->endChangeset();
282                }
283            }//end if
284        }//end if
285
286        if (isset($tokens[($stackPtr + 1)]) === false) {
287            return;
288        }
289
290        if ($tokens[($stackPtr + 1)]['code'] !== T_WHITESPACE) {
291            $error = "Expected 1 space after \"$operator\"; 0 found";
292            $fix   = $phpcsFile->addFixableError($error, $stackPtr, 'NoSpaceAfter');
293            if ($fix === true) {
294                $phpcsFile->fixer->addContent($stackPtr, ' ');
295            }
296
297            $phpcsFile->recordMetric($stackPtr, 'Space after operator', 0);
298        } else {
299            if (isset($tokens[($stackPtr + 2)]) === true
300                && $tokens[($stackPtr + 2)]['line'] !== $tokens[$stackPtr]['line']
301            ) {
302                $found = 'newline';
303            } else {
304                $found = $tokens[($stackPtr + 1)]['length'];
305            }
306
307            $phpcsFile->recordMetric($stackPtr, 'Space after operator', $found);
308            if ($found !== 1
309                && ($found !== 'newline' || $this->ignoreNewlines === false)
310            ) {
311                $error = 'Expected 1 space after "%s"; %s found';
312                $data  = array(
313                          $operator,
314                          $found,
315                         );
316                $fix   = $phpcsFile->addFixableError($error, $stackPtr, 'SpacingAfter', $data);
317                if ($fix === true) {
318                    $phpcsFile->fixer->replaceToken(($stackPtr + 1), ' ');
319                }
320            }
321        }//end if
322
323    }//end process()
324
325
326}//end class
327