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