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:
parent
63b35fc889
commit
af4a7cabe9
@ -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);
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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}>
|
||||
*/
|
||||
|
@ -25,6 +25,7 @@ function call(callable $gen) : Promise
|
||||
|
||||
/**
|
||||
* @template TReturn
|
||||
* @psalm-yield TReturn
|
||||
*/
|
||||
interface Promise
|
||||
{
|
||||
|
@ -351,6 +351,11 @@ class ClassLikeStorage
|
||||
*/
|
||||
public $template_type_implements_count;
|
||||
|
||||
/**
|
||||
* @var ?Type\Union
|
||||
*/
|
||||
public $yield;
|
||||
|
||||
/**
|
||||
* @var array<string, int>|null
|
||||
*/
|
||||
|
@ -798,6 +798,13 @@ class ReturnTypeTest extends TestCase
|
||||
return $foo;
|
||||
}'
|
||||
],
|
||||
'echoYield' => [
|
||||
'<?php
|
||||
/** @return Generator<void, void, string, void> */
|
||||
function gen(): Generator {
|
||||
echo yield;
|
||||
}'
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user