1
0
mirror of https://github.com/danog/psalm.git synced 2025-01-09 14:38:37 +01:00
psalm/src/Psalm/Internal/CliUtils.php
2022-01-21 00:17:06 +01:00

509 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
namespace Psalm\Internal;
use Composer\Autoload\ClassLoader;
use JsonException;
use PackageVersions\Versions;
use Phar;
use Psalm\Config;
use Psalm\Config\Creator;
use Psalm\Exception\ConfigException;
use Psalm\Exception\ConfigNotFoundException;
use Psalm\Internal\Analyzer\ProjectAnalyzer;
use Psalm\Report;
use function array_slice;
use function assert;
use function count;
use function define;
use function dirname;
use function fgets;
use function file_exists;
use function file_get_contents;
use function file_put_contents;
use function fwrite;
use function implode;
use function in_array;
use function ini_get;
use function is_array;
use function is_dir;
use function is_string;
use function json_decode;
use function preg_match;
use function preg_replace;
use function preg_split;
use function realpath;
use function stream_get_meta_data;
use function stream_set_blocking;
use function strlen;
use function strpos;
use function strtoupper;
use function substr;
use function substr_replace;
use function trim;
use const DIRECTORY_SEPARATOR;
use const JSON_THROW_ON_ERROR;
use const PHP_EOL;
use const STDERR;
use const STDIN;
/**
* @internal
*/
final class CliUtils
{
public static function requireAutoloaders(
string $current_dir,
bool $has_explicit_root,
string $vendor_dir
): ?ClassLoader {
$autoload_roots = [$current_dir];
$psalm_dir = dirname(__DIR__, 3);
$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 (!$in_phar && realpath($psalm_dir) !== realpath($current_dir)) {
$autoload_roots[] = $psalm_dir;
}
$autoload_files = [];
foreach ($autoload_roots as $autoload_root) {
$has_autoloader = false;
$nested_autoload_file = dirname($autoload_root, 2). 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;
}
$composer_json_file = Composer::getJsonFilePath($autoload_root);
if (!$has_autoloader && file_exists($composer_json_file)) {
$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', Versions::getVersion('vimeo/psalm'));
define('PHP_PARSER_VERSION', Versions::getVersion('nikic/php-parser'));
return $first_autoloader;
}
/**
* @psalm-suppress MixedAssignment
*/
public static function getVendorDir(string $current_dir): string
{
$composer_json_path = Composer::getJsonFilePath($current_dir);
if (!file_exists($composer_json_path)) {
return 'vendor';
}
try {
$composer_json = json_decode(file_get_contents($composer_json_path), true, 512, JSON_THROW_ON_ERROR);
} catch (JsonException $e) {
fwrite(
STDERR,
'Invalid composer.json at ' . $composer_json_path . "\n" . $e->getMessage() . "\n"
);
exit(1);
}
if (!$composer_json) {
fwrite(
STDERR,
'Invalid composer.json at ' . $composer_json_path . "\n"
);
exit(1);
}
if (is_array($composer_json)
&& 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 list<string>
*/
public static function getRawCliArguments(): array
{
global $argv;
if (!$argv) {
return [];
}
return array_slice($argv, 1);
}
/**
* @return list<string>
*/
public static function getArguments(): array
{
$argv = self::getRawCliArguments();
$filtered_input_paths = [];
for ($i = 0, $iMax = count($argv); $i < $iMax; ++$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 list<string>|null
*/
public static function getPathsToCheck($f_paths): ?array
{
$paths_to_check = [];
if ($f_paths) {
$input_paths = is_array($f_paths) ? $f_paths : [$f_paths];
} else {
$input_paths = self::getRawCliArguments();
if (!$input_paths) {
return null;
}
}
$filtered_input_paths = [];
for ($i = 0, $iMax = count($input_paths); $i < $iMax; ++$i) {
/** @var string */
$input_path = $input_paths[$i];
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 (strpos($input_path, '--') === 0 && strlen($input_path) > 2) {
if (substr($input_path, 2) === 'config') {
++$i;
}
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;
}
public static function initializeConfig(
?string $path_to_config,
string $current_dir,
string $output_format,
?ClassLoader $first_autoloader,
bool $create_if_non_existent = false
): Config {
try {
if ($path_to_config) {
$config = Config::loadFromXMLFile($path_to_config, $current_dir);
} else {
try {
$config = Config::getConfigForPath($current_dir, $current_dir);
} catch (ConfigNotFoundException $e) {
if (!$create_if_non_existent) {
if (in_array($output_format, [Report::TYPE_CONSOLE, Report::TYPE_PHP_STORM])) {
fwrite(
STDERR,
'Could not locate a config XML file in path ' . $current_dir
. '. Have you run \'psalm --init\' ?' . PHP_EOL
);
exit(1);
}
throw $e;
}
$config = Creator::createBareConfig(
$current_dir,
null,
self::getVendorDir($current_dir)
);
}
}
} catch (ConfigException $e) {
fwrite(
STDERR,
$e->getMessage() . PHP_EOL
);
exit(1);
}
$config->setComposerClassLoader($first_autoloader);
return $config;
}
public static function updateConfigFile(Config $config, string $config_file_path, string $baseline_path): void
{
if ($config->error_baseline === $baseline_path) {
return;
}
$config_file = $config_file_path;
if (is_dir($config_file_path)) {
$config_file = Config::locateConfigFile($config_file_path);
}
if (!$config_file) {
fwrite(STDERR, "Don't forget to set errorBaseline=\"{$baseline_path}\" to your config.");
return;
}
$config_file_contents = file_get_contents($config_file);
if ($config->error_baseline) {
$amended_config_file_contents = preg_replace(
'/errorBaseline=".*?"/',
"errorBaseline=\"{$baseline_path}\"",
$config_file_contents
);
} else {
$end_psalm_open_tag = strpos($config_file_contents, '>', (int)strpos($config_file_contents, '<psalm'));
if (!$end_psalm_open_tag) {
fwrite(STDERR, " Don't forget to set errorBaseline=\"{$baseline_path}\" in your config.");
return;
}
if ($config_file_contents[$end_psalm_open_tag - 1] === "\n") {
$amended_config_file_contents = substr_replace(
$config_file_contents,
" errorBaseline=\"{$baseline_path}\"\n>",
$end_psalm_open_tag,
1
);
} else {
$amended_config_file_contents = substr_replace(
$config_file_contents,
" errorBaseline=\"{$baseline_path}\">",
$end_psalm_open_tag,
1
);
}
}
file_put_contents($config_file, $amended_config_file_contents);
}
public static function getPathToConfig(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;
}
/**
* @psalm-pure
*/
public static function getMemoryLimitInBytes(): int
{
return self::convertMemoryLimitToBytes(ini_get('memory_limit'));
}
/** @psalm-pure */
public static function convertMemoryLimitToBytes(string $limit): int
{
// for unlimited = -1
if ($limit < 0) {
return -1;
}
if (preg_match('/^(\d+)(\D?)$/', $limit, $matches)) {
assert(isset($matches[1]));
$limit = (int)$matches[1];
switch (strtoupper($matches[2] ?? '')) {
case 'G':
$limit *= 1_024 * 1_024 * 1_024;
break;
case 'M':
$limit *= 1_024 * 1_024;
break;
case 'K':
$limit *= 1_024;
break;
}
}
return (int)$limit;
}
public static function initPhpVersion(array $options, Config $config, ProjectAnalyzer $project_analyzer): void
{
$source = null;
if (isset($options['php-version'])) {
if (!is_string($options['php-version'])) {
die('Expecting a version number in the format x.y' . PHP_EOL);
}
$version = $options['php-version'];
$source = 'cli';
} elseif ($version = $config->getPhpVersionFromConfig()) {
$source = 'config';
} elseif ($version = $config->getPHPVersionFromComposerJson()) {
$source = 'composer';
}
if ($version !== null && $source !== null) {
$project_analyzer->setPhpVersion($version, $source);
}
}
public static function runningInCI(): bool
{
return isset($_SERVER['TRAVIS'])
|| isset($_SERVER['CIRCLECI'])
|| isset($_SERVER['APPVEYOR'])
|| isset($_SERVER['JENKINS_URL'])
|| isset($_SERVER['SCRUTINIZER'])
|| isset($_SERVER['GITLAB_CI'])
|| isset($_SERVER['GITHUB_WORKFLOW']);
}
}