1
0
mirror of https://github.com/danog/psalm.git synced 2024-12-15 10:57:08 +01:00
psalm/src/Psalm/Internal/Analyzer/StatementsAnalyzer.php
2020-11-27 17:48:39 -05:00

1003 lines
36 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
namespace Psalm\Internal\Analyzer;
use PhpParser;
use Psalm\Internal\Analyzer\Statements\Block\DoAnalyzer;
use Psalm\Internal\Analyzer\Statements\Block\ForAnalyzer;
use Psalm\Internal\Analyzer\Statements\Block\ForeachAnalyzer;
use Psalm\Internal\Analyzer\Statements\Block\IfElseAnalyzer;
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;
use Psalm\Internal\Analyzer\Statements\Expression\Fetch\VariableFetchAnalyzer;
use Psalm\Internal\Analyzer\Statements\Expression\SimpleTypeInferer;
use Psalm\Internal\Analyzer\Statements\ExpressionAnalyzer;
use Psalm\Internal\Analyzer\Statements\ReturnAnalyzer;
use Psalm\Internal\Analyzer\Statements\ThrowAnalyzer;
use Psalm\Internal\Scanner\ParsedDocblock;
use Psalm\Internal\Codebase\DataFlowGraph;
use Psalm\Internal\Codebase\TaintFlowGraph;
use Psalm\Internal\Codebase\VariableUseGraph;
use Psalm\Internal\DataFlow\DataFlowNode;
use Psalm\Codebase;
use Psalm\CodeLocation;
use Psalm\Context;
use Psalm\DocComment;
use Psalm\Exception\DocblockParseException;
use Psalm\FileManipulation;
use Psalm\Internal\FileManipulation\FileManipulationBuffer;
use Psalm\Issue\ComplexFunction;
use Psalm\Issue\ComplexMethod;
use Psalm\Issue\InvalidDocblock;
use Psalm\Issue\MissingDocblockType;
use Psalm\Issue\Trace;
use Psalm\Issue\UndefinedTrace;
use Psalm\Issue\UnevaluatedCode;
use Psalm\Issue\UnrecognizedStatement;
use Psalm\Issue\UnusedVariable;
use Psalm\IssueBuffer;
use Psalm\Type;
use function strtolower;
use function fwrite;
use const STDERR;
use function array_filter;
use function array_merge;
use function preg_split;
use function get_class;
use function strrpos;
use function strlen;
use function substr;
use function array_change_key_case;
use function trim;
use function array_column;
use function array_combine;
use function array_keys;
use function round;
/**
* @internal
*/
class StatementsAnalyzer extends SourceAnalyzer
{
/**
* @var SourceAnalyzer
*/
protected $source;
/**
* @var FileAnalyzer
*/
protected $file_analyzer;
/**
* @var Codebase
*/
protected $codebase;
/**
* @var array<string, CodeLocation>
*/
private $all_vars = [];
/**
* @var array<string, int>
*/
private $var_branch_points = [];
/**
* Possibly undefined variables should be initialised if we're altering code
*
* @var array<string, int>|null
*/
private $vars_to_initialize;
/**
* @var array<string, FunctionAnalyzer>
*/
private $function_analyzers = [];
/**
* @var array<string, array{0: string, 1: CodeLocation}>
*/
private $unused_var_locations = [];
/**
* @var ?array<string, bool>
*/
public $byref_uses;
/**
* @var ParsedDocblock|null
*/
private $parsed_docblock = null;
/**
* @var ?string
*/
private $fake_this_class = null;
/** @var \Psalm\Internal\Provider\NodeDataProvider */
public $node_data;
/** @var ?DataFlowGraph */
public $data_flow_graph;
public function __construct(SourceAnalyzer $source, \Psalm\Internal\Provider\NodeDataProvider $node_data)
{
$this->source = $source;
$this->file_analyzer = $source->getFileAnalyzer();
$this->codebase = $source->getCodebase();
$this->node_data = $node_data;
if ($this->codebase->taint_flow_graph) {
$this->data_flow_graph = new TaintFlowGraph();
} elseif ($this->codebase->find_unused_variables) {
$this->data_flow_graph = new VariableUseGraph();
}
}
/**
* Checks an array of statements for validity
*
* @param array<PhpParser\Node\Stmt> $stmts
*
* @return null|false
*/
public function analyze(
array $stmts,
Context $context,
?Context $global_context = null,
bool $root_scope = false
): ?bool {
if (!$stmts) {
return null;
}
// hoist functions to the top
$this->hoistFunctions($stmts, $context);
$project_analyzer = $this->getFileAnalyzer()->project_analyzer;
$codebase = $project_analyzer->getCodebase();
if ($codebase->config->hoist_constants) {
self::hoistConstants($this, $stmts, $context);
}
foreach ($stmts as $stmt) {
if (self::analyzeStatement($this, $stmt, $context, $global_context) === false) {
return false;
}
}
if ($root_scope
&& !$context->collect_initializations
&& !$context->collect_mutations
&& $codebase->find_unused_variables
&& $context->check_variables
) {
//var_dump($this->data_flow_graph);
$this->checkUnreferencedVars($stmts);
}
if ($codebase->alter_code && $root_scope && $this->vars_to_initialize) {
$file_contents = $codebase->getFileContents($this->getFilePath());
foreach ($this->vars_to_initialize as $var_id => $branch_point) {
$newline_pos = (int)strrpos($file_contents, "\n", $branch_point - strlen($file_contents)) + 1;
$indentation = substr($file_contents, $newline_pos, $branch_point - $newline_pos);
FileManipulationBuffer::add($this->getFilePath(), [
new FileManipulation($branch_point, $branch_point, $var_id . ' = null;' . "\n" . $indentation),
]);
}
}
if ($root_scope
&& $this->data_flow_graph instanceof TaintFlowGraph
&& $this->codebase->taint_flow_graph
&& $codebase->config->trackTaintsInPath($this->getFilePath())
) {
$this->codebase->taint_flow_graph->addGraph($this->data_flow_graph);
}
return null;
}
/**
* @param array<PhpParser\Node\Stmt> $stmts
*/
private function hoistFunctions(array $stmts, Context $context) : void
{
foreach ($stmts as $stmt) {
if ($stmt instanceof PhpParser\Node\Stmt\Function_) {
$function_name = strtolower($stmt->name->name);
if ($ns = $this->getNamespace()) {
$fq_function_name = strtolower($ns) . '\\' . $function_name;
} else {
$fq_function_name = $function_name;
}
if ($this->data_flow_graph
&& $this->codebase->find_unused_variables
) {
foreach ($stmt->stmts as $function_stmt) {
if ($function_stmt instanceof PhpParser\Node\Stmt\Global_) {
foreach ($function_stmt->vars as $var) {
if (!$var instanceof PhpParser\Node\Expr\Variable
|| !\is_string($var->name)
) {
continue;
}
$var_id = '$' . $var->name;
if ($var_id !== '$argv' && $var_id !== '$argc') {
$context->byref_constraints[$var_id] = new \Psalm\Internal\ReferenceConstraint();
}
}
}
}
}
try {
$function_analyzer = new FunctionAnalyzer($stmt, $this->source);
$this->function_analyzers[$fq_function_name] = $function_analyzer;
} catch (\UnexpectedValueException $e) {
// do nothing
}
}
}
}
/**
* @param array<PhpParser\Node\Stmt> $stmts
*/
private static function hoistConstants(
StatementsAnalyzer $statements_analyzer,
array $stmts,
Context $context
) : void {
$codebase = $statements_analyzer->getCodebase();
foreach ($stmts as $stmt) {
if ($stmt instanceof PhpParser\Node\Stmt\Const_) {
foreach ($stmt->consts as $const) {
ConstFetchAnalyzer::setConstType(
$statements_analyzer,
$const->name->name,
SimpleTypeInferer::infer(
$codebase,
$statements_analyzer->node_data,
$const->value,
$statements_analyzer->getAliases(),
$statements_analyzer
) ?: Type::getMixed(),
$context
);
}
} elseif ($stmt instanceof PhpParser\Node\Stmt\Expression
&& $stmt->expr instanceof PhpParser\Node\Expr\FuncCall
&& $stmt->expr->name instanceof PhpParser\Node\Name
&& $stmt->expr->name->parts === ['define']
&& isset($stmt->expr->args[1])
) {
$const_name = ConstFetchAnalyzer::getConstName(
$stmt->expr->args[0]->value,
$statements_analyzer->node_data,
$codebase,
$statements_analyzer->getAliases()
);
if ($const_name !== null) {
ConstFetchAnalyzer::setConstType(
$statements_analyzer,
$const_name,
Statements\Expression\SimpleTypeInferer::infer(
$codebase,
$statements_analyzer->node_data,
$stmt->expr->args[1]->value,
$statements_analyzer->getAliases(),
$statements_analyzer
) ?: Type::getMixed(),
$context
);
}
}
}
}
/**
* @return false|null
*/
private static function analyzeStatement(
StatementsAnalyzer $statements_analyzer,
PhpParser\Node\Stmt $stmt,
Context $context,
?Context $global_context
): ?bool {
$ignore_variable_property = false;
$ignore_variable_method = false;
$codebase = $statements_analyzer->getCodebase();
if ($context->has_returned
&& !$context->collect_initializations
&& !$context->collect_mutations
&& !($stmt instanceof PhpParser\Node\Stmt\Nop)
&& !($stmt instanceof PhpParser\Node\Stmt\InlineHTML)
) {
if ($codebase->find_unused_variables) {
if (IssueBuffer::accepts(
new UnevaluatedCode(
'Expressions after return/throw/continue',
new CodeLocation($statements_analyzer->source, $stmt)
),
$statements_analyzer->source->getSuppressedIssues()
)) {
return false;
}
}
return null;
}
if ($statements_analyzer->getProjectAnalyzer()->debug_lines) {
fwrite(STDERR, $statements_analyzer->getFilePath() . ':' . $stmt->getLine() . "\n");
}
/*
if (isset($context->vars_in_scope['$array']) && !$stmt instanceof PhpParser\Node\Stmt\Nop) {
var_dump($stmt->getLine(), $context->vars_in_scope['$array']);
}
*/
$new_issues = null;
$traced_variables = [];
if ($docblock = $stmt->getDocComment()) {
$statements_analyzer->parseStatementDocblock($docblock, $stmt, $context);
if (isset($statements_analyzer->parsed_docblock->tags['psalm-trace'])) {
foreach ($statements_analyzer->parsed_docblock->tags['psalm-trace'] as $traced_variable_line) {
$possible_traced_variable_names = preg_split('/[\s]+/', $traced_variable_line);
if ($possible_traced_variable_names) {
$traced_variables = array_merge(
$traced_variables,
array_filter($possible_traced_variable_names)
);
}
}
}
if (isset($statements_analyzer->parsed_docblock->tags['psalm-ignore-variable-method'])) {
$context->ignore_variable_method = $ignore_variable_method = true;
}
if (isset($statements_analyzer->parsed_docblock->tags['psalm-ignore-variable-property'])) {
$context->ignore_variable_property = $ignore_variable_property = true;
}
if (isset($statements_analyzer->parsed_docblock->tags['psalm-suppress'])) {
$suppressed = $statements_analyzer->parsed_docblock->tags['psalm-suppress'];
if ($suppressed) {
$new_issues = [];
foreach ($suppressed as $offset => $suppress_entry) {
foreach (DocComment::parseSuppressList($suppress_entry) as $issue_offset => $issue_type) {
$new_issues[$issue_offset + $offset + $docblock->getStartFilePos()] = $issue_type;
if ($issue_type === 'InaccessibleMethod') {
continue;
}
if ($codebase->track_unused_suppressions) {
IssueBuffer::addUnusedSuppression(
$statements_analyzer->getFilePath(),
$issue_offset + $offset + $docblock->getStartFilePos(),
$issue_type
);
}
}
}
$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;
}
if ($stmt instanceof PhpParser\Node\Stmt\If_) {
if (IfElseAnalyzer::analyze($statements_analyzer, $stmt, $context) === false) {
return false;
}
} elseif ($stmt instanceof PhpParser\Node\Stmt\TryCatch) {
if (TryAnalyzer::analyze($statements_analyzer, $stmt, $context) === false) {
return false;
}
} elseif ($stmt instanceof PhpParser\Node\Stmt\For_) {
if (ForAnalyzer::analyze($statements_analyzer, $stmt, $context) === false) {
return false;
}
} elseif ($stmt instanceof PhpParser\Node\Stmt\Foreach_) {
if (ForeachAnalyzer::analyze($statements_analyzer, $stmt, $context) === false) {
return false;
}
} elseif ($stmt instanceof PhpParser\Node\Stmt\While_) {
if (WhileAnalyzer::analyze($statements_analyzer, $stmt, $context) === false) {
return false;
}
} elseif ($stmt instanceof PhpParser\Node\Stmt\Do_) {
DoAnalyzer::analyze($statements_analyzer, $stmt, $context);
} elseif ($stmt instanceof PhpParser\Node\Stmt\Const_) {
ConstFetchAnalyzer::analyzeConstAssignment($statements_analyzer, $stmt, $context);
} elseif ($stmt instanceof PhpParser\Node\Stmt\Unset_) {
Statements\UnsetAnalyzer::analyze($statements_analyzer, $stmt, $context);
} elseif ($stmt instanceof PhpParser\Node\Stmt\Return_) {
ReturnAnalyzer::analyze($statements_analyzer, $stmt, $context);
$context->has_returned = true;
} elseif ($stmt instanceof PhpParser\Node\Stmt\Throw_) {
ThrowAnalyzer::analyze($statements_analyzer, $stmt, $context);
$context->has_returned = true;
} elseif ($stmt instanceof PhpParser\Node\Stmt\Switch_) {
SwitchAnalyzer::analyze($statements_analyzer, $stmt, $context);
} elseif ($stmt instanceof PhpParser\Node\Stmt\Break_) {
Statements\BreakAnalyzer::analyze($statements_analyzer, $stmt, $context);
} elseif ($stmt instanceof PhpParser\Node\Stmt\Continue_) {
Statements\ContinueAnalyzer::analyze($statements_analyzer, $stmt, $context);
} elseif ($stmt instanceof PhpParser\Node\Stmt\Static_) {
Statements\StaticAnalyzer::analyze($statements_analyzer, $stmt, $context);
} elseif ($stmt instanceof PhpParser\Node\Stmt\Echo_) {
if (Statements\EchoAnalyzer::analyze($statements_analyzer, $stmt, $context) === false) {
return false;
}
} elseif ($stmt instanceof PhpParser\Node\Stmt\Function_) {
FunctionAnalyzer::analyzeStatement($statements_analyzer, $stmt, $context);
} elseif ($stmt instanceof PhpParser\Node\Stmt\Expression) {
if (ExpressionAnalyzer::analyze(
$statements_analyzer,
$stmt->expr,
$context,
false,
$global_context,
true
) === false) {
return false;
}
} elseif ($stmt instanceof PhpParser\Node\Stmt\InlineHTML) {
// do nothing
} elseif ($stmt instanceof PhpParser\Node\Stmt\Global_) {
Statements\GlobalAnalyzer::analyze($statements_analyzer, $stmt, $context, $global_context);
} elseif ($stmt instanceof PhpParser\Node\Stmt\Property) {
InstancePropertyAssignmentAnalyzer::analyzeStatement($statements_analyzer, $stmt, $context);
} elseif ($stmt instanceof PhpParser\Node\Stmt\ClassConst) {
ClassConstFetchAnalyzer::analyzeClassConstAssignment($statements_analyzer, $stmt, $context);
} elseif ($stmt instanceof PhpParser\Node\Stmt\Class_) {
try {
$class_analyzer = new ClassAnalyzer(
$stmt,
$statements_analyzer->source,
$stmt->name ? $stmt->name->name : null
);
$class_analyzer->analyze(null, $global_context);
} catch (\InvalidArgumentException $e) {
// disregard this exception, we'll likely see it elsewhere in the form
// of an issue
}
} elseif ($stmt instanceof PhpParser\Node\Stmt\Nop) {
// do nothing
} elseif ($stmt instanceof PhpParser\Node\Stmt\Goto_) {
// do nothing
} elseif ($stmt instanceof PhpParser\Node\Stmt\Label) {
// do nothing
} elseif ($stmt instanceof PhpParser\Node\Stmt\Declare_) {
foreach ($stmt->declares as $declaration) {
if ((string) $declaration->key === 'strict_types'
&& $declaration->value instanceof PhpParser\Node\Scalar\LNumber
&& $declaration->value->value === 1
) {
$context->strict_types = true;
}
}
} elseif ($stmt instanceof PhpParser\Node\Stmt\HaltCompiler) {
$context->has_returned = true;
} else {
if (IssueBuffer::accepts(
new UnrecognizedStatement(
'Psalm does not understand ' . get_class($stmt),
new CodeLocation($statements_analyzer->source, $stmt)
),
$statements_analyzer->getSuppressedIssues()
)) {
return false;
}
}
$codebase = $statements_analyzer->getCodebase();
$plugin_classes = $codebase->config->after_statement_checks;
if ($plugin_classes) {
$file_manipulations = [];
foreach ($plugin_classes as $plugin_fq_class_name) {
if ($plugin_fq_class_name::afterStatementAnalysis(
$stmt,
$context,
$statements_analyzer,
$codebase,
$file_manipulations
) === false) {
return false;
}
}
if ($file_manipulations) {
FileManipulationBuffer::add($statements_analyzer->getFilePath(), $file_manipulations);
}
}
if ($new_issues) {
$statements_analyzer->removeSuppressedIssues($new_issues);
}
if ($ignore_variable_property) {
$context->ignore_variable_property = false;
}
if ($ignore_variable_method) {
$context->ignore_variable_method = false;
}
foreach ($traced_variables as $traced_variable) {
if (isset($context->vars_in_scope[$traced_variable])) {
if (IssueBuffer::accepts(
new Trace(
$traced_variable . ': ' . $context->vars_in_scope[$traced_variable]->getId(),
new CodeLocation($statements_analyzer->source, $stmt)
),
$statements_analyzer->getSuppressedIssues()
)) {
// fall through
}
} else {
if (IssueBuffer::accepts(
new UndefinedTrace(
'Attempt to trace undefined variable ' . $traced_variable,
new CodeLocation($statements_analyzer->source, $stmt)
),
$statements_analyzer->getSuppressedIssues()
)) {
// fall through
}
}
}
return null;
}
private function parseStatementDocblock(
PhpParser\Comment\Doc $docblock,
PhpParser\Node\Stmt $stmt,
Context $context
) : void {
$codebase = $this->getCodebase();
try {
$this->parsed_docblock = DocComment::parsePreservingLength($docblock);
} catch (DocblockParseException $e) {
if (IssueBuffer::accepts(
new InvalidDocblock(
(string)$e->getMessage(),
new CodeLocation($this->getSource(), $stmt, null, true)
)
)) {
// fall through
}
$this->parsed_docblock = null;
}
$comments = $this->parsed_docblock;
if (isset($comments->tags['psalm-scope-this'])) {
$trimmed = trim(\reset($comments->tags['psalm-scope-this']));
if (!$codebase->classExists($trimmed)) {
if (IssueBuffer::accepts(
new \Psalm\Issue\UndefinedDocblockClass(
'Scope class ' . $trimmed . ' does not exist',
new CodeLocation($this->getSource(), $stmt, null, true),
$trimmed
)
)) {
// fall through
}
} else {
$this_type = Type::parseString($trimmed);
$context->self = $trimmed;
$context->vars_in_scope['$this'] = $this_type;
$this->setFQCLN($trimmed);
}
}
}
/**
* @param array<PhpParser\Node\Stmt> $stmts
*/
public function checkUnreferencedVars(array $stmts): void
{
$source = $this->getSource();
$codebase = $source->getCodebase();
$function_storage = $source instanceof FunctionLikeAnalyzer ? $source->getFunctionLikeStorage($this) : null;
$var_list = array_column($this->unused_var_locations, 0);
$loc_list = array_column($this->unused_var_locations, 1);
$project_analyzer = $this->getProjectAnalyzer();
$unused_var_remover = new Statements\UnusedAssignmentRemover();
if ($this->data_flow_graph instanceof VariableUseGraph
&& $codebase->config->limit_method_complexity
&& $source instanceof FunctionLikeAnalyzer
&& !$source instanceof ClosureAnalyzer
&& $function_storage
&& $function_storage->location
) {
[$count, , $unique_destinations, $mean] = $this->data_flow_graph->getEdgeStats();
$average_destination_branches_converging = $unique_destinations > 0 ? $count / $unique_destinations : 0;
if ($count > $codebase->config->max_graph_size
&& $mean > $codebase->config->max_avg_path_length
&& $average_destination_branches_converging > 1.1
) {
if ($source instanceof FunctionAnalyzer) {
if (IssueBuffer::accepts(
new ComplexFunction(
'This functions complexity is greater than the project limit'
. ' (method graph size = ' . $count .', average path length = ' . round($mean). ')',
$function_storage->location
),
$this->getSuppressedIssues(),
true
)) {
// fall through
}
} elseif ($source instanceof MethodAnalyzer) {
if (IssueBuffer::accepts(
new ComplexMethod(
'This methods complexity is greater than the project limit'
. ' (method graph size = ' . $count .', average path length = ' . round($mean) . ')',
$function_storage->location
),
$this->getSuppressedIssues(),
true
)) {
// fall through
}
}
}
}
foreach ($this->unused_var_locations as [$var_id, $original_location]) {
if (substr($var_id, 0, 2) === '$_') {
continue;
}
if ($function_storage) {
$param_index = \array_search(substr($var_id, 1), array_keys($function_storage->param_lookup));
if ($param_index !== false) {
$param = $function_storage->params[$param_index];
if ($param->location
&& ($original_location->raw_file_end === $param->location->raw_file_end
|| $param->by_ref)
) {
continue;
}
}
}
$assignment_node = DataFlowNode::getForAssignment($var_id, $original_location);
if (!isset($this->byref_uses[$var_id])
&& !VariableFetchAnalyzer::isSuperGlobal($var_id)
&& $this->data_flow_graph instanceof VariableUseGraph
&& !$this->data_flow_graph->isVariableUsed($assignment_node)
) {
$issue = new UnusedVariable(
$var_id . ' is never referenced or the value is not used',
$original_location
);
if ($codebase->alter_code
&& !$unused_var_remover->checkIfVarRemoved($var_id, $original_location)
&& isset($project_analyzer->getIssuesToFix()['UnusedVariable'])
&& !IssueBuffer::isSuppressed($issue, $this->getSuppressedIssues())
) {
$unused_var_remover->findUnusedAssignment(
$this->getCodebase(),
$stmts,
array_combine($var_list, $loc_list),
$var_id,
$original_location
);
}
if (IssueBuffer::accepts(
$issue,
$this->getSuppressedIssues(),
true
)) {
// fall through
}
}
}
}
public function hasVariable(string $var_name): bool
{
return isset($this->all_vars[$var_name]);
}
public function registerVariable(string $var_id, CodeLocation $location, ?int $branch_point): void
{
$this->all_vars[$var_id] = $location;
if ($branch_point) {
$this->var_branch_points[$var_id] = $branch_point;
}
$this->registerVariableAssignment($var_id, $location);
}
public function registerVariableAssignment(string $var_id, CodeLocation $location): void
{
$this->unused_var_locations[$location->getHash()] = [$var_id, $location];
}
/**
* @return array<string, array{0: string, 1: CodeLocation}>
*/
public function getUnusedVarLocations(): array
{
return $this->unused_var_locations;
}
public function registerPossiblyUndefinedVariable(
string $undefined_var_id,
PhpParser\Node\Expr\Variable $stmt
) : void {
if (!$this->data_flow_graph) {
return;
}
$use_location = new CodeLocation($this->getSource(), $stmt);
$use_node = DataFlowNode::getForAssignment($undefined_var_id, $use_location);
$stmt_type = $this->node_data->getType($stmt);
if ($stmt_type) {
$stmt_type->parent_nodes[$use_node->id] = $use_node;
}
foreach ($this->unused_var_locations as [$var_id, $original_location]) {
if ($var_id === $undefined_var_id) {
$parent_node = DataFlowNode::getForAssignment($var_id, $original_location);
$this->data_flow_graph->addPath($parent_node, $use_node, '=');
}
}
}
/**
* @return array<string, DataFlowNode>
*/
public function getParentNodesForPossiblyUndefinedVariable(string $undefined_var_id) : array
{
if (!$this->data_flow_graph) {
return [];
}
$parent_nodes = [];
foreach ($this->unused_var_locations as [$var_id, $original_location]) {
if ($var_id === $undefined_var_id) {
$assignment_node = DataFlowNode::getForAssignment($var_id, $original_location);
$parent_nodes[$assignment_node->id] = $assignment_node;
}
}
return $parent_nodes;
}
/**
* The first appearance of the variable in this set of statements being evaluated
*/
public function getFirstAppearance(string $var_id): ?CodeLocation
{
return isset($this->all_vars[$var_id]) ? $this->all_vars[$var_id] : null;
}
public function getBranchPoint(string $var_id): ?int
{
return isset($this->var_branch_points[$var_id]) ? $this->var_branch_points[$var_id] : null;
}
public function addVariableInitialization(string $var_id, int $branch_point): void
{
$this->vars_to_initialize[$var_id] = $branch_point;
}
public function getFileAnalyzer() : FileAnalyzer
{
return $this->file_analyzer;
}
public function getCodebase() : Codebase
{
return $this->codebase;
}
/**
* @return array<string, FunctionAnalyzer>
*/
public function getFunctionAnalyzers(): array
{
return $this->function_analyzers;
}
/**
* @param array<string, bool> $byref_uses
*/
public function setByRefUses(array $byref_uses): void
{
$this->byref_uses = $byref_uses;
}
/**
* @return array<string, array<array-key, CodeLocation>>
*/
public function getUncaughtThrows(Context $context): array
{
$uncaught_throws = [];
if ($context->collect_exceptions) {
if ($context->possibly_thrown_exceptions) {
$config = $this->codebase->config;
$ignored_exceptions = array_change_key_case(
$context->is_global ?
$config->ignored_exceptions_in_global_scope :
$config->ignored_exceptions
);
$ignored_exceptions_and_descendants = array_change_key_case(
$context->is_global ?
$config->ignored_exceptions_and_descendants_in_global_scope :
$config->ignored_exceptions_and_descendants
);
foreach ($context->possibly_thrown_exceptions as $possibly_thrown_exception => $codelocations) {
if (isset($ignored_exceptions[strtolower($possibly_thrown_exception)])) {
continue;
}
$is_expected = false;
foreach ($ignored_exceptions_and_descendants as $expected_exception => $_) {
try {
if ($expected_exception === strtolower($possibly_thrown_exception)
|| $this->codebase->classExtends($possibly_thrown_exception, $expected_exception)
) {
$is_expected = true;
break;
}
} catch (\InvalidArgumentException $e) {
$is_expected = true;
break;
}
}
if (!$is_expected) {
$uncaught_throws[$possibly_thrown_exception] = $codelocations;
}
}
}
}
return $uncaught_throws;
}
public function getFunctionAnalyzer(string $function_id) : ?FunctionAnalyzer
{
return $this->function_analyzers[$function_id] ?? null;
}
public function getParsedDocblock() : ?ParsedDocblock
{
return $this->parsed_docblock;
}
public function getFQCLN(): ?string
{
if ($this->fake_this_class) {
return $this->fake_this_class;
}
return parent::getFQCLN();
}
public function setFQCLN(string $fake_this_class) : void
{
$this->fake_this_class = $fake_this_class;
}
/**
* @return \Psalm\Internal\Provider\NodeDataProvider
*/
public function getNodeTypeProvider() : \Psalm\NodeTypeProvider
{
return $this->node_data;
}
}