1212
1313use PHP_CodeSniffer \Sniffs \Sniff ;
1414use 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