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