From a9f4ac414b7dec1b9e9707f274a74d6b653445c3 Mon Sep 17 00:00:00 2001 From: Mike Letellier Date: Tue, 27 Jan 2026 15:46:53 -0400 Subject: [PATCH] Update sniff to fix more redundant parens uses --- .../RedundantParenthesesSniff.php | 152 ++++++++++++++++-- 1 file changed, 142 insertions(+), 10 deletions(-) diff --git a/phpcs-sniffs/Formidable/Sniffs/CodeAnalysis/RedundantParenthesesSniff.php b/phpcs-sniffs/Formidable/Sniffs/CodeAnalysis/RedundantParenthesesSniff.php index 3ccc8f7c6a..868019e011 100644 --- a/phpcs-sniffs/Formidable/Sniffs/CodeAnalysis/RedundantParenthesesSniff.php +++ b/phpcs-sniffs/Formidable/Sniffs/CodeAnalysis/RedundantParenthesesSniff.php @@ -12,6 +12,7 @@ use PHP_CodeSniffer\Sniffs\Sniff; use PHP_CodeSniffer\Files\File; +use PHP_CodeSniffer\Util\Tokens; /** * Detects redundant parentheses around simple expressions in assignments. @@ -67,6 +68,9 @@ private function isRedundantComparisonInLogicalExpression( File $phpcsFile, $ope T_LOGICAL_AND, T_LOGICAL_OR, T_CLOSE_PARENTHESIS, + T_SEMICOLON, + T_COMMA, + T_INLINE_THEN, ); if ( ! in_array( $tokens[ $afterClose ]['code'], $validFollowing, true ) ) { @@ -95,14 +99,14 @@ public function process( File $phpcsFile, $stackPtr ) { $closeParen = $tokens[ $stackPtr ]['parenthesis_closer']; // Check what's before the opening parenthesis. - $prevToken = $phpcsFile->findPrevious( T_WHITESPACE, $stackPtr - 1, null, true ); + $prevToken = $phpcsFile->findPrevious( Tokens::$emptyTokens, $stackPtr - 1, null, true ); if ( false === $prevToken ) { return; } // Check what's after the closing parenthesis. - $afterClose = $phpcsFile->findNext( T_WHITESPACE, $closeParen + 1, null, true ); + $afterClose = $phpcsFile->findNext( Tokens::$emptyTokens, $closeParen + 1, null, true ); if ( false === $afterClose ) { return; @@ -115,6 +119,12 @@ public function process( File $phpcsFile, $stackPtr ) { return; } + // Check for redundant parentheses wrapping a standalone function call inside a logical expression. + if ( $this->isRedundantFunctionCallInLogicalExpression( $phpcsFile, $stackPtr, $closeParen, $prevToken, $afterClose ) ) { + $this->reportAndFix( $phpcsFile, $stackPtr, $closeParen ); + return; + } + // Check for redundant parentheses around a simple comparison inside a logical expression. if ( $this->isRedundantComparisonInLogicalExpression( $phpcsFile, $stackPtr, $closeParen, $prevToken, $afterClose ) ) { $this->reportAndFix( $phpcsFile, $stackPtr, $closeParen ); @@ -177,21 +187,21 @@ private function isRedundantNegatedFunctionCall( File $phpcsFile, $openParen, $c $tokens = $phpcsFile->getTokens(); // Must be preceded by a logical operator (&&, ||) or another open parenthesis. - $validPreceding = array( T_BOOLEAN_AND, T_BOOLEAN_OR, T_OPEN_PARENTHESIS ); + $validPreceding = array( T_BOOLEAN_AND, T_BOOLEAN_OR, T_LOGICAL_AND, T_LOGICAL_OR, T_OPEN_PARENTHESIS ); if ( ! in_array( $tokens[ $prevToken ]['code'], $validPreceding, true ) ) { return false; } // Must be followed by a logical operator (&&, ||) or a close parenthesis. - $validFollowing = array( T_BOOLEAN_AND, T_BOOLEAN_OR, T_CLOSE_PARENTHESIS ); + $validFollowing = array( T_BOOLEAN_AND, T_BOOLEAN_OR, T_LOGICAL_AND, T_LOGICAL_OR, T_CLOSE_PARENTHESIS ); if ( ! in_array( $tokens[ $afterClose ]['code'], $validFollowing, true ) ) { return false; } // Check the content inside: should be ! followed by a function call with no other operators. - $firstInside = $phpcsFile->findNext( T_WHITESPACE, $openParen + 1, $closeParen, true ); + $firstInside = $phpcsFile->findNext( Tokens::$emptyTokens, $openParen + 1, $closeParen, true ); if ( false === $firstInside ) { return false; @@ -208,7 +218,7 @@ private function isRedundantNegatedFunctionCall( File $phpcsFile, $openParen, $c } // Next should be a function call (empty, isset, or a T_STRING function). - $funcToken = $phpcsFile->findNext( T_WHITESPACE, $firstInside + 1, $closeParen, true ); + $funcToken = $phpcsFile->findNext( Tokens::$emptyTokens, $firstInside + 1, $closeParen, true ); if ( false === $funcToken ) { return false; @@ -221,7 +231,7 @@ private function isRedundantNegatedFunctionCall( File $phpcsFile, $openParen, $c } // Find the function's opening parenthesis. - $funcOpenParen = $phpcsFile->findNext( T_WHITESPACE, $funcToken + 1, $closeParen, true ); + $funcOpenParen = $phpcsFile->findNext( Tokens::$emptyTokens, $funcToken + 1, $closeParen, true ); if ( false === $funcOpenParen || $tokens[ $funcOpenParen ]['code'] !== T_OPEN_PARENTHESIS ) { return false; @@ -234,7 +244,7 @@ private function isRedundantNegatedFunctionCall( File $phpcsFile, $openParen, $c $funcCloseParen = $tokens[ $funcOpenParen ]['parenthesis_closer']; // The function's closing paren should be followed only by whitespace until our outer closing paren. - $afterFuncClose = $phpcsFile->findNext( T_WHITESPACE, $funcCloseParen + 1, $closeParen, true ); + $afterFuncClose = $phpcsFile->findNext( Tokens::$emptyTokens, $funcCloseParen + 1, $closeParen, true ); // If there's anything else between the function close and our close, it's not a simple pattern. if ( false !== $afterFuncClose ) { @@ -244,6 +254,52 @@ private function isRedundantNegatedFunctionCall( File $phpcsFile, $openParen, $c return true; } + /** + * Check for redundant parentheses around a simple function (or static/object) call. + * + * Example: ( empty( $var ) ) when surrounded by logical operators. + * + * @param File $phpcsFile File reference. + * @param int $openParen Position of opening parenthesis. + * @param int $closeParen Closing parenthesis. + * @param int $prevToken Previous meaningful token. + * @param int $afterClose Next meaningful token. + * + * @return bool + */ + private function isRedundantFunctionCallInLogicalExpression( File $phpcsFile, $openParen, $closeParen, $prevToken, $afterClose ) { + $tokens = $phpcsFile->getTokens(); + + $validPreceding = array( + T_BOOLEAN_AND, + T_BOOLEAN_OR, + T_LOGICAL_AND, + T_LOGICAL_OR, + T_OPEN_PARENTHESIS, + ); + + if ( ! in_array( $tokens[ $prevToken ]['code'], $validPreceding, true ) ) { + return false; + } + + $validFollowing = array( + T_BOOLEAN_AND, + T_BOOLEAN_OR, + T_LOGICAL_AND, + T_LOGICAL_OR, + T_CLOSE_PARENTHESIS, + T_SEMICOLON, + T_COMMA, + T_INLINE_THEN, + ); + + if ( ! in_array( $tokens[ $afterClose ]['code'], $validFollowing, true ) ) { + return false; + } + + return $this->isSimpleFunctionCall( $phpcsFile, $openParen, $closeParen ); + } + /** * Check if this is a simple comparison expression that doesn't need parentheses. * @@ -263,6 +319,7 @@ private function isSimpleComparisonExpression( File $phpcsFile, $openParen, $clo $logicalCount = 0; $nestedParenDepth = 0; $arithmeticCount = 0; + $hasTernary = false; $comparisonTokens = array( T_IS_EQUAL, @@ -313,10 +370,85 @@ private function isSimpleComparisonExpression( File $phpcsFile, $openParen, $clo if ( in_array( $code, $arithmeticTokens, true ) ) { ++$arithmeticCount; } + + // Presence of a ternary operator means parentheses are required. + if ( T_INLINE_THEN === $code || T_INLINE_ELSE === $code ) { + $hasTernary = true; + } + } + + // Simple comparison: exactly one comparison operator and no logical operators/arithmetic/ternary. + return 1 === $comparisonCount && 0 === $logicalCount && 0 === $arithmeticCount && false === $hasTernary; + } + + /** + * Determine if the contents are exactly a function (or method/static) call. + * + * @param File $phpcsFile File reference. + * @param int $openParen Opening parenthesis token. + * @param int $closeParen Closing parenthesis token. + * + * @return bool + */ + private function isSimpleFunctionCall( File $phpcsFile, $openParen, $closeParen ) { + $tokens = $phpcsFile->getTokens(); + + $first = $phpcsFile->findNext( Tokens::$emptyTokens, $openParen + 1, $closeParen, true ); + + if ( false === $first ) { + return false; } - // Simple comparison: exactly one comparison operator and no logical operators. - return 1 === $comparisonCount && 0 === $logicalCount && 0 === $arithmeticCount; + $allowedCallableTokens = array( + T_STRING, + T_NS_SEPARATOR, + T_DOUBLE_COLON, + T_OBJECT_OPERATOR, + T_VARIABLE, + T_SELF, + T_STATIC, + T_PARENT, + T_EMPTY, + T_ISSET, + ); + + $callOpenParen = null; + + for ( $i = $first; $i < $closeParen; $i++ ) { + $code = $tokens[ $i ]['code']; + + if ( isset( Tokens::$emptyTokens[ $code ] ) ) { + continue; + } + + if ( T_OPEN_PARENTHESIS === $code ) { + $callOpenParen = $i; + break; + } + + if ( ! in_array( $code, $allowedCallableTokens, true ) ) { + return false; + } + } + + if ( null === $callOpenParen ) { + return false; + } + + if ( ! isset( $tokens[ $callOpenParen ]['parenthesis_closer'] ) ) { + return false; + } + + $callCloseParen = $tokens[ $callOpenParen ]['parenthesis_closer']; + + if ( $callCloseParen >= $closeParen ) { + return false; + } + + // Ensure nothing but whitespace remains between the function close and our close. + $afterCall = $phpcsFile->findNext( Tokens::$emptyTokens, $callCloseParen + 1, $closeParen, true ); + + return false === $afterCall; } /**