diff --git a/src/Psalm/Internal/Analyzer/ClassAnalyzer.php b/src/Psalm/Internal/Analyzer/ClassAnalyzer.php index 4e40730140e..4ec9979297a 100644 --- a/src/Psalm/Internal/Analyzer/ClassAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/ClassAnalyzer.php @@ -391,6 +391,28 @@ public function analyze( } if ($storage->invalid_dependencies) { + foreach ($storage->invalid_dependencies as $dependency_name_lc => $_) { + if (!isset($storage->used_traits[$dependency_name_lc])) { + continue; + } + + foreach ($class->stmts as $stmt) { + if (!$stmt instanceof PhpParser\Node\Stmt\TraitUse) { + continue; + } + + $this->analyzeTraitUse( + $this->source->getAliases(), + $stmt, + $project_analyzer, + $storage, + $class_context, + $global_context, + $constructor_analyzer, + ); + } + } + return; } diff --git a/src/Psalm/Internal/Codebase/Populator.php b/src/Psalm/Internal/Codebase/Populator.php index 7f9b74e550d..043b48b65b2 100644 --- a/src/Psalm/Internal/Codebase/Populator.php +++ b/src/Psalm/Internal/Codebase/Populator.php @@ -251,7 +251,7 @@ private function populateClassLikeStorage(ClassLikeStorage $storage, array $depe $dependency->populated = false; unset($dependency->invalid_dependencies[$fq_classlike_name_lc]); - $this->populateClassLikeStorage($dependency, $dependent_classlikes); + $this->populateClassLikeStorage($dependency, $dependency->dependent_classlikes); } unset($this->invalid_class_storages[$fq_classlike_name_lc]); @@ -411,6 +411,9 @@ private function populateOverriddenMethods( } } + /** + * @param lowercase-string $used_trait_lc + */ private function populateDataFromTrait( ClassLikeStorage $storage, ClassLikeStorageProvider $storage_provider, @@ -425,6 +428,12 @@ private function populateDataFromTrait( ); $trait_storage = $storage_provider->get($used_trait_lc); } catch (InvalidArgumentException) { + $this->progress->debug('Populator could not find dependency (' . __LINE__ . ")\n"); + + $storage->invalid_dependencies[$used_trait_lc] = true; + + $this->invalid_class_storages[$used_trait_lc][] = $storage; + return; } @@ -589,14 +598,21 @@ private function populateInterfaceData( $new_parents = array_keys($interface_storage->parent_interfaces); $new_parents[] = $interface_storage->name; foreach ($new_parents as $new_parent) { + $new_parent_lc = strtolower($new_parent); try { - $new_parent = strtolower( + $new_parent_lc = strtolower( $this->classlikes->getUnAliasedName( - $new_parent, + $new_parent_lc, ), ); - $new_parent_interface_storage = $storage_provider->get($new_parent); + $new_parent_interface_storage = $storage_provider->get($new_parent_lc); } catch (InvalidArgumentException) { + $this->progress->debug('Populator could not find dependency (' . __LINE__ . ")\n"); + + $storage->invalid_dependencies[$new_parent_lc] = true; + + $this->invalid_class_storages[$new_parent_lc][] = $storage; + continue; } @@ -663,6 +679,9 @@ private static function extendTemplateParams( } } + /** + * @param lowercase-string $parent_interface_lc + */ private function populateInterfaceDataFromParentInterface( ClassLikeStorage $storage, ClassLikeStorageProvider $storage_provider, @@ -680,6 +699,9 @@ private function populateInterfaceDataFromParentInterface( $this->progress->debug('Populator could not find dependency (' . __LINE__ . ")\n"); $storage->invalid_dependencies[$parent_interface_lc] = true; + + $this->invalid_class_storages[$parent_interface_lc][] = $storage; + return; } @@ -706,6 +728,9 @@ private function populateInterfaceDataFromParentInterface( } } + /** + * @param lowercase-string $implemented_interface_lc + */ private function populateDataFromImplementedInterface( ClassLikeStorage $storage, ClassLikeStorageProvider $storage_provider, @@ -723,6 +748,9 @@ private function populateDataFromImplementedInterface( $this->progress->debug('Populator could not find dependency (' . __LINE__ . ")\n"); $storage->invalid_dependencies[$implemented_interface_lc] = true; + + $this->invalid_class_storages[$implemented_interface_lc][] = $storage; + return; } diff --git a/tests/StubTest.php b/tests/StubTest.php index cec3330077e..ea2347bb4c8 100644 --- a/tests/StubTest.php +++ b/tests/StubTest.php @@ -1163,6 +1163,138 @@ class A extends PartiallyStubbedClass {} $this->analyzeFile($file_path, new Context()); } + public function testUseOnlyStubbedTrait(): void + { + $this->project_analyzer = $this->getProjectAnalyzerWithConfig( + TestConfig::loadFromXML( + dirname(__DIR__), + ' + + + + + + + + + ', + ), + ); + + $file_path = (string) getcwd() . '/src/somefile.php'; + + $this->addFile( + $file_path, + 'run_stubbed_trait(rand(7, 9)); + } + }', + ); + + $this->analyzeFile($file_path, new Context()); + } + + public function testUseOnlyStubbedInterface(): void + { + $this->project_analyzer = $this->getProjectAnalyzerWithConfig( + TestConfig::loadFromXML( + dirname(__DIR__), + ' + + + + + + + + + ', + ), + ); + + $file_path = (string) getcwd() . '/src/somefile.php'; + + $this->addFile( + $file_path, + 'expectExceptionMessage('InvalidReturnStatement'); + $this->expectException(CodeException::class); + + $this->analyzeFile($file_path, new Context()); + } + + public function testUseOnlyStubbedTraitAndInterface(): void + { + $this->project_analyzer = $this->getProjectAnalyzerWithConfig( + TestConfig::loadFromXML( + dirname(__DIR__), + ' + + + + + + + + + + ', + ), + ); + + $file_path = (string) getcwd() . '/src/somefile.php'; + + $this->addFile( + $file_path, + 'run_stubbed_trait(rand(7, 9)); + } + + /** + * @param int $x + */ + public function run_stubbed($x): float { + return $x . ""; + } + }', + ); + + $this->expectExceptionMessage('InvalidReturnStatement'); + $this->expectException(CodeException::class); + + $this->analyzeFile($file_path, new Context()); + } + public function testStubFileWithExtendedStubbedClass(): void { $this->project_analyzer = $this->getProjectAnalyzerWithConfig( diff --git a/tests/fixtures/stubs/interface.phpstub b/tests/fixtures/stubs/interface.phpstub new file mode 100644 index 00000000000..dc08b65943e --- /dev/null +++ b/tests/fixtures/stubs/interface.phpstub @@ -0,0 +1,10 @@ +