Skip to content

Commit 9fc7599

Browse files
westonruterclaude
andcommitted
Build/Test Tools: Merge synthetic @var lines with existing globals 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>
1 parent 3c0d4d6 commit 9fc7599

1 file changed

Lines changed: 25 additions & 7 deletions

File tree

tests/phpstan/GlobalDocBlockVisitor.php

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,11 @@
3030
* function docblock does not list, are left untouched and continue to
3131
* resolve as `mixed` — preserving PHPStan's safety guarantees.
3232
*
33+
* Hand-written `@var` annotations on a `global` statement are honored
34+
* per-variable: in `global $a, $b;`, an existing `@var Foo $a` is left
35+
* alone, but `$b` will still receive a synthetic `@var` if the function
36+
* documents it via `@global`.
37+
*
3338
* Registered as `phpstan.parser.richParserNodeVisitor` in `base.neon`.
3439
*/
3540
final class GlobalDocBlockVisitor extends NodeVisitorAbstract {
@@ -77,24 +82,37 @@ public function enterNode( Node $node ): ?Node {
7782
return null;
7883
}
7984

80-
// Respect a hand-written `@var` already on the statement.
81-
$existing = $node->getDocComment();
82-
if ( $existing !== null && str_contains( $existing->getText(), '@var' ) ) {
83-
return null;
85+
// Collect variable names that already have a hand-written `@var` on this
86+
// statement so we can leave them alone but still inject `@var` lines for
87+
// the remaining variables in a multi-variable `global $a, $b;` statement.
88+
$existing = $node->getDocComment();
89+
$existing_text = $existing !== null ? $existing->getText() : '';
90+
$already_typed = array();
91+
if ( $existing_text !== '' && preg_match_all( '/@(?:phpstan-)?var\s+[^\n]*?\$(\w+)/', $existing_text, $existing_matches ) > 0 ) {
92+
$already_typed = array_flip( $existing_matches[1] );
8493
}
8594

8695
$lines = array();
8796
foreach ( $node->vars as $var ) {
8897
if ( ! $var instanceof Node\Expr\Variable || ! is_string( $var->name ) ) {
8998
continue;
9099
}
91-
if ( isset( $map[ $var->name ] ) ) {
92-
$lines[] = sprintf( ' * @var %s $%s', $map[ $var->name ], $var->name );
100+
if ( isset( $already_typed[ $var->name ] ) || ! isset( $map[ $var->name ] ) ) {
101+
continue;
93102
}
103+
$lines[] = sprintf( ' * @var %s $%s', $map[ $var->name ], $var->name );
104+
}
105+
106+
if ( $lines === array() ) {
107+
return null;
94108
}
95109

96-
if ( $lines !== array() ) {
110+
if ( $existing_text === '' ) {
97111
$node->setDocComment( new Doc( "/**\n" . implode( "\n", $lines ) . "\n */" ) );
112+
} else {
113+
// Insert the new `@var` lines just before the closing `*/`.
114+
$merged = preg_replace( '#\s*\*/\s*$#', "\n" . implode( "\n", $lines ) . "\n */", $existing_text, 1 );
115+
$node->setDocComment( new Doc( (string) $merged ) );
98116
}
99117

100118
return null;

0 commit comments

Comments
 (0)