Repository navigation
Build/Test Tools: Derive element shapes for T[] hashes and read a Required. hash marker - #14457
westonruter wants to merge 12 commits into
Conversation
Core commonly declares a list of arrays as `array[]` and documents each element with a single `...$0` entry, as `parse_blocks()` does. The visitor only put shapes on a bare `array`, `object`, or class, so these were left as `array<array>`, and every key read from an element, such as a block's `attrs`, was `mixed`. A `T[]` member now takes the hash when it is a single `...$0` entry typed as that `T`, yielding the same `array<array-key, ...>` a bare `array` would. A bare `array` entry is also accepted for an `array[]` element, which is how the `array[][]` of `wp_print_font_faces()` is written. Named keys beside a `T[]` still describe the array itself, so those hashes are left alone. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…rning one block The return was documented as `array[]` with a `...$0` hash, copied from `parse_blocks()`, but the method returns a single block rather than a list of them. Now that HashNotationVisitor derives element shapes for a `T[]` hash, that described every key of the returned block as another block. Document the block's keys directly instead. Regenerate the `return.type` baseline, from which the two entries for the old `array<array>|null` return type are dropped. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Both were documented as a bare `array`, so every block read from them was `mixed`, and the REST controllers that pass the result on to `serialize_blocks()` failed its block parameter. Document them with the same block hash as `parse_blocks()`, which HashNotationVisitor now derives element shapes from. At level 10 this removes 12 errors and introduces 5. The new ones arise because the keys of a `@param` hash are optional, so the blocks the function returns can no longer be shown to have every key. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Keys of a `@param` hash are optional by default, since the hash lists the keys core reads rather than all a caller must pass. That is wrong for a value whose keys are always set, such as a parsed block, and there was no way for the hash to say so. A `Required.` sentence opening or closing an entry's description now makes its key required, mirroring the `Optional.` marker. Core already writes it both ways, as the first sentence in the abilities and pattern registries, and as the last in the resource hints and the customizer's nav menu items. Only the whole sentence counts, so prose such as "Capability required for the panel" or "Required for multi-widgets" is not read as a marker. `Optional.` still takes precedence. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A parsed block always has all five keys, but as `@param` hash keys they were optional, so the function could not show that the blocks it reads or returns still have them. Mark each with the new `Required.` sentence. This removes the three `offsetAccess.notFound` errors on the keys read from each block. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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. |
`WP_Widget_Categories::widget()` always receives `before_widget`, `after_widget`, `before_title`, and `after_title`, but as `@param` hash keys they were optional, so each read of them was reported as a possibly missing offset. Mark them with the new `Required.` sentence. This removes the four `offsetAccess.notFound` errors that were deferred as a known limitation when the widget's arguments were first documented as hashes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
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 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. |
A bare `array ...$0` entry was accepted for any `array[]…[]` element, whatever its own hash held. Named keys there typed an array of arrays as a single shape, contradicting the declared type, and an entry without a hash dropped a level of it, widening the type. Both broke the visitor's promise to only ever narrow. The entry's own hash must now describe the elements of that `array[]` in turn, checked recursively, so a mismatched hash leaves the declared type as written. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A `T[]` member whose hash describes its elements was counted as a bare member, so a union that also has a bare `array`, as in `array|array[]`, had two of them and no shape was derived at all. On its own that type collapses back to `array[]`, but inside a larger hash the failure propagates and leaves the whole enclosing hash untranslated, which trunk did translate. `T[]` members are now kept apart and only chosen when there is no bare `array` or `object`, so such unions are shaped as they were before. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The `Optional.` marker has always been matched case-insensitively, but the new `Required.` marker was not, so a lowercase `required.` sentence left its key optional. Only the whole sentence still counts, so prose such as "Something is required." is not read as a marker. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The example showed the shape of the blocks `parse_blocks()` returns as open, with a trailing `...`, but a `@return` hash derives a sealed shape listing every key. Show the shape that is actually derived, and describe the depth check for `array[][]` and the preference for a bare `array` in a union beside a `T[]`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
🤖 Comment by Claude Opus 5.5 Code reviewA high-effort code review of this branch at 80c3b18 reported 10 findings. Four were fixed, three needed no change because they are limitations already described in the PR description, and three are left for follow-ups. Fixed
Each fix was checked with No change neededThese are the limitations already described under "Impact" in the PR description:
Worth considering in a follow-up
The full-core PHPStan totals in the PR description were measured before these fixes. Item 1 narrows where the visitor applies, so they may shift slightly. |
The `@return` hash of `get_settings_errors()` described `type` as "Optional.", copied from the `$type` parameter of `add_settings_error()`. But that function always sets `type`, defaulting it to 'error', and `settings_errors()` reads it without checking. Now that the hash is translated into a sealed shape, the marker made that read a possibly missing offset. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This improves how
HashNotationVisitortranslates hash notation into PHPStan shapes, in two ways.1. Element shapes for
T[]hashesCore commonly declares a list of arrays as
array[]and documents each element with a single...$0entry.parse_blocks()is the canonical example:* @return array[] { * Array of block structures. * * @type array ...$0 { * @type string|null $blockName Name of block. * @type array $attrs Attributes from block comment delimiters. * … * } * }The visitor only derived shapes for a bare
array,object, or class, soarray[]was treated as already more specific than the hash and left asarray<array>. Every key read from an element, such as a block'sattrs, was thereforemixed. That is what caused theCannot access offset 'metadata' on mixederrors inwp_install_defaults()on #13076 (Core-65890).A
T[]member now takes the hash when it is a single...$0entry typed as thatT, producing the samearray<array-key, …>a barearraywould.parse_blocks()now returnsarray<array{blockName: string|null, attrs: array, innerBlocks: array<array>, innerHTML: string, innerContent: array}>. A barearrayentry is also accepted for anarray[]element, which is how thearray[][]ofwp_print_font_faces()is written, but only when its own hash goes on to describe the inner arrays the same way, so a mismatched hash cannot contradict or widen the declared type. A barearraybeside aT[]in a union, as inarray|array[], keeps taking the shape, as it did before. Named keys beside aT[]still describe the array itself rather than its elements, as the 250+string[]hashes keyed by name do, so those are left alone.This change exposed two docblock bugs, which are corrected:
WP_Block_Processor::extract_full_block_and_advance()was documented as returningarray[](copied fromparse_blocks()), but it returns a single block.@returnhash ofget_settings_errors()described each error'stypeasOptional.(copied from the$typeparameter ofadd_settings_error()), butadd_settings_error()always sets it, andsettings_errors()reads it unguarded.resolve_pattern_blocks()is also documented with the block hash, as both the REST block patterns and templates controllers pass its result toserialize_blocks().2. A
Required.marker for hash keysKeys of a
@paramhash are optional by default, since a hash lists the keys core reads rather than all a caller must pass. That is wrong for a value whose keys are always set, such as a parsed block, and there was no way to say so. This has come up repeatedly.A
Required.sentence opening or closing an entry's description now makes a@paramkey required, mirroringOptional.. Core already writes it both ways: as the first sentence in 29 entries across 13 files (abilities, block patterns, pattern categories, connectors, icons,wp_initialize_site(), the filesystem classes, and others), and as the last sentence in 5 (resource hints and preloads, prefetch, and the customizer's nav menu auto-drafts). LikeOptional., it is matched case-insensitively, but only the whole sentence counts, so prose such as "Capability required for the panel" or "Required for multi-widgets, …" is not read as a marker.Optional.still takes precedence. This is documented intests/phpstan/README.md.3. Follow-up to the
WP_Widget_Categoriestyping in r64413When the Categories widget's arguments were documented as hashes in #8071 (committed in r64413, Core-62762), the lack of a way to mark a
@paramkey as required was raised forWP_Widget_Categories::widget(), whose display arguments are always passed. The PHPStan impact comment on that PR then deferred the resulting errors as a known limitation, to be "addressed in the tooling separately than worked around here."With the
Required.marker,before_widget,after_widget,before_title, andafter_titleare now marked as required. This removes the four deferredoffsetAccess.notFounderrors, takingclass-wp-widget-categories.phpfrom 11 errors to 7 at level 10, with none introduced.The other three errors deferred in that comment remain, as they come from a different limitation: a
@paramshape is always open. The filtered arguments passed towp_dropdown_categories()andwp_list_categories()cannot rule out an extra key with an unacceptable value, andupdate(), which starts from the open$old_instance, cannot satisfy its sealed@returnshape. Closing a@paramshape would need a convention of its own.Impact
Level-10 PHPStan totals, cold result cache, before and after. These were measured at c1535e9; the
get_settings_errors()correction in 099446c removes one more error:trunkThe introduced errors are not regressions in the changed code; they are existing issues that the sharper types make visible:
has_meta()(from$wpdb->get_results()),get_settings_errors()(a global merged from a transient),get_allowed_block_template_part_areas()andget_default_block_template_types()(filtered values whose hook@paramkeys are optional), andparse_blocks()itself (the parser class comes from theblock_parser_classfilter).arrayormixedwhere a shape is now expected:serialize_blocks()inWP_Classic_To_Block_Menu_Converter,wp_print_font_faces()from style variations,render_block()after a block is nulled out, the pattern and category registries inblock-patterns.php, andWP_Customize_Nav_Menus::insert_auto_draft_post().resolve_pattern_blocks(): an open@paramshape cannot flow into a sealed@returnshape,$innerBlockscannot be typed recursively, and...$0lists arearray-keyrather thanlistkeyed.WP_Block_Processor::extract_full_block_and_advance()buildsinnerHTMLfromget_html_content(), which may returnnull, and appends its own nullable result toinnerBlocks.Separately,
wp_initialize_site()documents$user_idasRequired., while$argsdefaults toarray()anduser_idfalls back to0viawp_parse_args(), so the docblock and the code disagree. The resulting default-value error is baselined rather than counted above.The baselines are regenerated: the two entries for the old
extract_full_block_and_advance()return type are dropped, thefilter_block_kses()entry is reworded, and thewp_initialize_site()default value is added.Possible follow-ups
Required.on filter hook hashes. Many of the remainingoffsetAccess.notFounderrors on optional keys come from a filter's hook docblock documenting the filtered value as a@paramhash, after which core reads the keys unguarded. The email filters are the largest cluster: for example,auto_core_update_emailis read aswp_mail( $email['to'], …, $email['subject'], $email['body'], $email['headers'] ), and marking its four keysRequired.removes all four errors there with none introduced. Similar clusters appear inpluggable.php,class-wp-recovery-mode-email-service.php,user.php,ms-functions.php, andfunctions.php(includingupload_dir).@paramshape. Several remaining errors, inresolve_pattern_blocks(),WP_Widget_Categories::update(), and thedefault_template_typesanddefault_wp_template_part_areasfilters, come from an open@param(or hook) shape flowing into a sealed@returnshape. Marking those keysRequired.only rewords the error.@phpstan-typealias for parsed blocks, in place of the copies of the block hash, which would also typeinnerBlocks.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: Implementing the visitor changes and docblock fixes, surveying core's hash notation usage, running and triaging the PHPStan comparisons, and drafting this description; the approach (including the
Required.convention) was directed by me, and the changes were 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