1
0
mirror of https://github.com/danog/psalm.git synced 2024-12-14 18:36:58 +01:00
psalm/src/Psalm/Internal/Analyzer/ProjectAnalyzer.php
Bruce Weirdan 2562e37e60
Scan preloaded stubs earlier
This should prevent Psalm from sometimes marking user-defined classes as
built-in.

Fixes vimeo/psalm#5126
Fixes vimeo/psalm#5626
2021-08-01 01:36:37 +03:00

1501 lines
49 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\Analyzer;
use Psalm\Codebase;
use Psalm\Config;
use Psalm\Context;
use Psalm\Exception\UnsupportedIssueToFixException;
use Psalm\FileManipulation;
use Psalm\Internal\Codebase\TaintFlowGraph;
use Psalm\Internal\LanguageServer\LanguageServer;
use Psalm\Internal\LanguageServer\ProtocolStreamReader;
use Psalm\Internal\LanguageServer\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\ProjectCacheProvider;
use Psalm\Internal\Provider\Providers;
use Psalm\Issue\CodeIssue;
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\MissingPropertyType;
use Psalm\Issue\MissingReturnType;
use Psalm\Issue\ParamNameMismatch;
use Psalm\Issue\PossiblyUndefinedGlobalVariable;
use Psalm\Issue\PossiblyUndefinedVariable;
use Psalm\Issue\PossiblyUnusedMethod;
use Psalm\Issue\PossiblyUnusedProperty;
use Psalm\Issue\RedundantCast;
use Psalm\Issue\RedundantCastGivenDocblockType;
use Psalm\Issue\UnnecessaryVarAnnotation;
use Psalm\Issue\UnusedMethod;
use Psalm\Issue\UnusedProperty;
use Psalm\Issue\UnusedVariable;
use Psalm\Plugin\EventHandler\Event\AfterCodebasePopulatedEvent;
use Psalm\Progress\Progress;
use Psalm\Progress\VoidProgress;
use Psalm\Report;
use Psalm\Report\ReportOptions;
use Psalm\Type;
use function array_combine;
use function array_diff;
use function array_fill_keys;
use function array_keys;
use function array_map;
use function array_merge;
use function array_shift;
use function cli_set_process_title;
use function count;
use function defined;
use function dirname;
use function end;
use function explode;
use function extension_loaded;
use function file_exists;
use function file_get_contents;
use function filter_var;
use function fwrite;
use function implode;
use function in_array;
use function ini_get;
use function is_dir;
use function is_file;
use function is_int;
use function is_readable;
use function is_string;
use function microtime;
use function mkdir;
use function pcntl_fork;
use function preg_match;
use function rename;
use function shell_exec;
use function stream_set_blocking;
use function stream_socket_accept;
use function stream_socket_client;
use function stream_socket_server;
use function strlen;
use function strpos;
use function strtolower;
use function substr;
use function substr_count;
use function trim;
use function usort;
use const FILTER_VALIDATE_INT;
use const PHP_EOL;
use const STDERR;
use const STDIN;
use const STDOUT;
/**
* @internal
*/
class ProjectAnalyzer
{
/**
* Cached config
*
* @var Config
*/
private $config;
/**
* @var self
*/
public static $instance;
/**
* An object representing everything we know about the code
*
* @var Codebase
*/
private $codebase;
/** @var FileProvider */
private $file_provider;
/** @var ClassLikeStorageProvider */
private $classlike_storage_provider;
/** @var ?ParserCacheProvider */
private $parser_cache_provider;
/** @var ?ProjectCacheProvider */
public $project_cache_provider;
/** @var FileReferenceProvider */
private $file_reference_provider;
/**
* @var Progress
*/
public $progress;
/**
* @var bool
*/
public $debug_lines = false;
/**
* @var bool
*/
public $debug_performance = false;
/**
* @var bool
*/
public $show_issues = true;
/** @var int */
public $threads;
/**
* @var array<string, bool>
*/
private $issues_to_fix = [];
/**
* @var bool
*/
public $dry_run = false;
/**
* @var bool
*/
public $full_run = false;
/**
* @var bool
*/
public $only_replace_php_types_with_non_docblock_types = false;
/**
* @var ?int
*/
public $onchange_line_limit;
/**
* @var bool
*/
public $provide_completion = false;
/**
* @var array<string,string>
*/
private $project_files = [];
/**
* @var array<string,string>
*/
private $extra_files = [];
/**
* @var array<string, string>
*/
private $to_refactor = [];
/**
* @var ?ReportOptions
*/
public $stdout_report_options;
/**
* @var array<ReportOptions>
*/
public $generated_report_options;
/**
* @var array<int, class-string<CodeIssue>>
*/
private const SUPPORTED_ISSUES_TO_FIX = [
InvalidFalsableReturnType::class,
InvalidNullableReturnType::class,
InvalidReturnType::class,
LessSpecificReturnType::class,
MismatchingDocblockParamType::class,
MismatchingDocblockReturnType::class,
MissingClosureReturnType::class,
MissingParamType::class,
MissingPropertyType::class,
MissingReturnType::class,
ParamNameMismatch::class,
PossiblyUndefinedGlobalVariable::class,
PossiblyUndefinedVariable::class,
PossiblyUnusedMethod::class,
PossiblyUnusedProperty::class,
RedundantCast::class,
RedundantCastGivenDocblockType::class,
UnusedMethod::class,
UnusedProperty::class,
UnusedVariable::class,
UnnecessaryVarAnnotation::class,
];
/**
* When this is true, the language server will send the diagnostic code with a help link.
*
* @var bool
*/
public $language_server_use_extended_diagnostic_codes = false;
/**
* If this is true then the language server will send log messages to the client with additional information.
*
* @var bool
*/
public $language_server_verbose = false;
/**
* @param array<ReportOptions> $generated_report_options
*/
public function __construct(
Config $config,
Providers $providers,
?ReportOptions $stdout_report_options = null,
array $generated_report_options = [],
int $threads = 1,
?Progress $progress = null
) {
if ($progress === null) {
$progress = new VoidProgress();
}
$this->parser_cache_provider = $providers->parser_cache_provider;
$this->project_cache_provider = $providers->project_cache_provider;
$this->file_provider = $providers->file_provider;
$this->classlike_storage_provider = $providers->classlike_storage_provider;
$this->file_reference_provider = $providers->file_reference_provider;
$this->progress = $progress;
$this->threads = $threads;
$this->config = $config;
$this->clearCacheDirectoryIfConfigOrComposerLockfileChanged();
$this->codebase = new Codebase(
$config,
$providers,
$progress
);
$this->stdout_report_options = $stdout_report_options;
$this->generated_report_options = $generated_report_options;
$file_extensions = $this->config->getFileExtensions();
foreach ($this->config->getProjectDirectories() as $dir_name) {
$file_paths = $this->file_provider->getFilesInDir($dir_name, $file_extensions);
foreach ($file_paths as $file_path) {
if ($this->config->isInProjectDirs($file_path)) {
$this->addProjectFile($file_path);
}
}
}
foreach ($this->config->getExtraDirectories() as $dir_name) {
$file_paths = $this->file_provider->getFilesInDir($dir_name, $file_extensions);
foreach ($file_paths as $file_path) {
if ($this->config->isInExtraDirs($file_path)) {
$this->addExtraFile($file_path);
}
}
}
foreach ($this->config->getProjectFiles() as $file_path) {
$this->addProjectFile($file_path);
}
self::$instance = $this;
}
private function clearCacheDirectoryIfConfigOrComposerLockfileChanged() : void
{
if ($this->project_cache_provider
&& $this->project_cache_provider->hasLockfileChanged()
) {
$this->progress->debug(
'Composer lockfile change detected, clearing cache' . "\n"
);
$cache_directory = $this->config->getCacheDirectory();
if ($cache_directory !== null) {
Config::removeCacheDirectory($cache_directory);
}
if ($this->file_reference_provider->cache) {
$this->file_reference_provider->cache->hasConfigChanged();
}
$this->project_cache_provider->updateComposerLockHash();
} elseif ($this->file_reference_provider->cache
&& $this->file_reference_provider->cache->hasConfigChanged()
) {
$this->progress->debug(
'Config change detected, clearing cache' . "\n"
);
$cache_directory = $this->config->getCacheDirectory();
if ($cache_directory !== null) {
Config::removeCacheDirectory($cache_directory);
}
if ($this->project_cache_provider) {
$this->project_cache_provider->hasLockfileChanged();
}
}
}
/**
* @param array<string> $report_file_paths
* @return list<ReportOptions>
*/
public static function getFileReportOptions(array $report_file_paths, bool $show_info = true): array
{
$report_options = [];
$mapping = [
'checkstyle.xml' => Report::TYPE_CHECKSTYLE,
'sonarqube.json' => Report::TYPE_SONARQUBE,
'codeclimate.json' => Report::TYPE_CODECLIMATE,
'summary.json' => Report::TYPE_JSON_SUMMARY,
'junit.xml' => Report::TYPE_JUNIT,
'.xml' => Report::TYPE_XML,
'.json' => Report::TYPE_JSON,
'.txt' => Report::TYPE_TEXT,
'.emacs' => Report::TYPE_EMACS,
'.pylint' => Report::TYPE_PYLINT,
'.console' => Report::TYPE_CONSOLE,
'.sarif' => Report::TYPE_SARIF,
];
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;
$o->use_color = false;
$report_options[] = $o;
continue 2;
}
}
throw new \UnexpectedValueException('Unknown report format ' . $report_file_path);
}
return $report_options;
}
private function visitAutoloadFiles() : void
{
$start_time = microtime(true);
$this->config->visitComposerAutoloadFiles($this, $this->progress);
$now_time = microtime(true);
$this->progress->debug(
'Visiting autoload files took ' . \number_format($now_time - $start_time, 3) . 's' . "\n"
);
}
public function server(?string $address = '127.0.0.1:12345', bool $socket_server_mode = false): void
{
$this->visitAutoloadFiles();
$this->codebase->diff_methods = true;
$this->file_reference_provider->loadReferenceCache();
$this->codebase->enterServerMode();
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
) {
// 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;
}
}
$this->config->initializePlugins($this);
foreach ($this->config->getProjectDirectories() as $dir_name) {
$this->checkDirWithConfig($dir_name, $this->config);
}
@cli_set_process_title('Psalm ' . PSALM_VERSION . ' - PHP Language Server');
if (!$socket_server_mode && $address) {
// 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
);
\Amp\Loop::run();
} elseif ($socket_server_mode && $address) {
// 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");
$fork_available = true;
if (!extension_loaded('pcntl')) {
fwrite(STDERR, "PCNTL is not available. Only a single connection will be accepted\n");
$fork_available = false;
}
$disabled_functions = array_map('trim', explode(',', ini_get('disable_functions')));
if (in_array('pcntl_fork', $disabled_functions)) {
fwrite(
STDERR,
"pcntl_fork() is disabled by php configuration (disable_functions directive)."
. " Only a single connection will be accepted\n"
);
$fork_available = false;
}
while ($socket = stream_socket_accept($tcpServer, -1)) {
fwrite(STDOUT, "Connection accepted\n");
stream_set_blocking($socket, false);
if ($fork_available) {
// 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',
function (): void {
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
);
\Amp\Loop::run();
}
}
} else {
// Use STDIO
stream_set_blocking(STDIN, false);
new LanguageServer(
new ProtocolStreamReader(STDIN),
new ProtocolStreamWriter(STDOUT),
$this
);
\Amp\Loop::run();
}
}
public static function getInstance(): ProjectAnalyzer
{
return self::$instance;
}
public function canReportIssues(string $file_path): bool
{
return isset($this->project_files[$file_path]);
}
public function check(string $base_dir, bool $is_diff = false): void
{
$start_checks = (int)microtime(true);
if (!$base_dir) {
throw new \InvalidArgumentException('Cannot work with empty base_dir');
}
$diff_files = null;
$deleted_files = null;
$this->full_run = true;
$reference_cache = $this->file_reference_provider->loadReferenceCache(true);
$this->codebase->diff_methods = $is_diff;
if ($is_diff
&& $reference_cache
&& $this->project_cache_provider
&& $this->project_cache_provider->canDiffFiles()
) {
$deleted_files = $this->file_reference_provider->getDeletedReferencedFiles();
$diff_files = $deleted_files;
foreach ($this->config->getProjectDirectories() as $dir_name) {
$diff_files = array_merge($diff_files, $this->getDiffFilesInDir($dir_name, $this->config));
}
}
$this->progress->startScanningFiles();
$diff_no_files = false;
if ($diff_files === null
|| $deleted_files === null
|| count($diff_files) > 200
) {
$this->config->visitPreloadedStubFiles($this->codebase, $this->progress);
$this->visitAutoloadFiles();
$this->codebase->scanner->addFilesToShallowScan($this->extra_files);
$this->codebase->scanner->addFilesToDeepScan($this->project_files);
$this->codebase->analyzer->addFilesToAnalyze($this->project_files);
$this->config->initializePlugins($this);
$this->codebase->scanFiles($this->threads);
$this->codebase->infer_types_from_usage = true;
} else {
$this->progress->debug(count($diff_files) . ' changed files: ' . "\n");
$this->progress->debug(' ' . implode("\n ", $diff_files) . "\n");
$this->codebase->analyzer->addFilesToShowResults($this->project_files);
if ($diff_files) {
$file_list = $this->getReferencedFilesFromDiff($diff_files);
// strip out deleted files
$file_list = array_diff($file_list, $deleted_files);
if ($file_list) {
$this->config->visitPreloadedStubFiles($this->codebase, $this->progress);
$this->visitAutoloadFiles();
$this->checkDiffFilesWithConfig($this->config, $file_list);
$this->config->initializePlugins($this);
$this->codebase->scanFiles($this->threads);
} else {
$diff_no_files = true;
}
} else {
$diff_no_files = true;
}
}
if (!$diff_no_files) {
$this->config->visitStubFiles($this->codebase, $this->progress);
$event = new AfterCodebasePopulatedEvent($this->codebase);
$this->config->eventDispatcher->dispatchAfterCodebasePopulated($event);
}
$this->progress->startAnalyzingFiles();
$this->codebase->analyzer->analyzeFiles(
$this,
$this->threads,
$this->codebase->alter_code,
true
);
if ($this->project_cache_provider && $this->parser_cache_provider) {
$removed_parser_files = $this->parser_cache_provider->deleteOldParserCaches(
$is_diff ? $this->project_cache_provider->getLastRun(\PSALM_VERSION) : $start_checks
);
if ($removed_parser_files) {
$this->progress->debug('Removed ' . $removed_parser_files . ' old parser caches' . "\n");
}
if ($is_diff) {
$this->parser_cache_provider->touchParserCaches($this->getAllFiles($this->config), $start_checks);
}
}
}
public function consolidateAnalyzedData(): void
{
$this->codebase->classlikes->consolidateAnalyzedData(
$this->codebase->methods,
$this->progress,
!!$this->codebase->find_unused_code
);
}
public function trackTaintedInputs(): void
{
$this->codebase->taint_flow_graph = new TaintFlowGraph();
}
public function trackUnusedSuppressions(): void
{
$this->codebase->track_unused_suppressions = true;
}
public function interpretRefactors() : void
{
if (!$this->codebase->alter_code) {
throw new \UnexpectedValueException('Should not be checking references');
}
// 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]);
}
}
foreach ($this->to_refactor as $source => $destination) {
$source_parts = explode('::', $source);
$destination_parts = explode('::', $destination);
if (!$this->codebase->classlikes->hasFullyQualifiedClassName($source_parts[0])) {
throw new \Psalm\Exception\RefactorException(
'Source class ' . $source_parts[0] . ' doesnt exist'
);
}
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, -1);
$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;
}
$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])
)
)) {
throw new \Psalm\Exception\RefactorException(
'Destination method ' . $destination . ' already exists'
);
}
if (!$this->codebase->classlikes->classExists($destination_parts[0])) {
throw new \Psalm\Exception\RefactorException(
'Destination class ' . $destination_parts[0] . ' doesnt exist'
);
}
if (strtolower($source_parts[0]) !== strtolower($destination_parts[0])) {
$source_method_storage = $this->codebase->methods->getStorage($source_method_id);
$destination_class_storage
= $this->codebase->classlike_storage_provider->get($destination_parts[0]);
if (!$source_method_storage->is_static
&& !isset(
$destination_class_storage->parent_classes[strtolower($source_method_id->fq_class_name)]
)
) {
throw new \Psalm\Exception\RefactorException(
'Cannot move non-static method ' . $source
. ' into unrelated class ' . $destination_parts[0]
);
}
$this->codebase->methods_to_move[strtolower($source)]= $destination;
} else {
$this->codebase->methods_to_rename[strtolower($source)] = $destination_parts[1];
}
$this->codebase->call_transforms[strtolower($source) . '\((.*\))'] = $destination . '($1)';
continue;
}
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] . ' doesnt 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]])) {
if (!$this->codebase->classlikes->hasFullyQualifiedClassName($destination_parts[0])) {
throw new \Psalm\Exception\RefactorException(
'Destination class ' . $destination_parts[0] . ' doesnt 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;
continue;
}
throw new \Psalm\Exception\RefactorException(
'Psalm cannot locate ' . $source
);
}
}
public function prepareMigration() : void
{
if (!$this->codebase->alter_code) {
throw new \UnexpectedValueException('Should not be checking references');
}
$this->codebase->classlikes->moveMethods(
$this->codebase->methods,
$this->progress
);
$this->codebase->classlikes->moveProperties(
$this->codebase->properties,
$this->progress
);
$this->codebase->classlikes->moveClassConstants(
$this->progress
);
}
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
);
if ($migration_manipulations) {
foreach ($migration_manipulations as $file_path => $file_manipulations) {
usort(
$file_manipulations,
function (FileManipulation $a, FileManipulation $b): int {
if ($a->start === $b->start) {
if ($b->end === $a->end) {
return $b->insertion_text > $a->insertion_text ? 1 : -1;
}
return $b->end > $a->end ? 1 : -1;
}
return $b->start > $a->start ? 1 : -1;
}
);
$existing_contents = $this->codebase->file_provider->getContents($file_path);
foreach ($file_manipulations as $manipulation) {
$existing_contents = $manipulation->transform($existing_contents);
}
$this->codebase->file_provider->setContents($file_path, $existing_contents);
}
}
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;
}
$potential_file_path = $this->config->getPotentialComposerFilePathForClassLike($destination);
if ($potential_file_path && !file_exists($potential_file_path)) {
$containing_dir = dirname($potential_file_path);
if (!file_exists($containing_dir)) {
mkdir($containing_dir, 0777, true);
}
rename($source_class_storage->location->file_path, $potential_file_path);
}
}
}
}
public function findReferencesTo(string $symbol): void
{
if (!$this->stdout_report_options) {
throw new \UnexpectedValueException('Not expecting to emit output');
}
$locations = $this->codebase->findReferencesToSymbol($symbol);
foreach ($locations as $location) {
$snippet = $location->getSnippet();
$snippet_bounds = $location->getSnippetBounds();
$selection_bounds = $location->getSelectionBounds();
$selection_start = $selection_bounds[0] - $snippet_bounds[0];
$selection_length = $selection_bounds[1] - $selection_bounds[0];
echo $location->file_name . ':' . $location->getLineNumber() . "\n" .
(
$this->stdout_report_options->use_color
? 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";
}
}
public function checkDir(string $dir_name): void
{
$this->file_reference_provider->loadReferenceCache();
$this->config->visitPreloadedStubFiles($this->codebase, $this->progress);
$this->checkDirWithConfig($dir_name, $this->config, true);
$this->progress->startScanningFiles();
$this->config->initializePlugins($this);
$this->codebase->scanFiles($this->threads);
$this->config->visitStubFiles($this->codebase, $this->progress);
$this->progress->startAnalyzingFiles();
$this->codebase->analyzer->analyzeFiles(
$this,
$this->threads,
$this->codebase->alter_code,
$this->codebase->find_unused_code === 'always'
);
}
private function checkDirWithConfig(string $dir_name, Config $config, bool $allow_non_project_files = false): void
{
$file_extensions = $config->getFileExtensions();
$file_paths = $this->file_provider->getFilesInDir($dir_name, $file_extensions);
$files_to_scan = [];
foreach ($file_paths as $file_path) {
if ($allow_non_project_files || $config->isInProjectDirs($file_path)) {
$files_to_scan[$file_path] = $file_path;
}
}
$this->codebase->addFilesToAnalyze($files_to_scan);
}
/**
* @return list<string>
*/
private function getAllFiles(Config $config): array
{
$file_extensions = $config->getFileExtensions();
$file_paths = [];
foreach ($config->getProjectDirectories() as $dir_name) {
$file_paths = array_merge(
$file_paths,
$this->file_provider->getFilesInDir($dir_name, $file_extensions)
);
}
return $file_paths;
}
public function addProjectFile(string $file_path): void
{
$this->project_files[$file_path] = $file_path;
}
public function addExtraFile(string $file_path) : void
{
$this->extra_files[$file_path] = $file_path;
}
/**
* @return list<string>
*/
protected function getDiffFilesInDir(string $dir_name, Config $config): array
{
$file_extensions = $config->getFileExtensions();
if (!$this->parser_cache_provider || !$this->project_cache_provider) {
throw new \UnexpectedValueException('Parser cache provider cannot be null here');
}
$diff_files = [];
$last_run = $this->project_cache_provider->getLastRun(\PSALM_VERSION);
$file_paths = $this->file_provider->getFilesInDir($dir_name, $file_extensions);
foreach ($file_paths as $file_path) {
if ($config->isInProjectDirs($file_path)) {
if ($this->file_provider->getModifiedTime($file_path) >= $last_run
&& $this->parser_cache_provider->loadExistingFileContentsFromCache($file_path)
!== $this->file_provider->getContents($file_path)
) {
$diff_files[] = $file_path;
}
}
}
return $diff_files;
}
/**
* @param array<string> $file_list
*
*/
private function checkDiffFilesWithConfig(Config $config, array $file_list = []): void
{
$files_to_scan = [];
foreach ($file_list as $file_path) {
if (!$this->file_provider->fileExists($file_path)) {
continue;
}
if (!$config->isInProjectDirs($file_path)) {
$this->progress->debug('skipping ' . $file_path . "\n");
continue;
}
$files_to_scan[$file_path] = $file_path;
}
$this->codebase->addFilesToAnalyze($files_to_scan);
}
public function checkFile(string $file_path): void
{
$this->progress->debug('Checking ' . $file_path . "\n");
$this->config->visitPreloadedStubFiles($this->codebase, $this->progress);
$this->config->hide_external_errors = $this->config->isInProjectDirs($file_path);
$this->codebase->addFilesToAnalyze([$file_path => $file_path]);
$this->file_reference_provider->loadReferenceCache();
$this->progress->startScanningFiles();
$this->config->initializePlugins($this);
$this->codebase->scanFiles($this->threads);
$this->config->visitStubFiles($this->codebase, $this->progress);
$this->progress->startAnalyzingFiles();
$this->codebase->analyzer->analyzeFiles(
$this,
$this->threads,
$this->codebase->alter_code,
$this->codebase->find_unused_code === 'always'
);
}
/**
* @param string[] $paths_to_check
*/
public function checkPaths(array $paths_to_check): void
{
$this->config->visitPreloadedStubFiles($this->codebase, $this->progress);
$this->visitAutoloadFiles();
$this->codebase->scanner->addFilesToShallowScan($this->extra_files);
foreach ($paths_to_check as $path) {
$this->progress->debug('Checking ' . $path . "\n");
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);
}
}
$this->file_reference_provider->loadReferenceCache();
$this->progress->startScanningFiles();
$this->config->initializePlugins($this);
$this->codebase->scanFiles($this->threads);
$this->config->visitStubFiles($this->codebase, $this->progress);
$this->progress->startAnalyzingFiles();
$this->codebase->analyzer->analyzeFiles(
$this,
$this->threads,
$this->codebase->alter_code,
$this->codebase->find_unused_code === 'always'
);
if ($this->stdout_report_options
&& in_array(
$this->stdout_report_options->format,
[\Psalm\Report::TYPE_CONSOLE, \Psalm\Report::TYPE_PHP_STORM]
)
&& $this->codebase->collect_references
) {
fwrite(
STDERR,
PHP_EOL . 'To whom it may concern: Psalm cannot detect unused classes, methods and properties'
. PHP_EOL . 'when analyzing individual files and folders. Run on the full project to enable'
. PHP_EOL . 'complete unused code detection.' . PHP_EOL
);
}
}
public function getConfig(): Config
{
return $this->config;
}
/**
* @param array<string> $diff_files
*
* @return array<string, string>
*/
public function getReferencedFilesFromDiff(array $diff_files, bool $include_referencing_files = true): array
{
$all_inherited_files_to_check = $diff_files;
while ($diff_files) {
$diff_file = array_shift($diff_files);
$dependent_files = $this->file_reference_provider->getFilesInheritingFromFile($diff_file);
$new_dependent_files = array_diff($dependent_files, $all_inherited_files_to_check);
$all_inherited_files_to_check = array_merge($all_inherited_files_to_check, $new_dependent_files);
$diff_files = array_merge($diff_files, $new_dependent_files);
}
$all_files_to_check = $all_inherited_files_to_check;
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);
}
}
return array_combine($all_files_to_check, $all_files_to_check);
}
public function fileExists(string $file_path): bool
{
return $this->file_provider->fileExists($file_path);
}
public function alterCodeAfterCompletion(
bool $dry_run = false,
bool $safe_types = false
): void {
$this->codebase->alter_code = true;
$this->codebase->infer_types_from_usage = true;
$this->show_issues = false;
$this->dry_run = $dry_run;
$this->only_replace_php_types_with_non_docblock_types = $safe_types;
}
/**
* @param array<string, string> $to_refactor
*
*/
public function refactorCodeAfterCompletion(array $to_refactor): void
{
$this->to_refactor = $to_refactor;
$this->codebase->alter_code = true;
$this->show_issues = false;
}
public function setPhpVersion(string $version): void
{
if (!preg_match('/^(5\.[456]|7\.[01234]|8\.[01])(\..*)?$/', $version)) {
throw new \UnexpectedValueException('Expecting a version number in the format x.y');
}
[$php_major_version, $php_minor_version] = explode('.', $version);
$php_major_version = (int) $php_major_version;
$php_minor_version = (int) $php_minor_version;
if ($this->codebase->php_major_version !== $php_major_version
|| $this->codebase->php_minor_version !== $php_minor_version
) {
// reset lexer and parser when php version changes
\Psalm\Internal\Provider\StatementsProvider::clearLexer();
\Psalm\Internal\Provider\StatementsProvider::clearParser();
}
$this->codebase->php_major_version = $php_major_version;
$this->codebase->php_minor_version = $php_minor_version;
}
/**
* @param array<string, bool> $issues
* @throws UnsupportedIssueToFixException
*
*/
public function setIssuesToFix(array $issues): void
{
$supported_issues_to_fix = static::getSupportedIssuesToFix();
$supported_issues_to_fix[] = 'MissingImmutableAnnotation';
$supported_issues_to_fix[] = 'MissingPureAnnotation';
$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)
);
}
$this->issues_to_fix = $issues;
}
public function setAllIssuesToFix(): void
{
$keyed_issues = array_fill_keys(static::getSupportedIssuesToFix(), true);
$this->setIssuesToFix($keyed_issues);
}
/**
* @return array<string, bool>
*/
public function getIssuesToFix(): array
{
return $this->issues_to_fix;
}
public function getCodebase(): Codebase
{
return $this->codebase;
}
public function getFileAnalyzerForClassLike(string $fq_class_name): FileAnalyzer
{
$fq_class_name_lc = strtolower($fq_class_name);
$file_path = $this->codebase->scanner->getClassLikeFilePath($fq_class_name_lc);
$file_analyzer = new FileAnalyzer(
$this,
$file_path,
$this->config->shortenFileName($file_path)
);
return $file_analyzer;
}
public function getMethodMutations(
\Psalm\Internal\MethodIdentifier $original_method_id,
Context $this_context,
string $root_file_path,
string $root_file_name
): void {
$fq_class_name = $original_method_id->fq_class_name;
$appearing_method_id = $this->codebase->methods->getAppearingMethodId($original_method_id);
if (!$appearing_method_id) {
// this can happen for some abstract classes implementing (but not fully) interfaces
return;
}
$appearing_fq_class_name = $appearing_method_id->fq_class_name;
$appearing_class_storage = $this->classlike_storage_provider->get($appearing_fq_class_name);
if (!$appearing_class_storage->user_defined) {
return;
}
$file_analyzer = $this->getFileAnalyzerForClassLike($fq_class_name);
$file_analyzer->setRootFilePath($root_file_path, $root_file_name);
if ($appearing_fq_class_name !== $fq_class_name) {
$file_analyzer = $this->getFileAnalyzerForClassLike($appearing_fq_class_name);
}
$stmts = $this->codebase->getStatementsForFile(
$file_analyzer->getFilePath()
);
$file_analyzer->populateCheckers($stmts);
if (!$this_context->self) {
$this_context->self = $fq_class_name;
$this_context->vars_in_scope['$this'] = Type::parseString($fq_class_name);
}
$file_analyzer->getMethodMutations($appearing_method_id, $this_context, true);
$file_analyzer->class_analyzers_to_analyze = [];
$file_analyzer->interface_analyzers_to_analyze = [];
$file_analyzer->clearSourceBeforeDestruction();
}
public function getFunctionLikeAnalyzer(
\Psalm\Internal\MethodIdentifier $method_id,
string $file_path
) : ?FunctionLikeAnalyzer {
$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;
}
/**
* Adapted from https://gist.github.com/divinity76/01ef9ca99c111565a72d3a8a6e42f7fb
* returns number of cpu cores
* Copyleft 2018, license: WTFPL
* @throws \RuntimeException
* @throws \LogicException
* @psalm-suppress ForbiddenCode
*/
public static function getCpuCount(): int
{
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;
}
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;
}
if (!extension_loaded('pcntl')) {
return 1;
}
$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!');
}
/**
* @return array<int, string>
*
* @psalm-pure
*/
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
);
}
}