Skip to content

Docs: Add PHPStan templates and generics - #13895

Open
swissspidy wants to merge 3 commits into
WordPress:trunkfrom
swissspidy:fix/phpstan-templates
Open

swissspidy wants to merge 3 commits into
WordPress:trunkfrom
swissspidy:fix/phpstan-templates

Conversation

@swissspidy

@swissspidy swissspidy commented Oct 1, 2026 •

Copy link
Copy Markdown
Member

Split out of #13530, as proposed in this comment (subset 8).

  • sanitize_term() and sanitize_category() get a conditional return type: a WP_Term stays a WP_Term, an array becomes array<string, mixed>, and any other object becomes object. They don't use a template, because both functions rewrite field values and add a filter key, so returning the input type unchanged would be unsound (feedback). Following from that, get_term_to_edit() now documents the WP_Term it returns, rather than the int and null it never returns.
  • urlencode_deep(), rawurlencode_deep() and urldecode_deep() get a conditional return type modelled on map_deep()'s.
  • WP_Widget becomes generic in the shape of its instance settings, so a widget can declare its settings with @extends WP_Widget<...>.
  • register_widget(), unregister_widget() and WP_Widget_Factory take class-string<WP_Widget>|WP_Widget. WP_Widget_Factory::register() instantiates the class with no arguments, so it carries an @phpstan-ignore arguments.count explaining that every widget class declares its own constructor. That ignore was left out of Pass the documented values for deprecated arguments #13872, because the error only exists once this change is in.

Changes from #13530

These follow the review analysis:

  • The *_deep() functions no longer claim T in, T out, since they return transformed strings. They use (T is array ? array<key-of<T>, mixed> : (T is object ? T : string)) instead.
  • WP_Widget generics. update()'s $old_instance and form()'s $instance are T|array{}, since both can be empty. $new_instance is array<string, mixed>, since it is the raw input. update() returns array<string, mixed>|false, since it can cancel saving.
  • Template on wp_is_uuid() is left out. It rejected the mixed its callers pass, and the @phpstan-assert-if-true alone is enough; it is in the assertions PR.
  • Templates on wp_http_validate_url() and rest_sanitize_boolean() are left out, since neither template was used.
  • WP_Widget_Factory::$widgets loses the second @phpstan-var array<int, WP_Widget>. It was a merge leftover that contradicted the existing array<non-decimal-int-string, WP_Widget>.
  • Templates on sanitize_term() and sanitize_category() are replaced by the conditional return type described above.

Effect on the analysis

composer run phpstan passes.

  • return.unusedType loses the two get_term_to_edit() entries.
  • property.nonObject loses three WP_Terms_List_Table errors.
  • The WP_Widget entries in argument.type and isset.property are reworded, now that the class is generic.

At level 10, compared against trunk, 44 errors are fixed and 22 are introduced. Most of the 22 are existing WP_Widget errors reworded with the generic class name, or mixed values passed to the newly typed parameters. (These counts were measured before the sanitize_term() change.)

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, checking each subset with composer run phpstan (also at level 10, compared against trunk) and phpcs, and drafting the commit message 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

Adds templates to the functions that return the value they are passed, such as `sanitize_term()` and `sanitize_category()`, and gives `urlencode_deep()`, `rawurlencode_deep()` and `urldecode_deep()` a conditional return type based on `map_deep()`'s.

`WP_Widget` becomes generic in the shape of its instance settings, so that a widget can declare its settings with `@extends WP_Widget<...>`. The old instance and the form instance can be empty, the new instance is the raw input, and `update()` can return `false`. The widget registration functions accept `class-string<WP_Widget>`, and `WP_Widget_Factory::register()` ignores the argument count error that follows, since every widget class declares its own constructor.

`get_term_to_edit()` now documents the `WP_Term` it returns, rather than `int` or `null`, which it never returns.

See #65817.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VieGtSQtUoVfHiGwTmkvtc
Copilot AI balanced review requested due to automatic review settings October 1, 2026 09:02
@github-actions

github-actions Bot commented Oct 1, 2026 •

Copy link
Copy Markdown

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 props-bot label.

Core Committers: Use this line as a base for the props when committing in SVN:

Props swissspidy, marian1.

To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook.

@github-actions

github-actions Bot commented Oct 1, 2026

Copy link
Copy Markdown

Test using WordPress Playground

The 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

  • All changes will be lost when closing a tab with a Playground instance.
  • All changes will be lost when refreshing the page.
  • A fresh instance is created each time the link below is clicked.
  • Every time this pull request is updated, a new ZIP file containing all changes is created. If changes are not reflected in the Playground instance,
    it's possible that the most recent build failed, or has not completed. Check the list of workflow runs to be sure.

