<?php
/**
* A Sniff to enforce the use of IDENTICAL type operators rather than EQUAL operators.
*
* @author Greg Sherwood <gsherwood@squiz.net>
* @copyright 2006-2015 Squiz Pty Ltd (ABN 77 084 670 600)
* @license https://github.com/squizlabs/PHP_CodeSniffer/blob/master/licence.txt BSD Licence
*/
namespace PHP_CodeSniffer\Standards\Squiz\Sniffs\Operators;
use PHP_CodeSniffer\Files\File;
use PHP_CodeSniffer\Sniffs\Sniff;
use PHP_CodeSniffer\Util\Tokens;
class ComparisonOperatorUsageSniff implements Sniff
{
/**
* A list of tokenizers this sniff supports.
*
* @var array
*/
public $supportedTokenizers = [
'PHP',
'JS',
];
/**
* A list of valid comparison operators.
*
* @var array
*/
private static $validOps = [
T_IS_IDENTICAL => true,
T_IS_NOT_IDENTICAL => true,
T_LESS_THAN => true,
T_GREATER_THAN => true,
T_IS_GREATER_OR_EQUAL => true,
T_IS_SMALLER_OR_EQUAL => true,
T_INSTANCEOF => true,
];
/**
* A list of invalid operators with their alternatives.
*
* @var array<int, string>
*/
private static $invalidOps = [
'PHP' => [
T_IS_EQUAL => '===',
T_IS_NOT_EQUAL => '!==',
T_BOOLEAN_NOT => '=== FALSE',
],
'JS' => [
T_IS_EQUAL => '===',
T_IS_NOT_EQUAL => '!==',
],
];
/**
* Registers the token types that this sniff wishes to listen to.
*
* @return array
*/
public function register()
{
return [
T_IF,
T_ELSEIF,
T_INLINE_THEN,
T_WHILE,
T_FOR,
];
}//end register()
/**
* Process the tokens that this sniff is listening for.
*
* @param \PHP_CodeSniffer\Files\File $phpcsFile The file where the token was found.
* @param int $stackPtr The position in the stack where the token
* was found.
*
* @return void
*/
public function process(File $phpcsFile, $stackPtr)
{
$tokens = $phpcsFile->getTokens();
$tokenizer = $phpcsFile->tokenizerType;
if ($tokens[$stackPtr]['code'] === T_INLINE_THEN) {
$end = $phpcsFile->findPrevious(Tokens::$emptyTokens, ($stackPtr - 1), null, true);
if ($tokens[$end]['code'] !== T_CLOSE_PARENTHESIS) {
// This inline IF statement does not have its condition
// bracketed, so we need to guess where it starts.
for ($i = ($end - 1); $i >= 0; $i--) {
if ($tokens[$i]['code'] === T_SEMICOLON) {
// Stop here as we assume it is the end
// of the previous statement.
break;
} else if ($tokens[$i]['code'] === T_OPEN_TAG) {
// Stop here as this is the start of the file.
break;
} else if ($tokens[$i]['code'] === T_CLOSE_CURLY_BRACKET) {
// Stop if this is the closing brace of
// a code block.
if (isset($tokens[$i]['scope_opener']) === true) {
break;
}
} else if ($tokens[$i]['code'] === T_OPEN_CURLY_BRACKET) {
// Stop if this is the opening brace of
// a code block.
if (isset($tokens[$i]['scope_closer']) === true) {
break;
}
} else if ($tokens[$i]['code'] === T_OPEN_PARENTHESIS) {
// Stop if this is the start of a pair of
// parentheses that surrounds the inline
// IF statement.
if (isset($tokens[$i]['parenthesis_closer']) === true
&& $tokens[$i]['parenthesis_closer'] >= $stackPtr
) {
break;
}
}//end if
}//end for
$start = $phpcsFile->findNext(Tokens::$emptyTokens, ($i + 1), null, true);
} else {
if (isset($tokens[$end]['parenthesis_opener']) === false) {
return;
}
$start = $tokens[$end]['parenthesis_opener'];
}//end if
} else if ($tokens[$stackPtr]['code'] === T_FOR) {
if (isset($tokens[$stackPtr]['parenthesis_opener']) === false) {
return;
}
$openingBracket = $tokens[$stackPtr]['parenthesis_opener'];
$closingBracket = $tokens[$stackPtr]['parenthesis_closer'];
$start = $phpcsFile->findNext(T_SEMICOLON, $openingBracket, $closingBracket);
$end = $phpcsFile->findNext(T_SEMICOLON, ($start + 1), $closingBracket);
if ($start === false || $end === false) {
return;
}
} else {
if (isset($tokens[$stackPtr]['parenthesis_opener']) === false) {
return;
}
$start = $tokens[$stackPtr]['parenthesis_opener'];
$end = $tokens[$stackPtr]['parenthesis_closer'];
}//end if
$requiredOps = 0;
$foundOps = 0;
$foundBooleans = 0;
$lastNonEmpty = $start;
for ($i = $start; $i <= $end; $i++) {
$type = $tokens[$i]['code'];
if (isset(self::$invalidOps[$tokenizer][$type]) === true) {
$error = 'Operator %s prohibited; use %s instead';
$data = [
$tokens[$i]['content'],
self::$invalidOps[$tokenizer][$type],
];
$phpcsFile->addError($error, $i, 'NotAllowed', $data);
$foundOps++;
} else if (isset(self::$validOps[$type]) === true) {
$foundOps++;
}
if ($type === T_OPEN_PARENTHESIS
&& isset($tokens[$i]['parenthesis_closer']) === true
&& isset(Tokens::$functionNameTokens[$tokens[$lastNonEmpty]['code']]) === true
) {
$i = $tokens[$i]['parenthesis_closer'];
$lastNonEmpty = $i;
continue;
}
if ($tokens[$i]['code'] === T_TRUE || $tokens[$i]['code'] === T_FALSE) {
$foundBooleans++;
}
if ($phpcsFile->tokenizerType !== 'JS'
&& ($tokens[$i]['code'] === T_BOOLEAN_AND
|| $tokens[$i]['code'] === T_BOOLEAN_OR)
) {
$requiredOps++;
// When the instanceof operator is used with another operator
// like ===, you can get more ops than are required.
if ($foundOps > $requiredOps) {
$foundOps = $requiredOps;
}
// If we get to here and we have not found the right number of
// comparison operators, then we must have had an implicit
// true operation i.e., if ($a) instead of the required
// if ($a === true), so let's add an error.
if ($requiredOps !== $foundOps) {
$error = 'Implicit true comparisons prohibited; use === TRUE instead';
$phpcsFile->addError($error, $stackPtr, 'ImplicitTrue');
$foundOps++;
}
}
if (isset(Tokens::$emptyTokens[$type]) === false) {
$lastNonEmpty = $i;
}
}//end for
$requiredOps++;
if ($phpcsFile->tokenizerType !== 'JS'
&& $foundOps < $requiredOps
&& ($requiredOps !== $foundBooleans)
) {
$error = 'Implicit true comparisons prohibited; use === TRUE instead';
$phpcsFile->addError($error, $stackPtr, 'ImplicitTrue');
}
}//end process()
}//end class