<commit_before><?php

namespace Psalm\Internal\Cli;

use Composer\Autoload\ClassLoader;
use Psalm\Config;
use Psalm\ErrorBaseline;
use Psalm\Exception\ConfigException;
use Psalm\Internal\Analyzer\ProjectAnalyzer;
use Psalm\Internal\CliUtils;
use Psalm\Internal\Composer;
use Psalm\Internal\ErrorHandler;
use Psalm\Internal\IncludeCollector;
use Psalm\Internal\Provider;
use Psalm\IssueBuffer;
use Psalm\Progress\DebugProgress;
use Psalm\Progress\DefaultProgress;
use Psalm\Progress\LongProgress;
use Psalm\Progress\Progress;
use Psalm\Progress\VoidProgress;
use Psalm\Report\ReportOptions;
use RuntimeException;
use Webmozart\PathUtil\Path;

use function array_filter;
use function array_key_exists;
use function array_map;
use function array_merge;
use function array_slice;
use function array_sum;
use function array_values;
use function chdir;
use function count;
use function file_exists;
use function file_put_contents;
use function fwrite;
use function gc_collect_cycles;
use function gc_disable;
use function getcwd;
use function getopt;
use function implode;
use function in_array;
use function ini_get;
use function ini_set;
use function is_array;
use function is_numeric;
use function is_scalar;
use function is_string;
use function json_encode;
use function max;
use function microtime;
use function preg_match;
use function preg_replace;
use function realpath;
use function setlocale;
use function str_repeat;
use function strpos;
use function substr;
use function version_compare;

use const DIRECTORY_SEPARATOR;
use const LC_CTYPE;
use const PHP_EOL;
use const PHP_OS;
use const PHP_VERSION;
use const STDERR;

// phpcs:disable PSR1.Files.SideEffects

require_once __DIR__ . '/../ErrorHandler.php';
require_once __DIR__ . '/../CliUtils.php';
require_once __DIR__ . '/../Composer.php';
require_once __DIR__ . '/../IncludeCollector.php';
require_once __DIR__ . '/../../IssueBuffer.php';

final class Psalm
{
    private const SHORT_OPTIONS = [
        'f:',
        'm',
        'h',
        'v',
        'c:',
        'i',
        'r:',
    ];

    private const LONG_OPTIONS = [
        'clear-cache',
        'clear-global-cache',
        'config:',
        'debug',
        'debug-by-line',
        'debug-performance',
        'debug-emitted-issues',
        'diff',
        'disable-extension:',
        'find-dead-code::',
        'find-unused-code::',
        'find-unused-variables',
        'find-references-to:',
        'help',
        'ignore-baseline',
        'init',
        'memory-limit:',
        'monochrome',
        'no-diff',
        'no-cache',
        'no-reflection-cache',
        'no-file-cache',
        'output-format:',
        'plugin:',
        'report:',
        'report-show-info:',
        'root:',
        'set-baseline:',
        'show-info:',
        'show-snippet:',
        'stats',
        'threads:',
        'update-baseline',
        'use-baseline:',
        'use-ini-defaults',
        'version',
        'php-version:',
        'generate-json-map:',
        'generate-stubs:',
        'alter',
        'language-server',
        'refactor',
        'shepherd::',
        'no-progress',
        'long-progress',
        'no-suggestions',
        'include-php-versions', // used for baseline
        'pretty-print', // used for JSON reports
        'track-tainted-input',
        'taint-analysis',
        'security-analysis',
        'dump-taint-graph:',
        'find-unused-psalm-suppress',
        'error-level:',
    ];

