Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix #6983 #8564 #8578

Merged
merged 3 commits into from Oct 13, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
90 changes: 12 additions & 78 deletions src/Psalm/Internal/Analyzer/FunctionLike/ReturnTypeCollector.php
Expand Up @@ -3,8 +3,10 @@
namespace Psalm\Internal\Analyzer\FunctionLike;

use PhpParser;
use PhpParser\NodeTraverser;
use Psalm\Codebase;
use Psalm\Internal\Analyzer\Statements\Block\ForeachAnalyzer;
use Psalm\Internal\PhpVisitor\YieldTypeCollector;
use Psalm\Internal\Provider\NodeDataProvider;
use Psalm\Type;
use Psalm\Type\Atomic\TArray;
Expand Down Expand Up @@ -298,87 +300,19 @@ private static function processYieldTypes(
}

/**
* @return list<Union>
* @return list<Union>
*/
protected static function getYieldTypeFromExpression(
private static function getYieldTypeFromExpression(
PhpParser\Node\Expr $stmt,
NodeDataProvider $nodes
): array {
if ($stmt instanceof PhpParser\Node\Expr\Yield_) {
$key_type = null;

if ($stmt->key && ($stmt_key_type = $nodes->getType($stmt->key))) {
$key_type = $stmt_key_type;
}

if ($stmt->value
&& $value_type = $nodes->getType($stmt->value)
) {
$generator_type = new TGenericObject(
'Generator',
[
$key_type ? clone $key_type : Type::getInt(),
clone $value_type,
Type::getMixed(),
Type::getMixed()
]
);

return [new Union([$generator_type])];
}

return [Type::getMixed()];
}

if ($stmt instanceof PhpParser\Node\Expr\YieldFrom) {
if ($stmt_expr_type = $nodes->getType($stmt->expr)) {
return [$stmt_expr_type];
}

return [Type::getMixed()];
}

if ($stmt instanceof PhpParser\Node\Expr\BinaryOp) {
return [
...self::getYieldTypeFromExpression($stmt->left, $nodes),
...self::getYieldTypeFromExpression($stmt->right, $nodes)
];
}

if ($stmt instanceof PhpParser\Node\Expr\Assign) {
return self::getYieldTypeFromExpression($stmt->expr, $nodes);
}

if ($stmt instanceof PhpParser\Node\Expr\MethodCall
|| $stmt instanceof PhpParser\Node\Expr\FuncCall
|| $stmt instanceof PhpParser\Node\Expr\StaticCall
|| $stmt instanceof PhpParser\Node\Expr\New_
) {
if ($stmt->isFirstClassCallable()) {
return [];
}

$yield_types = [];

foreach ($stmt->getArgs() as $arg) {
$yield_types = [...$yield_types, ...self::getYieldTypeFromExpression($arg->value, $nodes)];
}

return $yield_types;
}

if ($stmt instanceof PhpParser\Node\Expr\Array_) {
$yield_types = [];

foreach ($stmt->items as $item) {
if ($item instanceof PhpParser\Node\Expr\ArrayItem) {
$yield_types = [...$yield_types, ...self::getYieldTypeFromExpression($item->value, $nodes)];
}
}

return $yield_types;
}

return [];
$collector = new YieldTypeCollector($nodes);
$traverser = new NodeTraverser();
$traverser->addVisitor(
$collector
);
$traverser->traverse([$stmt]);

return $collector->getYieldTypes();
}
}
85 changes: 62 additions & 23 deletions src/Psalm/Internal/Analyzer/Statements/Expression/YieldAnalyzer.php
Expand Up @@ -9,6 +9,7 @@
use Psalm\Exception\DocblockParseException;
use Psalm\Internal\Analyzer\CommentAnalyzer;
use Psalm\Internal\Analyzer\FunctionLikeAnalyzer;
use Psalm\Internal\Analyzer\Statements\Expression\Call\ClassTemplateParamCollector;
use Psalm\Internal\Analyzer\Statements\Expression\Fetch\AtomicPropertyFetchAnalyzer;
use Psalm\Internal\Analyzer\Statements\ExpressionAnalyzer;
use Psalm\Internal\Analyzer\StatementsAnalyzer;
Expand All @@ -22,6 +23,8 @@
use Psalm\Type\Atomic\TGenericObject;
use Psalm\Type\Atomic\TNamedObject;

