From a2d35041e0dea91532fd487583788ca99d18fd29 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20D=C3=BCsterhus?= Date: Tue, 2 Feb 2021 16:20:44 +0100 Subject: [PATCH 1/2] Add MixedBooleanOperatorSniff --- package.xml | 3 + .../MixedBooleanOperatorSniff.php | 104 ++++++++++++++++++ .../MixedBooleanOperatorUnitTest.inc | 41 +++++++ .../MixedBooleanOperatorUnitTest.php | 61 ++++++++++ 4 files changed, 209 insertions(+) create mode 100644 src/Standards/Generic/Sniffs/CodeAnalysis/MixedBooleanOperatorSniff.php create mode 100644 src/Standards/Generic/Tests/CodeAnalysis/MixedBooleanOperatorUnitTest.inc create mode 100644 src/Standards/Generic/Tests/CodeAnalysis/MixedBooleanOperatorUnitTest.php 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..5fd9d113ba --- /dev/null +++ b/src/Standards/Generic/Sniffs/CodeAnalysis/MixedBooleanOperatorSniff.php @@ -0,0 +1,104 @@ + + * $var = true && true || true; + * + * + * @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; + +class MixedBooleanOperatorSniff implements Sniff +{ + + + /** + * Registers the tokens that this sniff wants to listen for. + * + * @return int[] + */ + public function register() + { + return [ + T_BOOLEAN_OR, + T_BOOLEAN_AND, + ]; + + }//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(); + $token = $tokens[$stackPtr]; + + $start = $phpcsFile->findStartOfStatement($stackPtr); + + if ($token['code'] === T_BOOLEAN_AND) { + $search = T_BOOLEAN_OR; + } else if ($token['code'] === T_BOOLEAN_OR) { + $search = T_BOOLEAN_AND; + } else { + throw new \LogicException('Unreachable'); + } + + while (true) { + $previous = $phpcsFile->findPrevious( + [ + $search, + T_OPEN_PARENTHESIS, + T_OPEN_SQUARE_BRACKET, + T_CLOSE_PARENTHESIS, + T_CLOSE_SQUARE_BRACKET, + ], + $stackPtr, + $start + ); + + if ($previous === false) { + break; + } + + if ($tokens[$previous]['code'] === T_OPEN_PARENTHESIS + || $tokens[$previous]['code'] === T_OPEN_SQUARE_BRACKET + ) { + // We halt if we reach the opening parens / bracket of the boolean operator. + return; + } else if ($tokens[$previous]['code'] === T_CLOSE_PARENTHESIS) { + // We skip the content of nested parens. + $stackPtr = ($tokens[$previous]['parenthesis_opener'] - 1); + } else if ($tokens[$previous]['code'] === T_CLOSE_SQUARE_BRACKET) { + // We skip the content of nested brackets. + $stackPtr = ($tokens[$previous]['bracket_opener'] - 1); + } else if ($tokens[$previous]['code'] === $search) { + // We reached a mismatching operator, thus we must report the error. + $error = "Mixed '&&' and '||' within an expression without using parentheses."; + $phpcsFile->addError($error, $stackPtr, 'MissingParentheses', []); + return; + } else { + throw new \LogicException('Unreachable'); + } + }//end while + + }//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..cc88ecd189 --- /dev/null +++ b/src/Standards/Generic/Tests/CodeAnalysis/MixedBooleanOperatorUnitTest.inc @@ -0,0 +1,41 @@ + + * @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, + ]; + + }//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 From 3f3b5223022a0a31a1485d93b753d5d49694e948 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20D=C3=BCsterhus?= Date: Mon, 16 Oct 2023 14:32:20 +0200 Subject: [PATCH 2/2] Sync implementation with PHPCSStandards/PHPCSExtra#271 --- .../MixedBooleanOperatorSniff.php | 118 ++++++++++-------- .../MixedBooleanOperatorUnitTest.inc | 114 +++++++++++++++-- .../MixedBooleanOperatorUnitTest.php | 48 +++++-- 3 files changed, 203 insertions(+), 77 deletions(-) diff --git a/src/Standards/Generic/Sniffs/CodeAnalysis/MixedBooleanOperatorSniff.php b/src/Standards/Generic/Sniffs/CodeAnalysis/MixedBooleanOperatorSniff.php index 5fd9d113ba..6e3a070147 100644 --- a/src/Standards/Generic/Sniffs/CodeAnalysis/MixedBooleanOperatorSniff.php +++ b/src/Standards/Generic/Sniffs/CodeAnalysis/MixedBooleanOperatorSniff.php @@ -1,10 +1,22 @@ - * $var = true && true || true; + * $one = false; + * $two = false; + * $three = true; + * + * $result = $one && $two || $three; + * + * $result3 = $one && !$two xor $three; + * + * + * if ( + * $result && !$result3 + * || !$result && $result3 + * ) {} * * * @author Tim Duesterhus @@ -16,22 +28,34 @@ 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 = []; + /** - * Registers the tokens that this sniff wants to listen for. + * Returns an array of tokens this test wants to listen for. * - * @return int[] + * @return array */ public function register() { - return [ - T_BOOLEAN_OR, - T_BOOLEAN_AND, - ]; + $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() @@ -48,55 +72,43 @@ public function register() public function process(File $phpcsFile, $stackPtr) { $tokens = $phpcsFile->getTokens(); - $token = $tokens[$stackPtr]; $start = $phpcsFile->findStartOfStatement($stackPtr); - if ($token['code'] === T_BOOLEAN_AND) { - $search = T_BOOLEAN_OR; - } else if ($token['code'] === T_BOOLEAN_OR) { - $search = T_BOOLEAN_AND; - } else { - throw new \LogicException('Unreachable'); + $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; } - while (true) { - $previous = $phpcsFile->findPrevious( - [ - $search, - T_OPEN_PARENTHESIS, - T_OPEN_SQUARE_BRACKET, - T_CLOSE_PARENTHESIS, - T_CLOSE_SQUARE_BRACKET, - ], - $stackPtr, - $start - ); - - if ($previous === false) { - break; - } - - if ($tokens[$previous]['code'] === T_OPEN_PARENTHESIS - || $tokens[$previous]['code'] === T_OPEN_SQUARE_BRACKET - ) { - // We halt if we reach the opening parens / bracket of the boolean operator. - return; - } else if ($tokens[$previous]['code'] === T_CLOSE_PARENTHESIS) { - // We skip the content of nested parens. - $stackPtr = ($tokens[$previous]['parenthesis_opener'] - 1); - } else if ($tokens[$previous]['code'] === T_CLOSE_SQUARE_BRACKET) { - // We skip the content of nested brackets. - $stackPtr = ($tokens[$previous]['bracket_opener'] - 1); - } else if ($tokens[$previous]['code'] === $search) { - // We reached a mismatching operator, thus we must report the error. - $error = "Mixed '&&' and '||' within an expression without using parentheses."; - $phpcsFile->addError($error, $stackPtr, 'MissingParentheses', []); - return; - } else { - throw new \LogicException('Unreachable'); - } - }//end while + // 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() diff --git a/src/Standards/Generic/Tests/CodeAnalysis/MixedBooleanOperatorUnitTest.inc b/src/Standards/Generic/Tests/CodeAnalysis/MixedBooleanOperatorUnitTest.inc index cc88ecd189..f46ab9081f 100644 --- a/src/Standards/Generic/Tests/CodeAnalysis/MixedBooleanOperatorUnitTest.inc +++ b/src/Standards/Generic/Tests/CodeAnalysis/MixedBooleanOperatorUnitTest.inc @@ -1,20 +1,20 @@ 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 index 889d4c9ea0..74a0b7530a 100644 --- a/src/Standards/Generic/Tests/CodeAnalysis/MixedBooleanOperatorUnitTest.php +++ b/src/Standards/Generic/Tests/CodeAnalysis/MixedBooleanOperatorUnitTest.php @@ -26,18 +26,42 @@ class MixedBooleanOperatorUnitTest extends AbstractSniffUnitTest 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, + 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()