Repository navigation
Docs: Add PHPStan types for optional callback parameters - #13890
swissspidy wants to merge 8 commits into
Conversation
The `add_*_page()` functions, and the `Custom_Background` and `Custom_Image_Header` constructors, default their callback to an empty string, which a plain `callable` does not allow. Their `@phpstan-param` now allows `''` and `null`, the latter because plugins and the Customizer commonly pass it. `unregister_setting()` narrows its deprecated third parameter to `''` in the same way. The activation, deactivation, uninstall and feed callbacks gain the signature core calls them with. This empties the `parameter.defaultValue` baseline, which is removed. See #65817. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VieGtSQtUoVfHiGwTmkvtc
|
The following accounts have interacted with this PR and/or linked issues. I will continue to update these lists as activity occurs. You can also manually ask me to refresh this list by adding the Unlinked AccountsThe following contributors have not linked their GitHub and WordPress.org accounts: @claude. Contributors, please read how to link your accounts to ensure your work is properly credited in WordPress releases. Core Committers: Use this line as a base for the props when committing in SVN: To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook. |
There was a problem hiding this comment.
Copilot review overview
🟡 Changes recommended
Two annotations currently reject callback values accepted and used by Core.
Review effort: Balanced
Findings: 2
Open (2)
What changed in this PR
Adds precise PHPStan callback types and removes the resolved default-value baseline.
Changes:
- Types optional callbacks and hook signatures.
- Narrows
unregister_setting()’s deprecated parameter. - Removes the emptied PHPStan baseline.
| File | Description |
|---|---|
src/wp-admin/includes/class-custom-background.php |
Types callback properties and parameters. |
src/wp-admin/includes/class-custom-image-header.php |
Types callback properties and constructor parameter. |
src/wp-admin/includes/plugin.php |
Types admin-page callbacks. |
src/wp-includes/option.php |
Narrows the deprecated callback parameter. |
src/wp-includes/plugin.php |
Types plugin lifecycle callbacks. |
src/wp-includes/rewrite.php |
Types feed callbacks. |
phpstan.neon.dist |
Removes the obsolete baseline include. |
tests/phpstan/baselines/parameter.defaultValue.neon |
Deletes the resolved baseline. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Test using WordPress PlaygroundThe changes in this pull request can previewed and tested using a WordPress Playground instance. WordPress Playground is an experimental project that creates a full WordPress instance entirely within the browser. Some things to be aware of
For more details about these limitations and more, check out the Limitations page in the WordPress Playground documentation. |
|
🤖 Comment by Claude Opus 5.5 Props for this subset, gathered from the full history of
Props for this PR (swissspidy for the split, westonruter for the review that shaped it): Generated by Claude Code |
Allow null and an empty string for the $admin_header_callback parameter of Custom_Image_Header::__construct(), since the Customizer and the theme-support defaults pass them. Also type the admin page callbacks as callable(): mixed, matching Custom_Image_Header and Custom_Background, since the page hook is fired without arguments. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015BuoHZQvJMvPEYp4Sfjgvd
westonruter
left a comment
There was a problem hiding this comment.
🤖 Comment by Claude Opus 5.5
All of the findings below are PHPDoc-only; none changes runtime behavior, and none is considered blocking. The new callback signatures were traced against how core invokes each callback (activation/deactivation pass a bool, the feed callback passes bool and string, the rest pass nothing), and they all match. The deleted baseline is not referenced anywhere else.
One finding concerns a file outside the diff, so it is noted here rather than inline:
get_theme_support() is the root cause of new mixed errors. The newly typed Custom_Image_Header and Custom_Background constructors now receive mixed from $args[0]['admin-head-callback'] / $args[0]['admin-preview-callback'] in _custom_header_background_just_in_time() (src/wp-includes/theme.php lines 2977 and 2990), and similarly in the Customizer (WP_Customize_Header_Image_Setting). These surface as new level-10 errors. Typing the custom-header/custom-background support args once at the source (e.g. a typed local in _custom_header_background_just_in_time(), or a return shape for those branches of get_theme_support()) would clear them all, rather than leaving every typed consumer to deal with mixed.
| * @param callable $callback The callback to run when the hook is called. Must be | ||
| * a static method or function. | ||
| * | ||
| * @phpstan-param callable(): mixed $callback |
There was a problem hiding this comment.
🤖 Comment by Claude Opus 5.5
callable(): mixed is looser than what this function actually accepts. It admits closures, invokable objects, and [$object, 'method'] arrays, but only a function name or a static [ClassName, 'method'] pair works:
register_uninstall_hook( __FILE__, function () { … } )is accepted by PHPStan, but the Closure is passed toupdate_option( 'uninstall_plugins', … ), where serialization throws "Serialization of 'Closure' is not allowed".register_uninstall_hook( __FILE__, array( $this, 'uninstall' ) )is accepted by PHPStan, but hits_doing_it_wrong()and is silently not registered.
Something along the lines of (callable-string|array{class-string, non-empty-string})&(callable(): mixed) would match the docblock's "Must be a static method or function" and catch both cases statically.
| * @param string $option_name The name of the option to unregister. | ||
| * @param callable $deprecated Optional. Deprecated. | ||
| * | ||
| * @phpstan-param '' $deprecated |
There was a problem hiding this comment.
🤖 Comment by Claude Opus 5.5
Narrowing $deprecated to '' contradicts the function body, which still handles a non-empty value: if ( '' !== $deprecated ) → _deprecated_argument() → remove_filter( "sanitize_option_{$option_name}", $deprecated ). Under this type that whole branch is dead code, and $deprecated is narrowed to never inside it.
PHPStan stays quiet today only because treatPhpDocTypesAsCertain: false is set in tests/phpstan/base.neon. Meanwhile, a third-party caller still passing a sanitize callback (which continues to work at runtime, with a deprecation notice) would now get an argument.type error. Perhaps ''|callable would better reflect the actual contract, with the deprecation conveyed by the description.
| * @since 3.0.0 | ||
| * @var callable | ||
| * | ||
| * @phpstan-var ''|(callable(): mixed)|null |
There was a problem hiding this comment.
🤖 Comment by Claude Opus 5.5
The @phpstan-var / @phpstan-param tags now allow ''|(callable(): mixed)|null, but the plain @var callable / @param callable tags alongside them still say callable only. So the documented type remains wrong for the default '' and for the null that core passes. IDEs and the developer reference read the non-prefixed tags. Updating them to callable|string|null (here and on $admin_image_div_callback and the constructor params) would keep both views consistent.
| * @param callable $admin_image_div_callback Optional. Custom image div output callback. | ||
| * Default empty string. | ||
| * | ||
| * @phpstan-param ''|(callable(): mixed)|null $admin_header_callback |
There was a problem hiding this comment.
🤖 Comment by Claude Opus 5.5
Same mismatch as in Custom_Background: $admin_header_callback now accepts '' and null via @phpstan-param, but the plain @param callable $admin_header_callback still reads "Administration header callback." with no indication that an empty string or null is allowed — even though core passes '' (the custom-header theme-support defaults) and null (WP_Customize_Header_Image_Setting::update()). The @var on the property has the same issue.
| * @param int|float $position Optional. The position in the menu order this item should appear. | ||
| * @return string The resulting page's hook_suffix. | ||
| * | ||
| * @phpstan-param ''|(callable(): mixed)|null $callback |
There was a problem hiding this comment.
🤖 Comment by Claude Opus 5.5
Nit: this same ''|(callable(): mixed)|null union is repeated across 13 add_*_page() docblocks, plus four more tags in the two Custom_* classes. If the accepted shape ever changes (e.g. dropping '' or tightening the return to void), all 17 sites must be edited in lockstep, and missing one silently yields inconsistent typing between add_submenu_page() and its wrappers. A shared @phpstan-type alias could avoid that, though it may not be worth the indirection for docblocks that are otherwise self-contained.
There was a problem hiding this comment.
This could be a global alias added in typeAliases located in base.neon.
Co-authored-by: Weston Ruter <westonruter@gmail.com>
- Add an `Optional_Callback` type alias to `base.neon` and use it for the `add_*_page()` callbacks and the `Custom_Background` and `Custom_Image_Header` callbacks, instead of repeating the union. - Document those `Custom_*` callbacks as `callable|string|null` in the plain `@var` and `@param` tags too, so IDEs and the developer reference see the empty string and `null` that core passes. - Type `unregister_setting()`'s deprecated `$deprecated` parameter as `''|callable`, since the function still handles a callable. - Restrict `register_uninstall_hook()`'s callback to a function name or a static `[ClassName, 'method']` pair, as the docblock requires. Closures cannot be serialized into the `uninstall_plugins` option, and instance methods are rejected. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BL6dYS5ydouQDBQmjem93A
| * Default empty string. | ||
| * @param callable $admin_image_div_callback Optional. Custom image div output callback. | ||
| * Default empty string. | ||
| * @param callable|string|null $admin_header_callback Optional. Administration header callback. |
There was a problem hiding this comment.
✅ I understand that string is needed here to account for the empty string case. Otherwise, callable|null would be sufficient since that includes callable-string.
| * @param string $option_name The name of the option to unregister. | ||
| * @param callable $deprecated Optional. Deprecated. | ||
| * | ||
| * @phpstan-param ''|callable $deprecated |
There was a problem hiding this comment.
Why not just:
| * @phpstan-param ''|callable $deprecated | |
| * @phpstan-param '' $deprecated |
Otherwise, a caller supplying anything but '' won't get flagged by PHPStan.
See also trackback_url():
wordpress-develop/src/wp-includes/comment-template.php
Lines 1255 to 1259 in c64de70
I'm curious why PHPStan doesn't flag if ( '' !== $deprecated ) as being an error if the type is declared as such, when it did for trackback_url().
westonruter
left a comment
There was a problem hiding this comment.
🤖 Comment by Claude Opus 5.5
Second-round review at 8b7e501. No correctness issues were found; the changes remain PHPDoc-only, PHPCS is clean, and PHPStan reports no new errors on changed lines relative to trunk.
Status of the earlier review's findings
- ✅
register_uninstall_hook(): addressed. A probe at level 10 confirms Closures,array( $this, … )and nonexistent methods are now rejected, while'Class::method',array( self::class, … )and function names are accepted. - ✅ Plain tags on
Custom_Background: addressed. - ✅
Custom_Image_Headerallowingnull: addressed, including the description. - ✅ Repeated union across 17 sites: addressed via the
Optional_Callbackalias. - ✅ Feed-name narrowing to
non-empty-string: applied, and it holds.do_feed()only firesdo_feed_{$feed}when that hook exists, so the value passed is the registered$feedname, which can no longer be''. - ⏳
get_theme_support()root cause: still open (see below). - ↩️
unregister_setting(): the earlier suggestion of''|callableis superseded; see the inline comment.
Corrections to the earlier review
- The
''|callablesuggestion forunregister_setting()'s$deprecatedwas the weaker option. Plain''is preferable (inline comment below). - The
theme.phperrors were described as new level-10 errors. They are pre-existingexpects callable, mixed givenerrors, now reworded to mention the narrower type, so they are not introduced by this PR and do not block it.
Still open (outside the diff): get_theme_support() root cause
Level 10 still reports mixed being passed to the Custom_Image_Header constructor at src/wp-includes/theme.php line 2977, to Custom_Background at line 2990, and to Custom_Image_Header at src/wp-includes/customize/class-wp-customize-header-image-setting.php line 58. A typed local for the custom-header / custom-background support args in _custom_header_background_just_in_time() would clear the theme.php ones. This could just as well be a follow-up.
| * @param string $option_name The name of the option to unregister. | ||
| * @param callable $deprecated Optional. Deprecated. | ||
| * | ||
| * @phpstan-param ''|callable $deprecated |
There was a problem hiding this comment.
🤖 Comment by Claude Opus 5.5
The '' suggestion in #13890 (comment) is preferable to ''|callable, and it supersedes the ''|callable suggestion made in the earlier review. With ''|callable, a caller still passing the sanitize callback (deprecated since 4.7.0) is never flagged, whereas r64163 established the convention of narrowing deprecated parameters to their default even where the function body still handles other values (e.g. the_author()).
The Copilot objection does not apply: remove_option_update_handler() forwards a callable, but it lives in src/wp-admin/includes/deprecated.php, which tests/phpstan/base.neon excludes from analysis, so narrowing to '' produces no error in core.
One small correction to the premise of the question: trackback_url()'s true !== $deprecated_echo comparison was not what got flagged there; it was conditionalType.alwaysTrue on its @phpstan-return. The '' !== $deprecated comparison here stays quiet because treatPhpDocTypesAsCertain: false is set.
If narrowed to '', the plain @param callable $deprecated could become @param string $deprecated to match.
| @@ -1494,6 +1496,8 @@ function add_menu_page( $page_title, $menu_title, $capability, $menu_slug, $call | |||
| * @param callable $callback Optional. The function to be called to output the content for this page. | |||
There was a problem hiding this comment.
🤖 Comment by Claude Opus 5.5
The plain @param callable $callback tags here, on add_menu_page(), and on the 11 add_*_page() wrappers were left as callable, whereas the corresponding plain tags in Custom_Background and Custom_Image_Header were widened to callable|string|null. For consistency (IDEs and the developer reference read the plain tags), these could get the same treatment, since the default is '' and Optional_Callback now admits null.
| * Default empty string. | ||
| * @param callable $admin_image_div_callback Optional. Custom image div output callback. | ||
| * Default empty string. | ||
| * @param callable|string|null $admin_header_callback Optional. Administration header callback. |
There was a problem hiding this comment.
🤖 Comment by Claude Opus 5.5
Nit: Custom_Image_Header's constructor descriptions now say "Empty string or null for none.", but these still only say "Default empty string." even though the type permits null. Aligning the wording would make the shared Optional_Callback contract read the same in both sibling classes.
| # An optional callback that is called without arguments, such as an admin page or | ||
| # custom header callback. Core treats an empty string or null as no callback. | ||
| Optional_Callback: ''' | ||
| ''|(callable(): mixed)|null |
There was a problem hiding this comment.
🤖 Comment by Claude Opus 5.5
Optional_Callback rejects false, although the consumers guard with ! empty( $callback ) (e.g. add_menu_page() and add_submenu_page()) or truthiness checks, so false works at runtime as "no callback". For example, add_menu_page( 'Title', 'Menu', 'manage_options', 'slug', false ) is now reported as argument.type, even at the CI level in phpstan.neon.dist. That may well be the desired outcome (steering callers to ''), but if not, false could be added to the alias. Either way, it seems worth a deliberate decision.
| # An optional callback that is called without arguments, such as an admin page or | ||
| # custom header callback. Core treats an empty string or null as no callback. | ||
| Optional_Callback: ''' | ||
| ''|(callable(): mixed)|null |
There was a problem hiding this comment.
@swissspidy May I ask why Optional_Callback is being introduced? This requires every developer who scans core directly or uses a stubs file to also define Optional_Callback or include WordPress's entire base.neon file.
Developers who do not regularly work with WordPress would also need to look up what it means - and first figure out where to find its definition. Is saving ten characters worth that additional complexity?
There was a problem hiding this comment.
This was my suggestion in #13890 (comment)
Given that PHPStan doesn't yet allow top-level @phpstan-type (until phpstan/phpstan#9164) the global typeAliases allow us to start using types in more places where previously we'd have to duplicate them every time.
There was a problem hiding this comment.
How often is it duplicated, and how often is it expected to need an update? How many developers will now have to define it themselves? This essentially shifts the duplication to others. Perhaps this is worth reconsidering.
There was a problem hiding this comment.
Also, why not use the WP_ prefix to avoid collisions with type aliases defined elsewhere? I do not expect many people to use this particular alias, but once this approach is introduced, it may encourage aliases for all sorts of relatively simple unions.
There was a problem hiding this comment.
How often is it duplicated
There are 13 duplications here.
How many developers will now have to define it themselves?
Could wordpress-stubs undo the alias when reading from core, replacing Optional_Callback with whatever is in base.neon? Then there would be no difference for developers.
Otherwise, if they are looking to use the types in their own code, either they duplicate the ''|(callable(): mixed)|null type or they add add their own alias.
Or maybe phpstan-wordpress could include all of these aliases for plugins to re-use.
Also, why not use the
WP_prefix to avoid collisions with type aliases defined elsewhere?
That probably makes sense. I think the prefix was omitted because we weren't using them on the @phpstan-type aliases on classes, but naturally this is redundant because it's already namespaced by the class.
ack '@phpstan-type' src/wp-includes/ src/wp-admin/
src/wp-includes/php-ai-client/src/Tools/DTO/FunctionCall.php
16: * @phpstan-type FunctionCallArrayShape array{id?: string, name?: string, args?: mixed}
src/wp-includes/php-ai-client/src/Tools/DTO/FunctionDeclaration.php
15: * @phpstan-type FunctionDeclarationArrayShape array{
src/wp-includes/php-ai-client/src/Tools/DTO/FunctionResponse.php
16: * @phpstan-type FunctionResponseArrayShape array{id?: string, name?: string, response: mixed}
src/wp-includes/php-ai-client/src/Tools/DTO/WebSearch.php
15: * @phpstan-type WebSearchArrayShape array{allowedDomains?: string[], disallowedDomains?: string[]}
src/wp-includes/php-ai-client/src/Messages/DTO/MessagePart.php
26: * @phpstan-type MessagePartArrayShape array{
src/wp-includes/php-ai-client/src/Messages/DTO/Message.php
19: * @phpstan-type MessageArrayShape array{
src/wp-includes/php-ai-client/src/Builders/MessageBuilder.php
23: * @phpstan-type Input string|MessagePart|MessagePartArrayShape|File|FunctionCall|FunctionResponse|null
src/wp-includes/php-ai-client/src/Builders/PromptBuilder.php
48: * @phpstan-type Prompt string|MessagePart|Message|MessageArrayShape|list<string|MessagePart|MessagePartArrayShape>|list<Message>|null
src/wp-includes/php-ai-client/src/Providers/DTO/ProviderModelsMetadata.php
20: * @phpstan-type ProviderModelsMetadataArrayShape array{
src/wp-includes/php-ai-client/src/Providers/DTO/ProviderMetadata.php
20: * @phpstan-type ProviderMetadataArrayShape array{
src/wp-includes/php-ai-client/src/Providers/Models/DTO/ModelRequirements.php
22: * @phpstan-type ModelRequirementsArrayShape array{
src/wp-includes/php-ai-client/src/Providers/Models/DTO/RequiredOption.php
16: * @phpstan-type RequiredOptionArrayShape array{
src/wp-includes/php-ai-client/src/Providers/Models/DTO/ModelConfig.php
25: * @phpstan-type ModelConfigArrayShape array{
src/wp-includes/php-ai-client/src/Providers/Models/DTO/SupportedOption.php
18: * @phpstan-type SupportedOptionArrayShape array{
src/wp-includes/php-ai-client/src/Providers/Models/DTO/ModelMetadata.php
19: * @phpstan-type ModelMetadataArrayShape array{
src/wp-includes/php-ai-client/src/Providers/OpenAiCompatibleImplementation/AbstractOpenAiCompatibleImageGenerationModel.php
33: * @phpstan-type ImageGenerationParams array{
42: * @phpstan-type ChoiceData array{
46: * @phpstan-type UsageData array{
51: * @phpstan-type ResponseData array{
src/wp-includes/php-ai-client/src/Providers/OpenAiCompatibleImplementation/AbstractOpenAiCompatibleTextGenerationModel.php
35: * @phpstan-type ToolCallData array{
43: * @phpstan-type MessageData array{
49: * @phpstan-type ChoiceData array{
53: * @phpstan-type UsageData array{
58: * @phpstan-type ResponseData array{
src/wp-includes/php-ai-client/src/Providers/Http/DTO/ApiKeyRequestAuthentication.php
13: * @phpstan-type ApiKeyRequestAuthenticationArrayShape array{
src/wp-includes/php-ai-client/src/Providers/Http/DTO/Response.php
17: * @phpstan-type ResponseArrayShape array{
src/wp-includes/php-ai-client/src/Providers/Http/DTO/RequestOptions.php
15: * @phpstan-type RequestOptionsArrayShape array{
src/wp-includes/php-ai-client/src/Providers/Http/DTO/Request.php
21: * @phpstan-type RequestArrayShape array{
src/wp-includes/php-ai-client/src/Operations/DTO/GenerativeAiOperation.php
20: * @phpstan-type GenerativeAiOperationArrayShape array{id: string, state: string, result?: GenerativeAiResultArrayShape}
src/wp-includes/php-ai-client/src/Results/DTO/Candidate.php
20: * @phpstan-type CandidateArrayShape array{message: MessageArrayShape, finishReason: string}
src/wp-includes/php-ai-client/src/Results/DTO/TokenUsage.php
18: * @phpstan-type TokenUsageArrayShape array{
src/wp-includes/php-ai-client/src/Results/DTO/GenerativeAiResult.php
27: * @phpstan-type GenerativeAiResultArrayShape array{
src/wp-includes/php-ai-client/src/Files/DTO/File.php
19: * @phpstan-type FileArrayShape array{
src/wp-includes/class-wp-script-modules.php
16: * @phpstan-type ScriptModule array{
src/wp-includes/class-wp-block-supports.php
17: * @phpstan-type ApplyCallback callable( WP_Block_Type, array<string, mixed> ): array<string, mixed>
18: * @phpstan-type RegisterCallback callable( WP_Block_Type ): void
src/wp-includes/class-wp-theme.php
9: * @phpstan-type Theme_Key 'Name'|'Version'|'Status'|'Title'|'Author'|'Author Name'|'Author URI'|'Description'|'Template'|'Stylesheet'|'Template Files'|'Stylesheet Files'|'Template Dir'|'Stylesheet Dir'|'Screenshot'|'Tags'|'Theme Root'|'Theme Root URI'|'Parent Theme'
src/wp-includes/class-wp-comment.php
41: * @phpstan-type Data_Array array{
src/wp-includes/class-wp-post.php
21: * @phpstan-type Data_Array array{
src/wp-includes/class-wp-hook.php
18: * @phpstan-type Hook_Callback array{
src/wp-includes/class-wp-connector-registry.php
30: * @phpstan-type Connector array{
src/wp-includes/rest-api/endpoints/class-wp-rest-attachments-controller.php
17: * @phpstan-type Image_Sub_Size array{
src/wp-includes/rest-api/class-wp-rest-server.php
23: * @phpstan-type Endpoint_Arg array{
31: * @phpstan-type Route_Handler array{
src/wp-includes/customize/class-wp-customize-header-image-setting.php
19: * @phpstan-type Header_Image_Data array{
src/wp-admin/includes/class-wp-filesystem-ssh2.php
36: * @phpstan-type Options array{
src/wp-admin/includes/class-wp-filesystem-base.php
14: * @phpstan-type FileListing array{
src/wp-admin/includes/class-wp-filesystem-ftpsockets.php
15: * @phpstan-type Options array{
src/wp-admin/includes/class-wp-filesystem-ftpext.php
15: * @phpstan-type Options array{
So we should rename Maybe_Callable to WP_Maybe_Callable.
There was a problem hiding this comment.
Could wordpress-stubs undo the alias when reading from core, replacing Optional_Callback with whatever is in base.neon? Then there would be no difference for developers.
Yes, wordpress-stubs could expand the alias and phpstan-wordpress could include its definition. Both would require additional ongoing maintenance, though, and neither would help developers who use core directly without these packages.
Type aliases declared with @phpstan-type are local anyways. Type aliases declared in the config are global and can collide with other type aliases.
I still do not see why avoiding a few repetitions of simple types justifies shifting that burden to downstream maintainers and developers.
# Conflicts: # src/wp-admin/includes/plugin.php # src/wp-includes/rewrite.php
- Rename the `Optional_Callback` type alias to `WP_Optional_Callback` to avoid collisions with aliases defined elsewhere. - Widen the plain `@param` tags for the `add_*_page()` callbacks to `callable|string|null`, like the `Custom_*` classes. - Note that an empty string or null means no callback in the `Custom_Background` and `Custom_Image_Header` constructor descriptions. - Narrow `unregister_setting()`'s deprecated parameter to `''`, and document it as a string. See #65817. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01T3XYNUvB9sQNDXA14uJQeH

Split out of #13530, as proposed in this comment (subset 3).
add_*_page()functions, and theCustom_BackgroundandCustom_Image_Headerconstructors and properties, default their callback to'', which a plaincallabledoes not allow. Their@phpstan-paramand@phpstan-varnow accept''andnullalongside acallable(): mixed, since the page and header hooks call the callback without arguments.unregister_setting()narrows its deprecated third parameter, documented ascallablewith a default of'', to''. This is deliberate, like the deprecated parameters in Docs: Narrow deprecated parameters to their default values in PHPStan types #13889: passing a callable still works at runtime but triggers_deprecated_argument(), so static analysis now flags it. The only core caller,remove_option_update_handler(), is itself deprecated and excluded from analysis.register_activation_hook(),register_deactivation_hook(),register_uninstall_hook()andadd_feed()document the signature core calls their callback with.This empties
parameter.defaultValue.neon. The baseline file is deleted, and so is its line inphpstan.neon.dist.Changes from #13530
These follow the review analysis:
''|callablebecomes''|callable|null. Plugins commonly passnulltoadd_menu_page()and the related functions, and the Customizer and_custom_header_background_just_in_time()passnullto the two constructors.mixedrather thanvoid, so a callback that returns a value is still accepted.@phpstan-return voidtags on the activation and deactivation hooks are dropped.Changes after review
Custom_Image_Header::__construct()'s$admin_header_callbacknow accepts''andnullas well. Before, only$admin_image_div_callbackdid, although core passes both values for both parameters.add_*_page()callbacks are typed''|(callable(): mixed)|null, matching theCustom_*classes, instead of a barecallable.Effect on the analysis
composer run phpstanpasses, and theparameter.defaultValuebaseline (17 entries) is removed. At level 10, compared against trunk, 4 errors are fixed and 4 are introduced. The 4 introduced errors are the same 4 calls, reworded:mixedis passed to the new constructor type. (These counts were measured before the changes after review.)Trac ticket: https://core.trac.wordpress.org/ticket/65817
Use of AI Tools
AI assistance: Yes
Tool(s): Claude Code
Model(s): Claude Opus 5.5
Used for: Splitting #13530 by tag kind, applying the corrections from the review analysis on #13530 and from review on this PR, checking each subset with
composer run phpstan(also at level 10, compared against trunk) andphpcs, and drafting the commit messages and this description. Reviewed by me.This Pull Request is for code review only. Please keep all other discussion in the Trac ticket. Do not merge this Pull Request. See GitHub Pull Requests for Code Review in the Core Handbook for more details.
🤖 Generated with Claude Code
https://claude.ai/code/session_015BuoHZQvJMvPEYp4Sfjgvd