use function array_values;

/**
* @internal
*/
Expand Down Expand Up @@ -147,33 +150,69 @@ public static function analyze(
$yield_type = null;

foreach ($expression_type->getAtomicTypes() as $expression_atomic_type) {
if ($expression_atomic_type instanceof TNamedObject) {
if (!$codebase->classlikes->classOrInterfaceExists($expression_atomic_type->value)) {
continue;
}
if (!$expression_atomic_type instanceof TNamedObject) {
continue;
}
if (!$codebase->classlikes->classOrInterfaceExists($expression_atomic_type->value)) {
continue;
}

$classlike_storage = $codebase->classlike_storage_provider->get($expression_atomic_type->value);

if (!$classlike_storage->yield) {
continue;
}
$declaring_classlike_storage = $classlike_storage->declaring_yield_fqcn
? $codebase->classlike_storage_provider->get($classlike_storage->declaring_yield_fqcn)
: $classlike_storage;

$yield_candidate_type = clone $classlike_storage->yield;
$yield_candidate_type = !$yield_candidate_type->isMixed()
? TypeExpander::expandUnion(
$codebase,
$yield_candidate_type,
$expression_atomic_type->value,
$expression_atomic_type->value,
null,
true,
false,
)
: $yield_candidate_type;

$class_template_params = ClassTemplateParamCollector::collect(
$codebase,
$declaring_classlike_storage,
$classlike_storage,
null,
$expression_atomic_type,
true
);

$classlike_storage = $codebase->classlike_storage_provider->get($expression_atomic_type->value);

if ($classlike_storage->yield) {
if ($expression_atomic_type instanceof TGenericObject) {
$yield_candidate_type = AtomicPropertyFetchAnalyzer::localizePropertyType(
$codebase,
clone $classlike_storage->yield,
$expression_atomic_type,
$classlike_storage,
$classlike_storage
);

$yield_type = Type::combineUnionTypes(
$yield_type,
$yield_candidate_type,
$codebase
);
} else {
$yield_type = Type::getMixed();
if ($class_template_params) {
if (!$expression_atomic_type instanceof TGenericObject) {
$type_params = [];

foreach ($class_template_params as $type_map) {
$type_params[] = clone array_values($type_map)[0];
}

$expression_atomic_type = new TGenericObject($expression_atomic_type->value, $type_params);
}

$yield_candidate_type = AtomicPropertyFetchAnalyzer::localizePropertyType(
$codebase,
$yield_candidate_type,
$expression_atomic_type,
$classlike_storage,
$declaring_classlike_storage
);
}

$yield_type = Type::combineUnionTypes(
$yield_type,
$yield_candidate_type,
$codebase
);
}

if ($yield_type) {
Expand Down
4 changes: 4 additions & 0 deletions src/Psalm/Internal/Codebase/Populator.php
Expand Up @@ -618,6 +618,10 @@ private static function extendTemplateParams(
ClassLikeStorage $parent_storage,
bool $from_direct_parent
): void {
if ($parent_storage->yield && !$storage->yield) {
$storage->yield = $parent_storage->yield;
$storage->declaring_yield_fqcn ??= $parent_storage->name;
}
if ($parent_storage->template_types) {
$storage->template_extended_params[$parent_storage->name] = [];

Expand Down
1 change: 0 additions & 1 deletion src/Psalm/Internal/LanguageServer/LanguageServer.php
Expand Up @@ -150,7 +150,6 @@ function (Message $msg): Generator {
* @var Promise
*/
$dispatched = $this->dispatch($msg->body);
/** @psalm-suppress MixedAssignment */
$result = yield $dispatched;
} catch (Error $e) {
// If a ResponseError is thrown, send it back in the Response
Expand Down
79 changes: 79 additions & 0 deletions src/Psalm/Internal/PhpVisitor/YieldTypeCollector.php
@@ -0,0 +1,79 @@
<?php

namespace Psalm\Internal\PhpVisitor;

use PhpParser\Node;
use PhpParser\Node\Expr\YieldFrom;
use PhpParser\Node\Expr\Yield_;
use PhpParser\Node\FunctionLike;
use PhpParser\NodeTraverser;
use PhpParser\NodeVisitorAbstract;
use Psalm\Internal\Provider\NodeDataProvider;
use Psalm\Type;
use Psalm\Type\Atomic\TGenericObject;
use Psalm\Type\Union;

/**
* @internal
*/
class YieldTypeCollector extends NodeVisitorAbstract
{
/** @var list<Union> */
private array $yield_types = [];

private NodeDataProvider $nodes;

public function __construct(NodeDataProvider $nodes)
{
$this->nodes = $nodes;
}

public function enterNode(Node $node): ?int
{
if ($node instanceof Yield_) {
$key_type = null;

if ($node->key && $node_key_type = $this->nodes->getType($node->key)) {
$key_type = $node_key_type;
}

if ($node->value
&& $value_type = $this->nodes->getType($node->value)
) {
$generator_type = new TGenericObject(
'Generator',
[
$key_type ? clone $key_type : Type::getInt(),
clone $value_type,
Type::getMixed(),
Type::getMixed()
]
);

$this->yield_types []= new Union([$generator_type]);
return null;
}

$this->yield_types []= Type::getMixed();
} elseif ($node instanceof YieldFrom) {
if ($node_expr_type = $this->nodes->getType($node->expr)) {
$this->yield_types []= $node_expr_type;
return null;
}

$this->yield_types []= Type::getMixed();
} elseif ($node instanceof FunctionLike) {
return NodeTraverser::DONT_TRAVERSE_CHILDREN;
}

return null;
}

/**
* @return list<Union>
*/
public function getYieldTypes(): array
{
return $this->yield_types;
}
}
3 changes: 3 additions & 0 deletions src/Psalm/Storage/ClassLikeStorage.php
Expand Up @@ -378,6 +378,9 @@ final class ClassLikeStorage implements HasAttributesInterface
*/
public $yield;

/** @var ?string */
public $declaring_yield_fqcn;

/**
* @var array<string, int>|null
*/
Expand Down
58 changes: 58 additions & 0 deletions tests/Template/ClassTemplateExtendsTest.php
Expand Up @@ -4044,6 +4044,64 @@ function c(): Promise {
return new Success("a");
}'
],
'yieldTemplatedComplex' => [
'code' => '<?php
/**
* @template T
* @psalm-yield T
*/
class a {
}

/**
* @template TT1
* @template TT2
* @extends a<TT2>
*/
class b extends a {}

/** @return Generator<int, b<"test1", "test2">, mixed, "test2"> */
function bb(): \Generator {
/** @var b<"test1", "test2"> */
$b = new b;
$result = yield $b;
return $result;
}'
],
'yieldTemplatedComplexResolved' => [
'code' => '<?php
/**
* @template T
* @psalm-yield T
*/
class a {
}

/**
* @extends a<"test">
*/
class b extends a {}

/** @return Generator<int, b, mixed, "test"> */
function bb(): \Generator {
$b = new b;
$result = yield $b;
return $result;
}'
],
'yieldTernary' => [
'code' => '<?php

/** @psalm-yield int */
class a {}

/**
* @return Generator<int, a, mixed, int>
*/
function a(): Generator {
return random_int(0, 1) ? 123 : yield new a;
}'
],
'multiLineTemplateExtends' => [
'code' => '<?php
interface IdInterface {}
Expand Down