$argv * * Note: the $argv parameter should not be renamed as it's used in the files this function includes via require_once */ function (array $argv): void { gc_collect_cycles(); gc_disable(); ErrorHandler::install(); $valid_short_options = [ 'f:', 'm', 'h', 'v', 'c:', 'i', 'r:', ]; $valid_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:', ]; $args = array_slice($argv, 1); // get options from command line $options = getopt(implode('', $valid_short_options), $valid_long_options); if (isset($options['alter'])) { require_once __DIR__ . '/psalter.php'; exit; } if (isset($options['language-server'])) { require_once __DIR__ . '/psalm-language-server.php'; exit; } if (isset($options['refactor'])) { require_once __DIR__ . '/psalm-refactor.php'; exit; } array_map( /** * @param string $arg */ function ($arg) use ($valid_long_options, $valid_short_options): void { if (substr($arg, 0, 2) === '--' && $arg !== '--') { $arg_name = preg_replace('/=.*$/', '', substr($arg, 2)); if (!in_array($arg_name, $valid_long_options) && !in_array($arg_name . ':', $valid_long_options) && !in_array($arg_name . '::', $valid_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, $valid_short_options) && !in_array($arg_name . ':', $valid_short_options) ) { fwrite( STDERR, 'Unrecognised argument "-' . $arg_name . '"' . PHP_EOL . 'Type --help to see a list of supported arguments'. PHP_EOL ); exit(1); } } }, $args ); 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); } if (array_key_exists('help', $options)) { $options['h'] = false; } if (array_key_exists('version', $options)) { $options['v'] = false; } if (array_key_exists('init', $options)) { $options['i'] = false; } if (array_key_exists('monochrome', $options)) { $options['m'] = false; } if (isset($options['config'])) { $options['c'] = $options['config']; } 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 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; } if (getcwd() === false) { fwrite(STDERR, 'Cannot get current working directory' . PHP_EOL); exit(1); } if (isset($options['root'])) { $options['r'] = $options['root']; } $current_dir = (string)getcwd() . DIRECTORY_SEPARATOR; if (isset($options['r']) && is_string($options['r'])) { $root_path = realpath($options['r']); if (!$root_path) { fwrite( STDERR, 'Could not locate root directory ' . $current_dir . DIRECTORY_SEPARATOR . $options['r'] . PHP_EOL ); exit(1); } $current_dir = $root_path . DIRECTORY_SEPARATOR; } $path_to_config = get_path_to_config($options); $vendor_dir = \Psalm\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 requireAutoloaders($current_dir, isset($options['r']), $vendor_dir); } ); $run_taint_analysis = (isset($options['track-tainted-input']) || isset($options['security-analysis']) || isset($options['taint-analysis'])); /** @var string|null $dump_taint_graph */ $dump_taint_graph = $options['dump-taint-graph'] ?? null; 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; $init_level = null; $init_source_dir = null; if (isset($options['i'])) { 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; } )); 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 = \Psalm\getVendorDir($current_dir); if ($init_level === null) { echo "Calculating best config level based on project files\n"; \Psalm\Config\Creator::createBareConfig($current_dir, $init_source_dir, $vendor_dir); $config = \Psalm\Config::getInstance(); $config->setComposerClassLoader($first_autoloader); } else { 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); } } else { $config = initialiseConfig( $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; } } $config->setIncludeCollector($include_collector); $in_ci = isset($_SERVER['TRAVIS']) || isset($_SERVER['CIRCLECI']) || isset($_SERVER['APPVEYOR']) || isset($_SERVER['JENKINS_URL']) || isset($_SERVER['SCRUTINIZER']) || isset($_SERVER['GITLAB_CI']) || isset($_SERVER['GITHUB_WORKFLOW']); // disable progressbar on CI if ($in_ci) { $options['long-progress'] = true; } if (isset($options['threads'])) { $threads = (int)$options['threads']; } elseif (isset($options['debug']) || $in_ci) { $threads = 1; } else { $threads = max(1, ProjectAnalyzer::getCpuCount() - 1); } if (!isset($options['threads']) && !isset($options['debug']) && $threads === 1 && ini_get('pcre.jit') === '1' && PHP_OS === 'Darwin' && version_compare(PHP_VERSION, '7.3.0') >= 0 && version_compare(PHP_VERSION, '7.4.0') < 0 ) { echo( 'If you want to run Psalm as a language server, or run Psalm with' . PHP_EOL . 'multiple processes (--threads=4), beware:' . PHP_EOL . \Psalm\Internal\Fork\Pool::MAC_PCRE_MESSAGE . PHP_EOL . PHP_EOL ); } $ini_handler = new \Psalm\Internal\Fork\PsalmRestarter('PSALM'); if (isset($options['disable-extension'])) { if (is_array($options['disable-extension'])) { /** @psalm-suppress MixedAssignment */ foreach ($options['disable-extension'] as $extension) { if (is_string($extension)) { $ini_handler->disableExtension($extension); } } } elseif (is_string($options['disable-extension'])) { $ini_handler->disableExtension($options['disable-extension']); } } if ($threads > 1) { $ini_handler->disableExtension('grpc'); } $ini_handler->disableExtension('uopz'); $type_map_location = null; if (isset($options['generate-json-map']) && is_string($options['generate-json-map'])) { $type_map_location = $options['generate-json-map']; } $stubs_location = null; if (isset($options['generate-stubs']) && is_string($options['generate-stubs'])) { $stubs_location = $options['generate-stubs']; } // If Xdebug is enabled, restart without it $ini_handler->check(); if ($config->load_xdebug_stub === null && '' !== $ini_handler->getSkippedVersion()) { $config->load_xdebug_stub = true; } 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 = \Psalm\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']); /** @var false|'always'|'auto' $find_unused_code */ $find_unused_code = false; if (isset($options['find-dead-code'])) { $options['find-unused-code'] = $options['find-dead-code']; } if (isset($options['find-unused-code'])) { if ($options['find-unused-code'] === 'always') { $find_unused_code = 'always'; } else { $find_unused_code = 'auto'; } } $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 = __DIR__ . '/Psalm/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'])) { $cache_directory = $config->getCacheDirectory(); if ($cache_directory !== null) { Config::removeCacheDirectory($cache_directory); } echo 'Cache directory deleted' . PHP_EOL; exit; } if (isset($options['clear-global-cache'])) { $cache_directory = $config->getGlobalCacheDirectory(); if ($cache_directory) { Config::removeCacheDirectory($cache_directory); echo 'Global cache directory deleted' . PHP_EOL; } exit; } $debug = array_key_exists('debug', $options) || array_key_exists('debug-by-line', $options); 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); } } 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)) ); } $stdout_report_options = new \Psalm\Report\ReportOptions(); $stdout_report_options->use_color = !array_key_exists('m', $options); $stdout_report_options->show_info = $show_info; $stdout_report_options->show_suggestions = !array_key_exists('no-suggestions', $options); /** * @psalm-suppress PropertyTypeCoercion */ $stdout_report_options->format = $output_format; $stdout_report_options->show_snippet = !isset($options['show-snippet']) || $options['show-snippet'] !== "false"; $stdout_report_options->pretty = isset($options['pretty-print']) && $options['pretty-print'] !== "false"; /** @var list|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 ); if (!isset($options['php-version'])) { $options['php-version'] = $config->getPhpVersion(); } if (isset($options['php-version'])) { if (!is_string($options['php-version'])) { die('Expecting a version number in the format x.y' . PHP_EOL); } $project_analyzer->setPhpVersion($options['php-version']); } if ($type_map_location) { $project_analyzer->getCodebase()->store_node_types = true; } $start_time = microtime(true); if (array_key_exists('debug-by-line', $options)) { $project_analyzer->debug_lines = true; } if (array_key_exists('debug-performance', $options)) { $project_analyzer->debug_performance = true; } if ($config->find_unused_code) { $find_unused_code = 'auto'; } if ($find_references_to !== null) { $project_analyzer->getCodebase()->collectLocations(); $project_analyzer->show_issues = false; } if ($find_unused_code) { $project_analyzer->getCodebase()->reportUnusedCode($find_unused_code); } if ($config->find_unused_variables || $find_unused_variables) { $project_analyzer->getCodebase()->reportUnusedVariables(); } if ($config->run_taint_analysis || $run_taint_analysis) { $is_diff = false; $project_analyzer->trackTaintedInputs(); } if ($config->find_unused_psalm_suppress || isset($options['find-unused-psalm-suppress'])) { $project_analyzer->trackUnusedSuppressions(); } /** @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); } $flow_graph = $project_analyzer->getCodebase()->taint_flow_graph; if ($flow_graph !== null && $dump_taint_graph !== null) { file_put_contents($dump_taint_graph, "digraph Taints {\n\t". implode("\n\t", array_map( function (array $edges) { return '"'.implode('" -> "', $edges).'"'; }, $flow_graph->summarizeEdges() )) . "\n}\n"); } $issue_baseline = []; if (isset($options['set-baseline']) && is_string($options['set-baseline'])) { 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']}."); update_config_file( $config, $path_to_config ?? $current_dir, $options['set-baseline'] ); fwrite(STDERR, PHP_EOL); } if (isset($options['update-baseline'])) { $baselineFile = Config::getInstance()->error_baseline; if (empty($baselineFile)) { die('Cannot update baseline, because no baseline file is configured.' . PHP_EOL); } try { $issue_current_baseline = ErrorBaseline::read( new \Psalm\Internal\Provider\FileProvider, $baselineFile ); $total_issues_current_baseline = ErrorBaseline::countTotalIssues($issue_current_baseline); $issue_baseline = ErrorBaseline::update( new \Psalm\Internal\Provider\FileProvider, $baselineFile, IssueBuffer::getIssuesData(), $config->include_php_versions_in_error_baseline || isset($options['include-php-versions']) ); $total_issues_updated_baseline = ErrorBaseline::countTotalIssues($issue_baseline); $total_fixed_issues = $total_issues_current_baseline - $total_issues_updated_baseline; if ($total_fixed_issues > 0) { echo str_repeat('-', 30) . "\n"; echo $total_fixed_issues . ' errors fixed' . "\n"; } } catch (\Psalm\Exception\ConfigException $exception) { fwrite(STDERR, 'Could not update baseline file: ' . $exception->getMessage() . PHP_EOL); exit(1); } } if (isset($options['use-baseline'])) { if (!is_string($options['use-baseline'])) { fwrite(STDERR, '--use-baseline must be a string' . PHP_EOL); exit(1); } $baseline_file_path = $options['use-baseline']; } else { $baseline_file_path = Config::getInstance()->error_baseline; } if (!$issue_baseline && $baseline_file_path && !isset($options['ignore-baseline'])) { try { $issue_baseline = ErrorBaseline::read( new \Psalm\Internal\Provider\FileProvider, $baseline_file_path ); } catch (\Psalm\Exception\ConfigException $exception) { fwrite(STDERR, 'Error while reading baseline: ' . $exception->getMessage() . PHP_EOL); exit(1); } } if ($type_map_location) { $file_map = $providers->file_reference_provider->getFileMaps(); $name_file_map = []; $expected_references = []; foreach ($file_map as $file_path => $map) { $file_name = $config->shortenFileName($file_path); foreach ($map[0] as $map_parts) { $expected_references[$map_parts[1]] = true; } $map[2] = []; $name_file_map[$file_name] = $map; } $reference_dictionary = \Psalm\Internal\Codebase\ReferenceMapGenerator::getReferenceMap( $providers->classlike_storage_provider, $expected_references ); $type_map_string = json_encode(['files' => $name_file_map, 'references' => $reference_dictionary]); $providers->file_provider->setContents( $type_map_location, $type_map_string ); } if ($stubs_location) { $providers->file_provider->setContents( $stubs_location, \Psalm\Internal\Stubs\Generator\StubsGenerator::getAll( $project_analyzer->getCodebase(), $providers->classlike_storage_provider, $providers->file_storage_provider ) ); } if (!isset($options['i'])) { IssueBuffer::finish( $project_analyzer, !$paths_to_check, $start_time, isset($options['stats']), $issue_baseline ); } else { $issues_by_file = IssueBuffer::getIssuesData(); if (!$issues_by_file) { $init_level = 1; } else { $codebase = $project_analyzer->getCodebase(); $mixed_counts = $codebase->analyzer->getTotalTypeCoverage($codebase); $init_level = \Psalm\Config\Creator::getLevel( array_merge(...array_values($issues_by_file)), array_sum($mixed_counts) ); } echo "\n" . 'Detected level ' . $init_level . ' as a suitable initial default' . "\n"; 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); } } )($argv);