diff --git a/src/Psalm/Checker/ClassChecker.php b/src/Psalm/Checker/ClassChecker.php index b503a3bcb..c14a70d72 100644 --- a/src/Psalm/Checker/ClassChecker.php +++ b/src/Psalm/Checker/ClassChecker.php @@ -398,69 +398,16 @@ class ClassChecker extends ClassLikeChecker $constructor_checker = $method_checker; } } elseif ($stmt instanceof PhpParser\Node\Stmt\TraitUse) { - $previous_context_include_location = $class_context->include_location; - foreach ($stmt->traits as $trait) { - $class_context->include_location = new CodeLocation($this, $trait, null, true); - - $fq_trait_name = self::getFQCLNFromNameObject( - $trait, - $this->source->getAliases() - ); - - if (!$codebase->classlikes->hasFullyQualifiedTraitName($fq_trait_name)) { - if (IssueBuffer::accepts( - new UndefinedTrait( - 'Trait ' . $fq_trait_name . ' does not exist', - new CodeLocation($this, $trait) - ), - array_merge($storage->suppressed_issues, $this->getSuppressedIssues()) - )) { - return false; - } - } else { - if (!$codebase->traitHasCorrectCase($fq_trait_name)) { - if (IssueBuffer::accepts( - new UndefinedTrait( - 'Trait ' . $fq_trait_name . ' has wrong casing', - new CodeLocation($this, $trait) - ), - array_merge($storage->suppressed_issues, $this->getSuppressedIssues()) - )) { - return false; - } - - continue; - } - - $trait_file_checker = $project_checker->getFileCheckerForClassLike($fq_trait_name); - $trait_node = $codebase->classlikes->getTraitNode($fq_trait_name); - $trait_aliases = $codebase->classlikes->getTraitAliases($fq_trait_name); - $trait_checker = new TraitChecker( - $trait_node, - $trait_file_checker, - $fq_trait_name, - $trait_aliases - ); - - foreach ($trait_node->stmts as $trait_stmt) { - if ($trait_stmt instanceof PhpParser\Node\Stmt\ClassMethod) { - $trait_method_checker = $this->analyzeClassMethod( - $trait_stmt, - $storage, - $trait_checker, - $class_context, - $global_context - ); - - if ($trait_stmt->name->name === '__construct') { - $constructor_checker = $trait_method_checker; - } - } - } - } + if ($this->analyzeTraitUse( + $stmt, + $project_checker, + $storage, + $class_context, + $global_context, + $constructor_checker + ) === false) { + return false; } - - $class_context->include_location = $previous_context_include_location; } elseif ($stmt instanceof PhpParser\Node\Stmt\Property) { foreach ($stmt->props as $prop) { if ($prop->default) { @@ -693,6 +640,96 @@ class ClassChecker extends ClassLikeChecker } } + /** + * @return false|null + */ + private function analyzeTraitUse( + PhpParser\Node\Stmt\TraitUse $stmt, + ProjectChecker $project_checker, + ClassLikeStorage $storage, + Context $class_context, + Context $global_context = null, + MethodChecker &$constructor_checker = null + ) { + $codebase = $project_checker->codebase; + + $previous_context_include_location = $class_context->include_location; + + foreach ($stmt->traits as $trait_name) { + $class_context->include_location = new CodeLocation($this, $trait_name, null, true); + + $fq_trait_name = self::getFQCLNFromNameObject( + $trait_name, + $this->source->getAliases() + ); + + if (!$codebase->classlikes->hasFullyQualifiedTraitName($fq_trait_name)) { + if (IssueBuffer::accepts( + new UndefinedTrait( + 'Trait ' . $fq_trait_name . ' does not exist', + new CodeLocation($this, $trait_name) + ), + array_merge($storage->suppressed_issues, $this->getSuppressedIssues()) + )) { + return false; + } + } else { + if (!$codebase->traitHasCorrectCase($fq_trait_name)) { + if (IssueBuffer::accepts( + new UndefinedTrait( + 'Trait ' . $fq_trait_name . ' has wrong casing', + new CodeLocation($this, $trait_name) + ), + array_merge($storage->suppressed_issues, $this->getSuppressedIssues()) + )) { + return false; + } + + continue; + } + + $trait_file_checker = $project_checker->getFileCheckerForClassLike($fq_trait_name); + $trait_node = $codebase->classlikes->getTraitNode($fq_trait_name); + $trait_aliases = $codebase->classlikes->getTraitAliases($fq_trait_name); + $trait_checker = new TraitChecker( + $trait_node, + $trait_file_checker, + $fq_trait_name, + $trait_aliases + ); + + foreach ($trait_node->stmts as $trait_stmt) { + if ($trait_stmt instanceof PhpParser\Node\Stmt\ClassMethod) { + $trait_method_checker = $this->analyzeClassMethod( + $trait_stmt, + $storage, + $trait_checker, + $class_context, + $global_context + ); + + if ($trait_stmt->name->name === '__construct') { + $constructor_checker = $trait_method_checker; + } + } elseif ($trait_stmt instanceof PhpParser\Node\Stmt\TraitUse) { + if ($this->analyzeTraitUse( + $trait_stmt, + $project_checker, + $storage, + $class_context, + $global_context, + $constructor_checker + ) === false) { + return false; + } + } + } + } + } + + $class_context->include_location = $previous_context_include_location; + } + /** * @param PhpParser\Node\Stmt\Property $stmt * diff --git a/src/Psalm/Checker/FileChecker.php b/src/Psalm/Checker/FileChecker.php index 502d79a15..6d2b79b98 100644 --- a/src/Psalm/Checker/FileChecker.php +++ b/src/Psalm/Checker/FileChecker.php @@ -35,7 +35,7 @@ class FileChecker extends SourceChecker implements StatementsSource /** * @var array */ - protected $included_file_paths = []; + protected $required_file_paths = []; /** * @var array @@ -351,7 +351,7 @@ class FileChecker extends SourceChecker implements StatementsSource */ public function addCheckedFilePath($file_path, $file_name) { - $this->included_file_paths[$file_path] = true; + $this->required_file_paths[$file_path] = true; $this->checked_file_names[] = $file_name; $this->checked_file_paths[] = $file_path; } @@ -390,7 +390,7 @@ class FileChecker extends SourceChecker implements StatementsSource */ public function hasAlreadyIncludedFilePath($file_path) { - return isset($this->included_file_paths[$file_path]); + return isset($this->required_file_paths[$file_path]); } /** diff --git a/src/Psalm/Codebase/Populator.php b/src/Psalm/Codebase/Populator.php index 70d82a6eb..215272c59 100644 --- a/src/Psalm/Codebase/Populator.php +++ b/src/Psalm/Codebase/Populator.php @@ -385,9 +385,9 @@ class Populator $dependent_file_paths[$file_path_lc] = true; - $all_included_file_paths = $storage->included_file_paths; + $all_required_file_paths = $storage->required_file_paths; - foreach ($storage->included_file_paths as $included_file_path => $_) { + foreach ($storage->required_file_paths as $included_file_path => $_) { try { $included_file_storage = $this->file_storage_provider->get($included_file_path); } catch (\InvalidArgumentException $e) { @@ -396,10 +396,10 @@ class Populator $this->populateFileStorage($included_file_storage, $dependent_file_paths); - $all_included_file_paths = $all_included_file_paths + $included_file_storage->included_file_paths; + $all_required_file_paths = $all_required_file_paths + $included_file_storage->required_file_paths; } - foreach ($all_included_file_paths as $included_file_path => $_) { + foreach ($all_required_file_paths as $included_file_path => $_) { try { $included_file_storage = $this->file_storage_provider->get($included_file_path); } catch (\InvalidArgumentException $e) { @@ -417,7 +417,37 @@ class Populator ); } - $storage->included_file_paths = $all_included_file_paths; + $storage->required_file_paths = $all_required_file_paths; + + foreach ($all_required_file_paths as $required_file_path) { + try { + $required_file_storage = $this->file_storage_provider->get($required_file_path); + } catch (\InvalidArgumentException $e) { + continue; + } + + $required_file_storage->required_by_file_paths += [$file_path_lc => $storage->file_path]; + } + + foreach ($storage->required_classes as $required_classlike) { + try { + $classlike_storage = $this->classlike_storage_provider->get($required_classlike); + } catch (\InvalidArgumentException $e) { + continue; + } + + if (!$classlike_storage->location) { + continue; + } + + try { + $required_file_storage = $this->file_storage_provider->get($classlike_storage->location->file_path); + } catch (\InvalidArgumentException $e) { + continue; + } + + $required_file_storage->required_by_file_paths += [$file_path_lc => $storage->file_path]; + } $storage->populated = true; } diff --git a/src/Psalm/Codebase/Scanner.php b/src/Psalm/Codebase/Scanner.php index 1a32df2f0..2863bf352 100644 --- a/src/Psalm/Codebase/Scanner.php +++ b/src/Psalm/Codebase/Scanner.php @@ -316,8 +316,8 @@ class Scanner $this->file_storage_provider->cache->writeToCache($file_storage, $file_contents); } } else { - foreach ($file_storage->included_file_paths as $include_file_path) { - $this->addFileToShallowScan($include_file_path); + foreach ($file_storage->required_file_paths as $required_file_path) { + $this->addFileToShallowScan($required_file_path); } foreach ($file_storage->classlikes_in_file as $fq_classlike_name) { diff --git a/src/Psalm/Config.php b/src/Psalm/Config.php index e926d4017..40e61b4d9 100644 --- a/src/Psalm/Config.php +++ b/src/Psalm/Config.php @@ -791,7 +791,25 @@ class Config if ($this->hide_external_errors) { $codebase = ProjectChecker::getInstance()->codebase; - if (!$codebase->analyzer->canReportIssues($file_path)) { + $dependent_files = [strtolower($file_path) => $file_path]; + + try { + $file_storage = $codebase->file_storage_provider->get($file_path); + $dependent_files += $file_storage->required_by_file_paths; + } catch (\InvalidArgumentException $e) { + // do nothing + } + + $any_file_path_matched = false; + + foreach ($dependent_files as $file_path) { + if ($codebase->analyzer->canReportIssues($file_path)) { + $any_file_path_matched = true; + break; + } + } + + if (!$any_file_path_matched) { return false; } } diff --git a/src/Psalm/Storage/FileStorage.php b/src/Psalm/Storage/FileStorage.php index f4c64ae49..6a397806c 100644 --- a/src/Psalm/Storage/FileStorage.php +++ b/src/Psalm/Storage/FileStorage.php @@ -48,7 +48,10 @@ class FileStorage public $declaring_constants = []; /** @var array */ - public $included_file_paths = []; + public $required_file_paths = []; + + /** @var array */ + public $required_by_file_paths = []; /** @var bool */ public $populated = false; diff --git a/src/Psalm/Visitor/DependencyFinderVisitor.php b/src/Psalm/Visitor/DependencyFinderVisitor.php index 4b83f7490..3d205bbcf 100644 --- a/src/Psalm/Visitor/DependencyFinderVisitor.php +++ b/src/Psalm/Visitor/DependencyFinderVisitor.php @@ -1632,7 +1632,7 @@ class DependencyFinderVisitor extends PhpParser\NodeVisitorAbstract implements P $this->codebase->scanner->addFileToShallowScan($path_to_file); } - $this->file_storage->included_file_paths[strtolower($path_to_file)] = $path_to_file; + $this->file_storage->required_file_paths[strtolower($path_to_file)] = $path_to_file; return; } diff --git a/tests/IncludeTest.php b/tests/IncludeTest.php index cfb923abb..1652bd49f 100644 --- a/tests/IncludeTest.php +++ b/tests/IncludeTest.php @@ -440,6 +440,52 @@ class IncludeTest extends TestCase 'error_message' => 'UndefinedGlobalVariable', 'hide_external_errors' => false ], + 'invalidTraitFunctionReturnInUncheckedFile' => [ + 'files' => [ + getcwd() . DIRECTORY_SEPARATOR . 'file2.php' => ' ' [ + getcwd() . DIRECTORY_SEPARATOR . 'file2.php', + ], + 'error_message' => 'InvalidReturnType', + ], + 'invalidDoubleNestedTraitFunctionReturnInUncheckedFile' => [ + 'files' => [ + getcwd() . DIRECTORY_SEPARATOR . 'file3.php' => ' ' ' [ + getcwd() . DIRECTORY_SEPARATOR . 'file3.php', + ], + 'error_message' => 'InvalidReturnType', + ], ]; } } diff --git a/tests/PropertyTypeTest.php b/tests/PropertyTypeTest.php index 823501253..6ed9a1911 100644 --- a/tests/PropertyTypeTest.php +++ b/tests/PropertyTypeTest.php @@ -460,7 +460,7 @@ class PropertyTypeTest extends TestCase } class A { - use T; + use T; }', ], 'abstractClassWithNoConstructor' => [ diff --git a/tests/TraitTest.php b/tests/TraitTest.php index c41f71045..a3c275f95 100644 --- a/tests/TraitTest.php +++ b/tests/TraitTest.php @@ -695,6 +695,23 @@ class TraitTest extends TestCase }', 'error_message' => 'MissingPropertyType', ], + 'nestedTraitWithBadReturnType' => [ + ' 'InvalidReturnType', + ], ]; } }