2017-02-18 19:41:27 +01:00
|
|
|
<?php
|
|
|
|
namespace Psalm\Provider;
|
|
|
|
|
|
|
|
use PhpParser;
|
|
|
|
use Psalm\Config;
|
|
|
|
|
2017-10-15 17:57:44 +02:00
|
|
|
class ParserCacheProvider
|
2017-02-18 19:41:27 +01:00
|
|
|
{
|
2017-03-16 15:58:13 +01:00
|
|
|
const FILE_HASHES = 'file_hashes_json';
|
2017-02-18 19:41:27 +01:00
|
|
|
const PARSER_CACHE_DIRECTORY = 'php-parser';
|
|
|
|
const GOOD_RUN_NAME = 'good_run';
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @var int|null
|
|
|
|
*/
|
2017-07-25 22:11:02 +02:00
|
|
|
protected $last_good_run = null;
|
2017-02-18 19:41:27 +01:00
|
|
|
|
|
|
|
/**
|
|
|
|
* A map of filename hashes to contents hashes
|
|
|
|
*
|
|
|
|
* @var array<string, string>|null
|
|
|
|
*/
|
2017-07-25 22:11:02 +02:00
|
|
|
protected $file_content_hashes = null;
|
2017-02-18 19:41:27 +01:00
|
|
|
|
2017-10-15 18:38:47 +02:00
|
|
|
/** @var bool */
|
|
|
|
public $use_igbinary = false;
|
|
|
|
|
2017-02-18 19:41:27 +01:00
|
|
|
/**
|
|
|
|
* @param string $file_content_hash
|
|
|
|
* @param string $file_cache_key
|
2017-07-25 22:11:02 +02:00
|
|
|
* @param mixed $file_modified_time
|
2017-05-27 02:16:18 +02:00
|
|
|
*
|
2017-02-18 19:41:27 +01:00
|
|
|
* @return array<int, PhpParser\Node\Stmt>|null
|
2017-10-15 18:38:47 +02:00
|
|
|
*
|
|
|
|
* @psalm-suppress UndefinedFunction
|
2017-02-18 19:41:27 +01:00
|
|
|
*/
|
2017-12-29 23:27:16 +01:00
|
|
|
public function loadStatementsFromCache($file_modified_time, $file_content_hash, $file_cache_key)
|
2017-02-18 19:41:27 +01:00
|
|
|
{
|
|
|
|
$root_cache_directory = Config::getInstance()->getCacheDirectory();
|
|
|
|
|
|
|
|
if (!$root_cache_directory) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
$parser_cache_directory = $root_cache_directory . DIRECTORY_SEPARATOR . self::PARSER_CACHE_DIRECTORY;
|
|
|
|
|
|
|
|
$cache_location = null;
|
|
|
|
|
2017-07-25 22:11:02 +02:00
|
|
|
$file_content_hashes = $this->getFileContentHashes();
|
2017-02-18 19:41:27 +01:00
|
|
|
|
|
|
|
$cache_location = $parser_cache_directory . DIRECTORY_SEPARATOR . $file_cache_key;
|
|
|
|
|
2017-02-27 05:09:18 +01:00
|
|
|
if (isset($file_content_hashes[$file_cache_key]) &&
|
|
|
|
$file_content_hash === $file_content_hashes[$file_cache_key] &&
|
2017-02-18 19:41:27 +01:00
|
|
|
is_readable($cache_location) &&
|
2017-07-25 22:11:02 +02:00
|
|
|
filemtime($cache_location) > $file_modified_time
|
2017-02-18 19:41:27 +01:00
|
|
|
) {
|
2017-10-15 18:38:47 +02:00
|
|
|
if ($this->use_igbinary) {
|
|
|
|
/** @var array<int, \PhpParser\Node\Stmt> */
|
|
|
|
return igbinary_unserialize((string)file_get_contents($cache_location)) ?: null;
|
|
|
|
}
|
|
|
|
|
2017-02-18 19:41:27 +01:00
|
|
|
/** @var array<int, \PhpParser\Node\Stmt> */
|
2017-10-13 02:27:33 +02:00
|
|
|
return unserialize((string)file_get_contents($cache_location)) ?: null;
|
2017-02-18 19:41:27 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @return array<string, string>
|
|
|
|
*/
|
2017-07-25 22:11:02 +02:00
|
|
|
private function getFileContentHashes()
|
2017-02-18 19:41:27 +01:00
|
|
|
{
|
|
|
|
$config = Config::getInstance();
|
|
|
|
$root_cache_directory = $config->getCacheDirectory();
|
|
|
|
|
2017-07-25 22:11:02 +02:00
|
|
|
if ($this->file_content_hashes === null || !$config->cache_file_hashes_during_run) {
|
2017-02-18 19:41:27 +01:00
|
|
|
$file_hashes_path = $root_cache_directory . DIRECTORY_SEPARATOR . self::FILE_HASHES;
|
|
|
|
/** @var array<string, string> */
|
2017-07-25 22:11:02 +02:00
|
|
|
$this->file_content_hashes =
|
2017-02-18 19:41:27 +01:00
|
|
|
$root_cache_directory && is_readable($file_hashes_path)
|
2017-03-16 15:58:13 +01:00
|
|
|
? json_decode((string)file_get_contents($file_hashes_path), true)
|
2017-02-18 19:41:27 +01:00
|
|
|
: [];
|
|
|
|
}
|
|
|
|
|
2017-07-25 22:11:02 +02:00
|
|
|
return $this->file_content_hashes;
|
2017-02-18 19:41:27 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param string $file_cache_key
|
|
|
|
* @param string $file_content_hash
|
|
|
|
* @param array<int, PhpParser\Node\Stmt> $stmts
|
|
|
|
* @param bool $touch_only
|
2017-05-27 02:16:18 +02:00
|
|
|
*
|
2017-02-18 19:41:27 +01:00
|
|
|
* @return void
|
2017-10-15 18:38:47 +02:00
|
|
|
*
|
|
|
|
* @psalm-suppress UndefinedFunction
|
2017-02-18 19:41:27 +01:00
|
|
|
*/
|
2017-07-25 22:11:02 +02:00
|
|
|
public function saveStatementsToCache($file_cache_key, $file_content_hash, array $stmts, $touch_only)
|
2017-02-18 19:41:27 +01:00
|
|
|
{
|
|
|
|
$root_cache_directory = Config::getInstance()->getCacheDirectory();
|
|
|
|
|
|
|
|
if (!$root_cache_directory) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
$parser_cache_directory = $root_cache_directory . DIRECTORY_SEPARATOR . self::PARSER_CACHE_DIRECTORY;
|
|
|
|
|
|
|
|
$cache_location = $parser_cache_directory . DIRECTORY_SEPARATOR . $file_cache_key;
|
|
|
|
|
|
|
|
if ($touch_only) {
|
|
|
|
touch($cache_location);
|
|
|
|
} else {
|
|
|
|
if (!is_dir($parser_cache_directory)) {
|
|
|
|
mkdir($parser_cache_directory, 0777, true);
|
|
|
|
}
|
|
|
|
|
2017-10-15 18:38:47 +02:00
|
|
|
if ($this->use_igbinary) {
|
|
|
|
file_put_contents($cache_location, igbinary_serialize($stmts));
|
|
|
|
} else {
|
|
|
|
file_put_contents($cache_location, serialize($stmts));
|
|
|
|
}
|
2017-02-18 19:41:27 +01:00
|
|
|
|
2017-07-25 22:11:02 +02:00
|
|
|
$this->file_content_hashes[$file_cache_key] = $file_content_hash;
|
2017-02-18 19:41:27 +01:00
|
|
|
|
|
|
|
file_put_contents(
|
|
|
|
$root_cache_directory . DIRECTORY_SEPARATOR . self::FILE_HASHES,
|
2017-07-25 22:11:02 +02:00
|
|
|
json_encode($this->file_content_hashes)
|
2017-02-18 19:41:27 +01:00
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @return bool
|
|
|
|
*/
|
2017-07-25 22:11:02 +02:00
|
|
|
public function canDiffFiles()
|
2017-02-18 19:41:27 +01:00
|
|
|
{
|
|
|
|
$cache_directory = Config::getInstance()->getCacheDirectory();
|
|
|
|
|
|
|
|
return $cache_directory && file_exists($cache_directory . DIRECTORY_SEPARATOR . self::GOOD_RUN_NAME);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param int $start_time
|
2017-05-27 02:16:18 +02:00
|
|
|
*
|
2017-02-18 19:41:27 +01:00
|
|
|
* @return void
|
|
|
|
*/
|
2017-07-25 22:11:02 +02:00
|
|
|
public function processSuccessfulRun($start_time)
|
2017-02-18 19:41:27 +01:00
|
|
|
{
|
|
|
|
$cache_directory = Config::getInstance()->getCacheDirectory();
|
|
|
|
|
|
|
|
if (!$cache_directory) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
$run_cache_location = $cache_directory . DIRECTORY_SEPARATOR . self::GOOD_RUN_NAME;
|
|
|
|
|
|
|
|
touch($run_cache_location, $start_time);
|
|
|
|
|
|
|
|
FileReferenceProvider::removeDeletedFilesFromReferences();
|
|
|
|
|
|
|
|
$cache_directory .= DIRECTORY_SEPARATOR . self::PARSER_CACHE_DIRECTORY;
|
|
|
|
|
|
|
|
if (is_dir($cache_directory)) {
|
|
|
|
/** @var array<string> */
|
|
|
|
$directory_files = scandir($cache_directory);
|
|
|
|
|
|
|
|
foreach ($directory_files as $directory_file) {
|
|
|
|
$full_path = $cache_directory . DIRECTORY_SEPARATOR . $directory_file;
|
|
|
|
|
|
|
|
if ($directory_file[0] === '.') {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
touch($full_path);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @return int
|
|
|
|
*/
|
2017-07-25 22:11:02 +02:00
|
|
|
public function getLastGoodRun()
|
2017-02-18 19:41:27 +01:00
|
|
|
{
|
2017-07-25 22:11:02 +02:00
|
|
|
if ($this->last_good_run === null) {
|
2017-02-18 19:41:27 +01:00
|
|
|
$cache_directory = Config::getInstance()->getCacheDirectory();
|
|
|
|
|
2017-07-25 22:11:02 +02:00
|
|
|
$this->last_good_run = filemtime($cache_directory . DIRECTORY_SEPARATOR . self::GOOD_RUN_NAME) ?: 0;
|
2017-02-18 19:41:27 +01:00
|
|
|
}
|
|
|
|
|
2017-07-25 22:11:02 +02:00
|
|
|
return $this->last_good_run;
|
2017-02-18 19:41:27 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param float $time_before
|
2017-05-27 02:16:18 +02:00
|
|
|
*
|
2017-02-18 19:41:27 +01:00
|
|
|
* @return int
|
|
|
|
*/
|
2017-07-25 22:11:02 +02:00
|
|
|
public function deleteOldParserCaches($time_before)
|
2017-02-18 19:41:27 +01:00
|
|
|
{
|
|
|
|
$cache_directory = Config::getInstance()->getCacheDirectory();
|
|
|
|
|
|
|
|
if ($cache_directory) {
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
$removed_count = 0;
|
|
|
|
|
|
|
|
$cache_directory .= DIRECTORY_SEPARATOR . self::PARSER_CACHE_DIRECTORY;
|
|
|
|
|
|
|
|
if (is_dir($cache_directory)) {
|
|
|
|
/** @var array<string> */
|
|
|
|
$directory_files = scandir($cache_directory);
|
|
|
|
|
|
|
|
foreach ($directory_files as $directory_file) {
|
|
|
|
$full_path = $cache_directory . DIRECTORY_SEPARATOR . $directory_file;
|
|
|
|
|
|
|
|
if ($directory_file[0] === '.') {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (filemtime($full_path) < $time_before && is_writable($full_path)) {
|
|
|
|
unlink($full_path);
|
2017-05-27 02:05:57 +02:00
|
|
|
++$removed_count;
|
2017-02-18 19:41:27 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return $removed_count;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param array<string> $file_names
|
|
|
|
* @param int $min_time
|
2017-05-27 02:16:18 +02:00
|
|
|
*
|
2017-02-18 19:41:27 +01:00
|
|
|
* @return void
|
|
|
|
*/
|
2017-07-25 22:11:02 +02:00
|
|
|
public function touchParserCaches(array $file_names, $min_time)
|
2017-02-18 19:41:27 +01:00
|
|
|
{
|
|
|
|
$cache_directory = Config::getInstance()->getCacheDirectory();
|
|
|
|
|
|
|
|
if (!$cache_directory) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
$cache_directory .= DIRECTORY_SEPARATOR . self::PARSER_CACHE_DIRECTORY;
|
|
|
|
|
|
|
|
if (is_dir($cache_directory)) {
|
|
|
|
foreach ($file_names as $file_name) {
|
2017-07-25 22:11:02 +02:00
|
|
|
$hash_file_name = $cache_directory . DIRECTORY_SEPARATOR . $this->getParserCacheKey($file_name);
|
2017-02-18 19:41:27 +01:00
|
|
|
|
|
|
|
if (file_exists($hash_file_name)) {
|
|
|
|
if (filemtime($hash_file_name) < $min_time) {
|
|
|
|
touch($hash_file_name, $min_time);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param string $file_name
|
2017-05-27 02:16:18 +02:00
|
|
|
*
|
2017-02-18 19:41:27 +01:00
|
|
|
* @return string
|
|
|
|
*/
|
|
|
|
public static function getParserCacheKey($file_name)
|
|
|
|
{
|
|
|
|
return md5($file_name);
|
|
|
|
}
|
|
|
|
}
|