From 4a434d9a2f8fa15dd0616243d1668e505972ccbf Mon Sep 17 00:00:00 2001 From: Brown Date: Wed, 15 May 2019 18:41:26 -0400 Subject: [PATCH] Add separate issue for undefined classes in docblocks --- config.xsd | 1 + docs/issues.md | 11 ++++++ src/Psalm/Config.php | 4 ++ .../Internal/Analyzer/ClassLikeAnalyzer.php | 37 +++++++++++++------ .../Expression/Call/MethodCallAnalyzer.php | 15 ++++++-- .../Statements/Expression/CallAnalyzer.php | 12 ++++++ .../LanguageServer/LanguageServer.php | 2 +- .../Internal/Visitor/ReflectorVisitor.php | 6 +++ src/Psalm/Issue/UndefinedDocblockClass.php | 6 +++ src/Psalm/Type/Atomic.php | 25 ++++++++++--- tests/AnnotationTest.php | 22 ++++++++++- tests/AssertTest.php | 2 +- tests/EnumTest.php | 4 +- tests/PropertyTypeTest.php | 2 +- tests/Template/TemplateExtendsTest.php | 6 +-- tests/TraitTest.php | 2 +- 16 files changed, 128 insertions(+), 29 deletions(-) create mode 100644 src/Psalm/Issue/UndefinedDocblockClass.php diff --git a/config.xsd b/config.xsd index be43dfaa0..f451608af 100644 --- a/config.xsd +++ b/config.xsd @@ -313,6 +313,7 @@ + diff --git a/docs/issues.md b/docs/issues.md index dbc590951..077f5d827 100644 --- a/docs/issues.md +++ b/docs/issues.md @@ -2084,6 +2084,17 @@ Emitted when referencing a constant that doesn’t exist echo FOO_BAR; ``` +### UndefinedDocblockClass + +Emitted when referencing a class that doesn’t exist from a docblock + +```php +/** + * @param DoesNotExist $a + */ +function foo($a) : void {} +``` + ### UndefinedFunction Emitted when referencing a function that doesn't exist diff --git a/src/Psalm/Config.php b/src/Psalm/Config.php index 53413042d..24f35a7c3 100644 --- a/src/Psalm/Config.php +++ b/src/Psalm/Config.php @@ -1252,6 +1252,10 @@ class Config return 'MoreSpecificImplementedParamType'; } + if ($issue_type === 'UndefinedDocblockClass') { + return 'UndefinedClass'; + } + if ($issue_type === 'MixedArgumentTypeCoercion' || $issue_type === 'MixedPropertyTypeCoercion' || $issue_type === 'MixedReturnTypeCoercion' diff --git a/src/Psalm/Internal/Analyzer/ClassLikeAnalyzer.php b/src/Psalm/Internal/Analyzer/ClassLikeAnalyzer.php index ceb1366c5..febee501a 100644 --- a/src/Psalm/Internal/Analyzer/ClassLikeAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/ClassLikeAnalyzer.php @@ -12,6 +12,7 @@ use Psalm\Issue\InvalidClass; use Psalm\Issue\MissingDependency; use Psalm\Issue\ReservedWord; use Psalm\Issue\UndefinedClass; +use Psalm\Issue\UndefinedDocblockClass; use Psalm\IssueBuffer; use Psalm\StatementsSource; use Psalm\Storage\ClassLikeStorage; @@ -183,9 +184,10 @@ abstract class ClassLikeAnalyzer extends SourceAnalyzer implements StatementsSou $fq_class_name, CodeLocation $code_location, array $suppressed_issues, - $inferred = true, + bool $inferred = true, bool $allow_trait = false, - bool $allow_interface = true + bool $allow_interface = true, + bool $from_docblock = false ) { $codebase = $statements_source->getCodebase(); if (empty($fq_class_name)) { @@ -242,15 +244,28 @@ abstract class ClassLikeAnalyzer extends SourceAnalyzer implements StatementsSou if (!$class_exists && !$interface_exists) { if (!$allow_trait || !$codebase->classlikes->traitExists($fq_class_name, $code_location)) { - if (IssueBuffer::accepts( - new UndefinedClass( - 'Class or interface ' . $fq_class_name . ' does not exist', - $code_location, - $fq_class_name - ), - $suppressed_issues - )) { - return false; + if ($from_docblock) { + if (IssueBuffer::accepts( + new UndefinedDocblockClass( + 'Docblock-defined class or interface ' . $fq_class_name . ' does not exist', + $code_location, + $fq_class_name + ), + $suppressed_issues + )) { + return false; + } + } else { + if (IssueBuffer::accepts( + new UndefinedClass( + 'Class or interface ' . $fq_class_name . ' does not exist', + $code_location, + $fq_class_name + ), + $suppressed_issues + )) { + return false; + } } } diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/MethodCallAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/MethodCallAnalyzer.php index a8c9b92f0..11c2a4c40 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/MethodCallAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/MethodCallAnalyzer.php @@ -554,12 +554,17 @@ class MethodCallAnalyzer extends \Psalm\Internal\Analyzer\Statements\Expression\ $statements_analyzer, $fq_class_name, new CodeLocation($source, $stmt->var), - $statements_analyzer->getSuppressedIssues() + $statements_analyzer->getSuppressedIssues(), + true, + false, + true, + $lhs_type_part->from_docblock ); } if (!$does_class_exist) { - return $does_class_exist; + $non_existent_class_method_ids[] = $fq_class_name . '::*'; + return false; } if (!$stmt->name instanceof PhpParser\Node\Identifier) { @@ -782,7 +787,11 @@ class MethodCallAnalyzer extends \Psalm\Internal\Analyzer\Statements\Expression\ $statements_analyzer, $fq_class_name, new CodeLocation($source, $stmt->var), - $statements_analyzer->getSuppressedIssues() + $statements_analyzer->getSuppressedIssues(), + false, + false, + true, + $intersection_type->from_docblock ); if (!$does_class_exist) { diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/CallAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/CallAnalyzer.php index b3e3774e1..be341193a 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/CallAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/CallAnalyzer.php @@ -2905,6 +2905,18 @@ class CallAnalyzer foreach ($changed_vars as $changed_var) { if (isset($op_vars_in_scope[$changed_var])) { $op_vars_in_scope[$changed_var]->from_docblock = true; + + foreach ($op_vars_in_scope[$changed_var]->getTypes() as $changed_atomic_type) { + $changed_atomic_type->from_docblock = true; + + if ($changed_atomic_type instanceof Type\Atomic\TNamedObject + && $changed_atomic_type->extra_types + ) { + foreach ($changed_atomic_type->extra_types as $extra_type) { + $extra_type->from_docblock = true; + } + } + } } } diff --git a/src/Psalm/Internal/LanguageServer/LanguageServer.php b/src/Psalm/Internal/LanguageServer/LanguageServer.php index bcfe7ea72..3f6bb6f93 100644 --- a/src/Psalm/Internal/LanguageServer/LanguageServer.php +++ b/src/Psalm/Internal/LanguageServer/LanguageServer.php @@ -111,7 +111,7 @@ class LanguageServer extends AdvancedJsonRpc\Dispatcher // Invoke the method handler to get a result /** * @var Promise - * @psalm-suppress UndefinedClass + * @psalm-suppress UndefinedDocblockClass */ $dispatched = $this->dispatch($msg->body); $result = yield $dispatched; diff --git a/src/Psalm/Internal/Visitor/ReflectorVisitor.php b/src/Psalm/Internal/Visitor/ReflectorVisitor.php index 61eb28cd9..b1ba1924c 100644 --- a/src/Psalm/Internal/Visitor/ReflectorVisitor.php +++ b/src/Psalm/Internal/Visitor/ReflectorVisitor.php @@ -1050,6 +1050,8 @@ class ReflectorVisitor extends PhpParser\NodeVisitorAbstract implements PhpParse } } + $extended_union_type->setFromDocblock(); + foreach ($extended_union_type->getTypes() as $atomic_type) { if (!$atomic_type instanceof Type\Atomic\TGenericObject) { if (IssueBuffer::accepts( @@ -1134,6 +1136,8 @@ class ReflectorVisitor extends PhpParser\NodeVisitorAbstract implements PhpParse } } + $implemented_union_type->setFromDocblock(); + foreach ($implemented_union_type->getTypes() as $atomic_type) { if (!$atomic_type instanceof Type\Atomic\TGenericObject) { if (IssueBuffer::accepts( @@ -1216,6 +1220,8 @@ class ReflectorVisitor extends PhpParser\NodeVisitorAbstract implements PhpParse } } + $used_union_type->setFromDocblock(); + foreach ($used_union_type->getTypes() as $atomic_type) { if (!$atomic_type instanceof Type\Atomic\TGenericObject) { if (IssueBuffer::accepts( diff --git a/src/Psalm/Issue/UndefinedDocblockClass.php b/src/Psalm/Issue/UndefinedDocblockClass.php new file mode 100644 index 000000000..0f1c7dede --- /dev/null +++ b/src/Psalm/Issue/UndefinedDocblockClass.php @@ -0,0 +1,6 @@ +value, $code_location, $suppressed_issues, - $inferred + $inferred, + false, + true, + $this->from_docblock ) === false ) { return false; @@ -312,7 +315,10 @@ abstract class Atomic $extra_type->value, $code_location, $suppressed_issues, - $inferred + $inferred, + false, + true, + $this->from_docblock ) === false ) { return false; @@ -327,7 +333,10 @@ abstract class Atomic $this->value, $code_location, $suppressed_issues, - $inferred + $inferred, + false, + true, + $this->from_docblock ) === false ) { return false; @@ -351,7 +360,10 @@ abstract class Atomic $this->as, $code_location, $suppressed_issues, - $inferred + $inferred, + false, + true, + $this->from_docblock ) === false ) { return false; @@ -403,7 +415,10 @@ abstract class Atomic $fq_classlike_name, $code_location, $suppressed_issues, - $inferred + $inferred, + false, + true, + $this->from_docblock ) === false ) { return false; diff --git a/tests/AnnotationTest.php b/tests/AnnotationTest.php index 5f1ed4638..63c82d066 100644 --- a/tests/AnnotationTest.php +++ b/tests/AnnotationTest.php @@ -1177,6 +1177,26 @@ class AnnotationTest extends TestCase }', 'error_message' => 'UndefinedClass', ], + 'undefinedDocblockClassCall' => [ + 'foo()->bar(); + } + } + ', + 'error_message' => 'UndefinedDocblockClass', + ], 'preventBadObjectLikeFormat' => [ ' 'UndefinedClass', + 'error_message' => 'UndefinedDocblockClass', ], 'badPsalmType' => [ 'sayHello();', - 'error_message' => 'UndefinedClass', + 'error_message' => 'UndefinedDocblockClass', ], 'detectRedundantCondition' => [ ' 'UndefinedClass', + 'error_message' => 'UndefinedDocblockClass', ], 'selfClassConstBadValue' => [ ' 'UndefinedClass', + 'error_message' => 'UndefinedDocblockClass', ], 'nonExistentClassConstant' => [ ' 'UndefinedClass', + 'error_message' => 'UndefinedDocblockClass', ], 'abstractClassWithNoConstructorButChild' => [ ' */ class B extends A {}', - 'error_message' => 'UndefinedClass', + 'error_message' => 'UndefinedDocblockClass', ], 'badTemplateExtendsInt' => [ ' */ class B implements I {}', - 'error_message' => 'UndefinedClass', + 'error_message' => 'UndefinedDocblockClass', ], 'badTemplateImplementsInt' => [ ' 'UndefinedClass', + 'error_message' => 'UndefinedDocblockClass', ], 'badTemplateUseBadFormat' => [ ' 'UndefinedClass', + 'error_message' => 'UndefinedDocblockClass', ], ]; }