    /**
     * @param array<int,string> $argv
     */
    public static function run(array $argv): void
    {
        gc_collect_cycles();
        gc_disable();

        ErrorHandler::install();

        $args = array_slice($argv, 1);

        // get options from command line
        $options = getopt(implode('', self::SHORT_OPTIONS), self::LONG_OPTIONS);
        if (false === $options) {
            throw new RuntimeException('Failed to parse CLI options');
        }

        self::forwardCliCall($options, $argv);

        self::validateCliArguments($args);

        self::setMemoryLimit($options);

        self::syncShortOptions($options);

        if (isset($options['c']) && is_array($options['c'])) {
            fwrite(STDERR, 'Too many config files provided' . PHP_EOL);
            exit(1);
        }


        if (array_key_exists('h', $options)) {
            echo CliUtils::getPsalmHelpText();
            /*
            --shepherd[=host]
                Send data to Shepherd, Psalm's GitHub integration tool.
                `host` is the location of the Shepherd server. It defaults to shepherd.dev
                More information is available at https://psalm.dev/shepherd
            */

            exit;
        }

        $current_dir = self::getCurrentDir($options);

        $path_to_config = CliUtils::getPathToConfig($options);

        $vendor_dir = CliUtils::getVendorDir($current_dir);

        // capture environment before registering autoloader (it may destroy it)
        IssueBuffer::captureServer($_SERVER);

        $include_collector = new IncludeCollector();
        $first_autoloader = $include_collector->runAndCollect(
            function () use ($current_dir, $options, $vendor_dir): ?\Composer\Autoload\ClassLoader {
                return CliUtils::requireAutoloaders($current_dir, isset($options['r']), $vendor_dir);
            }
        );

        $run_taint_analysis = self::shouldRunTaintAnalysis($options);

        if (array_key_exists('v', $options)) {
            echo 'Psalm ' . PSALM_VERSION . PHP_EOL;
            exit;
        }

        $output_format = isset($options['output-format']) && is_string($options['output-format'])
            ? $options['output-format']
            : \Psalm\Report::TYPE_CONSOLE;

        [$config, $init_source_dir] = self::initConfig(
            $current_dir,
            $args,
            $vendor_dir,
            $first_autoloader,
            $path_to_config,
            $output_format,
            $run_taint_analysis,
            $options
        );

        $config->setIncludeCollector($include_collector);

        $in_ci = self::runningInCI();        // disable progressbar on CI

        if ($in_ci) {
            $options['long-progress'] = true;
        }

        $threads = self::detectThreads($options, $in_ci);

        self::emitMacPcreWarning($options, $threads);

        self::restart($options, $config, $threads);

        if (isset($options['debug-emitted-issues'])) {
            $config->debug_emitted_issues = true;
        }


        setlocale(LC_CTYPE, 'C');

        if (isset($options['set-baseline'])) {
            if (is_array($options['set-baseline'])) {
                die('Only one baseline file can be created at a time' . PHP_EOL);
            }
        }

        $paths_to_check = CliUtils::getPathsToCheck(isset($options['f']) ? $options['f'] : null);

        if ($config->resolve_from_config_file) {
            $current_dir = $config->base_dir;
            chdir($current_dir);
        }

        $plugins = [];

        if (isset($options['plugin'])) {
            $plugins = $options['plugin'];

            if (!is_array($plugins)) {
                $plugins = [$plugins];
            }
        }



        $show_info = isset($options['show-info'])
            ? $options['show-info'] === 'true' || $options['show-info'] === '1'
            : false;

        $is_diff = !isset($options['no-diff'])
            && !isset($options['set-baseline'])
            && !isset($options['update-baseline']);

        $find_unused_code = self::shouldFindUnusedCode($options, $config);

        $find_unused_variables = isset($options['find-unused-variables']);

        $find_references_to = isset($options['find-references-to']) && is_string($options['find-references-to'])
            ? $options['find-references-to']
            : null;

        if (isset($options['shepherd'])) {
            if (is_string($options['shepherd'])) {
                $config->shepherd_host = $options['shepherd'];
            }

            $shepherd_plugin = Path::canonicalize(__DIR__ . '/../../Plugin/Shepherd.php');

            if (!file_exists($shepherd_plugin)) {
                die('Could not find Shepherd plugin location ' . $shepherd_plugin . PHP_EOL);
            }

            $plugins[] = $shepherd_plugin;
        }

        if (isset($options['clear-cache'])) {
            self::clearCache($config);
        }

        if (isset($options['clear-global-cache'])) {
            self::clearGlobalCache($config);
        }

        $progress = self::initProgress($options, $config);
        $providers = self::initProviders($options, $config, $current_dir);

        $stdout_report_options = self::initStdoutReportOptions($options, $show_info, $output_format);

        /** @var list<string>|string $report_file_paths type guaranteed by argument to getopt() */
        $report_file_paths = $options['report'] ?? [];
        if (is_string($report_file_paths)) {
            $report_file_paths = [$report_file_paths];
        }

        $project_analyzer = new ProjectAnalyzer(
            $config,
            $providers,
            $stdout_report_options,
            ProjectAnalyzer::getFileReportOptions(
                $report_file_paths,
                isset($options['report-show-info'])
                    ? $options['report-show-info'] !== 'false' && $options['report-show-info'] !== '0'
                    : true
            ),
            $threads,
            $progress
        );

        self::initPhpVersion($options, $config, $project_analyzer);


        $start_time = microtime(true);

        self::configureProjectAnalyzer(
            $options,
            $config,
            $project_analyzer,
            $find_references_to,
            $find_unused_code,
            $find_unused_variables,
            $run_taint_analysis
        );

        if ($config->run_taint_analysis || $run_taint_analysis) {
            $is_diff = false;
        }

        /** @var string $plugin_path */
        foreach ($plugins as $plugin_path) {
            $config->addPluginPath($plugin_path);
        }

        if ($paths_to_check === null) {
            $project_analyzer->check($current_dir, $is_diff);
        } elseif ($paths_to_check) {
            $project_analyzer->checkPaths($paths_to_check);
        }

        if ($find_references_to) {
            $project_analyzer->findReferencesTo($find_references_to);
        }

        self::storeFlowGraph($options, $project_analyzer);

        if (isset($options['generate-json-map']) && is_string($options['generate-json-map'])) {
            self::storeTypeMap($providers, $config, $options['generate-json-map']);
        }

        if (isset($options['generate-stubs'])) {
            self::generateStubs($options, $providers, $project_analyzer);
        }

        if (!isset($options['i'])) {
            IssueBuffer::finish(
                $project_analyzer,
                !$paths_to_check,
                $start_time,
                isset($options['stats']),
                self::initBaseline($options, $config, $current_dir, $path_to_config)
            );
        } else {
            self::autoGenerateConfig($project_analyzer, $current_dir, $init_source_dir, $vendor_dir);
        }
    }

