diff --git a/package.xml b/package.xml index a66c5b6256..739f44e648 100644 --- a/package.xml +++ b/package.xml @@ -466,6 +466,7 @@ http://pear.php.net/dtd/package-2.0.xsd"> + @@ -602,6 +603,8 @@ http://pear.php.net/dtd/package-2.0.xsd"> + + diff --git a/src/Standards/Generic/Sniffs/CodeAnalysis/MixedBooleanOperatorSniff.php b/src/Standards/Generic/Sniffs/CodeAnalysis/MixedBooleanOperatorSniff.php new file mode 100644 index 0000000000..6e3a070147 --- /dev/null +++ b/src/Standards/Generic/Sniffs/CodeAnalysis/MixedBooleanOperatorSniff.php @@ -0,0 +1,116 @@ + + * $one = false; + * $two = false; + * $three = true; + * + * $result = $one && $two || $three; + * + * $result3 = $one && !$two xor $three; + * + * + * if ( + * $result && !$result3 + * || !$result && $result3 + * ) {} + * + * + * @author Tim Duesterhus + * @copyright 2021 WoltLab GmbH. + * @license https://github.com/squizlabs/PHP_CodeSniffer/blob/master/licence.txt BSD Licence + */ + +namespace PHP_CodeSniffer\Standards\Generic\Sniffs\CodeAnalysis; + +use PHP_CodeSniffer\Files\File; +use PHP_CodeSniffer\Sniffs\Sniff; +use PHP_CodeSniffer\Util\Tokens; + +class MixedBooleanOperatorSniff implements Sniff +{ + + /** + * Array of tokens this test searches for to find either a boolean + * operator or the start of the current (sub-)expression. Used for + * performance optimization purposes. + * + * @var array + */ + private $searchTargets = []; + + + /** + * Returns an array of tokens this test wants to listen for. + * + * @return array + */ + public function register() + { + $this->searchTargets = Tokens::$booleanOperators; + $this->searchTargets += Tokens::$blockOpeners; + $this->searchTargets[\T_INLINE_THEN] = \T_INLINE_THEN; + $this->searchTargets[\T_INLINE_ELSE] = \T_INLINE_ELSE; + + return Tokens::$booleanOperators; + + }//end register() + + + /** + * Processes this test, when one of its tokens is encountered. + * + * @param \PHP_CodeSniffer\Files\File $phpcsFile The file being scanned. + * @param int $stackPtr The position of the current token + * in the stack passed in $tokens. + * + * @return void + */ + public function process(File $phpcsFile, $stackPtr) + { + $tokens = $phpcsFile->getTokens(); + + $start = $phpcsFile->findStartOfStatement($stackPtr); + + $previous = $phpcsFile->findPrevious( + $this->searchTargets, + ($stackPtr - 1), + $start, + false, + null, + true + ); + + if ($previous === false) { + // No token found. + return; + } + + if ($tokens[$previous]['code'] === $tokens[$stackPtr]['code']) { + // Identical operator found. + return; + } + + if (\in_array($tokens[$previous]['code'], [\T_INLINE_THEN, \T_INLINE_ELSE], true) === true) { + // Beginning of the expression found for the ternary conditional operator. + return; + } + + if (isset(Tokens::$blockOpeners[$tokens[$previous]['code']]) === true) { + // Beginning of the expression found for a block opener. Needed to + // correctly handle match arms. + return; + } + + // We found a mismatching operator, thus we must report the error. + $error = 'Mixing different binary boolean operators within an expression'; + $error .= ' without using parentheses to clarify precedence is not allowed.'; + $phpcsFile->addError($error, $stackPtr, 'MissingParentheses'); + + }//end process() + + +}//end class diff --git a/src/Standards/Generic/Tests/CodeAnalysis/MixedBooleanOperatorUnitTest.inc b/src/Standards/Generic/Tests/CodeAnalysis/MixedBooleanOperatorUnitTest.inc new file mode 100644 index 0000000000..f46ab9081f --- /dev/null +++ b/src/Standards/Generic/Tests/CodeAnalysis/MixedBooleanOperatorUnitTest.inc @@ -0,0 +1,131 @@ + true, +}; + +match (true) { + // Not OK. + $a || $b && $c => true, +}; + +match (true) { + // OK. + $a || $b => true, + $a && $b => true, +}; + +match (true) { + // Debatable. + $a || $b, $a && $b => true, +}; + +// OK. +$foo = fn ($a, $b, $c) => $a && ($b || $c); + +// Not OK. +$foo = fn ($a, $b, $c) => $a && $b || $c; + +// OK. +$foo = $a && (fn ($a, $b, $c) => $a || $b); + +// Debatable. +$foo = $a && fn ($a, $b, $c) => $a || $b; + +// OK. +\array_map( + fn ($a, $b, $c) => $a || $b, + $a && $b +); + +match (true) { + // Not OK. + $a || ($b && $c) && $d => true, + // Not OK. + $b && $c['a'] || $d => true, + // Not OK. + $b && ${$var} || $d => true, +}; diff --git a/src/Standards/Generic/Tests/CodeAnalysis/MixedBooleanOperatorUnitTest.php b/src/Standards/Generic/Tests/CodeAnalysis/MixedBooleanOperatorUnitTest.php new file mode 100644 index 0000000000..74a0b7530a --- /dev/null +++ b/src/Standards/Generic/Tests/CodeAnalysis/MixedBooleanOperatorUnitTest.php @@ -0,0 +1,85 @@ + + * @copyright 2021 WoltLab GmbH. + * @license https://github.com/squizlabs/PHP_CodeSniffer/blob/master/licence.txt BSD Licence + */ + +namespace PHP_CodeSniffer\Standards\Generic\Tests\CodeAnalysis; + +use PHP_CodeSniffer\Tests\Standards\AbstractSniffUnitTest; + +class MixedBooleanOperatorUnitTest extends AbstractSniffUnitTest +{ + + + /** + * Returns the lines where errors should occur. + * + * The key of the array should represent the line number and the value + * should represent the number of errors that should occur on that line. + * + * @return array + */ + public function getErrorList() + { + return [ + 3 => 1, + 7 => 1, + 12 => 1, + 17 => 1, + 29 => 1, + 31 => 1, + 33 => 1, + 34 => 1, + 35 => 1, + 37 => 1, + 39 => 1, + 41 => 2, + 43 => 2, + 44 => 1, + 47 => 1, + 61 => 1, + 65 => 3, + 68 => 2, + 71 => 1, + 72 => 1, + 73 => 1, + 76 => 2, + 78 => 1, + 79 => 1, + 80 => 1, + 81 => 2, + 83 => 1, + 92 => 1, + 110 => 1, + 126 => 1, + 128 => 1, + 130 => 1, + + // Debatable. + 103 => 1, + 116 => 1, + ]; + + }//end getErrorList() + + + /** + * Returns the lines where warnings should occur. + * + * The key of the array should represent the line number and the value + * should represent the number of warnings that should occur on that line. + * + * @return array + */ + public function getWarningList() + { + return []; + + }//end getWarningList() + + +}//end class