Build/Test Tools: Honor @global docblock tags in PHPStan analysis#11692
Build/Test Tools: Honor @global docblock tags in PHPStan analysis#11692westonruter wants to merge 7 commits intoWordPress:trunkfrom
@global docblock tags in PHPStan analysis#11692Conversation
WordPress core documents the globals imported into a function with `@global Type $varname` tags on the function's docblock, then writes `global $varname;` inside the body. PHPStan does not natively consult those tags when resolving the type of a variable imported via `global`; it only honors `@var` annotations placed directly on the `global` statement. As a result, every `global $wpdb;` site resolved as `mixed`, and any subsequent `$wpdb->prepare(...)` or `$wpdb->site` produced spurious "method/property on mixed" errors throughout core. Bootstrap-file and stub-file declarations of global variable types (the form used by `php-stubs/wordpress-stubs`) were verified not to address this — PHPStan's `global $foo;` resolution path does not consult them. The only mechanism that works is an `@var` directly on the `global` statement. This change adds a custom PHPStan parser node visitor, `WordPress\PHPStan\GlobalDocBlockVisitor`, that walks each function and method, parses the `@global` tags from its docblock, and injects an equivalent synthetic `/** @var Type $name */` doc comment onto every matching `global $name;` statement in the body. PHPStan's existing `@var`-on-global handling then assigns the documented type. Functions without `@global` tags, and globals not listed in the function's docblock, are left untouched and continue to resolve as `mixed` — preserving PHPStan's safety guarantees. Hand-written `@var` annotations already on a `global` statement are preserved as well. The visitor is registered as a `phpstan.parser.richParserNodeVisitor` service in `tests/phpstan/base.neon`. It is autoloaded via a new `autoload-dev` PSR-4 entry in `composer.json` mapping `WordPress\PHPStan\` to `tests/phpstan/`; the `composer install` step (which runs `composer dump-autoload`) makes the class available to PHPStan automatically. Effect on `composer phpstan` against the existing baseline: total error count drops from 40,081 to 36,310 — about 3,800 fewer false positives — without modifying any production code. The remaining `\$wpdb`-related errors are either in top-level scripts (where `global` is not used and PHPStan does not bridge bootstrap declarations) or are real type issues with `wpdb`'s own dynamic properties. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Specify the iterable value type on `beforeTraverse()`'s return, and cast `preg_replace()`'s `string|null` result to `string` (the regex is hard-coded and valid, so null is unreachable in practice). Co-Authored-By: Claude Opus 4.7 (1M context) <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. |
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. |
There was a problem hiding this comment.
Pull request overview
This PR enhances the repository’s PHPStan tooling by teaching PHPStan to interpret WordPress core’s @global Type $varname docblock convention as variable type information for matching global $varname; statements, reducing false-positive mixed-related errors during static analysis.
Changes:
- Adds
WordPress\PHPStan\GlobalDocBlockVisitor, a PHPStan rich parser node visitor that injects synthetic inline@vardocblocks ontoglobalstatements based on enclosing@globaltags. - Registers the visitor in
tests/phpstan/base.neonso it runs during PHPStan parsing. - Adds a Composer
autoload-devPSR-4 mapping so PHPStan can autoload the visitor class during analysis.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
| tests/phpstan/bootstrap.php | Clarifies PHPStan bootstrap purpose/documentation for discovery. |
| tests/phpstan/base.neon | Registers the new rich parser node visitor service for PHPStan runs. |
| tests/phpstan/GlobalDocBlockVisitor.php | Implements AST visitor that maps @global doc tags to inline @var on global statements. |
| composer.json | Adds autoload-dev PSR-4 mapping to autoload the visitor from tests/phpstan/. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
cc @szepeviktor |
…docblock. The previous implementation skipped a `global` statement entirely if any `@var` was already present on its docblock. This dropped synthetic annotations for the other variables in multi-variable forms like `global $a, $b, $c;` where only `$a` had a hand-written `@var`. The visitor now extracts the variable names already covered by `@var` (or `@phpstan-var`) from the existing docblock, injects synthetic `@var` lines only for the remaining globals, and merges them into the existing docblock just before the closing `*/` — preserving prose, hand-written types, and any other tags. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… paths. Without this, PHPStan emits a "Result cache might not behave correctly" warning because the visitor class is registered as a service but lives outside the analysed paths, so edits to the file do not invalidate the result cache. Adds the file specifically (rather than the whole `tests/phpstan/` directory) to avoid analyzing `bootstrap.php` and `baseline.php`, which are not intended as analysis subjects. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
With 9fc7599, the error count is now brought down by another three to 36,297 (from 36,300). |
|
@westonruter Please do not kill my package. |
apermo
left a comment
There was a problem hiding this comment.
Looking good to me so far, great work.
tl;dr: Adds a PHPStan parser-node visitor that bridges WordPress core's
@global Type $varnamePHPDoc convention to PHPStan's variable type resolution, eliminating 36,297 false-positivemixederrors without touching any production code.I use a local PHPStan configuration at level 10. This causes PHPStan to complain a lot, including about the use of globals like
$wpdb. For code like this:wordpress-develop/src/wp-includes/class-wp-network-query.php
Line 387 in 75b4131
I get two errors from PHPStan:
This is in spite of the fact that the method has:
wordpress-develop/src/wp-includes/class-wp-network-query.php
Line 322 in 75b4131
and
wordpress-develop/src/wp-includes/class-wp-network-query.php
Line 327 in 75b4131
The issue is that PHPStan doesn't recognize this use of
@global. It's a WordPress-specific thing. It does recognize this, however:While we could fix the issues by going throughout core and adding these
@vartags, this would be extremely noisy and be of very little value. Instead, we can introduce a PHPStan extension for core which causes PHPStan to treat the@globalannotations as aliases for inline@varannotations. This is what is implemented by this PR.Approach by Claude
Adds a custom parser node visitor,
WordPress\PHPStan\GlobalDocBlockVisitor, that:FunctionLikenode and parses any@global Type $nametags from its docblock.global $name;statement inside that function body, if$namematches a documented tag, attaches a synthetic/** @var Type $name */doc comment to theglobalAST node.@var-on-global handling then assigns the documented type.Behavior:
@globaltags are untouched; their globals continue to resolve asmixed.@globalblock are untouched; they continue to resolve asmixed.@varannotations already present on aglobalstatement are respected and preserved.@globalmap (visitor uses a stack).The visitor is registered as a
phpstan.parser.richParserNodeVisitorservice intests/phpstan/base.neonand autoloaded via a newautoload-devPSR-4 entry incomposer.jsonmappingWordPress\PHPStan\totests/phpstan/.composer install(which runscomposer dump-autoload) makes the class available to PHPStan automatically.Result
When running
composer -- phpstan --configuration=phpstan.neon.dist --level=10:[ERROR] Found 40069 errors[ERROR] Found 36300 errorsClaude comparison
Comparison summary
The fixed-vs-introduced asymmetry exists because the visitor narrows
$wpdbetc. frommixedtowpdb, which both removes errors (themixed.method()family) and exposes new ones (real type mismatches inwpdbmethod signatures,@var arrayinjections that PHPStan wantsarray<...>for, etc.).Top fixed identifiers
Top fixed messages
The 4456 fixed errors all stem from globals (mostly
$wpdb,$wp_query,$wp_locale,$wp_filter, etc.) becoming concretely typed where@globaltags exist. Nothing was suppressed via baseline; these are real PHPStan errors that the visitor now resolves.Trac ticket: https://core.trac.wordpress.org/ticket/64898
Use of AI Tools
AI assistance: Yes
Tool(s): Claude Code
Model(s): Opus 4.7
Used for: Research and code writing
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.