    /**
     * @param array<int,string> $args
     */
    private static function validateCliArguments(array $args): void
    {
        array_map(
            /**
             * @param string $arg
             */
            function ($arg): void {
                if (substr($arg, 0, 2) === '--' && $arg !== '--') {
                    $arg_name = preg_replace('/=.*$/', '', substr($arg, 2));

                    if (!in_array($arg_name, self::LONG_OPTIONS)
                        && !in_array($arg_name . ':', self::LONG_OPTIONS)
                        && !in_array($arg_name . '::', self::LONG_OPTIONS)
                    ) {
                        fwrite(
                            STDERR,
                            'Unrecognised argument "--' . $arg_name . '"' . PHP_EOL
                            . 'Type --help to see a list of supported arguments'. PHP_EOL
                        );
                        exit(1);
                    }
                } elseif (substr($arg, 0, 1) === '-' && $arg !== '-' && $arg !== '--') {
                    $arg_name = preg_replace('/=.*$/', '', substr($arg, 1));

                    if (!in_array($arg_name, self::SHORT_OPTIONS)
                        && !in_array($arg_name . ':', self::SHORT_OPTIONS)
                    ) {
                        fwrite(
                            STDERR,
                            'Unrecognised argument "-' . $arg_name . '"' . PHP_EOL
                            . 'Type --help to see a list of supported arguments'. PHP_EOL
                        );
                        exit(1);
                    }
                }
            },
            $args
        );
    }

    /**
     * @param array<string,string|false|list<mixed>> $options
     */
    private static function setMemoryLimit($options): void
    {
        if (!array_key_exists('use-ini-defaults', $options)) {
            ini_set('display_errors', 'stderr');
            ini_set('display_startup_errors', '1');

            $memoryLimit = (8 * 1024 * 1024 * 1024);

            if (array_key_exists('memory-limit', $options)) {
                $memoryLimit = $options['memory-limit'];

                if (!is_scalar($memoryLimit)) {
                    throw new ConfigException('Invalid memory limit specified.');
                }
            }

            ini_set('memory_limit', (string) $memoryLimit);
        }
    }

    /**
     * @param array<int, string> $args
     */
    private static function generateConfig(string $current_dir, array &$args): void
    {
        if (file_exists($current_dir . 'psalm.xml')) {
            die('A config file already exists in the current directory' . PHP_EOL);
        }

        $args = array_values(array_filter(
            $args,
            function (string $arg): bool {
                return $arg !== '--ansi'
                    && $arg !== '--no-ansi'
                    && $arg !== '-i'
                    && $arg !== '--init'
                    && $arg !== '--debug'
                    && $arg !== '--debug-by-line'
                    && $arg !== '--debug-emitted-issues'
                    && strpos($arg, '--disable-extension=') !== 0
                    && strpos($arg, '--root=') !== 0
                    && strpos($arg, '--r=') !== 0;
            }
        ));

        $init_level = null;
        $init_source_dir = null;
        if (count($args)) {
            if (count($args) > 2) {
                die('Too many arguments provided for psalm --init' . PHP_EOL);
            }

            if (isset($args[1])) {
                if (!preg_match('/^[1-8]$/', $args[1])) {
                    die('Config strictness must be a number between 1 and 8 inclusive' . PHP_EOL);
                }

                $init_level = (int)$args[1];
            }

            $init_source_dir = $args[0];
        }

        $vendor_dir = CliUtils::getVendorDir($current_dir);

        if (null !== $init_level) {
            try {
                $template_contents = \Psalm\Config\Creator::getContents(
                    $current_dir,
                    $init_source_dir,
                    $init_level,
                    $vendor_dir
                );
            } catch (\Psalm\Exception\ConfigCreationException $e) {
                die($e->getMessage() . PHP_EOL);
            }

            if (!file_put_contents($current_dir . 'psalm.xml', $template_contents)) {
                die('Could not write to psalm.xml' . PHP_EOL);
            }

            exit('Config file created successfully. Please re-run psalm.' . PHP_EOL);
        }
    }

