diff --git a/src/Psalm/DocComment.php b/src/Psalm/DocComment.php index c1a5924fe..183d4620b 100644 --- a/src/Psalm/DocComment.php +++ b/src/Psalm/DocComment.php @@ -22,6 +22,20 @@ use function trim; class DocComment { + private const PSALM_ANNOTATIONS = [ + 'return', 'param', 'template', 'var', 'type', + 'template-covariant', 'property', 'property-read', 'property-write', 'method', + 'assert', 'assert-if-true', 'assert-if-false', 'suppress', + 'ignore-nullable-return', 'override-property-visibility', + 'override-method-visibility', 'seal-properties', 'seal-methods', + 'generator-return', 'ignore-falsable-return', 'variadic', 'pure', + 'ignore-variable-method', 'ignore-variable-property', 'internal', + 'taint-sink', 'taint-source', 'assert-untainted', 'scope-this', + 'mutation-free', 'external-mutation-free', 'immutable', 'readonly', + 'remove-taint', 'allow-private-mutation', 'readonly-allow-private-mutation', + 'yield' + ]; + /** * @var bool */ @@ -146,18 +160,7 @@ class DocComment if (!in_array( $special_key, - [ - 'return', 'param', 'template', 'var', 'type', - 'template-covariant', 'property', 'property-read', 'property-write', 'method', - 'assert', 'assert-if-true', 'assert-if-false', 'suppress', - 'ignore-nullable-return', 'override-property-visibility', - 'override-method-visibility', 'seal-properties', 'seal-methods', - 'generator-return', 'ignore-falsable-return', 'variadic', 'pure', - 'ignore-variable-method', 'ignore-variable-property', 'internal', - 'taint-sink', 'taint-source', 'assert-untainted', 'scope-this', - 'mutation-free', 'external-mutation-free', 'immutable', 'readonly', - 'remove-taint', 'allow-private-mutation', 'readonly-allow-private-mutation', - ], + self::PSALM_ANNOTATIONS, true )) { throw new DocblockParseException('Unrecognised annotation @psalm-' . $special_key); @@ -274,18 +277,7 @@ class DocComment if (!in_array( $special_key, - [ - 'return', 'param', 'template', 'var', 'type', - 'template-covariant', 'property', 'property-read', 'property-write', 'method', - 'assert', 'assert-if-true', 'assert-if-false', 'suppress', - 'ignore-nullable-return', 'override-property-visibility', - 'override-method-visibility', 'seal-properties', 'seal-methods', - 'generator-return', 'ignore-falsable-return', 'variadic', 'pure', - 'ignore-variable-method', 'ignore-variable-property', 'internal', - 'taint-sink', 'taint-source', 'assert-untainted', 'scope-this', - 'mutation-free', 'external-mutation-free', 'immutable', 'readonly', - 'remove-taint', 'allow-private-mutation', 'readonly-allow-private-mutation', - ], + self::PSALM_ANNOTATIONS, true )) { throw new DocblockParseException('Unrecognised annotation @psalm-' . $special_key); diff --git a/src/Psalm/Internal/Analyzer/CommentAnalyzer.php b/src/Psalm/Internal/Analyzer/CommentAnalyzer.php index ea2efd9f4..4f1b6ed9a 100644 --- a/src/Psalm/Internal/Analyzer/CommentAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/CommentAnalyzer.php @@ -868,6 +868,13 @@ class CommentAnalyzer } } + if (isset($parsed_docblock['specials']['psalm-yield']) + ) { + $yield = reset($parsed_docblock['specials']['psalm-yield']); + + $info->yield = trim(preg_replace('@^[ \t]*\*@m', '', $yield)); + } + if (isset($parsed_docblock['specials']['deprecated'])) { $info->deprecated = true; } diff --git a/src/Psalm/Internal/Analyzer/FunctionLike/ReturnTypeAnalyzer.php b/src/Psalm/Internal/Analyzer/FunctionLike/ReturnTypeAnalyzer.php index eec12d2ec..532b82a10 100644 --- a/src/Psalm/Internal/Analyzer/FunctionLike/ReturnTypeAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/FunctionLike/ReturnTypeAnalyzer.php @@ -167,6 +167,7 @@ class ReturnTypeAnalyzer && !$return_type->from_docblock && !$return_type->isVoid() && !$inferred_yield_types + && (!$function_like_storage || !$function_like_storage->has_yield) && ScopeAnalyzer::getFinalControlActions( $function_stmts, $type_provider, @@ -376,7 +377,10 @@ class ReturnTypeAnalyzer $function_like_storage instanceof MethodStorage && $function_like_storage->final ); - if (!$inferred_return_type_parts && !$inferred_yield_types) { + if (!$inferred_return_type_parts + && !$inferred_yield_types + && (!$function_like_storage || !$function_like_storage->has_yield) + ) { if ($declared_return_type->isVoid() || $declared_return_type->isNever()) { return null; } @@ -424,7 +428,9 @@ class ReturnTypeAnalyzer } if (!$declared_return_type->hasMixed()) { - if ($inferred_return_type->isVoid() && $declared_return_type->isVoid()) { + if ($inferred_return_type->isVoid() + && ($declared_return_type->isVoid() || ($function_like_storage && $function_like_storage->has_yield)) + ) { return null; } diff --git a/src/Psalm/Internal/Analyzer/Statements/ExpressionAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/ExpressionAnalyzer.php index 221df090e..9630d2c5f 100644 --- a/src/Psalm/Internal/Analyzer/Statements/ExpressionAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/ExpressionAnalyzer.php @@ -1913,16 +1913,40 @@ class ExpressionAnalyzer $context->inside_call = false; if ($var_comment_type) { - $statements_analyzer->node_data->setType($stmt, $var_comment_type); + $expression_type = $var_comment_type; } elseif ($stmt_var_type = $statements_analyzer->node_data->getType($stmt->value)) { - $statements_analyzer->node_data->setType($stmt, $stmt_var_type); + $expression_type = clone $stmt_var_type; } else { - $statements_analyzer->node_data->setType($stmt, Type::getMixed()); + $expression_type = Type::getMixed(); } } else { - $statements_analyzer->node_data->setType($stmt, Type::getNull()); + $expression_type = Type::getEmpty(); } + foreach ($expression_type->getAtomicTypes() as $expression_atomic_type) { + if ($expression_atomic_type instanceof Type\Atomic\TNamedObject) { + $classlike_storage = $codebase->classlike_storage_provider->get($expression_atomic_type->value); + + if ($classlike_storage->yield) { + $yield_type = $classlike_storage->yield; + + if ($expression_atomic_type instanceof Type\Atomic\TGenericObject) { + $yield_type = PropertyFetchAnalyzer::localizePropertyType( + $codebase, + $yield_type, + $expression_atomic_type, + $classlike_storage, + $classlike_storage + ); + } + + $expression_type->substitute($expression_type, $yield_type); + } + } + } + + $statements_analyzer->node_data->setType($stmt, $expression_type); + $source = $statements_analyzer->getSource(); if ($source instanceof FunctionLikeAnalyzer @@ -1934,13 +1958,28 @@ class ExpressionAnalyzer if ($storage->return_type) { foreach ($storage->return_type->getAtomicTypes() as $atomic_return_type) { - if ($atomic_return_type instanceof Type\Atomic\TGenericObject + if ($atomic_return_type instanceof Type\Atomic\TNamedObject && $atomic_return_type->value === 'Generator' ) { - if (!$atomic_return_type->type_params[2]->isMixed() - && !$atomic_return_type->type_params[2]->isVoid() - ) { - $statements_analyzer->node_data->setType($stmt, clone $atomic_return_type->type_params[2]); + if ($atomic_return_type instanceof Type\Atomic\TGenericObject) { + if (!$atomic_return_type->type_params[2]->isVoid()) { + $statements_analyzer->node_data->setType( + $stmt, + Type::combineUnionTypes( + $atomic_return_type->type_params[2], + $expression_type, + $codebase + ) + ); + } + } else { + $statements_analyzer->node_data->setType( + $stmt, + Type::combineUnionTypes( + Type::getMixed(), + $expression_type + ) + ); } } } diff --git a/src/Psalm/Internal/LanguageServer/Client/TextDocument.php b/src/Psalm/Internal/LanguageServer/Client/TextDocument.php index 4d34a1480..4a8eb3be4 100644 --- a/src/Psalm/Internal/LanguageServer/Client/TextDocument.php +++ b/src/Psalm/Internal/LanguageServer/Client/TextDocument.php @@ -37,7 +37,7 @@ class TextDocument * @param string $uri * @param Diagnostic[] $diagnostics * - * @return Promise + * @return Promise */ public function publishDiagnostics(string $uri, array $diagnostics): Promise { @@ -59,14 +59,17 @@ class TextDocument { return call( /** - * @return \Generator + * @return \Generator, object, TextDocumentItem> */ function () use ($textDocument) { - $result = yield $this->handler->request( + /** @var Promise */ + $promise = $this->handler->request( 'textDocument/xcontent', ['textDocument' => $textDocument] ); + $result = yield $promise; + /** @var TextDocumentItem */ return $this->mapper->map($result, new TextDocumentItem); } diff --git a/src/Psalm/Internal/LanguageServer/ClientHandler.php b/src/Psalm/Internal/LanguageServer/ClientHandler.php index 98fc27ddd..870bf2172 100644 --- a/src/Psalm/Internal/LanguageServer/ClientHandler.php +++ b/src/Psalm/Internal/LanguageServer/ClientHandler.php @@ -96,10 +96,11 @@ class ClientHandler * @param string $method The method to call * @param array|object $params The method parameters * - * @return Promise Will be resolved as soon as the notification has been sent + * @return Promise Will be resolved as soon as the notification has been sent */ public function notify(string $method, $params): Promise { + /** @var Promise */ return $this->protocolWriter->write( new Message( new AdvancedJsonRpc\Notification($method, (object)$params) diff --git a/src/Psalm/Internal/LanguageServer/LanguageServer.php b/src/Psalm/Internal/LanguageServer/LanguageServer.php index 91f09c45f..8c171f368 100644 --- a/src/Psalm/Internal/LanguageServer/LanguageServer.php +++ b/src/Psalm/Internal/LanguageServer/LanguageServer.php @@ -135,9 +135,9 @@ class LanguageServer extends AdvancedJsonRpc\Dispatcher // Invoke the method handler to get a result /** * @var Promise - * @psalm-suppress UndefinedDocblockClass */ $dispatched = $this->dispatch($msg->body); + /** @psalm-suppress MixedAssignment */ $result = yield $dispatched; } catch (AdvancedJsonRpc\Error $e) { // If a ResponseError is thrown, send it back in the Response diff --git a/src/Psalm/Internal/LanguageServer/ProtocolStreamReader.php b/src/Psalm/Internal/LanguageServer/ProtocolStreamReader.php index e296d9c59..2dd88a29f 100644 --- a/src/Psalm/Internal/LanguageServer/ProtocolStreamReader.php +++ b/src/Psalm/Internal/LanguageServer/ProtocolStreamReader.php @@ -4,6 +4,7 @@ namespace Psalm\Internal\LanguageServer; use AdvancedJsonRpc\Message as MessageBody; use function Amp\asyncCall; +use Amp\Promise; use Amp\ByteStream\ResourceInputStream; use Exception; use function explode; @@ -47,13 +48,15 @@ class ProtocolStreamReader implements ProtocolReader $input = new ResourceInputStream($input); asyncCall( /** - * @return \Generator + * @return \Generator, ?string, void> * @psalm-suppress MixedReturnTypeCoercion */ function () use ($input) : \Generator { while ($this->is_accepting_new_requests) { - /** @var ?string $chunk */ - $chunk = yield $input->read(); + /** @var Promise */ + $read_promise = $input->read(); + + $chunk = yield $read_promise; if ($chunk === null) { break; diff --git a/src/Psalm/Internal/PhpVisitor/ReflectorVisitor.php b/src/Psalm/Internal/PhpVisitor/ReflectorVisitor.php index 37fa8818d..655d4c954 100644 --- a/src/Psalm/Internal/PhpVisitor/ReflectorVisitor.php +++ b/src/Psalm/Internal/PhpVisitor/ReflectorVisitor.php @@ -1174,6 +1174,33 @@ class ReflectorVisitor extends PhpParser\NodeVisitorAbstract implements PhpParse $this->implementTemplatedType($storage, $node, $implemented_class_name); } + if ($docblock_info->yield) { + $yield_type_tokens = Type::fixUpLocalType( + $docblock_info->yield, + $this->aliases, + $storage->template_types, + $this->type_aliases + ); + + try { + $yield_type = Type::parseTokens( + $yield_type_tokens, + null, + $storage->template_types ?: [] + ); + $yield_type->setFromDocblock(); + $yield_type->queueClassLikesForScanning( + $this->codebase, + $this->file_storage, + $storage->template_types ?: [] + ); + + $storage->yield = $yield_type; + } catch (TypeParseTreeException $e) { + // do nothing + } + } + $storage->sealed_properties = $docblock_info->sealed_properties; $storage->sealed_methods = $docblock_info->sealed_methods; diff --git a/src/Psalm/Internal/Scanner/ClassLikeDocblockComment.php b/src/Psalm/Internal/Scanner/ClassLikeDocblockComment.php index 7d4bf8044..c515ef852 100644 --- a/src/Psalm/Internal/Scanner/ClassLikeDocblockComment.php +++ b/src/Psalm/Internal/Scanner/ClassLikeDocblockComment.php @@ -47,6 +47,11 @@ class ClassLikeDocblockComment */ public $template_implements = []; + /** + * @var ?string + */ + public $yield = null; + /** * @var array */ diff --git a/src/Psalm/Internal/Stubs/Amp.php b/src/Psalm/Internal/Stubs/Amp.php index 906ddac6d..e341ddd30 100644 --- a/src/Psalm/Internal/Stubs/Amp.php +++ b/src/Psalm/Internal/Stubs/Amp.php @@ -25,6 +25,7 @@ function call(callable $gen) : Promise /** * @template TReturn + * @psalm-yield TReturn */ interface Promise { diff --git a/src/Psalm/Storage/ClassLikeStorage.php b/src/Psalm/Storage/ClassLikeStorage.php index d08bd9032..a0ba614df 100644 --- a/src/Psalm/Storage/ClassLikeStorage.php +++ b/src/Psalm/Storage/ClassLikeStorage.php @@ -351,6 +351,11 @@ class ClassLikeStorage */ public $template_type_implements_count; + /** + * @var ?Type\Union + */ + public $yield; + /** * @var array|null */ diff --git a/tests/ReturnTypeTest.php b/tests/ReturnTypeTest.php index 66d3ab52a..8e31a338d 100644 --- a/tests/ReturnTypeTest.php +++ b/tests/ReturnTypeTest.php @@ -798,6 +798,13 @@ class ReturnTypeTest extends TestCase return $foo; }' ], + 'echoYield' => [ + ' */ + function gen(): Generator { + echo yield; + }' + ], ]; }