diff --git a/classes/helpers/FrmAppHelper.php b/classes/helpers/FrmAppHelper.php index 8df4e8607d..ffcf9bd42e 100644 --- a/classes/helpers/FrmAppHelper.php +++ b/classes/helpers/FrmAppHelper.php @@ -4679,7 +4679,7 @@ public static function set_current_screen_and_hook_suffix() { * * @since 5.2.02 * - * @param string $text Text in the pill. Default is NEW. + * @param string|null $text Text in the pill. Default is NEW. */ public static function show_pill_text( $text = null ) { if ( null === $text ) { diff --git a/classes/helpers/FrmOnSubmitHelper.php b/classes/helpers/FrmOnSubmitHelper.php index 7ae25cbac6..4067eee828 100644 --- a/classes/helpers/FrmOnSubmitHelper.php +++ b/classes/helpers/FrmOnSubmitHelper.php @@ -237,9 +237,9 @@ public static function save_on_submit_settings( $form_id ) { /** * Populates the On Submit data to form options. * - * @param array $form_options Form options. - * @param object $action Optional. The On Submit action object. - * @param string $event Form event. Default is `create`. + * @param array $form_options Form options. + * @param object|null $action Optional. The On Submit action object. + * @param string $event Form event. Default is `create`. * * @return void */ diff --git a/classes/models/FrmAddon.php b/classes/models/FrmAddon.php index 2fa3c67b98..f7874f62e2 100644 --- a/classes/models/FrmAddon.php +++ b/classes/models/FrmAddon.php @@ -175,9 +175,9 @@ public function edd_plugin_updater() { * * @uses api_request() * - * @param mixed $_data - * @param string $_action - * @param object $_args + * @param mixed $_data + * @param string $_action + * @param object|null $_args * * @return object Data. */ diff --git a/classes/models/FrmEntryMeta.php b/classes/models/FrmEntryMeta.php index 4b5455cfe4..be544352fd 100644 --- a/classes/models/FrmEntryMeta.php +++ b/classes/models/FrmEntryMeta.php @@ -47,7 +47,7 @@ public static function add_entry_meta( $entry_id, $field_id, $meta_key, $meta_va * @param string $meta_key Deprecated. * @param array|string $meta_value * - * @return bool|false|int + * @return bool|int */ public static function update_entry_meta( $entry_id, $field_id, $meta_key, $meta_value ) { if ( ! $field_id ) { diff --git a/phpcs-sniffs/Formidable/Sniffs/Commenting/AddMissingParamTypeFromDefaultSniff.php b/phpcs-sniffs/Formidable/Sniffs/Commenting/AddMissingParamTypeFromDefaultSniff.php new file mode 100644 index 0000000000..9acb39fdeb --- /dev/null +++ b/phpcs-sniffs/Formidable/Sniffs/Commenting/AddMissingParamTypeFromDefaultSniff.php @@ -0,0 +1,512 @@ +getTokens(); + + // Check if function has a docblock. + $docblock = $this->findDocblock( $phpcsFile, $stackPtr ); + + if ( false === $docblock ) { + return; + } + + // Get function parameters with their default values. + $params = $this->getParametersWithDefaults( $phpcsFile, $stackPtr ); + + if ( empty( $params ) ) { + return; + } + + // Get existing @param tags with their types. + $existingParams = $this->getExistingParamTags( $phpcsFile, $docblock ); + + if ( empty( $existingParams ) ) { + return; + } + + // Find missing types based on default values. + $missingTypes = array(); + + foreach ( $params as $param ) { + $paramName = $param['name']; + + if ( ! isset( $existingParams[ $paramName ] ) ) { + continue; + } + + if ( null === $param['default_type'] ) { + continue; + } + + $existingType = $existingParams[ $paramName ]['type']; + $defaultType = $param['default_type']; + + if ( ! $this->typeIncludesType( $existingType, $defaultType ) ) { + $missingTypes[ $paramName ] = array( + 'existing' => $existingType, + 'missing' => $defaultType, + 'token' => $existingParams[ $paramName ]['token'], + ); + } + } + + if ( empty( $missingTypes ) ) { + return; + } + + // Report and fix each missing type. + foreach ( $missingTypes as $paramName => $info ) { + $existingType = $info['existing']; + $missingType = $info['missing']; + + // If adding 'bool', remove 'false' from the existing type. + if ( $missingType === 'bool' ) { + $existingType = $this->removeFalseFromType( $existingType ); + } + + $newType = $existingType . '|' . $missingType; + + $fix = $phpcsFile->addFixableError( + '@param %s is missing type "%s" based on default value', + $info['token'], + 'MissingParamTypeFromDefault', + array( $paramName, $missingType ) + ); + + 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 with their default values. + * + * @param File $phpcsFile The file being scanned. + * @param int $stackPtr The function token position. + * + * @return array Array of parameter info including default type. + */ + private function getParametersWithDefaults( 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 ) { + continue; + } + + $paramName = $tokens[ $i ]['content']; + $defaultType = null; + + // Look for = after the variable. + $next = $phpcsFile->findNext( T_WHITESPACE, $i + 1, $closer, true ); + + if ( false !== $next && $tokens[ $next ]['code'] === T_EQUAL ) { + $defaultType = $this->getDefaultValueType( $phpcsFile, $next + 1, $closer ); + } + + $params[] = array( + 'name' => $paramName, + 'token' => $i, + 'default_type' => $defaultType, + ); + } + + return $params; + } + + /** + * Determine the type of a default value. + * + * @param File $phpcsFile The file being scanned. + * @param int $startPtr The position after the = sign. + * @param int $endPtr The end of the parameter list. + * + * @return string|null The type of the default value, or null if unknown. + */ + private function getDefaultValueType( File $phpcsFile, $startPtr, $endPtr ) { + $tokens = $phpcsFile->getTokens(); + + // Find the first non-whitespace token after =. + $valueToken = $phpcsFile->findNext( T_WHITESPACE, $startPtr, $endPtr, true ); + + if ( false === $valueToken ) { + return null; + } + + $code = $tokens[ $valueToken ]['code']; + $content = $tokens[ $valueToken ]['content']; + + // Array literals. + if ( $code === T_ARRAY || $code === T_OPEN_SHORT_ARRAY ) { + return 'array'; + } + + // Null. + if ( $code === T_NULL ) { + return 'null'; + } + + // Boolean true - add bool type. + if ( $code === T_TRUE ) { + return 'bool'; + } + + // Boolean false - only add if 'false' is not already in the type. + if ( $code === T_FALSE ) { + return 'false'; + } + + // Integer. + if ( $code === T_LNUMBER ) { + return 'int'; + } + + // Float. + if ( $code === T_DNUMBER ) { + return 'float'; + } + + // String (single or double quoted, or heredoc/nowdoc). + if ( $code === T_CONSTANT_ENCAPSED_STRING || $code === T_START_HEREDOC || $code === T_START_NOWDOC ) { + return 'string'; + } + + // Empty string ''. + if ( $content === "''" || $content === '""' ) { + return 'string'; + } + + return null; + } + + /** + * 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; + } + + /** + * 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 ) { + // If existing type is 'mixed', everything is covered. + if ( strtolower( trim( $existingType ) ) === 'mixed' ) { + return true; + } + + // Split existing type by | for checking. + $existingTypes = explode( '|', $existingType ); + + // If checking for 'array', also check for typed arrays and shorthand notation. + if ( $checkType === 'array' ) { + foreach ( $existingTypes as $existing ) { + $normalized = $this->normalizeType( $existing ); + + if ( $normalized === 'array' ) { + return true; + } + } + + return false; + } + + // If checking for 'object', treat any class name as covering it. + if ( $checkType === 'object' ) { + foreach ( $existingTypes as $existing ) { + $normalized = $this->normalizeType( $existing ); + + if ( $normalized === 'object' || $this->isClassName( $existing ) ) { + return true; + } + } + + return false; + } + + // If checking for 'bool', accept 'bool' or 'true' as covering it (but not 'false' alone). + if ( $checkType === 'bool' ) { + foreach ( $existingTypes as $existing ) { + $normalized = $this->normalizeType( $existing ); + + if ( $normalized === 'bool' || $normalized === 'true' ) { + return true; + } + } + + return false; + } + + // If checking for 'false', accept 'false' or 'bool' as covering it. + if ( $checkType === 'false' ) { + foreach ( $existingTypes as $existing ) { + $normalized = $this->normalizeType( $existing ); + + if ( $normalized === 'false' || $normalized === 'bool' ) { + return true; + } + } + + return false; + } + + foreach ( $existingTypes as $existing ) { + $normalizedExisting = $this->normalizeType( $existing ); + $normalizedCheck = $this->normalizeType( $checkType ); + + if ( $normalizedExisting === $normalizedCheck ) { + return true; + } + + // Check if 'mixed' covers everything. + if ( $normalizedExisting === 'mixed' ) { + return true; + } + } + + return false; + } + + /** + * Check if a type string looks like a class name. + * + * @param string $type The type to check. + * + * @return bool + */ + private function isClassName( $type ) { + $type = trim( $type ); + + // Skip primitive types and known non-class types. + $primitives = array( + 'int', 'integer', 'float', 'double', 'real', 'string', 'bool', 'boolean', + 'array', 'object', 'null', 'mixed', 'void', 'callable', 'iterable', + 'resource', 'true', 'false', 'self', 'static', 'parent', + ); + + $lowerType = strtolower( $type ); + + if ( in_array( $lowerType, $primitives, true ) ) { + return false; + } + + // Skip if it's an array notation. + if ( preg_match( '/\[\]$/', $type ) || preg_match( '/^array\s*[<{]/', $lowerType ) ) { + return false; + } + + // If it starts with uppercase or contains backslash (namespace), it's likely a class. + if ( preg_match( '/^[A-Z]/', $type ) || strpos( $type, '\\' ) !== false ) { + 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 504157f4c6..55391ce7c5 100644 --- a/phpcs-sniffs/Formidable/ruleset.xml +++ b/phpcs-sniffs/Formidable/ruleset.xml @@ -72,4 +72,5 @@ + diff --git a/stripe/models/FrmTransLitePayment.php b/stripe/models/FrmTransLitePayment.php index a759c78213..5998d5de43 100755 --- a/stripe/models/FrmTransLitePayment.php +++ b/stripe/models/FrmTransLitePayment.php @@ -73,8 +73,8 @@ public function get_defaults() { * * @since 6.7 * - * @param string $from_date From date. - * @param string $to_date To date. + * @param string|null $from_date From date. + * @param string|null $to_date To date. * * @return array Contains `count` and `total`. */ diff --git a/tests/phpunit/misc/test_FrmDirectFileAccess.php b/tests/phpunit/misc/test_FrmDirectFileAccess.php index 58c8d00369..cce7d91b71 100644 --- a/tests/phpunit/misc/test_FrmDirectFileAccess.php +++ b/tests/phpunit/misc/test_FrmDirectFileAccess.php @@ -6,7 +6,7 @@ class test_FrmDirectFileAccess extends FrmUnitTest { /** - * @param string $dir + * @param bool|string $dir * @param array $results * * @return array