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\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
|
||||||
|
@ -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\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) {
|
||||||
|
@ -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 {}'
|
||||||
|
],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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';
|
||||||
|
Loading…
Reference in New Issue
Block a user