diff --git a/classes/controllers/FrmFormsController.php b/classes/controllers/FrmFormsController.php
index 476daa2e93..8d40f04461 100644
--- a/classes/controllers/FrmFormsController.php
+++ b/classes/controllers/FrmFormsController.php
@@ -596,23 +596,25 @@ public static function preview_block_theme_body_classnames( $classes ) {
* @return void
*/
private static function fallback_when_page_template_part_is_not_supported_by_theme() {
- if ( have_posts() ) {
- the_post();
- self::get_template( 'header' );
+ if ( ! have_posts() ) {
+ return;
+ }
- // add some generic class names to the container to add some natural padding to the content.
- // .entry-content catches the WordPress TwentyTwenty theme.
- // .container catches Customizr content.
- echo '
';
- the_content();
- echo '
';
+ the_post();
+ self::get_template( 'header' );
- // Prevent extra unexpected forms in the footer.
- // For some reason this happens in the Twenty Twenty Five theme.
- add_filter( 'frm_filter_final_form', '__return_empty_string' );
+ // add some generic class names to the container to add some natural padding to the content.
+ // .entry-content catches the WordPress TwentyTwenty theme.
+ // .container catches Customizr content.
+ echo '';
+ the_content();
+ echo '
';
- self::get_template( 'footer' );
- }
+ // Prevent extra unexpected forms in the footer.
+ // For some reason this happens in the Twenty Twenty Five theme.
+ add_filter( 'frm_filter_final_form', '__return_empty_string' );
+
+ self::get_template( 'footer' );
}
/**
diff --git a/classes/controllers/FrmOnboardingWizardController.php b/classes/controllers/FrmOnboardingWizardController.php
index 9eebe2e653..acf6dd9e60 100644
--- a/classes/controllers/FrmOnboardingWizardController.php
+++ b/classes/controllers/FrmOnboardingWizardController.php
@@ -203,19 +203,21 @@ public static function do_admin_redirects() {
* @return void
*/
public static function maybe_load_page() {
- if ( self::is_onboarding_wizard_page() ) {
- // Dismiss the onboarding wizard message so it stops appearing after it is clicked.
- $message = new FrmInbox();
- $message->dismiss( 'onboarding_wizard' );
-
- add_action( 'admin_menu', self::class . '::menu', 99 );
- add_action( 'admin_init', self::class . '::assign_properties' );
- add_action( 'admin_enqueue_scripts', self::class . '::enqueue_assets', 15 );
- add_action( 'admin_head', self::class . '::remove_menu' );
-
- add_filter( 'admin_body_class', self::class . '::add_admin_body_classes', 999 );
- add_filter( 'frm_show_footer_links', '__return_false' );
+ if ( ! self::is_onboarding_wizard_page() ) {
+ return;
}
+
+ // Dismiss the onboarding wizard message so it stops appearing after it is clicked.
+ $message = new FrmInbox();
+ $message->dismiss( 'onboarding_wizard' );
+
+ add_action( 'admin_menu', self::class . '::menu', 99 );
+ add_action( 'admin_init', self::class . '::assign_properties' );
+ add_action( 'admin_enqueue_scripts', self::class . '::enqueue_assets', 15 );
+ add_action( 'admin_head', self::class . '::remove_menu' );
+
+ add_filter( 'admin_body_class', self::class . '::add_admin_body_classes', 999 );
+ add_filter( 'frm_show_footer_links', '__return_false' );
}
/**
diff --git a/phpcs-sniffs/Formidable/Sniffs/CodeAnalysis/FlipIfToEarlyReturnSniff.php b/phpcs-sniffs/Formidable/Sniffs/CodeAnalysis/FlipIfToEarlyReturnSniff.php
new file mode 100644
index 0000000000..eab0038041
--- /dev/null
+++ b/phpcs-sniffs/Formidable/Sniffs/CodeAnalysis/FlipIfToEarlyReturnSniff.php
@@ -0,0 +1,417 @@
+getTokens();
+
+ // Skip closures.
+ if ( $tokens[ $stackPtr ]['code'] === T_CLOSURE ) {
+ return;
+ }
+
+ // Get the function's scope opener and closer.
+ if ( ! isset( $tokens[ $stackPtr ]['scope_opener'] ) || ! isset( $tokens[ $stackPtr ]['scope_closer'] ) ) {
+ return;
+ }
+
+ $scopeOpener = $tokens[ $stackPtr ]['scope_opener'];
+ $scopeCloser = $tokens[ $stackPtr ]['scope_closer'];
+
+ // Find the first non-whitespace token inside the function.
+ $firstStatement = $phpcsFile->findNext(
+ T_WHITESPACE,
+ $scopeOpener + 1,
+ $scopeCloser,
+ true
+ );
+
+ if ( false === $firstStatement ) {
+ return;
+ }
+
+ // Check if the first statement is an if.
+ if ( $tokens[ $firstStatement ]['code'] !== T_IF ) {
+ return;
+ }
+
+ $ifToken = $firstStatement;
+
+ // Check if this if has an else or elseif - skip those.
+ if ( isset( $tokens[ $ifToken ]['scope_closer'] ) ) {
+ $next = $phpcsFile->findNext( T_WHITESPACE, $tokens[ $ifToken ]['scope_closer'] + 1, null, true );
+
+ if ( false !== $next && in_array( $tokens[ $next ]['code'], array( T_ELSE, T_ELSEIF ), true ) ) {
+ return;
+ }
+ }
+
+ // Get the if's scope closer.
+ if ( ! isset( $tokens[ $ifToken ]['scope_closer'] ) ) {
+ return;
+ }
+
+ $ifScopeCloser = $tokens[ $ifToken ]['scope_closer'];
+
+ // Check if the if statement is the only thing in the function body.
+ // The next non-whitespace token after the if's closing brace should be the function's closing brace.
+ $afterIf = $phpcsFile->findNext(
+ T_WHITESPACE,
+ $ifScopeCloser + 1,
+ null,
+ true
+ );
+
+ // If the next token after the if isn't the function's closing brace, there's more code.
+ if ( false === $afterIf || $afterIf !== $scopeCloser ) {
+ return;
+ }
+
+ // Count statements inside the if body.
+ $statementCount = $this->countStatementsInScope( $phpcsFile, $ifToken );
+
+ if ( $statementCount < $this->minStatements ) {
+ return;
+ }
+
+ // Get the condition for the error message.
+ $condition = $this->getConditionString( $phpcsFile, $ifToken );
+
+ $fix = $phpcsFile->addFixableError(
+ 'Consider using early return pattern. Flip the condition "if (%s)" to "if (! %s) { return; }" and move the body outside.',
+ $ifToken,
+ 'Found',
+ array( $condition, $condition )
+ );
+
+ if ( true === $fix ) {
+ $this->applyFix( $phpcsFile, $ifToken, $scopeOpener, $scopeCloser );
+ }
+ }
+
+ /**
+ * Apply the fix to flip the if statement to early return.
+ *
+ * @param File $phpcsFile The file being scanned.
+ * @param int $ifToken The position of the if token.
+ * @param int $funcOpener The function's scope opener.
+ * @param int $funcCloser The function's scope closer.
+ *
+ * @return void
+ */
+ private function applyFix( File $phpcsFile, $ifToken, $funcOpener, $funcCloser ) {
+ $tokens = $phpcsFile->getTokens();
+ $fixer = $phpcsFile->fixer;
+
+ $ifOpener = $tokens[ $ifToken ]['scope_opener'];
+ $ifCloser = $tokens[ $ifToken ]['scope_closer'];
+ $conditionOpener = $tokens[ $ifToken ]['parenthesis_opener'];
+ $conditionCloser = $tokens[ $ifToken ]['parenthesis_closer'];
+
+ // Get the original condition.
+ $originalCondition = '';
+
+ for ( $i = $conditionOpener + 1; $i < $conditionCloser; $i++ ) {
+ $originalCondition .= $tokens[ $i ]['content'];
+ }
+ $originalCondition = trim( $originalCondition );
+
+ // Negate the condition.
+ $negatedCondition = $this->negateCondition( $originalCondition );
+
+ // Get the if body content (without the braces), and dedent by one level.
+ $ifBodyContent = '';
+
+ for ( $i = $ifOpener + 1; $i < $ifCloser; $i++ ) {
+ $ifBodyContent .= $tokens[ $i ]['content'];
+ }
+
+ // Remove one level of indentation from the body.
+ $ifBodyContent = $this->dedentCode( $ifBodyContent );
+
+ // Determine the indentation.
+ $indent = $this->getIndentation( $phpcsFile, $ifToken );
+
+ $fixer->beginChangeset();
+
+ // Remove everything from the if token to the if's closing brace.
+ for ( $i = $ifToken; $i <= $ifCloser; $i++ ) {
+ $fixer->replaceToken( $i, '' );
+ }
+
+ // Build the new code: early return + body.
+ $newCode = 'if ( ' . $negatedCondition . ' ) {' . $phpcsFile->eolChar;
+ $newCode .= $indent . "\treturn;" . $phpcsFile->eolChar;
+ $newCode .= $indent . '}' . $phpcsFile->eolChar;
+ $newCode .= $phpcsFile->eolChar;
+ $newCode .= $indent . ltrim( $ifBodyContent );
+
+ // Insert the new code at the if token position.
+ $fixer->addContent( $ifToken, $newCode );
+
+ $fixer->endChangeset();
+ }
+
+ /**
+ * Remove one level of indentation from code.
+ *
+ * @param string $code The code to dedent.
+ *
+ * @return string The dedented code.
+ */
+ private function dedentCode( $code ) {
+ $lines = explode( "\n", $code );
+ $result = array();
+
+ foreach ( $lines as $line ) {
+ // Remove one tab or 4 spaces from the beginning.
+ if ( strpos( $line, "\t" ) === 0 ) {
+ $line = substr( $line, 1 );
+ } elseif ( strpos( $line, ' ' ) === 0 ) {
+ $line = substr( $line, 4 );
+ }
+ $result[] = $line;
+ }
+
+ return implode( "\n", $result );
+ }
+
+ /**
+ * Negate a condition string.
+ *
+ * @param string $condition The original condition.
+ *
+ * @return string The negated condition.
+ */
+ private function negateCondition( $condition ) {
+ $condition = trim( $condition );
+
+ // If condition starts with !, remove it.
+ if ( strpos( $condition, '! ' ) === 0 ) {
+ return substr( $condition, 2 );
+ }
+
+ if ( strpos( $condition, '!' ) === 0 ) {
+ return substr( $condition, 1 );
+ }
+
+ // Otherwise, add negation.
+ // If condition is simple (no spaces or operators at top level), just add !
+ // If complex, wrap in parentheses.
+ if ( $this->isSimpleCondition( $condition ) ) {
+ return '! ' . $condition;
+ }
+
+ return '! ( ' . $condition . ' )';
+ }
+
+ /**
+ * Check if a condition is simple (single variable/function call).
+ *
+ * @param string $condition The condition to check.
+ *
+ * @return bool
+ */
+ private function isSimpleCondition( $condition ) {
+ // Simple conditions: $var, function(), $obj->method(), etc.
+ // Complex conditions contain: &&, ||, and, or, comparisons.
+ $complexPatterns = array( '&&', '||', ' and ', ' or ', '===', '!==', '==', '!=', '<=', '>=', '<', '>' );
+
+ foreach ( $complexPatterns as $pattern ) {
+ if ( strpos( $condition, $pattern ) !== false ) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Get the indentation of a token.
+ *
+ * @param File $phpcsFile The file being scanned.
+ * @param int $stackPtr The token position.
+ *
+ * @return string The indentation string.
+ */
+ private function getIndentation( File $phpcsFile, $stackPtr ) {
+ $tokens = $phpcsFile->getTokens();
+
+ // Find the first token on this line.
+ $lineStart = $stackPtr;
+
+ while ( $lineStart > 0 && $tokens[ $lineStart - 1 ]['line'] === $tokens[ $stackPtr ]['line'] ) {
+ --$lineStart;
+ }
+
+ // If the first token is whitespace, that's our indentation.
+ if ( $tokens[ $lineStart ]['code'] === T_WHITESPACE ) {
+ return $tokens[ $lineStart ]['content'];
+ }
+
+ return '';
+ }
+
+ /**
+ * Check if an if statement has an else or elseif clause.
+ *
+ * @param File $phpcsFile The file being scanned.
+ * @param int $ifToken The position of the if token.
+ *
+ * @return bool
+ */
+ private function hasElseOrElseif( File $phpcsFile, $ifToken ) {
+ $tokens = $phpcsFile->getTokens();
+
+ if ( ! isset( $tokens[ $ifToken ]['scope_closer'] ) ) {
+ return false;
+ }
+
+ $scopeCloser = $tokens[ $ifToken ]['scope_closer'];
+
+ // Look for else or elseif after the if's closing brace.
+ $next = $phpcsFile->findNext( T_WHITESPACE, $scopeCloser + 1, null, true );
+
+ if ( false === $next ) {
+ return false;
+ }
+
+ return in_array( $tokens[ $next ]['code'], array( T_ELSE, T_ELSEIF ), true );
+ }
+
+ /**
+ * Count the number of statements inside a scope.
+ *
+ * @param File $phpcsFile The file being scanned.
+ * @param int $scopeToken The token with the scope to count.
+ *
+ * @return int
+ */
+ private function countStatementsInScope( File $phpcsFile, $scopeToken ) {
+ $tokens = $phpcsFile->getTokens();
+
+ if ( ! isset( $tokens[ $scopeToken ]['scope_opener'] ) || ! isset( $tokens[ $scopeToken ]['scope_closer'] ) ) {
+ return 0;
+ }
+
+ $scopeOpener = $tokens[ $scopeToken ]['scope_opener'];
+ $scopeCloser = $tokens[ $scopeToken ]['scope_closer'];
+
+ // The target level is the level inside the scope (opener level + 1).
+ $targetLevel = $tokens[ $scopeOpener ]['level'] + 1;
+
+ $count = 0;
+
+ for ( $i = $scopeOpener + 1; $i < $scopeCloser; $i++ ) {
+ // Only count semicolons at the immediate scope level (not nested).
+ if ( $tokens[ $i ]['code'] === T_SEMICOLON && $tokens[ $i ]['level'] === $targetLevel ) {
+ ++$count;
+ }
+
+ // Also count control structures as statements.
+ if ( in_array( $tokens[ $i ]['code'], array( T_IF, T_FOREACH, T_FOR, T_WHILE, T_SWITCH, T_TRY ), true ) ) {
+ if ( $tokens[ $i ]['level'] === $targetLevel ) {
+ ++$count;
+
+ // Skip to the end of this control structure.
+ if ( isset( $tokens[ $i ]['scope_closer'] ) ) {
+ $i = $tokens[ $i ]['scope_closer'];
+ }
+ }
+ }
+ }
+
+ return $count;
+ }
+
+ /**
+ * Get the condition string from an if statement.
+ *
+ * @param File $phpcsFile The file being scanned.
+ * @param int $ifToken The position of the if token.
+ *
+ * @return string
+ */
+ private function getConditionString( File $phpcsFile, $ifToken ) {
+ $tokens = $phpcsFile->getTokens();
+
+ if ( ! isset( $tokens[ $ifToken ]['parenthesis_opener'] ) || ! isset( $tokens[ $ifToken ]['parenthesis_closer'] ) ) {
+ return '...';
+ }
+
+ $opener = $tokens[ $ifToken ]['parenthesis_opener'];
+ $closer = $tokens[ $ifToken ]['parenthesis_closer'];
+
+ $condition = '';
+
+ for ( $i = $opener + 1; $i < $closer; $i++ ) {
+ $condition .= $tokens[ $i ]['content'];
+ }
+
+ // Trim and truncate if too long.
+ $condition = trim( $condition );
+
+ if ( strlen( $condition ) > 50 ) {
+ $condition = substr( $condition, 0, 47 ) . '...';
+ }
+
+ return $condition;
+ }
+}
diff --git a/phpcs-sniffs/Formidable/ruleset.xml b/phpcs-sniffs/Formidable/ruleset.xml
index 5cff08c243..9cb6a2ffd1 100644
--- a/phpcs-sniffs/Formidable/ruleset.xml
+++ b/phpcs-sniffs/Formidable/ruleset.xml
@@ -32,4 +32,5 @@
+