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 @@
+