1
0
mirror of https://github.com/danog/psalm.git synced 2024-12-03 18:17:55 +01:00
psalm/src/Psalm/Internal/Analyzer/StatementsAnalyzer.php
Yannick Gottschalk 5a2f7c0a71 Use getParts() instead of $parts on PhpParser\Node\Name.
also use getFirst(), getLast() and getString()
2023-06-28 03:13:25 +02:00

1171 lines
44 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 InvalidArgumentException;
use PhpParser;
use Psalm\CodeLocation;
use Psalm\Codebase;
use Psalm\Context;
use Psalm\DocComment;
use Psalm\Exception\DocblockParseException;
use Psalm\Exception\IncorrectDocblockException;
use Psalm\Exception\TypeParseTreeException;
use Psalm\FileManipulation;
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\BreakAnalyzer;
use Psalm\Internal\Analyzer\Statements\ContinueAnalyzer;
use Psalm\Internal\Analyzer\Statements\EchoAnalyzer;
use Psalm\Internal\Analyzer\Statements\Expression\Assignment\InstancePropertyAssignmentAnalyzer;
use Psalm\Internal\Analyzer\Statements\Expression\AssignmentAnalyzer;
use Psalm\Internal\Analyzer\Statements\Expression\ClassConstAnalyzer;
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\GlobalAnalyzer;
use Psalm\Internal\Analyzer\Statements\ReturnAnalyzer;
use Psalm\Internal\Analyzer\Statements\StaticAnalyzer;
use Psalm\Internal\Analyzer\Statements\ThrowAnalyzer;
use Psalm\Internal\Analyzer\Statements\UnsetAnalyzer;
use Psalm\Internal\Analyzer\Statements\UnusedAssignmentRemover;
use Psalm\Internal\Codebase\DataFlowGraph;
use Psalm\Internal\Codebase\TaintFlowGraph;
use Psalm\Internal\Codebase\VariableUseGraph;
use Psalm\Internal\DataFlow\DataFlowNode;
use Psalm\Internal\FileManipulation\FileManipulationBuffer;
use Psalm\Internal\Provider\NodeDataProvider;
use Psalm\Internal\ReferenceConstraint;
use Psalm\Internal\Scanner\ParsedDocblock;
use Psalm\Internal\Type\Comparator\UnionTypeComparator;
use Psalm\Issue\CheckType;
use Psalm\Issue\ComplexFunction;
use Psalm\Issue\ComplexMethod;
use Psalm\Issue\InvalidDocblock;
use Psalm\Issue\MissingDocblockType;
use Psalm\Issue\Trace;
use Psalm\Issue\UndefinedDocblockClass;
use Psalm\Issue\UndefinedTrace;
use Psalm\Issue\UnevaluatedCode;
use Psalm\Issue\UnrecognizedStatement;
use Psalm\Issue\UnusedForeachValue;
use Psalm\Issue\UnusedVariable;
use Psalm\IssueBuffer;
use Psalm\NodeTypeProvider;
use Psalm\Plugin\EventHandler\Event\AfterStatementAnalysisEvent;
use Psalm\Plugin\EventHandler\Event\BeforeStatementAnalysisEvent;
use Psalm\Type;
use UnexpectedValueException;
use function array_change_key_case;
use function array_column;
use function array_combine;
use function array_keys;
use function array_map;
use function array_search;
use function assert;
use function count;
use function explode;
use function fwrite;
use function get_class;
use function in_array;
use function is_string;
use function preg_split;
use function reset;
use function round;
use function strlen;
use function strpos;
use function strrpos;
use function strtolower;
use function substr;
use function trim;
use const PREG_SPLIT_NO_EMPTY;
use const STDERR;
/**
* @internal
*/
class StatementsAnalyzer extends SourceAnalyzer
{
protected SourceAnalyzer $source;
protected FileAnalyzer $file_analyzer;
protected Codebase $codebase;
/**
* @var array<string, CodeLocation>
*/
private array $all_vars = [];
/**
* @var array<string, int>
*/
private array $var_branch_points = [];
/**
* Possibly undefined variables should be initialised if we're altering code
*
* @var array<string, int>|null
*/
private ?array $vars_to_initialize = null;
/**
* @var array<string, FunctionAnalyzer>
*/
private array $function_analyzers = [];
/**
* @var array<string, array{0: string, 1: CodeLocation}>
*/
private array $unused_var_locations = [];
/**
* @var array<string, true>
*/
public array $byref_uses = [];
private ?ParsedDocblock $parsed_docblock = null;
private ?string $fake_this_class = null;
public NodeDataProvider $node_data;
public ?DataFlowGraph $data_flow_graph = null;
/**
* Locations of foreach values
*
* Used to discern ordinary UnusedVariables from UnusedForeachValues
*
* @var array<string, list<CodeLocation>>
* @psalm-internal Psalm\Internal\Analyzer
*/
public array $foreach_var_locations = [];
public function __construct(SourceAnalyzer $source, 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
) {
$this->checkUnreferencedVars($stmts, $context);
}
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 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->getParts() === ['define']
&& isset($stmt->expr->getArgs()[1])
) {
$const_name = ConstFetchAnalyzer::getConstName(
$stmt->expr->getArgs()[0]->value,
$statements_analyzer->node_data,
$codebase,
$statements_analyzer->getAliases(),
);
if ($const_name !== null) {
ConstFetchAnalyzer::setConstType(
$statements_analyzer,
$const_name,
SimpleTypeInferer::infer(
$codebase,
$statements_analyzer->node_data,
$stmt->expr->getArgs()[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 {
if (self::dispatchBeforeStatementAnalysis($stmt, $context, $statements_analyzer) === false) {
return false;
}
$ignore_variable_property = false;
$ignore_variable_method = false;
$codebase = $statements_analyzer->getCodebase();
if ($statements_analyzer->getProjectAnalyzer()->debug_lines) {
fwrite(STDERR, $statements_analyzer->getFilePath() . ':' . $stmt->getLine() . "\n");
}
$new_issues = null;
$traced_variables = [];
$checked_types = [];
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*,\s*|\s+)/',
$traced_variable_line,
-1,
PREG_SPLIT_NO_EMPTY,
);
if ($possible_traced_variable_names) {
$traced_variables = [...$traced_variables, ...$possible_traced_variable_names];
}
}
}
foreach ($statements_analyzer->parsed_docblock->tags['psalm-check-type'] ?? [] as $inexact_check) {
$checked_types[] = [$inexact_check, false];
}
foreach ($statements_analyzer->parsed_docblock->tags['psalm-check-type-exact'] ?? [] as $exact_check) {
$checked_types[] = [$exact_check, true];
}
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] = $issue_type;
}
}
if ($codebase->track_unused_suppressions
&& (
(count($new_issues) === 1) // UnusedPsalmSuppress by itself should be marked as unused
|| !in_array("UnusedPsalmSuppress", $new_issues)
)
) {
foreach ($new_issues as $offset => $issue_type) {
if ($issue_type === 'InaccessibleMethod') {
continue;
}
IssueBuffer::addUnusedSuppression(
$statements_analyzer->getFilePath(),
$offset,
$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 = $codebase->config->disable_var_parsing
? []
: CommentAnalyzer::arrayToDocblocks(
$docblock,
$statements_analyzer->parsed_docblock,
$statements_analyzer->getSource(),
$statements_analyzer->getAliases(),
$template_type_map,
$file_storage->type_aliases,
);
} catch (IncorrectDocblockException $e) {
IssueBuffer::maybeAdd(
new MissingDocblockType(
$e->getMessage(),
new CodeLocation($statements_analyzer->getSource(), $stmt),
),
);
} catch (DocblockParseException $e) {
IssueBuffer::maybeAdd(
new InvalidDocblock(
$e->getMessage(),
new CodeLocation($statements_analyzer->getSource(), $stmt),
),
);
}
foreach ($var_comments as $var_comment) {
AssignmentAnalyzer::assignTypeFromVarDocblock(
$statements_analyzer,
$stmt,
$var_comment,
$context,
);
if ($var_comment->var_id === '$this'
&& $var_comment->type
&& $codebase->classExists((string)$var_comment->type)
) {
$statements_analyzer->setFQCLN((string)$var_comment->type);
}
}
}
} else {
$statements_analyzer->parsed_docblock = null;
}
if ($context->has_returned
&& !$context->collect_initializations
&& !$context->collect_mutations
&& !($stmt instanceof PhpParser\Node\Stmt\Nop)
&& !($stmt instanceof PhpParser\Node\Stmt\Function_)
&& !($stmt instanceof PhpParser\Node\Stmt\Class_)
&& !($stmt instanceof PhpParser\Node\Stmt\Interface_)
&& !($stmt instanceof PhpParser\Node\Stmt\Trait_)
&& !($stmt instanceof PhpParser\Node\Stmt\HaltCompiler)
&& !($stmt instanceof PhpParser\Node\Stmt\Declare_)
) {
if ($codebase->find_unused_variables) {
IssueBuffer::maybeAdd(
new UnevaluatedCode(
'Expressions after return/throw/continue',
new CodeLocation($statements_analyzer->source, $stmt),
),
$statements_analyzer->source->getSuppressedIssues(),
);
}
return 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_) {
if (DoAnalyzer::analyze($statements_analyzer, $stmt, $context) === false) {
return false;
}
} elseif ($stmt instanceof PhpParser\Node\Stmt\Const_) {
ConstFetchAnalyzer::analyzeConstAssignment($statements_analyzer, $stmt, $context);
} elseif ($stmt instanceof PhpParser\Node\Stmt\Unset_) {
UnsetAnalyzer::analyze($statements_analyzer, $stmt, $context);
} elseif ($stmt instanceof PhpParser\Node\Stmt\Return_) {
ReturnAnalyzer::analyze($statements_analyzer, $stmt, $context);
} elseif ($stmt instanceof PhpParser\Node\Stmt\Throw_) {
ThrowAnalyzer::analyze($statements_analyzer, $stmt, $context);
} elseif ($stmt instanceof PhpParser\Node\Stmt\Switch_) {
SwitchAnalyzer::analyze($statements_analyzer, $stmt, $context);
} elseif ($stmt instanceof PhpParser\Node\Stmt\Break_) {
BreakAnalyzer::analyze($statements_analyzer, $stmt, $context);
} elseif ($stmt instanceof PhpParser\Node\Stmt\Continue_) {
ContinueAnalyzer::analyze($statements_analyzer, $stmt, $context);
} elseif ($stmt instanceof PhpParser\Node\Stmt\Static_) {
StaticAnalyzer::analyze($statements_analyzer, $stmt, $context);
} elseif ($stmt instanceof PhpParser\Node\Stmt\Echo_) {
if (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_) {
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) {
ClassConstAnalyzer::analyzeAssignment($statements_analyzer, $stmt, $context);
} elseif ($stmt instanceof PhpParser\Node\Stmt\Class_) {
try {
$class_analyzer = new ClassAnalyzer(
$stmt,
$statements_analyzer->source,
$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\Trait_) {
TraitAnalyzer::analyze($statements_analyzer, $stmt, $context);
} 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;
}
}
if (self::dispatchAfterStatementAnalysis($stmt, $context, $statements_analyzer) === false) {
return false;
}
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])) {
IssueBuffer::maybeAdd(
new Trace(
$traced_variable . ': ' . $context->vars_in_scope[$traced_variable]->getId(),
new CodeLocation($statements_analyzer->source, $stmt),
),
$statements_analyzer->getSuppressedIssues(),
);
} else {
IssueBuffer::maybeAdd(
new UndefinedTrace(
'Attempt to trace undefined variable ' . $traced_variable,
new CodeLocation($statements_analyzer->source, $stmt),
),
$statements_analyzer->getSuppressedIssues(),
);
}
}
foreach ($checked_types as [$check_type_line, $is_exact]) {
[$checked_var, $check_type_string] = array_map('trim', explode('=', $check_type_line, 2)) + ['', ''];
if ($check_type_string === '' || $checked_var === '') {
IssueBuffer::maybeAdd(
new InvalidDocblock(
"Invalid format for @psalm-check-type" . ($is_exact ? "-exact" : ""),
new CodeLocation($statements_analyzer->source, $stmt),
),
$statements_analyzer->getSuppressedIssues(),
);
} else {
$checked_var_id = $checked_var;
$possibly_undefined = strrpos($checked_var_id, "?") === strlen($checked_var_id) - 1;
if ($possibly_undefined) {
$checked_var_id = substr($checked_var_id, 0, strlen($checked_var_id) - 1);
}
if (!isset($context->vars_in_scope[$checked_var_id])) {
IssueBuffer::maybeAdd(
new InvalidDocblock(
"Attempt to check undefined variable $checked_var_id",
new CodeLocation($statements_analyzer->source, $stmt),
),
$statements_analyzer->getSuppressedIssues(),
);
} else {
try {
$checked_type = $context->vars_in_scope[$checked_var_id];
$check_type = Type::parseString($check_type_string);
/** @psalm-suppress InaccessibleProperty We just created this type */
$check_type->possibly_undefined = $possibly_undefined;
if ($check_type->possibly_undefined !== $checked_type->possibly_undefined
|| !UnionTypeComparator::isContainedBy($codebase, $checked_type, $check_type)
|| ($is_exact && !UnionTypeComparator::isContainedBy($codebase, $check_type, $checked_type))
) {
$check_var = $checked_var_id . ($checked_type->possibly_undefined ? "?" : "");
IssueBuffer::maybeAdd(
new CheckType(
"Checked variable $checked_var = {$check_type->getId()} does not match "
. "$check_var = {$checked_type->getId()}",
new CodeLocation($statements_analyzer->source, $stmt),
),
$statements_analyzer->getSuppressedIssues(),
);
}
} catch (TypeParseTreeException $e) {
IssueBuffer::maybeAdd(
new InvalidDocblock(
$e->getMessage(),
new CodeLocation($statements_analyzer->source, $stmt),
),
$statements_analyzer->getSuppressedIssues(),
);
}
}
}
}
return null;
}
private static function dispatchAfterStatementAnalysis(
PhpParser\Node\Stmt $stmt,
Context $context,
StatementsAnalyzer $statements_analyzer
): ?bool {
$codebase = $statements_analyzer->getCodebase();
$event = new AfterStatementAnalysisEvent(
$stmt,
$context,
$statements_analyzer,
$codebase,
[],
);
if ($codebase->config->eventDispatcher->dispatchAfterStatementAnalysis($event) === false) {
return false;
}
$file_manipulations = $event->getFileReplacements();
if ($file_manipulations) {
FileManipulationBuffer::add($statements_analyzer->getFilePath(), $file_manipulations);
}
return null;
}
private static function dispatchBeforeStatementAnalysis(
PhpParser\Node\Stmt $stmt,
Context $context,
StatementsAnalyzer $statements_analyzer
): ?bool {
$codebase = $statements_analyzer->getCodebase();
$event = new BeforeStatementAnalysisEvent(
$stmt,
$context,
$statements_analyzer,
$codebase,
[],
);
if ($codebase->config->eventDispatcher->dispatchBeforeStatementAnalysis($event) === false) {
return false;
}
$file_manipulations = $event->getFileReplacements();
if ($file_manipulations) {
FileManipulationBuffer::add($statements_analyzer->getFilePath(), $file_manipulations);
}
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) {
IssueBuffer::maybeAdd(
new InvalidDocblock(
$e->getMessage(),
new CodeLocation($this->getSource(), $stmt, null, true),
),
);
$this->parsed_docblock = null;
}
$comments = $this->parsed_docblock;
if (isset($comments->tags['psalm-scope-this'])) {
$trimmed = trim(reset($comments->tags['psalm-scope-this']));
$scope_fqcn = Type::getFQCLNFromString($trimmed, $this->getAliases());
if (!$codebase->classExists($scope_fqcn)) {
IssueBuffer::maybeAdd(
new UndefinedDocblockClass(
'Scope class ' . $scope_fqcn . ' does not exist',
new CodeLocation($this->getSource(), $stmt, null, true),
$scope_fqcn,
),
);
} else {
$this_type = Type::parseString($scope_fqcn);
$context->self = $scope_fqcn;
$context->vars_in_scope['$this'] = $this_type;
$this->setFQCLN($scope_fqcn);
}
}
}
/**
* @param array<PhpParser\Node\Stmt> $stmts
*/
public function checkUnreferencedVars(array $stmts, Context $context): 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 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) {
IssueBuffer::maybeAdd(
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(),
);
} elseif ($source instanceof MethodAnalyzer) {
IssueBuffer::maybeAdd(
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(),
);
}
}
}
foreach ($this->unused_var_locations as [$var_id, $original_location]) {
if (strpos($var_id, '$_') === 0) {
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])
&& !isset($context->referenced_globals[$var_id])
&& !VariableFetchAnalyzer::isSuperGlobal($var_id)
&& $this->data_flow_graph instanceof VariableUseGraph
&& !$this->data_flow_graph->isVariableUsed($assignment_node)
) {
$is_foreach_var = false;
if (isset($this->foreach_var_locations[$var_id])) {
foreach ($this->foreach_var_locations[$var_id] as $location) {
if ($location->raw_file_start === $original_location->raw_file_start) {
$is_foreach_var = true;
break;
}
}
}
if ($is_foreach_var) {
$issue = new UnusedForeachValue(
$var_id . ' is never referenced or the value is not used',
$original_location,
);
} else {
$issue = new UnusedVariable(
$var_id . ' is never referenced or the value is not used',
$original_location,
);
}
if ($codebase->alter_code
&& $issue instanceof UnusedVariable
&& !$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,
);
}
IssueBuffer::maybeAdd(
$issue,
$this->getSuppressedIssues(),
$issue instanceof UnusedVariable,
);
}
}
}
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 = $stmt_type->addParentNodes([$use_node->id => $use_node]);
$this->node_data->setType($stmt, $stmt_type);
}
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 $this->all_vars[$var_id] ?? null;
}
public function getBranchPoint(string $var_id): ?int
{
return $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, true> $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)
|| $this->codebase->interfaceExtends($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;
}
/** @psalm-mutation-free */
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 NodeDataProvider
*/
public function getNodeTypeProvider(): NodeTypeProvider
{
return $this->node_data;
}
public function getFullyQualifiedFunctionMethodOrNamespaceName(): ?string
{
if ($this->source instanceof MethodAnalyzer) {
$fqcn = $this->getFQCLN();
$method_name = $this->source->getFunctionLikeStorage($this)->cased_name;
assert($fqcn !== null && $method_name !== null);
return "$fqcn::$method_name";
}
if ($this->source instanceof FunctionAnalyzer) {
$namespace = $this->getNamespace();
$namespace = $namespace === "" ? "" : "$namespace\\";
$function_name = $this->source->getFunctionLikeStorage($this)->cased_name;
assert($function_name !== null);
return "{$namespace}{$function_name}";
}
return $this->getNamespace();
}
}