-
Notifications
You must be signed in to change notification settings - Fork 3.4k
Build/Test Tools: Honor @global docblock tags in PHPStan analysis
#11692
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
westonruter
wants to merge
8
commits into
WordPress:trunk
Choose a base branch
from
westonruter:phpstan/honor-global-docblock-tags
base: trunk
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+173
−1
Open
Changes from 3 commits
Commits
Show all changes
8 commits
Select commit
Hold shift + click to select a range
c943c2e
Build/Test Tools: Honor `@global` docblock tags in PHPStan analysis.
westonruter edd9423
Build/Test Tools: Fix PHPStan errors in `GlobalDocBlockVisitor`.
westonruter 3c0d4d6
Add missing newline at EOF
westonruter 9fc7599
Build/Test Tools: Merge synthetic `@var` lines with existing globals …
westonruter b8694b4
Use multiline comment
westonruter fb92e2c
Build/Test Tools: Include `GlobalDocBlockVisitor` in PHPStan analyzed…
westonruter 24a9df9
Add more specific types
westonruter 38f2bf2
Build/Test Tools: Simplify the `@global` type pattern in `GlobalDocBl…
westonruter File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,134 @@ | ||
| <?php | ||
| /** | ||
| * PHPStan parser node visitor that bridges WordPress core's `@global` PHPDoc | ||
| * convention to PHPStan's variable type resolution. | ||
| * | ||
| * @package WordPress | ||
| */ | ||
|
|
||
| declare(strict_types=1); | ||
|
|
||
| namespace WordPress\PHPStan; | ||
|
|
||
| use PhpParser\Comment\Doc; | ||
| use PhpParser\Node; | ||
| use PhpParser\NodeVisitorAbstract; | ||
|
|
||
| /** | ||
| * Reads `@global Type $varname` tags from function and method docblocks and | ||
| * injects equivalent inline `@var` docblocks onto matching `global $foo;` | ||
| * statements inside the function body. | ||
| * | ||
| * PHPStan does not consult bootstrap- or stub-declared variable types when | ||
| * resolving `global $foo;` inside functions. It only honors `@var` | ||
| * annotations placed directly on the `global` statement. WordPress core | ||
| * documents globals with `@global` tags on function docblocks instead. This | ||
| * visitor closes the gap so PHPStan can use the existing core annotations | ||
| * without each `global` statement needing its own redundant `@var`. | ||
| * | ||
| * Functions that do not document a global, or that import a global the | ||
| * function docblock does not list, are left untouched and continue to | ||
| * resolve as `mixed` — preserving PHPStan's safety guarantees. | ||
| * | ||
| * Registered as `phpstan.parser.richParserNodeVisitor` in `base.neon`. | ||
| */ | ||
| final class GlobalDocBlockVisitor extends NodeVisitorAbstract { | ||
|
|
||
| /** | ||
| * Stack of `@global` tag maps, one frame per enclosing function-like node. | ||
| * | ||
| * Each frame maps variable names (without `$`) to their declared type. | ||
| * | ||
| * @var list<array<string, string>> | ||
| */ | ||
| private array $stack = array(); | ||
|
|
||
| /** | ||
| * Resets state at the start of each parser traversal. | ||
| * | ||
| * @param array<int, Node> $nodes Top-level nodes about to be traversed. | ||
| * @return array<int, Node>|null | ||
| */ | ||
| public function beforeTraverse( array $nodes ): ?array { | ||
| $this->stack = array(); | ||
| return null; | ||
| } | ||
|
|
||
| /** | ||
| * Pushes a frame when entering a function/method, and injects synthetic | ||
| * `@var` doc comments on `global` statements that match a documented tag. | ||
| * | ||
| * @param Node $node The node being entered. | ||
| * @return null | ||
| */ | ||
| public function enterNode( Node $node ): ?Node { | ||
| if ( $node instanceof Node\FunctionLike ) { | ||
| $doc = $node->getDocComment(); | ||
| $this->stack[] = $doc !== null ? $this->parse_global_tags( $doc->getText() ) : array(); | ||
| return null; | ||
| } | ||
|
|
||
| if ( ! ( $node instanceof Node\Stmt\Global_ ) || $this->stack === array() ) { | ||
| return null; | ||
| } | ||
|
|
||
| $map = $this->stack[ count( $this->stack ) - 1 ]; | ||
| if ( $map === array() ) { | ||
| return null; | ||
| } | ||
|
|
||
| // Respect a hand-written `@var` already on the statement. | ||
| $existing = $node->getDocComment(); | ||
| if ( $existing !== null && str_contains( $existing->getText(), '@var' ) ) { | ||
| return null; | ||
| } | ||
|
westonruter marked this conversation as resolved.
Outdated
|
||
|
|
||
| $lines = array(); | ||
| foreach ( $node->vars as $var ) { | ||
| if ( ! $var instanceof Node\Expr\Variable || ! is_string( $var->name ) ) { | ||
| continue; | ||
| } | ||
| if ( isset( $map[ $var->name ] ) ) { | ||
| $lines[] = sprintf( ' * @var %s $%s', $map[ $var->name ], $var->name ); | ||
| } | ||
| } | ||
|
|
||
| if ( $lines !== array() ) { | ||
| $node->setDocComment( new Doc( "/**\n" . implode( "\n", $lines ) . "\n */" ) ); | ||
| } | ||
|
|
||
| return null; | ||
| } | ||
|
|
||
| /** | ||
| * Pops the function-like frame on the way back up. | ||
| * | ||
| * @param Node $node The node being left. | ||
| * @return null | ||
| */ | ||
| public function leaveNode( Node $node ): ?Node { | ||
| if ( $node instanceof Node\FunctionLike ) { | ||
| array_pop( $this->stack ); | ||
| } | ||
| return null; | ||
| } | ||
|
|
||
| /** | ||
| * Extracts `@global Type $varname` tags from a docblock. | ||
| * | ||
| * Handles union types (`A|B`) and namespaced/array forms (`A\B`, `A[]`). | ||
| * Whitespace inside the type is collapsed. | ||
| * | ||
| * @param string $docblock Raw docblock text including `/**` markers. | ||
| * @return array<string, string> Map of variable name (no `$`) to type. | ||
| */ | ||
| private function parse_global_tags( string $docblock ): array { | ||
| $map = array(); | ||
| if ( preg_match_all( '/@global\s+(\S+(?:\s*\|\s*\S+)*)\s+\$(\w+)/', $docblock, $matches, PREG_SET_ORDER ) > 0 ) { | ||
| foreach ( $matches as $match ) { | ||
| $map[ $match[2] ] = (string) preg_replace( '/\s+/', '', $match[1] ); | ||
| } | ||
| } | ||
| return $map; | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.