From c71c9335ae1ae42efbd9a4f230cd3982d41666f3 Mon Sep 17 00:00:00 2001 From: Nikolay Strikhar Date: Mon, 24 Aug 2026 15:36:58 +0200 Subject: [PATCH] Name the member every report is made from, not just the class `_doing_it_wrong()`'s first argument came in two shapes: `Class::method` at the facade's two trampolines and at the too-late boot, and a bare class name at the other eight sites. WordPress prints it as "Function %s was called incorrectly", so it is the only part of a report that says where in the library the sentence came from -- and a bare class cannot separate two reports made by the same class, which is exactly what `Absorber::render_notices` and `Absorber::filter_activation_error_markup` are, and what `Loader::load_all`, `Loader::load` and `Loader::announce` are. `Class::method` everywhere. Written as `self::class . '::method'` rather than `__METHOD__`, because both traits here are reported through and `__METHOD__` inside a trait method names the trait rather than the class using it -- a host would be sent to `Traits\Guards_Hook_Prefix` instead of to the facade or the load pass that actually stood down. The too-late boot keeps naming `Absorber::boot`: the mistake there is in a call the host made, and that is the call it has to move. `Traits\Reports_Errors` now states the rule, so the next report site has one shape to copy rather than three to choose from, and `WithIncorrectUsage::assert_the_library_reported_incorrect_usage()` enforces it across the suite -- the shape, not the member name, so a rename still passes. --- src/Boot/Scheduler.php | 2 +- src/Conflict/Resolver.php | 2 +- src/Loader.php | 6 +++--- src/Registry/Reader.php | 2 +- src/Traits/Guards_Hook_Prefix.php | 2 +- src/Traits/Reports_Errors.php | 14 ++++++++++++-- tests/_support/Traits/WithIncorrectUsage.php | 10 ++++++++++ 7 files changed, 29 insertions(+), 9 deletions(-) diff --git a/src/Boot/Scheduler.php b/src/Boot/Scheduler.php index a5b27ef..527f51f 100644 --- a/src/Boot/Scheduler.php +++ b/src/Boot/Scheduler.php @@ -278,7 +278,7 @@ private static function load( ContainerInterface $container ): void { */ private static function report_a_step_that_threw( string $step, string $consequence, Throwable $thrown ): void { self::report_error( - self::class, + self::class . '::report_a_step_that_threw', sprintf( 'The %s threw, so %s: %s', $step, $consequence, $thrown->getMessage() ) ); } diff --git a/src/Conflict/Resolver.php b/src/Conflict/Resolver.php index a20ef2d..947780b 100644 --- a/src/Conflict/Resolver.php +++ b/src/Conflict/Resolver.php @@ -144,7 +144,7 @@ public function resolve_all(): void { // failure a host is likeliest to hear about as "the site is broken", and on a // production site the developer channel says nothing at all. self::report_error( - self::class, + self::class . '::resolve_all', sprintf( 'The conflict for "%s" threw while being resolved, so it was abandoned: %s', $sub_plugin->get_slug(), diff --git a/src/Loader.php b/src/Loader.php index 6cb1266..e136870 100644 --- a/src/Loader.php +++ b/src/Loader.php @@ -108,7 +108,7 @@ public function load_all(): void { $this->load( $sub_plugin ); } catch ( Throwable $thrown ) { self::report_error( - self::class, + self::class . '::load_all', sprintf( 'The sub-plugin "%s" threw while loading, so it was abandoned: %s', $sub_plugin->get_slug(), @@ -168,7 +168,7 @@ private function load( Sub_Plugin $sub_plugin ): void { // what anything watching `skipped` is counting. A host listening to both sees this one // twice, and that is said plainly in the docs rather than solved by picking one. self::report_error( - self::class, + self::class . '::load', sprintf( 'The bundled plugin file for "%s" is missing or unreadable: %s', $sub_plugin->get_slug(), @@ -263,7 +263,7 @@ private function announce( string $hook, array $arguments, Sub_Plugin $sub_plugi do_action_ref_array( $hook, $arguments ); } catch ( Throwable $thrown ) { self::report_error( - self::class, + self::class . '::announce', sprintf( 'A listener on %s threw, and was abandoned: %s', $hook, diff --git a/src/Registry/Reader.php b/src/Registry/Reader.php index 332ddcb..b0fade3 100644 --- a/src/Registry/Reader.php +++ b/src/Registry/Reader.php @@ -170,7 +170,7 @@ protected function flush(): void { // listener is being told which object was thrown away, and the one already in the // registrar is readable from every other path there is. self::report_error( - self::class, + self::class . '::flush', sprintf( '%1$s The registration already held was kept; %2$s was discarded.', $exception->getMessage(), diff --git a/src/Traits/Guards_Hook_Prefix.php b/src/Traits/Guards_Hook_Prefix.php index 62c6382..f4bcf8a 100644 --- a/src/Traits/Guards_Hook_Prefix.php +++ b/src/Traits/Guards_Hook_Prefix.php @@ -40,7 +40,7 @@ private static function has_hook_prefix(): bool { try { Config::get_hook_prefix(); } catch ( Config_Exception $exception ) { - self::report_error( self::class, $exception->getMessage() ); + self::report_error( self::class . '::has_hook_prefix', $exception->getMessage() ); return false; } diff --git a/src/Traits/Reports_Errors.php b/src/Traits/Reports_Errors.php index cd70b27..cf81d3a 100644 --- a/src/Traits/Reports_Errors.php +++ b/src/Traits/Reports_Errors.php @@ -48,9 +48,19 @@ trait Reports_Errors { * `catch` around it would turn every assertion about a report this library makes into a silent * pass. * + * `$function` is always `Class::method`, never a bare class name. WordPress prints it as + * "Function %s was called incorrectly", so it is the only part of a report that says where in this + * library the sentence came from — and a class alone cannot separate two of them, which is exactly + * what the facade needs, where `render_notices()` and `filter_activation_error_markup()` are both + * static trampolines reporting under the same name. Normally that is `self::class` joined to the + * method the report is made from; where the mistake is in a call the host made, it names that call + * instead, as the too-late boot names `Absorber::boot`. `self::class` rather than `__METHOD__`, + * because two traits report through here and `__METHOD__` inside a trait names the trait rather + * than the class using it. + * * @since 1.0.0 * - * @param string $function Member the mistake is reported against, as + * @param string $function Member the mistake is reported against, `Class::method`, as * `_doing_it_wrong()` takes it. * @param string $message What went wrong, in the words the developer needs. * @param Sub_Plugin|null $sub_plugin Sub-plugin the failure belongs to, where it belongs to one. @@ -80,7 +90,7 @@ private static function report_error( string $function, string $message, ?Sub_Pl // out -- from inside `plugins_loaded`, on every request. The developer channel is the one // that cannot be broken by whatever is on the hook. _doing_it_wrong( - self::class, + self::class . '::report_error', sprintf( 'A listener on %s threw, and was abandoned: %s', $hook, diff --git a/tests/_support/Traits/WithIncorrectUsage.php b/tests/_support/Traits/WithIncorrectUsage.php index 3bdaa58..4a9964a 100644 --- a/tests/_support/Traits/WithIncorrectUsage.php +++ b/tests/_support/Traits/WithIncorrectUsage.php @@ -108,6 +108,16 @@ protected function assert_the_library_reported_incorrect_usage(): void { $report, 'The report has to name this library, or the host goes looking in WordPress.' ); + + // The member, not just the class. Which method reported is the only thing separating two + // reports from the same class -- the facade makes two, from separate trampolines -- and + // pinning the shape rather than the name is what keeps this assertion loose enough to + // survive a rename while still refusing a bare class name. + $this->assertStringContainsString( + '::', + $report, + 'The report has to name the member it was made from, not just the class.' + ); } }