1
0
mirror of https://github.com/danog/psalm.git synced 2024-11-26 20:34:47 +01:00

Fix #3057 and add support for @psalm-yield annotation

This commit is contained in:
Brown 2020-04-02 22:38:10 -04:00
parent 63b35fc889
commit af4a7cabe9
13 changed files with 139 additions and 43 deletions

View File

@ -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);

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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
)
);
}
}
}

View File

@ -37,7 +37,7 @@ class TextDocument
* @param string $uri
* @param Diagnostic[] $diagnostics
*
* @return Promise <void>
* @return Promise<void>
*/
public function publishDiagnostics(string $uri, array $diagnostics): Promise
{
@ -59,14 +59,17 @@ class TextDocument
{
return call(
/**
* @return \Generator<int, mixed, mixed, TextDocumentItem>
* @return \Generator<int, Promise<object>, object, TextDocumentItem>
*/
function () use ($textDocument) {
$result = yield $this->handler->request(
/** @var Promise<object> */
$promise = $this->handler->request(
'textDocument/xcontent',
['textDocument' => $textDocument]
);
$result = yield $promise;
/** @var TextDocumentItem */
return $this->mapper->map($result, new TextDocumentItem);
}

View File

@ -96,10 +96,11 @@ class ClientHandler
* @param string $method The method to call
* @param array|object $params The method parameters
*
* @return Promise <null> Will be resolved as soon as the notification has been sent
* @return Promise<void> Will be resolved as soon as the notification has been sent
*/
public function notify(string $method, $params): Promise
{
/** @var Promise<void> */
return $this->protocolWriter->write(
new Message(
new AdvancedJsonRpc\Notification($method, (object)$params)

View File

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

View File

@ -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<int, string, string, void>
* @return \Generator<int, Promise<?string>, ?string, void>
* @psalm-suppress MixedReturnTypeCoercion
*/
function () use ($input) : \Generator {
while ($this->is_accepting_new_requests) {
/** @var ?string $chunk */
$chunk = yield $input->read();
/** @var Promise<?string> */
$read_promise = $input->read();
$chunk = yield $read_promise;
if ($chunk === null) {
break;

View File

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

View File

@ -47,6 +47,11 @@ class ClassLikeDocblockComment
*/
public $template_implements = [];
/**
* @var ?string
*/
public $yield = null;
/**
* @var array<int, array{name:string, type:string, tag:string, line_number:int}>
*/

View File

@ -25,6 +25,7 @@ function call(callable $gen) : Promise
/**
* @template TReturn
* @psalm-yield TReturn
*/
interface Promise
{

View File

@ -351,6 +351,11 @@ class ClassLikeStorage
*/
public $template_type_implements_count;
/**
* @var ?Type\Union
*/
public $yield;
/**
* @var array<string, int>|null
*/

View File

@ -798,6 +798,13 @@ class ReturnTypeTest extends TestCase
return $foo;
}'
],
'echoYield' => [
'<?php
/** @return Generator<void, void, string, void> */
function gen(): Generator {
echo yield;
}'
],
];
}