Skip to content

Preserve existing phpstan-return tags when applying function map - #498

Open
szepeviktor wants to merge 4 commits into
masterfrom
fix/preserve-existing-phpstan-return
Open

szepeviktor wants to merge 4 commits into
masterfrom
fix/preserve-existing-phpstan-return

Conversation

@szepeviktor

@szepeviktor szepeviktor commented Oct 20, 2026 •

Copy link
Copy Markdown
Member

The Visitor appended function-map return types even when a docblock already contained @phpstan-return, producing duplicate tags. Skip the mapped return tag when one is already present, preserving the existing type and all other mapped tags.

Use the existing docblock parser to detect return tags. Add a regression test covering preservation of an existing return tag, retention of mapped parameter tags, and addition of a mapped return type when none exists.

Regenerate wordpress-stubs.php, removing all 15 duplicate mapped return tags while preserving the existing types.

Remove 137 type assertions that depended on the skipped map return types. Keep passing assertions in mixed fixtures and delete four fixtures left without assertions.

Validation: the full PHPUnit suite passed (1281 tests, 1463 assertions); PHPStan, PHPCS for changed PHP files, the generated stub syntax check, and git diff --check passed. A scan of the regenerated stub found zero docblocks with duplicate @phpstan-return tags.

@szepeviktor

Copy link
Copy Markdown
Member Author

@westonruter Please chime in!

danog added a commit to danog/wordpress-stubs that referenced this pull request Oct 20, 2026
Map parameters replace the ones generated from the hash notation, and map
parameters and templates do not duplicate the ones written in core, like
map return types since php-stubs#498.

PHPStan doesn't support shapes of generic arrays (array<K, V>{...}), so
they become unsealed shapes (array{..., ...<K, V>}), and the string type
of args parameters is only removed from the top-level union. Lists of
lists of shapes are nested generic arrays.

Claude-Session: https://claude.ai/code/session_01WNSb29tptiTQoc44Tk7XZW
@westonruter

Copy link
Copy Markdown

Our types are better.

Why would this be? Wouldn't types in core be better at least in that they will be more up to date? And we're working on migrating the types from this repo over to core. Once that's done, I should think core should be the source of truth.

@szepeviktor

Copy link
Copy Markdown
Member Author

Here is what RoboViktor says.


  1. WP_Http::request()

    @phpstan-return array{
        headers: \WpOrg\Requests\Utility\CaseInsensitiveDictionary,
        body: string,
        response: array{code: int, message: string},
        cookies: array<int, \WP_Http_Cookie>,
        filename: string|null,
        http_response: \WP_HTTP_Requests_Response
    }|\WP_Error
  2. WP_Query::query()

    @phpstan-return (
        $query is array
            ? ($query is array{fields: 'id=>parent'|'ids', ...} ? array<int, int> : array<int, \WP_Post>)
            : array<int, int>|array<int, \WP_Post>
    )
  3. stripslashes_from_strings_only()

    Update the associated template and parameter tags together with the return tag:

    @phpstan-template T
    @phpstan-param T|string $value
    @phpstan-return ($value is string ? ($value is '' ? '' : string) : T)
  4. wp_get_upload_dir()

  5. wp_upload_dir()

    Both use:

    @phpstan-return array{
        path: string,
        url: string,
        subdir: string,
        basedir: string,
        baseurl: string,
        error: false|string
    }
  6. wp_die()

    @phpstan-return ($args is array{exit: false, ...} ? void : never)
  7. check_admin_referer()

    @phpstan-return ($action is -1 ? 1|2|false : 1|2)
  8. check_ajax_referer()

    @phpstan-return ($stop is true ? 1|2 : 1|2|false)
  9. get_post_types()

    @phpstan-return ($output is 'names' ? array<string, string> : array<string, \WP_Post_Type>)
  10. get_posts()

    @phpstan-return ($args is array{fields: 'id=>parent'|'ids', ...} ? array<int, int> : array<int, \WP_Post>)
  11. sanitize_post_field()

    Include the template and parameter tags required by the return type:

    @phpstan-template T of mixed
    @phpstan-param T $value
    @phpstan-return (
        $field is 'ID'|'post_parent'|'menu_order'
            ? (T is int ? T : int)
            : ($field is 'ancestors'
                ? (T is array<int<0, max>>|list<int<0, max>>
                    ? T
                    : (T is list ? list<int<0, max>> : array<int<0, max>>))
                : ($context is 'raw'
                    ? T
                    : ($context is 'attribute'|'js'
                        ? string
                        : ($context is 'edit'
                            ? ($field is not 'post_content' ? string : mixed)
                            : mixed))))
    )
  12. wp_insert_post()

  13. wp_update_post()

  14. wp_insert_attachment()

    All three use:

    @phpstan-return ($wp_error is false ? int<0, max> : int<1, max>|\WP_Error)
  15. term_exists()

    This is our current map definition:

    @phpstan-return (
        $term is 0
            ? 0
            : ($term is ''
                ? null
                : ($taxonomy is ''
                    ? string|null
                    : array{term_id: string, term_taxonomy_id: string}|null))
    )

    This entry deserves reconciliation before copying: Core's existing annotation explicitly handles a null input and uses the more precise numeric-string for the array's ID fields. Those details should be considered alongside our string scalar return type and empty-string handling.

@IanDelMar

Copy link
Copy Markdown
Contributor

The Visitor appended function-map return types even when a docblock already contained @phpstan-return, producing duplicate tags. Skip the mapped return tag when one is already present, preserving the existing type and all other mapped tags.

Which removes the ability to "correct" types.

@szepeviktor

Copy link
Copy Markdown
Member Author

Which removes the ability to "correct" types.

I do not dare to go against Weston.

@westonruter

Copy link
Copy Markdown

We haven't finished porting the PHPStan types from stubs to core, so it's not really right to compare yet. But let's say we do port everything over: after that, I should think core's types should be considered canonical. Now, maybe there are some types in core which are needing to be written in a way internally that isn't appropriate for themes and plugins. In that case, perhaps wordpress-stubs should have a mechanism to override types on an as-needed basis. But hopefully this wouldn't be needed extensively.

@westonruter

Copy link
Copy Markdown

Can this move forward?

Note that this question is related to bringing PHPStan analysis into Gutenberg:

I want to use stubs generated from trunk for Gutenberg's analysis (since it often makes use of pre-release APIs), so this is a dependency for that.

@westonruter

Copy link
Copy Markdown

Or should this be waiting for the rest of the stubs to be merged into core?

@westonruter

westonruter commented Oct 26, 2026 •

Copy link
Copy Markdown

Note that I've incorporated the logic here into the above PR which adds PHPStan analysis for Gutenberg. It now bundles it's own wordpress-stubs which is generated from core trunk. Ideally this would not be needed, and the php-stubs/wordpress-stubs package could have a nightly pre-release that is regenerated from trunk. For the immediate term, however, this allows for Gutenberg's analysis to run against core trunk.

@westonruter

Copy link
Copy Markdown

Remaining stubs PRs not yet merged:

These were split out from WordPress/wordpress-develop#13530.

I believe once these are all merged, then WordPress core will have a superset of the annotations that exist in wordpress-stubs.

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