Make WordPress Core

Changeset 62939


Ignore:
Timestamp:
07/30/2026 04:45:34 AM (less than one hour ago)
Author:
westonruter
Message:

Build/Test Tools: Verify hook docblocks statically.

Add PHPStan extensions that read the docblock documenting each hook where the hook is fired. The value apply_filters() returns is typed from the first @param that docblock documents rather than mixed; core's /** This filter is documented in <file> */ convention is resolved, so a hook documented elsewhere is analyzed against its canonical docblock, including a dynamic canonical name such as "{$type}_template_hierarchy"; and two rules require every hook invocation to be documented, and to pass as many arguments as its documentation describes.

These conventions were previously enforced by review alone. A reference comment could name a file that no longer documents the hook, and a call site could pass fewer arguments than documented, which raises an ArgumentCountError in a callback registered for the documented count, or more, which drops the extra argument and leaves the documentation wrong. Both are now reported where they occur. The hook issues this surfaced in core were fixed in preceding commits.

The generated src/wp-includes/build tree is excluded from analysis, as its sources live in the Gutenberg plugin.

Developed in https://github.com/WordPress/wordpress-develop/pull/12022.
Follow-up to r61699, r62292, r62893, r62894, r62925, r62926, r62927, r62928, r62930, r62931, r62932, r62933, r62934, r62935.

Props westonruter, szepeviktor, khokansardar.
See #64898.
Fixes #65376.

Location:
trunk/tests/phpstan
Files:
6 added
2 edited

Legend:

