1
0
mirror of https://github.com/danog/psalm.git synced 2024-12-14 02:07:37 +01:00
psalm/src/Psalm/Checker/ProjectChecker.php

665 lines
19 KiB
PHP
Raw Normal View History

2016-06-10 00:08:25 +02:00
<?php
namespace Psalm\Checker;
2016-06-10 00:08:25 +02:00
use Psalm\Config;
use Psalm\Exception;
2016-11-02 07:29:00 +01:00
use Psalm\IssueBuffer;
use Psalm\Storage\PropertyStorage;
use Psalm\Type;
2016-11-02 07:29:00 +01:00
use RecursiveDirectoryIterator;
use RecursiveIteratorIterator;
2016-06-10 00:08:25 +02:00
class ProjectChecker
{
2016-06-26 19:45:20 +02:00
/**
* Cached config
2016-11-02 07:29:00 +01:00
*
2016-06-26 19:45:20 +02:00
* @var Config|null
*/
2016-12-08 04:38:57 +01:00
protected $config;
/**
* @var self
*/
public static $instance;
2016-06-26 19:45:20 +02:00
2016-07-25 21:05:58 +02:00
/**
* Whether or not to use colors in error output
2016-11-02 07:29:00 +01:00
*
2016-07-25 21:05:58 +02:00
* @var boolean
*/
2016-12-08 04:38:57 +01:00
public $use_color;
2016-07-25 21:05:58 +02:00
/**
* Whether or not to show informational messages
2016-11-02 07:29:00 +01:00
*
* @var boolean
*/
2016-12-08 04:38:57 +01:00
public $show_info;
/**
* @var string
*/
public $output_format;
/**
* @var bool
*/
public $debug_output = false;
/**
* @var array<string, string>
*/
protected $files_to_visit = [];
/**
* @var array<string, bool>
*/
protected $visited_files = [];
/**
* @var array<string, bool>
*/
protected $visited_classes = [];
/**
* @var array<string, FileChecker>
*/
protected $file_checkers = [];
2016-12-08 04:38:57 +01:00
/**
* @var array<string, string>
*/
public $fake_files = [];
const TYPE_CONSOLE = 'console';
const TYPE_JSON = 'json';
2016-12-17 06:48:31 +01:00
/**
* @param boolean $use_color
* @param boolean $show_info
* @param boolean $debug_output
2016-12-17 06:48:31 +01:00
* @param string $output_format
*/
public function __construct(
$use_color = true,
$show_info = true,
$output_format = self::TYPE_CONSOLE,
$debug_output = false
) {
2016-12-08 04:38:57 +01:00
$this->use_color = $use_color;
$this->show_info = $show_info;
$this->debug_output = $debug_output;
2016-12-08 04:38:57 +01:00
if (!in_array($output_format, [self::TYPE_CONSOLE, self::TYPE_JSON])) {
throw new \UnexpectedValueException('Unrecognised output format ' . $output_format);
}
$this->output_format = $output_format;
self::$instance = $this;
}
/**
* @return self
*/
public static function getInstance()
{
return self::$instance;
}
2016-10-15 06:12:57 +02:00
/**
* @param boolean $is_diff
2016-11-13 17:24:46 +01:00
* @param boolean $update_docblocks
2016-10-15 06:12:57 +02:00
* @return void
*/
public function check($is_diff = false, $update_docblocks = false)
2016-06-13 21:33:18 +02:00
{
2016-10-15 06:12:57 +02:00
$cwd = getcwd();
$start_checks = (int)microtime(true);
2016-10-15 06:12:57 +02:00
if (!$cwd) {
throw new \InvalidArgumentException('Cannot work with empty cwd');
}
2016-12-08 04:38:57 +01:00
if (!$this->config) {
$this->config = $this->getConfigForPath($cwd);
2016-06-26 21:33:51 +02:00
}
2016-06-26 19:45:20 +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-07 06:58:08 +02:00
if ($is_diff && FileChecker::loadReferenceCache() && FileChecker::canDiffFiles()) {
2016-10-07 19:26:29 +02:00
$deleted_files = FileChecker::getDeletedReferencedFiles();
$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) {
2016-12-08 04:38:57 +01:00
$diff_files = array_merge($diff_files, self::getDiffFilesInDir($dir_name, $this->config));
2016-10-07 06:58:08 +02:00
}
}
2016-10-07 06:58:08 +02:00
$files_checked = [];
2016-10-07 19:26:29 +02:00
if ($diff_files === null || $deleted_files === null || count($diff_files) > 200) {
2016-12-29 16:24:10 +01:00
foreach ($this->config->getProjectDirectories() as $dir_name) {
$this->checkDirWithConfig($dir_name, $this->config, $update_docblocks);
}
$this->visitFiles();
$this->analyzeFiles();
2016-11-02 07:29:00 +01:00
} else {
if ($this->debug_output) {
2016-10-07 06:58:08 +02:00
echo count($diff_files) . ' changed files' . PHP_EOL;
}
2016-10-07 06:58:08 +02:00
$file_list = self::getReferencedFilesFromDiff($diff_files);
2016-11-02 07:29:00 +01:00
2016-10-07 19:26:29 +02:00
// strip out deleted files
$file_list = array_diff($file_list, $deleted_files);
$this->checkDiffFilesWithConfig($this->config, $file_list);
2016-10-07 06:58:08 +02:00
}
$removed_parser_files = FileChecker::deleteOldParserCaches(
$is_diff ? FileChecker::getLastGoodRun() : $start_checks
);
if ($this->debug_output && $removed_parser_files) {
echo 'Removed ' . $removed_parser_files . ' old parser caches' . PHP_EOL;
}
if ($is_diff) {
2016-12-08 04:38:57 +01:00
FileChecker::touchParserCaches($this->getAllFiles($this->config), $start_checks);
}
IssueBuffer::finish(true, (int)$start_checks, $this->debug_output);
}
/**
* @return void
*/
protected function visitFiles()
{
if (!$this->config) {
throw new \UnexpectedValueException('$this->config cannot be null');
}
$filetype_handlers = $this->config->getFiletypeHandlers();
while (count($this->files_to_visit)) {
$file_path = array_shift($this->files_to_visit);
$this->visitFile($file_path, $filetype_handlers);
}
}
/**
* @return void
*/
protected function analyzeFiles()
{
while (count($this->file_checkers)) {
$file_checker = array_shift($this->file_checkers);
if ($this->debug_output) {
echo 'Analyzing ' . $file_checker->getFilePath() . PHP_EOL;
}
$file_checker->checkMethods();
}
2016-06-13 21:33:18 +02:00
}
2016-10-15 06:12:57 +02:00
/**
* @param string $dir_name
2016-11-13 17:24:46 +01:00
* @param boolean $update_docblocks
2016-10-15 06:12:57 +02:00
* @return void
*/
public function checkDir($dir_name, $update_docblocks = false)
2016-06-10 00:08:25 +02:00
{
2016-12-08 04:38:57 +01:00
if (!$this->config) {
$this->config = $this->getConfigForPath($dir_name);
2016-12-08 04:38:57 +01:00
$this->config->hide_external_errors = $this->config->isInProjectDirs(
$this->config->shortenFileName($dir_name . '/')
2016-11-02 07:29:00 +01:00
);
2016-06-26 19:45:20 +02:00
}
2016-06-10 00:08:25 +02:00
FileChecker::loadReferenceCache();
$start_checks = (int)microtime(true);
$this->checkDirWithConfig($dir_name, $this->config, $update_docblocks);
$this->visitFiles();
$this->analyzeFiles();
IssueBuffer::finish(false, $start_checks, $this->debug_output);
}
2016-10-15 06:12:57 +02:00
/**
* @param string $dir_name
* @param Config $config
2016-11-13 17:24:46 +01:00
* @param bool $update_docblocks
2016-10-15 06:12:57 +02:00
* @return void
*/
protected function checkDirWithConfig($dir_name, Config $config, $update_docblocks)
{
$file_extensions = $config->getFileExtensions();
2016-06-13 21:33:18 +02:00
/** @var RecursiveDirectoryIterator */
2016-06-28 20:28:45 +02:00
$iterator = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($dir_name));
2016-06-13 21:33:18 +02:00
$iterator->rewind();
while ($iterator->valid()) {
if (!$iterator->isDot()) {
$extension = $iterator->getExtension();
if (in_array($extension, $file_extensions)) {
$file_path = (string)$iterator->getRealPath();
if ($config->isInProjectDirs($config->shortenFileName($file_path))) {
$this->files_to_visit[$file_path] = $file_path;
}
2016-06-13 21:33:18 +02:00
}
}
$iterator->next();
}
}
2016-06-10 00:08:25 +02:00
2016-11-07 23:07:59 +01:00
/**
* @param Config $config
* @return array<int, string>
*/
2016-12-08 04:38:57 +01:00
protected function getAllFiles(Config $config)
{
$file_extensions = $config->getFileExtensions();
$file_names = [];
2016-12-29 16:24:10 +01:00
foreach ($config->getProjectDirectories() as $dir_name) {
/** @var RecursiveDirectoryIterator */
$iterator = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($dir_name));
$iterator->rewind();
while ($iterator->valid()) {
if (!$iterator->isDot()) {
$extension = $iterator->getExtension();
if (in_array($extension, $file_extensions)) {
$file_names[] = (string)$iterator->getRealPath();
}
}
$iterator->next();
}
}
return $file_names;
}
2016-10-15 06:12:57 +02:00
/**
* @param string $dir_name
* @param Config $config
* @return array<string>
*/
2016-10-07 06:58:08 +02:00
protected static function getDiffFilesInDir($dir_name, Config $config)
{
$file_extensions = $config->getFileExtensions();
$filetype_handlers = $config->getFiletypeHandlers();
/** @var RecursiveDirectoryIterator */
$iterator = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($dir_name));
$iterator->rewind();
$diff_files = [];
while ($iterator->valid()) {
if (!$iterator->isDot()) {
$extension = $iterator->getExtension();
if (in_array($extension, $file_extensions)) {
2016-10-15 06:12:57 +02:00
$file_name = (string)$iterator->getRealPath();
2016-10-07 06:58:08 +02:00
if ($config->isInProjectDirs($config->shortenFileName($file_name))) {
if (FileChecker::hasFileChanged($file_name)) {
$diff_files[] = $file_name;
}
2016-10-07 06:58:08 +02:00
}
}
}
$iterator->next();
}
return $diff_files;
}
2016-10-15 06:12:57 +02:00
/**
* @param Config $config
2016-11-04 01:51:56 +01:00
* @param array<string> $file_list
2016-10-15 06:12:57 +02:00
* @return void
*/
protected function checkDiffFilesWithConfig(Config $config, array $file_list = [])
{
$file_extensions = $config->getFileExtensions();
$filetype_handlers = $config->getFiletypeHandlers();
foreach ($file_list as $file_name) {
2016-10-09 02:49:14 +02:00
if (!file_exists($file_name)) {
continue;
}
2016-11-02 07:29:00 +01:00
if (!$config->isInProjectDirs(
preg_replace('/^' . preg_quote($config->getBaseDir(), '/') . '/', '', $file_name)
)) {
if ($this->debug_output) {
echo('skipping ' . $file_name . PHP_EOL);
}
continue;
}
2016-10-09 02:49:14 +02:00
$extension = pathinfo($file_name, PATHINFO_EXTENSION);
if ($this->debug_output) {
echo 'Checking affected file ' . $file_name . PHP_EOL;
}
if (isset($filetype_handlers[$extension])) {
/** @var FileChecker */
$file_checker = new $filetype_handlers[$extension]($file_name);
2016-11-02 07:29:00 +01:00
} else {
$file_checker = new FileChecker($file_name, $this);
}
$file_checker->visit();
$file_checker->checkMethods();
}
}
2016-10-15 06:12:57 +02:00
/**
* @param string $file_name
2016-11-13 17:24:46 +01:00
* @param bool $update_docblocks
2016-10-15 06:12:57 +02:00
* @return void
*/
public function checkFile($file_name, $update_docblocks = false)
{
if ($this->debug_output) {
echo 'Checking ' . $file_name . PHP_EOL;
}
2016-06-10 00:08:25 +02:00
2016-12-08 04:38:57 +01:00
if (!$this->config) {
$this->config = $this->getConfigForPath($file_name);
2016-06-26 19:45:20 +02:00
}
$start_checks = (int)microtime(true);
2016-12-08 04:38:57 +01:00
$this->config->hide_external_errors = $this->config->isInProjectDirs(
$this->config->shortenFileName($file_name)
2016-11-02 07:29:00 +01:00
);
$file_name_parts = explode('.', $file_name);
$extension = array_pop($file_name_parts);
2016-12-08 04:38:57 +01:00
$filetype_handlers = $this->config->getFiletypeHandlers();
FileChecker::loadReferenceCache();
if (isset($filetype_handlers[$extension])) {
/** @var FileChecker */
2016-06-28 20:28:45 +02:00
$file_checker = new $filetype_handlers[$extension]($file_name);
2016-11-02 07:29:00 +01:00
} else {
$file_checker = new FileChecker($file_name, $this);
}
$file_checker->visit(null);
if ($this->debug_output) {
echo 'Analyzing ' . $file_checker->getFilePath() . PHP_EOL;
2016-06-10 00:08:25 +02:00
}
$file_checker->checkMethods();
IssueBuffer::finish(false, $start_checks, $this->debug_output);
}
/**
* @param string $file_path
* @param array $filetype_handlers
* @return void
*/
public function visitFile($file_path, array $filetype_handlers)
{
if (isset($this->visited_files[$file_path])) {
return;
}
$this->visited_files[$file_path] = true;
$extension = (string)pathinfo($file_path)['extension'];
if (isset($filetype_handlers[$extension])) {
/** @var FileChecker */
$file_checker = new $filetype_handlers[$extension]($file_path);
} else {
$file_checker = new FileChecker($file_path, $this);
}
if ($this->debug_output) {
echo 'Visiting ' . $file_path . PHP_EOL;
}
$file_checker->visit(null);
if ($this->debug_output) {
echo 'Analyzing ' . $file_checker->getFilePath() . PHP_EOL;
}
$file_checker->checkMethods();
}
/**
* @param string $fq_class_name
* @return boolean
* @psalm-suppress MixedMethodCall due to Reflection class weirdness
*/
public function visitFileForClassLike($fq_class_name)
{
if (!$fq_class_name || strpos($fq_class_name, '::') !== false) {
throw new \InvalidArgumentException('Invalid class name ' . $fq_class_name);
}
if (isset($this->visited_classes[$fq_class_name])) {
return $this->visited_classes[$fq_class_name];
}
$this->visited_classes[$fq_class_name] = true;
$old_level = error_reporting();
error_reporting(0);
try {
$reflected_class = new \ReflectionClass($fq_class_name);
} catch (\ReflectionException $e) {
error_reporting($old_level);
$this->visited_classes[$fq_class_name] = false;
return false;
}
error_reporting($old_level);
if ($reflected_class->isUserDefined()) {
$file_path = (string)$reflected_class->getFileName();
if (isset($this->visited_files[$file_path])) {
return true;
}
$this->visited_files[$file_path] = true;
$file_checker = new FileChecker($file_path, $this);
$short_file_name = $file_checker->getFileName();
ClassLikeChecker::$file_classes[$file_path][] = $fq_class_name;
if (!isset(ClassLikeChecker::$storage[$fq_class_name])) {
ClassLikeChecker::$storage[$fq_class_name] = new \Psalm\Storage\ClassLikeStorage();
ClassLikeChecker::$storage[$fq_class_name]->file_path = $file_path;
ClassLikeChecker::$storage[$fq_class_name]->file_name = $short_file_name;
}
$storage = ClassLikeChecker::$storage[$fq_class_name];
if ($this->debug_output) {
echo 'Visiting ' . $file_path . PHP_EOL;
}
$file_checker->visit();
if (isset($this->files_to_visit[$file_path])) {
$this->file_checkers[$file_path] = $file_checker;
}
unset($this->files_to_visit[$file_path]);
if (ClassLikeChecker::inPropertyMap($fq_class_name)) {
$public_mapped_properties = ClassLikeChecker::getPropertyMap()[strtolower($fq_class_name)];
foreach ($public_mapped_properties as $property_name => $public_mapped_property) {
$property_type = Type::parseString($public_mapped_property);
$storage->properties[$property_name] = new PropertyStorage();
$storage->properties[$property_name]->type = $property_type;
$storage->properties[$property_name]->visibility = ClassLikeChecker::VISIBILITY_PUBLIC;
$property_id = $fq_class_name . '::$' . $property_name;
$storage->declaring_property_ids[$property_name] = $property_id;
$storage->appearing_property_ids[$property_name] = $property_id;
}
}
} else {
ClassLikeChecker::registerReflectedClass($reflected_class->name, $reflected_class);
}
return true;
2016-06-10 00:08:25 +02:00
}
2016-06-26 19:45:20 +02:00
/**
* Gets a Config object from an XML file.
2016-11-02 07:29:00 +01:00
*
* Searches up a folder hierarchy for the most immediate config.
2016-06-26 19:45:20 +02:00
*
* @param string $path
* @return Config
2016-11-02 07:29:00 +01:00
* @throws Exception\ConfigException If a config path is not found.
2016-06-26 19:45:20 +02:00
*/
protected function getConfigForPath($path)
2016-06-26 19:45:20 +02:00
{
$dir_path = realpath($path) . '/';
if (!is_dir($dir_path)) {
$dir_path = dirname($dir_path) . '/';
}
$config = null;
do {
$maybe_path = $dir_path . Config::DEFAULT_FILE_NAME;
if (file_exists($maybe_path)) {
2016-12-29 16:24:10 +01:00
$config = Config::loadFromXMLFile($maybe_path);
2016-07-26 21:03:15 +02:00
if ($config->autoloader) {
require_once($dir_path . $config->autoloader);
}
2016-11-21 03:49:06 +01:00
$config->collectPredefinedConstants();
2016-06-26 19:45:20 +02:00
break;
}
$dir_path = preg_replace('/[^\/]+\/$/', '', $dir_path);
2016-11-02 07:29:00 +01:00
} while ($dir_path !== '/');
2016-06-26 19:45:20 +02:00
if (!$config) {
throw new Exception\ConfigException('Config not found for path ' . $path);
}
$config->initializePlugins($this);
2016-06-26 19:45:20 +02:00
return $config;
}
2016-06-26 21:33:51 +02:00
2016-10-15 06:12:57 +02:00
/**
* @param string $path_to_config
2016-11-02 07:29:00 +01:00
* @return void
* @throws Exception\ConfigException If a config file is not found in the given location.
2016-10-15 06:12:57 +02:00
*/
2016-12-08 04:38:57 +01:00
public function setConfigXML($path_to_config)
2016-06-26 21:33:51 +02:00
{
if (!file_exists($path_to_config)) {
throw new Exception\ConfigException('Config not found at location ' . $path_to_config);
}
$dir_path = dirname($path_to_config) . '/';
2016-12-29 16:24:10 +01:00
$this->config = Config::loadFromXMLFile($path_to_config);
2016-12-08 04:38:57 +01:00
if ($this->config->autoloader) {
require_once($dir_path . $this->config->autoloader);
2016-07-26 21:03:15 +02:00
}
2016-11-21 03:49:06 +01:00
2016-12-08 04:38:57 +01:00
$this->config->collectPredefinedConstants();
2016-06-26 21:33:51 +02:00
}
2016-10-15 06:12:57 +02:00
/**
* @param array<string> $diff_files
* @return array<string>
*/
public static function getReferencedFilesFromDiff(array $diff_files)
{
2016-10-05 23:08:20 +02:00
$all_inherited_files_to_check = $diff_files;
while ($diff_files) {
$diff_file = array_shift($diff_files);
2016-10-05 23:08:20 +02:00
$dependent_files = FileChecker::getFilesInheritingFromFile($diff_file);
$new_dependent_files = array_diff($dependent_files, $all_inherited_files_to_check);
$all_inherited_files_to_check += $new_dependent_files;
$diff_files += $new_dependent_files;
}
$all_files_to_check = $all_inherited_files_to_check;
foreach ($all_inherited_files_to_check as $file_name) {
$dependent_files = FileChecker::getFilesReferencingFile($file_name);
$all_files_to_check = array_merge($dependent_files, $all_files_to_check);
}
2016-10-05 23:08:20 +02:00
return array_unique($all_files_to_check);
}
2016-12-08 04:38:57 +01:00
/**
* @param string $file_path
* @param string $file_contents
* @return void
*/
public function registerFile($file_path, $file_contents)
{
$this->fake_files[$file_path] = $file_contents;
}
/**
* @param string $file_path
* @return string
*/
public function getFileContents($file_path)
{
if (isset($this->fake_files[$file_path])) {
return $this->fake_files[$file_path];
}
return (string)file_get_contents($file_path);
}
2016-06-10 00:08:25 +02:00
}