diff --git a/composer.json b/composer.json index b510b99156d53..8adb519ac31da 100644 --- a/composer.json +++ b/composer.json @@ -54,6 +54,7 @@ "phpcompatibility/phpcompatibility-wp": "~2.1.3", "phpstan/phpstan": "2.2.13", "phpstan/phpstan-phpunit": "2.0.18", + "szepeviktor/phpstan-wordpress": "^2.0.4", "yoast/phpunit-polyfills": "^1.1.0" }, "config": { diff --git a/src/wp-admin/customize.php b/src/wp-admin/customize.php index 75c0865b1f8eb..332d7cfc9f13a 100644 --- a/src/wp-admin/customize.php +++ b/src/wp-admin/customize.php @@ -110,8 +110,10 @@ $wp_scripts = new WP_Scripts(); $wp_scripts->registered = $registered; +// @phpstan-ignore return.void (WordPress discards an action callback's return value.) add_action( 'customize_controls_print_scripts', 'print_head_scripts', 20 ); add_action( 'customize_controls_print_footer_scripts', '_wp_footer_scripts' ); +// @phpstan-ignore return.void (WordPress discards an action callback's return value.) add_action( 'customize_controls_print_styles', 'print_admin_styles', 20 ); /** diff --git a/src/wp-admin/includes/admin-filters.php b/src/wp-admin/includes/admin-filters.php index 5337cc02c88c9..3f619d5df2664 100644 --- a/src/wp-admin/includes/admin-filters.php +++ b/src/wp-admin/includes/admin-filters.php @@ -11,6 +11,7 @@ add_action( 'admin_page_access_denied', 'wp_link_manager_disabled_message' ); // Dashboard hooks. +// @phpstan-ignore return.void (WordPress discards an action callback's return value.) add_action( 'activity_box_end', 'wp_dashboard_quota' ); add_action( 'welcome_panel', 'wp_welcome_panel' ); @@ -18,9 +19,13 @@ add_action( 'attachment_submitbox_misc_actions', 'attachment_submitbox_metadata' ); add_filter( 'plupload_init', 'wp_show_heic_upload_error' ); +// @phpstan-ignore return.void (WordPress discards an action callback's return value.) add_action( 'media_upload_image', 'wp_media_upload_handler' ); +// @phpstan-ignore return.void (WordPress discards an action callback's return value.) add_action( 'media_upload_audio', 'wp_media_upload_handler' ); +// @phpstan-ignore return.void (WordPress discards an action callback's return value.) add_action( 'media_upload_video', 'wp_media_upload_handler' ); +// @phpstan-ignore return.void (WordPress discards an action callback's return value.) add_action( 'media_upload_file', 'wp_media_upload_handler' ); add_action( 'post-plupload-upload-ui', 'media_upload_flash_bypass' ); @@ -57,10 +62,12 @@ } add_action( 'admin_print_scripts', 'print_emoji_detection_script' ); +// @phpstan-ignore return.void (WordPress discards an action callback's return value.) add_action( 'admin_print_scripts', 'print_head_scripts', 20 ); add_action( 'admin_print_footer_scripts', '_wp_footer_scripts' ); add_action( 'admin_enqueue_scripts', 'wp_enqueue_emoji_styles' ); add_action( 'admin_print_styles', 'print_emoji_styles' ); // Retained for backwards-compatibility. Unhooked by wp_enqueue_emoji_styles(). +// @phpstan-ignore return.void (WordPress discards an action callback's return value.) add_action( 'admin_print_styles', 'print_admin_styles', 20 ); add_action( 'admin_print_scripts-index.php', 'wp_localize_community_events' ); @@ -130,10 +137,12 @@ add_action( 'load-plugins.php', 'wp_plugin_update_rows', 20 ); // After wp_update_plugins() is called. add_action( 'load-themes.php', 'wp_theme_update_rows', 20 ); // After wp_update_themes() is called. +// @phpstan-ignore return.void (WordPress discards an action callback's return value.) add_action( 'admin_notices', 'update_nag', 3 ); add_action( 'admin_notices', 'deactivated_plugins_notice', 5 ); add_action( 'admin_notices', 'paused_plugins_notice', 5 ); add_action( 'admin_notices', 'paused_themes_notice', 5 ); +// @phpstan-ignore return.void (WordPress discards an action callback's return value.) add_action( 'admin_notices', 'maintenance_nag', 10 ); add_action( 'admin_notices', 'wp_recovery_mode_nag', 1 ); diff --git a/src/wp-admin/includes/class-wp-posts-list-table.php b/src/wp-admin/includes/class-wp-posts-list-table.php index d3d631b9b4467..9957a765dde47 100644 --- a/src/wp-admin/includes/class-wp-posts-list-table.php +++ b/src/wp-admin/includes/class-wp-posts-list-table.php @@ -1856,7 +1856,7 @@ public function inline_edit() { * @param array $users_opt An array of arguments passed to wp_dropdown_users(). * @param bool $bulk A flag to denote if it's a bulk action. */ - $users_opt = apply_filters( 'quick_edit_dropdown_authors_args', $users_opt, $bulk ); + $users_opt = apply_filters( 'quick_edit_dropdown_authors_args', $users_opt, (bool) $bulk ); $authors = wp_dropdown_users( $users_opt ); @@ -1961,7 +1961,7 @@ public function inline_edit() { * @param array $dropdown_args An array of arguments passed to wp_dropdown_pages(). * @param bool $bulk A flag to denote if it's a bulk action. */ - $dropdown_args = apply_filters( 'quick_edit_dropdown_pages_args', $dropdown_args, $bulk ); + $dropdown_args = apply_filters( 'quick_edit_dropdown_pages_args', $dropdown_args, (bool) $bulk ); wp_dropdown_pages( $dropdown_args ); ?> @@ -2108,12 +2108,12 @@ public function inline_edit() { * * @since 6.9.0 * - * @param array $inline_edit_statuses An array of statuses available in the Quick Edit UI. - * @param string $post_type The post type slug. - * @param bool $bulk A flag to denote if it's a bulk action. - * @param bool $can_publish A flag to denote if the user can publish posts. + * @param string[] $inline_edit_statuses An array of statuses available in the Quick Edit UI. + * @param string $post_type The post type slug. + * @param bool $bulk A flag to denote if it's a bulk action. + * @param bool $can_publish A flag to denote if the user can publish posts. */ - $inline_edit_statuses = apply_filters( 'quick_edit_statuses', $inline_edit_statuses, $screen->post_type, $bulk, $can_publish ); + $inline_edit_statuses = apply_filters( 'quick_edit_statuses', $inline_edit_statuses, $screen->post_type, (bool) $bulk, $can_publish ); foreach ( $inline_edit_statuses as $inline_status_value => $inline_status_text ) : ?> diff --git a/src/wp-admin/includes/class-wp-upgrader.php b/src/wp-admin/includes/class-wp-upgrader.php index ba27113ff73de..2005c1e231dd8 100644 --- a/src/wp-admin/includes/class-wp-upgrader.php +++ b/src/wp-admin/includes/class-wp-upgrader.php @@ -933,6 +933,7 @@ public function run( $options ) { * internally during actions, causing an error because * `WP_Upgrader::restore_temp_backup()` expects an array. */ + // @phpstan-ignore return.void (WordPress discards an action callback's return value.) add_action( 'shutdown', array( $this, 'restore_temp_backup' ), 10, 0 ); } $this->skin->error( $result ); @@ -950,6 +951,7 @@ public function run( $options ) { // Clean up the backup kept in the temporary backup directory. if ( ! empty( $options['hook_extra']['temp_backup'] ) ) { // Delete the backup on `shutdown` to avoid a PHP timeout. + // @phpstan-ignore return.void (WordPress discards an action callback's return value.) add_action( 'shutdown', array( $this, 'delete_temp_backup' ), 100, 0 ); } diff --git a/src/wp-admin/includes/dashboard.php b/src/wp-admin/includes/dashboard.php index 488e01aa92481..aafcaa9ac503b 100644 --- a/src/wp-admin/includes/dashboard.php +++ b/src/wp-admin/includes/dashboard.php @@ -1622,7 +1622,7 @@ function wp_dashboard_primary() { * * @since 4.4.0 * - * @param string $items How many items to show in the secondary feed. + * @param int $items How many items to show in the secondary feed. */ 'items' => apply_filters( 'dashboard_secondary_items', 3 ), 'show_summary' => 0, diff --git a/src/wp-admin/includes/media.php b/src/wp-admin/includes/media.php index beaf88101e648..3eedee6969695 100644 --- a/src/wp-admin/includes/media.php +++ b/src/wp-admin/includes/media.php @@ -211,9 +211,9 @@ function image_add_caption( $html, $id, $caption, $title, $align, $url, $size, $ * @since 2.6.0 * * @param bool $bool Whether to disable appending captions. Returning true from the filter - * will disable captions. Default empty string. + * will disable captions. Default false. */ - if ( empty( $caption ) || apply_filters( 'disable_captions', '' ) ) { + if ( empty( $caption ) || apply_filters( 'disable_captions', false ) ) { return $html; } diff --git a/src/wp-admin/includes/ms-admin-filters.php b/src/wp-admin/includes/ms-admin-filters.php index b1d73825f1f4b..dd894ebfc72ea 100644 --- a/src/wp-admin/includes/ms-admin-filters.php +++ b/src/wp-admin/includes/ms-admin-filters.php @@ -29,11 +29,15 @@ add_filter( 'import_allow_create_users', 'check_import_new_users' ); // Notices hooks. +// @phpstan-ignore return.void (WordPress discards an action callback's return value.) add_action( 'admin_notices', 'site_admin_notice' ); +// @phpstan-ignore return.void (WordPress discards an action callback's return value.) add_action( 'network_admin_notices', 'site_admin_notice' ); // Update hooks. +// @phpstan-ignore return.void (WordPress discards an action callback's return value.) add_action( 'network_admin_notices', 'update_nag', 3 ); +// @phpstan-ignore return.void (WordPress discards an action callback's return value.) add_action( 'network_admin_notices', 'maintenance_nag', 10 ); // Network Admin hooks. diff --git a/src/wp-admin/includes/update.php b/src/wp-admin/includes/update.php index b0e998264fe06..bc74d0595453b 100644 --- a/src/wp-admin/includes/update.php +++ b/src/wp-admin/includes/update.php @@ -432,6 +432,7 @@ function wp_plugin_update_rows() { $plugins = array_keys( $plugins->response ); foreach ( $plugins as $plugin_file ) { + // @phpstan-ignore return.void (WordPress discards an action callback's return value.) add_action( "after_plugin_row_{$plugin_file}", 'wp_plugin_update_row', 10, 2 ); } } @@ -657,6 +658,7 @@ function wp_theme_update_rows() { $themes = array_keys( $themes->response ); foreach ( $themes as $theme ) { + // @phpstan-ignore return.void (WordPress discards an action callback's return value.) add_action( "after_theme_row_{$theme}", 'wp_theme_update_row', 10, 2 ); } } diff --git a/src/wp-content/themes/twentytwentyone/inc/template-functions.php b/src/wp-content/themes/twentytwentyone/inc/template-functions.php index f141b99a8cf8f..6af17e19ff5d6 100644 --- a/src/wp-content/themes/twentytwentyone/inc/template-functions.php +++ b/src/wp-content/themes/twentytwentyone/inc/template-functions.php @@ -50,7 +50,7 @@ function twenty_twenty_one_post_classes( $classes ) { return $classes; } -add_filter( 'post_class', 'twenty_twenty_one_post_classes', 10, 3 ); +add_filter( 'post_class', 'twenty_twenty_one_post_classes' ); /** * Adds a pingback url auto-discovery header for single posts, pages, or attachments. diff --git a/src/wp-includes/block-supports/block-style-variations.php b/src/wp-includes/block-supports/block-style-variations.php index 746ed38177a57..8ade72e92f1db 100644 --- a/src/wp-includes/block-supports/block-style-variations.php +++ b/src/wp-includes/block-supports/block-style-variations.php @@ -262,7 +262,7 @@ function wp_enqueue_block_style_variation_styles() { // Register the block support. WP_Block_Supports::get_instance()->register( 'block-style-variation', array() ); -add_filter( 'render_block_data', 'wp_render_block_style_variation_support_styles', 10, 2 ); +add_filter( 'render_block_data', 'wp_render_block_style_variation_support_styles' ); add_filter( 'render_block', 'wp_render_block_style_variation_class_name', 10, 2 ); add_action( 'wp_enqueue_scripts', 'wp_enqueue_block_style_variation_styles', 1 ); diff --git a/src/wp-includes/class-wp-customize-setting.php b/src/wp-includes/class-wp-customize-setting.php index 68dd8b1433857..e5c9615124da2 100644 --- a/src/wp-includes/class-wp-customize-setting.php +++ b/src/wp-includes/class-wp-customize-setting.php @@ -362,6 +362,7 @@ public function preview() { // If the setting does not need previewing now, defer to when it has a value to preview. if ( ! $needs_preview ) { if ( ! has_action( "customize_post_value_set_{$this->id}", array( $this, 'preview' ) ) ) { + // @phpstan-ignore return.void (WordPress discards an action callback's return value.) add_action( "customize_post_value_set_{$this->id}", array( $this, 'preview' ) ); } return false; diff --git a/src/wp-includes/class-wp-recovery-mode.php b/src/wp-includes/class-wp-recovery-mode.php index 8fa6bf22cbdea..b1cbf6af8442b 100644 --- a/src/wp-includes/class-wp-recovery-mode.php +++ b/src/wp-includes/class-wp-recovery-mode.php @@ -92,6 +92,7 @@ public function __construct() { public function initialize() { $this->is_initialized = true; + // @phpstan-ignore return.void (WordPress discards an action callback's return value.) add_action( 'wp_logout', array( $this, 'exit_recovery_mode' ) ); add_action( 'login_form_' . self::EXIT_ACTION, array( $this, 'handle_exit_recovery_mode' ) ); add_action( 'recovery_mode_clean_expired_keys', array( $this, 'clean_expired_keys' ) ); diff --git a/src/wp-includes/comment.php b/src/wp-includes/comment.php index 8293c8b750314..0c7f060bc935f 100644 --- a/src/wp-includes/comment.php +++ b/src/wp-includes/comment.php @@ -785,7 +785,7 @@ function wp_allow_comment( $commentdata, $wp_error = false ) { wp_unslash( $commentdata['comment_content'] ) ); - $dupe_id = $wpdb->get_var( $dupe ); + $dupe_id = (int) $wpdb->get_var( $dupe ); /** * Filters the ID, if any, of the duplicate comment found when creating a new comment. @@ -2815,6 +2815,7 @@ function wp_set_comment_status( $comment_id, $comment_status, $wp_error = false case 'approve': case '1': $status = '1'; + // @phpstan-ignore return.void (WordPress discards an action callback's return value.) add_action( 'wp_set_comment_status', 'wp_new_comment_notify_postauthor' ); break; case 'spam': diff --git a/src/wp-includes/cron.php b/src/wp-includes/cron.php index 1070ae4680b91..2f9b88082639e 100644 --- a/src/wp-includes/cron.php +++ b/src/wp-includes/cron.php @@ -1021,11 +1021,13 @@ function wp_cron(): void { if ( did_action( 'wp_loaded' ) ) { _wp_cron(); } else { + // @phpstan-ignore return.void (WordPress discards an action callback's return value.) add_action( 'wp_loaded', '_wp_cron', 20 ); } } elseif ( doing_action( 'shutdown' ) ) { _wp_cron(); } else { + // @phpstan-ignore return.void (WordPress discards an action callback's return value.) add_action( 'shutdown', '_wp_cron' ); } } diff --git a/src/wp-includes/default-filters.php b/src/wp-includes/default-filters.php index 025a371781200..f87fd014aef11 100644 --- a/src/wp-includes/default-filters.php +++ b/src/wp-includes/default-filters.php @@ -118,6 +118,7 @@ add_action( 'admin_init', 'wp_schedule_update_user_counts' ); add_action( 'wp_update_user_counts', 'wp_schedule_update_user_counts', 10, 0 ); foreach ( array( 'user_register', 'deleted_user' ) as $action ) { + // @phpstan-ignore return.void (WordPress discards an action callback's return value.) add_action( $action, 'wp_maybe_update_user_counts', 10, 0 ); } @@ -307,7 +308,7 @@ add_filter( 'pre_kses', 'wp_pre_kses_less_than' ); add_filter( 'pre_kses', 'wp_pre_kses_block_attributes', 10, 3 ); add_filter( 'sanitize_title', 'sanitize_title_with_dashes', 10, 3 ); -add_action( 'check_comment_flood', 'check_comment_flood_db', 10, 4 ); +add_action( 'check_comment_flood', 'check_comment_flood_db' ); add_filter( 'comment_flood_filter', 'wp_throttle_comment_flood', 10, 3 ); add_filter( 'pre_comment_content', 'wp_rel_ugc', 15 ); @@ -360,7 +361,9 @@ add_action( 'publish_future_post', 'check_and_publish_future_post', 10, 1 ); add_action( 'wp_head', 'wp_robots', 1 ); add_action( 'wp_head', 'print_emoji_detection_script', 7 ); +// @phpstan-ignore return.void (WordPress discards an action callback's return value.) add_action( 'wp_head', 'wp_print_styles', 8 ); +// @phpstan-ignore return.void (WordPress discards an action callback's return value.) add_action( 'wp_head', 'wp_print_head_scripts', 9 ); add_action( 'wp_head', 'wp_generator' ); add_action( 'wp_head', 'rel_canonical' ); @@ -394,7 +397,9 @@ // Login actions. add_action( 'login_head', 'wp_robots', 1 ); add_action( 'login_head', 'wp_resource_hints', 8 ); +// @phpstan-ignore return.void (WordPress discards an action callback's return value.) add_action( 'login_head', 'wp_print_head_scripts', 9 ); +// @phpstan-ignore return.void (WordPress discards an action callback's return value.) add_action( 'login_head', 'print_admin_styles', 9 ); add_action( 'login_head', 'wp_site_icon', 99 ); add_action( 'login_footer', 'wp_print_footer_scripts', 20 ); @@ -428,6 +433,7 @@ add_action( 'do_all_pings', 'do_all_pingbacks', 10, 0 ); add_action( 'do_all_pings', 'do_all_enclosures', 10, 0 ); add_action( 'do_all_pings', 'do_all_trackbacks', 10, 0 ); +// @phpstan-ignore return.void (WordPress discards an action callback's return value.) add_action( 'do_all_pings', 'generic_ping', 10, 0 ); // Disable pings (pingbacks, trackbacks, and ping service notifications) in non-production environments. @@ -437,6 +443,7 @@ add_action( 'do_robots', 'do_robots' ); add_action( 'do_favicon', 'do_favicon' ); +// @phpstan-ignore return.void (WordPress discards an action callback's return value.) add_action( 'wp_before_include_template', 'wp_start_template_enhancement_output_buffer', 1000 ); // Late priority to let `wp_template_enhancement_output_buffer` filters and `wp_finalized_template_enhancement_output_buffer` actions be registered. add_action( 'set_comment_cookies', 'wp_set_comment_cookies', 10, 3 ); add_action( 'sanitize_comment_cookies', 'sanitize_comment_cookies' ); @@ -446,6 +453,7 @@ add_action( 'shutdown', 'wp_ob_end_flush_all', 1 ); // Create a revision whenever a post is updated. add_action( 'wp_after_insert_post', 'wp_save_post_revision_on_insert', 9, 3 ); +// @phpstan-ignore return.void (WordPress discards an action callback's return value.) add_action( 'post_updated', 'wp_save_post_revision', 10, 1 ); add_action( 'publish_post', '_publish_post_hook', 5, 1 ); add_action( 'transition_post_status', '_transition_post_status', 5, 3 ); @@ -467,7 +475,9 @@ // Cron tasks. add_action( 'wp_scheduled_delete', 'wp_scheduled_delete' ); add_action( 'wp_scheduled_auto_draft_delete', 'wp_delete_auto_drafts' ); +// @phpstan-ignore return.void (WordPress discards an action callback's return value.) add_action( 'importer_scheduled_cleanup', 'wp_delete_attachment' ); +// @phpstan-ignore return.void (WordPress discards an action callback's return value.) add_action( 'upgrader_scheduled_cleanup', 'wp_delete_attachment' ); add_action( 'delete_expired_transients', 'delete_expired_transients' ); @@ -536,7 +546,9 @@ add_action( 'wp_update_comment_type_batch', '_wp_batch_update_comment_type' ); // Email notifications. +// @phpstan-ignore return.void (WordPress discards an action callback's return value.) add_action( 'comment_post', 'wp_new_comment_notify_moderator' ); +// @phpstan-ignore return.void (WordPress discards an action callback's return value.) add_action( 'comment_post', 'wp_new_comment_notify_postauthor' ); add_action( 'rest_insert_comment', 'wp_new_comment_via_rest_notify_postauthor' ); add_action( 'rest_insert_comment', 'wp_notify_note_mentions', 10, 3 ); @@ -560,6 +572,7 @@ add_action( 'init', '_wp_connectors_init', 15 ); // Sitemaps actions. +// @phpstan-ignore return.void (WordPress discards an action callback's return value.) add_action( 'init', 'wp_sitemaps_get_server' ); /** @@ -688,6 +701,7 @@ add_action( 'change_locale', 'create_initial_taxonomies' ); // Canonical. +// @phpstan-ignore return.void (WordPress discards an action callback's return value.) add_action( 'template_redirect', 'redirect_canonical' ); add_action( 'template_redirect', 'wp_redirect_admin_locations', 1000 ); @@ -716,12 +730,16 @@ // Admin Bar. // Don't remove. Wrong way to disable. +// @phpstan-ignore return.void (WordPress discards an action callback's return value.) add_action( 'template_redirect', '_wp_admin_bar_init', 0 ); +// @phpstan-ignore return.void (WordPress discards an action callback's return value.) add_action( 'admin_init', '_wp_admin_bar_init' ); add_action( 'wp_enqueue_scripts', 'wp_enqueue_admin_bar_bump_styles' ); add_action( 'wp_enqueue_scripts', 'wp_enqueue_admin_bar_header_styles' ); add_action( 'admin_enqueue_scripts', 'wp_enqueue_admin_bar_header_styles' ); +// @phpstan-ignore return.void (WordPress discards an action callback's return value.) add_action( 'before_signup_header', '_wp_admin_bar_init' ); +// @phpstan-ignore return.void (WordPress discards an action callback's return value.) add_action( 'activate_header', '_wp_admin_bar_init' ); add_action( 'wp_body_open', 'wp_admin_bar_render', 0 ); add_action( 'wp_footer', 'wp_admin_bar_render', 1000 ); // Back-compat for themes not using `wp_body_open`. @@ -745,7 +763,9 @@ add_action( 'embed_head', 'print_emoji_detection_script' ); add_action( 'embed_head', 'wp_enqueue_embed_styles', 9 ); add_action( 'embed_head', 'print_embed_styles' ); // Retained for backwards-compatibility. Unhooked by wp_enqueue_embed_styles(). +// @phpstan-ignore return.void (WordPress discards an action callback's return value.) add_action( 'embed_head', 'wp_print_head_scripts', 20 ); +// @phpstan-ignore return.void (WordPress discards an action callback's return value.) add_action( 'embed_head', 'wp_print_styles', 20 ); add_action( 'embed_head', 'wp_robots' ); add_action( 'embed_head', 'rel_canonical' ); diff --git a/src/wp-includes/media.php b/src/wp-includes/media.php index 1e0b86655e82a..58a86d5eed71a 100644 --- a/src/wp-includes/media.php +++ b/src/wp-includes/media.php @@ -3699,11 +3699,12 @@ function wp_audio_shortcode( $attr, $content = '' ) { * * @since 3.6.0 * - * @param string $html Audio shortcode HTML output. - * @param array $atts Array of audio shortcode attributes. - * @param string $audio Audio file. - * @param int $post_id Post ID. - * @param string $library Media library used for the audio shortcode. + * @param string $html Audio shortcode HTML output. + * @param array $atts Array of audio shortcode attributes. + * @param WP_Post|null $audio Audio attachment post when the shortcode has no source and + * an attached audio file is used, null otherwise. + * @param int $post_id Post ID. + * @param string $library Media library used for the audio shortcode. */ return apply_filters( 'wp_audio_shortcode', $html, $atts, $audio, $post_id, $library ); } @@ -3994,11 +3995,12 @@ function wp_video_shortcode( $attr, $content = '' ) { * * @since 3.6.0 * - * @param string $output Video shortcode HTML output. - * @param array $atts Array of video shortcode attributes. - * @param string $video Video file. - * @param int $post_id Post ID. - * @param string $library Media library used for the video shortcode. + * @param string $output Video shortcode HTML output. + * @param array $atts Array of video shortcode attributes. + * @param WP_Post|null $video Video attachment post when the shortcode has no source and + * an attached video file is used, null otherwise. + * @param int $post_id Post ID. + * @param string $library Media library used for the video shortcode. */ return apply_filters( 'wp_video_shortcode', $output, $atts, $video, $post_id, $library ); } @@ -4072,7 +4074,7 @@ function next_image_link( $size = 'thumbnail', $text = false ) { * @param bool $prev Optional. Whether to display the next (false) or previous (true) link. Default true. * @param string|int[] $size Optional. Image size. Accepts any registered image size name, or an array * of width and height values in pixels (in that order). Default 'thumbnail'. - * @param bool $text Optional. Link text. Default false. + * @param string|false $text Optional. Link text. Default false. * @return string Markup for image link. */ function get_adjacent_image_link( $prev = true, $size = 'thumbnail', $text = false ) { @@ -4128,7 +4130,7 @@ function get_adjacent_image_link( $prev = true, $size = 'thumbnail', $text = fal * @param int $attachment_id Attachment ID * @param string|int[] $size Requested image size. Can be any registered image size name, or * an array of width and height values in pixels (in that order). - * @param string $text Link text. + * @param string|false $text Link text, or false for the image itself. */ return apply_filters( "{$adjacent}_image_link", $output, $attachment_id, $size, $text ); } @@ -4143,7 +4145,7 @@ function get_adjacent_image_link( $prev = true, $size = 'thumbnail', $text = fal * @param bool $prev Optional. Whether to display the next (false) or previous (true) link. Default true. * @param string|int[] $size Optional. Image size. Accepts any registered image size name, or an array * of width and height values in pixels (in that order). Default 'thumbnail'. - * @param bool $text Optional. Link text. Default false. + * @param string|false $text Optional. Link text. Default false. */ function adjacent_image_link( $prev = true, $size = 'thumbnail', $text = false ) { echo get_adjacent_image_link( $prev, $size, $text ); diff --git a/src/wp-includes/meta.php b/src/wp-includes/meta.php index 62d28cdbd9f5c..27bbb6d396353 100644 --- a/src/wp-includes/meta.php +++ b/src/wp-includes/meta.php @@ -266,6 +266,7 @@ function update_metadata( $meta_type, $object_id, $meta_key, $meta_value, $prev_ if ( empty( $meta_ids ) ) { return add_metadata( $meta_type, $object_id, $raw_meta_key, $passed_value ); } + $meta_ids = array_map( 'intval', $meta_ids ); $_meta_value = $meta_value; $meta_value = maybe_serialize( $meta_value ); diff --git a/src/wp-includes/ms-blogs.php b/src/wp-includes/ms-blogs.php index c54563fbbd2b8..f834ceb23a88e 100644 --- a/src/wp-includes/ms-blogs.php +++ b/src/wp-includes/ms-blogs.php @@ -265,7 +265,8 @@ function get_blog_details( $fields = null, $get_all = true ) { * @since MU (3.0.0) * @deprecated 4.7.0 Use {@see 'site_details'} instead. * - * @param WP_Site $details The blog details. + * @param WP_Site|stdClass $details The blog details: a WP_Site from get_blog_details(), or a plain + * object copy of one from WP_Site::get_details(). */ $details = apply_filters_deprecated( 'blog_details', array( $details ), '4.7.0', 'site_details' ); diff --git a/src/wp-includes/ms-default-filters.php b/src/wp-includes/ms-default-filters.php index 8682d48e18e45..439683a80c89a 100644 --- a/src/wp-includes/ms-default-filters.php +++ b/src/wp-includes/ms-default-filters.php @@ -23,9 +23,12 @@ // Users. add_filter( 'wpmu_validate_user_signup', 'signup_nonce_check' ); add_action( 'init', 'maybe_add_existing_user_to_blog' ); +// @phpstan-ignore return.void (WordPress discards an action callback's return value.) add_action( 'wpmu_new_user', 'newuser_notify_siteadmin' ); add_action( 'wpmu_activate_user', 'add_new_user_to_blog', 10, 3 ); +// @phpstan-ignore return.void (WordPress discards an action callback's return value.) add_action( 'wpmu_activate_user', 'wpmu_welcome_user_notification', 10, 3 ); +// @phpstan-ignore return.void (WordPress discards an action callback's return value.) add_action( 'after_signup_user', 'wpmu_signup_user_notification', 10, 4 ); add_action( 'network_site_new_created_user', 'wp_send_new_user_notifications' ); add_action( 'network_site_users_created_user', 'wp_send_new_user_notifications' ); @@ -38,7 +41,9 @@ // Blogs. add_filter( 'wpmu_validate_blog_signup', 'signup_nonce_check' ); +// @phpstan-ignore return.void (WordPress discards an action callback's return value.) add_action( 'wpmu_activate_blog', 'wpmu_welcome_notification', 10, 5 ); +// @phpstan-ignore return.void (WordPress discards an action callback's return value.) add_action( 'after_signup_site', 'wpmu_signup_blog_notification', 10, 7 ); add_filter( 'wp_normalize_site_data', 'wp_normalize_site_data', 10, 1 ); add_action( 'wp_validate_site_data', 'wp_validate_site_data', 10, 3 ); @@ -48,9 +53,12 @@ add_action( 'wp_insert_site', 'wp_maybe_transition_site_statuses_on_update', 10, 1 ); add_action( 'wp_update_site', 'wp_maybe_transition_site_statuses_on_update', 10, 2 ); add_action( 'wp_update_site', 'wp_maybe_clean_new_site_cache_on_update', 10, 2 ); +// @phpstan-ignore return.void (WordPress discards an action callback's return value.) add_action( 'wp_initialize_site', 'wp_initialize_site', 10, 2 ); add_action( 'wp_initialize_site', 'wpmu_log_new_registrations', 100, 2 ); +// @phpstan-ignore return.void (WordPress discards an action callback's return value.) add_action( 'wp_initialize_site', 'newblog_notify_siteadmin', 100, 1 ); +// @phpstan-ignore return.void (WordPress discards an action callback's return value.) add_action( 'wp_uninitialize_site', 'wp_uninitialize_site', 10, 1 ); add_action( 'update_blog_public', 'wp_update_blog_public_option_on_site_update', 1, 2 ); diff --git a/src/wp-includes/taxonomy.php b/src/wp-includes/taxonomy.php index 56f12f7013dcb..db824959645b0 100644 --- a/src/wp-includes/taxonomy.php +++ b/src/wp-includes/taxonomy.php @@ -2110,7 +2110,7 @@ function wp_delete_term( $term, $taxonomy, $args = array() ) { return $ids; } - $tt_id = $ids['term_taxonomy_id']; + $tt_id = (int) $ids['term_taxonomy_id']; $defaults = array(); diff --git a/tests/phpstan/README.md b/tests/phpstan/README.md index 3ab750a069471..6dc5935f2c2e8 100644 --- a/tests/phpstan/README.md +++ b/tests/phpstan/README.md @@ -106,6 +106,17 @@ Calls whose hook name contains no literal text, such as the `apply_filters_ref_a One consequence worth knowing: because a hook's documentation may live in a different file than the call inheriting it, editing a hook docblock in a file that reference comments point at discards PHPStan's result cache. Every call site inheriting that docblock has to be analyzed again, and PHPStan cannot infer that dependency on its own. +### Extensions from szepeviktor/phpstan-wordpress + +[szepeviktor/phpstan-wordpress](https://github.com/szepeviktor/phpstan-wordpress) is the set of PHPStan extensions maintained for the WordPress ecosystem, and the hook extensions above began as adaptations of it. It is installed as a Composer development dependency so that core can load its extensions directly rather than carry copies of them. Its own `extension.neon` is not included, because that loads [php-stubs/wordpress-stubs](https://github.com/php-stubs/wordpress-stubs), a declaration of every core function and class, which is the code being analyzed here. Instead, [`base.neon`](base.neon) registers the extensions that apply to core one at a time. Installing the package installs the stubs as well; they are not read. + +What the package provides falls into four groups, and only the first is loaded: + +- **Used, because a docblock cannot express what they do.** `ShortcodeAttsDynamicFunctionReturnTypeExtension` types the result of `shortcode_atts()` from the defaults passed to it, the same merge that `wp_parse_args()` performs. `HookCallbackRule` checks a callback registered with `add_action()` or `add_filter()` against its registration: that `$accepted_args` agrees with the parameters the callback declares, and that a filter callback returns a value. Its third check, that an action callback returns nothing, does not fit core, which registers functions that happen to return a value on actions as a matter of course and discards the value; each such registration carries an inline `@phpstan-ignore` saying so. `HookDocsRule` checks that the type each `@param` of a hook docblock documents accepts the value the hook passes. That documented type is what `apply_filters()` is typed from, so a wrong one misleads every caller. It reads only a docblock written at the call, so a hook documented elsewhere through a reference comment is not checked. +- **Not used, because core's own versions do the same and more.** `HookDocsVisitor`, `HookDocBlock` and `ApplyFiltersDynamicFunctionReturnTypeExtension` are what the [hook documentation](#hook-documentation) extensions were adapted from. Core's resolve the "This filter is documented in" reference comments, which the originals do not, and core's rules already check the `@param` counts that `HookDocsRule` also checks, so only its type check adds anything. +- **Not used, because a docblock does the same.** `EscSql`, `NormalizeWhitespace`, `StripslashesFromStringsOnly`, `SlashitFunctions`, `WpParseUrl` and `WpSlash` each narrow one function's return type from its arguments in a way a conditional `@phpstan-return` expresses. Where core has that docblock already, as `wp_parse_url()`, `wp_slash()` and `stripslashes_from_strings_only()` do, loading the extension changes nothing; where it does not yet, as `esc_sql()`, the docblock is the fix to make. A docblock in core also types the function for every plugin, since the stubs are generated from core. phpstan-wordpress itself has gone that way: its 2.x branch dropped the extensions it had for `get_post()`, `get_terms()`, `current_time()`, `wp_die()`, `is_wp_error()` and others in favor of types carried by the stubs. The [function map](https://github.com/php-stubs/wordpress-stubs/blob/master/functionMap.php) those stubs applied on top of core's docblocks has since been brought into core itself, as `@phpstan-param` and `@phpstan-return` tags on the functions it named, so the same types now reach core's own analysis, the stubs, and every plugin from one place. +- **Not applicable to core.** `WpConstantFetchRule` discourages reading a constant such as `MULTISITE` where a function exists to read it, but core is where those functions read them. `AssertWpErrorTypeSpecifyingExtension` narrows the argument of `assertWPError()` in tests, which are not analyzed; when they are, `@phpstan-assert` on the methods themselves is the way to express it. + ### Errors these rules report These identifiers are specific to WordPress, and can be ignored or baselined like any other error, as described [below](#ignoring-and-baselining-errors). @@ -118,6 +129,17 @@ These identifiers are specific to WordPress, and can be ignored or baselined lik | `wordpress.hookDocReferenceHookMissing` | The referenced file exists, but documents no hook of that name. Either the reference is stale, or the canonical docblock has moved. | | `wordpress.hookParamCountMismatch` | The call passes a different number of arguments than the docblock documents `@param` tags for. Passing fewer risks an `ArgumentCountError` in a callback registered for the documented count; passing more silently drops the extra argument and leaves the documentation misleading. | +The rules loaded from szepeviktor/phpstan-wordpress report under PHPStan's own identifiers rather than ones of their own, so their errors share a baseline with the errors PHPStan itself reports under that identifier. They are told apart by their messages. + +| Identifier | Message | What it means | +| --- | --- | --- | +| `arguments.count` | `Callback expects N parameters, $accepted_args is set to M.` | The `$accepted_args` of an `add_action()` or `add_filter()` call does not fit the callback's signature. Fewer than the callback requires is an `ArgumentCountError` when the hook fires; more than it declares is misleading, and usually a leftover from an earlier signature. | +| `return.missing` | `Filter callback return statement is missing.` | A filter callback returns nothing, so the value being filtered becomes `null`. | +| `return.void` | `Action callback returns X but should not return anything.` | An action callback returns a value. WordPress discards it, so where core registers such a function deliberately the call carries an inline `@phpstan-ignore` saying so. | +| `parameter.phpDocType` | `@param X $name does not accept actual type of parameter: Y.` | The type a hook docblock documents for a parameter does not accept the value the hook passes. Fix the docblock, or the value; the documented type is what callbacks and the `apply_filters()` return type rely on. | +| `paramTag.count` | `Expected N @param tags, found M.` | The same mismatch `wordpress.hookParamCountMismatch` reports, for a docblock written at the call. | +| `phpDoc.parseError` | `One or more @param tags has an invalid name or invalid syntax.` | A `@param` tag in a hook docblock could not be parsed, or is named `$this`. | + ## Ignoring and baselining errors As we adopt PHPStan iteratively, you may be faced with false positives due to legacy code, or code that is not worth changing at this time. diff --git a/tests/phpstan/base.neon b/tests/phpstan/base.neon index 4cf457f07e0f7..f7a5ad88bc4f2 100644 --- a/tests/phpstan/base.neon +++ b/tests/phpstan/base.neon @@ -62,6 +62,42 @@ services: tags: - phpstan.resultCacheMetaExtension + # The services below come from szepeviktor/phpstan-wordpress, the PHPStan extensions + # maintained for the WordPress ecosystem, installed through Composer. Its own + # extension.neon is deliberately not included: that bootstraps php-stubs/wordpress-stubs, + # which describes the very code analyzed here, so every function and class would be + # declared twice. Instead, the extensions that apply to core are registered one by one. + # See tests/phpstan/README.md for which ones are used, which are not, and why. + + # Types the return value of shortcode_atts() from the defaults it was called with, so + # a shortcode's attributes are an array shape rather than a plain array. The merge + # a call performs is not something a docblock can express. + - + class: SzepeViktor\PHPStan\WordPress\ShortcodeAttsDynamicFunctionReturnTypeExtension + tags: + - phpstan.broker.dynamicFunctionReturnTypeExtension + + # Checks the callback given to add_action() and add_filter() against the call: that + # `$accepted_args` agrees with the number of parameters the callback declares, that + # a filter callback returns a value, and that an action callback does not. The last + # check is switched off for core in phpstan.neon.dist; see the note there. + - + class: SzepeViktor\PHPStan\WordPress\HookCallbackRule + tags: + - phpstan.rules.rule + + # Checks that the type each `@param` of a hook docblock documents accepts the value + # the hook passes. That documented type is what apply_filters() is typed from above, + # so a wrong one misleads every caller. Only a docblock written at the call is checked: + # the rule reads the docblock through phpstan-wordpress's own resolver, registered + # beside it, which does not follow "This filter is documented in" reference comments. + - + class: SzepeViktor\PHPStan\WordPress\HookDocBlock + - + class: SzepeViktor\PHPStan\WordPress\HookDocsRule + tags: + - phpstan.rules.rule + # Runs the visitors above over every file PHPStan parses, not only the ones it # analyzes. # diff --git a/tests/phpstan/baselines/argument.type.neon b/tests/phpstan/baselines/argument.type.neon index 14681f13d929a..c214bdddc8fbe 100644 --- a/tests/phpstan/baselines/argument.type.neon +++ b/tests/phpstan/baselines/argument.type.neon @@ -1039,7 +1039,7 @@ parameters: count: 1 path: ../../../src/wp-includes/load.php - - message: '#^Parameter \#5 \$text of function wp_get_attachment_link expects string\|false, bool given\.$#' + message: '#^Parameter \#1 \$args of function get_posts expects array\{numberposts\?\: int, category\?\: int\|string, include\?\: array\, exclude\?\: array\, suppress_filters\?\: bool, \.\.\.\}\|string\|null, array\{include\: non\-falsy\-string, post_status\: ''inherit'', post_type\: ''attachment'', post_mime_type\: ''image'', order\: string, orderby\: string\} given\.$#' identifier: argument.type count: 1 path: ../../../src/wp-includes/media.php