2016-06-10 00:08:25 +02:00
|
|
|
|
<?php
|
2018-11-06 03:57:36 +01:00
|
|
|
|
namespace Psalm\Internal\Analyzer;
|
2016-06-10 00:08:25 +02:00
|
|
|
|
|
2018-01-21 19:38:51 +01:00
|
|
|
|
use Psalm\Codebase;
|
2016-08-13 20:20:46 +02:00
|
|
|
|
use Psalm\Config;
|
2017-01-12 06:54:41 +01:00
|
|
|
|
use Psalm\Context;
|
2019-05-27 16:05:15 +02:00
|
|
|
|
use Psalm\Exception\UnsupportedIssueToFixException;
|
2019-06-01 06:56:54 +02:00
|
|
|
|
use Psalm\FileManipulation;
|
2018-11-06 03:57:36 +01:00
|
|
|
|
use Psalm\Internal\LanguageServer\{LanguageServer, ProtocolStreamReader, ProtocolStreamWriter};
|
|
|
|
|
use Psalm\Internal\Provider\ClassLikeStorageProvider;
|
|
|
|
|
use Psalm\Internal\Provider\FileProvider;
|
|
|
|
|
use Psalm\Internal\Provider\FileReferenceProvider;
|
|
|
|
|
use Psalm\Internal\Provider\ParserCacheProvider;
|
|
|
|
|
use Psalm\Internal\Provider\Providers;
|
2019-05-27 16:05:15 +02:00
|
|
|
|
use Psalm\Issue\InvalidFalsableReturnType;
|
|
|
|
|
use Psalm\Issue\InvalidNullableReturnType;
|
|
|
|
|
use Psalm\Issue\InvalidReturnType;
|
|
|
|
|
use Psalm\Issue\LessSpecificReturnType;
|
|
|
|
|
use Psalm\Issue\MismatchingDocblockParamType;
|
|
|
|
|
use Psalm\Issue\MismatchingDocblockReturnType;
|
|
|
|
|
use Psalm\Issue\MissingClosureReturnType;
|
|
|
|
|
use Psalm\Issue\MissingParamType;
|
|
|
|
|
use Psalm\Issue\MissingReturnType;
|
|
|
|
|
use Psalm\Issue\PossiblyUndefinedGlobalVariable;
|
|
|
|
|
use Psalm\Issue\PossiblyUndefinedVariable;
|
|
|
|
|
use Psalm\Issue\PossiblyUnusedMethod;
|
|
|
|
|
use Psalm\Issue\PossiblyUnusedProperty;
|
2019-09-26 21:08:05 +02:00
|
|
|
|
use Psalm\Issue\UnnecessaryVarAnnotation;
|
2019-05-27 16:05:15 +02:00
|
|
|
|
use Psalm\Issue\UnusedMethod;
|
|
|
|
|
use Psalm\Issue\UnusedProperty;
|
2019-07-24 22:48:54 +02:00
|
|
|
|
use Psalm\Issue\UnusedVariable;
|
2019-05-30 16:30:41 +02:00
|
|
|
|
use Psalm\Progress\Progress;
|
|
|
|
|
use Psalm\Progress\VoidProgress;
|
2019-06-09 18:37:28 +02:00
|
|
|
|
use Psalm\Report;
|
|
|
|
|
use Psalm\Report\ReportOptions;
|
2017-01-02 21:31:18 +01:00
|
|
|
|
use Psalm\Type;
|
2019-05-27 16:05:15 +02:00
|
|
|
|
use Psalm\Issue\CodeIssue;
|
2019-06-26 22:52:29 +02:00
|
|
|
|
use function substr;
|
|
|
|
|
use function strlen;
|
|
|
|
|
use function cli_set_process_title;
|
|
|
|
|
use function stream_socket_client;
|
|
|
|
|
use function fwrite;
|
|
|
|
|
use const STDERR;
|
|
|
|
|
use function stream_set_blocking;
|
|
|
|
|
use function stream_socket_server;
|
|
|
|
|
use const STDOUT;
|
|
|
|
|
use function extension_loaded;
|
|
|
|
|
use function stream_socket_accept;
|
|
|
|
|
use function pcntl_fork;
|
|
|
|
|
use const STDIN;
|
|
|
|
|
use function microtime;
|
|
|
|
|
use function array_merge;
|
|
|
|
|
use function count;
|
|
|
|
|
use function implode;
|
|
|
|
|
use function array_diff;
|
|
|
|
|
use function strpos;
|
|
|
|
|
use function explode;
|
|
|
|
|
use function array_pop;
|
|
|
|
|
use function strtolower;
|
|
|
|
|
use function usort;
|
|
|
|
|
use function file_exists;
|
|
|
|
|
use function dirname;
|
|
|
|
|
use function mkdir;
|
|
|
|
|
use function rename;
|
|
|
|
|
use function is_dir;
|
|
|
|
|
use function is_file;
|
|
|
|
|
use const PHP_EOL;
|
|
|
|
|
use function array_shift;
|
|
|
|
|
use function array_combine;
|
|
|
|
|
use function preg_match;
|
|
|
|
|
use function array_keys;
|
|
|
|
|
use function array_fill_keys;
|
|
|
|
|
use function defined;
|
|
|
|
|
use function trim;
|
|
|
|
|
use function shell_exec;
|
|
|
|
|
use function is_string;
|
|
|
|
|
use function filter_var;
|
|
|
|
|
use const FILTER_VALIDATE_INT;
|
|
|
|
|
use function is_int;
|
|
|
|
|
use function is_readable;
|
|
|
|
|
use function file_get_contents;
|
|
|
|
|
use function substr_count;
|
|
|
|
|
use function array_map;
|
|
|
|
|
use function end;
|
2019-08-04 16:37:36 +02:00
|
|
|
|
use Psalm\Internal\Codebase\Taint;
|
2016-08-13 20:20:46 +02:00
|
|
|
|
|
2018-12-02 00:37:49 +01:00
|
|
|
|
/**
|
|
|
|
|
* @internal
|
|
|
|
|
*/
|
2018-11-06 03:57:36 +01:00
|
|
|
|
class ProjectAnalyzer
|
2016-06-10 00:08:25 +02:00
|
|
|
|
{
|
2016-06-26 19:45:20 +02:00
|
|
|
|
/**
|
|
|
|
|
* Cached config
|
2016-11-02 07:29:00 +01:00
|
|
|
|
*
|
2018-01-21 16:22:04 +01:00
|
|
|
|
* @var Config
|
2016-06-26 19:45:20 +02:00
|
|
|
|
*/
|
2018-11-06 03:57:36 +01:00
|
|
|
|
private $config;
|
2016-12-08 04:38:57 +01:00
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @var self
|
|
|
|
|
*/
|
|
|
|
|
public static $instance;
|
2016-06-26 19:45:20 +02:00
|
|
|
|
|
2018-01-21 19:38:51 +01:00
|
|
|
|
/**
|
|
|
|
|
* An object representing everything we know about the code
|
|
|
|
|
*
|
|
|
|
|
* @var Codebase
|
|
|
|
|
*/
|
2018-11-06 03:57:36 +01:00
|
|
|
|
private $codebase;
|
2018-01-21 19:38:51 +01:00
|
|
|
|
|
2017-07-25 22:11:02 +02:00
|
|
|
|
/** @var FileProvider */
|
|
|
|
|
private $file_provider;
|
|
|
|
|
|
2017-07-29 21:05:06 +02:00
|
|
|
|
/** @var ClassLikeStorageProvider */
|
2018-11-11 18:19:53 +01:00
|
|
|
|
private $classlike_storage_provider;
|
2017-07-29 21:05:06 +02:00
|
|
|
|
|
2018-09-28 22:18:45 +02:00
|
|
|
|
/** @var ?ParserCacheProvider */
|
2018-11-11 18:19:53 +01:00
|
|
|
|
private $parser_cache_provider;
|
2018-09-28 22:18:45 +02:00
|
|
|
|
|
|
|
|
|
/** @var FileReferenceProvider */
|
2018-11-11 18:19:53 +01:00
|
|
|
|
private $file_reference_provider;
|
2017-07-25 22:11:02 +02:00
|
|
|
|
|
2017-01-02 21:31:18 +01:00
|
|
|
|
/**
|
2019-05-30 16:30:41 +02:00
|
|
|
|
* @var Progress
|
2017-01-02 21:31:18 +01:00
|
|
|
|
*/
|
2019-05-30 16:30:41 +02:00
|
|
|
|
public $progress;
|
2017-01-02 21:31:18 +01:00
|
|
|
|
|
2018-03-26 15:08:55 +02:00
|
|
|
|
/**
|
|
|
|
|
* @var bool
|
|
|
|
|
*/
|
|
|
|
|
public $debug_lines = false;
|
|
|
|
|
|
2018-03-16 16:15:01 +01:00
|
|
|
|
/**
|
|
|
|
|
* @var bool
|
|
|
|
|
*/
|
|
|
|
|
public $show_issues = true;
|
|
|
|
|
|
2017-07-25 22:11:02 +02:00
|
|
|
|
/** @var int */
|
|
|
|
|
public $threads;
|
|
|
|
|
|
2018-01-05 19:22:48 +01:00
|
|
|
|
/**
|
2018-01-06 01:49:27 +01:00
|
|
|
|
* @var array<string, bool>
|
2018-01-05 19:22:48 +01:00
|
|
|
|
*/
|
2018-01-06 01:49:27 +01:00
|
|
|
|
private $issues_to_fix = [];
|
2018-01-05 19:22:48 +01:00
|
|
|
|
|
2018-01-07 17:48:33 +01:00
|
|
|
|
/**
|
|
|
|
|
* @var bool
|
|
|
|
|
*/
|
|
|
|
|
public $dry_run = false;
|
|
|
|
|
|
2019-04-17 19:15:06 +02:00
|
|
|
|
/**
|
|
|
|
|
* @var bool
|
|
|
|
|
*/
|
|
|
|
|
public $full_run = false;
|
|
|
|
|
|
2018-01-07 22:11:51 +01:00
|
|
|
|
/**
|
|
|
|
|
* @var bool
|
|
|
|
|
*/
|
|
|
|
|
public $only_replace_php_types_with_non_docblock_types = false;
|
|
|
|
|
|
2018-10-30 23:58:22 +01:00
|
|
|
|
/**
|
|
|
|
|
* @var ?int
|
|
|
|
|
*/
|
|
|
|
|
public $onchange_line_limit;
|
|
|
|
|
|
2019-05-09 17:20:13 +02:00
|
|
|
|
/**
|
|
|
|
|
* @var bool
|
|
|
|
|
*/
|
|
|
|
|
public $provide_completion = false;
|
|
|
|
|
|
2019-04-29 18:07:34 +02:00
|
|
|
|
/**
|
|
|
|
|
* @var array<string,string>
|
|
|
|
|
*/
|
|
|
|
|
private $project_files;
|
|
|
|
|
|
2019-06-03 05:33:57 +02:00
|
|
|
|
/**
|
|
|
|
|
* @var array<string, string>
|
|
|
|
|
*/
|
|
|
|
|
private $to_refactor = [];
|
|
|
|
|
|
2019-06-09 18:37:28 +02:00
|
|
|
|
/**
|
|
|
|
|
* @var ?ReportOptions
|
|
|
|
|
*/
|
|
|
|
|
public $stdout_report_options;
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @var array<ReportOptions>
|
|
|
|
|
*/
|
|
|
|
|
public $generated_report_options;
|
2018-02-19 06:57:06 +01:00
|
|
|
|
|
2019-05-27 16:05:15 +02:00
|
|
|
|
/**
|
|
|
|
|
* @var array<int, class-string<CodeIssue>>
|
|
|
|
|
*/
|
|
|
|
|
const SUPPORTED_ISSUES_TO_FIX = [
|
|
|
|
|
InvalidFalsableReturnType::class,
|
|
|
|
|
InvalidNullableReturnType::class,
|
|
|
|
|
InvalidReturnType::class,
|
|
|
|
|
LessSpecificReturnType::class,
|
|
|
|
|
MismatchingDocblockParamType::class,
|
|
|
|
|
MismatchingDocblockReturnType::class,
|
|
|
|
|
MissingClosureReturnType::class,
|
|
|
|
|
MissingParamType::class,
|
|
|
|
|
MissingReturnType::class,
|
|
|
|
|
PossiblyUndefinedGlobalVariable::class,
|
|
|
|
|
PossiblyUndefinedVariable::class,
|
|
|
|
|
PossiblyUnusedMethod::class,
|
|
|
|
|
PossiblyUnusedProperty::class,
|
|
|
|
|
UnusedMethod::class,
|
|
|
|
|
UnusedProperty::class,
|
2019-07-24 22:48:54 +02:00
|
|
|
|
UnusedVariable::class,
|
2019-09-26 21:08:05 +02:00
|
|
|
|
UnnecessaryVarAnnotation::class,
|
2019-05-27 16:05:15 +02:00
|
|
|
|
];
|
|
|
|
|
|
2016-12-17 06:48:31 +01:00
|
|
|
|
/**
|
2019-06-09 18:37:28 +02:00
|
|
|
|
* @param array<ReportOptions> $generated_report_options
|
2017-09-08 17:18:48 +02:00
|
|
|
|
* @param int $threads
|
|
|
|
|
* @param string $reports
|
2016-12-17 06:48:31 +01:00
|
|
|
|
*/
|
2017-01-02 21:31:18 +01:00
|
|
|
|
public function __construct(
|
2018-01-21 16:22:04 +01:00
|
|
|
|
Config $config,
|
2018-09-28 22:18:45 +02:00
|
|
|
|
Providers $providers,
|
2019-06-09 18:37:28 +02:00
|
|
|
|
?ReportOptions $stdout_report_options = null,
|
|
|
|
|
array $generated_report_options = [],
|
|
|
|
|
int $threads = 1,
|
2019-06-09 19:12:08 +02:00
|
|
|
|
Progress $progress = null
|
2017-01-02 21:31:18 +01:00
|
|
|
|
) {
|
2019-05-30 16:30:41 +02:00
|
|
|
|
if ($progress === null) {
|
|
|
|
|
$progress = new VoidProgress();
|
|
|
|
|
}
|
|
|
|
|
|
2018-09-28 22:18:45 +02:00
|
|
|
|
$this->parser_cache_provider = $providers->parser_cache_provider;
|
|
|
|
|
$this->file_provider = $providers->file_provider;
|
|
|
|
|
$this->classlike_storage_provider = $providers->classlike_storage_provider;
|
|
|
|
|
$this->file_reference_provider = $providers->file_reference_provider;
|
|
|
|
|
|
2019-05-30 16:30:41 +02:00
|
|
|
|
$this->progress = $progress;
|
2017-07-25 22:11:02 +02:00
|
|
|
|
$this->threads = $threads;
|
2018-01-21 16:22:04 +01:00
|
|
|
|
$this->config = $config;
|
2016-12-08 04:38:57 +01:00
|
|
|
|
|
2018-01-21 19:38:51 +01:00
|
|
|
|
$this->codebase = new Codebase(
|
|
|
|
|
$config,
|
2018-09-28 22:18:45 +02:00
|
|
|
|
$providers,
|
2019-05-30 16:30:41 +02:00
|
|
|
|
$progress
|
2018-01-21 19:38:51 +01:00
|
|
|
|
);
|
|
|
|
|
|
2019-06-09 18:37:28 +02:00
|
|
|
|
$this->stdout_report_options = $stdout_report_options;
|
|
|
|
|
$this->generated_report_options = $generated_report_options;
|
2017-09-08 17:18:48 +02:00
|
|
|
|
|
2019-04-29 18:07:34 +02:00
|
|
|
|
$project_files = [];
|
|
|
|
|
|
|
|
|
|
foreach ($this->config->getProjectDirectories() as $dir_name) {
|
|
|
|
|
$file_extensions = $this->config->getFileExtensions();
|
|
|
|
|
|
|
|
|
|
$file_paths = $this->file_provider->getFilesInDir($dir_name, $file_extensions);
|
|
|
|
|
|
|
|
|
|
foreach ($file_paths as $file_path) {
|
|
|
|
|
if ($this->config->isInProjectDirs($file_path)) {
|
|
|
|
|
$project_files[$file_path] = $file_path;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
foreach ($this->config->getProjectFiles() as $file_path) {
|
|
|
|
|
$project_files[$file_path] = $file_path;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$this->project_files = $project_files;
|
|
|
|
|
|
2016-12-08 04:38:57 +01:00
|
|
|
|
self::$instance = $this;
|
|
|
|
|
}
|
|
|
|
|
|
2019-06-09 18:37:28 +02:00
|
|
|
|
/**
|
|
|
|
|
* @param array<string> $report_file_paths
|
|
|
|
|
* @param bool $show_info
|
|
|
|
|
* @return array<ReportOptions>
|
|
|
|
|
*/
|
|
|
|
|
public static function getFileReportOptions(array $report_file_paths, bool $show_info = true)
|
|
|
|
|
{
|
|
|
|
|
$report_options = [];
|
|
|
|
|
|
|
|
|
|
$mapping = [
|
|
|
|
|
'checkstyle.xml' => Report::TYPE_CHECKSTYLE,
|
2019-06-18 03:17:09 +02:00
|
|
|
|
'sonarqube.json' => Report::TYPE_SONARQUBE,
|
2019-06-09 18:37:28 +02:00
|
|
|
|
'summary.json' => Report::TYPE_JSON_SUMMARY,
|
2019-12-19 21:18:09 +01:00
|
|
|
|
'junit.xml' => Report::TYPE_JUNIT,
|
2019-06-09 18:37:28 +02:00
|
|
|
|
'.xml' => Report::TYPE_XML,
|
|
|
|
|
'.json' => Report::TYPE_JSON,
|
|
|
|
|
'.txt' => Report::TYPE_TEXT,
|
|
|
|
|
'.emacs' => Report::TYPE_EMACS,
|
|
|
|
|
'.pylint' => Report::TYPE_PYLINT,
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
foreach ($report_file_paths as $report_file_path) {
|
|
|
|
|
foreach ($mapping as $extension => $type) {
|
|
|
|
|
if (substr($report_file_path, -strlen($extension)) === $extension) {
|
|
|
|
|
$o = new ReportOptions();
|
|
|
|
|
|
|
|
|
|
$o->format = $type;
|
|
|
|
|
$o->show_info = $show_info;
|
|
|
|
|
$o->output_path = $report_file_path;
|
|
|
|
|
$report_options[] = $o;
|
|
|
|
|
continue 2;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
throw new \UnexpectedValueException('Unknown report format ' . $report_file_path);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $report_options;
|
|
|
|
|
}
|
|
|
|
|
|
2018-10-17 21:52:26 +02:00
|
|
|
|
/**
|
|
|
|
|
* @param string|null $address
|
|
|
|
|
* @return void
|
|
|
|
|
*/
|
2018-11-20 04:53:11 +01:00
|
|
|
|
public function server($address = '127.0.0.1:12345', bool $socket_server_mode = false)
|
2018-10-17 21:52:26 +02:00
|
|
|
|
{
|
2018-11-06 03:57:36 +01:00
|
|
|
|
$this->codebase->diff_methods = true;
|
2018-10-17 21:52:26 +02:00
|
|
|
|
$this->file_reference_provider->loadReferenceCache();
|
|
|
|
|
$this->codebase->enterServerMode();
|
|
|
|
|
|
2020-01-13 23:28:00 +01:00
|
|
|
|
if (\ini_get('pcre.jit') === '1'
|
|
|
|
|
&& \PHP_OS === 'Darwin'
|
|
|
|
|
&& \version_compare(\PHP_VERSION, '7.3.0') >= 0
|
|
|
|
|
&& \version_compare(\PHP_VERSION, '7.4.0') < 0
|
2020-01-13 22:50:09 +01:00
|
|
|
|
) {
|
|
|
|
|
// do nothing
|
|
|
|
|
} else {
|
|
|
|
|
$cpu_count = self::getCpuCount();
|
|
|
|
|
|
|
|
|
|
// let's not go crazy
|
|
|
|
|
$usable_cpus = $cpu_count - 2;
|
|
|
|
|
|
|
|
|
|
if ($usable_cpus > 1) {
|
|
|
|
|
$this->threads = $usable_cpus;
|
|
|
|
|
}
|
2018-10-17 21:52:26 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$this->config->initializePlugins($this);
|
|
|
|
|
|
|
|
|
|
foreach ($this->config->getProjectDirectories() as $dir_name) {
|
|
|
|
|
$this->checkDirWithConfig($dir_name, $this->config);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@cli_set_process_title('Psalm PHP Language Server');
|
|
|
|
|
|
2018-11-20 04:53:11 +01:00
|
|
|
|
if (!$socket_server_mode && $address) {
|
2018-10-17 21:52:26 +02:00
|
|
|
|
// Connect to a TCP server
|
|
|
|
|
$socket = stream_socket_client('tcp://' . $address, $errno, $errstr);
|
|
|
|
|
if ($socket === false) {
|
|
|
|
|
fwrite(STDERR, "Could not connect to language client. Error $errno\n$errstr");
|
|
|
|
|
exit(1);
|
|
|
|
|
}
|
|
|
|
|
stream_set_blocking($socket, false);
|
|
|
|
|
new LanguageServer(
|
|
|
|
|
new ProtocolStreamReader($socket),
|
|
|
|
|
new ProtocolStreamWriter($socket),
|
|
|
|
|
$this
|
|
|
|
|
);
|
2019-01-19 06:58:08 +01:00
|
|
|
|
\Amp\Loop::run();
|
2018-11-20 04:53:11 +01:00
|
|
|
|
} elseif ($socket_server_mode && $address) {
|
2018-10-17 21:52:26 +02:00
|
|
|
|
// Run a TCP Server
|
|
|
|
|
$tcpServer = stream_socket_server('tcp://' . $address, $errno, $errstr);
|
|
|
|
|
if ($tcpServer === false) {
|
|
|
|
|
fwrite(STDERR, "Could not listen on $address. Error $errno\n$errstr");
|
|
|
|
|
exit(1);
|
|
|
|
|
}
|
|
|
|
|
fwrite(STDOUT, "Server listening on $address\n");
|
|
|
|
|
if (!extension_loaded('pcntl')) {
|
|
|
|
|
fwrite(STDERR, "PCNTL is not available. Only a single connection will be accepted\n");
|
|
|
|
|
}
|
|
|
|
|
while ($socket = stream_socket_accept($tcpServer, -1)) {
|
|
|
|
|
fwrite(STDOUT, "Connection accepted\n");
|
|
|
|
|
stream_set_blocking($socket, false);
|
|
|
|
|
if (extension_loaded('pcntl')) {
|
|
|
|
|
// If PCNTL is available, fork a child process for the connection
|
|
|
|
|
// An exit notification will only terminate the child process
|
|
|
|
|
$pid = pcntl_fork();
|
|
|
|
|
if ($pid === -1) {
|
|
|
|
|
fwrite(STDERR, "Could not fork\n");
|
|
|
|
|
exit(1);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ($pid === 0) {
|
|
|
|
|
// Child process
|
|
|
|
|
$reader = new ProtocolStreamReader($socket);
|
|
|
|
|
$reader->on(
|
|
|
|
|
'close',
|
|
|
|
|
/** @return void */
|
|
|
|
|
function () {
|
|
|
|
|
fwrite(STDOUT, "Connection closed\n");
|
|
|
|
|
}
|
|
|
|
|
);
|
|
|
|
|
new LanguageServer(
|
|
|
|
|
$reader,
|
|
|
|
|
new ProtocolStreamWriter($socket),
|
|
|
|
|
$this
|
|
|
|
|
);
|
|
|
|
|
// Just for safety
|
|
|
|
|
exit(0);
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
// If PCNTL is not available, we only accept one connection.
|
|
|
|
|
// An exit notification will terminate the server
|
|
|
|
|
new LanguageServer(
|
|
|
|
|
new ProtocolStreamReader($socket),
|
|
|
|
|
new ProtocolStreamWriter($socket),
|
|
|
|
|
$this
|
|
|
|
|
);
|
2019-01-19 06:58:08 +01:00
|
|
|
|
\Amp\Loop::run();
|
2018-10-17 21:52:26 +02:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
// Use STDIO
|
|
|
|
|
stream_set_blocking(STDIN, false);
|
|
|
|
|
new LanguageServer(
|
|
|
|
|
new ProtocolStreamReader(STDIN),
|
|
|
|
|
new ProtocolStreamWriter(STDOUT),
|
|
|
|
|
$this
|
|
|
|
|
);
|
2019-01-19 06:58:08 +01:00
|
|
|
|
\Amp\Loop::run();
|
2018-10-17 21:52:26 +02:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2016-12-08 04:38:57 +01:00
|
|
|
|
/**
|
2018-10-07 02:11:19 +02:00
|
|
|
|
* @return self
|
2016-12-08 04:38:57 +01:00
|
|
|
|
*/
|
|
|
|
|
public static function getInstance()
|
|
|
|
|
{
|
|
|
|
|
return self::$instance;
|
|
|
|
|
}
|
2016-08-04 20:38:43 +02:00
|
|
|
|
|
2019-04-29 18:07:34 +02:00
|
|
|
|
/**
|
|
|
|
|
* @param string $file_path
|
|
|
|
|
*
|
|
|
|
|
* @return bool
|
|
|
|
|
*/
|
|
|
|
|
public function canReportIssues($file_path)
|
|
|
|
|
{
|
|
|
|
|
return isset($this->project_files[$file_path]);
|
|
|
|
|
}
|
|
|
|
|
|
2016-10-15 06:12:57 +02:00
|
|
|
|
/**
|
2017-05-04 20:25:58 +02:00
|
|
|
|
* @param string $base_dir
|
2017-05-25 04:07:49 +02:00
|
|
|
|
* @param bool $is_diff
|
2017-05-27 02:16:18 +02:00
|
|
|
|
*
|
2016-10-15 06:12:57 +02:00
|
|
|
|
* @return void
|
|
|
|
|
*/
|
2017-05-04 20:25:58 +02:00
|
|
|
|
public function check($base_dir, $is_diff = false)
|
2016-06-13 21:33:18 +02:00
|
|
|
|
{
|
2016-11-06 05:59:29 +01:00
|
|
|
|
$start_checks = (int)microtime(true);
|
|
|
|
|
|
2017-05-04 20:25:58 +02:00
|
|
|
|
if (!$base_dir) {
|
|
|
|
|
throw new \InvalidArgumentException('Cannot work with empty base_dir');
|
2016-10-15 06:12:57 +02:00
|
|
|
|
}
|
|
|
|
|
|
2016-10-07 06:58:08 +02:00
|
|
|
|
$diff_files = null;
|
2016-10-07 19:26:29 +02:00
|
|
|
|
$deleted_files = null;
|
2016-10-05 19:24:46 +02:00
|
|
|
|
|
2019-04-27 23:38:24 +02:00
|
|
|
|
$this->full_run = true;
|
2019-04-17 19:15:06 +02:00
|
|
|
|
|
2018-10-17 17:03:32 +02:00
|
|
|
|
$reference_cache = $this->file_reference_provider->loadReferenceCache(true);
|
2018-10-07 02:11:19 +02:00
|
|
|
|
|
2018-09-28 22:18:45 +02:00
|
|
|
|
if ($is_diff
|
2018-10-07 02:11:19 +02:00
|
|
|
|
&& $reference_cache
|
2018-09-28 22:18:45 +02:00
|
|
|
|
&& $this->parser_cache_provider
|
|
|
|
|
&& $this->parser_cache_provider->canDiffFiles()
|
|
|
|
|
) {
|
|
|
|
|
$deleted_files = $this->file_reference_provider->getDeletedReferencedFiles();
|
2016-10-07 19:26:29 +02:00
|
|
|
|
$diff_files = $deleted_files;
|
2016-10-07 06:58:08 +02:00
|
|
|
|
|
2016-12-29 16:24:10 +01:00
|
|
|
|
foreach ($this->config->getProjectDirectories() as $dir_name) {
|
2017-07-25 22:11:02 +02:00
|
|
|
|
$diff_files = array_merge($diff_files, $this->getDiffFilesInDir($dir_name, $this->config));
|
2016-10-07 06:58:08 +02:00
|
|
|
|
}
|
2016-10-05 19:24:46 +02:00
|
|
|
|
}
|
2016-10-07 06:58:08 +02:00
|
|
|
|
|
2019-05-30 16:30:41 +02:00
|
|
|
|
$this->progress->startScanningFiles();
|
2018-02-04 21:27:03 +01:00
|
|
|
|
|
2019-04-27 23:38:24 +02:00
|
|
|
|
if ($diff_files === null
|
|
|
|
|
|| $deleted_files === null
|
|
|
|
|
|| count($diff_files) > 200
|
|
|
|
|
|| $this->codebase->find_unused_code) {
|
2019-04-29 18:07:34 +02:00
|
|
|
|
$this->codebase->scanner->addFilesToDeepScan($this->project_files);
|
2019-04-27 23:38:24 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ($diff_files === null
|
|
|
|
|
|| $deleted_files === null
|
|
|
|
|
|| count($diff_files) > 200
|
|
|
|
|
) {
|
2019-04-29 18:07:34 +02:00
|
|
|
|
$this->codebase->analyzer->addFiles($this->project_files);
|
2018-05-23 05:38:27 +02:00
|
|
|
|
|
2018-06-30 21:29:37 +02:00
|
|
|
|
$this->config->initializePlugins($this);
|
|
|
|
|
|
2018-10-11 19:58:39 +02:00
|
|
|
|
$this->codebase->scanFiles($this->threads);
|
2019-12-02 21:24:01 +01:00
|
|
|
|
|
|
|
|
|
$this->codebase->infer_types_from_usage = true;
|
2016-11-02 07:29:00 +01:00
|
|
|
|
} else {
|
2019-05-30 16:30:41 +02:00
|
|
|
|
$this->progress->debug(count($diff_files) . ' changed files: ' . "\n");
|
|
|
|
|
$this->progress->debug(' ' . implode("\n ", $diff_files) . "\n");
|
2016-06-21 01:30:38 +02:00
|
|
|
|
|
2019-04-27 23:38:24 +02:00
|
|
|
|
if ($diff_files || $this->codebase->find_unused_code) {
|
2018-09-28 22:18:45 +02:00
|
|
|
|
$file_list = $this->getReferencedFilesFromDiff($diff_files);
|
2016-11-02 07:29:00 +01:00
|
|
|
|
|
2018-02-20 05:36:29 +01:00
|
|
|
|
// strip out deleted files
|
|
|
|
|
$file_list = array_diff($file_list, $deleted_files);
|
2017-07-25 22:11:02 +02:00
|
|
|
|
|
2018-02-20 05:36:29 +01:00
|
|
|
|
$this->checkDiffFilesWithConfig($this->config, $file_list);
|
2017-01-12 07:12:01 +01:00
|
|
|
|
|
2018-06-30 21:29:37 +02:00
|
|
|
|
$this->config->initializePlugins($this);
|
|
|
|
|
|
2018-10-11 19:58:39 +02:00
|
|
|
|
$this->codebase->scanFiles($this->threads);
|
2018-02-20 05:36:29 +01:00
|
|
|
|
}
|
2018-02-04 21:27:03 +01:00
|
|
|
|
}
|
2017-02-15 06:15:51 +01:00
|
|
|
|
|
2019-05-30 16:30:41 +02:00
|
|
|
|
$this->config->visitStubFiles($this->codebase, $this->progress);
|
2018-06-30 21:29:37 +02:00
|
|
|
|
|
2019-02-24 17:40:06 +01:00
|
|
|
|
$plugin_classes = $this->config->after_codebase_populated;
|
|
|
|
|
|
|
|
|
|
if ($plugin_classes) {
|
|
|
|
|
foreach ($plugin_classes as $plugin_fq_class_name) {
|
|
|
|
|
$plugin_fq_class_name::afterCodebasePopulated($this->codebase);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2019-05-31 18:14:53 +02:00
|
|
|
|
$this->progress->startAnalyzingFiles();
|
|
|
|
|
|
2018-11-06 03:57:36 +01:00
|
|
|
|
$this->codebase->analyzer->analyzeFiles($this, $this->threads, $this->codebase->alter_code);
|
2018-02-04 21:27:03 +01:00
|
|
|
|
|
2018-09-28 22:18:45 +02:00
|
|
|
|
if ($this->parser_cache_provider) {
|
|
|
|
|
$removed_parser_files = $this->parser_cache_provider->deleteOldParserCaches(
|
|
|
|
|
$is_diff ? $this->parser_cache_provider->getLastGoodRun() : $start_checks
|
|
|
|
|
);
|
2016-11-06 05:59:29 +01:00
|
|
|
|
|
2019-05-30 16:30:41 +02:00
|
|
|
|
if ($removed_parser_files) {
|
|
|
|
|
$this->progress->debug('Removed ' . $removed_parser_files . ' old parser caches' . "\n");
|
2018-09-28 22:18:45 +02:00
|
|
|
|
}
|
2016-11-06 05:59:29 +01:00
|
|
|
|
|
2018-09-28 22:18:45 +02:00
|
|
|
|
if ($is_diff) {
|
|
|
|
|
$this->parser_cache_provider->touchParserCaches($this->getAllFiles($this->config), $start_checks);
|
|
|
|
|
}
|
2016-11-06 05:59:29 +01:00
|
|
|
|
}
|
2018-01-22 05:42:57 +01:00
|
|
|
|
}
|
2016-11-06 05:59:29 +01:00
|
|
|
|
|
2018-01-22 05:42:57 +01:00
|
|
|
|
/**
|
|
|
|
|
* @return void
|
|
|
|
|
*/
|
2019-12-02 21:24:01 +01:00
|
|
|
|
public function consolidateAnalyzedData()
|
2018-01-22 05:42:57 +01:00
|
|
|
|
{
|
2019-12-02 21:24:01 +01:00
|
|
|
|
$this->codebase->classlikes->consolidateAnalyzedData(
|
2019-04-17 22:41:35 +02:00
|
|
|
|
$this->codebase->methods,
|
2019-12-02 21:24:01 +01:00
|
|
|
|
$this->progress,
|
2019-12-24 02:02:34 +01:00
|
|
|
|
!!$this->codebase->find_unused_code
|
2019-04-17 22:41:35 +02:00
|
|
|
|
);
|
2018-01-22 05:42:57 +01:00
|
|
|
|
}
|
2017-02-27 07:30:44 +01:00
|
|
|
|
|
2019-08-04 16:37:36 +02:00
|
|
|
|
/**
|
|
|
|
|
* @return void
|
|
|
|
|
*/
|
|
|
|
|
public function trackTaintedInputs()
|
|
|
|
|
{
|
|
|
|
|
$this->codebase->taint = new Taint();
|
|
|
|
|
}
|
|
|
|
|
|
2019-08-18 20:27:50 +02:00
|
|
|
|
/**
|
|
|
|
|
* @return void
|
|
|
|
|
*/
|
|
|
|
|
public function trackUnusedSuppressions()
|
|
|
|
|
{
|
|
|
|
|
$this->codebase->track_unused_suppressions = true;
|
|
|
|
|
}
|
|
|
|
|
|
2019-06-03 05:33:57 +02:00
|
|
|
|
public function interpretRefactors() : void
|
|
|
|
|
{
|
|
|
|
|
if (!$this->codebase->alter_code) {
|
|
|
|
|
throw new \UnexpectedValueException('Should not be checking references');
|
|
|
|
|
}
|
|
|
|
|
|
2019-06-14 00:28:46 +02:00
|
|
|
|
// interpret wildcards
|
|
|
|
|
foreach ($this->to_refactor as $source => $destination) {
|
|
|
|
|
if (($source_pos = strpos($source, '*'))
|
|
|
|
|
&& ($destination_pos = strpos($destination, '*'))
|
|
|
|
|
&& $source_pos === (strlen($source) - 1)
|
|
|
|
|
&& $destination_pos === (strlen($destination) - 1)
|
|
|
|
|
) {
|
|
|
|
|
foreach ($this->codebase->classlike_storage_provider->getAll() as $class_storage) {
|
|
|
|
|
if (substr($class_storage->name, 0, $source_pos) === substr($source, 0, -1)) {
|
|
|
|
|
$this->to_refactor[$class_storage->name]
|
|
|
|
|
= substr($destination, 0, -1) . substr($class_storage->name, $source_pos);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
unset($this->to_refactor[$source]);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2019-06-03 05:33:57 +02:00
|
|
|
|
foreach ($this->to_refactor as $source => $destination) {
|
|
|
|
|
$source_parts = explode('::', $source);
|
|
|
|
|
$destination_parts = explode('::', $destination);
|
|
|
|
|
|
2019-06-04 22:36:32 +02:00
|
|
|
|
if (!$this->codebase->classlikes->hasFullyQualifiedClassName($source_parts[0])) {
|
2019-06-04 06:32:19 +02:00
|
|
|
|
throw new \Psalm\Exception\RefactorException(
|
|
|
|
|
'Source class ' . $source_parts[0] . ' doesn’t exist'
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2019-06-04 22:36:32 +02:00
|
|
|
|
if (count($source_parts) === 1 && count($destination_parts) === 1) {
|
|
|
|
|
if ($this->codebase->classlikes->hasFullyQualifiedClassName($destination_parts[0])) {
|
|
|
|
|
throw new \Psalm\Exception\RefactorException(
|
|
|
|
|
'Destination class ' . $destination_parts[0] . ' already exists'
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$source_class_storage = $this->codebase->classlike_storage_provider->get($source_parts[0]);
|
|
|
|
|
|
|
|
|
|
$destination_parts = explode('\\', $destination);
|
|
|
|
|
|
|
|
|
|
array_pop($destination_parts);
|
|
|
|
|
$destination_ns = implode('\\', $destination_parts);
|
|
|
|
|
|
|
|
|
|
$this->codebase->classes_to_move[strtolower($source)] = $destination;
|
|
|
|
|
|
|
|
|
|
$destination_class_storage = $this->codebase->classlike_storage_provider->create($destination);
|
|
|
|
|
|
|
|
|
|
$destination_class_storage->name = $destination;
|
|
|
|
|
|
|
|
|
|
if ($source_class_storage->aliases) {
|
|
|
|
|
$destination_class_storage->aliases = clone $source_class_storage->aliases;
|
|
|
|
|
$destination_class_storage->aliases->namespace = $destination_ns;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$destination_class_storage->location = $source_class_storage->location;
|
|
|
|
|
$destination_class_storage->stmt_location = $source_class_storage->stmt_location;
|
|
|
|
|
$destination_class_storage->populated = true;
|
|
|
|
|
|
|
|
|
|
$this->codebase->class_transforms[strtolower($source)] = $destination;
|
|
|
|
|
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
2020-02-15 02:54:26 +01:00
|
|
|
|
$source_method_id = new \Psalm\Internal\MethodIdentifier(
|
|
|
|
|
$source_parts[0],
|
|
|
|
|
strtolower($source_parts[1])
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if ($this->codebase->methods->methodExists($source_method_id)) {
|
|
|
|
|
if ($this->codebase->methods->methodExists(
|
|
|
|
|
new \Psalm\Internal\MethodIdentifier(
|
|
|
|
|
$destination_parts[0],
|
|
|
|
|
strtolower($destination_parts[1])
|
|
|
|
|
)
|
|
|
|
|
)) {
|
2019-06-03 05:33:57 +02:00
|
|
|
|
throw new \Psalm\Exception\RefactorException(
|
2019-06-04 06:32:19 +02:00
|
|
|
|
'Destination method ' . $destination . ' already exists'
|
2019-06-03 05:33:57 +02:00
|
|
|
|
);
|
2019-06-04 06:32:19 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!$this->codebase->classlikes->classExists($destination_parts[0])) {
|
2019-06-03 05:33:57 +02:00
|
|
|
|
throw new \Psalm\Exception\RefactorException(
|
|
|
|
|
'Destination class ' . $destination_parts[0] . ' doesn’t exist'
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (strtolower($source_parts[0]) !== strtolower($destination_parts[0])) {
|
2020-02-15 02:54:26 +01:00
|
|
|
|
$source_method_storage = $this->codebase->methods->getStorage($source_method_id);
|
2019-06-08 22:59:15 +02:00
|
|
|
|
$destination_class_storage
|
|
|
|
|
= $this->codebase->classlike_storage_provider->get($destination_parts[0]);
|
2019-06-03 05:33:57 +02:00
|
|
|
|
|
2019-06-08 22:59:15 +02:00
|
|
|
|
if (!$source_method_storage->is_static
|
2020-02-15 02:54:26 +01:00
|
|
|
|
&& !isset(
|
|
|
|
|
$destination_class_storage->parent_classes[strtolower($source_method_id->fq_class_name)]
|
|
|
|
|
)
|
2019-06-08 22:59:15 +02:00
|
|
|
|
) {
|
2019-06-03 05:33:57 +02:00
|
|
|
|
throw new \Psalm\Exception\RefactorException(
|
|
|
|
|
'Cannot move non-static method ' . $source
|
2019-06-08 22:59:15 +02:00
|
|
|
|
. ' into unrelated class ' . $destination_parts[0]
|
2019-06-03 05:33:57 +02:00
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2020-02-15 02:54:26 +01:00
|
|
|
|
$this->codebase->methods_to_move[strtolower($source)]= $destination;
|
2019-06-03 05:33:57 +02:00
|
|
|
|
} else {
|
|
|
|
|
$this->codebase->methods_to_rename[strtolower($source)] = $destination_parts[1];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$this->codebase->call_transforms[strtolower($source) . '\((.*\))'] = $destination . '($1)';
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
2019-06-04 06:32:19 +02:00
|
|
|
|
if ($source_parts[1][0] === '$') {
|
|
|
|
|
if ($destination_parts[1][0] !== '$') {
|
|
|
|
|
throw new \Psalm\Exception\RefactorException(
|
|
|
|
|
'Destination property must be of the form Foo::$bar'
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!$this->codebase->properties->propertyExists($source, true)) {
|
|
|
|
|
throw new \Psalm\Exception\RefactorException(
|
|
|
|
|
'Property ' . $source . ' does not exist'
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ($this->codebase->properties->propertyExists($destination, true)) {
|
|
|
|
|
throw new \Psalm\Exception\RefactorException(
|
|
|
|
|
'Destination property ' . $destination . ' already exists'
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!$this->codebase->classlikes->classExists($destination_parts[0])) {
|
|
|
|
|
throw new \Psalm\Exception\RefactorException(
|
|
|
|
|
'Destination class ' . $destination_parts[0] . ' doesn’t exist'
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$source_id = strtolower($source_parts[0]) . '::' . $source_parts[1];
|
|
|
|
|
|
|
|
|
|
if (strtolower($source_parts[0]) !== strtolower($destination_parts[0])) {
|
|
|
|
|
$source_storage = $this->codebase->properties->getStorage($source);
|
|
|
|
|
|
|
|
|
|
if (!$source_storage->is_static) {
|
|
|
|
|
throw new \Psalm\Exception\RefactorException(
|
|
|
|
|
'Cannot move non-static property ' . $source
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$this->codebase->properties_to_move[$source_id] = $destination;
|
|
|
|
|
} else {
|
|
|
|
|
$this->codebase->properties_to_rename[$source_id] = substr($destination_parts[1], 1);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$this->codebase->property_transforms[$source_id] = $destination;
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$source_class_constants = $this->codebase->classlikes->getConstantsForClass(
|
|
|
|
|
$source_parts[0],
|
|
|
|
|
\ReflectionProperty::IS_PRIVATE
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if (isset($source_class_constants[$source_parts[1]])) {
|
2019-06-04 22:36:32 +02:00
|
|
|
|
if (!$this->codebase->classlikes->hasFullyQualifiedClassName($destination_parts[0])) {
|
2019-06-04 06:32:19 +02:00
|
|
|
|
throw new \Psalm\Exception\RefactorException(
|
|
|
|
|
'Destination class ' . $destination_parts[0] . ' doesn’t exist'
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$destination_class_constants = $this->codebase->classlikes->getConstantsForClass(
|
|
|
|
|
$destination_parts[0],
|
|
|
|
|
\ReflectionProperty::IS_PRIVATE
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if (isset($destination_class_constants[$destination_parts[1]])) {
|
|
|
|
|
throw new \Psalm\Exception\RefactorException(
|
|
|
|
|
'Destination constant ' . $destination . ' already exists'
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$source_id = strtolower($source_parts[0]) . '::' . $source_parts[1];
|
|
|
|
|
|
|
|
|
|
if (strtolower($source_parts[0]) !== strtolower($destination_parts[0])) {
|
|
|
|
|
$this->codebase->class_constants_to_move[$source_id] = $destination;
|
|
|
|
|
} else {
|
|
|
|
|
$this->codebase->class_constants_to_rename[$source_id] = $destination_parts[1];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$this->codebase->class_constant_transforms[$source_id] = $destination;
|
2019-06-04 17:14:49 +02:00
|
|
|
|
continue;
|
2019-06-04 06:32:19 +02:00
|
|
|
|
}
|
|
|
|
|
|
2019-06-03 05:33:57 +02:00
|
|
|
|
throw new \Psalm\Exception\RefactorException(
|
2019-06-04 06:32:19 +02:00
|
|
|
|
'Psalm cannot locate ' . $source
|
2019-06-03 05:33:57 +02:00
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2019-06-01 06:56:54 +02:00
|
|
|
|
public function prepareMigration() : void
|
|
|
|
|
{
|
|
|
|
|
if (!$this->codebase->alter_code) {
|
|
|
|
|
throw new \UnexpectedValueException('Should not be checking references');
|
|
|
|
|
}
|
|
|
|
|
|
2019-06-02 18:02:32 +02:00
|
|
|
|
$this->codebase->classlikes->moveMethods(
|
2019-06-01 06:56:54 +02:00
|
|
|
|
$this->codebase->methods,
|
|
|
|
|
$this->progress
|
|
|
|
|
);
|
2019-06-04 06:32:19 +02:00
|
|
|
|
|
|
|
|
|
$this->codebase->classlikes->moveProperties(
|
|
|
|
|
$this->codebase->properties,
|
|
|
|
|
$this->progress
|
|
|
|
|
);
|
2019-06-04 17:14:49 +02:00
|
|
|
|
|
2019-06-04 22:36:32 +02:00
|
|
|
|
$this->codebase->classlikes->moveClassConstants(
|
2019-06-04 17:14:49 +02:00
|
|
|
|
$this->progress
|
|
|
|
|
);
|
2019-06-01 06:56:54 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function migrateCode() : void
|
|
|
|
|
{
|
|
|
|
|
if (!$this->codebase->alter_code) {
|
|
|
|
|
throw new \UnexpectedValueException('Should not be checking references');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$migration_manipulations = \Psalm\Internal\FileManipulation\FileManipulationBuffer::getMigrationManipulations(
|
|
|
|
|
$this->codebase->file_provider
|
|
|
|
|
);
|
|
|
|
|
|
2019-06-05 06:14:25 +02:00
|
|
|
|
if ($migration_manipulations) {
|
|
|
|
|
foreach ($migration_manipulations as $file_path => $file_manipulations) {
|
|
|
|
|
usort(
|
|
|
|
|
$file_manipulations,
|
|
|
|
|
/**
|
|
|
|
|
* @return int
|
|
|
|
|
*/
|
|
|
|
|
function (FileManipulation $a, FileManipulation $b) {
|
|
|
|
|
if ($a->start === $b->start) {
|
|
|
|
|
if ($b->end === $a->end) {
|
|
|
|
|
return $b->insertion_text > $a->insertion_text ? 1 : -1;
|
|
|
|
|
}
|
2019-06-01 06:56:54 +02:00
|
|
|
|
|
2019-06-05 06:14:25 +02:00
|
|
|
|
return $b->end > $a->end ? 1 : -1;
|
2019-06-01 06:56:54 +02:00
|
|
|
|
}
|
|
|
|
|
|
2019-06-05 06:14:25 +02:00
|
|
|
|
return $b->start > $a->start ? 1 : -1;
|
2019-06-01 06:56:54 +02:00
|
|
|
|
}
|
2019-06-05 06:14:25 +02:00
|
|
|
|
);
|
2019-06-01 06:56:54 +02:00
|
|
|
|
|
2019-06-05 06:14:25 +02:00
|
|
|
|
$existing_contents = $this->codebase->file_provider->getContents($file_path);
|
2019-06-01 06:56:54 +02:00
|
|
|
|
|
2019-06-05 06:14:25 +02:00
|
|
|
|
foreach ($file_manipulations as $manipulation) {
|
|
|
|
|
$existing_contents = $manipulation->transform($existing_contents);
|
|
|
|
|
}
|
2019-06-01 06:56:54 +02:00
|
|
|
|
|
2019-06-05 06:14:25 +02:00
|
|
|
|
$this->codebase->file_provider->setContents($file_path, $existing_contents);
|
2019-06-01 06:56:54 +02:00
|
|
|
|
}
|
2019-06-05 06:14:25 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ($this->codebase->classes_to_move) {
|
|
|
|
|
foreach ($this->codebase->classes_to_move as $source => $destination) {
|
|
|
|
|
$source_class_storage = $this->codebase->classlike_storage_provider->get($source);
|
|
|
|
|
|
|
|
|
|
if (!$source_class_storage->location) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
2019-06-01 06:56:54 +02:00
|
|
|
|
|
2019-06-05 06:14:25 +02:00
|
|
|
|
$potential_file_path = $this->config->getPotentialComposerFilePathForClassLike($destination);
|
|
|
|
|
|
|
|
|
|
if ($potential_file_path && !file_exists($potential_file_path)) {
|
2019-06-18 23:16:10 +02:00
|
|
|
|
$containing_dir = dirname($potential_file_path);
|
|
|
|
|
|
|
|
|
|
if (!file_exists($containing_dir)) {
|
|
|
|
|
mkdir($containing_dir, 0777, true);
|
|
|
|
|
}
|
|
|
|
|
|
2019-06-05 06:14:25 +02:00
|
|
|
|
rename($source_class_storage->location->file_path, $potential_file_path);
|
|
|
|
|
}
|
|
|
|
|
}
|
2019-06-01 06:56:54 +02:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2018-01-22 05:42:57 +01:00
|
|
|
|
/**
|
|
|
|
|
* @param string $symbol
|
|
|
|
|
*
|
|
|
|
|
* @return void
|
|
|
|
|
*/
|
|
|
|
|
public function findReferencesTo($symbol)
|
|
|
|
|
{
|
2019-06-09 18:37:28 +02:00
|
|
|
|
if (!$this->stdout_report_options) {
|
|
|
|
|
throw new \UnexpectedValueException('Not expecting to emit output');
|
|
|
|
|
}
|
|
|
|
|
|
2019-04-13 00:28:07 +02:00
|
|
|
|
$locations = $this->codebase->findReferencesToSymbol($symbol);
|
2017-02-27 07:30:44 +01:00
|
|
|
|
|
2019-04-13 00:28:07 +02:00
|
|
|
|
foreach ($locations as $location) {
|
|
|
|
|
$snippet = $location->getSnippet();
|
2017-03-02 04:27:52 +01:00
|
|
|
|
|
2019-04-13 00:28:07 +02:00
|
|
|
|
$snippet_bounds = $location->getSnippetBounds();
|
|
|
|
|
$selection_bounds = $location->getSelectionBounds();
|
2017-03-02 04:27:52 +01:00
|
|
|
|
|
2019-04-13 00:28:07 +02:00
|
|
|
|
$selection_start = $selection_bounds[0] - $snippet_bounds[0];
|
|
|
|
|
$selection_length = $selection_bounds[1] - $selection_bounds[0];
|
2017-02-27 07:30:44 +01:00
|
|
|
|
|
2019-04-13 00:28:07 +02:00
|
|
|
|
echo $location->file_name . ':' . $location->getLineNumber() . "\n" .
|
|
|
|
|
(
|
2019-06-09 18:37:28 +02:00
|
|
|
|
$this->stdout_report_options->use_color
|
2019-04-13 00:28:07 +02:00
|
|
|
|
? substr($snippet, 0, $selection_start) .
|
|
|
|
|
"\e[97;42m" . substr($snippet, $selection_start, $selection_length) .
|
|
|
|
|
"\e[0m" . substr($snippet, $selection_length + $selection_start)
|
|
|
|
|
: $snippet
|
|
|
|
|
) . "\n" . "\n";
|
2017-01-30 05:44:05 +01:00
|
|
|
|
}
|
2017-01-02 21:31:18 +01:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2018-01-21 19:38:51 +01:00
|
|
|
|
* @param string $dir_name
|
|
|
|
|
*
|
2017-01-02 21:31:18 +01:00
|
|
|
|
* @return void
|
|
|
|
|
*/
|
2018-01-21 19:38:51 +01:00
|
|
|
|
public function checkDir($dir_name)
|
2017-01-02 21:31:18 +01:00
|
|
|
|
{
|
2018-09-28 22:18:45 +02:00
|
|
|
|
$this->file_reference_provider->loadReferenceCache();
|
2017-01-02 21:31:18 +01:00
|
|
|
|
|
2018-01-21 19:38:51 +01:00
|
|
|
|
$this->checkDirWithConfig($dir_name, $this->config, true);
|
2017-07-25 22:11:02 +02:00
|
|
|
|
|
2019-05-30 16:30:41 +02:00
|
|
|
|
$this->progress->startScanningFiles();
|
2018-02-04 21:27:03 +01:00
|
|
|
|
|
2018-06-30 21:29:37 +02:00
|
|
|
|
$this->config->initializePlugins($this);
|
|
|
|
|
|
2018-10-11 19:58:39 +02:00
|
|
|
|
$this->codebase->scanFiles($this->threads);
|
2018-02-04 21:27:03 +01:00
|
|
|
|
|
2019-05-30 16:30:41 +02:00
|
|
|
|
$this->config->visitStubFiles($this->codebase, $this->progress);
|
2018-06-30 21:29:37 +02:00
|
|
|
|
|
2019-05-30 16:30:41 +02:00
|
|
|
|
$this->progress->startAnalyzingFiles();
|
2018-02-04 21:27:03 +01:00
|
|
|
|
|
2018-11-06 03:57:36 +01:00
|
|
|
|
$this->codebase->analyzer->analyzeFiles($this, $this->threads, $this->codebase->alter_code);
|
2018-01-21 19:38:51 +01:00
|
|
|
|
}
|
2017-07-25 22:11:02 +02:00
|
|
|
|
|
2018-01-21 19:38:51 +01:00
|
|
|
|
/**
|
|
|
|
|
* @param string $dir_name
|
|
|
|
|
* @param Config $config
|
|
|
|
|
* @param bool $allow_non_project_files
|
|
|
|
|
*
|
|
|
|
|
* @return void
|
|
|
|
|
*/
|
|
|
|
|
private function checkDirWithConfig($dir_name, Config $config, $allow_non_project_files = false)
|
|
|
|
|
{
|
|
|
|
|
$file_extensions = $config->getFileExtensions();
|
2017-07-25 22:11:02 +02:00
|
|
|
|
|
2018-10-17 17:03:32 +02:00
|
|
|
|
$file_paths = $this->file_provider->getFilesInDir($dir_name, $file_extensions);
|
2017-07-25 22:11:02 +02:00
|
|
|
|
|
2018-01-21 19:38:51 +01:00
|
|
|
|
$files_to_scan = [];
|
2017-07-25 22:11:02 +02:00
|
|
|
|
|
2018-10-17 17:03:32 +02:00
|
|
|
|
foreach ($file_paths as $file_path) {
|
|
|
|
|
if ($allow_non_project_files || $config->isInProjectDirs($file_path)) {
|
|
|
|
|
$files_to_scan[$file_path] = $file_path;
|
2017-07-25 22:11:02 +02:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2018-02-04 00:52:35 +01:00
|
|
|
|
$this->codebase->addFilesToAnalyze($files_to_scan);
|
2018-01-21 19:38:51 +01:00
|
|
|
|
}
|
2017-07-25 22:11:02 +02:00
|
|
|
|
|
2018-01-21 19:38:51 +01:00
|
|
|
|
/**
|
|
|
|
|
* @param Config $config
|
|
|
|
|
*
|
|
|
|
|
* @return array<int, string>
|
|
|
|
|
*/
|
|
|
|
|
private function getAllFiles(Config $config)
|
|
|
|
|
{
|
|
|
|
|
$file_extensions = $config->getFileExtensions();
|
2018-10-17 17:03:32 +02:00
|
|
|
|
$file_paths = [];
|
2017-07-25 22:11:02 +02:00
|
|
|
|
|
2018-01-21 19:38:51 +01:00
|
|
|
|
foreach ($config->getProjectDirectories() as $dir_name) {
|
2018-10-17 17:03:32 +02:00
|
|
|
|
$file_paths = array_merge(
|
|
|
|
|
$file_paths,
|
|
|
|
|
$this->file_provider->getFilesInDir($dir_name, $file_extensions)
|
|
|
|
|
);
|
2017-07-25 22:11:02 +02:00
|
|
|
|
}
|
|
|
|
|
|
2018-10-17 17:03:32 +02:00
|
|
|
|
return $file_paths;
|
2017-07-25 22:11:02 +02:00
|
|
|
|
}
|
|
|
|
|
|
2018-01-09 17:49:10 +01:00
|
|
|
|
/**
|
2018-01-21 19:38:51 +01:00
|
|
|
|
* @param string $dir_name
|
|
|
|
|
* @param Config $config
|
|
|
|
|
*
|
|
|
|
|
* @return array<string>
|
2018-01-09 17:49:10 +01:00
|
|
|
|
*/
|
2018-01-21 19:38:51 +01:00
|
|
|
|
protected function getDiffFilesInDir($dir_name, Config $config)
|
2018-01-09 17:49:10 +01:00
|
|
|
|
{
|
2018-01-21 19:38:51 +01:00
|
|
|
|
$file_extensions = $config->getFileExtensions();
|
|
|
|
|
|
2018-09-28 22:18:45 +02:00
|
|
|
|
if (!$this->parser_cache_provider) {
|
|
|
|
|
throw new \UnexpectedValueException('Parser cache provider cannot be null here');
|
|
|
|
|
}
|
|
|
|
|
|
2018-01-21 19:38:51 +01:00
|
|
|
|
$diff_files = [];
|
|
|
|
|
|
2018-09-28 22:18:45 +02:00
|
|
|
|
$last_good_run = $this->parser_cache_provider->getLastGoodRun();
|
|
|
|
|
|
2018-10-17 17:03:32 +02:00
|
|
|
|
$file_paths = $this->file_provider->getFilesInDir($dir_name, $file_extensions);
|
|
|
|
|
|
|
|
|
|
foreach ($file_paths as $file_path) {
|
|
|
|
|
if ($config->isInProjectDirs($file_path)) {
|
2020-03-18 18:36:11 +01:00
|
|
|
|
if ($this->file_provider->getModifiedTime($file_path) > $last_good_run
|
|
|
|
|
&& $this->parser_cache_provider->loadExistingFileContentsFromCache($file_path)
|
|
|
|
|
!== $this->file_provider->getContents($file_path)
|
|
|
|
|
) {
|
2018-10-17 17:03:32 +02:00
|
|
|
|
$diff_files[] = $file_path;
|
2018-01-21 19:38:51 +01:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $diff_files;
|
2018-01-09 17:49:10 +01:00
|
|
|
|
}
|
|
|
|
|
|
2017-07-25 22:11:02 +02:00
|
|
|
|
/**
|
2018-01-21 19:38:51 +01:00
|
|
|
|
* @param Config $config
|
|
|
|
|
* @param array<string> $file_list
|
2017-07-25 22:11:02 +02:00
|
|
|
|
*
|
|
|
|
|
* @return void
|
|
|
|
|
*/
|
2018-01-21 19:38:51 +01:00
|
|
|
|
private function checkDiffFilesWithConfig(Config $config, array $file_list = [])
|
2017-07-25 22:11:02 +02:00
|
|
|
|
{
|
2018-01-21 19:38:51 +01:00
|
|
|
|
$files_to_scan = [];
|
2017-07-25 22:11:02 +02:00
|
|
|
|
|
2018-01-21 19:38:51 +01:00
|
|
|
|
foreach ($file_list as $file_path) {
|
2018-10-17 17:03:32 +02:00
|
|
|
|
if (!$this->file_provider->fileExists($file_path)) {
|
2018-01-21 19:38:51 +01:00
|
|
|
|
continue;
|
2017-12-22 18:56:59 +01:00
|
|
|
|
}
|
|
|
|
|
|
2018-01-21 19:38:51 +01:00
|
|
|
|
if (!$config->isInProjectDirs($file_path)) {
|
2019-05-30 16:30:41 +02:00
|
|
|
|
$this->progress->debug('skipping ' . $file_path . "\n");
|
2017-07-29 21:05:06 +02:00
|
|
|
|
|
2017-12-29 17:26:28 +01:00
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
2018-01-21 19:38:51 +01:00
|
|
|
|
$files_to_scan[$file_path] = $file_path;
|
2017-12-29 17:26:28 +01:00
|
|
|
|
}
|
|
|
|
|
|
2018-02-04 00:52:35 +01:00
|
|
|
|
$this->codebase->addFilesToAnalyze($files_to_scan);
|
2018-01-21 19:38:51 +01:00
|
|
|
|
}
|
2017-07-25 22:11:02 +02:00
|
|
|
|
|
2018-01-21 19:38:51 +01:00
|
|
|
|
/**
|
|
|
|
|
* @param string $file_path
|
|
|
|
|
*
|
|
|
|
|
* @return void
|
|
|
|
|
*/
|
|
|
|
|
public function checkFile($file_path)
|
|
|
|
|
{
|
2019-05-30 16:30:41 +02:00
|
|
|
|
$this->progress->debug('Checking ' . $file_path . "\n");
|
2017-07-25 22:11:02 +02:00
|
|
|
|
|
2018-01-21 19:38:51 +01:00
|
|
|
|
$this->config->hide_external_errors = $this->config->isInProjectDirs($file_path);
|
2017-07-25 22:11:02 +02:00
|
|
|
|
|
2018-02-04 00:52:35 +01:00
|
|
|
|
$this->codebase->addFilesToAnalyze([$file_path => $file_path]);
|
2017-07-25 22:11:02 +02:00
|
|
|
|
|
2018-09-28 22:18:45 +02:00
|
|
|
|
$this->file_reference_provider->loadReferenceCache();
|
2017-07-25 22:11:02 +02:00
|
|
|
|
|
2019-05-30 16:30:41 +02:00
|
|
|
|
$this->progress->startScanningFiles();
|
2018-02-04 21:27:03 +01:00
|
|
|
|
|
2018-06-30 21:29:37 +02:00
|
|
|
|
$this->config->initializePlugins($this);
|
|
|
|
|
|
2018-10-11 19:58:39 +02:00
|
|
|
|
$this->codebase->scanFiles($this->threads);
|
2018-02-04 21:27:03 +01:00
|
|
|
|
|
2019-05-30 16:30:41 +02:00
|
|
|
|
$this->config->visitStubFiles($this->codebase, $this->progress);
|
2018-06-30 21:29:37 +02:00
|
|
|
|
|
2019-05-30 16:30:41 +02:00
|
|
|
|
$this->progress->startAnalyzingFiles();
|
2018-02-04 21:27:03 +01:00
|
|
|
|
|
2018-11-06 03:57:36 +01:00
|
|
|
|
$this->codebase->analyzer->analyzeFiles($this, $this->threads, $this->codebase->alter_code);
|
2018-01-21 19:38:51 +01:00
|
|
|
|
}
|
2017-01-12 06:54:41 +01:00
|
|
|
|
|
2018-08-15 17:57:40 +02:00
|
|
|
|
/**
|
|
|
|
|
* @param string[] $paths_to_check
|
|
|
|
|
* @return void
|
|
|
|
|
*/
|
|
|
|
|
public function checkPaths(array $paths_to_check)
|
|
|
|
|
{
|
|
|
|
|
foreach ($paths_to_check as $path) {
|
2019-05-30 16:30:41 +02:00
|
|
|
|
$this->progress->debug('Checking ' . $path . "\n");
|
2018-08-15 17:57:40 +02:00
|
|
|
|
|
|
|
|
|
if (is_dir($path)) {
|
|
|
|
|
$this->checkDirWithConfig($path, $this->config, true);
|
|
|
|
|
} elseif (is_file($path)) {
|
|
|
|
|
$this->codebase->addFilesToAnalyze([$path => $path]);
|
|
|
|
|
$this->config->hide_external_errors = $this->config->isInProjectDirs($path);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2018-09-28 22:18:45 +02:00
|
|
|
|
$this->file_reference_provider->loadReferenceCache();
|
2018-08-15 17:57:40 +02:00
|
|
|
|
|
2019-05-30 16:30:41 +02:00
|
|
|
|
$this->progress->startScanningFiles();
|
2018-08-15 17:57:40 +02:00
|
|
|
|
|
|
|
|
|
$this->config->initializePlugins($this);
|
|
|
|
|
|
2018-10-11 19:58:39 +02:00
|
|
|
|
$this->codebase->scanFiles($this->threads);
|
2018-08-15 17:57:40 +02:00
|
|
|
|
|
2019-05-30 16:30:41 +02:00
|
|
|
|
$this->config->visitStubFiles($this->codebase, $this->progress);
|
2018-08-15 17:57:40 +02:00
|
|
|
|
|
2019-05-30 16:30:41 +02:00
|
|
|
|
$this->progress->startAnalyzingFiles();
|
2018-08-15 17:57:40 +02:00
|
|
|
|
|
2018-11-06 03:57:36 +01:00
|
|
|
|
$this->codebase->analyzer->analyzeFiles($this, $this->threads, $this->codebase->alter_code);
|
2019-04-30 19:23:18 +02:00
|
|
|
|
|
2019-06-09 18:37:28 +02:00
|
|
|
|
if ($this->stdout_report_options
|
|
|
|
|
&& $this->stdout_report_options->format === Report::TYPE_CONSOLE
|
|
|
|
|
&& $this->codebase->collect_references
|
|
|
|
|
) {
|
2019-05-30 16:30:41 +02:00
|
|
|
|
fwrite(
|
|
|
|
|
STDERR,
|
|
|
|
|
PHP_EOL . 'To whom it may concern: Psalm cannot detect unused classes, methods and properties'
|
2019-04-30 19:26:11 +02:00
|
|
|
|
. PHP_EOL . 'when analyzing individual files and folders. Run on the full project to enable'
|
2019-05-30 16:30:41 +02:00
|
|
|
|
. PHP_EOL . 'complete unused code detection.' . PHP_EOL
|
|
|
|
|
);
|
2019-04-30 19:23:18 +02:00
|
|
|
|
}
|
2018-08-15 17:57:40 +02:00
|
|
|
|
}
|
|
|
|
|
|
2016-06-26 19:45:20 +02:00
|
|
|
|
/**
|
|
|
|
|
* @return Config
|
|
|
|
|
*/
|
2018-01-02 02:04:03 +01:00
|
|
|
|
public function getConfig()
|
|
|
|
|
{
|
|
|
|
|
return $this->config;
|
|
|
|
|
}
|
|
|
|
|
|
2016-10-15 06:12:57 +02:00
|
|
|
|
/**
|
|
|
|
|
* @param array<string> $diff_files
|
2017-05-27 02:16:18 +02:00
|
|
|
|
*
|
2018-10-07 02:11:19 +02:00
|
|
|
|
* @return array<string, string>
|
2016-10-15 06:12:57 +02:00
|
|
|
|
*/
|
2018-10-18 00:11:02 +02:00
|
|
|
|
public function getReferencedFilesFromDiff(array $diff_files, bool $include_referencing_files = true)
|
2016-10-05 19:24:46 +02:00
|
|
|
|
{
|
2016-10-05 23:08:20 +02:00
|
|
|
|
$all_inherited_files_to_check = $diff_files;
|
2016-10-05 19:24:46 +02:00
|
|
|
|
|
|
|
|
|
while ($diff_files) {
|
|
|
|
|
$diff_file = array_shift($diff_files);
|
|
|
|
|
|
2018-09-28 22:18:45 +02:00
|
|
|
|
$dependent_files = $this->file_reference_provider->getFilesInheritingFromFile($diff_file);
|
2018-10-18 00:11:02 +02:00
|
|
|
|
|
2016-10-05 23:08:20 +02:00
|
|
|
|
$new_dependent_files = array_diff($dependent_files, $all_inherited_files_to_check);
|
|
|
|
|
|
2018-10-18 00:11:02 +02:00
|
|
|
|
$all_inherited_files_to_check = array_merge($all_inherited_files_to_check, $new_dependent_files);
|
|
|
|
|
$diff_files = array_merge($diff_files, $new_dependent_files);
|
2016-10-05 23:08:20 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$all_files_to_check = $all_inherited_files_to_check;
|
|
|
|
|
|
2018-10-18 00:11:02 +02:00
|
|
|
|
if ($include_referencing_files) {
|
|
|
|
|
foreach ($all_inherited_files_to_check as $file_name) {
|
|
|
|
|
$dependent_files = $this->file_reference_provider->getFilesReferencingFile($file_name);
|
|
|
|
|
$all_files_to_check = array_merge($dependent_files, $all_files_to_check);
|
|
|
|
|
}
|
2016-10-05 19:24:46 +02:00
|
|
|
|
}
|
|
|
|
|
|
2018-10-07 02:11:19 +02:00
|
|
|
|
return array_combine($all_files_to_check, $all_files_to_check);
|
2016-10-05 19:24:46 +02:00
|
|
|
|
}
|
2016-12-08 04:38:57 +01:00
|
|
|
|
|
2017-07-25 22:11:02 +02:00
|
|
|
|
/**
|
|
|
|
|
* @param string $file_path
|
|
|
|
|
*
|
|
|
|
|
* @return bool
|
|
|
|
|
*/
|
|
|
|
|
public function fileExists($file_path)
|
2017-01-19 18:15:42 +01:00
|
|
|
|
{
|
2017-07-25 22:11:02 +02:00
|
|
|
|
return $this->file_provider->fileExists($file_path);
|
2017-01-19 18:15:42 +01:00
|
|
|
|
}
|
|
|
|
|
|
2018-01-06 01:49:27 +01:00
|
|
|
|
/**
|
2018-01-07 06:11:23 +01:00
|
|
|
|
* @param int $php_major_version
|
|
|
|
|
* @param int $php_minor_version
|
2018-01-07 17:48:33 +01:00
|
|
|
|
* @param bool $dry_run
|
2018-01-07 22:14:16 +01:00
|
|
|
|
* @param bool $safe_types
|
2018-01-07 06:11:23 +01:00
|
|
|
|
*
|
2018-01-06 01:49:27 +01:00
|
|
|
|
* @return void
|
|
|
|
|
*/
|
2018-01-07 22:11:51 +01:00
|
|
|
|
public function alterCodeAfterCompletion(
|
|
|
|
|
$dry_run = false,
|
|
|
|
|
$safe_types = false
|
|
|
|
|
) {
|
2018-11-06 03:57:36 +01:00
|
|
|
|
$this->codebase->alter_code = true;
|
2019-02-02 17:28:48 +01:00
|
|
|
|
$this->codebase->infer_types_from_usage = true;
|
2018-03-16 16:15:01 +01:00
|
|
|
|
$this->show_issues = false;
|
2018-01-07 17:48:33 +01:00
|
|
|
|
$this->dry_run = $dry_run;
|
2018-01-07 22:11:51 +01:00
|
|
|
|
$this->only_replace_php_types_with_non_docblock_types = $safe_types;
|
2018-01-02 03:17:23 +01:00
|
|
|
|
}
|
2018-01-05 19:22:48 +01:00
|
|
|
|
|
2019-06-02 07:10:50 +02:00
|
|
|
|
/**
|
2019-06-03 05:33:57 +02:00
|
|
|
|
* @param array<string, string> $to_refactor
|
2019-06-02 07:10:50 +02:00
|
|
|
|
*
|
|
|
|
|
* @return void
|
|
|
|
|
*/
|
2019-06-03 05:33:57 +02:00
|
|
|
|
public function refactorCodeAfterCompletion(array $to_refactor)
|
2019-06-02 07:10:50 +02:00
|
|
|
|
{
|
2019-06-03 05:33:57 +02:00
|
|
|
|
$this->to_refactor = $to_refactor;
|
2019-06-02 07:10:50 +02:00
|
|
|
|
$this->codebase->alter_code = true;
|
|
|
|
|
$this->show_issues = false;
|
|
|
|
|
}
|
|
|
|
|
|
2019-02-07 18:25:57 +01:00
|
|
|
|
/**
|
|
|
|
|
* @return void
|
|
|
|
|
*/
|
|
|
|
|
public function setPhpVersion(string $version)
|
|
|
|
|
{
|
2019-12-28 01:06:09 +01:00
|
|
|
|
if (!preg_match('/^(5\.[456]|7\.[01234]|8\.[0])(\..*)?$/', $version)) {
|
2019-02-07 18:25:57 +01:00
|
|
|
|
throw new \UnexpectedValueException('Expecting a version number in the format x.y');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
list($php_major_version, $php_minor_version) = explode('.', $version);
|
|
|
|
|
|
|
|
|
|
$this->codebase->php_major_version = (int) $php_major_version;
|
|
|
|
|
$this->codebase->php_minor_version = (int) $php_minor_version;
|
|
|
|
|
}
|
|
|
|
|
|
2018-01-05 19:22:48 +01:00
|
|
|
|
/**
|
2018-01-06 01:49:27 +01:00
|
|
|
|
* @param array<string, bool> $issues
|
2019-05-27 16:05:15 +02:00
|
|
|
|
* @throws UnsupportedIssueToFixException
|
2018-01-06 01:49:27 +01:00
|
|
|
|
*
|
2018-01-05 19:22:48 +01:00
|
|
|
|
* @return void
|
|
|
|
|
*/
|
2018-01-07 06:11:23 +01:00
|
|
|
|
public function setIssuesToFix(array $issues)
|
2018-01-06 01:49:27 +01:00
|
|
|
|
{
|
2019-05-27 16:05:15 +02:00
|
|
|
|
$supported_issues_to_fix = static::getSupportedIssuesToFix();
|
|
|
|
|
|
|
|
|
|
$unsupportedIssues = array_diff(array_keys($issues), $supported_issues_to_fix);
|
|
|
|
|
|
|
|
|
|
if (! empty($unsupportedIssues)) {
|
|
|
|
|
throw new UnsupportedIssueToFixException(
|
|
|
|
|
'Psalm doesn\'t know how to fix issue(s): ' . implode(', ', $unsupportedIssues) . PHP_EOL
|
|
|
|
|
. 'Supported issues to fix are: ' . implode(',', $supported_issues_to_fix)
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2018-01-06 01:49:27 +01:00
|
|
|
|
$this->issues_to_fix = $issues;
|
|
|
|
|
}
|
|
|
|
|
|
2019-05-27 16:05:15 +02:00
|
|
|
|
public function setAllIssuesToFix(): void
|
|
|
|
|
{
|
|
|
|
|
$keyed_issues = array_fill_keys(static::getSupportedIssuesToFix(), true);
|
|
|
|
|
|
|
|
|
|
$this->setIssuesToFix($keyed_issues);
|
|
|
|
|
}
|
|
|
|
|
|
2018-01-06 01:49:27 +01:00
|
|
|
|
/**
|
|
|
|
|
* @return array<string, bool>
|
|
|
|
|
*/
|
|
|
|
|
public function getIssuesToFix()
|
2018-01-05 19:22:48 +01:00
|
|
|
|
{
|
2018-01-06 01:49:27 +01:00
|
|
|
|
return $this->issues_to_fix;
|
2018-01-05 19:22:48 +01:00
|
|
|
|
}
|
2018-01-21 18:44:46 +01:00
|
|
|
|
|
|
|
|
|
/**
|
2018-01-21 19:38:51 +01:00
|
|
|
|
* @return Codebase
|
2018-01-21 18:44:46 +01:00
|
|
|
|
*/
|
2018-01-21 19:38:51 +01:00
|
|
|
|
public function getCodebase()
|
2018-01-21 18:44:46 +01:00
|
|
|
|
{
|
2018-01-21 19:38:51 +01:00
|
|
|
|
return $this->codebase;
|
2018-01-21 18:44:46 +01:00
|
|
|
|
}
|
|
|
|
|
|
2018-02-04 00:52:35 +01:00
|
|
|
|
/**
|
|
|
|
|
* @param string $fq_class_name
|
|
|
|
|
*
|
2018-11-06 03:57:36 +01:00
|
|
|
|
* @return FileAnalyzer
|
2018-02-04 00:52:35 +01:00
|
|
|
|
*/
|
2018-11-06 03:57:36 +01:00
|
|
|
|
public function getFileAnalyzerForClassLike($fq_class_name)
|
2018-02-04 00:52:35 +01:00
|
|
|
|
{
|
|
|
|
|
$fq_class_name_lc = strtolower($fq_class_name);
|
|
|
|
|
|
|
|
|
|
$file_path = $this->codebase->scanner->getClassLikeFilePath($fq_class_name_lc);
|
|
|
|
|
|
2018-11-11 18:01:14 +01:00
|
|
|
|
$file_analyzer = new FileAnalyzer(
|
2018-02-04 00:52:35 +01:00
|
|
|
|
$this,
|
|
|
|
|
$file_path,
|
|
|
|
|
$this->config->shortenFileName($file_path)
|
|
|
|
|
);
|
|
|
|
|
|
2018-11-11 18:01:14 +01:00
|
|
|
|
return $file_analyzer;
|
2018-02-04 00:52:35 +01:00
|
|
|
|
}
|
|
|
|
|
|
2018-01-21 18:44:46 +01:00
|
|
|
|
/**
|
2018-01-21 19:38:51 +01:00
|
|
|
|
* @param Context $this_context
|
2018-01-21 18:44:46 +01:00
|
|
|
|
*
|
2018-01-21 19:38:51 +01:00
|
|
|
|
* @return void
|
2018-01-21 18:44:46 +01:00
|
|
|
|
*/
|
2019-03-02 21:07:26 +01:00
|
|
|
|
public function getMethodMutations(
|
2020-02-15 02:54:26 +01:00
|
|
|
|
\Psalm\Internal\MethodIdentifier $original_method_id,
|
2019-03-02 21:07:26 +01:00
|
|
|
|
Context $this_context,
|
|
|
|
|
string $root_file_path,
|
|
|
|
|
string $root_file_name
|
|
|
|
|
) {
|
2020-02-15 02:54:26 +01:00
|
|
|
|
$fq_class_name = $original_method_id->fq_class_name;
|
2018-01-21 19:38:51 +01:00
|
|
|
|
|
2018-02-04 00:52:35 +01:00
|
|
|
|
$appearing_method_id = $this->codebase->methods->getAppearingMethodId($original_method_id);
|
2018-01-21 19:38:51 +01:00
|
|
|
|
|
|
|
|
|
if (!$appearing_method_id) {
|
|
|
|
|
// this can happen for some abstract classes implementing (but not fully) interfaces
|
|
|
|
|
return;
|
2018-01-21 18:44:46 +01:00
|
|
|
|
}
|
|
|
|
|
|
2020-02-15 02:54:26 +01:00
|
|
|
|
$appearing_fq_class_name = $appearing_method_id->fq_class_name;
|
2018-01-21 18:44:46 +01:00
|
|
|
|
|
2018-01-21 19:38:51 +01:00
|
|
|
|
$appearing_class_storage = $this->classlike_storage_provider->get($appearing_fq_class_name);
|
2018-01-21 18:44:46 +01:00
|
|
|
|
|
2018-01-21 19:38:51 +01:00
|
|
|
|
if (!$appearing_class_storage->user_defined) {
|
|
|
|
|
return;
|
2018-01-21 18:44:46 +01:00
|
|
|
|
}
|
|
|
|
|
|
2019-04-14 18:19:07 +02:00
|
|
|
|
$file_analyzer = $this->getFileAnalyzerForClassLike($fq_class_name);
|
|
|
|
|
|
|
|
|
|
$file_analyzer->setRootFilePath($root_file_path, $root_file_name);
|
|
|
|
|
|
2020-02-15 02:54:26 +01:00
|
|
|
|
if ($appearing_fq_class_name !== $fq_class_name) {
|
2018-11-11 18:01:14 +01:00
|
|
|
|
$file_analyzer = $this->getFileAnalyzerForClassLike($appearing_fq_class_name);
|
2018-01-21 19:38:51 +01:00
|
|
|
|
}
|
|
|
|
|
|
2018-10-07 02:11:19 +02:00
|
|
|
|
$stmts = $this->codebase->getStatementsForFile(
|
2018-11-11 18:01:14 +01:00
|
|
|
|
$file_analyzer->getFilePath()
|
2018-10-07 02:11:19 +02:00
|
|
|
|
);
|
2018-01-21 19:38:51 +01:00
|
|
|
|
|
2018-11-11 18:01:14 +01:00
|
|
|
|
$file_analyzer->populateCheckers($stmts);
|
2018-01-21 19:38:51 +01:00
|
|
|
|
|
|
|
|
|
if (!$this_context->self) {
|
|
|
|
|
$this_context->self = $fq_class_name;
|
|
|
|
|
$this_context->vars_in_scope['$this'] = Type::parseString($fq_class_name);
|
|
|
|
|
}
|
|
|
|
|
|
2019-03-01 23:30:55 +01:00
|
|
|
|
$file_analyzer->getMethodMutations($appearing_method_id, $this_context, true);
|
2018-11-18 17:39:14 +01:00
|
|
|
|
|
|
|
|
|
$file_analyzer->class_analyzers_to_analyze = [];
|
|
|
|
|
$file_analyzer->interface_analyzers_to_analyze = [];
|
2019-06-30 03:06:21 +02:00
|
|
|
|
$file_analyzer->clearSourceBeforeDestruction();
|
2018-01-21 18:44:46 +01:00
|
|
|
|
}
|
2018-10-17 21:52:26 +02:00
|
|
|
|
|
2020-02-15 02:54:26 +01:00
|
|
|
|
public function getFunctionLikeAnalyzer(
|
|
|
|
|
\Psalm\Internal\MethodIdentifier $method_id,
|
|
|
|
|
string $file_path
|
|
|
|
|
) : ?FunctionLikeAnalyzer {
|
2019-05-31 07:47:35 +02:00
|
|
|
|
$file_analyzer = new FileAnalyzer(
|
|
|
|
|
$this,
|
|
|
|
|
$file_path,
|
|
|
|
|
$this->config->shortenFileName($file_path)
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
$stmts = $this->codebase->getStatementsForFile(
|
|
|
|
|
$file_analyzer->getFilePath()
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
$file_analyzer->populateCheckers($stmts);
|
|
|
|
|
|
|
|
|
|
$function_analyzer = $file_analyzer->getFunctionLikeAnalyzer($method_id);
|
|
|
|
|
|
|
|
|
|
$file_analyzer->class_analyzers_to_analyze = [];
|
|
|
|
|
$file_analyzer->interface_analyzers_to_analyze = [];
|
|
|
|
|
|
|
|
|
|
return $function_analyzer;
|
|
|
|
|
}
|
|
|
|
|
|
2018-10-17 21:52:26 +02:00
|
|
|
|
/**
|
|
|
|
|
* Adapted from https://gist.github.com/divinity76/01ef9ca99c111565a72d3a8a6e42f7fb
|
|
|
|
|
* returns number of cpu cores
|
|
|
|
|
* Copyleft 2018, license: WTFPL
|
|
|
|
|
* @throws \RuntimeException
|
|
|
|
|
* @throws \LogicException
|
|
|
|
|
* @return int
|
|
|
|
|
* @psalm-suppress ForbiddenCode
|
|
|
|
|
*/
|
2019-06-02 15:59:45 +02:00
|
|
|
|
public static function getCpuCount(): int
|
2018-10-17 21:52:26 +02:00
|
|
|
|
{
|
|
|
|
|
if (defined('PHP_WINDOWS_VERSION_MAJOR')) {
|
|
|
|
|
/*
|
|
|
|
|
$str = trim((string) shell_exec('wmic cpu get NumberOfCores 2>&1'));
|
|
|
|
|
if (!preg_match('/(\d+)/', $str, $matches)) {
|
|
|
|
|
throw new \RuntimeException('wmic failed to get number of cpu cores on windows!');
|
|
|
|
|
}
|
|
|
|
|
return ((int) $matches [1]);
|
|
|
|
|
*/
|
|
|
|
|
return 1;
|
|
|
|
|
}
|
|
|
|
|
|
2020-02-17 22:33:28 +01:00
|
|
|
|
if (\ini_get('pcre.jit') === '1'
|
|
|
|
|
&& \PHP_OS === 'Darwin'
|
|
|
|
|
&& \version_compare(\PHP_VERSION, '7.3.0') >= 0
|
|
|
|
|
&& \version_compare(\PHP_VERSION, '7.4.0') < 0
|
|
|
|
|
) {
|
|
|
|
|
return 1;
|
|
|
|
|
}
|
|
|
|
|
|
2019-01-10 06:15:59 +01:00
|
|
|
|
if (!extension_loaded('pcntl')) {
|
|
|
|
|
return 1;
|
|
|
|
|
}
|
|
|
|
|
|
2018-10-17 21:52:26 +02:00
|
|
|
|
$has_nproc = trim((string) @shell_exec('command -v nproc'));
|
|
|
|
|
if ($has_nproc) {
|
|
|
|
|
$ret = @shell_exec('nproc');
|
|
|
|
|
if (is_string($ret)) {
|
|
|
|
|
$ret = trim($ret);
|
|
|
|
|
$tmp = filter_var($ret, FILTER_VALIDATE_INT);
|
|
|
|
|
if (is_int($tmp)) {
|
|
|
|
|
return $tmp;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$ret = @shell_exec('sysctl -n hw.ncpu');
|
|
|
|
|
if (is_string($ret)) {
|
|
|
|
|
$ret = trim($ret);
|
|
|
|
|
$tmp = filter_var($ret, FILTER_VALIDATE_INT);
|
|
|
|
|
if (is_int($tmp)) {
|
|
|
|
|
return $tmp;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (is_readable('/proc/cpuinfo')) {
|
|
|
|
|
$cpuinfo = file_get_contents('/proc/cpuinfo');
|
|
|
|
|
$count = substr_count($cpuinfo, 'processor');
|
|
|
|
|
if ($count > 0) {
|
|
|
|
|
return $count;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
throw new \LogicException('failed to detect number of CPUs!');
|
|
|
|
|
}
|
2019-05-27 16:05:15 +02:00
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* @return array<string>
|
|
|
|
|
*/
|
|
|
|
|
public static function getSupportedIssuesToFix(): array
|
|
|
|
|
{
|
|
|
|
|
return array_map(
|
|
|
|
|
/** @param class-string $issue_class */
|
|
|
|
|
function (string $issue_class): string {
|
|
|
|
|
$parts = explode('\\', $issue_class);
|
|
|
|
|
return end($parts);
|
|
|
|
|
},
|
|
|
|
|
self::SUPPORTED_ISSUES_TO_FIX
|
|
|
|
|
);
|
|
|
|
|
}
|
2016-06-10 00:08:25 +02:00
|
|
|
|
}
|