1<?php
2/**
3 * Generic_Sniffs_Formatting_MultipleStatementAlignmentSniff.
4 *
5 * PHP version 5
6 *
7 * @category  PHP
8 * @package   PHP_CodeSniffer
9 * @author    Greg Sherwood <gsherwood@squiz.net>
10 * @copyright 2006-2014 Squiz Pty Ltd (ABN 77 084 670 600)
11 * @license   https://github.com/squizlabs/PHP_CodeSniffer/blob/master/licence.txt BSD Licence
12 * @link      http://pear.php.net/package/PHP_CodeSniffer
13 */
14
15/**
16 * Generic_Sniffs_Formatting_MultipleStatementAlignmentSniff.
17 *
18 * Checks alignment of assignments. If there are multiple adjacent assignments,
19 * it will check that the equals signs of each assignment are aligned. It will
20 * display a warning to advise that the signs should be aligned.
21 *
22 * @category  PHP
23 * @package   PHP_CodeSniffer
24 * @author    Greg Sherwood <gsherwood@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_Formatting_MultipleStatementAlignmentSniff 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 = false;
49
50    /**
51     * The maximum amount of padding before the alignment is ignored.
52     *
53     * If the amount of padding required to align this assignment with the
54     * surrounding assignments exceeds this number, the assignment will be
55     * ignored and no errors or warnings will be thrown.
56     *
57     * @var int
58     */
59    public $maxPadding = 1000;
60
61
62    /**
63     * Returns an array of tokens this test wants to listen for.
64     *
65     * @return array
66     */
67    public function register()
68    {
69        $tokens = PHP_CodeSniffer_Tokens::$assignmentTokens;
70        unset($tokens[T_DOUBLE_ARROW]);
71        return $tokens;
72
73    }//end register()
74
75
76    /**
77     * Processes this test, when one of its tokens is encountered.
78     *
79     * @param PHP_CodeSniffer_File $phpcsFile The file being scanned.
80     * @param int                  $stackPtr  The position of the current token
81     *                                        in the stack passed in $tokens.
82     *
83     * @return int
84     */
85    public function process(PHP_CodeSniffer_File $phpcsFile, $stackPtr)
86    {
87        $tokens = $phpcsFile->getTokens();
88
89        // Ignore assignments used in a condition, like an IF or FOR.
90        if (isset($tokens[$stackPtr]['nested_parenthesis']) === true) {
91            foreach ($tokens[$stackPtr]['nested_parenthesis'] as $start => $end) {
92                if (isset($tokens[$start]['parenthesis_owner']) === true) {
93                    return;
94                }
95            }
96        }
97
98        $lastAssign = $this->checkAlignment($phpcsFile, $stackPtr);
99        return ($lastAssign + 1);
100
101    }//end process()
102
103
104    /**
105     * Processes this test, when one of its tokens is encountered.
106     *
107     * @param PHP_CodeSniffer_File $phpcsFile The file being scanned.
108     * @param int                  $stackPtr  The position of the current token
109     *                                        in the stack passed in $tokens.
110     *
111     * @return int
112     */
113    public function checkAlignment(PHP_CodeSniffer_File $phpcsFile, $stackPtr)
114    {
115        $tokens = $phpcsFile->getTokens();
116
117        $assignments = array();
118        $prevAssign  = null;
119        $lastLine    = $tokens[$stackPtr]['line'];
120        $maxPadding  = null;
121        $stopped     = null;
122        $lastCode    = $stackPtr;
123        $lastSemi    = null;
124
125        $find = PHP_CodeSniffer_Tokens::$assignmentTokens;
126        unset($find[T_DOUBLE_ARROW]);
127
128        for ($assign = $stackPtr; $assign < $phpcsFile->numTokens; $assign++) {
129            if (isset($find[$tokens[$assign]['code']]) === false) {
130                // A blank line indicates that the assignment block has ended.
131                if (isset(PHP_CodeSniffer_Tokens::$emptyTokens[$tokens[$assign]['code']]) === false) {
132                    if (($tokens[$assign]['line'] - $tokens[$lastCode]['line']) > 1) {
133                        break;
134                    }
135
136                    $lastCode = $assign;
137
138                    if ($tokens[$assign]['code'] === T_SEMICOLON) {
139                        if ($tokens[$assign]['conditions'] === $tokens[$stackPtr]['conditions']) {
140                            if ($lastSemi !== null && $prevAssign !== null && $lastSemi > $prevAssign) {
141                                // This statement did not have an assignment operator in it.
142                                break;
143                            } else {
144                                $lastSemi = $assign;
145                            }
146                        } else {
147                            // Statement is in a different context, so the block is over.
148                            break;
149                        }
150                    }
151                }//end if
152
153                continue;
154            } else if ($assign !== $stackPtr && $tokens[$assign]['line'] === $lastLine) {
155                // Skip multiple assignments on the same line. We only need to
156                // try and align the first assignment.
157                continue;
158            }//end if
159
160            if ($assign !== $stackPtr) {
161                // Has to be nested inside the same conditions as the first assignment.
162                if ($tokens[$assign]['conditions'] !== $tokens[$stackPtr]['conditions']) {
163                    break;
164                }
165
166                // Make sure it is not assigned inside a condition (eg. IF, FOR).
167                if (isset($tokens[$assign]['nested_parenthesis']) === true) {
168                    foreach ($tokens[$assign]['nested_parenthesis'] as $start => $end) {
169                        if (isset($tokens[$start]['parenthesis_owner']) === true) {
170                            break(2);
171                        }
172                    }
173                }
174            }//end if
175
176            $var = $phpcsFile->findPrevious(
177                PHP_CodeSniffer_Tokens::$emptyTokens,
178                ($assign - 1),
179                null,
180                true
181            );
182
183            // Make sure we wouldn't break our max padding length if we
184            // aligned with this statement, or they wouldn't break the max
185            // padding length if they aligned with us.
186            $varEnd    = $tokens[($var + 1)]['column'];
187            $assignLen = $tokens[$assign]['length'];
188            if ($assign !== $stackPtr) {
189                if (($varEnd + 1) > $assignments[$prevAssign]['assign_col']) {
190                    $padding      = 1;
191                    $assignColumn = ($varEnd + 1);
192                } else {
193                    $padding = ($assignments[$prevAssign]['assign_col'] - $varEnd + $assignments[$prevAssign]['assign_len'] - $assignLen);
194                    if ($padding === 0) {
195                        $padding = 1;
196                    }
197
198                    if ($padding > $this->maxPadding) {
199                        $stopped = $assign;
200                        break;
201                    }
202
203                    $assignColumn = ($varEnd + $padding);
204                }//end if
205
206                if (($assignColumn + $assignLen) > ($assignments[$maxPadding]['assign_col'] + $assignments[$maxPadding]['assign_len'])) {
207                    $newPadding = ($varEnd - $assignments[$maxPadding]['var_end'] + $assignLen - $assignments[$maxPadding]['assign_len'] + 1);
208                    if ($newPadding > $this->maxPadding) {
209                        $stopped = $assign;
210                        break;
211                    } else {
212                        // New alignment settings for previous assignments.
213                        foreach ($assignments as $i => $data) {
214                            if ($i === $assign) {
215                                break;
216                            }
217
218                            $newPadding = ($varEnd - $data['var_end'] + $assignLen - $data['assign_len'] + 1);
219                            $assignments[$i]['expected']   = $newPadding;
220                            $assignments[$i]['assign_col'] = ($data['var_end'] + $newPadding);
221                        }
222
223                        $padding      = 1;
224                        $assignColumn = ($varEnd + 1);
225                    }
226                } else if ($padding > $assignments[$maxPadding]['expected']) {
227                    $maxPadding = $assign;
228                }//end if
229            } else {
230                $padding      = 1;
231                $assignColumn = ($varEnd + 1);
232                $maxPadding   = $assign;
233            }//end if
234
235            $found = 0;
236            if ($tokens[($var + 1)]['code'] === T_WHITESPACE) {
237                $found = $tokens[($var + 1)]['length'];
238                if ($found === 0) {
239                    // This means a newline was found.
240                    $found = 1;
241                }
242            }
243
244            $assignments[$assign] = array(
245                                     'var_end'    => $varEnd,
246                                     'assign_len' => $assignLen,
247                                     'assign_col' => $assignColumn,
248                                     'expected'   => $padding,
249                                     'found'      => $found,
250                                    );
251
252            $lastLine   = $tokens[$assign]['line'];
253            $prevAssign = $assign;
254        }//end for
255
256        if (empty($assignments) === true) {
257            return $stackPtr;
258        }
259
260        $numAssignments = count($assignments);
261
262        $errorGenerated = false;
263        foreach ($assignments as $assignment => $data) {
264            if ($data['found'] === $data['expected']) {
265                continue;
266            }
267
268            $expectedText = $data['expected'].' space';
269            if ($data['expected'] !== 1) {
270                $expectedText .= 's';
271            }
272
273            if ($data['found'] === null) {
274                $foundText = 'a new line';
275            } else {
276                $foundText = $data['found'].' space';
277                if ($data['found'] !== 1) {
278                    $foundText .= 's';
279                }
280            }
281
282            if ($numAssignments === 1) {
283                $type  = 'Incorrect';
284                $error = 'Equals sign not aligned correctly; expected %s but found %s';
285            } else {
286                $type  = 'NotSame';
287                $error = 'Equals sign not aligned with surrounding assignments; expected %s but found %s';
288            }
289
290            $errorData = array(
291                          $expectedText,
292                          $foundText,
293                         );
294
295            if ($this->error === true) {
296                $fix = $phpcsFile->addFixableError($error, $assignment, $type, $errorData);
297            } else {
298                $fix = $phpcsFile->addFixableWarning($error, $assignment, $type.'Warning', $errorData);
299            }
300
301            $errorGenerated = true;
302
303            if ($fix === true && $data['found'] !== null) {
304                $newContent = str_repeat(' ', $data['expected']);
305                if ($data['found'] === 0) {
306                    $phpcsFile->fixer->addContentBefore($assignment, $newContent);
307                } else {
308                    $phpcsFile->fixer->replaceToken(($assignment - 1), $newContent);
309                }
310            }
311        }//end foreach
312
313        if ($numAssignments > 1) {
314            if ($errorGenerated === true) {
315                $phpcsFile->recordMetric($stackPtr, 'Adjacent assignments aligned', 'no');
316            } else {
317                $phpcsFile->recordMetric($stackPtr, 'Adjacent assignments aligned', 'yes');
318            }
319        }
320
321        if ($stopped !== null) {
322            return $this->checkAlignment($phpcsFile, $stopped);
323        } else {
324            return $assignment;
325        }
326
327    }//end checkAlignment()
328
329
330}//end class
331