1<?php
2/**
3 * A Sniff to enforce the use of IDENTICAL type operators rather than EQUAL operators.
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 * A Sniff to enforce the use of IDENTICAL type operators rather than EQUAL operators.
18 *
19 * The use of === true is enforced over implicit true statements,
20 * for example:
21 *
22 * <code>
23 * if ($a)
24 * {
25 *     ...
26 * }
27 * </code>
28 *
29 * should be:
30 *
31 * <code>
32 * if ($a === true)
33 * {
34 *     ...
35 * }
36 * </code>
37 *
38 * It also enforces the use of === false over ! operators.
39 *
40 * @category  PHP
41 * @package   PHP_CodeSniffer
42 * @author    Greg Sherwood <gsherwood@squiz.net>
43 * @author    Marc McIntyre <mmcintyre@squiz.net>
44 * @copyright 2006-2014 Squiz Pty Ltd (ABN 77 084 670 600)
45 * @license   https://github.com/squizlabs/PHP_CodeSniffer/blob/master/licence.txt BSD Licence
46 * @version   Release: @package_version@
47 * @link      http://pear.php.net/package/PHP_CodeSniffer
48 */
49class Squiz_Sniffs_Operators_ComparisonOperatorUsageSniff implements PHP_CodeSniffer_Sniff
50{
51
52    /**
53     * A list of tokenizers this sniff supports.
54     *
55     * @var array
56     */
57    public $supportedTokenizers = array(
58                                   'PHP',
59                                   'JS',
60                                  );
61
62    /**
63     * A list of valid comparison operators.
64     *
65     * @var array
66     */
67    private static $_validOps = array(
68                                 T_IS_IDENTICAL,
69                                 T_IS_NOT_IDENTICAL,
70                                 T_LESS_THAN,
71                                 T_GREATER_THAN,
72                                 T_IS_GREATER_OR_EQUAL,
73                                 T_IS_SMALLER_OR_EQUAL,
74                                 T_INSTANCEOF,
75                                );
76
77    /**
78     * A list of invalid operators with their alternatives.
79     *
80     * @var array(int => string)
81     */
82    private static $_invalidOps = array(
83                                   'PHP' => array(
84                                             T_IS_EQUAL     => '===',
85                                             T_IS_NOT_EQUAL => '!==',
86                                             T_BOOLEAN_NOT  => '=== FALSE',
87                                            ),
88                                   'JS'  => array(
89                                             T_IS_EQUAL     => '===',
90                                             T_IS_NOT_EQUAL => '!==',
91                                            ),
92                                  );
93
94
95    /**
96     * Registers the token types that this sniff wishes to listen to.
97     *
98     * @return array
99     */
100    public function register()
101    {
102        return array(
103                T_IF,
104                T_ELSEIF,
105                T_INLINE_THEN,
106                T_WHILE,
107                T_FOR,
108               );
109
110    }//end register()
111
112
113    /**
114     * Process the tokens that this sniff is listening for.
115     *
116     * @param PHP_CodeSniffer_File $phpcsFile The file where the token was found.
117     * @param int                  $stackPtr  The position in the stack where the token
118     *                                        was found.
119     *
120     * @return void
121     */
122    public function process(PHP_CodeSniffer_File $phpcsFile, $stackPtr)
123    {
124        $tokens    = $phpcsFile->getTokens();
125        $tokenizer = $phpcsFile->tokenizerType;
126
127        if ($tokens[$stackPtr]['code'] === T_INLINE_THEN) {
128            $end = $phpcsFile->findPrevious(PHP_CodeSniffer_Tokens::$emptyTokens, ($stackPtr - 1), null, true);
129            if ($tokens[$end]['code'] !== T_CLOSE_PARENTHESIS) {
130                // This inline IF statement does not have its condition
131                // bracketed, so we need to guess where it starts.
132                for ($i = ($end - 1); $i >= 0; $i--) {
133                    if ($tokens[$i]['code'] === T_SEMICOLON) {
134                        // Stop here as we assume it is the end
135                        // of the previous statement.
136                        break;
137                    } else if ($tokens[$i]['code'] === T_OPEN_TAG) {
138                        // Stop here as this is the start of the file.
139                        break;
140                    } else if ($tokens[$i]['code'] === T_CLOSE_CURLY_BRACKET) {
141                        // Stop if this is the closing brace of
142                        // a code block.
143                        if (isset($tokens[$i]['scope_opener']) === true) {
144                            break;
145                        }
146                    } else if ($tokens[$i]['code'] === T_OPEN_CURLY_BRACKET) {
147                        // Stop if this is the opening brace of
148                        // a code block.
149                        if (isset($tokens[$i]['scope_closer']) === true) {
150                            break;
151                        }
152                    }
153                }//end for
154
155                $start = $phpcsFile->findNext(PHP_CodeSniffer_Tokens::$emptyTokens, ($i + 1), null, true);
156            } else {
157                if (isset($tokens[$end]['parenthesis_opener']) === false) {
158                    return;
159                }
160
161                $start = $tokens[$end]['parenthesis_opener'];
162            }//end if
163        } else if ($tokens[$stackPtr]['code'] === T_FOR) {
164            if (isset($tokens[$stackPtr]['parenthesis_opener']) === false) {
165                return;
166            }
167
168            $openingBracket = $tokens[$stackPtr]['parenthesis_opener'];
169            $closingBracket = $tokens[$stackPtr]['parenthesis_closer'];
170
171            $start = $phpcsFile->findNext(T_SEMICOLON, $openingBracket, $closingBracket);
172            $end   = $phpcsFile->findNext(T_SEMICOLON, ($start + 1), $closingBracket);
173            if ($start === false || $end === false) {
174                return;
175            }
176        } else {
177            if (isset($tokens[$stackPtr]['parenthesis_opener']) === false) {
178                return;
179            }
180
181            $start = $tokens[$stackPtr]['parenthesis_opener'];
182            $end   = $tokens[$stackPtr]['parenthesis_closer'];
183        }//end if
184
185        $requiredOps = 0;
186        $foundOps    = 0;
187        $foundBools  = 0;
188
189        for ($i = $start; $i <= $end; $i++) {
190            $type = $tokens[$i]['code'];
191            if (in_array($type, array_keys(self::$_invalidOps[$tokenizer])) === true) {
192                $error = 'Operator %s prohibited; use %s instead';
193                $data  = array(
194                          $tokens[$i]['content'],
195                          self::$_invalidOps[$tokenizer][$type],
196                         );
197                $phpcsFile->addError($error, $i, 'NotAllowed', $data);
198                $foundOps++;
199            } else if (in_array($type, self::$_validOps) === true) {
200                $foundOps++;
201            }
202
203            if ($tokens[$i]['code'] === T_TRUE || $tokens[$i]['code'] === T_FALSE) {
204                $foundBools++;
205            }
206
207            if ($phpcsFile->tokenizerType !== 'JS'
208                && ($tokens[$i]['code'] === T_BOOLEAN_AND
209                || $tokens[$i]['code'] === T_BOOLEAN_OR)
210            ) {
211                $requiredOps++;
212
213                // When the instanceof operator is used with another operator
214                // like ===, you can get more ops than are required.
215                if ($foundOps > $requiredOps) {
216                    $foundOps = $requiredOps;
217                }
218
219                // If we get to here and we have not found the right number of
220                // comparison operators, then we must have had an implicit
221                // true operation i.e., if ($a) instead of the required
222                // if ($a === true), so let's add an error.
223                if ($requiredOps !== $foundOps) {
224                    $error = 'Implicit true comparisons prohibited; use === TRUE instead';
225                    $phpcsFile->addError($error, $stackPtr, 'ImplicitTrue');
226                    $foundOps++;
227                }
228            }
229        }//end for
230
231        $requiredOps++;
232
233        if ($phpcsFile->tokenizerType !== 'JS'
234            && $foundOps < $requiredOps
235            && ($requiredOps !== $foundBools)
236        ) {
237            $error = 'Implicit true comparisons prohibited; use === TRUE instead';
238            $phpcsFile->addError($error, $stackPtr, 'ImplicitTrue');
239        }
240
241    }//end process()
242
243
244}//end class
245