diff --git a/psalm.xml.dist b/psalm.xml.dist
index e2951428d..623a19e9c 100644
--- a/psalm.xml.dist
+++ b/psalm.xml.dist
@@ -95,6 +95,12 @@
+
+
+
+
+
+
diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/AtomicCallContext.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/AtomicCallContext.php
index de4ae82b5..7aaccfe1d 100644
--- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/AtomicCallContext.php
+++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/AtomicCallContext.php
@@ -2,7 +2,6 @@
namespace Psalm\Internal\Analyzer\Statements\Expression\Call\Method;
use Psalm\Internal\MethodIdentifier;
-use Psalm\Internal\Provider\NodeDataProvider;
use PhpParser;
class AtomicCallContext
diff --git a/src/Psalm/Internal/Codebase/Analyzer.php b/src/Psalm/Internal/Codebase/Analyzer.php
index d8438f590..37572b3db 100644
--- a/src/Psalm/Internal/Codebase/Analyzer.php
+++ b/src/Psalm/Internal/Codebase/Analyzer.php
@@ -46,12 +46,14 @@ use function implode;
* nonmethod_references_to_classes: array>,
* method_references_to_classes: array>,
* file_references_to_class_members: array>,
+ * file_references_to_class_properties: array>,
* file_references_to_missing_class_members: array>,
* mixed_counts: array,
* mixed_member_names: array>,
* function_timings: array,
* file_manipulations: array,
* method_references_to_class_members: array>,
+ * method_references_to_class_properties: array>,
* method_references_to_missing_class_members: array>,
* method_param_uses: array>>,
* analyzed_methods: array>,
@@ -417,7 +419,9 @@ class Analyzer
$file_reference_provider->setNonMethodReferencesToClasses([]);
$file_reference_provider->setCallingMethodReferencesToClassMembers([]);
+ $file_reference_provider->setCallingMethodReferencesToClassProperties([]);
$file_reference_provider->setFileReferencesToClassMembers([]);
+ $file_reference_provider->setFileReferencesToClassProperties([]);
$file_reference_provider->setCallingMethodReferencesToMissingClassMembers([]);
$file_reference_provider->setFileReferencesToMissingClassMembers([]);
$file_reference_provider->setReferencesToMixedMemberNames([]);
@@ -441,6 +445,8 @@ class Analyzer
'method_references_to_classes' => $file_reference_provider->getAllMethodReferencesToClasses(),
'file_references_to_class_members' => $file_reference_provider->getAllFileReferencesToClassMembers(),
'method_references_to_class_members' => $file_reference_provider->getAllMethodReferencesToClassMembers(),
+ 'file_references_to_class_properties' => $file_reference_provider->getAllFileReferencesToClassProperties(),
+ 'method_references_to_class_properties' => $file_reference_provider->getAllMethodReferencesToClassProperties(),
'file_references_to_missing_class_members' => $file_reference_provider->getAllFileReferencesToMissingClassMembers(),
'method_references_to_missing_class_members' => $file_reference_provider->getAllMethodReferencesToMissingClassMembers(),
'method_param_uses' => $file_reference_provider->getAllMethodParamUses(),
@@ -497,9 +503,15 @@ class Analyzer
$codebase->file_reference_provider->addFileReferencesToClassMembers(
$pool_data['file_references_to_class_members']
);
+ $codebase->file_reference_provider->addFileReferencesToClassProperties(
+ $pool_data['file_references_to_class_properties']
+ );
$codebase->file_reference_provider->addMethodReferencesToClassMembers(
$pool_data['method_references_to_class_members']
);
+ $codebase->file_reference_provider->addMethodReferencesToClassProperties(
+ $pool_data['method_references_to_class_properties']
+ );
$codebase->file_reference_provider->addFileReferencesToMissingClassMembers(
$pool_data['file_references_to_missing_class_members']
);
@@ -612,8 +624,10 @@ class Analyzer
$diff_map = $statements_provider->getDiffMap();
$deletion_ranges = $statements_provider->getDeletionRanges();
- $method_references_to_class_members
- = $file_reference_provider->getAllMethodReferencesToClassMembers();
+ $method_references_to_class_members = $file_reference_provider->getAllMethodReferencesToClassMembers();
+
+ $method_references_to_class_properties = $file_reference_provider->getAllMethodReferencesToClassProperties();
+
$method_references_to_missing_class_members =
$file_reference_provider->getAllMethodReferencesToMissingClassMembers();
@@ -625,8 +639,10 @@ class Analyzer
$method_param_uses = $file_reference_provider->getAllMethodParamUses();
- $file_references_to_class_members
- = $file_reference_provider->getAllFileReferencesToClassMembers();
+ $file_references_to_class_members = $file_reference_provider->getAllFileReferencesToClassMembers();
+
+ $file_references_to_class_properties = $file_reference_provider->getAllFileReferencesToClassProperties();
+
$file_references_to_missing_class_members
= $file_reference_provider->getAllFileReferencesToMissingClassMembers();
@@ -707,7 +723,9 @@ class Analyzer
unset(
$method_references_to_class_members[$member_id],
+ $method_references_to_class_properties[$member_id],
$file_references_to_class_members[$member_id],
+ $file_references_to_class_properties[$member_id],
$method_references_to_missing_class_members[$member_id],
$file_references_to_missing_class_members[$member_id],
$references_to_mixed_member_names[$member_id],
@@ -730,6 +748,10 @@ class Analyzer
unset($method_references_to_class_members[$i][$method_id]);
}
+ foreach ($method_references_to_class_properties as $i => $_) {
+ unset($method_references_to_class_properties[$i][$method_id]);
+ }
+
foreach ($method_references_to_classes as $i => $_) {
unset($method_references_to_classes[$i][$method_id]);
}
@@ -783,6 +805,10 @@ class Analyzer
unset($file_references_to_class_members[$i][$file_path]);
}
+ foreach ($file_references_to_class_properties as $i => $_) {
+ unset($file_references_to_class_properties[$i][$file_path]);
+ }
+
foreach ($nonmethod_references_to_classes as $i => $_) {
unset($nonmethod_references_to_classes[$i][$file_path]);
}
@@ -810,6 +836,10 @@ class Analyzer
$method_references_to_class_members
);
+ $method_references_to_class_properties = array_filter(
+ $method_references_to_class_properties
+ );
+
$method_references_to_missing_class_members = array_filter(
$method_references_to_missing_class_members
);
@@ -818,6 +848,10 @@ class Analyzer
$file_references_to_class_members
);
+ $file_references_to_class_properties = array_filter(
+ $file_references_to_class_properties
+ );
+
$file_references_to_missing_class_members = array_filter(
$file_references_to_missing_class_members
);
@@ -842,10 +876,18 @@ class Analyzer
$method_references_to_class_members
);
+ $file_reference_provider->setCallingMethodReferencesToClassProperties(
+ $method_references_to_class_properties
+ );
+
$file_reference_provider->setFileReferencesToClassMembers(
$file_references_to_class_members
);
+ $file_reference_provider->setFileReferencesToClassProperties(
+ $file_references_to_class_properties
+ );
+
$file_reference_provider->setCallingMethodReferencesToMissingClassMembers(
$method_references_to_missing_class_members
);
diff --git a/src/Psalm/Internal/Codebase/ClassLikes.php b/src/Psalm/Internal/Codebase/ClassLikes.php
index af07c3e48..ae0c744e6 100644
--- a/src/Psalm/Internal/Codebase/ClassLikes.php
+++ b/src/Psalm/Internal/Codebase/ClassLikes.php
@@ -2044,7 +2044,7 @@ class ClassLikes
$property_constructor_referenced = false;
if ($property_referenced && $property_storage->visibility === ClassLikeAnalyzer::VISIBILITY_PRIVATE) {
- $all_method_references = $this->file_reference_provider->getAllMethodReferencesToClassMembers();
+ $all_method_references = $this->file_reference_provider->getAllMethodReferencesToClassProperties();
if (isset($all_method_references[$referenced_property_name])
&& count($all_method_references[$referenced_property_name]) === 1) {
diff --git a/src/Psalm/Internal/Codebase/Properties.php b/src/Psalm/Internal/Codebase/Properties.php
index d225f5878..a1a1efc47 100644
--- a/src/Psalm/Internal/Codebase/Properties.php
+++ b/src/Psalm/Internal/Codebase/Properties.php
@@ -133,11 +133,25 @@ class Properties
$context->calling_method_id,
strtolower($declaring_property_class) . '::$' . $property_name
);
+
+ if ($read_mode) {
+ $this->file_reference_provider->addMethodReferenceToClassProperty(
+ $context->calling_method_id,
+ strtolower($declaring_property_class) . '::$' . $property_name
+ );
+ }
} elseif ($source) {
$this->file_reference_provider->addFileReferenceToClassMember(
$source->getFilePath(),
strtolower($declaring_property_class) . '::$' . $property_name
);
+
+ if ($read_mode) {
+ $this->file_reference_provider->addFileReferenceToClassProperty(
+ $source->getFilePath(),
+ strtolower($declaring_property_class) . '::$' . $property_name
+ );
+ }
}
if ($this->collect_locations && $code_location) {
diff --git a/src/Psalm/Internal/Provider/FileReferenceCacheProvider.php b/src/Psalm/Internal/Provider/FileReferenceCacheProvider.php
index 66497d846..332469504 100644
--- a/src/Psalm/Internal/Provider/FileReferenceCacheProvider.php
+++ b/src/Psalm/Internal/Provider/FileReferenceCacheProvider.php
@@ -25,7 +25,9 @@ class FileReferenceCacheProvider
private const METHOD_CLASS_REFERENCE_CACHE_NAME = 'method_class_references';
private const ANALYZED_METHODS_CACHE_NAME = 'analyzed_methods';
private const CLASS_METHOD_CACHE_NAME = 'class_method_references';
+ private const CLASS_PROPERTY_CACHE_NAME = 'class_property_references';
private const FILE_CLASS_MEMBER_CACHE_NAME = 'file_class_member_references';
+ private const FILE_CLASS_PROPERTY_CACHE_NAME = 'file_class_property_references';
private const ISSUES_CACHE_NAME = 'issues';
private const FILE_MAPS_CACHE_NAME = 'file_maps';
private const TYPE_COVERAGE_CACHE_NAME = 'type_coverage';
@@ -182,6 +184,32 @@ class FileReferenceCacheProvider
return $class_member_reference_cache;
}
+ /**
+ * @psalm-suppress MixedAssignment
+ */
+ public function getCachedMethodPropertyReferences(): ?array
+ {
+ $cache_directory = $this->config->getCacheDirectory();
+
+ if (!$cache_directory) {
+ return null;
+ }
+
+ $class_member_cache_location = $cache_directory . DIRECTORY_SEPARATOR . self::CLASS_PROPERTY_CACHE_NAME;
+
+ if (!is_readable($class_member_cache_location)) {
+ return null;
+ }
+
+ $class_member_reference_cache = unserialize((string) file_get_contents($class_member_cache_location));
+
+ if (!is_array($class_member_reference_cache)) {
+ throw new \UnexpectedValueException('The reference cache must be an array');
+ }
+
+ return $class_member_reference_cache;
+ }
+
/**
* @psalm-suppress MixedAssignment
*/
@@ -234,6 +262,34 @@ class FileReferenceCacheProvider
return $file_class_member_reference_cache;
}
+ /**
+ * @psalm-suppress MixedAssignment
+ */
+ public function getCachedFilePropertyReferences(): ?array
+ {
+ $cache_directory = $this->config->getCacheDirectory();
+
+ if (!$cache_directory) {
+ return null;
+ }
+
+ $file_class_member_cache_location = $cache_directory
+ . DIRECTORY_SEPARATOR
+ . self::FILE_CLASS_PROPERTY_CACHE_NAME;
+
+ if (!is_readable($file_class_member_cache_location)) {
+ return null;
+ }
+
+ $file_class_member_reference_cache = unserialize((string) file_get_contents($file_class_member_cache_location));
+
+ if (!is_array($file_class_member_reference_cache)) {
+ throw new \UnexpectedValueException('The reference cache must be an array');
+ }
+
+ return $file_class_member_reference_cache;
+ }
+
/**
* @psalm-suppress MixedAssignment
*/
@@ -404,6 +460,19 @@ class FileReferenceCacheProvider
file_put_contents($member_cache_location, serialize($member_references));
}
+ public function setCachedMethodPropertyReferences(array $property_references): void
+ {
+ $cache_directory = $this->config->getCacheDirectory();
+
+ if (!$cache_directory) {
+ return;
+ }
+
+ $member_cache_location = $cache_directory . DIRECTORY_SEPARATOR . self::CLASS_PROPERTY_CACHE_NAME;
+
+ file_put_contents($member_cache_location, serialize($property_references));
+ }
+
public function setCachedMethodMissingMemberReferences(array $member_references): void
{
$cache_directory = $this->config->getCacheDirectory();
@@ -430,6 +499,19 @@ class FileReferenceCacheProvider
file_put_contents($member_cache_location, serialize($member_references));
}
+ public function setCachedFilePropertyReferences(array $property_references): void
+ {
+ $cache_directory = $this->config->getCacheDirectory();
+
+ if (!$cache_directory) {
+ return;
+ }
+
+ $member_cache_location = $cache_directory . DIRECTORY_SEPARATOR . self::FILE_CLASS_PROPERTY_CACHE_NAME;
+
+ file_put_contents($member_cache_location, serialize($property_references));
+ }
+
public function setCachedFileMissingMemberReferences(array $member_references): void
{
$cache_directory = $this->config->getCacheDirectory();
diff --git a/src/Psalm/Internal/Provider/FileReferenceProvider.php b/src/Psalm/Internal/Provider/FileReferenceProvider.php
index 1f2217313..b4643d526 100644
--- a/src/Psalm/Internal/Provider/FileReferenceProvider.php
+++ b/src/Psalm/Internal/Provider/FileReferenceProvider.php
@@ -46,6 +46,13 @@ class FileReferenceProvider
*/
private static $file_references_to_class_members = [];
+ /**
+ * A lookup table used for getting all the files that reference a class property
+ *
+ * @var array>
+ */
+ private static $file_references_to_class_properties = [];
+
/**
* A lookup table used for getting all the files that reference a missing class member
*
@@ -77,6 +84,11 @@ class FileReferenceProvider
*/
private static $method_references_to_class_members = [];
+ /**
+ * @var array>
+ */
+ private static $method_references_to_class_properties = [];
+
/**
* @var array>
*/
@@ -206,6 +218,11 @@ class FileReferenceProvider
self::$file_references_to_class_members[$referenced_member_id][$source_file] = true;
}
+ public function addFileReferenceToClassProperty(string $source_file, string $referenced_property_id): void
+ {
+ self::$file_references_to_class_properties[$referenced_property_id][$source_file] = true;
+ }
+
public function addFileReferenceToMissingClassMember(string $source_file, string $referenced_member_id): void
{
self::$file_references_to_missing_class_members[$referenced_member_id][$source_file] = true;
@@ -219,6 +236,14 @@ class FileReferenceProvider
return self::$file_references_to_class_members;
}
+ /**
+ * @return array>
+ */
+ public function getAllFileReferencesToClassProperties(): array
+ {
+ return self::$file_references_to_class_properties;
+ }
+
/**
* @return array>
*/
@@ -245,6 +270,24 @@ class FileReferenceProvider
}
}
+ /**
+ * @param array> $references
+ *
+ */
+ public function addFileReferencesToClassProperties(array $references): void
+ {
+ foreach ($references as $key => $reference) {
+ if (isset(self::$file_references_to_class_properties[$key])) {
+ self::$file_references_to_class_properties[$key] = array_merge(
+ $reference,
+ self::$file_references_to_class_properties[$key]
+ );
+ } else {
+ self::$file_references_to_class_properties[$key] = $reference;
+ }
+ }
+ }
+
/**
* @param array> $references
*
@@ -372,6 +415,14 @@ class FileReferenceProvider
return self::$method_references_to_class_members;
}
+ /**
+ * @return array>
+ */
+ public function getAllMethodReferencesToClassProperties(): array
+ {
+ return self::$method_references_to_class_properties;
+ }
+
/**
* @return array>
*/
@@ -444,6 +495,14 @@ class FileReferenceProvider
self::$method_references_to_class_members = $method_references_to_class_members;
+ $method_references_to_class_properties = $this->cache->getCachedMethodPropertyReferences();
+
+ if ($method_references_to_class_properties === null) {
+ return false;
+ }
+
+ self::$method_references_to_class_properties = $method_references_to_class_properties;
+
$method_references_to_missing_class_members = $this->cache->getCachedMethodMissingMemberReferences();
if ($method_references_to_missing_class_members === null) {
@@ -460,6 +519,14 @@ class FileReferenceProvider
self::$file_references_to_class_members = $file_references_to_class_members;
+ $file_references_to_class_properties = $this->cache->getCachedFilePropertyReferences();
+
+ if ($file_references_to_class_properties === null) {
+ return false;
+ }
+
+ self::$file_references_to_class_properties = $file_references_to_class_properties;
+
$file_references_to_missing_class_members = $this->cache->getCachedFileMissingMemberReferences();
if ($file_references_to_missing_class_members === null) {
@@ -556,7 +623,9 @@ class FileReferenceProvider
$this->cache->setCachedMethodClassReferences(self::$method_references_to_classes);
$this->cache->setCachedNonMethodClassReferences(self::$nonmethod_references_to_classes);
$this->cache->setCachedMethodMemberReferences(self::$method_references_to_class_members);
+ $this->cache->setCachedMethodPropertyReferences(self::$method_references_to_class_properties);
$this->cache->setCachedFileMemberReferences(self::$file_references_to_class_members);
+ $this->cache->setCachedFilePropertyReferences(self::$file_references_to_class_properties);
$this->cache->setCachedMethodMissingMemberReferences(self::$method_references_to_missing_class_members);
$this->cache->setCachedFileMissingMemberReferences(self::$file_references_to_missing_class_members);
$this->cache->setCachedMixedMemberNameReferences(self::$references_to_mixed_member_names);
@@ -590,6 +659,15 @@ class FileReferenceProvider
}
}
+ public function addMethodReferenceToClassProperty(string $calling_function_id, string $referenced_property_id): void
+ {
+ if (!isset(self::$method_references_to_class_properties[$referenced_property_id])) {
+ self::$method_references_to_class_properties[$referenced_property_id] = [$calling_function_id => true];
+ } else {
+ self::$method_references_to_class_properties[$referenced_property_id][$calling_function_id] = true;
+ }
+ }
+
public function addMethodReferenceToMissingClassMember(
string $calling_function_id,
string $referenced_member_id
@@ -638,8 +716,8 @@ class FileReferenceProvider
public function isClassPropertyReferenced(string $property_id) : bool
{
- return !empty(self::$file_references_to_class_members[$property_id])
- || !empty(self::$method_references_to_class_members[$property_id]);
+ return !empty(self::$file_references_to_class_properties[$property_id])
+ || !empty(self::$method_references_to_class_properties[$property_id]);
}
public function isClassReferenced(string $fq_class_name_lc) : bool
@@ -728,6 +806,24 @@ class FileReferenceProvider
}
}
+ /**
+ * @param array> $references
+ *
+ */
+ public function addMethodReferencesToClassProperties(array $references): void
+ {
+ foreach ($references as $key => $reference) {
+ if (isset(self::$method_references_to_class_properties[$key])) {
+ self::$method_references_to_class_properties[$key] = array_merge(
+ $reference,
+ self::$method_references_to_class_properties[$key]
+ );
+ } else {
+ self::$method_references_to_class_properties[$key] = $reference;
+ }
+ }
+ }
+
/**
* @param array> $references
*
@@ -806,6 +902,15 @@ class FileReferenceProvider
self::$method_references_to_class_members = $references;
}
+ /**
+ * @param array> $references
+ *
+ */
+ public function setCallingMethodReferencesToClassProperties(array $references): void
+ {
+ self::$method_references_to_class_properties = $references;
+ }
+
/**
* @param array> $references
*
@@ -824,6 +929,15 @@ class FileReferenceProvider
self::$file_references_to_class_members = $references;
}
+ /**
+ * @param array> $references
+ *
+ */
+ public function setFileReferencesToClassProperties(array $references): void
+ {
+ self::$file_references_to_class_properties = $references;
+ }
+
/**
* @param array> $references
*
@@ -993,7 +1107,9 @@ class FileReferenceProvider
self::$deleted_files = null;
self::$file_references = [];
self::$file_references_to_class_members = [];
+ self::$file_references_to_class_properties = [];
self::$method_references_to_class_members = [];
+ self::$method_references_to_class_properties = [];
self::$method_references_to_classes = [];
self::$nonmethod_references_to_classes = [];
self::$file_references_to_missing_class_members = [];
diff --git a/src/Psalm/Storage/EnumCaseStorage.php b/src/Psalm/Storage/EnumCaseStorage.php
index 560503b50..b98cf19d9 100644
--- a/src/Psalm/Storage/EnumCaseStorage.php
+++ b/src/Psalm/Storage/EnumCaseStorage.php
@@ -1,8 +1,6 @@
cached_method_class_references;
+ return $this->cached_nonmethod_class_references;
}
public function getCachedFileMemberReferences(): ?array
@@ -85,11 +91,21 @@ class FakeFileReferenceCacheProvider extends \Psalm\Internal\Provider\FileRefere
return $this->cached_file_member_references;
}
+ public function getCachedFilePropertyReferences(): ?array
+ {
+ return $this->cached_file_property_references;
+ }
+
public function getCachedMethodMemberReferences(): ?array
{
return $this->cached_method_member_references;
}
+ public function getCachedMethodPropertyReferences(): ?array
+ {
+ return $this->cached_method_property_references;
+ }
+
public function getCachedFileMissingMemberReferences(): ?array
{
return $this->cached_file_missing_member_references;
@@ -107,7 +123,7 @@ class FakeFileReferenceCacheProvider extends \Psalm\Internal\Provider\FileRefere
public function getCachedMethodParamUses(): ?array
{
- return $this->cached_method_missing_member_references;
+ return $this->cached_method_param_uses;
}
public function getCachedIssues(): ?array
@@ -140,6 +156,11 @@ class FakeFileReferenceCacheProvider extends \Psalm\Internal\Provider\FileRefere
$this->cached_method_member_references = $member_references;
}
+ public function setCachedMethodPropertyReferences(array $property_references): void
+ {
+ $this->cached_method_property_references = $property_references;
+ }
+
public function setCachedMethodMissingMemberReferences(array $member_references): void
{
$this->cached_method_missing_member_references = $member_references;
@@ -150,6 +171,11 @@ class FakeFileReferenceCacheProvider extends \Psalm\Internal\Provider\FileRefere
$this->cached_file_member_references = $member_references;
}
+ public function setCachedFilePropertyReferences(array $property_references): void
+ {
+ $this->cached_file_property_references = $property_references;
+ }
+
public function setCachedFileMissingMemberReferences(array $member_references): void
{
$this->cached_file_missing_member_references = $member_references;
diff --git a/tests/UnusedCodeTest.php b/tests/UnusedCodeTest.php
index 327ca974b..6100dd7d3 100644
--- a/tests/UnusedCodeTest.php
+++ b/tests/UnusedCodeTest.php
@@ -374,18 +374,22 @@ class UnusedCodeTest extends TestCase
'foo = 5;
}
+
+ public function getFoo(): void {
+ echo $this->foo;
+ }
}
class D extends C {
- protected $foo = 2;
+ protected int $foo = 2;
}
- (new D)->bar();',
+ (new D)->bar();
+ (new D)->getFoo();',
],
'usedClassAfterExtensionLoaded' => [
'getPrevious() ?? $exception);'
],
+ 'publicPropertyReadInFile' => [
+ 'a = "hello";
+ }
+ }
+
+ $foo = new A();
+ echo $foo->a;',
+ ],
+ 'publicPropertyReadInMethod' => [
+ 'a === "goodbye") {}
+ }
+ }
+
+ (new B)->foo(new A());',
+ ],
+ 'privatePropertyReadInMethod' => [
+ 'a = "hello";
+ }
+
+ public function emitA(): void {
+ echo $this->a;
+ }
+ }
+
+ (new A())->emitA();',
+ ],
];
}
@@ -1296,6 +1343,22 @@ class UnusedCodeTest extends TestCase
}',
'error_message' => 'UnusedFunctionCall',
],
+ 'propertyWrittenButNotRead' => [
+ 'a = "hello";
+ $this->b = "world";
+ }
+ }
+
+ $foo = new A();
+ echo $foo->a;',
+ 'error_message' => 'PossiblyUnusedProperty',
+ ],
];
}
}