Skip to content

Commit

Permalink
Merge pull request #9931 from tscni/fix/delete-file-invalidate-refere…
Browse files Browse the repository at this point in the history
…ncing-cached-methods

Invalidate cached methods when referenced files are deleted
  • Loading branch information
orklah committed Jun 21, 2023
2 parents 653ad66 + 1161edf commit fd4a97b
Show file tree
Hide file tree
Showing 4 changed files with 175 additions and 0 deletions.
10 changes: 10 additions & 0 deletions src/Psalm/Internal/Codebase/Analyzer.php
Original file line number Diff line number Diff line change
Expand Up @@ -687,6 +687,16 @@ public function loadCachedResults(ProjectAnalyzer $project_analyzer): void
}
}

// This could be optimized by storing method references to files
foreach ($file_reference_provider->getDeletedReferencedFiles() as $deleted_file) {
foreach ($file_reference_provider->getFilesReferencingFile($deleted_file) as $file_referencing_deleted) {
$methods_referencing_deleted = $this->analyzed_methods[$file_referencing_deleted] ?? [];
foreach ($methods_referencing_deleted as $method_referencing_deleted => $_) {
$newly_invalidated_methods[$method_referencing_deleted] = true;
}
}
}

foreach ($newly_invalidated_methods as $method_id => $_) {
foreach ($method_references_to_class_members as $i => $_) {
unset($method_references_to_class_members[$i][$method_id]);
Expand Down
6 changes: 6 additions & 0 deletions src/Psalm/Internal/Provider/FakeFileProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,12 @@ public function registerFile(string $file_path, string $file_contents): void
$this->fake_file_times[$file_path] = (int)microtime(true);
}

public function deleteFile(string $file_path): void
{
unset($this->fake_files[$file_path]);
unset($this->fake_file_times[$file_path]);
}

/**
* @param array<string> $file_extensions
* @param null|callable(string):bool $filter
Expand Down
159 changes: 159 additions & 0 deletions tests/Cache/CacheTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
<?php

declare(strict_types=1);

namespace Psalm\Tests\Cache;

use Psalm\Config;
use Psalm\Internal\Analyzer\IssueData;
use Psalm\Internal\Analyzer\ProjectAnalyzer;
use Psalm\Internal\IncludeCollector;
use Psalm\Internal\Provider\FakeFileProvider;
use Psalm\Internal\Provider\Providers;
use Psalm\Internal\RuntimeCaches;
use Psalm\IssueBuffer;
use Psalm\Tests\Internal\Provider\ClassLikeStorageInstanceCacheProvider;
use Psalm\Tests\Internal\Provider\FakeFileReferenceCacheProvider;
use Psalm\Tests\Internal\Provider\FileStorageInstanceCacheProvider;
use Psalm\Tests\Internal\Provider\ParserInstanceCacheProvider;
use Psalm\Tests\Internal\Provider\ProjectCacheProvider;
use Psalm\Tests\TestCase;

use function str_replace;

use const DIRECTORY_SEPARATOR;

class CacheTest extends TestCase
{
public function setUp(): void
{
parent::setUp();

RuntimeCaches::clearAll();
}

public function tearDown(): void
{
RuntimeCaches::clearAll();

parent::tearDown();
}

/**
* @param array<string, list<IssueData>> $issue_data
* @return array<string, list<string>>
*/
private static function normalizeIssueData(array $issue_data): array
{
$return = [];
foreach ($issue_data as $issue_data_per_file) {
foreach ($issue_data_per_file as $one_issue_data) {
$file_name = str_replace(DIRECTORY_SEPARATOR, '/', $one_issue_data->file_name);
$return[$file_name][] = $one_issue_data->type . ': ' . $one_issue_data->message;
}
}

return $return;
}

/**
* @param list<array{
* files: array<string, string|null>,
* issues?: array<string, list<string>>,
* }> $interactions
* @dataProvider provideCacheInteractions
*/
public function testCacheInteractions(
array $interactions
): void {
$config = Config::loadFromXML(
__DIR__ . DIRECTORY_SEPARATOR . 'test_base_dir',
<<<'XML'
<?xml version="1.0"?>
<psalm>
<projectFiles>
<directory name="src" />
</projectFiles>
</psalm>
XML,
);
$config->setIncludeCollector(new IncludeCollector());

$file_provider = new FakeFileProvider();
$providers = new Providers(
$file_provider,
new ParserInstanceCacheProvider(),
new FileStorageInstanceCacheProvider(),
new ClassLikeStorageInstanceCacheProvider(),
new FakeFileReferenceCacheProvider(),
new ProjectCacheProvider(),
);

foreach ($interactions as $interaction) {
foreach ($interaction['files'] as $file_path => $file_contents) {
$file_path = $config->base_dir . str_replace('/', DIRECTORY_SEPARATOR, $file_path);
if ($file_contents === null) {
$file_provider->deleteFile($file_path);
} else {
$file_provider->registerFile($file_path, $file_contents);
}
}

RuntimeCaches::clearAll();

$project_analyzer = new ProjectAnalyzer($config, $providers);
$project_analyzer->check($config->base_dir, true);

$issues = self::normalizeIssueData(IssueBuffer::getIssuesData());
self::assertSame($interaction['issues'] ?? [], $issues);
}
}

/**
* @return iterable<string, list{
* list<array{
* files: array<string, string|null>,
* issues?: array<string, list<string>>,
* }>,
* }>
*/
public static function provideCacheInteractions(): iterable
{
yield 'deletedFileInvalidatesReferencingMethod' => [
[
[
'files' => [
'/src/A.php' => <<<'PHP'
<?php
class A {
public function do(B $b): void
{
$b->do();
}
}
PHP,
'/src/B.php' => <<<'PHP'
<?php
class B {
public function do(): void
{
echo 'B';
}
}
PHP,
],
],
[
'files' => [
'/src/B.php' => null,
],
'issues' => [
'/src/A.php' => [
'UndefinedClass: Class, interface or enum named B does not exist',
],
],
],
],
];
}
}
Empty file.

0 comments on commit fd4a97b

Please sign in to comment.