mirror of
https://github.com/danog/psalm.git
synced 2025-01-22 13:51:54 +01:00
parent
bb9aabe5b1
commit
8c9558c92b
@ -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;
|
||||
@ -298,87 +300,19 @@ class ReturnTypeCollector
|
||||
}
|
||||
|
||||
/**
|
||||
* @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;
|
||||
$collector = new YieldTypeCollector($nodes);
|
||||
$traverser = new NodeTraverser();
|
||||
$traverser->addVisitor(
|
||||
$collector
|
||||
);
|
||||
$traverser->traverse([$stmt]);
|
||||
|
||||
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 [];
|
||||
return $collector->getYieldTypes();
|
||||
}
|
||||
}
|
||||
|
@ -9,6 +9,7 @@ use Psalm\Context;
|
||||
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;
|
||||
@ -22,6 +23,8 @@ use Psalm\Type;
|
||||
use Psalm\Type\Atomic\TGenericObject;
|
||||
use Psalm\Type\Atomic\TNamedObject;
|
||||
|
||||
use function array_values;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
@ -147,33 +150,69 @@ class YieldAnalyzer
|
||||
$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;
|
||||
}
|
||||
|
||||
$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 (!$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
|
||||
);
|
||||
|
||||
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) {
|
||||
|
@ -618,6 +618,10 @@ class Populator
|
||||
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] = [];
|
||||
|
||||
|
75
src/Psalm/Internal/PhpVisitor/YieldTypeCollector.php
Normal file
75
src/Psalm/Internal/PhpVisitor/YieldTypeCollector.php
Normal file
@ -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;
|
||||
}
|
||||
}
|
@ -378,6 +378,9 @@ final class ClassLikeStorage implements HasAttributesInterface
|
||||
*/
|
||||
public $yield;
|
||||
|
||||
/** @var ?string */
|
||||
public $declaring_yield_fqcn;
|
||||
|
||||
/**
|
||||
* @var array<string, int>|null
|
||||
*/
|
||||
|
@ -4044,6 +4044,64 @@ class ClassTemplateExtendsTest extends TestCase
|
||||
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 {}
|
||||
|
Loading…
x
Reference in New Issue
Block a user