diff --git a/src/wp-includes/class-wp-dependencies.php b/src/wp-includes/class-wp-dependencies.php index 057fd8059e4af..60c117d2f67b4 100644 --- a/src/wp-includes/class-wp-dependencies.php +++ b/src/wp-includes/class-wp-dependencies.php @@ -104,6 +104,18 @@ class WP_Dependencies { */ private $queued_before_register = array(); + /** + * List of handles for dependencies encountered which themselves have missing dependencies. + * + * A dependency handle is added to this list when it is discovered to have missing dependencies. At this time, a + * warning is emitted with {@see _doing_it_wrong()}. The handle is then added to this list, so that duplicate + * warnings don't occur. + * + * @since 7.0.0 + * @var string[] + */ + private $dependencies_with_missing_dependencies = array(); + /** * Processes the items and dependencies. * @@ -199,10 +211,22 @@ public function all_deps( $handles, $recursion = false, $group = false ) { continue; } - $keep_going = true; + $keep_going = true; + $missing_dependencies = array(); + if ( isset( $this->registered[ $handle ] ) && count( $this->registered[ $handle ]->deps ) > 0 ) { + $missing_dependencies = array_diff( $this->registered[ $handle ]->deps, array_keys( $this->registered ) ); + } if ( ! isset( $this->registered[ $handle ] ) ) { $keep_going = false; // Item doesn't exist. - } elseif ( $this->registered[ $handle ]->deps && array_diff( $this->registered[ $handle ]->deps, array_keys( $this->registered ) ) ) { + } elseif ( count( $missing_dependencies ) > 0 ) { + if ( ! in_array( $handle, $this->dependencies_with_missing_dependencies, true ) ) { + _doing_it_wrong( + get_class( $this ) . '::add', + $this->get_dependency_warning_message( $handle, $missing_dependencies ), + '7.0.0' + ); + $this->dependencies_with_missing_dependencies[] = $handle; + } $keep_going = false; // Item requires dependencies that don't exist. } elseif ( $this->registered[ $handle ]->deps && ! $this->all_deps( $this->registered[ $handle ]->deps, true, $new_group ) ) { $keep_going = false; // Item requires dependencies that don't exist. @@ -535,4 +559,22 @@ public function get_etag( $load ) { */ return 'W/"' . md5( $etag ) . '"'; } + + /** + * Gets a dependency warning message for a handle. + * + * @since 7.0.0 + * + * @param string $handle Handle with missing dependencies. + * @param string[] $missing_dependency_handles Missing dependency handles. + * @return string Formatted, localized warning message. + */ + protected function get_dependency_warning_message( $handle, $missing_dependency_handles ) { + return sprintf( + /* translators: 1: Handle, 2: Comma-separated list of missing dependency handles. */ + __( 'The handle "%1$s" was enqueued with dependencies that are not registered: %2$s.' ), + $handle, + implode( ', ', $missing_dependency_handles ) + ); + } } diff --git a/src/wp-includes/class-wp-script-modules.php b/src/wp-includes/class-wp-script-modules.php index 6a919e2625125..e05e1900a2b49 100644 --- a/src/wp-includes/class-wp-script-modules.php +++ b/src/wp-includes/class-wp-script-modules.php @@ -70,6 +70,17 @@ class WP_Script_Modules { 'high', ); + /** + * List of IDs for script modules encountered which have missing dependencies. + * + * An ID is added to this list when it is discovered to have missing dependencies. At this time, a warning is + * emitted with {@see _doing_it_wrong()}. The ID is then added to this list, so that duplicate warnings don't occur. + * + * @since 7.0.0 + * @var string[] + */ + private $modules_with_missing_dependencies = array(); + /** * Registers the script module if no script module with that script module * identifier has already been registered. @@ -722,7 +733,22 @@ private function sort_item_dependencies( string $id, array $import_types, array } // If the item requires dependencies that do not exist, fail. - if ( count( array_diff( $dependency_ids, array_keys( $this->registered ) ) ) > 0 ) { + $missing_dependencies = array_diff( $dependency_ids, array_keys( $this->registered ) ); + if ( count( $missing_dependencies ) > 0 ) { + if ( ! in_array( $id, $this->modules_with_missing_dependencies, true ) ) { + _doing_it_wrong( + get_class( $this ) . '::register', + sprintf( + /* translators: 1: Script module ID, 2: Comma-separated list of missing dependency IDs. */ + __( 'The script module with the ID "%1$s" was enqueued with dependencies that are not registered: %2$s.' ), + $id, + implode( ', ', $missing_dependencies ) + ), + '7.0.0' + ); + $this->modules_with_missing_dependencies[] = $id; + } + return false; } diff --git a/src/wp-includes/class-wp-scripts.php b/src/wp-includes/class-wp-scripts.php index b15bd3f8e904e..a30b09249fd52 100644 --- a/src/wp-includes/class-wp-scripts.php +++ b/src/wp-includes/class-wp-scripts.php @@ -1164,4 +1164,22 @@ public function reset() { $this->ext_version = ''; $this->ext_handles = ''; } + + /** + * Gets a script-specific dependency warning message. + * + * @since 7.0.0 + * + * @param string $handle Script handle with missing dependencies. + * @param string[] $missing_dependency_handles Missing dependency handles. + * @return string Formatted, localized warning message. + */ + protected function get_dependency_warning_message( $handle, $missing_dependency_handles ) { + return sprintf( + /* translators: 1: Script handle, 2: Comma-separated list of missing dependency handles. */ + __( 'The script with the handle "%1$s" was enqueued with dependencies that are not registered: %2$s.' ), + $handle, + implode( ', ', $missing_dependency_handles ) + ); + } } diff --git a/src/wp-includes/class-wp-styles.php b/src/wp-includes/class-wp-styles.php index 9d6d5b5dd2460..67fb3a0fd40e1 100644 --- a/src/wp-includes/class-wp-styles.php +++ b/src/wp-includes/class-wp-styles.php @@ -493,4 +493,22 @@ public function reset() { $this->concat_version = ''; $this->print_html = ''; } + + /** + * Gets a style-specific dependency warning message. + * + * @since 7.0.0 + * + * @param string $handle Style handle with missing dependencies. + * @param string[] $missing_dependency_handles Missing dependency handles. + * @return string Formatted, localized warning message. + */ + protected function get_dependency_warning_message( $handle, $missing_dependency_handles ) { + return sprintf( + /* translators: 1: Style handle, 2: Comma-separated list of missing dependency handles. */ + __( 'The style with the handle "%1$s" was enqueued with dependencies that are not registered: %2$s.' ), + $handle, + implode( ', ', $missing_dependency_handles ) + ); + } } diff --git a/tests/phpunit/tests/dependencies/scripts.php b/tests/phpunit/tests/dependencies/scripts.php index cf6960099cb6c..a3c8b92695f4f 100644 --- a/tests/phpunit/tests/dependencies/scripts.php +++ b/tests/phpunit/tests/dependencies/scripts.php @@ -4093,4 +4093,33 @@ public function test_print_translations_no_display_no_sourceurl() { $translations_script_data = $wp_scripts->print_translations( 'test-example', false ); $this->assertStringNotContainsStringIgnoringCase( 'sourceURL=', $translations_script_data ); } + + /** + * Tests that WP_Scripts emits a _doing_it_wrong() notice for missing dependencies. + * + * @ticket 64229 + * @covers WP_Dependencies::all_deps + */ + public function test_wp_scripts_doing_it_wrong_for_missing_dependencies() { + $expected_incorrect_usage = 'WP_Scripts::add'; + $this->setExpectedIncorrectUsage( $expected_incorrect_usage ); + + wp_register_script( 'registered-dep', '/registered-dep.js' ); + wp_enqueue_script( 'main', '/main.js', array( 'registered-dep', 'missing-dep' ) ); + + $markup = get_echo( 'wp_print_scripts' ); + $this->assertStringNotContainsString( 'main.js', $markup, 'Expected script to be absent.' ); + + $this->assertArrayHasKey( + $expected_incorrect_usage, + $this->caught_doing_it_wrong, + "Expected $expected_incorrect_usage to trigger a _doing_it_wrong() notice for missing dependency." + ); + + $this->assertStringContainsString( + 'The script with the handle "main" was enqueued with dependencies that are not registered: missing-dep', + $this->caught_doing_it_wrong[ $expected_incorrect_usage ], + 'Expected _doing_it_wrong() notice to indicate missing dependencies for enqueued script.' + ); + } } diff --git a/tests/phpunit/tests/dependencies/styles.php b/tests/phpunit/tests/dependencies/styles.php index c9aed449da139..74e4db47330b4 100644 --- a/tests/phpunit/tests/dependencies/styles.php +++ b/tests/phpunit/tests/dependencies/styles.php @@ -814,4 +814,36 @@ public function test_source_url_with_concat() { $this->assertEqualHTML( $expected, $printed ); } + + /** + * Tests that WP_Styles emits a _doing_it_wrong() notice for missing dependencies. + * + * @ticket 64229 + * @covers WP_Dependencies::all_deps + */ + public function test_wp_style_doing_it_wrong_for_missing_dependencies() { + $expected_incorrect_usage = 'WP_Styles::add'; + $this->setExpectedIncorrectUsage( $expected_incorrect_usage ); + + wp_enqueue_style( + 'main-style', + '/main-style.css', + array( 'missing-style-dep' ) + ); + + $markup = get_echo( 'wp_print_styles' ); + $this->assertStringNotContainsString( 'main-style.css', $markup, 'Expected style to be absent.' ); + + $this->assertArrayHasKey( + $expected_incorrect_usage, + $this->caught_doing_it_wrong, + "Expected $expected_incorrect_usage to trigger a _doing_it_wrong() notice for missing dependency." + ); + + $this->assertStringContainsString( + 'The style with the handle "main-style" was enqueued with dependencies that are not registered: missing-style-dep', + $this->caught_doing_it_wrong[ $expected_incorrect_usage ], + 'Expected _doing_it_wrong() notice to indicate missing dependencies for enqueued styles.' + ); + } } diff --git a/tests/phpunit/tests/script-modules/wpScriptModules.php b/tests/phpunit/tests/script-modules/wpScriptModules.php index f253dec49fc7d..c80d1e745779c 100644 --- a/tests/phpunit/tests/script-modules/wpScriptModules.php +++ b/tests/phpunit/tests/script-modules/wpScriptModules.php @@ -2059,7 +2059,7 @@ public function test_script_module_printing_and_dependency_ordering( bool $use_g "Snapshot:\n" . var_export( $actual, true ) ); - $deregister( array( 'b', 'c ' ) ); + $deregister( array( 'b', 'c' ) ); // Test that registered dependency in footer doesn't place dependant in footer. $register( 'd', '/d.js', array(), '1.0.0', array( 'in_footer' => true ) ); @@ -2312,4 +2312,32 @@ public function test_static_import_dependency_with_dynamic_imports_depending_on_ "Expected script modules to match snapshot:\n$script_modules" ); } + + /** + * Tests that a missing script module dependency triggers a _doing_it_wrong() notice. + * + * @ticket 64229 + * @covers WP_Script_Modules::sort_item_dependencies + */ + public function test_missing_script_module_dependency_triggers_incorrect_usage() { + $expected_incorrect_usage = 'WP_Script_Modules::register'; + $this->setExpectedIncorrectUsage( $expected_incorrect_usage ); + + $this->script_modules->enqueue( 'main-module', '/main-module.js', array( 'missing-mod-dep' ) ); + + $markup = get_echo( array( $this->script_modules, 'print_enqueued_script_modules' ) ); + $this->assertStringNotContainsString( 'main-module.js', $markup, 'Expected script module to be absent.' ); + + $this->assertArrayHasKey( + $expected_incorrect_usage, + $this->caught_doing_it_wrong, + 'Expected WP_Script_Modules::register to be reported via doing_it_wrong().' + ); + + // Assert the message mentions the missing dependency handle. + $this->assertStringContainsString( + 'The script module with the ID "main-module" was enqueued with dependencies that are not registered: missing-mod-dep', + $this->caught_doing_it_wrong[ $expected_incorrect_usage ] + ); + } }