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

1276 lines
37 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;
2017-01-12 06:54:41 +01:00
use Psalm\Context;
use Psalm\Exception;
use Psalm\Issue\PossiblyUnusedMethod;
use Psalm\Issue\UnusedClass;
use Psalm\Issue\UnusedMethod;
use Psalm\IssueBuffer;
use Psalm\Provider\CacheProvider;
use Psalm\Provider\FileProvider;
use Psalm\Provider\FileReferenceProvider;
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
*/
private $config;
2016-12-08 04:38:57 +01:00
/**
* @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
*
2017-05-25 04:07:49 +02:00
* @var bool
2016-07-25 21:05:58 +02:00
*/
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
*
2017-05-25 04:07:49 +02:00
* @var bool
*/
2016-12-08 04:38:57 +01:00
public $show_info;
/**
* @var string
*/
public $output_format;
2017-01-30 04:30:27 +01:00
/**
* @var bool
*/
public $collect_references = false;
2017-01-30 04:30:27 +01:00
/**
* @var string|null
*/
public $find_references_to;
/**
* @var bool
*/
public $debug_output = false;
2017-01-13 20:06:05 +01:00
/**
2017-05-25 04:07:49 +02:00
* @var bool
2017-01-13 20:06:05 +01:00
*/
public $update_docblocks = false;
2017-01-12 06:54:41 +01:00
/**
2017-05-25 04:07:49 +02:00
* @var bool
2017-01-12 06:54:41 +01:00
*/
public $cache = false;
2017-01-09 05:58:06 +01:00
/**
* @var array<string, bool>
*/
private $existing_classlikes_ci = [];
2017-01-09 05:58:06 +01:00
/**
* @var array<string, bool>
*/
private $existing_classlikes = [];
2017-01-09 05:58:06 +01:00
/**
* @var array<string, bool>
*/
private $existing_classes_ci = [];
2017-01-09 05:58:06 +01:00
/**
* @var array<string, bool>
*/
public $existing_classes = [];
/**
* @var array<string, bool>
*/
private $existing_interfaces_ci = [];
2017-01-09 05:58:06 +01:00
/**
* @var array<string, bool>
*/
public $existing_interfaces = [];
/**
* @var array<string, bool>
*/
private $existing_traits_ci = [];
2017-01-09 05:58:06 +01:00
/**
* @var array<string, bool>
*/
public $existing_traits = [];
/**
* @var array<string, string>
*/
private $classlike_files = [];
2017-01-09 05:58:06 +01:00
/**
* @var array<string, string>
*/
private $files_to_visit = [];
2017-01-08 18:55:32 +01:00
/**
* @var array<string, bool>
2017-01-08 18:55:32 +01:00
*/
private $files_to_analyze = [];
2017-01-08 18:55:32 +01:00
/**
* @var array<string, bool>
*/
private $visited_files = [];
/**
* @var array<string, bool>
*/
private $visited_classes = [];
/**
* @var array<string, FileChecker>
*/
private $file_checkers = [];
2017-01-12 06:54:41 +01:00
/**
* @var array<string, MethodChecker>
*/
public $method_checkers = [];
/**
* @var array<string, int>
*/
public $classlike_references = [];
2017-02-28 00:24:20 +01:00
/**
* A map of fully-qualified use declarations to the files
* that reference them (keyed by filename)
2017-03-11 01:36:17 +01:00
*
* @var array<string, array<string, array<int, \Psalm\CodeLocation>>>
2017-02-28 00:24:20 +01:00
*/
public $use_referencing_locations = [];
2017-03-11 01:36:17 +01:00
/**
* A map of file names to the classes that they contain explicit references to
* used in collaboration with use_referencing_locations
*
* @var array<string, array<string, bool>>
*/
public $use_referencing_files = [];
2016-12-08 04:38:57 +01:00
/**
* @var array<string, string>
*/
public $fake_files = [];
2017-02-15 06:15:51 +01:00
/**
2017-05-25 04:07:49 +02:00
* @var bool
2017-02-15 06:15:51 +01:00
*/
public $server_mode = false;
/**
* Whether to log functions just at the file level or globally (for stubs)
*
2017-05-25 04:07:49 +02:00
* @var bool
*/
public $register_global_functions = false;
2016-12-08 04:38:57 +01:00
const TYPE_CONSOLE = 'console';
const TYPE_JSON = 'json';
2017-01-16 04:39:26 +01:00
const TYPE_EMACS = 'emacs';
2016-12-08 04:38:57 +01:00
2016-12-17 06:48:31 +01:00
/**
2017-05-25 04:07:49 +02:00
* @param bool $use_color
* @param bool $show_info
* @param bool $debug_output
2016-12-17 06:48:31 +01:00
* @param string $output_format
2017-01-13 20:06:05 +01:00
* @param bool $update_docblocks
* @param bool $collect_references
* @param string $find_references_to
2016-12-17 06:48:31 +01:00
*/
public function __construct(
$use_color = true,
$show_info = true,
$output_format = self::TYPE_CONSOLE,
2017-01-13 20:06:05 +01:00
$debug_output = false,
$update_docblocks = false,
$collect_references = false,
$find_references_to = null
) {
2016-12-08 04:38:57 +01:00
$this->use_color = $use_color;
$this->show_info = $show_info;
$this->debug_output = $debug_output;
2017-01-13 20:06:05 +01:00
$this->update_docblocks = $update_docblocks;
$this->collect_references = $collect_references;
$this->find_references_to = $find_references_to;
2016-12-08 04:38:57 +01:00
2017-05-27 02:05:57 +02:00
if (!in_array($output_format, [self::TYPE_CONSOLE, self::TYPE_JSON, self::TYPE_EMACS], true)) {
2016-12-08 04:38:57 +01:00
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 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
*/
public function check($base_dir, $is_diff = false)
2016-06-13 21:33:18 +02:00
{
$start_checks = (int)microtime(true);
if (!$base_dir) {
throw new \InvalidArgumentException('Cannot work with empty base_dir');
2016-10-15 06:12:57 +02:00
}
2016-12-08 04:38:57 +01:00
if (!$this->config) {
$this->config = $this->getConfigForPath($base_dir, $base_dir);
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;
if ($is_diff && FileReferenceProvider::loadReferenceCache() && CacheProvider::canDiffFiles()) {
$deleted_files = FileReferenceProvider::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) {
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
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) {
2017-01-13 20:06:05 +01:00
$this->checkDirWithConfig($dir_name, $this->config);
}
$this->visitFiles();
2017-02-15 06:15:51 +01:00
if (!$this->server_mode) {
$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);
2017-01-12 07:12:01 +01:00
$this->visitFiles();
2017-02-15 06:15:51 +01:00
if (!$this->server_mode) {
$this->analyzeFiles();
}
2016-10-07 06:58:08 +02:00
}
$removed_parser_files = CacheProvider::deleteOldParserCaches(
$is_diff ? CacheProvider::getLastGoodRun() : $start_checks
);
if ($this->debug_output && $removed_parser_files) {
echo 'Removed ' . $removed_parser_files . ' old parser caches' . PHP_EOL;
}
if ($is_diff) {
CacheProvider::touchParserCaches($this->getAllFiles($this->config), $start_checks);
}
if ($this->collect_references) {
if ($this->find_references_to) {
if (strpos($this->find_references_to, '::') !== false) {
2017-02-28 00:24:20 +01:00
$locations_by_files = $this->findReferencesToMethod($this->find_references_to);
} else {
2017-02-28 00:24:20 +01:00
$locations_by_files = $this->findReferencesToClassLike($this->find_references_to);
}
2017-02-28 00:24:20 +01:00
foreach ($locations_by_files as $locations) {
2017-03-02 07:10:00 +01:00
$bounds_starts = [];
2017-03-02 04:27:52 +01:00
2017-02-28 00:24:20 +01:00
foreach ($locations as $location) {
$snippet = $location->getSnippet();
2017-02-28 00:24:20 +01:00
$snippet_bounds = $location->getSnippetBounds();
$selection_bounds = $location->getSelectionBounds();
2017-03-02 07:10:00 +01:00
if (isset($bounds_starts[$selection_bounds[0]])) {
2017-03-02 04:27:52 +01:00
continue;
}
2017-03-02 07:10:00 +01:00
$bounds_starts[$selection_bounds[0]] = true;
2017-03-02 04:27:52 +01:00
2017-02-28 00:24:20 +01:00
$selection_start = $selection_bounds[0] - $snippet_bounds[0];
$selection_length = $selection_bounds[1] - $selection_bounds[0];
2017-02-28 00:24:20 +01:00
echo $location->file_name . ':' . $location->getLineNumber() . PHP_EOL .
($this->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
) . PHP_EOL . PHP_EOL;
}
}
2017-03-03 21:54:08 +01:00
} else {
$this->checkClassReferences();
}
}
IssueBuffer::finish(true, (int)$start_checks, $this->visited_files);
}
/**
* @return void
*/
private function visitFiles()
{
if (!$this->config) {
throw new \UnexpectedValueException('$this->config cannot be null');
}
$filetype_handlers = $this->config->getFiletypeHandlers();
2017-01-08 18:55:32 +01:00
foreach ($this->files_to_analyze as $file_path => $_) {
2017-02-15 06:15:51 +01:00
if (!isset($this->visited_files[$file_path])) {
$this->visitFile($file_path, $filetype_handlers);
}
}
}
/**
* @return void
*/
private function analyzeFiles()
{
2017-01-08 18:55:32 +01:00
if (!$this->config) {
throw new \UnexpectedValueException('$this->config cannot be null');
}
$filetype_handlers = $this->config->getFiletypeHandlers();
foreach ($this->files_to_analyze as $file_path => $_) {
2017-02-15 06:15:51 +01:00
$file_checker = $this->visitFile($file_path, $filetype_handlers, true);
if ($this->debug_output) {
echo 'Analyzing ' . $file_checker->getFilePath() . PHP_EOL;
}
2017-01-13 20:06:05 +01:00
$file_checker->analyze($this->update_docblocks);
}
2016-06-13 21:33:18 +02:00
}
/**
* @param string $method_id
2017-05-27 02:16:18 +02:00
*
2017-02-28 00:24:20 +01:00
* @return array<string, \Psalm\CodeLocation[]>
*/
public function findReferencesToMethod($method_id)
{
list($fq_class_name, $method_name) = explode('::', $method_id);
if (!isset(ClassLikeChecker::$storage[strtolower($fq_class_name)])) {
die('Class ' . $fq_class_name . ' cannot be found' . PHP_EOL);
}
$class_storage = ClassLikeChecker::$storage[strtolower($fq_class_name)];
if (!isset($class_storage->methods[strtolower($method_name)])) {
die('Method ' . $method_id . ' cannot be found' . PHP_EOL);
}
$method_storage = $class_storage->methods[strtolower($method_name)];
if ($method_storage->referencing_locations === null) {
die('No references found for ' . $method_id . PHP_EOL);
}
return $method_storage->referencing_locations;
}
/**
* @param string $fq_class_name
2017-05-27 02:16:18 +02:00
*
2017-02-28 00:24:20 +01:00
* @return array<string, \Psalm\CodeLocation[]>
*/
public function findReferencesToClassLike($fq_class_name)
{
if (!isset(ClassLikeChecker::$storage[strtolower($fq_class_name)])) {
die('Class ' . $fq_class_name . ' cannot be found' . PHP_EOL);
}
$class_storage = ClassLikeChecker::$storage[strtolower($fq_class_name)];
if ($class_storage->referencing_locations === null) {
die('No references found for ' . $fq_class_name . PHP_EOL);
}
$classlike_references_by_file = $class_storage->referencing_locations;
if (isset($this->use_referencing_locations[strtolower($fq_class_name)])) {
foreach ($this->use_referencing_locations[strtolower($fq_class_name)] as $file_path => $locations) {
if (!isset($classlike_references_by_file[$file_path])) {
$classlike_references_by_file[$file_path] = $locations;
} else {
$classlike_references_by_file[$file_path] = array_merge(
$locations,
$classlike_references_by_file[$file_path]
);
}
}
}
return $classlike_references_by_file;
}
/**
* @return void
*/
public function checkClassReferences()
{
foreach ($this->existing_classlikes_ci as $fq_class_name_ci => $_) {
if (isset(ClassLikeChecker::$storage[$fq_class_name_ci])) {
$classlike_storage = ClassLikeChecker::$storage[$fq_class_name_ci];
if ($classlike_storage->location &&
$this->config &&
$this->config->isInProjectDirs($classlike_storage->location->file_path)
) {
if (!isset($this->classlike_references[$fq_class_name_ci])) {
if (IssueBuffer::accepts(
new UnusedClass(
'Class ' . $classlike_storage->name . ' is never used',
$classlike_storage->location
)
)) {
// fall through
}
} else {
self::checkMethodReferences($classlike_storage);
}
}
}
}
}
/**
* @param \Psalm\Storage\ClassLikeStorage $classlike_storage
2017-05-27 02:16:18 +02:00
*
* @return void
*/
protected static function checkMethodReferences($classlike_storage)
{
foreach ($classlike_storage->methods as $method_name => $method_storage) {
if (count($method_storage->referencing_locations) === 0 &&
!$classlike_storage->overridden_method_ids[$method_name] &&
(substr($method_name, 0, 2) !== '__' || $method_name === '__construct') &&
2017-02-08 06:42:53 +01:00
$method_storage->location
) {
$method_id = $classlike_storage->name . '::' . $method_storage->cased_name;
if ($method_storage->visibility === ClassLikeChecker::VISIBILITY_PUBLIC) {
if (IssueBuffer::accepts(
new PossiblyUnusedMethod(
'Cannot find public calls to method ' . $method_id,
$method_storage->location
)
)) {
// fall through
}
} else {
if (IssueBuffer::accepts(
new UnusedMethod(
'Method ' . $method_id . ' is never used',
$method_storage->location
)
)) {
// fall through
}
}
}
}
}
2016-10-15 06:12:57 +02:00
/**
* @param string $dir_name
* @param string $base_dir
2017-05-27 02:16:18 +02:00
*
2016-10-15 06:12:57 +02:00
* @return void
*/
public function checkDir($dir_name, $base_dir)
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, $base_dir);
2017-01-18 04:10:21 +01:00
$this->config->hide_external_errors = $this->config->isInProjectDirs($dir_name . DIRECTORY_SEPARATOR);
2016-06-26 19:45:20 +02:00
}
2016-06-10 00:08:25 +02:00
FileReferenceProvider::loadReferenceCache();
$start_checks = (int)microtime(true);
$this->checkDirWithConfig($dir_name, $this->config, true);
$this->visitFiles();
$this->analyzeFiles();
IssueBuffer::finish(false, $start_checks, $this->visited_files);
}
2016-10-15 06:12:57 +02:00
/**
* @param string $dir_name
* @param Config $config
2017-01-19 05:38:21 +01:00
* @param bool $allow_non_project_files
2017-05-27 02:16:18 +02:00
*
2016-10-15 06:12:57 +02:00
* @return void
*/
private function checkDirWithConfig($dir_name, Config $config, $allow_non_project_files = false)
{
$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();
2017-05-27 02:05:57 +02:00
if (in_array($extension, $file_extensions, true)) {
$file_path = (string)$iterator->getRealPath();
if ($allow_non_project_files || $config->isInProjectDirs($file_path)) {
$this->files_to_analyze[$file_path] = true;
}
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
2017-05-27 02:16:18 +02:00
*
2016-11-07 23:07:59 +01:00
* @return array<int, string>
*/
private 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();
2017-05-27 02:05:57 +02:00
if (in_array($extension, $file_extensions, true)) {
$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
2017-05-27 02:16:18 +02:00
*
2016-10-15 06:12:57 +02:00
* @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();
2017-05-27 02:05:57 +02:00
if (in_array($extension, $file_extensions, true)) {
2016-10-15 06:12:57 +02:00
$file_name = (string)$iterator->getRealPath();
2016-10-07 06:58:08 +02:00
2017-01-16 17:51:42 +01:00
if ($config->isInProjectDirs($file_name)) {
if (FileProvider::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
2017-05-27 02:16:18 +02:00
*
2016-10-15 06:12:57 +02:00
* @return void
*/
private function checkDiffFilesWithConfig(Config $config, array $file_list = [])
{
2017-01-12 07:12:01 +01:00
foreach ($file_list as $file_path) {
if (!file_exists($file_path)) {
2016-10-09 02:49:14 +02:00
continue;
}
2017-01-16 17:51:42 +01:00
if (!$config->isInProjectDirs($file_path)) {
if ($this->debug_output) {
2017-05-27 02:05:57 +02:00
echo 'skipping ' . $file_path . PHP_EOL;
}
continue;
}
2016-10-09 02:49:14 +02:00
$this->files_to_analyze[$file_path] = true;
}
}
2016-10-15 06:12:57 +02:00
/**
* @param string $file_path
* @param string $base_dir
2017-05-27 02:16:18 +02:00
*
2016-10-15 06:12:57 +02:00
* @return void
*/
public function checkFile($file_path, $base_dir)
{
if ($this->debug_output) {
echo 'Checking ' . $file_path . 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_path, $base_dir);
2016-06-26 19:45:20 +02:00
}
$start_checks = (int)microtime(true);
$this->config->hide_external_errors = $this->config->isInProjectDirs($file_path);
$this->files_to_analyze[$file_path] = true;
2016-12-08 04:38:57 +01:00
$filetype_handlers = $this->config->getFiletypeHandlers();
FileReferenceProvider::loadReferenceCache();
$file_checker = $this->visitFile($file_path, $filetype_handlers, true);
if ($this->debug_output) {
echo 'Analyzing ' . $file_checker->getFilePath() . PHP_EOL;
2016-06-10 00:08:25 +02:00
}
2017-01-13 20:06:05 +01:00
$file_checker->analyze($this->update_docblocks);
IssueBuffer::finish(false, $start_checks, $this->visited_files);
}
/**
* @param string $file_path
* @param array $filetype_handlers
2017-02-15 06:15:51 +01:00
* @param bool $will_analyze
2017-05-27 02:16:18 +02:00
*
2017-01-08 18:55:32 +01:00
* @return FileChecker
*/
2017-02-15 06:15:51 +01:00
private function visitFile($file_path, array $filetype_handlers, $will_analyze = false)
{
$extension = (string)pathinfo($file_path)['extension'];
if (isset($filetype_handlers[$extension])) {
/** @var FileChecker */
$file_checker = new $filetype_handlers[$extension]($file_path, $this);
} else {
2017-02-15 06:15:51 +01:00
$file_checker = new FileChecker($file_path, $this, null, $will_analyze);
}
if ($this->debug_output) {
scrutinizer-ci (#152) * swapping phpcs for php-cs-fixer * workaround for php-cs-fixer treating parenthesis following echo as the function call variant * amending rules * blank_line_before_return * majority of files pass with these disabled, could remove later * combine_consecutive_unsets * concat_space * placeholder for if vimeo/psalm ever goes php:^7.0 * function_to_constant * disabling include * linebreak_after_opening_tag, lowercase_cast, magic_constant_casing * mb_str_functions disabled * method_separation * native_function_casing * native_function_invocations * new_with_braces disabled to match usage * no_alias_functions * no_blank_lines_after_class_opening * no_blank_lines_after_phpdoc * no_blank_lines_before_namespace * no_empty_comment * no_empty_phpdoc * no_empty_statement * no_extra_consecutive_blank_lines * no_leading_import_slash to discuss * no_leading_namespace_whitespace * no_mixed_echo_print * no_multiline_whitespace_around_double_arrow * no_multiline_whitespace_before_semicolons * no_php4_constructor * no_short_bool_cast * no_short_echo_tag * no_singleline_whitespace_before_semicolons * no_spaces_around_offset * no_trailing_comma_in_list_call * no_trailing_comma_in_singleline_array * no_unneeded_control_parentheses to discuss * no_unreachable_default_argument_value * no_unused_imports to discuss * no_useless_else to discuss * no_useless_return * no_whitespace_before_comma_in_array * no_whitespace_in_blank_line * non_printable_character * normalize_index_brace * ordered_class_elements to discuss * ordered_imports to discss * php_unit_construct * php_unit_dedicate_assert * php_unit_fqcn_annotation * php_unit_strict to discuss * php_unit_test_class_requires_covers to discuss * phpdoc_add_missing_param_annotation * phpdoc_align to discuss * phpdoc_annotation_without_dot to discuss * phpdoc_indent to discuss * phpdoc_inline_tag * phpdoc_no_access * phpdoc_no_alias_tag * phpdoc_no_empty_return * phpdoc_no_package * phpdoc_no_useless_inheritdoc * phpdoc_order to discuss * phpdoc_return_self_reference * phpdoc_scalar to discuss * phpdoc_separation to discuss * phpdoc_single_line_var_spacing * phpdoc_summary to discuss * phpdoc_to_comment to discuss * phpdoc_trim to discuss * phpdoc_types * phpdoc_var_without_name * pow_to_exponentiation * pre_increment to discuss * protected_to_private * psr0 turned off * psr4 turned on * random_api_migration * return_type_declaration to discuss * self_accessor to discuss * semicolon_after_instruction * short_scalar_cast * silenced_deprecation_error turned off * simplified_null_return to discuss * single_quote * space_after_semicolon * standardize_not_equals * strict_comparison to discuss * strict_param to discuss * ternary_operator_spaces * ternary_to_null_coalescing should be set to true if vimeo/psalm ever goes php:^7.0 * trailing_comma_in_multiline_array to discuss * trim_array_spaces * unary_operator_spaces * whitespace_after_comma_in_array to discuss * multi-version scrutinizer to match travis * binary_operator_space * not the best solution, but it works to exclude the call map from php-cs-fixer * reducing verbosity of config where defaults were used * dry run php-cs-fixer as part of tests * disabling rule pending FriendsOfPHP/PHP-CS-Fixer#2739 * enabling no_unused_imports * enabling ordered_imports * ignoring user-defined .php_cs * using $TRAVIS_COMMIT_RANGE to only test modified files * enabling no_leading_import_slash * conditionally testing everything * filter output then perform exact match * restoring phpcs via partial cherry pick of f65c618
2017-05-27 00:26:14 +02:00
$rev_or_V = (isset($this->visited_files[$file_path]) ? 'Rev' : 'V');
echo $rev_or_V . 'isiting ' . $file_path . PHP_EOL;
}
2017-01-08 19:21:21 +01:00
$this->visited_files[$file_path] = true;
2017-01-08 18:55:32 +01:00
$file_checker->visit();
return $file_checker;
}
/**
2017-01-09 05:58:06 +01:00
* Checks whether a class exists, and if it does then records what file it's in
* for later checking
*
* @param string $fq_class_name
2017-05-27 02:16:18 +02:00
*
2017-05-25 04:07:49 +02:00
* @return bool
* @psalm-suppress MixedMethodCall due to Reflection class weirdness
*/
2017-01-09 05:58:06 +01:00
public function fileExistsForClassLike($fq_class_name)
{
$fq_class_name_ci = strtolower($fq_class_name);
if (isset($this->existing_classlikes_ci[$fq_class_name_ci])) {
return $this->existing_classlikes_ci[$fq_class_name_ci];
}
2017-02-01 01:21:33 +01:00
if (!$this->config) {
throw new \UnexpectedValueException('Config should not be null here');
}
$predefined_classlikes = $this->config->getPredefinedClassLikes();
2017-02-01 01:21:33 +01:00
if (isset($predefined_classlikes[$fq_class_name_ci])) {
$this->visited_classes[$fq_class_name_ci] = true;
$reflected_class = new \ReflectionClass($fq_class_name);
ClassLikeChecker::registerReflectedClass($reflected_class->name, $reflected_class, $this);
2017-05-25 04:07:49 +02:00
2017-02-01 01:21:33 +01:00
return true;
}
$old_level = error_reporting();
error_reporting(0);
try {
$reflected_class = new \ReflectionClass($fq_class_name);
} catch (\ReflectionException $e) {
error_reporting($old_level);
// do not cache any results here (as case-sensitive filenames can screw things up)
return false;
}
error_reporting($old_level);
$file_path = (string)$reflected_class->getFileName();
// if the file was autoloaded but exists in evaled code only, return false
if (!file_exists($file_path)) {
return false;
}
2017-02-01 01:21:33 +01:00
$fq_class_name = $reflected_class->getName();
$this->existing_classlikes_ci[$fq_class_name_ci] = true;
$this->existing_classlikes[$fq_class_name] = true;
if ($reflected_class->isInterface()) {
$this->addFullyQualifiedInterfaceName($fq_class_name, $file_path);
2017-02-01 01:21:33 +01:00
} elseif ($reflected_class->isTrait()) {
$this->addFullyQualifiedTraitName($fq_class_name, $file_path);
2017-01-09 05:58:06 +01:00
} else {
$this->addFullyQualifiedClassName($fq_class_name, $file_path);
2017-01-09 05:58:06 +01:00
}
return true;
}
/**
2017-01-12 06:54:41 +01:00
* @param string $fq_class_name
2017-05-27 02:16:18 +02:00
*
2017-05-25 04:07:49 +02:00
* @return bool
2017-01-09 05:58:06 +01:00
* @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);
}
$fq_class_name_ci = strtolower($fq_class_name);
if (isset($this->visited_classes[$fq_class_name_ci])) {
return $this->visited_classes[$fq_class_name_ci];
2017-01-09 05:58:06 +01:00
}
// this registers the class if it's not user defined
if (!$this->fileExistsForClassLike($fq_class_name)) {
return false;
}
$this->visited_classes[$fq_class_name_ci] = true;
if (isset($this->classlike_files[$fq_class_name_ci])) {
$file_path = $this->classlike_files[$fq_class_name_ci];
if (isset($this->visited_files[$file_path])) {
return true;
}
2017-01-09 05:58:06 +01:00
$this->visited_files[$file_path] = true;
2017-02-15 06:15:51 +01:00
$file_checker = new FileChecker(
$file_path,
$this,
null,
false
);
ClassLikeChecker::$file_classes[$file_path][] = $fq_class_name;
2017-01-09 05:58:06 +01:00
$fq_class_name_lower = strtolower($fq_class_name);
if ($this->debug_output) {
echo 'Visiting ' . $file_path . PHP_EOL;
}
$file_checker->visit();
$storage = ClassLikeChecker::$storage[$fq_class_name_lower];
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;
}
}
}
return true;
2016-06-10 00:08:25 +02:00
}
2016-06-26 19:45:20 +02:00
2017-01-12 06:54:41 +01:00
/**
* @return void
*/
public function enableCache()
{
$this->cache = true;
}
/**
* @return void
*/
public function disableCache()
{
$this->cache = false;
}
/**
* @return bool
*/
public function canCache()
{
return $this->cache;
}
/**
* @param string $original_method_id
* @param Context $this_context
2017-05-27 02:16:18 +02:00
*
2017-01-12 06:54:41 +01:00
* @return void
*/
public function getMethodMutations($original_method_id, Context $this_context)
{
list($fq_class_name) = explode('::', $original_method_id);
2017-01-12 06:54:41 +01:00
$file_checker = $this->getVisitedFileCheckerForClassLike($fq_class_name);
$appearing_method_id = (string)MethodChecker::getAppearingMethodId($original_method_id);
list($appearing_fq_class_name) = explode('::', $appearing_method_id);
2017-01-12 06:54:41 +01:00
2017-07-09 21:27:07 +02:00
$appearing_class_storage = ClassLikeChecker::$storage[strtolower($appearing_fq_class_name)];
if (!$appearing_class_storage->user_defined) {
return;
}
if (strtolower($appearing_fq_class_name) !== strtolower($fq_class_name)) {
$file_checker = $this->getVisitedFileCheckerForClassLike($appearing_fq_class_name);
2017-01-12 06:54:41 +01:00
}
$file_checker->analyze(false, true);
if (!$this_context->self) {
$this_context->self = $fq_class_name;
$this_context->vars_in_scope['$this'] = Type::parseString($fq_class_name);
}
$file_checker->getMethodMutations($appearing_method_id, $this_context);
2017-01-12 06:54:41 +01:00
}
/**
* @param string $fq_class_name
2017-05-27 02:16:18 +02:00
*
2017-01-12 06:54:41 +01:00
* @return FileChecker
*/
private function getVisitedFileCheckerForClassLike($fq_class_name)
2017-01-12 06:54:41 +01:00
{
$fq_class_name_ci = strtolower($fq_class_name);
2017-01-12 06:54:41 +01:00
if (!$this->fake_files) {
// this registers the class if it's not user defined
if (!$this->fileExistsForClassLike($fq_class_name)) {
throw new \UnexpectedValueException('File does not exist for ' . $fq_class_name);
}
if (!isset($this->classlike_files[$fq_class_name_ci])) {
2017-01-12 06:54:41 +01:00
throw new \UnexpectedValueException('Class ' . $fq_class_name . ' is not user-defined');
}
$file_path = $this->classlike_files[$fq_class_name_ci];
2017-01-12 06:54:41 +01:00
} else {
$file_path = array_keys($this->fake_files)[0];
}
if ($this->cache && isset($this->file_checkers[$file_path])) {
return $this->file_checkers[$file_path];
}
$file_checker = new FileChecker($file_path, $this, null, true);
2017-01-12 06:54:41 +01:00
$file_checker->visit();
if ($this->debug_output) {
echo 'Visiting ' . $file_path . PHP_EOL;
}
if ($this->cache) {
$this->file_checkers[$file_path] = $file_checker;
}
return $file_checker;
}
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
* @param string $base_dir
2017-05-27 02:16:18 +02:00
*
* @throws Exception\ConfigException if a config path is not found
*
2016-06-26 19:45:20 +02:00
* @return Config
*/
private function getConfigForPath($path, $base_dir)
2016-06-26 19:45:20 +02:00
{
$dir_path = realpath($path);
2016-06-26 19:45:20 +02:00
if (!is_dir($dir_path)) {
$dir_path = dirname($dir_path);
2016-06-26 19:45:20 +02:00
}
$config = null;
do {
$maybe_path = $dir_path . DIRECTORY_SEPARATOR . Config::DEFAULT_FILE_NAME;
2016-06-26 19:45:20 +02:00
if (file_exists($maybe_path)) {
$config = Config::loadFromXMLFile($maybe_path, $base_dir);
2016-07-26 21:03:15 +02:00
2016-06-26 19:45:20 +02:00
break;
}
$dir_path = dirname($dir_path);
} while (dirname($dir_path) !== $dir_path);
2016-06-26 19:45:20 +02:00
if (!$config) {
2017-02-13 05:59:33 +01:00
if ($this->output_format === self::TYPE_CONSOLE) {
exit(
'Could not locate a config XML file in path ' . $path . '. Have you run \'psalm --init\' ?' .
PHP_EOL
);
2017-02-13 05:59:33 +01:00
}
2016-06-26 19:45:20 +02:00
throw new Exception\ConfigException('Config not found for path ' . $path);
}
2017-02-01 01:21:33 +01:00
$this->config = $config;
$config->visitStubFiles($this);
$config->initializePlugins($this);
2016-06-26 19:45:20 +02:00
return $config;
}
2016-06-26 21:33:51 +02:00
2017-02-01 01:21:33 +01:00
/**
* @param Config $config
2017-05-27 02:16:18 +02:00
*
2017-02-01 01:21:33 +01:00
* @return void
*/
public function setConfig(Config $config)
{
$this->config = $config;
$config->visitStubFiles($this);
$config->initializePlugins($this);
}
2016-10-15 06:12:57 +02:00
/**
* @param string $path_to_config
* @param string $base_dir
2017-05-27 02:16:18 +02:00
*
* @throws Exception\ConfigException if a config file is not found in the given location
*
2016-11-02 07:29:00 +01:00
* @return void
2016-10-15 06:12:57 +02:00
*/
public function setConfigXML($path_to_config, $base_dir)
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);
}
$this->config = Config::loadFromXMLFile($path_to_config, $base_dir);
2017-02-13 05:59:33 +01:00
$this->config->visitStubFiles($this);
$this->config->initializePlugins($this);
2016-06-26 21:33:51 +02:00
}
2016-10-15 06:12:57 +02:00
/**
* @param array<string> $diff_files
2017-05-27 02:16:18 +02:00
*
2016-10-15 06:12:57 +02:00
* @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);
$dependent_files = FileReferenceProvider::getFilesInheritingFromFile($diff_file);
2016-10-05 23:08:20 +02:00
$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 = FileReferenceProvider::getFilesReferencingFile($file_name);
2016-10-05 23:08:20 +02:00
$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
2017-05-27 02:16:18 +02:00
*
2016-12-08 04:38:57 +01:00
* @return void
*/
public function registerFile($file_path, $file_contents)
{
$this->fake_files[$file_path] = $file_contents;
}
2017-01-19 18:15:42 +01:00
/**
* @param string $file_path
2017-05-27 02:16:18 +02:00
*
2017-01-19 18:15:42 +01:00
* @return void
*/
public function registerAnalyzableFile($file_path)
2017-01-19 18:15:42 +01:00
{
$this->visited_files[$file_path] = true;
$this->files_to_analyze[$file_path] = true;
2017-01-19 18:15:42 +01:00
}
2016-12-08 04:38:57 +01:00
/**
* @param string $file_path
2017-05-27 02:16:18 +02:00
*
2016-12-08 04:38:57 +01:00
* @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);
}
2017-01-09 05:58:06 +01:00
/**
2017-01-19 18:15:42 +01:00
* @param string $fq_class_name
* @param string|null $file_path
2017-05-27 02:16:18 +02:00
*
2017-01-09 05:58:06 +01:00
* @return void
*/
public function addFullyQualifiedClassName($fq_class_name, $file_path = null)
2017-01-09 05:58:06 +01:00
{
$fq_class_name_ci = strtolower($fq_class_name);
$this->existing_classlikes_ci[$fq_class_name_ci] = true;
$this->existing_classes_ci[$fq_class_name_ci] = true;
$this->existing_traits_ci[$fq_class_name_ci] = false;
$this->existing_interfaces_ci[$fq_class_name_ci] = false;
$this->existing_classes[$fq_class_name] = true;
if ($file_path) {
$this->classlike_files[$fq_class_name_ci] = $file_path;
}
2017-01-09 05:58:06 +01:00
}
/**
2017-01-19 18:15:42 +01:00
* @param string $fq_class_name
* @param string|null $file_path
2017-05-27 02:16:18 +02:00
*
2017-01-09 05:58:06 +01:00
* @return void
*/
public function addFullyQualifiedInterfaceName($fq_class_name, $file_path = null)
2017-01-09 05:58:06 +01:00
{
$fq_class_name_ci = strtolower($fq_class_name);
$this->existing_classlikes_ci[$fq_class_name_ci] = true;
$this->existing_interfaces_ci[$fq_class_name_ci] = true;
$this->existing_classes_ci[$fq_class_name_ci] = false;
$this->existing_traits_ci[$fq_class_name_ci] = false;
$this->existing_interfaces[$fq_class_name] = true;
if ($file_path) {
$this->classlike_files[$fq_class_name_ci] = $file_path;
}
2017-01-09 05:58:06 +01:00
}
/**
2017-01-19 18:15:42 +01:00
* @param string $fq_class_name
* @param string|null $file_path
2017-05-27 02:16:18 +02:00
*
2017-01-09 05:58:06 +01:00
* @return void
*/
public function addFullyQualifiedTraitName($fq_class_name, $file_path = null)
2017-01-09 05:58:06 +01:00
{
$fq_class_name_ci = strtolower($fq_class_name);
$this->existing_classlikes_ci[$fq_class_name_ci] = true;
$this->existing_traits_ci[$fq_class_name_ci] = true;
$this->existing_classes_ci[$fq_class_name_ci] = false;
$this->existing_interfaces_ci[$fq_class_name_ci] = false;
$this->existing_traits[$fq_class_name] = true;
if ($file_path) {
$this->classlike_files[$fq_class_name_ci] = $file_path;
}
2017-01-09 05:58:06 +01:00
}
/**
* @param string $fq_class_name
2017-05-27 02:16:18 +02:00
*
2017-01-09 05:58:06 +01:00
* @return bool
*/
public function hasFullyQualifiedClassName($fq_class_name)
{
$fq_class_name_ci = strtolower($fq_class_name);
2017-01-30 04:30:27 +01:00
if (!isset($this->existing_classes_ci[$fq_class_name_ci]) ||
!$this->existing_classes_ci[$fq_class_name_ci]
) {
return false;
}
if ($this->collect_references) {
if (!isset($this->classlike_references[$fq_class_name_ci])) {
$this->classlike_references[$fq_class_name_ci] = 0;
}
2017-05-27 02:05:57 +02:00
++$this->classlike_references[$fq_class_name_ci];
2017-01-30 04:30:27 +01:00
}
return true;
2017-01-09 05:58:06 +01:00
}
/**
* @param string $fq_class_name
2017-05-27 02:16:18 +02:00
*
2017-01-09 05:58:06 +01:00
* @return bool
*/
public function hasFullyQualifiedInterfaceName($fq_class_name)
{
$fq_class_name_ci = strtolower($fq_class_name);
2017-01-30 04:30:27 +01:00
if (!isset($this->existing_interfaces_ci[$fq_class_name_ci]) ||
!$this->existing_interfaces_ci[$fq_class_name_ci]
) {
return false;
}
if ($this->collect_references) {
if (!isset($this->classlike_references[$fq_class_name_ci])) {
$this->classlike_references[$fq_class_name_ci] = 0;
}
2017-05-27 02:05:57 +02:00
++$this->classlike_references[$fq_class_name_ci];
2017-01-30 04:30:27 +01:00
}
return true;
2017-01-09 05:58:06 +01:00
}
/**
* @param string $fq_class_name
2017-05-27 02:16:18 +02:00
*
2017-01-09 05:58:06 +01:00
* @return bool
*/
public function hasFullyQualifiedTraitName($fq_class_name)
{
$fq_class_name_ci = strtolower($fq_class_name);
2017-01-30 04:30:27 +01:00
if (!isset($this->existing_traits_ci[$fq_class_name_ci]) ||
!$this->existing_traits_ci[$fq_class_name_ci]
) {
return false;
}
if ($this->collect_references) {
if (!isset($this->classlike_references[$fq_class_name_ci])) {
$this->classlike_references[$fq_class_name_ci] = 0;
}
2017-05-27 02:05:57 +02:00
++$this->classlike_references[$fq_class_name_ci];
2017-01-30 04:30:27 +01:00
}
return true;
2017-01-09 05:58:06 +01:00
}
/**
* @param string $file_path
2017-05-27 02:16:18 +02:00
*
* @return bool
*/
public function canReportIssues($file_path)
{
return isset($this->files_to_analyze[$file_path]);
}
2016-06-10 00:08:25 +02:00
}