    private static function loadConfig(
        ?string $path_to_config,
        string $current_dir,
        string $output_format,
        ?ClassLoader $first_autoloader,
        bool $run_taint_analysis,
        array $options
    ): Config {
        $config = CliUtils::initializeConfig(
            $path_to_config,
            $current_dir,
            $output_format,
            $first_autoloader,
            $run_taint_analysis
        );

        if (isset($options['error-level'])
            && is_numeric($options['error-level'])
        ) {
            $config_level = (int) $options['error-level'];

            if (!in_array($config_level, [1, 2, 3, 4, 5, 6, 7, 8], true)) {
                throw new \Psalm\Exception\ConfigException(
                    'Invalid error level ' . $config_level
                );
            }

            $config->level = $config_level;
        }
        return $config;
    }

    private static function initProgress(array $options, Config $config): Progress
    {
        $debug = array_key_exists('debug', $options) || array_key_exists('debug-by-line', $options);

        $show_info = isset($options['show-info'])
            ? $options['show-info'] === 'true' || $options['show-info'] === '1'
            : false;

        if ($debug) {
            $progress = new DebugProgress();
        } elseif (isset($options['no-progress'])) {
            $progress = new VoidProgress();
        } else {
            $show_errors = !$config->error_baseline || isset($options['ignore-baseline']);
            if (isset($options['long-progress'])) {
                $progress = new LongProgress($show_errors, $show_info);
            } else {
                $progress = new DefaultProgress($show_errors, $show_info);
            }
        }
        return $progress;
    }

    private static function initProviders(array $options, Config $config, string $current_dir): Provider\Providers
    {
        if (isset($options['no-cache']) || isset($options['i'])) {
            $providers = new Provider\Providers(
                new Provider\FileProvider
            );
        } else {
            $no_reflection_cache = isset($options['no-reflection-cache']);
            $no_file_cache = isset($options['no-file-cache']);

            $file_storage_cache_provider = $no_reflection_cache
                ? null
                : new Provider\FileStorageCacheProvider($config);

            $classlike_storage_cache_provider = $no_reflection_cache
                ? null
                : new Provider\ClassLikeStorageCacheProvider($config);

            $providers = new Provider\Providers(
                new Provider\FileProvider,
                new Provider\ParserCacheProvider($config, !$no_file_cache),
                $file_storage_cache_provider,
                $classlike_storage_cache_provider,
                new Provider\FileReferenceCacheProvider($config),
                new Provider\ProjectCacheProvider(Composer::getLockFilePath($current_dir))
            );
        }
        return $providers;
    }

    /**
     * @param array{set-baseline:string} $options
     * @return array<string,array<string,array{o:int, s: list<string>}>>
     */
    private static function generateBaseline(
        array $options,
        Config $config,
        string $current_dir,
        ?string $path_to_config
    ): array {
        fwrite(STDERR, 'Writing error baseline to file...' . PHP_EOL);

        $issue_baseline = ErrorBaseline::read(
            new \Psalm\Internal\Provider\FileProvider,
            $options['set-baseline']
        );

        ErrorBaseline::create(
            new \Psalm\Internal\Provider\FileProvider,
            $options['set-baseline'],
            IssueBuffer::getIssuesData(),
            $config->include_php_versions_in_error_baseline || isset($options['include-php-versions'])
        );

        fwrite(STDERR, "Baseline saved to {$options['set-baseline']}.");

        CliUtils::updateConfigFile(
            $config,
            $path_to_config ?? $current_dir,
            $options['set-baseline']
        );

        fwrite(STDERR, PHP_EOL);

        return $issue_baseline;
    }

