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', + ], ]; } }