diff --git a/phpcs-sniffs/Formidable/Sniffs/PHPUnit/PreferAssertStringContainsSniff.php b/phpcs-sniffs/Formidable/Sniffs/PHPUnit/PreferAssertStringContainsSniff.php new file mode 100644 index 0000000000..5bb62882e6 --- /dev/null +++ b/phpcs-sniffs/Formidable/Sniffs/PHPUnit/PreferAssertStringContainsSniff.php @@ -0,0 +1,232 @@ +assertTrue(str_contains(...)) and converts to $this->assertStringContainsString(...). + * Note: str_contains($haystack, $needle) becomes assertStringContainsString($needle, $haystack). + * + * @package Formidable\Sniffs + */ + +namespace Formidable\Sniffs\PHPUnit; + +use PHP_CodeSniffer\Sniffs\Sniff; +use PHP_CodeSniffer\Files\File; + +/** + * Converts assertTrue(str_contains()) and assertNotFalse(strpos()) to assertStringContainsString(). + * + * Bad: + * $this->assertTrue( str_contains( $haystack, $needle ) ); + * $this->assertNotFalse( strpos( $haystack, $needle ) ); + * + * Good: + * $this->assertStringContainsString( $needle, $haystack ); + */ +class PreferAssertStringContainsSniff implements Sniff { + + /** + * Returns an array of tokens this test wants to listen for. + * + * @return array + */ + public function register() { + return array( T_STRING ); + } + + /** + * Processes this test, when one of its tokens is encountered. + * + * @param 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 ]; + + // Check for assertTrue, assertFalse, assertNotFalse. + $methodName = strtolower( $token['content'] ); + + $validMethods = array( 'asserttrue', 'assertfalse', 'assertnotfalse' ); + + if ( ! in_array( $methodName, $validMethods, true ) ) { + return; + } + + // Check that this is a method call ($this-> or self:: or static::). + $prevToken = $phpcsFile->findPrevious( T_WHITESPACE, $stackPtr - 1, null, true ); + + if ( false === $prevToken ) { + return; + } + + if ( $tokens[ $prevToken ]['code'] !== T_OBJECT_OPERATOR && $tokens[ $prevToken ]['code'] !== T_DOUBLE_COLON ) { + return; + } + + // Find the opening parenthesis. + $openParen = $phpcsFile->findNext( T_WHITESPACE, $stackPtr + 1, null, true ); + + if ( false === $openParen || $tokens[ $openParen ]['code'] !== T_OPEN_PARENTHESIS ) { + return; + } + + // Find the first argument (skip whitespace). + $firstArg = $phpcsFile->findNext( T_WHITESPACE, $openParen + 1, null, true ); + + if ( false === $firstArg ) { + return; + } + + // Check if first argument is str_contains or strpos function call. + if ( $tokens[ $firstArg ]['code'] !== T_STRING ) { + return; + } + + $functionName = strtolower( $tokens[ $firstArg ]['content'] ); + + if ( 'str_contains' !== $functionName && 'strpos' !== $functionName ) { + return; + } + + // Find the opening parenthesis of str_contains. + $funcOpenParen = $phpcsFile->findNext( T_WHITESPACE, $firstArg + 1, null, true ); + + if ( false === $funcOpenParen || $tokens[ $funcOpenParen ]['code'] !== T_OPEN_PARENTHESIS ) { + return; + } + + // Find the matching closing parenthesis of str_contains. + if ( ! isset( $tokens[ $funcOpenParen ]['parenthesis_closer'] ) ) { + return; + } + $funcCloseParen = $tokens[ $funcOpenParen ]['parenthesis_closer']; + + // Find the closing parenthesis of assertTrue/assertFalse. + if ( ! isset( $tokens[ $openParen ]['parenthesis_closer'] ) ) { + return; + } + $assertCloseParen = $tokens[ $openParen ]['parenthesis_closer']; + + // Check that str_contains is the only argument (no && or || after it). + $nextAfterFunc = $phpcsFile->findNext( T_WHITESPACE, $funcCloseParen + 1, $assertCloseParen, true ); + + if ( false !== $nextAfterFunc ) { + // There's something else after the str_contains call - skip this. + return; + } + + // Parse the two arguments of str_contains. + $args = $this->parseArguments( $phpcsFile, $funcOpenParen, $funcCloseParen ); + + if ( count( $args ) !== 2 ) { + return; + } + + // Determine the new method name. + // assertTrue(str_contains()) -> assertStringContainsString + // assertFalse(str_contains()) -> assertStringNotContainsString + // assertNotFalse(strpos()) -> assertStringContainsString + // assertFalse(strpos()) -> assertStringNotContainsString + if ( 'asserttrue' === $methodName || 'assertnotfalse' === $methodName ) { + $newMethodName = 'assertStringContainsString'; + } else { + $newMethodName = 'assertStringNotContainsString'; + } + + $fix = $phpcsFile->addFixableError( + 'Use %s() instead of %s(%s()).', + $stackPtr, + 'Found', + array( $newMethodName, $token['content'], $functionName ) + ); + + if ( true === $fix ) { + $this->applyFix( $phpcsFile, $stackPtr, $openParen, $assertCloseParen, $newMethodName, $args ); + } + } + + /** + * Parse the arguments of a function call. + * + * @param File $phpcsFile The file being scanned. + * @param int $funcOpenParen The opening parenthesis position. + * @param int $funcCloseParen The closing parenthesis position. + * + * @return array Array of argument strings. + */ + private function parseArguments( File $phpcsFile, $funcOpenParen, $funcCloseParen ) { + $tokens = $phpcsFile->getTokens(); + $args = array(); + $start = $funcOpenParen + 1; + $depth = 0; + + $currentArg = ''; + + for ( $i = $start; $i < $funcCloseParen; $i++ ) { + $tokenCode = $tokens[ $i ]['code']; + + // Track nested parentheses. + if ( $tokenCode === T_OPEN_PARENTHESIS || $tokenCode === T_OPEN_SQUARE_BRACKET ) { + $depth++; + $currentArg .= $tokens[ $i ]['content']; + } elseif ( $tokenCode === T_CLOSE_PARENTHESIS || $tokenCode === T_CLOSE_SQUARE_BRACKET ) { + $depth--; + $currentArg .= $tokens[ $i ]['content']; + } elseif ( $tokenCode === T_COMMA && 0 === $depth ) { + // End of argument. + $args[] = trim( $currentArg ); + $currentArg = ''; + } else { + $currentArg .= $tokens[ $i ]['content']; + } + } + + // Add the last argument. + $trimmed = trim( $currentArg ); + + if ( '' !== $trimmed ) { + $args[] = $trimmed; + } + + return $args; + } + + /** + * Apply the fix to convert assertTrue(str_contains($haystack, $needle)) to assertStringContainsString($needle, $haystack). + * + * @param File $phpcsFile The file being scanned. + * @param int $methodNamePtr The assertTrue/assertFalse token position. + * @param int $openParen The opening parenthesis of assertTrue. + * @param int $assertCloseParen The closing parenthesis of assertTrue. + * @param string $newMethodName The new method name. + * @param array $args The parsed arguments (haystack, needle). + * + * @return void + */ + private function applyFix( File $phpcsFile, $methodNamePtr, $openParen, $assertCloseParen, $newMethodName, $args ) { + $fixer = $phpcsFile->fixer; + + $fixer->beginChangeset(); + + // Replace the method name. + $fixer->replaceToken( $methodNamePtr, $newMethodName ); + + // Replace everything between the parentheses with swapped arguments. + // str_contains($haystack, $needle) -> assertStringContainsString($needle, $haystack) + $haystack = $args[0]; + $needle = $args[1]; + + // Remove all tokens between open and close paren. + for ( $i = $openParen + 1; $i < $assertCloseParen; $i++ ) { + $fixer->replaceToken( $i, '' ); + } + + // Insert the new content after the opening paren. + $fixer->addContent( $openParen, ' ' . $needle . ', ' . $haystack . ' ' ); + + $fixer->endChangeset(); + } +} diff --git a/phpcs-sniffs/Formidable/ruleset.xml b/phpcs-sniffs/Formidable/ruleset.xml index 2c0f54c1a3..e07917bf8e 100644 --- a/phpcs-sniffs/Formidable/ruleset.xml +++ b/phpcs-sniffs/Formidable/ruleset.xml @@ -39,6 +39,7 @@ + diff --git a/tests/phpunit/database/test_FrmMigrate.php b/tests/phpunit/database/test_FrmMigrate.php index cd91bb7c6b..547bb8a71a 100644 --- a/tests/phpunit/database/test_FrmMigrate.php +++ b/tests/phpunit/database/test_FrmMigrate.php @@ -315,11 +315,11 @@ private function assert_collation() { $collation = $frmdb->collation(); if ( ! empty( $wpdb->charset ) ) { - $this->assertTrue( str_contains( $collation, 'DEFAULT CHARACTER SET' ) ); + $this->assertStringContainsString( 'DEFAULT CHARACTER SET', $collation ); } if ( ! empty( $wpdb->collate ) ) { - $this->assertTrue( str_contains( $collation, 'COLLATE' ) ); + $this->assertStringContainsString( 'COLLATE', $collation ); } } @@ -367,7 +367,7 @@ public function test_migrate_from_12_to_current() { $this->assertEquals( 1, count( $form_actions ), 'Old form settings are not converted to email action.' ); foreach ( $form_actions as $action ) { - $this->assertTrue( str_contains( $action->post_content['email_to'], 'emailto@test.com' ) ); + $this->assertStringContainsString( 'emailto@test.com', $action->post_content['email_to'] ); } } diff --git a/tests/phpunit/emails/test_FrmEmail.php b/tests/phpunit/emails/test_FrmEmail.php index e006a0b078..988928ec9b 100644 --- a/tests/phpunit/emails/test_FrmEmail.php +++ b/tests/phpunit/emails/test_FrmEmail.php @@ -678,9 +678,9 @@ public function test_message_user_info() { $actual = $this->get_private_property( $email, 'message' ); if ( $setting['compare'] === 'Contains' ) { - $this->assertNotFalse( strpos( $actual, 'Referrer:' ) ); + $this->assertStringContainsString( 'Referrer:', $actual ); } else { - $this->assertFalse( strpos( $actual, 'Referrer:' ) ); + $this->assertStringNotContainsString( 'Referrer:', $actual ); } } } diff --git a/tests/phpunit/fields/test_FrmFieldName.php b/tests/phpunit/fields/test_FrmFieldName.php index 88e405cde3..d1cb6e6d5a 100644 --- a/tests/phpunit/fields/test_FrmFieldName.php +++ b/tests/phpunit/fields/test_FrmFieldName.php @@ -19,8 +19,8 @@ public function test_get_processed_sub_fields() { $processed_sub_fields = $this->run_private_method( array( $name_field, 'get_processed_sub_fields' ) ); $this->assertEquals( array( 'first', 'middle', 'last' ), array_keys( $processed_sub_fields ) ); - $this->assertNotFalse( strpos( $processed_sub_fields['first']['wrapper_classes'], 'frm4' ) ); - $this->assertNotFalse( strpos( $processed_sub_fields['middle']['wrapper_classes'], 'frm4' ) ); - $this->assertNotFalse( strpos( $processed_sub_fields['last']['wrapper_classes'], 'frm4' ) ); + $this->assertStringContainsString( 'frm4', $processed_sub_fields['first']['wrapper_classes'] ); + $this->assertStringContainsString( 'frm4', $processed_sub_fields['middle']['wrapper_classes'] ); + $this->assertStringContainsString( 'frm4', $processed_sub_fields['last']['wrapper_classes'] ); } } diff --git a/tests/phpunit/fields/test_FrmFieldType.php b/tests/phpunit/fields/test_FrmFieldType.php index 7839970349..90155bc9ce 100644 --- a/tests/phpunit/fields/test_FrmFieldType.php +++ b/tests/phpunit/fields/test_FrmFieldType.php @@ -28,9 +28,9 @@ public function test_html_min_number() { 'id' => $form_id, ) ); - $this->assertNotFalse( strpos( $form, ' min="10"' ) ); - $this->assertNotFalse( strpos( $form, ' max="999"' ) ); - $this->assertNotFalse( strpos( $form, ' step="any"' ) ); + $this->assertStringContainsString( ' min="10"', $form ); + $this->assertStringContainsString( ' max="999"', $form ); + $this->assertStringContainsString( ' step="any"', $form ); } /** diff --git a/tests/phpunit/misc/test_FrmSimpleBlocksController.php b/tests/phpunit/misc/test_FrmSimpleBlocksController.php index fbddcf07a7..3fe3a6e497 100644 --- a/tests/phpunit/misc/test_FrmSimpleBlocksController.php +++ b/tests/phpunit/misc/test_FrmSimpleBlocksController.php @@ -12,11 +12,11 @@ public function test_maybe_remove_fade_on_load_for_block_preview() { $_SERVER['CONTENT_TYPE'] = 'application/json'; if ( is_callable( 'wp_is_json_request' ) ) { - $this->assertFalse( strpos( $this->maybe_remove_fade_on_load_for_block_preview( $form ), 'frm_logic_form' ) ); + $this->assertStringNotContainsString( 'frm_logic_form', $this->maybe_remove_fade_on_load_for_block_preview( $form ) ); } unset( $_SERVER['HTTP_ACCEPT'], $_SERVER['CONTENT_TYPE'] ); - $this->assertTrue( str_contains( $this->maybe_remove_fade_on_load_for_block_preview( $form ), 'frm_logic_form' ) ); + $this->assertStringContainsString( 'frm_logic_form', $this->maybe_remove_fade_on_load_for_block_preview( $form ) ); } private function maybe_remove_fade_on_load_for_block_preview( $form ) { diff --git a/tests/phpunit/styles/test_FrmStylesController.php b/tests/phpunit/styles/test_FrmStylesController.php index 8e605064b7..7aee7f8c5b 100644 --- a/tests/phpunit/styles/test_FrmStylesController.php +++ b/tests/phpunit/styles/test_FrmStylesController.php @@ -73,7 +73,7 @@ public function test_save() { FrmStylesController::save(); $returned = ob_get_clean(); - $this->assertNotFalse( strpos( $returned, 'Your styling settings have been saved.' ) ); + $this->assertStringContainsString( 'Your styling settings have been saved.', $returned ); $frm_style = new FrmStyle( $style->ID ); $updated_style = $frm_style->get_one(); $this->assertEquals( $style->post_title . ' Updated', $updated_style->post_title ); diff --git a/tests/phpunit/styles/test_FrmStylesHelper.php b/tests/phpunit/styles/test_FrmStylesHelper.php index 6b1a32e68f..ea31fb3fb2 100644 --- a/tests/phpunit/styles/test_FrmStylesHelper.php +++ b/tests/phpunit/styles/test_FrmStylesHelper.php @@ -11,11 +11,11 @@ class test_FrmStylesHelper extends FrmUnitTest { public function test_get_upload_base() { $base = FrmStylesHelper::get_upload_base(); $this->assertTrue( isset( $base['baseurl'] ) ); - $this->assertTrue( str_contains( $base['baseurl'], 'http://' ) ); + $this->assertStringContainsString( 'http://', $base['baseurl'] ); $_SERVER['HTTPS'] = 'on'; $base = FrmStylesHelper::get_upload_base(); - $this->assertTrue( str_contains( $base['baseurl'], 'https://' ) ); + $this->assertStringContainsString( 'https://', $base['baseurl'] ); } /**