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:
parent
42ad366dc8
commit
eddd7b8c11
@ -10,6 +10,7 @@ use Psalm\Internal\Analyzer\Statements\Expression\Assignment\StaticPropertyAssig
|
||||
use Psalm\Internal\Analyzer\Statements\ExpressionAnalyzer;
|
||||
use Psalm\Internal\Analyzer\StatementsAnalyzer;
|
||||
use Psalm\Internal\Type\Comparator\UnionTypeComparator;
|
||||
use Psalm\Internal\Scanner\VarDocblockComment;
|
||||
use Psalm\CodeLocation;
|
||||
use Psalm\Context;
|
||||
use Psalm\Exception\DocblockParseException;
|
||||
@ -129,101 +130,15 @@ class AssignmentAnalyzer
|
||||
$removed_taints = $var_comment->removed_taints;
|
||||
}
|
||||
|
||||
if (!$var_comment->type) {
|
||||
continue;
|
||||
}
|
||||
|
||||
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(), $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
|
||||
}
|
||||
}
|
||||
self::assignTypeFromVarDocblock(
|
||||
$statements_analyzer,
|
||||
$assign_var,
|
||||
$var_comment,
|
||||
$context,
|
||||
$var_id,
|
||||
$comment_type,
|
||||
$comment_type_location
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -980,6 +895,114 @@ class AssignmentAnalyzer
|
||||
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 PhpParser\Node\Expr\AssignOp $stmt
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -9,6 +9,7 @@ use Psalm\Internal\Analyzer\Statements\Block\IfAnalyzer;
|
||||
use Psalm\Internal\Analyzer\Statements\Block\SwitchAnalyzer;
|
||||
use Psalm\Internal\Analyzer\Statements\Block\TryAnalyzer;
|
||||
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\Fetch\ClassConstFetchAnalyzer;
|
||||
use Psalm\Internal\Analyzer\Statements\Expression\Fetch\ConstFetchAnalyzer;
|
||||
@ -26,6 +27,7 @@ use Psalm\Exception\DocblockParseException;
|
||||
use Psalm\FileManipulation;
|
||||
use Psalm\Internal\FileManipulation\FileManipulationBuffer;
|
||||
use Psalm\Issue\InvalidDocblock;
|
||||
use Psalm\Issue\MissingDocblockType;
|
||||
use Psalm\Issue\Trace;
|
||||
use Psalm\Issue\UndefinedTrace;
|
||||
use Psalm\Issue\UnevaluatedCode;
|
||||
@ -379,6 +381,61 @@ class StatementsAnalyzer extends SourceAnalyzer implements StatementsSource
|
||||
$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 {
|
||||
$statements_analyzer->parsed_docblock = null;
|
||||
}
|
||||
@ -462,7 +519,7 @@ class StatementsAnalyzer extends SourceAnalyzer implements StatementsSource
|
||||
// of an issue
|
||||
}
|
||||
} elseif ($stmt instanceof PhpParser\Node\Stmt\Nop) {
|
||||
Statements\NopAnalyzer::analyze($statements_analyzer, $stmt, $context);
|
||||
// do nothing
|
||||
} elseif ($stmt instanceof PhpParser\Node\Stmt\Goto_) {
|
||||
// do nothing
|
||||
} elseif ($stmt instanceof PhpParser\Node\Stmt\Label) {
|
||||
|
@ -1156,6 +1156,18 @@ class AnnotationTest extends TestCase
|
||||
*/
|
||||
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 {}'
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
|
@ -155,7 +155,6 @@ class CodebaseTest extends TestCase
|
||||
Codebase $codebase,
|
||||
array &$file_replacements = []
|
||||
) {
|
||||
/** @var ClassLikeStorage $storage */
|
||||
if ($storage->name === 'C') {
|
||||
$storage->custom_metadata['a'] = 'b';
|
||||
$storage->methods['m']->custom_metadata['c'] = 'd';
|
||||
|
Loading…
Reference in New Issue
Block a user