diff --git a/src/Psalm/Internal/Analyzer/FunctionLike/ReturnTypeCollector.php b/src/Psalm/Internal/Analyzer/FunctionLike/ReturnTypeCollector.php index 21fa3715577..d84d226640f 100644 --- a/src/Psalm/Internal/Analyzer/FunctionLike/ReturnTypeCollector.php +++ b/src/Psalm/Internal/Analyzer/FunctionLike/ReturnTypeCollector.php @@ -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 @@ private static function processYieldTypes( } /** - * @return list + * @return list */ - 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(); } } diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/YieldAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/YieldAnalyzer.php index 4847e04599b..9539bc08ce4 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/YieldAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/YieldAnalyzer.php @@ -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; @@ -22,6 +23,8 @@ use Psalm\Type\Atomic\TGenericObject; use Psalm\Type\Atomic\TNamedObject; +use function array_values; + /** * @internal */ @@ -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) { diff --git a/src/Psalm/Internal/Codebase/Populator.php b/src/Psalm/Internal/Codebase/Populator.php index 5902fa63dc1..a2e0f228f5f 100644 --- a/src/Psalm/Internal/Codebase/Populator.php +++ b/src/Psalm/Internal/Codebase/Populator.php @@ -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] = []; diff --git a/src/Psalm/Internal/PhpVisitor/YieldTypeCollector.php b/src/Psalm/Internal/PhpVisitor/YieldTypeCollector.php new file mode 100644 index 00000000000..d9c20fde573 --- /dev/null +++ b/src/Psalm/Internal/PhpVisitor/YieldTypeCollector.php @@ -0,0 +1,75 @@ + */ + 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 + */ + public function getYieldTypes(): array + { + return $this->yield_types; + } +} diff --git a/src/Psalm/Storage/ClassLikeStorage.php b/src/Psalm/Storage/ClassLikeStorage.php index d9954713107..8a9b25bfffd 100644 --- a/src/Psalm/Storage/ClassLikeStorage.php +++ b/src/Psalm/Storage/ClassLikeStorage.php @@ -378,6 +378,9 @@ final class ClassLikeStorage implements HasAttributesInterface */ public $yield; + /** @var ?string */ + public $declaring_yield_fqcn; + /** * @var array|null */ diff --git a/tests/Template/ClassTemplateExtendsTest.php b/tests/Template/ClassTemplateExtendsTest.php index a8c05c1551f..ea883366ca3 100644 --- a/tests/Template/ClassTemplateExtendsTest.php +++ b/tests/Template/ClassTemplateExtendsTest.php @@ -4044,6 +4044,64 @@ function c(): Promise { return new Success("a"); }' ], + 'yieldTemplatedComplex' => [ + 'code' => ' + */ + class b extends a {} + + /** @return Generator, mixed, "test2"> */ + function bb(): \Generator { + /** @var b<"test1", "test2"> */ + $b = new b; + $result = yield $b; + return $result; + }' + ], + 'yieldTemplatedComplexResolved' => [ + 'code' => ' + */ + class b extends a {} + + /** @return Generator */ + function bb(): \Generator { + $b = new b; + $result = yield $b; + return $result; + }' + ], + 'yieldTernary' => [ + 'code' => ' + */ + function a(): Generator { + return random_int(0, 1) ? 123 : yield new a; + }' + ], 'multiLineTemplateExtends' => [ 'code' => '