Skip to content

Code Quality: Type _get_list_table() by the class name it is given - #13553

Closed
Soean wants to merge 10 commits into
WordPress:trunkfrom
Soean:code-quality/list-table-generic-return-type
Closed

Soean wants to merge 10 commits into
WordPress:trunkfrom
Soean:code-quality/list-table-generic-return-type

Conversation

@Soean

@Soean Soean commented Sep 16, 2026

Copy link
Copy Markdown
Member

✅ Committed in r63648 (cf0ad1e).


_get_list_table() returns new $class_name( $args ), but the docblock only promised the base class. PHPStan therefore saw WP_List_Table|false at every call site and reported each use of a subclass member as an error, all of which were suppressed in baselines. A @phpstan-template bound to WP_List_Table ties the return type to the class-string that was passed in. The @return tag keeps WP_List_Table|false, so the generated developer documentation is unaffected.

The wp_list_table_class_name filter from [54378] (#18449) can substitute a different class; since it exists so extenders can swap in a replacement list table, the annotation treats the substitute as satisfying T.

Effect on the baselines

Eleven entries covering twelve errors resolve: all 7 method.nonObject and 4 arguments.count. That empties method.nonObject, so as its header directs the file is deleted along with its includes entry in phpstan.neon.dist.

The 3 property.protected entries change message but not substance — same accesses, same lines, totals unchanged at 8 entries and 11 errors. Only the class named in the message moves from WP_List_Table to the concrete subclass. Fixing those accesses is separate work.

Addendum: the remaining level 10 errors

Later commits take the function to zero PHPStan level 10 errors. Everything still reported came from $args, documented only as Optional. Arguments to pass to the class. Accepts 'screen'.

$args now carries the hash notation from WP_List_Table::__construct(). "Accepts 'screen'" understated it: wp-admin/export-personal-data.php and wp-admin/erase-personal-data.php both pass plural and singular, and those reach the base constructor because the privacy request tables do not override it. HashNotationVisitor turns the @type list into an array shape, so the argument is typed at every call site instead of being a bare array. The shape is unsealed, so callers passing keys that only a subclass consumes are unaffected.

screen accepts a WP_Screen, not only a hook name. Typing the shape immediately produced four argument.type errors — wp-admin/includes/ajax-actions.php twice, wp-admin/includes/update.php, and the constructor call inside _get_list_table() itself — all of which pass or forward get_current_screen(). That has always worked: _get_list_table() runs the value through convert_to_screen(), which forwards to WP_Screen::get(), which returns a WP_Screen it is handed unchanged and already documented string|WP_Screen. Only the docblocks in between were narrower than the code.

WP_List_Table::__construct(), _get_list_table(), convert_to_screen() and WP_Screen::get() now agree on string|WP_Screen|null. The null is there because the constructor defaults the argument to null and hands that value straight down, and both functions fall through to the $hook_suffix global when they receive it. No behavior changes.

The wp_list_table_class_name docblock carries the template too, as @phpstan-template T of WP_List_Table and @phpstan-param class-string<T> $class_name. Without them the filtered value is a plain string and return new $class_name( $args ) reports Function _get_list_table() should return (T of WP_List_Table)|false but returns object. Worth noting that this is a level 10 report on a line the branch does not touch, so neither the level 5 CI run nor a diff-scoped check surfaces it.

The baselines are unchanged from what the first commits left them at: the two entries these changes briefly needed were removed again once the docblocks were widened.

Noted for a follow-up ticket

Four call sites carry an inline @var hint on the result — two in wp-admin/includes/update.php, two in wp-admin/includes/ajax-actions.php. Before this PR they did two jobs: naming the subclass and dropping the |false. The template now does the first, leaving them as pure |false suppression.

Each of those four passes a literal class name that is in $core_classes, where false is unreachable — _get_list_table() returns false only for a name that is not in the map, and then it returns false unconditionally rather than an instance. So the honest contract is a conditional @phpstan-return keyed on the known names, which resolves those calls to the bare instance type and lets all four hints be deleted; that checks out against the full suite. It also duplicates the seventeen class names into the docblock with nothing keeping them in sync, and a name dropped from the array but left in the docblock would fail silently in the unsafe direction. That trade deserves its own ticket rather than riding along here.

Trac ticket: https://core.trac.wordpress.org/ticket/65817


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.

_get_list_table() instantiates whichever class `$class_name` names, but the docblock only promised `WP_List_Table|false`. Every caller reaching for a member the subclass adds was therefore an error to PHPStan and got suppressed in a baseline: `WP_Posts_List_Table::inline_edit()` and `WP_Terms_List_Table::inline_edit()`, `WP_Privacy_Requests_Table::process_bulk_action()` and `::embed_scripts()`, `WP_Theme_Install_List_Table::theme_installer_single()`, and the extra parameters that `WP_Terms_List_Table::single_row()`, `WP_Users_List_Table::single_row()`, `WP_Posts_List_Table::display_rows()` and `WP_Post_Comments_List_Table::display()` accept over their parent signatures.

A `@phpstan-template` bound to `WP_List_Table` lets the return type follow the `class-string` that was passed in, which is what the function has always done at runtime. Eleven baseline entries covering twelve errors resolve as a result: all seven `method.nonObject` entries, emptying that baseline so the file is deleted along with its `includes` entry in phpstan.neon.dist, and four `arguments.count` entries. The three `property.protected` entries for `$screen` are the same three accesses as before and are re-baselined only because the message now names the concrete subclass rather than `WP_List_Table`.

The `@return` tag keeps its `WP_List_Table|false` type so the generated developer documentation is unchanged; only its description gains a note about the type being the one requested.
@github-actions

github-actions Bot commented Sep 16, 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 soean, westonruter.

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

@github-actions

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.

Comment on lines +87 to +89
*
* @phpstan-template T of WP_List_Table
* @phpstan-param class-string<T> $class_name

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 fixes the following PHPStan error:

Function _get_list_table() should return (T of WP_List_Table)|false but returns object.

Comment thread src/wp-admin/includes/list-table.php Outdated
Comment on lines +18 to +33
* @param array $args {
* Optional. Arguments to pass to the class.
*
* @type string $plural Plural value used for labels and the objects being listed.
* This affects things such as CSS class-names and nonces used
* in the list table, e.g. 'posts'. Default empty.
* @type string $singular Singular label for an object being listed, e.g. 'post'.
* Default empty
* @type bool $ajax Whether the list table supports Ajax. This includes loading
* and sorting data, for example. If true, the class will call
* the _js_vars() method in the footer to provide variables
* to any scripts handling Ajax events. Default false.
* @type string $screen String containing the hook name used to determine the current
* screen. If left null, the current screen will be automatically set.
* Default 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.

Adding this fixes the following argument.type PHPStan error:

Parameter #1 $args of class WP_List_Table constructor expects array{plural?: string, singular?: string, ajax?: bool, screen?: string, ...}|string, non-empty-array given.

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.

Also a missingType.iterableValue error:

Function _get_list_table() has parameter $args with no value type specified in iterable type array.

@westonruter westonruter Sep 16, 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.

And on:

if ( isset( $args['screen'] ) ) {
$args['screen'] = convert_to_screen( $args['screen'] );

It fixes an argument.type error:

Parameter #1 $hook_name of function convert_to_screen expects string, mixed given.

Comment thread tests/phpstan/baselines/argument.type.neon Outdated

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.

Typing out the $args exposed this bug where WP_List_Table is supposed to take a screen arg which is string|null, but the _get_list_table() and other callers are passing WP_Screen. This seems out of scope here, but it's why the new entries are added to the baseline.

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.

Actually, it wasn't hard to fix this. Addressed in 2fb1b42

westonruter and others added 5 commits September 16, 2026 14:50
Adding the hash notation to $args dropped the padding that lined its type
and name up with $class_name. Core aligns those columns even when the hash
notation param is last.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
WP_List_Table::__construct() defaults its screen argument to null and hands
that value straight to convert_to_screen(), which forwards it to
WP_Screen::get(). Both fall through to the $hook_suffix global when given
null, so null has always been accepted; only the two docblocks said
otherwise, contradicting the hash notation that now documents the argument
as string|WP_Screen|null.

Also rewords the note about instances being returned unchanged, which read
awkwardly.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
"Default empty" on $singular has been missing its period since the hash
notation was written; copying the block into _get_list_table() duplicated
it, so fix both.

"or the current screen instance" reads as though only the screen currently
in scope qualifies. Any WP_Screen does, which is how convert_to_screen()
now words it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
wporg-sync pushed a commit that referenced this pull request Sep 17, 2026
- The return type promised `WP_List_Table|false` whatever class was asked for. A `@phpstan-template` now ties it to the `class-string` passed in.
- The `$args` param was described as accepting only `screen`. It now carries the hash notation from `WP_List_Table::__construct()`.
- That `screen` argument was typed as a string. It is documented as the `string|WP_Screen|null` it has always accepted. This type union is also applied to `convert_to_screen()` and `WP_Screen::get()`.

Nothing changes at runtime. The function is left free of PHPStan errors at every rule level.

Developed in #13553.
Follow-up to r29459, r54378, r63020, r63420.

Props soean, westonruter.
See #65817.


git-svn-id: https://develop.svn.wordpress.org/trunk@63648 602fd350-edb4-49c9-b593-d223f7449a82
wporg-sync pushed a commit to WordPress/WordPress that referenced this pull request Sep 17, 2026
- The return type promised `WP_List_Table|false` whatever class was asked for. A `@phpstan-template` now ties it to the `class-string` passed in.
- The `$args` param was described as accepting only `screen`. It now carries the hash notation from `WP_List_Table::__construct()`.
- That `screen` argument was typed as a string. It is documented as the `string|WP_Screen|null` it has always accepted. This type union is also applied to `convert_to_screen()` and `WP_Screen::get()`.

Nothing changes at runtime. The function is left free of PHPStan errors at every rule level.

Developed in WordPress/wordpress-develop#13553.
Follow-up to r29459, r54378, r63020, r63420.

Props soean, westonruter.
See #65817.

Built from https://develop.svn.wordpress.org/trunk@63648


git-svn-id: http://core.svn.wordpress.org/trunk@62823 1a063a9b-81f0-0310-95a4-ce76da25c4cd
@Soean
Soean deleted the code-quality/list-table-generic-return-type branch September 17, 2026 05:53
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.

2 participants