Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
  • Loading branch information
danog committed Oct 13, 2022
1 parent bb9aabe commit 8c9558c
Show file tree
Hide file tree
Showing 6 changed files with 214 additions and 101 deletions.
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,
new TNamedObject($expression_atomic_type->value),
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
75 changes: 75 additions & 0 deletions src/Psalm/Internal/PhpVisitor/YieldTypeCollector.php
@@ -0,0 +1,75 @@
<?php

namespace Psalm\Internal\PhpVisitor;

use PhpParser\Node;
use PhpParser\Node\Expr\YieldFrom;
use PhpParser\Node\Expr\Yield_;
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 $stmt): ?Node
{
if ($stmt instanceof Yield_) {
$key_type = null;

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

if ($stmt->value
&& $value_type = $this->nodes->getType($stmt->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 ($stmt instanceof YieldFrom) {
if ($stmt_expr_type = $this->nodes->getType($stmt->expr)) {
$this->yield_types []= $stmt_expr_type;
return null;
}

$this->yield_types []= Type::getMixed();
}

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

0 comments on commit 8c9558c

Please sign in to comment.