Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion src/Analyser/Generics/StaticVariableInference.php
Original file line number Diff line number Diff line change
Expand Up @@ -389,8 +389,15 @@ private static function collectRootNames(Expr $expr, array &$names): void
private static function hasVarTag(Node\Stmt\Static_ $stmt): bool
{
$docComment = $stmt->getDocComment();
if ($docComment === null) {
return false;
}

$text = $docComment->getText();

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

}
23 changes: 19 additions & 4 deletions src/Analyser/StatementsHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -1920,6 +1920,7 @@ public function processStmtVarAnnotation(NodeScopeResolver $nodeScopeResolver, M
{
$function = $scope->getFunction();
$variableLessTags = [];
$isVariableLessTagIgnored = false;

foreach ($stmt->getComments() as $comment) {
if (!$comment instanceof Doc) {
Expand All @@ -1944,9 +1945,17 @@ public function processStmtVarAnnotation(NodeScopeResolver $nodeScopeResolver, M
$assignedVariable = $stmt->expr->var->name;
}

$areVarTagsIgnored = $resolvedPhpDoc->areVarTagsIgnored();
foreach ($resolvedPhpDoc->getVarTags() as $name => $varTag) {
if (is_int($name)) {
$variableLessTags[] = $varTag;
$isVariableLessTagIgnored = $areVarTagsIgnored;
continue;
}

// WrongVariableNameInVarTagRule checks these: with their type not assigned, the
// second run of this method for an assignment would report them again
if ($areVarTagsIgnored) {
continue;
}

Expand Down Expand Up @@ -1988,7 +1997,7 @@ public function processStmtVarAnnotation(NodeScopeResolver $nodeScopeResolver, M
}
}

if (count($variableLessTags) === 1 && $defaultExpr !== null) {
if (count($variableLessTags) === 1 && !$isVariableLessTagIgnored && $defaultExpr !== null) {
// only the scope effect here: the changed-type node is emitted by the
// statement handler AFTER it walked the expression (emitVarTagChangedNode),
// so the rule prices the expression from its stored result rather than
Expand All @@ -2002,9 +2011,10 @@ public function processStmtVarAnnotation(NodeScopeResolver $nodeScopeResolver, M
/**
* The single variable-less @var tag on the statement's doc comment, or null
* when there is none or more than one - the shape processStmtVarAnnotation()
* applies to $defaultExpr and emitVarTagChangedNode() reports on.
* applies to $defaultExpr and emitVarTagChangedNode() reports on. $isIgnored
* tells whether its PHPDoc has @phpstan-ignore-var.
*/
private function findSingleVariableLessVarTag(MutatingScope $scope, Node\Stmt $stmt): ?VarTag
private function findSingleVariableLessVarTag(MutatingScope $scope, Node\Stmt $stmt, bool &$isIgnored): ?VarTag
{
$function = $scope->getFunction();
$variableLessTags = [];
Expand All @@ -2026,6 +2036,7 @@ private function findSingleVariableLessVarTag(MutatingScope $scope, Node\Stmt $s
}

$variableLessTags[] = $varTag;
$isIgnored = $resolvedPhpDoc->areVarTagsIgnored();
}
}

Expand All @@ -2041,12 +2052,16 @@ private function findSingleVariableLessVarTag(MutatingScope $scope, Node\Stmt $s
*/
public function emitVarTagChangedNode(NodeScopeResolver $nodeScopeResolver, MutatingScope $scope, ExpressionResultStorage $storage, Node\Stmt $stmt, Expr $defaultExpr, callable $nodeCallback): TemplateArgumentConstraints
{
$varTag = $this->findSingleVariableLessVarTag($scope, $stmt);
$isIgnored = false;
$varTag = $this->findSingleVariableLessVarTag($scope, $stmt, $isIgnored);
if ($varTag === null) {
return TemplateArgumentConstraints::createEmpty();
}

$nodeScopeResolver->callNodeCallback($nodeCallback, new VarTagChangedExpressionTypeNode($varTag, $defaultExpr), $scope, $storage);
if ($isIgnored) {
return TemplateArgumentConstraints::createEmpty();
}
$defaultExprResult = $storage->findExpressionResult($defaultExpr);
if ($defaultExprResult === null || $nodeScopeResolver->observingTemplateArgumentFrame($defaultExprResult->getScope()) === null) {
return TemplateArgumentConstraints::createEmpty();
Expand Down
3 changes: 3 additions & 0 deletions src/Analyser/VarAnnotationProcessor.php
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,9 @@ public function processVarAnnotation(MutatingScope $scope, array $variableNames,
$function !== null ? $function->getName() : null,
$comment->getText(),
);
if ($resolvedPhpDoc->areVarTagsIgnored()) {
continue;
}
foreach ($resolvedPhpDoc->getVarTags() as $key => $varTag) {
$varTags[$key] = $varTag;
}
Expand Down
8 changes: 6 additions & 2 deletions src/Parser/RichParser.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
use function count;
use function implode;
use function in_array;
use function preg_match;
use function preg_match_all;
use function sprintf;
use function str_contains;
Expand All @@ -47,6 +48,9 @@ final class RichParser implements Parser

private const PHPDOC_DOCTRINE_TAG_REGEX = '(@[a-z_\\\\][a-z0-9_\:\\\\]*[a-z_][a-z0-9_]*)';

// @phpstan-ignore-var is a PHPDoc tag of its own, not an ignore comment
private const IGNORE_COMMENT_REGEX = '~@phpstan-ignore(?!-var\b)~';

/**
* @param ExtensionsCollection<NodeVisitor> $nodeVisitors
*/
Expand Down Expand Up @@ -219,10 +223,10 @@ private function getLinesToIgnore(array $tokens): array
}
}

$ignorePos = strpos($text, '@phpstan-ignore');
if ($ignorePos === false) {
if (preg_match(self::IGNORE_COMMENT_REGEX, $text, $ignoreMatch, PREG_OFFSET_CAPTURE) !== 1) {
continue;
}
$ignorePos = $ignoreMatch[0][1];

$ignoreLine = substr_count(substr($text, 0, $ignorePos), "\n") - 1;

Expand Down
35 changes: 34 additions & 1 deletion src/PhpDoc/PhpDocNodeResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,9 @@
use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprNullNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\MixinTagValueNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocTagNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\TemplateTagValueNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\VarTagValueNode;
use PHPStan\Reflection\PassedByReference;
use PHPStan\Rules\PhpDoc\UnresolvableTypeHelper;
use PHPStan\Type\Generic\TemplateTypeFactory;
Expand All @@ -48,13 +50,17 @@
use function count;
use function in_array;
use function method_exists;
use function preg_match;
use function str_starts_with;
use function substr;

#[AutowiredService]
final class PhpDocNodeResolver
{

/** The names of the @var tag, the last one taking precedence. */
public const VAR_TAG_NAMES = ['@var', '@phan-var', '@psalm-var', '@phpstan-var'];

public function __construct(
private TypeNodeResolver $typeNodeResolver,
private ConstExprNodeResolver $constExprNodeResolver,
Expand All @@ -70,7 +76,7 @@ public function resolveVarTags(PhpDocNode $phpDocNode, NameScope $nameScope): ar
{
$resolved = [];
$resolvedByTag = [];
foreach (['@var', '@phan-var', '@psalm-var', '@phpstan-var'] as $tagName) {
foreach (self::VAR_TAG_NAMES as $tagName) {
$tagResolved = [];
foreach ($phpDocNode->getVarTagValues($tagName) as $tagValue) {
$type = $this->typeNodeResolver->resolve($tagValue->type, $nameScope);
Expand Down Expand Up @@ -715,6 +721,33 @@ public function resolveAllMethodsImpure(PhpDocNode $phpDocNode): bool
return count($phpDocNode->getTagsByName('@phpstan-all-methods-impure')) > 0;
}

public function resolveAreVarTagsIgnored(PhpDocNode $phpDocNode): bool
{
if (count($phpDocNode->getTagsByName('@phpstan-ignore-var')) > 0) {
return true;
}

foreach ($phpDocNode->getTags() as $phpDocTag) {
if (self::isIgnoreVarInVarTagDescription($phpDocTag)) {
return true;
}
}

return false;
}

/**
* phpdoc-parser reads @phpstan-ignore-var written on the line of a @var tag, as in
* a one-line PHPDoc, as the description of that tag. It counts only at the start of
* the description, so a description that mentions the tag keeps the @var applied.
*/
public static function isIgnoreVarInVarTagDescription(PhpDocTagNode $phpDocTag): bool
{
return in_array($phpDocTag->name, self::VAR_TAG_NAMES, true)
&& $phpDocTag->value instanceof VarTagValueNode
&& preg_match('~^@phpstan-ignore-var\b~', $phpDocTag->value->description) === 1;
}

public function resolveIsReadOnly(PhpDocNode $phpDocNode): bool
{
foreach (['@readonly', '@phan-read-only', '@psalm-readonly', '@phpstan-readonly', '@phpstan-readonly-allow-private-mutation', '@psalm-readonly-allow-private-mutation'] as $tagName) {
Expand Down
15 changes: 15 additions & 0 deletions src/PhpDoc/ResolvedPhpDocBlock.php
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,8 @@ final class ResolvedPhpDocBlock

private ?bool $acceptsNamedArguments = null;

private ?bool $areVarTagsIgnored = null;

private function __construct()
{
}
Expand Down Expand Up @@ -255,6 +257,7 @@ public static function createEmpty(): self
$self->isAllowedPrivateMutation = false;
$self->hasConsistentConstructor = false;
$self->acceptsNamedArguments = true;
$self->areVarTagsIgnored = false;

return $self;
}
Expand Down Expand Up @@ -313,6 +316,7 @@ public function merge(ResolvedPhpDocBlock $parent, InheritedPhpDocParameterMappi
$result->isAllowedPrivateMutation = $this->isAllowedPrivateMutation();
$result->hasConsistentConstructor = $this->hasConsistentConstructor();
$result->acceptsNamedArguments = $acceptsNamedArguments;
$result->areVarTagsIgnored = $this->areVarTagsIgnored();

return $result;
}
Expand Down Expand Up @@ -483,6 +487,17 @@ public function getVarTags(): array
return $this->varTags;
}

/**
* Whether the PHPDoc has @phpstan-ignore-var. Its inline @var tags are then still
* returned by getVarTags() and checked by rules, but do not change any inferred type.
*/
public function areVarTagsIgnored(): bool
{
return $this->areVarTagsIgnored ??= $this->phpDocNodeResolver->resolveAreVarTagsIgnored(
$this->phpDocNode,
);
}

/**
* @return array<string, MethodTag>
*/
Expand Down
Loading
Loading