1<?php
2/**
3 * PEAR_Sniffs_ControlStructures_MultiLineConditionSniff.
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 * PEAR_Sniffs_ControlStructures_MultiLineConditionSniff.
17 *
18 * Ensure multi-line IF conditions are defined correctly.
19 *
20 * @category  PHP
21 * @package   PHP_CodeSniffer
22 * @author    Greg Sherwood <gsherwood@squiz.net>
23 * @copyright 2006-2014 Squiz Pty Ltd (ABN 77 084 670 600)
24 * @license   https://github.com/squizlabs/PHP_CodeSniffer/blob/master/licence.txt BSD Licence
25 * @version   Release: @package_version@
26 * @link      http://pear.php.net/package/PHP_CodeSniffer
27 */
28class PEAR_Sniffs_ControlStructures_MultiLineConditionSniff implements PHP_CodeSniffer_Sniff
29{
30
31    /**
32     * A list of tokenizers this sniff supports.
33     *
34     * @var array
35     */
36    public $supportedTokenizers = array(
37                                   'PHP',
38                                   'JS',
39                                  );
40
41    /**
42     * The number of spaces code should be indented.
43     *
44     * @var int
45     */
46    public $indent = 4;
47
48
49    /**
50     * Returns an array of tokens this test wants to listen for.
51     *
52     * @return array
53     */
54    public function register()
55    {
56        return array(
57                T_IF,
58                T_ELSEIF,
59               );
60
61    }//end register()
62
63
64    /**
65     * Processes this test, when one of its tokens is encountered.
66     *
67     * @param PHP_CodeSniffer_File $phpcsFile The file being scanned.
68     * @param int                  $stackPtr  The position of the current token
69     *                                        in the stack passed in $tokens.
70     *
71     * @return void
72     */
73    public function process(PHP_CodeSniffer_File $phpcsFile, $stackPtr)
74    {
75        $tokens = $phpcsFile->getTokens();
76
77        if (isset($tokens[$stackPtr]['parenthesis_opener']) === false) {
78            return;
79        }
80
81        $openBracket    = $tokens[$stackPtr]['parenthesis_opener'];
82        $closeBracket   = $tokens[$stackPtr]['parenthesis_closer'];
83        $spaceAfterOpen = 0;
84        if ($tokens[($openBracket + 1)]['code'] === T_WHITESPACE) {
85            if (strpos($tokens[($openBracket + 1)]['content'], $phpcsFile->eolChar) !== false) {
86                $spaceAfterOpen = 'newline';
87            } else {
88                $spaceAfterOpen = strlen($tokens[($openBracket + 1)]['content']);
89            }
90        }
91
92        if ($spaceAfterOpen !== 0) {
93            $error = 'First condition of a multi-line IF statement must directly follow the opening parenthesis';
94            $fix   = $phpcsFile->addFixableError($error, ($openBracket + 1), 'SpacingAfterOpenBrace');
95            if ($fix === true) {
96                if ($spaceAfterOpen === 'newline') {
97                    $phpcsFile->fixer->replaceToken(($openBracket + 1), '');
98                } else {
99                    $phpcsFile->fixer->replaceToken(($openBracket + 1), '');
100                }
101            }
102        }
103
104        // We need to work out how far indented the if statement
105        // itself is, so we can work out how far to indent conditions.
106        $statementIndent = 0;
107        for ($i = ($stackPtr - 1); $i >= 0; $i--) {
108            if ($tokens[$i]['line'] !== $tokens[$stackPtr]['line']) {
109                $i++;
110                break;
111            }
112        }
113
114        if ($i >= 0 && $tokens[$i]['code'] === T_WHITESPACE) {
115            $statementIndent = strlen($tokens[$i]['content']);
116        }
117
118        // Each line between the parenthesis should be indented 4 spaces
119        // and start with an operator, unless the line is inside a
120        // function call, in which case it is ignored.
121        $prevLine = $tokens[$openBracket]['line'];
122        for ($i = ($openBracket + 1); $i <= $closeBracket; $i++) {
123            if ($i === $closeBracket && $tokens[$openBracket]['line'] !== $tokens[$i]['line']) {
124                $prev = $phpcsFile->findPrevious(T_WHITESPACE, ($i - 1), null, true);
125                if ($tokens[$prev]['line'] === $tokens[$i]['line']) {
126                    // Closing bracket is on the same line as a condition.
127                    $error = 'Closing parenthesis of a multi-line IF statement must be on a new line';
128                    $fix   = $phpcsFile->addFixableError($error, $closeBracket, 'CloseBracketNewLine');
129                    if ($fix === true) {
130                        // Account for a comment at the end of the line.
131                        $next = $phpcsFile->findNext(T_WHITESPACE, ($closeBracket + 1), null, true);
132                        if ($tokens[$next]['code'] !== T_COMMENT) {
133                            $phpcsFile->fixer->addNewlineBefore($closeBracket);
134                        } else {
135                            $next = $phpcsFile->findNext(PHP_CodeSniffer_Tokens::$emptyTokens, ($next + 1), null, true);
136                            $phpcsFile->fixer->beginChangeset();
137                            $phpcsFile->fixer->replaceToken($closeBracket, '');
138                            $phpcsFile->fixer->addContentBefore($next, ')');
139                            $phpcsFile->fixer->endChangeset();
140                        }
141                    }
142                }
143            }//end if
144
145            if ($tokens[$i]['line'] !== $prevLine) {
146                if ($tokens[$i]['line'] === $tokens[$closeBracket]['line']) {
147                    $next = $phpcsFile->findNext(T_WHITESPACE, $i, null, true);
148                    if ($next !== $closeBracket) {
149                        $expectedIndent = ($statementIndent + $this->indent);
150                    } else {
151                        // Closing brace needs to be indented to the same level
152                        // as the statement.
153                        $expectedIndent = $statementIndent;
154                    }//end if
155                } else {
156                    $expectedIndent = ($statementIndent + $this->indent);
157                }//end if
158
159                if ($tokens[$i]['code'] === T_COMMENT) {
160                    $prevLine = $tokens[$i]['line'];
161                    continue;
162                }
163
164                // We changed lines, so this should be a whitespace indent token.
165                if ($tokens[$i]['code'] !== T_WHITESPACE) {
166                    $foundIndent = 0;
167                } else {
168                    $foundIndent = strlen($tokens[$i]['content']);
169                }
170
171                if ($expectedIndent !== $foundIndent) {
172                    $error = 'Multi-line IF statement not indented correctly; expected %s spaces but found %s';
173                    $data  = array(
174                              $expectedIndent,
175                              $foundIndent,
176                             );
177
178                    $fix = $phpcsFile->addFixableError($error, $i, 'Alignment', $data);
179                    if ($fix === true) {
180                        $spaces = str_repeat(' ', $expectedIndent);
181                        if ($foundIndent === 0) {
182                            $phpcsFile->fixer->addContentBefore($i, $spaces);
183                        } else {
184                            $phpcsFile->fixer->replaceToken($i, $spaces);
185                        }
186                    }
187                }
188
189                $next = $phpcsFile->findNext(PHP_CodeSniffer_Tokens::$emptyTokens, $i, null, true);
190                if ($next !== $closeBracket) {
191                    if (isset(PHP_CodeSniffer_Tokens::$booleanOperators[$tokens[$next]['code']]) === false) {
192                        $error = 'Each line in a multi-line IF statement must begin with a boolean operator';
193                        $fix   = $phpcsFile->addFixableError($error, $i, 'StartWithBoolean');
194                        if ($fix === true) {
195                            $prev = $phpcsFile->findPrevious(PHP_CodeSniffer_Tokens::$emptyTokens, ($i - 1), $openBracket, true);
196                            if (isset(PHP_CodeSniffer_Tokens::$booleanOperators[$tokens[$prev]['code']]) === true) {
197                                $phpcsFile->fixer->beginChangeset();
198                                $phpcsFile->fixer->replaceToken($prev, '');
199                                $phpcsFile->fixer->addContentBefore($next, $tokens[$prev]['content'].' ');
200                                $phpcsFile->fixer->endChangeset();
201                            } else {
202                                for ($x = ($prev + 1); $x < $next; $x++) {
203                                    $phpcsFile->fixer->replaceToken($x, '');
204                                }
205                            }
206                        }
207                    }
208                }//end if
209
210                $prevLine = $tokens[$i]['line'];
211            }//end if
212
213            if ($tokens[$i]['code'] === T_STRING) {
214                $next = $phpcsFile->findNext(T_WHITESPACE, ($i + 1), null, true);
215                if ($tokens[$next]['code'] === T_OPEN_PARENTHESIS) {
216                    // This is a function call, so skip to the end as they
217                    // have their own indentation rules.
218                    $i        = $tokens[$next]['parenthesis_closer'];
219                    $prevLine = $tokens[$i]['line'];
220                    continue;
221                }
222            }
223        }//end for
224
225        // From here on, we are checking the spacing of the opening and closing
226        // braces. If this IF statement does not use braces, we end here.
227        if (isset($tokens[$stackPtr]['scope_opener']) === false) {
228            return;
229        }
230
231        // The opening brace needs to be one space away from the closing parenthesis.
232        $openBrace = $tokens[$stackPtr]['scope_opener'];
233        $next      = $phpcsFile->findNext(T_WHITESPACE, ($closeBracket + 1), $openBrace, true);
234        if ($next !== false) {
235            // Probably comments in between tokens, so don't check.
236            return;
237        }
238
239        if ($tokens[$openBrace]['line'] > $tokens[$closeBracket]['line']) {
240            $length = -1;
241        } else if ($openBrace === ($closeBracket + 1)) {
242            $length = 0;
243        } else if ($openBrace === ($closeBracket + 2)
244            && $tokens[($closeBracket + 1)]['code'] === T_WHITESPACE
245        ) {
246            $length = strlen($tokens[($closeBracket + 1)]['content']);
247        } else {
248            // Confused, so don't check.
249            $length = 1;
250        }
251
252        if ($length === 1) {
253            return;
254        }
255
256        $data = array($length);
257        $code = 'SpaceBeforeOpenBrace';
258
259        $error = 'There must be a single space between the closing parenthesis and the opening brace of a multi-line IF statement; found ';
260        if ($length === -1) {
261            $error .= 'newline';
262            $code   = 'NewlineBeforeOpenBrace';
263        } else {
264            $error .= '%s spaces';
265        }
266
267        $fix = $phpcsFile->addFixableError($error, ($closeBracket + 1), $code, $data);
268        if ($fix === true) {
269            if ($length === 0) {
270                $phpcsFile->fixer->addContent($closeBracket, ' ');
271            } else {
272                $phpcsFile->fixer->replaceToken(($closeBracket + 1), ' ');
273            }
274        }
275
276    }//end process()
277
278
279}//end class
280