1
0
mirror of https://github.com/danog/psalm.git synced 2024-11-26 20:34:47 +01:00

Fix #1916 - support @var docblock annotations in more places

This commit is contained in:
Brown 2020-07-26 13:23:21 -04:00
parent 42ad366dc8
commit eddd7b8c11
5 changed files with 188 additions and 158 deletions

View File

@ -10,6 +10,7 @@ use Psalm\Internal\Analyzer\Statements\Expression\Assignment\StaticPropertyAssig
use Psalm\Internal\Analyzer\Statements\ExpressionAnalyzer; use Psalm\Internal\Analyzer\Statements\ExpressionAnalyzer;
use Psalm\Internal\Analyzer\StatementsAnalyzer; use Psalm\Internal\Analyzer\StatementsAnalyzer;
use Psalm\Internal\Type\Comparator\UnionTypeComparator; use Psalm\Internal\Type\Comparator\UnionTypeComparator;
use Psalm\Internal\Scanner\VarDocblockComment;
use Psalm\CodeLocation; use Psalm\CodeLocation;
use Psalm\Context; use Psalm\Context;
use Psalm\Exception\DocblockParseException; use Psalm\Exception\DocblockParseException;
@ -129,101 +130,15 @@ class AssignmentAnalyzer
$removed_taints = $var_comment->removed_taints; $removed_taints = $var_comment->removed_taints;
} }
if (!$var_comment->type) { self::assignTypeFromVarDocblock(
continue; $statements_analyzer,
} $assign_var,
$var_comment,
try { $context,
$var_comment_type = \Psalm\Internal\Type\TypeExpander::expandUnion( $var_id,
$codebase, $comment_type,
$var_comment->type, $comment_type_location
$context->self, );
$context->self,
$statements_analyzer->getParentFQCLN()
);
$var_comment_type->setFromDocblock();
$var_comment_type->check(
$statements_analyzer,
new CodeLocation($statements_analyzer->getSource(), $assign_var),
$statements_analyzer->getSuppressedIssues(),
[],
false,
false,
false,
$context->calling_method_id
);
$type_location = null;
if ($var_comment->type_start
&& $var_comment->type_end
&& $var_comment->line_number
) {
$type_location = new CodeLocation\DocblockTypeLocation(
$statements_analyzer,
$var_comment->type_start,
$var_comment->type_end,
$var_comment->line_number
);
if ($codebase->alter_code) {
$codebase->classlikes->handleDocblockTypeInMigration(
$codebase,
$statements_analyzer,
$var_comment_type,
$type_location,
$context->calling_method_id
);
}
}
if (!$var_comment->var_id || $var_comment->var_id === $var_id) {
$comment_type = $var_comment_type;
$comment_type_location = $type_location;
continue;
}
if ($codebase->find_unused_variables
&& $type_location
&& isset($context->vars_in_scope[$var_comment->var_id])
&& $context->vars_in_scope[$var_comment->var_id]->getId() === $var_comment_type->getId()
&& !$var_comment_type->isMixed()
) {
$project_analyzer = $statements_analyzer->getProjectAnalyzer();
if ($codebase->alter_code
&& isset($project_analyzer->getIssuesToFix()['UnnecessaryVarAnnotation'])
) {
FileManipulationBuffer::addVarAnnotationToRemove($type_location);
} elseif (IssueBuffer::accepts(
new UnnecessaryVarAnnotation(
'The @var ' . $var_comment_type . ' annotation for '
. $var_comment->var_id . ' is unnecessary',
$type_location
),
$statements_analyzer->getSuppressedIssues(),
true
)) {
// fall through
}
}
$parent_nodes = $context->vars_in_scope[$var_comment->var_id]->parent_nodes ?? [];
$var_comment_type->parent_nodes = $parent_nodes;
$context->vars_in_scope[$var_comment->var_id] = $var_comment_type;
} catch (\UnexpectedValueException $e) {
if (IssueBuffer::accepts(
new InvalidDocblock(
(string)$e->getMessage(),
new CodeLocation($statements_analyzer->getSource(), $assign_var)
)
)) {
// fall through
}
}
} }
} }
@ -980,6 +895,114 @@ class AssignmentAnalyzer
return $assign_value_type; return $assign_value_type;
} }
public static function assignTypeFromVarDocblock(
StatementsAnalyzer $statements_analyzer,
PhpParser\Node $stmt,
VarDocblockComment $var_comment,
Context $context,
?string $var_id = null,
?Type\Union &$comment_type = null,
?CodeLocation\DocblockTypeLocation &$comment_type_location = null
) : void {
if (!$var_comment->type) {
return;
}
$codebase = $statements_analyzer->getCodebase();
try {
$var_comment_type = \Psalm\Internal\Type\TypeExpander::expandUnion(
$codebase,
$var_comment->type,
$context->self,
$context->self,
$statements_analyzer->getParentFQCLN()
);
$var_comment_type->setFromDocblock();
$var_comment_type->check(
$statements_analyzer,
new CodeLocation($statements_analyzer->getSource(), $stmt),
$statements_analyzer->getSuppressedIssues(),
[],
false,
false,
false,
$context->calling_method_id
);
$type_location = null;
if ($var_comment->type_start
&& $var_comment->type_end
&& $var_comment->line_number
) {
$type_location = new CodeLocation\DocblockTypeLocation(
$statements_analyzer,
$var_comment->type_start,
$var_comment->type_end,
$var_comment->line_number
);
if ($codebase->alter_code) {
$codebase->classlikes->handleDocblockTypeInMigration(
$codebase,
$statements_analyzer,
$var_comment_type,
$type_location,
$context->calling_method_id
);
}
}
if (!$var_comment->var_id || $var_comment->var_id === $var_id) {
$comment_type = $var_comment_type;
$comment_type_location = $type_location;
return;
}
if ($codebase->find_unused_variables
&& $type_location
&& isset($context->vars_in_scope[$var_comment->var_id])
&& $context->vars_in_scope[$var_comment->var_id]->getId() === $var_comment_type->getId()
&& !$var_comment_type->isMixed()
) {
$project_analyzer = $statements_analyzer->getProjectAnalyzer();
if ($codebase->alter_code
&& isset($project_analyzer->getIssuesToFix()['UnnecessaryVarAnnotation'])
) {
FileManipulationBuffer::addVarAnnotationToRemove($type_location);
} elseif (IssueBuffer::accepts(
new UnnecessaryVarAnnotation(
'The @var ' . $var_comment_type . ' annotation for '
. $var_comment->var_id . ' is unnecessary',
$type_location
),
$statements_analyzer->getSuppressedIssues(),
true
)) {
// fall through
}
}
$parent_nodes = $context->vars_in_scope[$var_comment->var_id]->parent_nodes ?? [];
$var_comment_type->parent_nodes = $parent_nodes;
$context->vars_in_scope[$var_comment->var_id] = $var_comment_type;
} catch (\UnexpectedValueException $e) {
if (IssueBuffer::accepts(
new InvalidDocblock(
(string)$e->getMessage(),
new CodeLocation($statements_analyzer->getSource(), $stmt)
)
)) {
// fall through
}
}
}
/** /**
* @param StatementsAnalyzer $statements_analyzer * @param StatementsAnalyzer $statements_analyzer
* @param PhpParser\Node\Expr\AssignOp $stmt * @param PhpParser\Node\Expr\AssignOp $stmt

View File

@ -1,61 +0,0 @@
<?php
namespace Psalm\Internal\Analyzer\Statements;
use PhpParser;
use Psalm\Internal\Analyzer\CommentAnalyzer;
use Psalm\Internal\Analyzer\StatementsAnalyzer;
use Psalm\CodeLocation;
use Psalm\Context;
use Psalm\Exception\DocblockParseException;
use Psalm\Issue\InvalidDocblock;
use Psalm\IssueBuffer;
class NopAnalyzer
{
public static function analyze(
StatementsAnalyzer $statements_analyzer,
PhpParser\Node\Stmt\Nop $stmt,
Context $context
) : void {
if (($doc_comment = $stmt->getDocComment()) && $parsed_docblock = $statements_analyzer->getParsedDocblock()) {
$var_comments = [];
try {
$var_comments = CommentAnalyzer::arrayToDocblocks(
$doc_comment,
$parsed_docblock,
$statements_analyzer->getSource(),
$statements_analyzer->getSource()->getAliases(),
$statements_analyzer->getSource()->getTemplateTypeMap()
);
} catch (DocblockParseException $e) {
if (IssueBuffer::accepts(
new InvalidDocblock(
(string)$e->getMessage(),
new CodeLocation($statements_analyzer->getSource(), $stmt, null, true)
)
)) {
// fall through
}
}
$codebase = $statements_analyzer->getCodebase();
foreach ($var_comments as $var_comment) {
if (!$var_comment->var_id || !$var_comment->type) {
continue;
}
$comment_type = \Psalm\Internal\Type\TypeExpander::expandUnion(
$codebase,
$var_comment->type,
$context->self,
$context->self,
$statements_analyzer->getParentFQCLN()
);
$context->vars_in_scope[$var_comment->var_id] = $comment_type;
}
}
}
}

View File

@ -9,6 +9,7 @@ use Psalm\Internal\Analyzer\Statements\Block\IfAnalyzer;
use Psalm\Internal\Analyzer\Statements\Block\SwitchAnalyzer; use Psalm\Internal\Analyzer\Statements\Block\SwitchAnalyzer;
use Psalm\Internal\Analyzer\Statements\Block\TryAnalyzer; use Psalm\Internal\Analyzer\Statements\Block\TryAnalyzer;
use Psalm\Internal\Analyzer\Statements\Block\WhileAnalyzer; use Psalm\Internal\Analyzer\Statements\Block\WhileAnalyzer;
use Psalm\Internal\Analyzer\Statements\Expression\AssignmentAnalyzer;
use Psalm\Internal\Analyzer\Statements\Expression\Assignment\InstancePropertyAssignmentAnalyzer; use Psalm\Internal\Analyzer\Statements\Expression\Assignment\InstancePropertyAssignmentAnalyzer;
use Psalm\Internal\Analyzer\Statements\Expression\Fetch\ClassConstFetchAnalyzer; use Psalm\Internal\Analyzer\Statements\Expression\Fetch\ClassConstFetchAnalyzer;
use Psalm\Internal\Analyzer\Statements\Expression\Fetch\ConstFetchAnalyzer; use Psalm\Internal\Analyzer\Statements\Expression\Fetch\ConstFetchAnalyzer;
@ -26,6 +27,7 @@ use Psalm\Exception\DocblockParseException;
use Psalm\FileManipulation; use Psalm\FileManipulation;
use Psalm\Internal\FileManipulation\FileManipulationBuffer; use Psalm\Internal\FileManipulation\FileManipulationBuffer;
use Psalm\Issue\InvalidDocblock; use Psalm\Issue\InvalidDocblock;
use Psalm\Issue\MissingDocblockType;
use Psalm\Issue\Trace; use Psalm\Issue\Trace;
use Psalm\Issue\UndefinedTrace; use Psalm\Issue\UndefinedTrace;
use Psalm\Issue\UnevaluatedCode; use Psalm\Issue\UnevaluatedCode;
@ -379,6 +381,61 @@ class StatementsAnalyzer extends SourceAnalyzer implements StatementsSource
$statements_analyzer->addSuppressedIssues($new_issues); $statements_analyzer->addSuppressedIssues($new_issues);
} }
} }
if (isset($statements_analyzer->parsed_docblock->combined_tags['var'])
&& !($stmt instanceof PhpParser\Node\Stmt\Expression
&& $stmt->expr instanceof PhpParser\Node\Expr\Assign)
&& !$stmt instanceof PhpParser\Node\Stmt\Foreach_
&& !$stmt instanceof PhpParser\Node\Stmt\Return_
) {
$file_path = $statements_analyzer->getRootFilePath();
$file_storage_provider = $codebase->file_storage_provider;
$file_storage = $file_storage_provider->get($file_path);
$template_type_map = $statements_analyzer->getTemplateTypeMap();
$var_comments = [];
try {
$var_comments = CommentAnalyzer::arrayToDocblocks(
$docblock,
$statements_analyzer->parsed_docblock,
$statements_analyzer->getSource(),
$statements_analyzer->getAliases(),
$template_type_map,
$file_storage->type_aliases
);
} catch (\Psalm\Exception\IncorrectDocblockException $e) {
if (IssueBuffer::accepts(
new MissingDocblockType(
(string)$e->getMessage(),
new CodeLocation($statements_analyzer->getSource(), $stmt)
)
)) {
// fall through
}
} catch (\Psalm\Exception\DocblockParseException $e) {
if (IssueBuffer::accepts(
new InvalidDocblock(
(string)$e->getMessage(),
new CodeLocation($statements_analyzer->getSource(), $stmt)
)
)) {
// fall through
}
}
foreach ($var_comments as $var_comment) {
AssignmentAnalyzer::assignTypeFromVarDocblock(
$statements_analyzer,
$stmt,
$var_comment,
$context
);
}
}
} else { } else {
$statements_analyzer->parsed_docblock = null; $statements_analyzer->parsed_docblock = null;
} }
@ -462,7 +519,7 @@ class StatementsAnalyzer extends SourceAnalyzer implements StatementsSource
// of an issue // of an issue
} }
} elseif ($stmt instanceof PhpParser\Node\Stmt\Nop) { } elseif ($stmt instanceof PhpParser\Node\Stmt\Nop) {
Statements\NopAnalyzer::analyze($statements_analyzer, $stmt, $context); // do nothing
} elseif ($stmt instanceof PhpParser\Node\Stmt\Goto_) { } elseif ($stmt instanceof PhpParser\Node\Stmt\Goto_) {
// do nothing // do nothing
} elseif ($stmt instanceof PhpParser\Node\Stmt\Label) { } elseif ($stmt instanceof PhpParser\Node\Stmt\Label) {

View File

@ -1156,6 +1156,18 @@ class AnnotationTest extends TestCase
*/ */
function bar() : void {}' function bar() : void {}'
], ],
'varDocblockAboveCall' => [
'<?php
function example(string $s): void {
if (preg_match(\'{foo-(\w+)}\', $s, $m)) {
/** @var array{string, string} $m */
takesString($m[1]);
}
}
function takesString(string $s): void {}'
],
]; ];
} }

View File

@ -155,7 +155,6 @@ class CodebaseTest extends TestCase
Codebase $codebase, Codebase $codebase,
array &$file_replacements = [] array &$file_replacements = []
) { ) {
/** @var ClassLikeStorage $storage */
if ($storage->name === 'C') { if ($storage->name === 'C') {
$storage->custom_metadata['a'] = 'b'; $storage->custom_metadata['a'] = 'b';
$storage->methods['m']->custom_metadata['c'] = 'd'; $storage->methods['m']->custom_metadata['c'] = 'd';