1
0
mirror of https://github.com/danog/psalm.git synced 2024-12-12 17:27:28 +01:00
psalm/src/command_functions.php
Tyson Andre 018c4bf545
Support generating a .console report text file. (#3777)
This is useful for use cases such as saving multiline taint detection results.

Only the compact and console reports seem to use color right now.
In many cases, adding color codes to a text file would make it harder to read
in an editor.
2020-07-08 15:09:31 -04:00

539 lines
15 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

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

<?php
use Composer\Autoload\ClassLoader;
use Psalm\Config;
use Psalm\Exception\ConfigException;
/**
* @param string $current_dir
* @param bool $has_explicit_root
* @param string $vendor_dir
*
* @return ?\Composer\Autoload\ClassLoader
*/
function requireAutoloaders($current_dir, $has_explicit_root, $vendor_dir)
{
$autoload_roots = [$current_dir];
$psalm_dir = dirname(__DIR__);
/** @psalm-suppress UndefinedConstant */
$in_phar = Phar::running() || strpos(__NAMESPACE__, 'HumbugBox');
if ($in_phar) {
require_once(__DIR__ . '/../vendor/autoload.php');
// hack required for JsonMapper
require_once __DIR__ . '/../vendor/netresearch/jsonmapper/src/JsonMapper.php';
require_once __DIR__ . '/../vendor/netresearch/jsonmapper/src/JsonMapper/Exception.php';
}
if (realpath($psalm_dir) !== realpath($current_dir) && !$in_phar) {
$autoload_roots[] = $psalm_dir;
}
$autoload_files = [];
foreach ($autoload_roots as $autoload_root) {
$has_autoloader = false;
$nested_autoload_file = dirname(dirname($autoload_root)) . DIRECTORY_SEPARATOR . 'autoload.php';
// note: don't realpath $nested_autoload_file, or phar version will fail
if (file_exists($nested_autoload_file)) {
if (!in_array($nested_autoload_file, $autoload_files, false)) {
$autoload_files[] = $nested_autoload_file;
}
$has_autoloader = true;
}
$vendor_autoload_file =
$autoload_root . DIRECTORY_SEPARATOR . $vendor_dir . DIRECTORY_SEPARATOR . 'autoload.php';
// note: don't realpath $vendor_autoload_file, or phar version will fail
if (file_exists($vendor_autoload_file)) {
if (!in_array($vendor_autoload_file, $autoload_files, false)) {
$autoload_files[] = $vendor_autoload_file;
}
$has_autoloader = true;
}
if (!$has_autoloader && file_exists($autoload_root . '/composer.json')) {
$error_message = 'Could not find any composer autoloaders in ' . $autoload_root;
if (!$has_explicit_root) {
$error_message .= PHP_EOL . 'Add a --root=[your/project/directory] flag '
. 'to specify a particular project to run Psalm on.';
}
fwrite(STDERR, $error_message . PHP_EOL);
exit(1);
}
}
$first_autoloader = null;
foreach ($autoload_files as $file) {
/**
* @psalm-suppress UnresolvableInclude
*
* @var mixed
*/
$autoloader = require_once $file;
if (!$first_autoloader
&& $autoloader instanceof ClassLoader
) {
$first_autoloader = $autoloader;
}
}
if ($first_autoloader === null && !$in_phar) {
if (!$autoload_files) {
fwrite(STDERR, 'Failed to find a valid Composer autoloader' . "\n");
} else {
fwrite(STDERR, 'Failed to find a valid Composer autoloader in ' . implode(', ', $autoload_files) . "\n");
}
fwrite(
STDERR,
'Please make sure youve run `composer install` in the current directory before using Psalm.' . "\n"
);
exit(1);
}
define('PSALM_VERSION', \PackageVersions\Versions::getVersion('vimeo/psalm'));
define('PHP_PARSER_VERSION', \PackageVersions\Versions::getVersion('nikic/php-parser'));
return $first_autoloader;
}
/**
* @param string $current_dir
*
* @return string
*
* @psalm-suppress MixedArrayAccess
* @psalm-suppress MixedAssignment
* @psalm-suppress PossiblyUndefinedStringArrayOffset
*/
function getVendorDir($current_dir)
{
$composer_json_path = $current_dir . DIRECTORY_SEPARATOR . 'composer.json';
if (!file_exists($composer_json_path)) {
return 'vendor';
}
if (!$composer_json = json_decode(file_get_contents($composer_json_path), true)) {
fwrite(
STDERR,
'Invalid composer.json at ' . $composer_json_path . "\n"
);
exit(1);
}
if (isset($composer_json['config'])
&& is_array($composer_json['config'])
&& isset($composer_json['config']['vendor-dir'])
&& is_string($composer_json['config']['vendor-dir'])
) {
return $composer_json['config']['vendor-dir'];
}
return 'vendor';
}
/**
* @return string[]
*/
function getArguments() : array
{
global $argv;
if (!$argv) {
return [];
}
$filtered_input_paths = [];
for ($i = 0; $i < count($argv); ++$i) {
$input_path = $argv[$i];
if (realpath($input_path) !== false) {
continue;
}
if ($input_path[0] === '-' && strlen($input_path) === 2) {
if ($input_path[1] === 'c' || $input_path[1] === 'f') {
++$i;
}
continue;
}
if ($input_path[0] === '-' && $input_path[2] === '=') {
continue;
}
$filtered_input_paths[] = $input_path;
}
return $filtered_input_paths;
}
/**
* @param string|array|null|false $f_paths
*
* @return string[]|null
*/
function getPathsToCheck($f_paths)
{
global $argv;
$paths_to_check = [];
if ($f_paths) {
$input_paths = is_array($f_paths) ? $f_paths : [$f_paths];
} else {
$input_paths = $argv ? $argv : null;
}
if ($input_paths) {
$filtered_input_paths = [];
for ($i = 0; $i < count($input_paths); ++$i) {
/** @var string */
$input_path = $input_paths[$i];
if (realpath($input_path) === realpath(dirname(__DIR__) . DIRECTORY_SEPARATOR . 'psalm')
|| realpath($input_path) === realpath(dirname(__DIR__) . DIRECTORY_SEPARATOR . 'psalter')
|| realpath($input_path) === realpath(Phar::running(false))
) {
continue;
}
if ($input_path[0] === '-' && strlen($input_path) === 2) {
if ($input_path[1] === 'c' || $input_path[1] === 'f') {
++$i;
}
continue;
}
if ($input_path[0] === '-' && $input_path[2] === '=') {
continue;
}
if (substr($input_path, 0, 2) === '--' && strlen($input_path) > 2) {
continue;
}
$filtered_input_paths[] = $input_path;
}
if ($filtered_input_paths === ['-']) {
$meta = stream_get_meta_data(STDIN);
stream_set_blocking(STDIN, false);
if ($stdin = fgets(STDIN)) {
$filtered_input_paths = preg_split('/\s+/', trim($stdin));
}
$blocked = $meta['blocked'];
stream_set_blocking(STDIN, $blocked);
}
foreach ($filtered_input_paths as $path_to_check) {
if ($path_to_check[0] === '-') {
fwrite(STDERR, 'Invalid usage, expecting psalm [options] [file...]' . PHP_EOL);
exit(1);
}
if (!file_exists($path_to_check)) {
fwrite(STDERR, 'Cannot locate ' . $path_to_check . PHP_EOL);
exit(1);
}
$path_to_check = realpath($path_to_check);
if (!$path_to_check) {
fwrite(STDERR, 'Error getting realpath for file' . PHP_EOL);
exit(1);
}
$paths_to_check[] = $path_to_check;
}
if (!$paths_to_check) {
$paths_to_check = null;
}
}
return $paths_to_check;
}
function getPsalmHelpText(): string
{
return <<<HELP
Usage:
psalm [options] [file...]
Basic configuration:
-c, --config=psalm.xml
Path to a psalm.xml configuration file. Run psalm --init to create one.
--use-ini-defaults
Use PHP-provided ini defaults for memory and error display
--disable-extension=[extension]
Used to disable certain extensions while Psalm is running.
--threads=INT
If greater than one, Psalm will run analysis on multiple threads, speeding things up.
--diff
Runs Psalm in diff mode, only checking files that have changed since last run (and their dependents)
--diff-methods
Only checks methods that have changed since last run (and their dependents)
Surfacing issues:
--show-info[=BOOLEAN]
Show non-exception parser findings (defaults to false).
--show-snippet[=true]
Show code snippets with errors. Options are 'true' or 'false'
--find-dead-code[=auto]
--find-unused-code[=auto]
Look for unused code. Options are 'auto' or 'always'. If no value is specified, default is 'auto'
--find-unused-psalm-suppress
Finds all @psalm-suppress annotations that arent used
--find-references-to=[class|method|property]
Searches the codebase for references to the given fully-qualified class or method,
where method is in the format class::methodName
--no-suggestions
Hide suggestions
--taint-analysis
Run Psalm in taint analysis mode see https://psalm.dev/docs/security_analysis for more info
Issue baselines:
--set-baseline=PATH
Save all current error level issues to a file, to mark them as info in subsequent runs
Add --include-php-versions to also include a list of PHP extension versions
--use-baseline=PATH
Allows you to use a baseline other than the default baseline provided in your config
--ignore-baseline
Ignore the error baseline
--update-baseline
Update the baseline by removing fixed issues. This will not add new issues to the baseline
Add --include-php-versions to also include a list of PHP extension versions
Plugins:
--plugin=PATH
Executes a plugin, an alternative to using the Psalm config
Output:
-m, --monochrome
Enable monochrome output
--output-format=console
Changes the output format.
Available formats: compact, console, text, emacs, json, pylint, xml, checkstyle, junit, sonarqube, github
--no-progress
Disable the progress indicator
--long-progress
Use a progress indicator suitable for Continuous Integration logs
--stats
Shows a breakdown of Psalm's ability to infer types in the codebase
Reports:
--report=PATH
The path where to output report file. The output format is based on the file extension.
(Currently supported formats: ".json", ".xml", ".txt", ".emacs", ".pylint", ".console",
"checkstyle.xml", "sonarqube.json", "summary.json", "junit.xml")
--report-show-info[=BOOLEAN]
Whether the report should include non-errors in its output (defaults to true)
Caching:
--clear-cache
Clears all cache files that Psalm uses for this specific project
--clear-global-cache
Clears all cache files that Psalm uses for all projects
--no-cache
Runs Psalm without using cache
--no-reflection-cache
Runs Psalm without using cached representations of unchanged classes and files.
Useful if you want the afterClassLikeVisit plugin hook to run every time you visit a file.
--no-file-cache
Runs Psalm without using caching every single file for later diffing.
This reduces the space Psalm uses on disk and file I/O.
Miscellaneous:
-h, --help
Display this help message
-v, --version
Display the Psalm version
-i, --init [source_dir=src] [level=3]
Create a psalm config file in the current directory that points to [source_dir]
at the required level, from 1, most strict, to 8, most permissive.
--debug
Debug information
--debug-by-line
Debug information on a line-by-line level
--debug-emitted-issues
Print a php backtrace to stderr when emitting issues.
-r, --root
If running Psalm globally you'll need to specify a project root. Defaults to cwd
--generate-json-map=PATH
Generate a map of node references and types in JSON format, saved to the given path.
--generate-stubs=PATH
Generate stubs for the project and dump the file in the given path
--shepherd[=host]
Send data to Shepherd, Psalm's GitHub integration tool.
--alter
Run Psalter
--language-server
Run Psalm Language Server
HELP;
}
function initialiseConfig(
?string $path_to_config,
string $current_dir,
string $output_format,
?ClassLoader $first_autoloader
): Config {
try {
if ($path_to_config) {
$config = Config::loadFromXMLFile($path_to_config, $current_dir);
} else {
$config = Config::getConfigForPath($current_dir, $current_dir, $output_format);
}
} catch (Psalm\Exception\ConfigException $e) {
fwrite(STDERR, $e->getMessage() . PHP_EOL);
exit(1);
}
$config->setComposerClassLoader($first_autoloader);
return $config;
}
function update_config_file(Config $config, string $config_file_path, string $baseline_path) : void
{
if ($config->error_baseline === $baseline_path) {
return;
}
$configFile = $config_file_path;
if (is_dir($config_file_path)) {
$configFile = Config::locateConfigFile($config_file_path);
}
if (!$configFile) {
fwrite(STDERR, "Don't forget to set errorBaseline=\"{$baseline_path}\" to your config.");
return;
}
$configFileContents = file_get_contents($configFile);
if ($config->error_baseline) {
$amendedConfigFileContents = preg_replace(
'/errorBaseline=".*?"/',
"errorBaseline=\"{$baseline_path}\"",
$configFileContents
);
} else {
$endPsalmOpenTag = strpos($configFileContents, '>', (int)strpos($configFileContents, '<psalm'));
if (!$endPsalmOpenTag) {
fwrite(STDERR, " Don't forget to set errorBaseline=\"{$baseline_path}\" in your config.");
return;
}
if ($configFileContents[$endPsalmOpenTag - 1] === "\n") {
$amendedConfigFileContents = substr_replace(
$configFileContents,
" errorBaseline=\"{$baseline_path}\"\n>",
$endPsalmOpenTag,
1
);
} else {
$amendedConfigFileContents = substr_replace(
$configFileContents,
" errorBaseline=\"{$baseline_path}\">",
$endPsalmOpenTag,
1
);
}
}
file_put_contents($configFile, $amendedConfigFileContents);
}
function get_path_to_config(array $options): ?string
{
$path_to_config = isset($options['c']) && is_string($options['c']) ? realpath($options['c']) : null;
if ($path_to_config === false) {
fwrite(STDERR, 'Could not resolve path to config ' . (string) ($options['c'] ?? '') . PHP_EOL);
exit(1);
}
return $path_to_config;
}
function getMemoryLimitInBytes(): int
{
$limit = ini_get('memory_limit');
// for unlimited = -1
if ($limit < 0) {
return -1;
}
if (preg_match('/^(\d+)(\D?)$/', $limit, $matches)) {
$limit = (int)$matches[1];
switch (strtoupper($matches[2] ?? '')) {
case 'G':
$limit *= 1024 * 1024 * 1024;
break;
case 'M':
$limit *= 1024 * 1024;
break;
case 'K':
$limit *= 1024;
break;
}
}
return (int)$limit;
}