Skip to content

Commit 4961d4a

Browse files
authored
Merge pull request #2894 from Strategy11/update_redundant_paren_sniff
Update sniff to fix more redundant parens uses
2 parents e5dc093 + a9f4ac4 commit 4961d4a

File tree

1 file changed

+142
-10
lines changed

1 file changed

+142
-10
lines changed

phpcs-sniffs/Formidable/Sniffs/CodeAnalysis/RedundantParenthesesSniff.php

Lines changed: 142 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212

1313
use PHP_CodeSniffer\Sniffs\Sniff;
1414
use PHP_CodeSniffer\Files\File;
15+
use PHP_CodeSniffer\Util\Tokens;
1516

1617
/**
1718
* Detects redundant parentheses around simple expressions in assignments.
@@ -67,6 +68,9 @@ private function isRedundantComparisonInLogicalExpression( File $phpcsFile, $ope
6768
T_LOGICAL_AND,
6869
T_LOGICAL_OR,
6970
T_CLOSE_PARENTHESIS,
71+
T_SEMICOLON,
72+
T_COMMA,
73+
T_INLINE_THEN,
7074
);
7175

7276
if ( ! in_array( $tokens[ $afterClose ]['code'], $validFollowing, true ) ) {
@@ -95,14 +99,14 @@ public function process( File $phpcsFile, $stackPtr ) {
9599
$closeParen = $tokens[ $stackPtr ]['parenthesis_closer'];
96100

97101
// Check what's before the opening parenthesis.
98-
$prevToken = $phpcsFile->findPrevious( T_WHITESPACE, $stackPtr - 1, null, true );
102+
$prevToken = $phpcsFile->findPrevious( Tokens::$emptyTokens, $stackPtr - 1, null, true );
99103

100104
if ( false === $prevToken ) {
101105
return;
102106
}
103107

104108
// Check what's after the closing parenthesis.
105-
$afterClose = $phpcsFile->findNext( T_WHITESPACE, $closeParen + 1, null, true );
109+
$afterClose = $phpcsFile->findNext( Tokens::$emptyTokens, $closeParen + 1, null, true );
106110

107111
if ( false === $afterClose ) {
108112
return;
@@ -115,6 +119,12 @@ public function process( File $phpcsFile, $stackPtr ) {
115119
return;
116120
}
117121

122+
// Check for redundant parentheses wrapping a standalone function call inside a logical expression.
123+
if ( $this->isRedundantFunctionCallInLogicalExpression( $phpcsFile, $stackPtr, $closeParen, $prevToken, $afterClose ) ) {
124+
$this->reportAndFix( $phpcsFile, $stackPtr, $closeParen );
125+
return;
126+
}
127+
118128
// Check for redundant parentheses around a simple comparison inside a logical expression.
119129
if ( $this->isRedundantComparisonInLogicalExpression( $phpcsFile, $stackPtr, $closeParen, $prevToken, $afterClose ) ) {
120130
$this->reportAndFix( $phpcsFile, $stackPtr, $closeParen );
@@ -177,21 +187,21 @@ private function isRedundantNegatedFunctionCall( File $phpcsFile, $openParen, $c
177187
$tokens = $phpcsFile->getTokens();
178188

179189
// Must be preceded by a logical operator (&&, ||) or another open parenthesis.
180-
$validPreceding = array( T_BOOLEAN_AND, T_BOOLEAN_OR, T_OPEN_PARENTHESIS );
190+
$validPreceding = array( T_BOOLEAN_AND, T_BOOLEAN_OR, T_LOGICAL_AND, T_LOGICAL_OR, T_OPEN_PARENTHESIS );
181191

182192
if ( ! in_array( $tokens[ $prevToken ]['code'], $validPreceding, true ) ) {
183193
return false;
184194
}
185195

186196
// Must be followed by a logical operator (&&, ||) or a close parenthesis.
187-
$validFollowing = array( T_BOOLEAN_AND, T_BOOLEAN_OR, T_CLOSE_PARENTHESIS );
197+
$validFollowing = array( T_BOOLEAN_AND, T_BOOLEAN_OR, T_LOGICAL_AND, T_LOGICAL_OR, T_CLOSE_PARENTHESIS );
188198

189199
if ( ! in_array( $tokens[ $afterClose ]['code'], $validFollowing, true ) ) {
190200
return false;
191201
}
192202

193203
// Check the content inside: should be ! followed by a function call with no other operators.
194-
$firstInside = $phpcsFile->findNext( T_WHITESPACE, $openParen + 1, $closeParen, true );
204+
$firstInside = $phpcsFile->findNext( Tokens::$emptyTokens, $openParen + 1, $closeParen, true );
195205

196206
if ( false === $firstInside ) {
197207
return false;
@@ -208,7 +218,7 @@ private function isRedundantNegatedFunctionCall( File $phpcsFile, $openParen, $c
208218
}
209219

210220
// Next should be a function call (empty, isset, or a T_STRING function).
211-
$funcToken = $phpcsFile->findNext( T_WHITESPACE, $firstInside + 1, $closeParen, true );
221+
$funcToken = $phpcsFile->findNext( Tokens::$emptyTokens, $firstInside + 1, $closeParen, true );
212222

213223
if ( false === $funcToken ) {
214224
return false;
@@ -221,7 +231,7 @@ private function isRedundantNegatedFunctionCall( File $phpcsFile, $openParen, $c
221231
}
222232

223233
// Find the function's opening parenthesis.
224-
$funcOpenParen = $phpcsFile->findNext( T_WHITESPACE, $funcToken + 1, $closeParen, true );
234+
$funcOpenParen = $phpcsFile->findNext( Tokens::$emptyTokens, $funcToken + 1, $closeParen, true );
225235

226236
if ( false === $funcOpenParen || $tokens[ $funcOpenParen ]['code'] !== T_OPEN_PARENTHESIS ) {
227237
return false;
@@ -234,7 +244,7 @@ private function isRedundantNegatedFunctionCall( File $phpcsFile, $openParen, $c
234244
$funcCloseParen = $tokens[ $funcOpenParen ]['parenthesis_closer'];
235245

236246
// The function's closing paren should be followed only by whitespace until our outer closing paren.
237-
$afterFuncClose = $phpcsFile->findNext( T_WHITESPACE, $funcCloseParen + 1, $closeParen, true );
247+
$afterFuncClose = $phpcsFile->findNext( Tokens::$emptyTokens, $funcCloseParen + 1, $closeParen, true );
238248

239249
// If there's anything else between the function close and our close, it's not a simple pattern.
240250
if ( false !== $afterFuncClose ) {
@@ -244,6 +254,52 @@ private function isRedundantNegatedFunctionCall( File $phpcsFile, $openParen, $c
244254
return true;
245255
}
246256

257+
/**
258+
* Check for redundant parentheses around a simple function (or static/object) call.
259+
*
260+
* Example: ( empty( $var ) ) when surrounded by logical operators.
261+
*
262+
* @param File $phpcsFile File reference.
263+
* @param int $openParen Position of opening parenthesis.
264+
* @param int $closeParen Closing parenthesis.
265+
* @param int $prevToken Previous meaningful token.
266+
* @param int $afterClose Next meaningful token.
267+
*
268+
* @return bool
269+
*/
270+
private function isRedundantFunctionCallInLogicalExpression( File $phpcsFile, $openParen, $closeParen, $prevToken, $afterClose ) {
271+
$tokens = $phpcsFile->getTokens();
272+
273+
$validPreceding = array(
274+
T_BOOLEAN_AND,
275+
T_BOOLEAN_OR,
276+
T_LOGICAL_AND,
277+
T_LOGICAL_OR,
278+
T_OPEN_PARENTHESIS,
279+
);
280+
281+
if ( ! in_array( $tokens[ $prevToken ]['code'], $validPreceding, true ) ) {
282+
return false;
283+
}
284+
285+
$validFollowing = array(
286+
T_BOOLEAN_AND,
287+
T_BOOLEAN_OR,
288+
T_LOGICAL_AND,
289+
T_LOGICAL_OR,
290+
T_CLOSE_PARENTHESIS,
291+
T_SEMICOLON,
292+
T_COMMA,
293+
T_INLINE_THEN,
294+
);
295+
296+
if ( ! in_array( $tokens[ $afterClose ]['code'], $validFollowing, true ) ) {
297+
return false;
298+
}
299+
300+
return $this->isSimpleFunctionCall( $phpcsFile, $openParen, $closeParen );
301+
}
302+
247303
/**
248304
* Check if this is a simple comparison expression that doesn't need parentheses.
249305
*
@@ -263,6 +319,7 @@ private function isSimpleComparisonExpression( File $phpcsFile, $openParen, $clo
263319
$logicalCount = 0;
264320
$nestedParenDepth = 0;
265321
$arithmeticCount = 0;
322+
$hasTernary = false;
266323

267324
$comparisonTokens = array(
268325
T_IS_EQUAL,
@@ -313,10 +370,85 @@ private function isSimpleComparisonExpression( File $phpcsFile, $openParen, $clo
313370
if ( in_array( $code, $arithmeticTokens, true ) ) {
314371
++$arithmeticCount;
315372
}
373+
374+
// Presence of a ternary operator means parentheses are required.
375+
if ( T_INLINE_THEN === $code || T_INLINE_ELSE === $code ) {
376+
$hasTernary = true;
377+
}
378+
}
379+
380+
// Simple comparison: exactly one comparison operator and no logical operators/arithmetic/ternary.
381+
return 1 === $comparisonCount && 0 === $logicalCount && 0 === $arithmeticCount && false === $hasTernary;
382+
}
383+
384+
/**
385+
* Determine if the contents are exactly a function (or method/static) call.
386+
*
387+
* @param File $phpcsFile File reference.
388+
* @param int $openParen Opening parenthesis token.
389+
* @param int $closeParen Closing parenthesis token.
390+
*
391+
* @return bool
392+
*/
393+
private function isSimpleFunctionCall( File $phpcsFile, $openParen, $closeParen ) {
394+
$tokens = $phpcsFile->getTokens();
395+
396+
$first = $phpcsFile->findNext( Tokens::$emptyTokens, $openParen + 1, $closeParen, true );
397+
398+
if ( false === $first ) {
399+
return false;
316400
}
317401

318-
// Simple comparison: exactly one comparison operator and no logical operators.
319-
return 1 === $comparisonCount && 0 === $logicalCount && 0 === $arithmeticCount;
402+
$allowedCallableTokens = array(
403+
T_STRING,
404+
T_NS_SEPARATOR,
405+
T_DOUBLE_COLON,
406+
T_OBJECT_OPERATOR,
407+
T_VARIABLE,
408+
T_SELF,
409+
T_STATIC,
410+
T_PARENT,
411+
T_EMPTY,
412+
T_ISSET,
413+
);
414+
415+
$callOpenParen = null;
416+
417+
for ( $i = $first; $i < $closeParen; $i++ ) {
418+
$code = $tokens[ $i ]['code'];
419+
420+
if ( isset( Tokens::$emptyTokens[ $code ] ) ) {
421+
continue;
422+
}
423+
424+
if ( T_OPEN_PARENTHESIS === $code ) {
425+
$callOpenParen = $i;
426+
break;
427+
}
428+
429+
if ( ! in_array( $code, $allowedCallableTokens, true ) ) {
430+
return false;
431+
}
432+
}
433+
434+
if ( null === $callOpenParen ) {
435+
return false;
436+
}
437+
438+
if ( ! isset( $tokens[ $callOpenParen ]['parenthesis_closer'] ) ) {
439+
return false;
440+
}
441+
442+
$callCloseParen = $tokens[ $callOpenParen ]['parenthesis_closer'];
443+
444+
if ( $callCloseParen >= $closeParen ) {
445+
return false;
446+
}
447+
448+
// Ensure nothing but whitespace remains between the function close and our close.
449+
$afterCall = $phpcsFile->findNext( Tokens::$emptyTokens, $callCloseParen + 1, $closeParen, true );
450+
451+
return false === $afterCall;
320452
}
321453

322454
/**

0 commit comments

Comments
 (0)