For more details about these limitations and more, check out the Limitations page in the WordPress Playground documentation.

Test this pull request with WordPress Playground.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Copilot review overview

🟡 Changes recommended

Array identity types are unsound, generic widget specializations are rejected by bare annotations, and unregister_widget() remains incomplete.

Review effort: Balanced
Findings: 3 Medium severity · 1 Low severity

Open (4)
What changed in this PR

Adds PHPStan templates and generics for sanitization, deep URL encoding, and widget APIs.

Changes:

  • Models container-aware return types for sanitization and encoding.
  • Makes WP_Widget settings generic and narrows widget registration types.
  • Updates PHPStan baselines for improved inference.
File Description
src/​wp-includes/​category.php Types category sanitization.
src/​wp-includes/​class-wp-widget-factory.php Narrows widget registration types.
src/​wp-includes/​class-wp-widget.php Adds generic widget settings.
src/​wp-includes/​formatting.php Types deep URL transformations.
src/​wp-includes/​taxonomy.php Types term sanitization results.
src/​wp-includes/​widgets.php Narrows register_widget().
tests/​phpstan/​baselines/​argument.type.neon Updates generic diagnostic.
tests/​phpstan/​baselines/​isset.property.neon Updates generic diagnostic.
tests/​phpstan/​baselines/​property.nonObject.neon Removes resolved diagnostics.
tests/​phpstan/​baselines/​return.unusedType.neon Removes obsolete return diagnostics.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread src/wp-includes/category.php Outdated
Comment thread src/wp-includes/class-wp-widget.php
Comment thread src/wp-includes/taxonomy.php Outdated
Comment thread src/wp-includes/widgets.php

Copy link
Copy Markdown
Member Author

🤖 Comment by Claude Opus 5.5

Props for this subset, gathered from the full history of functionMap.php in php-stubs/wordpress-stubs. A change counts only where it introduced or changed the specific tag this PR carries, not just any change to the function's map entry. Purely mechanical changes are left out: tag key renames (php-stubs/wordpress-stubs#420), parameter renames (php-stubs/wordpress-stubs#115), coding standards (php-stubs/wordpress-stubs#119) and the duplicate-code refactor (php-stubs/wordpress-stubs#85).

Stubs change Author Entries
php-stubs/wordpress-stubs#37 @johnbillion rawurlencode_deep(), sanitize_category(), sanitize_term(), urldecode_deep(), urlencode_deep()
php-stubs/wordpress-stubs#160 @lipemat WP_Widget, WP_Widget::form(), WP_Widget::update(), WP_Widget::widget()
php-stubs/wordpress-stubs#221 @IanDelMar WP_Widget_Factory::register(), WP_Widget_Factory::unregister()
php-stubs/wordpress-stubs#418 @IanDelMar WP_Widget
php-stubs/wordpress-stubs#419 @IanDelMar register_widget()

Props for this PR (swissspidy for the split, westonruter for the review that shaped it):

swissspidy, westonruter, johnbillion, marian1, mat-lipe

Generated by Claude Code

@IanDelMar

IanDelMar commented Oct 1, 2026 •

Copy link
Copy Markdown
  • sanitize_term() and sanitize_category() return the type they are passed (T in, T out).

This no longer holds when a constant array or object is passed. Both, sanitize_term() and sanitize_category(), may modify the array or object, invalidating the T in, T out assumption. This is exactly what Copilot is pointing out.

See https://phpstan.org/r/5401ad35-d4b7-415e-b296-811713ca0ea5

I strongly recommend against using @template in cases like this. We have started removing @template from php-stubs/wordpress-stubs as well.

swissspidy and others added 2 commits October 5, 2026 14:20
Both sanitize_term() and sanitize_category() rewrite field values and add a filter key, so returning the template type T was unsound for array shapes. Replace the template with a conditional return type that keeps WP_Term, and otherwise returns array<string, mixed> or object.

Also add the class-string<WP_Widget>|WP_Widget parameter type to unregister_widget(), matching register_widget().

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015BuoHZQvJMvPEYp4Sfjgvd

Copy link
Copy Markdown
Member Author

@IanDelMar Thanks, agreed. d2e5e84 removes the template from sanitize_term() and sanitize_category(). They now use a conditional return type: WP_Term stays WP_Term, which keeps get_term_to_edit() precise, an array becomes array<string, mixed>, and any other object becomes object.

The *_deep() functions still use a template. For arrays, it only keeps the keys (array<key-of<T>, mixed>), not the value types. For objects, T stays as is, because map_deep() changes the properties of the same instance. Would you rather drop the template there too, and return plain array/object/string?


Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants