Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion phpstan.neon.dist
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,6 @@ includes:
- tests/phpstan/baselines/offsetAccess.nonOffsetAccessible.neon
- tests/phpstan/baselines/offsetAccess.notFound.neon
- tests/phpstan/baselines/offsetAssign.valueType.neon
- tests/phpstan/baselines/parameter.defaultValue.neon
- tests/phpstan/baselines/parameter.notFound.neon
- tests/phpstan/baselines/parameterByRef.type.neon
- tests/phpstan/baselines/parameterByRef.unusedType.neon
Expand Down
19 changes: 13 additions & 6 deletions src/wp-admin/includes/class-custom-background.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,19 @@ class Custom_Background {
* Callback for administration header.
*
* @since 3.0.0
* @var callable
* @var callable|string|null
*
* @phpstan-var WP_Optional_Callback
*/
public $admin_header_callback;

/**
* Callback for header div.
*
* @since 3.0.0
* @var callable
* @var callable|string|null
*
* @phpstan-var WP_Optional_Callback
*/
public $admin_image_div_callback;

Expand All @@ -43,10 +47,13 @@ class Custom_Background {
*
* @since 3.0.0
*
* @param callable $admin_header_callback Optional. Administration header callback.
* 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.

@westonruter westonruter Oct 7, 2026 •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ 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.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 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.

* Empty string or null for none. Default empty string.
* @param callable|string|null $admin_image_div_callback Optional. Custom image div output callback.
* Empty string or null for none. Default empty string.
*
* @phpstan-param WP_Optional_Callback $admin_header_callback
* @phpstan-param WP_Optional_Callback $admin_image_div_callback
*/
public function __construct( $admin_header_callback = '', $admin_image_div_callback = '' ) {
$this->admin_header_callback = $admin_header_callback;
Expand Down
18 changes: 13 additions & 5 deletions src/wp-admin/includes/class-custom-image-header.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,19 @@ class Custom_Image_Header {
* Callback for administration header.
*
* @since 2.1.0
* @var callable
* @var callable|string|null
*
* @phpstan-var WP_Optional_Callback
*/
public $admin_header_callback;

/**
* Callback for header div.
*
* @since 3.0.0
* @var callable
* @var callable|string|null
*
* @phpstan-var WP_Optional_Callback
*/
public $admin_image_div_callback;

Expand All @@ -51,9 +55,13 @@ class Custom_Image_Header {
*
* @since 2.1.0
*
* @param callable $admin_header_callback Administration header callback.
* @param callable $admin_image_div_callback Optional. Custom image div output callback.
* Default empty string.
* @param callable|string|null $admin_header_callback Administration header callback.
* Empty string or null for none.
* @param callable|string|null $admin_image_div_callback Optional. Custom image div output callback.
* Empty string or null for none. Default empty string.
*
* @phpstan-param WP_Optional_Callback $admin_header_callback
* @phpstan-param WP_Optional_Callback $admin_image_div_callback
*/
public function __construct( $admin_header_callback, $admin_image_div_callback = '' ) {
$this->admin_header_callback = $admin_header_callback;
Expand Down
221 changes: 117 additions & 104 deletions src/wp-admin/includes/plugin.php

Large diffs are not rendered by default.

8 changes: 5 additions & 3 deletions src/wp-includes/option.php
Original file line number Diff line number Diff line change
Expand Up @@ -3120,9 +3120,11 @@ function register_setting( $option_group, $option_name, $args = array() ) {
* @global array $new_allowed_options
* @global array $wp_registered_settings
*
* @param string $option_group The settings group name used during registration.
* @param string $option_name The name of the option to unregister.
* @param callable $deprecated Optional. Deprecated.
* @param string $option_group The settings group name used during registration.
* @param string $option_name The name of the option to unregister.
* @param string $deprecated Optional. Deprecated.
*
* @phpstan-param '' $deprecated
*/
function unregister_setting( $option_group, $option_name, $deprecated = '' ) {
global $new_allowed_options, $wp_registered_settings;
Expand Down
6 changes: 6 additions & 0 deletions src/wp-includes/plugin.php
Original file line number Diff line number Diff line change
Expand Up @@ -896,6 +896,8 @@ function plugin_dir_url( $file ) {
*
* @param string $file The filename of the plugin including the path.
* @param callable $callback The function hooked to the 'activate_PLUGIN' action.
*
* @phpstan-param callable(bool): mixed $callback
*/
function register_activation_hook( $file, $callback ) {
$file = plugin_basename( $file );
Expand All @@ -919,6 +921,8 @@ function register_activation_hook( $file, $callback ) {
*
* @param string $file The filename of the plugin including the path.
* @param callable $callback The function hooked to the 'deactivate_PLUGIN' action.
*
* @phpstan-param callable(bool): mixed $callback
*/
function register_deactivation_hook( $file, $callback ) {
$file = plugin_basename( $file );
Expand Down Expand Up @@ -950,6 +954,8 @@ function register_deactivation_hook( $file, $callback ) {
* @param string $file Plugin file.
* @param callable $callback The callback to run when the hook is called. Must be
* a static method or function.
*
* @phpstan-param (callable-string|array{class-string, non-empty-string})&(callable(): mixed) $callback
*/
function register_uninstall_hook( $file, $callback ) {
if ( is_array( $callback ) && is_object( $callback[0] ) ) {
Expand Down
2 changes: 2 additions & 0 deletions src/wp-includes/rewrite.php
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,8 @@ function remove_permastruct( $name ) {
* @param callable $callback Callback to run on feed display.
* @return string Feed action name.
*
* @phpstan-param non-empty-string $feedname
* @phpstan-param callable(bool, non-empty-string): mixed $callback
* @phpstan-return non-falsy-string
*/
function add_feed( $feedname, $callback ) {
Expand Down
6 changes: 6 additions & 0 deletions tests/phpstan/base.neon
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,12 @@ parameters:
}
'''

# 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.
WP_Optional_Callback: '''
''|(callable(): mixed)|null

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 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.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@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?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

'''

# The properties of a block style as WP_Block_Styles_Registry stores them, which is
# more than its register() hash accepts: `name` is required for a style to be
# registered at all, and `label` is filled in from the name when the caller omits
Expand Down
105 changes: 0 additions & 105 deletions tests/phpstan/baselines/parameter.defaultValue.neon

This file was deleted.

Loading