    /**
     * @return array<string,array<string,array{o:int, s: list<string>}>>
     */
    private static function updateBaseline(array $options, Config $config): array
    {<commit_msg>Fix #5960 - catch exception when baseline cannot be located<commit_after><?php

namespace Psalm\Internal\Cli;

use Composer\Autoload\ClassLoader;
use Psalm\Config;
use Psalm\ErrorBaseline;
use Psalm\Exception\ConfigException;
use Psalm\Internal\Analyzer\ProjectAnalyzer;
use Psalm\Internal\CliUtils;
use Psalm\Internal\Composer;
use Psalm\Internal\ErrorHandler;
use Psalm\Internal\IncludeCollector;
use Psalm\Internal\Provider;
use Psalm\IssueBuffer;
use Psalm\Progress\DebugProgress;
use Psalm\Progress\DefaultProgress;
use Psalm\Progress\LongProgress;
use Psalm\Progress\Progress;
use Psalm\Progress\VoidProgress;
use Psalm\Report\ReportOptions;
use RuntimeException;
use Webmozart\PathUtil\Path;

use function array_filter;
use function array_key_exists;
use function array_map;
use function array_merge;
use function array_slice;
use function array_sum;
use function array_values;
use function chdir;
use function count;
use function file_exists;
use function file_put_contents;
use function fwrite;
use function gc_collect_cycles;
use function gc_disable;
use function getcwd;
use function getopt;
use function implode;
use function in_array;
use function ini_get;
use function ini_set;
use function is_array;
use function is_numeric;
use function is_scalar;
use function is_string;
use function json_encode;
use function max;
use function microtime;
use function preg_match;
use function preg_replace;
use function realpath;
use function setlocale;
use function str_repeat;
use function strpos;
use function substr;
use function version_compare;

use const DIRECTORY_SEPARATOR;
use const LC_CTYPE;
use const PHP_EOL;
use const PHP_OS;
use const PHP_VERSION;
use const STDERR;

// phpcs:disable PSR1.Files.SideEffects

require_once __DIR__ . '/../ErrorHandler.php';
require_once __DIR__ . '/../CliUtils.php';
require_once __DIR__ . '/../Composer.php';
require_once __DIR__ . '/../IncludeCollector.php';
require_once __DIR__ . '/../../IssueBuffer.php';

final class Psalm
{
    private const SHORT_OPTIONS = [
        'f:',
        'm',
        'h',
        'v',
        'c:',
        'i',
        'r:',
    ];

    private const LONG_OPTIONS = [
        'clear-cache',
        'clear-global-cache',
        'config:',
        'debug',
        'debug-by-line',
        'debug-performance',
        'debug-emitted-issues',
        'diff',
        'disable-extension:',
        'find-dead-code::',
        'find-unused-code::',
        'find-unused-variables',
        'find-references-to:',
        'help',
        'ignore-baseline',
        'init',
        'memory-limit:',
        'monochrome',
        'no-diff',
        'no-cache',
        'no-reflection-cache',
        'no-file-cache',
        'output-format:',
        'plugin:',
        'report:',
        'report-show-info:',
        'root:',
        'set-baseline:',
        'show-info:',
        'show-snippet:',
        'stats',
        'threads:',
        'update-baseline',
        'use-baseline:',
        'use-ini-defaults',
        'version',
        'php-version:',
        'generate-json-map:',
        'generate-stubs:',
        'alter',
        'language-server',
        'refactor',
        'shepherd::',
        'no-progress',
        'long-progress',
        'no-suggestions',
        'include-php-versions', // used for baseline
        'pretty-print', // used for JSON reports
        'track-tainted-input',
        'taint-analysis',
        'security-analysis',
        'dump-taint-graph:',
        'find-unused-psalm-suppress',
        'error-level:',
    ];

    /**
     * @param array<int,string> $argv
     */
    public static function run(array $argv): void
    {
        gc_collect_cycles();
        gc_disable();

        ErrorHandler::install();

        $args = array_slice($argv, 1);

        // get options from command line
        $options = getopt(implode('', self::SHORT_OPTIONS), self::LONG_OPTIONS);
        if (false === $options) {
            throw new RuntimeException('Failed to parse CLI options');
        }

        self::forwardCliCall($options, $argv);

        self::validateCliArguments($args);

        self::setMemoryLimit($options);

        self::syncShortOptions($options);

        if (isset($options['c']) && is_array($options['c'])) {
            fwrite(STDERR, 'Too many config files provided' . PHP_EOL);
            exit(1);
        }


        if (array_key_exists('h', $options)) {
            echo CliUtils::getPsalmHelpText();
            /*
            --shepherd[=host]
                Send data to Shepherd, Psalm's GitHub integration tool.
                `host` is the location of the Shepherd server. It defaults to shepherd.dev
                More information is available at https://psalm.dev/shepherd
            */

            exit;
        }

        $current_dir = self::getCurrentDir($options);

        $path_to_config = CliUtils::getPathToConfig($options);

        $vendor_dir = CliUtils::getVendorDir($current_dir);

        // capture environment before registering autoloader (it may destroy it)
        IssueBuffer::captureServer($_SERVER);

        $include_collector = new IncludeCollector();
        $first_autoloader = $include_collector->runAndCollect(
            function () use ($current_dir, $options, $vendor_dir): ?\Composer\Autoload\ClassLoader {
                return CliUtils::requireAutoloaders($current_dir, isset($options['r']), $vendor_dir);
            }
        );

        $run_taint_analysis = self::shouldRunTaintAnalysis($options);

        if (array_key_exists('v', $options)) {
            echo 'Psalm ' . PSALM_VERSION . PHP_EOL;
            exit;
        }

        $output_format = isset($options['output-format']) && is_string($options['output-format'])
            ? $options['output-format']
            : \Psalm\Report::TYPE_CONSOLE;

        [$config, $init_source_dir] = self::initConfig(
            $current_dir,
            $args,
            $vendor_dir,
            $first_autoloader,
            $path_to_config,
            $output_format,
            $run_taint_analysis,
            $options
        );

        $config->setIncludeCollector($include_collector);

        $in_ci = self::runningInCI();        // disable progressbar on CI

        if ($in_ci) {
            $options['long-progress'] = true;
        }

        $threads = self::detectThreads($options, $in_ci);

        self::emitMacPcreWarning($options, $threads);

        self::restart($options, $config, $threads);

        if (isset($options['debug-emitted-issues'])) {
            $config->debug_emitted_issues = true;
        }


        setlocale(LC_CTYPE, 'C');

        if (isset($options['set-baseline'])) {
            if (is_array($options['set-baseline'])) {
                die('Only one baseline file can be created at a time' . PHP_EOL);
            }
        }

        $paths_to_check = CliUtils::getPathsToCheck(isset($options['f']) ? $options['f'] : null);

        if ($config->resolve_from_config_file) {
            $current_dir = $config->base_dir;
            chdir($current_dir);
        }

        $plugins = [];

        if (isset($options['plugin'])) {
            $plugins = $options['plugin'];

            if (!is_array($plugins)) {
                $plugins = [$plugins];
            }
        }



        $show_info = isset($options['show-info'])
            ? $options['show-info'] === 'true' || $options['show-info'] === '1'
            : false;

        $is_diff = !isset($options['no-diff'])
            && !isset($options['set-baseline'])
            && !isset($options['update-baseline']);

        $find_unused_code = self::shouldFindUnusedCode($options, $config);

        $find_unused_variables = isset($options['find-unused-variables']);

        $find_references_to = isset($options['find-references-to']) && is_string($options['find-references-to'])
            ? $options['find-references-to']
            : null;

        if (isset($options['shepherd'])) {
            if (is_string($options['shepherd'])) {
                $config->shepherd_host = $options['shepherd'];
            }

            $shepherd_plugin = Path::canonicalize(__DIR__ . '/../../Plugin/Shepherd.php');

            if (!file_exists($shepherd_plugin)) {
                die('Could not find Shepherd plugin location ' . $shepherd_plugin . PHP_EOL);
            }

            $plugins[] = $shepherd_plugin;
        }

        if (isset($options['clear-cache'])) {
            self::clearCache($config);
        }

        if (isset($options['clear-global-cache'])) {
            self::clearGlobalCache($config);
        }

        $progress = self::initProgress($options, $config);
        $providers = self::initProviders($options, $config, $current_dir);

        $stdout_report_options = self::initStdoutReportOptions($options, $show_info, $output_format);

        /** @var list<string>|string $report_file_paths type guaranteed by argument to getopt() */
        $report_file_paths = $options['report'] ?? [];
        if (is_string($report_file_paths)) {
            $report_file_paths = [$report_file_paths];
        }

        $project_analyzer = new ProjectAnalyzer(
            $config,
            $providers,
            $stdout_report_options,
            ProjectAnalyzer::getFileReportOptions(
                $report_file_paths,
                isset($options['report-show-info'])
                    ? $options['report-show-info'] !== 'false' && $options['report-show-info'] !== '0'
                    : true
            ),
            $threads,
            $progress
        );

        self::initPhpVersion($options, $config, $project_analyzer);


        $start_time = microtime(true);

        self::configureProjectAnalyzer(
            $options,
            $config,
            $project_analyzer,
            $find_references_to,
            $find_unused_code,
            $find_unused_variables,
            $run_taint_analysis
        );

        if ($config->run_taint_analysis || $run_taint_analysis) {
            $is_diff = false;
        }

        /** @var string $plugin_path */
        foreach ($plugins as $plugin_path) {
            $config->addPluginPath($plugin_path);
        }

        if ($paths_to_check === null) {
            $project_analyzer->check($current_dir, $is_diff);
        } elseif ($paths_to_check) {
            $project_analyzer->checkPaths($paths_to_check);
        }

        if ($find_references_to) {
            $project_analyzer->findReferencesTo($find_references_to);
        }

        self::storeFlowGraph($options, $project_analyzer);

        if (isset($options['generate-json-map']) && is_string($options['generate-json-map'])) {
            self::storeTypeMap($providers, $config, $options['generate-json-map']);
        }

        if (isset($options['generate-stubs'])) {
            self::generateStubs($options, $providers, $project_analyzer);
        }

        if (!isset($options['i'])) {
            IssueBuffer::finish(
                $project_analyzer,
                !$paths_to_check,
                $start_time,
                isset($options['stats']),
                self::initBaseline($options, $config, $current_dir, $path_to_config)
            );
        } else {
            self::autoGenerateConfig($project_analyzer, $current_dir, $init_source_dir, $vendor_dir);
        }
    }

    /**
     * @param array<int,string> $args
     */
    private static function validateCliArguments(array $args): void
    {
        array_map(
            /**
             * @param string $arg
             */
            function ($arg): void {
                if (substr($arg, 0, 2) === '--' && $arg !== '--') {
                    $arg_name = preg_replace('/=.*$/', '', substr($arg, 2));

                    if (!in_array($arg_name, self::LONG_OPTIONS)
                        && !in_array($arg_name . ':', self::LONG_OPTIONS)
                        && !in_array($arg_name . '::', self::LONG_OPTIONS)
                    ) {
                        fwrite(
                            STDERR,
                            'Unrecognised argument "--' . $arg_name . '"' . PHP_EOL
                            . 'Type --help to see a list of supported arguments'. PHP_EOL
                        );
                        exit(1);
                    }
                } elseif (substr($arg, 0, 1) === '-' && $arg !== '-' && $arg !== '--') {
                    $arg_name = preg_replace('/=.*$/', '', substr($arg, 1));

                    if (!in_array($arg_name, self::SHORT_OPTIONS)
                        && !in_array($arg_name . ':', self::SHORT_OPTIONS)
                    ) {
                        fwrite(
                            STDERR,
                            'Unrecognised argument "-' . $arg_name . '"' . PHP_EOL
                            . 'Type --help to see a list of supported arguments'. PHP_EOL
                        );
                        exit(1);
                    }
                }
            },
            $args
        );
    }

    /**
     * @param array<string,string|false|list<mixed>> $options
     */
    private static function setMemoryLimit($options): void
    {
        if (!array_key_exists('use-ini-defaults', $options)) {
            ini_set('display_errors', 'stderr');
            ini_set('display_startup_errors', '1');

            $memoryLimit = (8 * 1024 * 1024 * 1024);

            if (array_key_exists('memory-limit', $options)) {
                $memoryLimit = $options['memory-limit'];

                if (!is_scalar($memoryLimit)) {
                    throw new ConfigException('Invalid memory limit specified.');
                }
            }

            ini_set('memory_limit', (string) $memoryLimit);
        }
    }

    /**
     * @param array<int, string> $args
     */
    private static function generateConfig(string $current_dir, array &$args): void
    {
        if (file_exists($current_dir . 'psalm.xml')) {
            die('A config file already exists in the current directory' . PHP_EOL);
        }

        $args = array_values(array_filter(
            $args,
            function (string $arg): bool {
                return $arg !== '--ansi'
                    && $arg !== '--no-ansi'
                    && $arg !== '-i'
                    && $arg !== '--init'
                    && $arg !== '--debug'
                    && $arg !== '--debug-by-line'
                    && $arg !== '--debug-emitted-issues'
                    && strpos($arg, '--disable-extension=') !== 0
                    && strpos($arg, '--root=') !== 0
                    && strpos($arg, '--r=') !== 0;
            }
        ));

        $init_level = null;
        $init_source_dir = null;
        if (count($args)) {
            if (count($args) > 2) {
                die('Too many arguments provided for psalm --init' . PHP_EOL);
            }

            if (isset($args[1])) {
                if (!preg_match('/^[1-8]$/', $args[1])) {
                    die('Config strictness must be a number between 1 and 8 inclusive' . PHP_EOL);
                }

                $init_level = (int)$args[1];
            }

            $init_source_dir = $args[0];
        }

        $vendor_dir = CliUtils::getVendorDir($current_dir);

        if (null !== $init_level) {
            try {
                $template_contents = \Psalm\Config\Creator::getContents(
                    $current_dir,
                    $init_source_dir,
                    $init_level,
                    $vendor_dir
                );
            } catch (\Psalm\Exception\ConfigCreationException $e) {
                die($e->getMessage() . PHP_EOL);
            }

            if (!file_put_contents($current_dir . 'psalm.xml', $template_contents)) {
                die('Could not write to psalm.xml' . PHP_EOL);
            }

            exit('Config file created successfully. Please re-run psalm.' . PHP_EOL);
        }
    }

    private static function loadConfig(
        ?string $path_to_config,
        string $current_dir,
        string $output_format,
        ?ClassLoader $first_autoloader,
        bool $run_taint_analysis,
        array $options
    ): Config {
        $config = CliUtils::initializeConfig(
            $path_to_config,
            $current_dir,
            $output_format,
            $first_autoloader,
            $run_taint_analysis
        );

        if (isset($options['error-level'])
            && is_numeric($options['error-level'])
        ) {
            $config_level = (int) $options['error-level'];

            if (!in_array($config_level, [1, 2, 3, 4, 5, 6, 7, 8], true)) {
                throw new \Psalm\Exception\ConfigException(
                    'Invalid error level ' . $config_level
                );
            }

            $config->level = $config_level;
        }
        return $config;
    }

    private static function initProgress(array $options, Config $config): Progress
    {
        $debug = array_key_exists('debug', $options) || array_key_exists('debug-by-line', $options);

        $show_info = isset($options['show-info'])
            ? $options['show-info'] === 'true' || $options['show-info'] === '1'
            : false;

        if ($debug) {
            $progress = new DebugProgress();
        } elseif (isset($options['no-progress'])) {
            $progress = new VoidProgress();
        } else {
            $show_errors = !$config->error_baseline || isset($options['ignore-baseline']);
            if (isset($options['long-progress'])) {
                $progress = new LongProgress($show_errors, $show_info);
            } else {
                $progress = new DefaultProgress($show_errors, $show_info);
            }
        }
        return $progress;
    }

    private static function initProviders(array $options, Config $config, string $current_dir): Provider\Providers
    {
        if (isset($options['no-cache']) || isset($options['i'])) {
            $providers = new Provider\Providers(
                new Provider\FileProvider
            );
        } else {
            $no_reflection_cache = isset($options['no-reflection-cache']);
            $no_file_cache = isset($options['no-file-cache']);

            $file_storage_cache_provider = $no_reflection_cache
                ? null
                : new Provider\FileStorageCacheProvider($config);

            $classlike_storage_cache_provider = $no_reflection_cache
                ? null
                : new Provider\ClassLikeStorageCacheProvider($config);

            $providers = new Provider\Providers(
                new Provider\FileProvider,
                new Provider\ParserCacheProvider($config, !$no_file_cache),
                $file_storage_cache_provider,
                $classlike_storage_cache_provider,
                new Provider\FileReferenceCacheProvider($config),
                new Provider\ProjectCacheProvider(Composer::getLockFilePath($current_dir))
            );
        }
        return $providers;
    }

    /**
     * @param array{set-baseline:string} $options
     * @return array<string,array<string,array{o:int, s: list<string>}>>
     */
    private static function generateBaseline(
        array $options,
        Config $config,
        string $current_dir,
        ?string $path_to_config
    ): array {
        fwrite(STDERR, 'Writing error baseline to file...' . PHP_EOL);

        try {
            $issue_baseline = ErrorBaseline::read(
                new \Psalm\Internal\Provider\FileProvider,
                $options['set-baseline']
            );
        } catch (\Psalm\Exception\ConfigException $e) {
            $issue_baseline = [];
        }

        ErrorBaseline::create(
            new \Psalm\Internal\Provider\FileProvider,
            $options['set-baseline'],
            IssueBuffer::getIssuesData(),
            $config->include_php_versions_in_error_baseline || isset($options['include-php-versions'])
        );

        fwrite(STDERR, "Baseline saved to {$options['set-baseline']}.");

        CliUtils::updateConfigFile(
            $config,
            $path_to_config ?? $current_dir,
            $options['set-baseline']
        );

        fwrite(STDERR, PHP_EOL);

        return $issue_baseline;
    }

    /**
     * @return array<string,array<string,array{o:int, s: list<string>}>>
     */
    private static function updateBaseline(array $options, Config $config): array
    {
{'fraction_non_alphanumeric': 0.13575605680868838, 'fraction_numerical': 0.002457123200157256, 'mean_word_length': 1.8940482116191424, 'pattern_counts': {'":': 0, '<': 29, '<?xml version=': 0, '>': 59, 'https://': 2, 'lorem ipsum': 0, 'www.': 0, 'xml': 6}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '1189171', 'n_tokens_mistral': 11929, 'n_tokens_neox': 11262, 'n_words': 2158}