diff --git a/classes/controllers/FrmFieldsController.php b/classes/controllers/FrmFieldsController.php index d326cd5d9c..99d43c5b1e 100644 --- a/classes/controllers/FrmFieldsController.php +++ b/classes/controllers/FrmFieldsController.php @@ -155,9 +155,9 @@ public static function get_field_array_from_id( $field_id ) { /** * @since 3.0 * - * @param array|int|object $field_object - * @param array $values - * @param int $form_id + * @param array|int|object|string $field_object + * @param array $values + * @param int $form_id * * @return void */ diff --git a/classes/controllers/FrmFormActionsController.php b/classes/controllers/FrmFormActionsController.php index ad33f6b6e7..5db7552e0f 100644 --- a/classes/controllers/FrmFormActionsController.php +++ b/classes/controllers/FrmFormActionsController.php @@ -639,7 +639,7 @@ public static function trigger_create_actions( $entry_id, $form_id, $args = arra /** * @param string $event * @param int|object|string $form - * @param int|string $entry + * @param int|object|string $entry * @param string $type * @param array $args * diff --git a/classes/controllers/FrmFormsController.php b/classes/controllers/FrmFormsController.php index a3205657ec..8fa889e1f2 100644 --- a/classes/controllers/FrmFormsController.php +++ b/classes/controllers/FrmFormsController.php @@ -3394,9 +3394,9 @@ private static function show_lone_success_message( $atts ) { * @since 2.05 * @since 6.0.x Added the third parameter. * - * @param object $form Form object. - * @param int $entry_id Entry ID. - * @param array $args See {@see FrmFormsController::run_success_action()}. + * @param object $form Form object. + * @param int|string $entry_id Entry ID. + * @param array $args See {@see FrmFormsController::run_success_action()}. * * @return string */ diff --git a/classes/helpers/FrmAppHelper.php b/classes/helpers/FrmAppHelper.php index 78851d3c0d..8df4e8607d 100644 --- a/classes/helpers/FrmAppHelper.php +++ b/classes/helpers/FrmAppHelper.php @@ -2432,7 +2432,7 @@ public static function checked( $values, $current ) { /** * @param array|string $values - * @param string $current + * @param string|null $current * * @return bool */ diff --git a/classes/helpers/FrmEntriesHelper.php b/classes/helpers/FrmEntriesHelper.php index fb81851ce2..83c642bcad 100644 --- a/classes/helpers/FrmEntriesHelper.php +++ b/classes/helpers/FrmEntriesHelper.php @@ -416,9 +416,9 @@ private static function set_parent_field_posted_value( $field, $value, $args ) { } /** - * @param array|int|object $field - * @param mixed $value - * @param array $args + * @param array|int|object|string $field + * @param mixed $value + * @param array $args * * @return void */ diff --git a/classes/models/FrmEntry.php b/classes/models/FrmEntry.php index e9c65b7fdc..6a16db6dc1 100644 --- a/classes/models/FrmEntry.php +++ b/classes/models/FrmEntry.php @@ -462,7 +462,7 @@ public static function get_new_entry_name( $values, $default = '' ) { * * @since 2.0.9 * - * @param int|object $entry By reference. + * @param int|object|string $entry By reference. * * @return void */ diff --git a/classes/models/FrmForm.php b/classes/models/FrmForm.php index d032d8245a..d77620a6aa 100644 --- a/classes/models/FrmForm.php +++ b/classes/models/FrmForm.php @@ -605,8 +605,8 @@ public static function translatable_strings( $form ) { } /** - * @param int $id - * @param string $status + * @param array|int $id + * @param string $status * * @return bool|int */ @@ -837,7 +837,7 @@ public static function get_key_by_id( $id ) { * * @since 2.0.9 * - * @param int|object $form + * @param array|int|object $form * * @return void */ diff --git a/classes/models/FrmFormAction.php b/classes/models/FrmFormAction.php index 8f93d76919..b10ff4ff9a 100644 --- a/classes/models/FrmFormAction.php +++ b/classes/models/FrmFormAction.php @@ -666,8 +666,8 @@ public static function get_action_limit( $form_id, $limit = 99 ) { /** * @since 3.04 * - * @param array $args - * @param string $default_status + * @param array|string $args + * @param string $default_status * * @return void */ diff --git a/phpcs-sniffs/Formidable/Sniffs/Commenting/AddMissingParamTypeSniff.php b/phpcs-sniffs/Formidable/Sniffs/Commenting/AddMissingParamTypeSniff.php new file mode 100644 index 0000000000..f465cde47f --- /dev/null +++ b/phpcs-sniffs/Formidable/Sniffs/Commenting/AddMissingParamTypeSniff.php @@ -0,0 +1,508 @@ + 'array', + 'is_object' => 'object', + 'is_int' => 'int', + 'is_integer' => 'int', + 'is_long' => 'int', + 'is_float' => 'float', + 'is_double' => 'float', + 'is_real' => 'float', + 'is_string' => 'string', + 'is_bool' => 'bool', + 'is_null' => 'null', + 'is_numeric' => 'string', + 'is_iterable' => 'iterable', + 'is_resource' => 'resource', + ); + + /** + * Returns an array of tokens this test wants to listen for. + * + * @return array + */ + public function register() { + return array( T_FUNCTION ); + } + + /** + * 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(); + + // Skip if no scope (abstract method, interface method). + if ( ! isset( $tokens[ $stackPtr ]['scope_opener'] ) || ! isset( $tokens[ $stackPtr ]['scope_closer'] ) ) { + return; + } + + // Check if function has a docblock. + $docblock = $this->findDocblock( $phpcsFile, $stackPtr ); + + if ( false === $docblock ) { + return; + } + + // Get function parameters. + $params = $this->getParameters( $phpcsFile, $stackPtr ); + + if ( empty( $params ) ) { + return; + } + + // Get existing @param tags with their types. + $existingParams = $this->getExistingParamTags( $phpcsFile, $docblock ); + + if ( empty( $existingParams ) ) { + return; + } + + $scopeOpener = $tokens[ $stackPtr ]['scope_opener']; + $scopeCloser = $tokens[ $stackPtr ]['scope_closer']; + + // Find is_* checks for each parameter. + $missingTypes = array(); + + foreach ( $params as $param ) { + $paramName = $param['name']; + + if ( ! isset( $existingParams[ $paramName ] ) ) { + continue; + } + + $existingType = $existingParams[ $paramName ]['type']; + $checkedTypes = $this->findIsChecksForParam( $phpcsFile, $paramName, $scopeOpener, $scopeCloser ); + + foreach ( $checkedTypes as $checkedType ) { + if ( ! $this->typeIncludesType( $existingType, $checkedType ) ) { + if ( ! isset( $missingTypes[ $paramName ] ) ) { + $missingTypes[ $paramName ] = array( + 'existing' => $existingType, + 'missing' => array(), + 'token' => $existingParams[ $paramName ]['token'], + ); + } + $missingTypes[ $paramName ]['missing'][] = $checkedType; + } + } + } + + if ( empty( $missingTypes ) ) { + return; + } + + // Report and fix each missing type. + foreach ( $missingTypes as $paramName => $info ) { + $missingList = array_unique( $info['missing'] ); + $newType = $info['existing'] . '|' . implode( '|', $missingList ); + + $fix = $phpcsFile->addFixableError( + '@param %s is missing type(s) %s based on is_* checks in function body', + $info['token'], + 'MissingParamType', + array( $paramName, implode( ', ', $missingList ) ) + ); + + if ( $fix ) { + $this->fixParamType( $phpcsFile, $info['token'], $info['existing'], $newType ); + } + } + } + + /** + * Find the docblock for a function. + * + * @param File $phpcsFile The file being scanned. + * @param int $stackPtr The function token position. + * + * @return false|int The docblock opener position, or false. + */ + private function findDocblock( File $phpcsFile, $stackPtr ) { + $tokens = $phpcsFile->getTokens(); + + $ignore = array( + T_WHITESPACE, + T_STATIC, + T_PUBLIC, + T_PRIVATE, + T_PROTECTED, + T_ABSTRACT, + T_FINAL, + ); + + $prev = $phpcsFile->findPrevious( $ignore, $stackPtr - 1, null, true ); + + if ( false !== $prev && $tokens[ $prev ]['code'] === T_DOC_COMMENT_CLOSE_TAG ) { + return $tokens[ $prev ]['comment_opener']; + } + + return false; + } + + /** + * Get function parameters. + * + * @param File $phpcsFile The file being scanned. + * @param int $stackPtr The function token position. + * + * @return array Array of parameter info. + */ + private function getParameters( File $phpcsFile, $stackPtr ) { + $tokens = $phpcsFile->getTokens(); + $params = array(); + + if ( ! isset( $tokens[ $stackPtr ]['parenthesis_opener'] ) ) { + return $params; + } + + $opener = $tokens[ $stackPtr ]['parenthesis_opener']; + $closer = $tokens[ $stackPtr ]['parenthesis_closer']; + + for ( $i = $opener + 1; $i < $closer; $i++ ) { + if ( $tokens[ $i ]['code'] === T_VARIABLE ) { + $params[] = array( + 'name' => $tokens[ $i ]['content'], + 'token' => $i, + ); + } + } + + return $params; + } + + /** + * Get existing @param tags from docblock with their types. + * + * @param File $phpcsFile The file being scanned. + * @param int $docblock The docblock opener position. + * + * @return array Associative array of param name => array('type' => string, 'token' => int). + */ + private function getExistingParamTags( File $phpcsFile, $docblock ) { + $tokens = $phpcsFile->getTokens(); + $existing = array(); + + if ( ! isset( $tokens[ $docblock ]['comment_closer'] ) ) { + return $existing; + } + + $closer = $tokens[ $docblock ]['comment_closer']; + + for ( $i = $docblock; $i < $closer; $i++ ) { + if ( $tokens[ $i ]['code'] !== T_DOC_COMMENT_TAG ) { + continue; + } + + if ( $tokens[ $i ]['content'] !== '@param' ) { + continue; + } + + // Find the type and parameter name in this @param line. + for ( $j = $i + 1; $j < $closer; $j++ ) { + if ( $tokens[ $j ]['code'] === T_DOC_COMMENT_STRING ) { + $content = $tokens[ $j ]['content']; + + // Parse "type $varName description" format. + if ( preg_match( '/^([^\s]+)\s+(\$\w+)/', $content, $matches ) ) { + $existing[ $matches[2] ] = array( + 'type' => $matches[1], + 'token' => $j, + ); + } + break; + } + + if ( $tokens[ $j ]['code'] === T_DOC_COMMENT_TAG ) { + break; + } + } + } + + return $existing; + } + + /** + * Find is_* checks for a specific parameter in the function body. + * + * @param File $phpcsFile The file being scanned. + * @param string $varName The variable name. + * @param int $scopeOpener The function scope opener. + * @param int $scopeCloser The function scope closer. + * + * @return array Array of types that are checked. + */ + private function findIsChecksForParam( File $phpcsFile, $varName, $scopeOpener, $scopeCloser ) { + $tokens = $phpcsFile->getTokens(); + $checkedTypes = array(); + + // Find the first reassignment of this variable (if any). + $reassignmentPos = $this->findFirstReassignment( $phpcsFile, $varName, $scopeOpener, $scopeCloser ); + + for ( $i = $scopeOpener + 1; $i < $scopeCloser; $i++ ) { + if ( $tokens[ $i ]['code'] !== T_STRING ) { + continue; + } + + $functionName = $tokens[ $i ]['content']; + + if ( ! isset( $this->isCheckToType[ $functionName ] ) ) { + continue; + } + + // Check if followed by ( $varName ). + $openParen = $phpcsFile->findNext( T_WHITESPACE, $i + 1, $scopeCloser, true ); + + if ( false === $openParen || $tokens[ $openParen ]['code'] !== T_OPEN_PARENTHESIS ) { + continue; + } + + $varToken = $phpcsFile->findNext( T_WHITESPACE, $openParen + 1, $scopeCloser, true ); + + if ( false === $varToken || $tokens[ $varToken ]['code'] !== T_VARIABLE || $tokens[ $varToken ]['content'] !== $varName ) { + continue; + } + + // Make sure the variable is used directly, not as array access or object property. + $afterVar = $phpcsFile->findNext( T_WHITESPACE, $varToken + 1, $scopeCloser, true ); + + if ( false !== $afterVar ) { + $afterCode = $tokens[ $afterVar ]['code']; + + // Skip $var[...] (array access) or $var->... (object property). + if ( $afterCode === T_OPEN_SQUARE_BRACKET || $afterCode === T_OBJECT_OPERATOR || $afterCode === T_NULLSAFE_OBJECT_OPERATOR ) { + continue; + } + } + + // Skip if this is_* check occurs after the variable has been reassigned. + if ( false !== $reassignmentPos && $i > $reassignmentPos ) { + continue; + } + + $checkedTypes[] = $this->isCheckToType[ $functionName ]; + } + + return array_unique( $checkedTypes ); + } + + /** + * Find the first reassignment of a variable in the function body. + * + * @param File $phpcsFile The file being scanned. + * @param string $varName The variable name. + * @param int $scopeOpener The function scope opener. + * @param int $scopeCloser The function scope closer. + * + * @return false|int The position of the first reassignment, or false if none. + */ + private function findFirstReassignment( File $phpcsFile, $varName, $scopeOpener, $scopeCloser ) { + $tokens = $phpcsFile->getTokens(); + $reassignmentPos = false; + + for ( $i = $scopeOpener + 1; $i < $scopeCloser; $i++ ) { + if ( $tokens[ $i ]['code'] !== T_VARIABLE ) { + continue; + } + + if ( $tokens[ $i ]['content'] !== $varName ) { + continue; + } + + // Check if this is an assignment ($var = ...). + $next = $phpcsFile->findNext( T_WHITESPACE, $i + 1, $scopeCloser, true ); + + if ( false !== $next && $tokens[ $next ]['code'] === T_EQUAL ) { + $reassignmentPos = $i; + break; + } + } + + // Also check for FrmAppHelper::unserialize_or_decode( $var ) which changes the type. + $typeChangingCall = $this->findTypeChangingCall( $phpcsFile, $varName, $scopeOpener, $scopeCloser ); + + // Return the earliest position. + if ( false === $reassignmentPos ) { + return $typeChangingCall; + } + + if ( false === $typeChangingCall ) { + return $reassignmentPos; + } + + return min( $reassignmentPos, $typeChangingCall ); + } + + /** + * Find calls that change a variable's type (like FrmAppHelper::unserialize_or_decode). + * + * @param File $phpcsFile The file being scanned. + * @param string $varName The variable name. + * @param int $scopeOpener The function scope opener. + * @param int $scopeCloser The function scope closer. + * + * @return false|int The position of the call, or false if none. + */ + private function findTypeChangingCall( File $phpcsFile, $varName, $scopeOpener, $scopeCloser ) { + $tokens = $phpcsFile->getTokens(); + + for ( $i = $scopeOpener + 1; $i < $scopeCloser; $i++ ) { + if ( $tokens[ $i ]['code'] !== T_STRING ) { + continue; + } + + if ( $tokens[ $i ]['content'] !== 'unserialize_or_decode' ) { + continue; + } + + // Check if followed by ( $varName ). + $openParen = $phpcsFile->findNext( T_WHITESPACE, $i + 1, $scopeCloser, true ); + + if ( false === $openParen || $tokens[ $openParen ]['code'] !== T_OPEN_PARENTHESIS ) { + continue; + } + + $varToken = $phpcsFile->findNext( T_WHITESPACE, $openParen + 1, $scopeCloser, true ); + + if ( false !== $varToken && $tokens[ $varToken ]['code'] === T_VARIABLE && $tokens[ $varToken ]['content'] === $varName ) { + return $i; + } + } + + return false; + } + + /** + * Check if an existing type string includes a specific type. + * + * @param string $existingType The existing type string (e.g., "string|array"). + * @param string $checkType The type to check for. + * + * @return bool + */ + private function typeIncludesType( $existingType, $checkType ) { + // Handle compound types from is_numeric. + $checkTypes = explode( '|', $checkType ); + + // Split existing type by | for checking. + $existingTypes = explode( '|', $existingType ); + + // If callable is in existing types, treat array and string as covered (callable can be an array or string). + if ( $checkType === 'array' || $checkType === 'string' ) { + foreach ( $existingTypes as $existing ) { + $normalizedExisting = $this->normalizeType( $existing ); + + if ( $normalizedExisting === 'callable' ) { + return true; + } + } + } + + foreach ( $checkTypes as $singleType ) { + foreach ( $existingTypes as $existing ) { + // Normalize types for comparison. + $normalizedExisting = $this->normalizeType( $existing ); + $normalizedCheck = $this->normalizeType( $singleType ); + + if ( $normalizedExisting === $normalizedCheck ) { + return true; + } + + // Check if 'mixed' covers everything. + if ( $normalizedExisting === 'mixed' ) { + return true; + } + } + } + + return false; + } + + /** + * Normalize a type for comparison. + * + * @param string $type The type to normalize. + * + * @return string + */ + private function normalizeType( $type ) { + $type = strtolower( trim( $type ) ); + + // Normalize common aliases. + $aliases = array( + 'integer' => 'int', + 'boolean' => 'bool', + 'double' => 'float', + 'real' => 'float', + 'long' => 'int', + 'stdclass' => 'object', + 'closure' => 'callable', + ); + + if ( isset( $aliases[ $type ] ) ) { + return $aliases[ $type ]; + } + + // Normalize typed arrays (array, array, etc.) to just 'array'. + if ( preg_match( '/^array\s*getTokens(); + $content = $tokens[ $tokenPtr ]['content']; + + // Replace the old type with the new type. + $newContent = preg_replace( + '/^' . preg_quote( $oldType, '/' ) . '/', + $newType, + $content, + 1 + ); + + $phpcsFile->fixer->beginChangeset(); + $phpcsFile->fixer->replaceToken( $tokenPtr, $newContent ); + $phpcsFile->fixer->endChangeset(); + } +} diff --git a/phpcs-sniffs/Formidable/ruleset.xml b/phpcs-sniffs/Formidable/ruleset.xml index 4c7ede4a6f..504157f4c6 100644 --- a/phpcs-sniffs/Formidable/ruleset.xml +++ b/phpcs-sniffs/Formidable/ruleset.xml @@ -71,4 +71,5 @@ +