<?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\DeclareAnalyzer;
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\Internal\Type\TypeParser;
use Psalm\Internal\Type\TypeTokenizer;
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
 */
final 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_) {
            DeclareAnalyzer::analyze($statements_analyzer, $stmt, $context);
        } 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];

                        $path = $statements_analyzer->getRootFilePath();
                        $file_storage = $codebase->file_storage_provider->get($path);

                        $check_tokens = TypeTokenizer::getFullyQualifiedTokens(
                            $check_type_string,
                            $statements_analyzer->getAliases(),
                            $statements_analyzer->getTemplateTypeMap(),
                            $file_storage->type_aliases,
                        );
                        $check_type = TypeParser::parseTokens(
                            $check_tokens,
                            null,
                            $statements_analyzer->getTemplateTypeMap() ?? [],
                            $file_storage->type_aliases,
                            true,
                        );
                        /** @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;
        }

        if ($this->parsed_docblock === null) {
            try {
                $this->parsed_docblock = DocComment::parsePreservingLength($docblock, true);
            } catch (DocblockParseException $e) {
                // already reported above
            }
        }

        $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 function’s 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 method’s 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), true);
                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();
    }
}