Unmodified
Added
Removed
  • trunk/tests/phpstan/README.md

    r61699 r62939  
    5050For more information about configuring PHPStan, see the [PHPStan documentation's Config reference](https://phpstan.org/config-reference).
    5151
     52## WordPress-specific extensions
     53
     54This directory also contains extensions that teach PHPStan conventions specific to WordPress. They are registered in [`base.neon`](base.neon), so they apply to the default configuration and to any local override of it.
     55
     56### Global variables in function docblocks
     57
     58Core documents the globals a function uses with `@global Type $varname`. `GlobalDocBlockVisitor` bridges that convention to PHPStan's variable type resolution, so those globals are typed rather than `mixed` inside the function.
     59
     60### Hook documentation
     61
     62The remaining extensions read the docblock documenting a hook where the hook is fired, which is where WordPress documents its hooks. They cover `apply_filters()`, `do_action()` and their `_deprecated` and `_ref_array` variants.
     63
     64- **The value a filter returns is typed from its documentation.** `apply_filters()` returns the type of the first `@param` its docblock documents, rather than `mixed`. This assumes callbacks honor the documented type; one that returns something else is treated as the unusual case.
     65- **Hooks documented elsewhere are resolved.** Core's `/** This filter is documented in <file> */` convention is followed, in its action form as well, so a hook documented in another file is analyzed against its canonical docblock. A dynamic canonical name such as `"{$type}_template_hierarchy"` is matched against the literal name used at the referencing site.
     66- **Two rules check the documentation itself**: that a hook is documented at all, and that it is fired with as many arguments as its documentation describes.
     67
     68Calls whose hook name contains no literal text, such as the `apply_filters_ref_array( $hook_name, $args )` re-dispatch in `plugin.php`, name no concrete hook and are skipped.
     69
     70One consequence worth knowing: because a hook's documentation may live in a different file than the call inheriting it, editing a hook docblock in a file that reference comments point at discards PHPStan's result cache. Every call site inheriting that docblock has to be analyzed again, and PHPStan cannot infer that dependency on its own.
     71
     72### Errors these rules report
     73
     74These identifiers are specific to WordPress, and can be ignored or baselined like any other error, as described [below](#ignoring-and-baselining-errors).
     75
     76| Identifier | What it means |
     77| --- | --- |
     78| `wordpress.hookDocMissing` | The hook is fired with neither a docblock documenting it nor a reference comment. Document it, or point at wherever it is documented. |
     79| `wordpress.hookDocNoParams` | A filter's docblock documents no parameters. A filter always passes at least the value being filtered, so document that value with `@param`, plus one for each further argument. This also fires when an unrelated docblock, such as a `@var` annotation, happens to sit immediately above the call. |
     80| `wordpress.hookDocReferenceFileMissing` | A reference comment names a file that does not exist in the tree being analyzed. The path is resolved relative to the file holding the comment and to the WordPress root; one that resolves outside the tree counts as missing, since the analysis does not read it. |
     81| `wordpress.hookDocReferenceHookMissing` | The referenced file exists, but documents no hook of that name. Either the reference is stale, or the canonical docblock has moved. |
     82| `wordpress.hookParamCountMismatch` | The call passes a different number of arguments than the docblock documents `@param` tags for. Passing fewer risks an `ArgumentCountError` in a callback registered for the documented count; passing more silently drops the extra argument and leaves the documentation misleading. |
     83
    5284## Ignoring and baselining errors
    5385
  • trunk/tests/phpstan/base.neon

    r62657 r62939  
    1212                tags:
    1313                        - phpstan.parser.richParserNodeVisitor
     14
     15        # Attaches the docblock documenting a hook to the hook's call, so that the return
     16        # type extension and the rules below can all read it.
     17        -
     18                class: WordPress\PHPStan\HookDocsVisitor
     19                tags:
     20                        - phpstan.parser.richParserNodeVisitor
     21
     22        # Resolves a hook call's documentation, whether written at the call or inherited
     23        # through the "This filter is documented in <file>" convention.
     24        -
     25                class: WordPress\PHPStan\HookDocBlock
     26
     27        # Types the return value of apply_filters() (and variants) from the `@param` type
     28        # documented for the value being filtered. Adapted from szepeviktor/phpstan-wordpress.
     29        -
     30                class: WordPress\PHPStan\ApplyFiltersDynamicFunctionReturnTypeExtension
     31                tags:
     32                        - phpstan.broker.dynamicFunctionReturnTypeExtension
     33
     34        # Enforces that every hook invocation is preceded by a documenting docblock or
     35        # a valid "This filter is documented in <file>" reference comment.
     36        -
     37                class: WordPress\PHPStan\HookDocumentationRule
     38                tags:
     39                        - phpstan.rules.rule
     40
     41        # Enforces that a hook invocation passes as many arguments as its documentation
     42        # (inline or referenced) describes.
     43        -
     44                class: WordPress\PHPStan\HookParamCountRule
     45                tags:
     46                        - phpstan.rules.rule
     47
     48        # Docblocks inherited from another file, and the sources above that read them,
     49        # are invisible to PHPStan's dependency graph, so both are folded into the result
     50        # cache key. Without this, editing a canonical hook docblock leaves the cached
     51        # results of every call site inheriting it in place.
     52        -
     53                class: WordPress\PHPStan\HookDocsResultCacheMetaExtension
     54                tags:
     55                        - phpstan.resultCacheMetaExtension
    1456
    1557parameters:
     
    100142                - ../../src/xmlrpc.php
    101143                - GlobalDocBlockVisitor.php
     144                - HookDocsVisitor.php
     145                - HookDocBlock.php
     146                - ApplyFiltersDynamicFunctionReturnTypeExtension.php
     147                - HookDocumentationRule.php
     148                - HookParamCountRule.php
     149                - HookDocsResultCacheMetaExtension.php
    102150        bootstrapFiles:
    103151                - bootstrap.php
     
    120168                        # These files are autogenerated by tools/gutenberg/copy.js.
    121169                        - ../../src/wp-includes/blocks
     170                        # Generated output from the Gutenberg plugin's wp-build templates.
     171                        - ../../src/wp-includes/build
    122172                        # Third-party libraries.
    123173                        - ../../src/wp-admin/includes/class-ftp-pure.php
Note: See TracChangeset for help on using the changeset viewer.