diff --git a/phpcs-sniffs/Formidable/Sniffs/CodeAnalysis/SimplifyRedundantFalsyCheckSniff.php b/phpcs-sniffs/Formidable/Sniffs/CodeAnalysis/SimplifyRedundantFalsyCheckSniff.php new file mode 100644 index 0000000000..abdfb1abd8 --- /dev/null +++ b/phpcs-sniffs/Formidable/Sniffs/CodeAnalysis/SimplifyRedundantFalsyCheckSniff.php @@ -0,0 +1,382 @@ +getTokens(); + + // Find the condition boundaries (inside parentheses). + $conditionStart = $this->findConditionStart( $phpcsFile, $stackPtr ); + $conditionEnd = $this->findConditionEnd( $phpcsFile, $stackPtr ); + + if ( false === $conditionStart || false === $conditionEnd ) { + return; + } + + // Parse the left side of ||. + $leftSide = $this->parseConditionPart( $phpcsFile, $conditionStart, $stackPtr - 1 ); + + // Parse the right side of ||. + $rightSide = $this->parseConditionPart( $phpcsFile, $stackPtr + 1, $conditionEnd ); + + if ( false === $leftSide || false === $rightSide ) { + return; + } + + // Check for pattern: $var == '' || ! $var (or similar falsy comparisons). + $simplification = $this->findRedundantPattern( $leftSide, $rightSide ); + + if ( false === $simplification ) { + return; + } + + $fix = $phpcsFile->addFixableError( + 'Redundant falsy check. "%s" can be simplified to "%s".', + $stackPtr, + 'Found', + array( $simplification['original'], $simplification['simplified'] ) + ); + + if ( true === $fix ) { + $this->applyFix( $phpcsFile, $conditionStart, $conditionEnd, $simplification['simplified'] ); + } + } + + /** + * Find the start of the condition (after opening parenthesis). + * + * @param File $phpcsFile The file being scanned. + * @param int $stackPtr The position of the || token. + * + * @return false|int + */ + private function findConditionStart( File $phpcsFile, $stackPtr ) { + $tokens = $phpcsFile->getTokens(); + $parenDepth = 0; + + for ( $i = $stackPtr - 1; $i >= 0; $i-- ) { + $code = $tokens[ $i ]['code']; + + if ( $code === T_CLOSE_PARENTHESIS ) { + ++$parenDepth; + continue; + } + + if ( $code === T_OPEN_PARENTHESIS ) { + if ( $parenDepth > 0 ) { + --$parenDepth; + continue; + } + + // Found the opening parenthesis. + return $i + 1; + } + + // Stop if we hit another || at the same level (compound condition). + if ( $parenDepth === 0 && ( $code === T_BOOLEAN_OR || $code === T_BOOLEAN_AND ) ) { + return false; + } + } + + return false; + } + + /** + * Find the end of the condition (before closing parenthesis). + * + * @param File $phpcsFile The file being scanned. + * @param int $stackPtr The position of the || token. + * + * @return false|int + */ + private function findConditionEnd( File $phpcsFile, $stackPtr ) { + $tokens = $phpcsFile->getTokens(); + $parenDepth = 0; + + for ( $i = $stackPtr + 1; $i < count( $tokens ); $i++ ) { + $code = $tokens[ $i ]['code']; + + if ( $code === T_OPEN_PARENTHESIS ) { + ++$parenDepth; + continue; + } + + if ( $code === T_CLOSE_PARENTHESIS ) { + if ( $parenDepth > 0 ) { + --$parenDepth; + continue; + } + + // Found the closing parenthesis. + return $i - 1; + } + + // Stop if we hit another || at the same level (compound condition). + if ( $parenDepth === 0 && ( $code === T_BOOLEAN_OR || $code === T_BOOLEAN_AND ) ) { + return false; + } + } + + return false; + } + + /** + * Parse a condition part to extract variable and comparison info. + * + * @param File $phpcsFile The file being scanned. + * @param int $start Start position. + * @param int $end End position. + * + * @return array|false Array with 'type', 'variable', 'value' or false. + */ + private function parseConditionPart( File $phpcsFile, $start, $end ) { + $tokens = $phpcsFile->getTokens(); + + // Skip whitespace at boundaries. + $start = $phpcsFile->findNext( T_WHITESPACE, $start, $end + 1, true ); + $end = $phpcsFile->findPrevious( T_WHITESPACE, $end, $start - 1, true ); + + if ( false === $start || false === $end ) { + return false; + } + + // Check for ! $var pattern. + if ( $tokens[ $start ]['code'] === T_BOOLEAN_NOT ) { + $varToken = $phpcsFile->findNext( T_WHITESPACE, $start + 1, $end + 1, true ); + + if ( false !== $varToken && $tokens[ $varToken ]['code'] === T_VARIABLE ) { + // Make sure there's nothing after the variable. + $afterVar = $phpcsFile->findNext( T_WHITESPACE, $varToken + 1, $end + 1, true ); + + if ( false === $afterVar || $afterVar > $end ) { + return array( + 'type' => 'negation', + 'variable' => $tokens[ $varToken ]['content'], + 'raw' => $this->getTokensContent( $phpcsFile, $start, $end ), + ); + } + } + + return false; + } + + // Check for $var == '' or $var === '' or $var == false, etc. + if ( $tokens[ $start ]['code'] === T_VARIABLE ) { + $variable = $tokens[ $start ]['content']; + + // Find comparison operator. + $opToken = $phpcsFile->findNext( T_WHITESPACE, $start + 1, $end + 1, true ); + + if ( false === $opToken ) { + return false; + } + + $opCode = $tokens[ $opToken ]['code']; + + if ( ! in_array( $opCode, array( T_IS_EQUAL, T_IS_IDENTICAL ), true ) ) { + return false; + } + + // Find the value being compared. + $valueToken = $phpcsFile->findNext( T_WHITESPACE, $opToken + 1, $end + 1, true ); + + if ( false === $valueToken ) { + return false; + } + + // Check if it's a falsy value. + $falsyValue = $this->isFalsyValue( $phpcsFile, $valueToken ); + + if ( false === $falsyValue ) { + return false; + } + + // Make sure there's nothing after the value. + $afterValue = $phpcsFile->findNext( T_WHITESPACE, $valueToken + 1, $end + 1, true ); + + if ( false !== $afterValue && $afterValue <= $end ) { + return false; + } + + return array( + 'type' => 'comparison', + 'variable' => $variable, + 'value' => $falsyValue, + 'operator' => $tokens[ $opToken ]['content'], + 'raw' => $this->getTokensContent( $phpcsFile, $start, $end ), + ); + } + + return false; + } + + /** + * Check if a token represents a falsy value. + * + * @param File $phpcsFile The file being scanned. + * @param int $valueToken The token position. + * + * @return false|string The falsy value type or false. + */ + private function isFalsyValue( File $phpcsFile, $valueToken ) { + $tokens = $phpcsFile->getTokens(); + $code = $tokens[ $valueToken ]['code']; + + // Empty string ''. + if ( $code === T_CONSTANT_ENCAPSED_STRING ) { + $content = $tokens[ $valueToken ]['content']; + + if ( $content === "''" || $content === '""' ) { + return 'empty_string'; + } + + return false; + } + + // false. + if ( $code === T_FALSE ) { + return 'false'; + } + + // 0. + if ( $code === T_LNUMBER && $tokens[ $valueToken ]['content'] === '0' ) { + return 'zero'; + } + + // null. + if ( $code === T_NULL ) { + return 'null'; + } + + return false; + } + + /** + * Find redundant pattern and return simplification info. + * + * @param array $leftSide Left side of ||. + * @param array $rightSide Right side of ||. + * + * @return array|false Array with 'original' and 'simplified' or false. + */ + private function findRedundantPattern( $leftSide, $rightSide ) { + // Pattern 1: $var == '' || ! $var. + if ( $leftSide['type'] === 'comparison' && $rightSide['type'] === 'negation' ) { + if ( $leftSide['variable'] === $rightSide['variable'] ) { + return array( + 'original' => $leftSide['raw'] . ' || ' . $rightSide['raw'], + 'simplified' => '! ' . $leftSide['variable'], + ); + } + } + + // Pattern 2: ! $var || $var == ''. + if ( $leftSide['type'] === 'negation' && $rightSide['type'] === 'comparison' ) { + if ( $leftSide['variable'] === $rightSide['variable'] ) { + return array( + 'original' => $leftSide['raw'] . ' || ' . $rightSide['raw'], + 'simplified' => '! ' . $rightSide['variable'], + ); + } + } + + return false; + } + + /** + * Get the content of tokens between two positions. + * + * @param File $phpcsFile The file being scanned. + * @param int $start Start position. + * @param int $end End position. + * + * @return string + */ + private function getTokensContent( File $phpcsFile, $start, $end ) { + $tokens = $phpcsFile->getTokens(); + $content = ''; + + for ( $i = $start; $i <= $end; $i++ ) { + $content .= $tokens[ $i ]['content']; + } + + return trim( $content ); + } + + /** + * Apply the fix. + * + * @param File $phpcsFile The file being scanned. + * @param int $conditionStart Start of the condition. + * @param int $conditionEnd End of the condition. + * @param string $simplified The simplified condition. + * + * @return void + */ + private function applyFix( File $phpcsFile, $conditionStart, $conditionEnd, $simplified ) { + $tokens = $phpcsFile->getTokens(); + $fixer = $phpcsFile->fixer; + + $fixer->beginChangeset(); + + // Find actual start (skip leading whitespace). + $actualStart = $phpcsFile->findNext( T_WHITESPACE, $conditionStart, $conditionEnd + 1, true ); + + // Find actual end (skip trailing whitespace). + $actualEnd = $phpcsFile->findPrevious( T_WHITESPACE, $conditionEnd, $actualStart - 1, true ); + + // Remove all tokens in the condition. + for ( $i = $actualStart; $i <= $actualEnd; $i++ ) { + $fixer->replaceToken( $i, '' ); + } + + // Add the simplified condition. + $fixer->addContent( $actualStart, $simplified ); + + $fixer->endChangeset(); + } +} diff --git a/phpcs-sniffs/Formidable/ruleset.xml b/phpcs-sniffs/Formidable/ruleset.xml index b4e9356c49..b76dfb58e7 100644 --- a/phpcs-sniffs/Formidable/ruleset.xml +++ b/phpcs-sniffs/Formidable/ruleset.xml @@ -20,6 +20,7 @@ + diff --git a/tests/phpunit/entries/test_FrmShowEntryShortcode.php b/tests/phpunit/entries/test_FrmShowEntryShortcode.php index a4a2be3926..a86499f3ec 100644 --- a/tests/phpunit/entries/test_FrmShowEntryShortcode.php +++ b/tests/phpunit/entries/test_FrmShowEntryShortcode.php @@ -1128,7 +1128,7 @@ protected function expected_array( $entry, $atts ) { if ( empty( $atts['include_blank'] ) ) { foreach ( $expected as $field_key => $value ) { // phpcs:ignore Universal.Operators.StrictComparisons - if ( $value == '' || ! $value ) { + if ( ! $value ) { unset( $expected[ $field_key ] ); } }