Skip to content

Commit cf0dfd6

Browse files
committed
Infer static variables whose @var tag @phpstan-ignore-var ignores
1 parent 944ac27 commit cf0dfd6

3 files changed

Lines changed: 38 additions & 2 deletions

File tree

‎src/Analyser/Generics/StaticVariableInference.php‎

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -389,8 +389,15 @@ private static function collectRootNames(Expr $expr, array &$names): void
389389
private static function hasVarTag(Node\Stmt\Static_ $stmt): bool
390390
{
391391
$docComment = $stmt->getDocComment();
392+
if ($docComment === null) {
393+
return false;
394+
}
395+
396+
$text = $docComment->getText();
392397

393-
return $docComment !== null && preg_match('~@(?:phpstan-|psalm-)?var\s~', $docComment->getText()) === 1;
398+
// a @var tag ignored by @phpstan-ignore-var does not type the variable, so it is inferred
399+
return preg_match('~@(?:phpstan-|psalm-)?var\s~', $text) === 1
400+
&& preg_match('~@phpstan-ignore-var\b~', $text) !== 1;
394401
}
395402

396403
}

‎tests/PHPStan/Analyser/nsrt/phpstan-ignore-var.php‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,3 +63,14 @@ function doBaz(int $i): void
6363
echo $i;
6464
assertType('int', $i);
6565
}
66+
67+
function doStatic(int $i): void
68+
{
69+
/**
70+
* @var array<int, string> $cache
71+
* @phpstan-ignore-var
72+
*/
73+
static $cache = [];
74+
assertType('array<int, \'x\'>', $cache);
75+
$cache[$i] = 'x';
76+
}

‎turbo-ext/src/StaticVariableInference.cpp‎

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -137,12 +137,30 @@ bool declaresVar(zend_string *text)
137137
return false;
138138
}
139139

140+
/* preg_match('~@phpstan-ignore-var\b~', $text) === 1 */
141+
bool ignoresVar(zend_string *text)
142+
{
143+
const char *s = ZSTR_VAL(text);
144+
size_t n = ZSTR_LEN(text);
145+
const char tag[] = "@phpstan-ignore-var";
146+
const size_t len = sizeof(tag) - 1;
147+
for (size_t i = 0; i + len <= n; i++) {
148+
if (s[i] != '@' || memcmp(s + i, tag, len) != 0) continue;
149+
// \b after the word character 'r': the end of the text or a non-word character
150+
if (i + len == n) return true;
151+
char c = s[i + len];
152+
bool isWord = (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9') || c == '_';
153+
if (!isWord) return true;
154+
}
155+
return false;
156+
}
157+
140158
/* Mirrors the private hasVarTag(); false = pending exception */
141159
[[nodiscard]] bool hasVarTag(zval *stmt, bool &out)
142160
{
143161
zv::Val text = pt_node_doc_comment_text(stmt);
144162
if (UNEXPECTED(text.isUndef())) return false;
145-
out = text.ref().isString() && declaresVar(Z_STR_P(text.raw()));
163+
out = text.ref().isString() && declaresVar(Z_STR_P(text.raw())) && !ignoresVar(Z_STR_P(text.raw()));
146164
return true;
147165
}
148166

0 commit comments

Comments
 (0)