1
0
mirror of https://github.com/danog/psalm.git synced 2025-01-21 21:31:13 +01:00

Improve test coverage of ProjectChecker

This commit is contained in:
Brown 2018-10-17 11:03:32 -04:00
parent 5a484901cd
commit 1d5759a35c
10 changed files with 443 additions and 57 deletions

View File

@ -19,6 +19,7 @@
<file name="src/Psalm/CallMap.php" />
<directory name="src/Psalm/Stubs" />
<directory name="tests/stubs" />
<directory name="tests/DummyProject" />
<file name="vendor/phpunit/phpunit/src/Framework/TestCase.php" />
<file name="src/Psalm/Diff/Differ.php" />
</ignoreFiles>

View File

@ -14,8 +14,6 @@ use Psalm\Provider\ParserCacheProvider;
use Psalm\Provider\Providers;
use Psalm\Provider\StatementsProvider;
use Psalm\Type;
use RecursiveDirectoryIterator;
use RecursiveIteratorIterator;
use Sabre\Event\Loop;
class ProjectChecker
@ -255,7 +253,7 @@ class ProjectChecker
$diff_files = null;
$deleted_files = null;
$reference_cache = $this->file_reference_provider->loadReferenceCache();
$reference_cache = $this->file_reference_provider->loadReferenceCache(true);
if ($is_diff
&& $reference_cache
@ -419,25 +417,14 @@ class ProjectChecker
{
$file_extensions = $config->getFileExtensions();
/** @var RecursiveDirectoryIterator */
$iterator = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($dir_name));
$iterator->rewind();
$file_paths = $this->file_provider->getFilesInDir($dir_name, $file_extensions);
$files_to_scan = [];
while ($iterator->valid()) {
if (!$iterator->isDot()) {
$extension = $iterator->getExtension();
if (in_array($extension, $file_extensions, true)) {
$file_path = (string)$iterator->getRealPath();
if ($allow_non_project_files || $config->isInProjectDirs($file_path)) {
$files_to_scan[$file_path] = $file_path;
}
}
foreach ($file_paths as $file_path) {
if ($allow_non_project_files || $config->isInProjectDirs($file_path)) {
$files_to_scan[$file_path] = $file_path;
}
$iterator->next();
}
$this->codebase->addFilesToAnalyze($files_to_scan);
@ -451,26 +438,16 @@ class ProjectChecker
private function getAllFiles(Config $config)
{
$file_extensions = $config->getFileExtensions();
$file_names = [];
$file_paths = [];
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, true)) {
$file_names[] = (string)$iterator->getRealPath();
}
}
$iterator->next();
}
$file_paths = array_merge(
$file_paths,
$this->file_provider->getFilesInDir($dir_name, $file_extensions)
);
}
return $file_names;
return $file_paths;
}
/**
@ -487,29 +464,18 @@ class ProjectChecker
throw new \UnexpectedValueException('Parser cache provider cannot be null here');
}
/** @var RecursiveDirectoryIterator */
$iterator = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($dir_name));
$iterator->rewind();
$diff_files = [];
$last_good_run = $this->parser_cache_provider->getLastGoodRun();
while ($iterator->valid()) {
if (!$iterator->isDot()) {
$extension = $iterator->getExtension();
if (in_array($extension, $file_extensions, true)) {
$file_path = (string)$iterator->getRealPath();
$file_paths = $this->file_provider->getFilesInDir($dir_name, $file_extensions);
if ($config->isInProjectDirs($file_path)) {
if ($this->file_provider->getModifiedTime($file_path) > $last_good_run) {
$diff_files[] = $file_path;
}
}
foreach ($file_paths as $file_path) {
if ($config->isInProjectDirs($file_path)) {
if ($this->file_provider->getModifiedTime($file_path) > $last_good_run) {
$diff_files[] = $file_path;
}
}
$iterator->next();
}
return $diff_files;
@ -526,7 +492,7 @@ class ProjectChecker
$files_to_scan = [];
foreach ($file_list as $file_path) {
if (!file_exists($file_path)) {
if (!$this->file_provider->fileExists($file_path)) {
continue;
}

View File

@ -43,4 +43,32 @@ class FileProvider
{
return file_exists($file_path);
}
/**
* @param string $dir_path
* @param array<string> $file_extensions
*
* @return array<int, string>
*/
public function getFilesInDir($dir_path, array $file_extensions)
{
$file_paths = [];
/** @var \RecursiveDirectoryIterator */
$iterator = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($dir_path));
$iterator->rewind();
while ($iterator->valid()) {
if (!$iterator->isDot()) {
$extension = $iterator->getExtension();
if (in_array($extension, $file_extensions, true)) {
$file_paths[] = (string)$iterator->getRealPath();
}
}
$iterator->next();
}
return $file_paths;
}
}

View File

@ -252,13 +252,14 @@ class FileReferenceProvider
}
/**
* @param bool $force_reload
* @return bool
* @psalm-suppress MixedAssignment
* @psalm-suppress MixedTypeCoercion
*/
public function loadReferenceCache()
public function loadReferenceCache($force_reload = true)
{
if ($this->cache && !$this->loaded_from_cache) {
if ($this->cache && (!$this->loaded_from_cache || $force_reload)) {
$this->loaded_from_cache = true;
$file_references = $this->cache->getCachedFileReferences();

View File

@ -14,21 +14,21 @@ class ParserCacheProvider
/**
* @var int|null
*/
protected $last_good_run = null;
private $last_good_run = null;
/**
* A map of filename hashes to contents hashes
*
* @var array<string, string>|null
*/
protected $existing_file_content_hashes = null;
private $existing_file_content_hashes = null;
/**
* A map of recently-added filename hashes to contents hashes
*
* @var array<string, string>
*/
protected $new_file_content_hashes = [];
private $new_file_content_hashes = [];
/** @var bool */
private $use_igbinary;
@ -343,7 +343,11 @@ class ParserCacheProvider
if ($this->last_good_run === null) {
$cache_directory = Config::getInstance()->getCacheDirectory();
$this->last_good_run = filemtime($cache_directory . DIRECTORY_SEPARATOR . self::GOOD_RUN_NAME) ?: 0;
if (file_exists($cache_directory . DIRECTORY_SEPARATOR . self::GOOD_RUN_NAME)) {
$this->last_good_run = filemtime($cache_directory . DIRECTORY_SEPARATOR . self::GOOD_RUN_NAME);
} else {
$this->last_good_run = 0;
}
}
return $this->last_good_run;

View File

@ -0,0 +1,14 @@
<?php
namespace Foo;
class Bar
{
/** @var string */
public $x;
public function __construct()
{
$this->x = "hello";
}
}

View File

@ -0,0 +1,11 @@
<?php
namespace Foo;
class Bat
{
public function __construct()
{
$a = new Bar();
}
}

View File

@ -0,0 +1,311 @@
<?php
namespace Psalm\Tests;
use Psalm\Checker\FileChecker;
use Psalm\Config;
use Psalm\Context;
class ProjectCheckerTest extends TestCase
{
/** @var TestConfig */
protected static $config;
/** @var \Psalm\Checker\ProjectChecker */
protected $project_checker;
/**
* @return void
*/
public static function setUpBeforeClass()
{
self::$config = new TestConfig();
if (!defined('PSALM_VERSION')) {
define('PSALM_VERSION', '2.0.0');
}
if (!defined('PHP_PARSER_VERSION')) {
define('PHP_PARSER_VERSION', '4.0.0');
}
}
/**
* @return void
*/
public function setUp()
{
FileChecker::clearCache();
$this->file_provider = new Provider\FakeFileProvider();
}
/**
* @return string[]
* @psalm-return array<mixed, string>
*/
public static function getAllIssues()
{
return array_filter(
array_map(
/**
* @param string $file_name
*
* @return string
*/
function ($file_name) {
return substr($file_name, 0, -4);
},
scandir(dirname(__DIR__) . '/src/Psalm/Issue')
),
/**
* @param string $issue_name
*
* @return bool
*/
function ($issue_name) {
return !empty($issue_name)
&& $issue_name !== 'MethodIssue'
&& $issue_name !== 'PropertyIssue'
&& $issue_name !== 'ClassIssue'
&& $issue_name !== 'CodeIssue';
}
);
}
/**
* @param Config $config
*
* @return \Psalm\Checker\ProjectChecker
*/
private function getProjectCheckerWithConfig(Config $config)
{
return new \Psalm\Checker\ProjectChecker(
$config,
new \Psalm\Provider\Providers(
$this->file_provider,
new Provider\ParserInstanceCacheProvider(),
new Provider\FileStorageInstanceCacheProvider(),
new Provider\ClassLikeStorageInstanceCacheProvider(),
new Provider\FakeFileReferenceCacheProvider()
)
);
}
/**
* @return void
*/
public function testCheck()
{
$this->project_checker = $this->getProjectCheckerWithConfig(
Config::loadFromXML(
(string)getcwd(),
'<?xml version="1.0"?>
<psalm>
<projectFiles>
<directory name="tests/DummyProject" />
</projectFiles>
</psalm>'
)
);
ob_start();
$this->project_checker->check('tests/DummyProject');
$output = ob_get_clean();
$this->assertSame('Scanning files...' . "\n" . 'Analyzing files...' . "\n", $output);
$this->assertSame(0, \Psalm\IssueBuffer::getErrorCount());
$this->assertSame(
'Psalm was able to infer types for 100.000% of analyzed code (2 files)',
$this->project_checker->codebase->analyzer->getTypeInferenceSummary()
);
}
/**
* @return void
*/
public function testCheckAfterNoChange()
{
$this->project_checker = $this->getProjectCheckerWithConfig(
Config::loadFromXML(
(string)getcwd(),
'<?xml version="1.0"?>
<psalm>
<projectFiles>
<directory name="tests/DummyProject" />
</projectFiles>
</psalm>'
)
);
$this->project_checker->output_format = \Psalm\Checker\ProjectChecker::TYPE_JSON;
$this->project_checker->check('tests/DummyProject', true);
\Psalm\IssueBuffer::finish($this->project_checker, true, microtime(true));
$this->assertSame(
'Psalm was able to infer types for 100.000% of analyzed code (2 files)',
$this->project_checker->codebase->analyzer->getTypeInferenceSummary()
);
$this->project_checker->codebase->reloadFiles($this->project_checker, []);
$this->project_checker->check('tests/DummyProject', true);
$this->assertSame(0, \Psalm\IssueBuffer::getErrorCount());
$this->assertSame(
'No files analyzed',
$this->project_checker->codebase->analyzer->getTypeInferenceSummary()
);
}
/**
* @return void
*/
public function testCheckAfterFileChange()
{
$this->project_checker = $this->getProjectCheckerWithConfig(
Config::loadFromXML(
(string)getcwd(),
'<?xml version="1.0"?>
<psalm>
<projectFiles>
<directory name="tests/DummyProject" />
</projectFiles>
</psalm>'
)
);
$this->project_checker->output_format = \Psalm\Checker\ProjectChecker::TYPE_JSON;
$this->project_checker->check('tests/DummyProject', true);
\Psalm\IssueBuffer::finish($this->project_checker, true, microtime(true));
$this->assertSame(
'Psalm was able to infer types for 100.000% of analyzed code (2 files)',
$this->project_checker->codebase->analyzer->getTypeInferenceSummary()
);
$bat_file_path = getcwd() . DIRECTORY_SEPARATOR . 'tests' . DIRECTORY_SEPARATOR . 'DummyProject' . DIRECTORY_SEPARATOR . 'Bat.php';
$bat_replacement_contents = '<?php
namespace Foo;
class Bat
{
public function __construct()
{
$a = new Bar();
}
}
';
$this->file_provider->registerFile($bat_file_path, $bat_replacement_contents);
$this->project_checker->codebase->reloadFiles($this->project_checker, []);
$this->project_checker->check('tests/DummyProject', true);
$this->assertSame(0, \Psalm\IssueBuffer::getErrorCount());
$this->assertSame(
'Psalm was able to infer types for 100.000% of analyzed code (1 file)',
$this->project_checker->codebase->analyzer->getTypeInferenceSummary()
);
}
/**
* @return void
*/
public function testCheckDir()
{
$this->project_checker = $this->getProjectCheckerWithConfig(
Config::loadFromXML(
(string)getcwd(),
'<?xml version="1.0"?>
<psalm>
<projectFiles>
<directory name="tests/DummyProject" />
</projectFiles>
</psalm>'
)
);
ob_start();
$this->project_checker->checkDir('tests/DummyProject');
$output = ob_get_clean();
$this->assertSame('Scanning files...' . "\n" . 'Analyzing files...' . "\n", $output);
$this->assertSame(0, \Psalm\IssueBuffer::getErrorCount());
$this->assertSame(
'Psalm was able to infer types for 100.000% of analyzed code (2 files)',
$this->project_checker->codebase->analyzer->getTypeInferenceSummary()
);
}
/**
* @return void
*/
public function testCheckPaths()
{
$this->project_checker = $this->getProjectCheckerWithConfig(
Config::loadFromXML(
(string)getcwd(),
'<?xml version="1.0"?>
<psalm>
<projectFiles>
<directory name="tests/DummyProject" />
</projectFiles>
</psalm>'
)
);
ob_start();
$this->project_checker->checkPaths(['tests/DummyProject/Bar.php']);
$output = ob_get_clean();
$this->assertSame('Scanning files...' . "\n" . 'Analyzing files...' . "\n", $output);
$this->assertSame(0, \Psalm\IssueBuffer::getErrorCount());
$this->assertSame(
'Psalm was able to infer types for 100.000% of analyzed code (1 file)',
$this->project_checker->codebase->analyzer->getTypeInferenceSummary()
);
}
/**
* @return void
*/
public function testCheckFile()
{
$this->project_checker = $this->getProjectCheckerWithConfig(
Config::loadFromXML(
(string)getcwd(),
'<?xml version="1.0"?>
<psalm>
<projectFiles>
<directory name="tests/DummyProject" />
</projectFiles>
</psalm>'
)
);
ob_start();
$this->project_checker->checkFile('tests/DummyProject/Bar.php');
$output = ob_get_clean();
$this->assertSame('Scanning files...' . "\n" . 'Analyzing files...' . "\n", $output);
$this->assertSame(0, \Psalm\IssueBuffer::getErrorCount());
$this->assertSame(
'Psalm was able to infer types for 100.000% of analyzed code (1 file)',
$this->project_checker->codebase->analyzer->getTypeInferenceSummary()
);
}
}

View File

@ -74,4 +74,23 @@ class FakeFileProvider extends \Psalm\Provider\FileProvider
$this->fake_files[$file_path] = $file_contents;
$this->fake_file_times[$file_path] = microtime(true);
}
/**
* @param string $dir_path
* @param array<string> $file_extensions
*
* @return array<int, string>
*/
public function getFilesInDir($dir_path, array $file_extensions)
{
$file_paths = parent::getFilesInDir($dir_path, $file_extensions);
foreach ($this->fake_files as $file_path => $_) {
if (strpos(strtolower($file_path), strtolower($dir_path)) === 0) {
$file_paths[] = $file_path;
}
}
return $file_paths;
}
}

View File

@ -20,6 +20,11 @@ class ParserInstanceCacheProvider extends \Psalm\Provider\ParserCacheProvider
*/
private $statements_cache_time = [];
/**
* @var int
*/
private $last_good_run = 0;
public function __construct()
{
}
@ -89,4 +94,30 @@ class ParserInstanceCacheProvider extends \Psalm\Provider\ParserCacheProvider
{
$this->file_contents_cache[$file_path] = $file_contents;
}
/**
* @return int
*/
public function getLastGoodRun()
{
return $this->last_good_run;
}
/**
* @param float $start_time
*
* @return void
*/
public function processSuccessfulRun($start_time)
{
$this->last_good_run = (int) $start_time;
}
/**
* @return bool
*/
public function canDiffFiles()
{
return $this->last_good_run > 0;
}
}