typeFactory = $typeFactory; $this->simpleCallableNodeTraverser = $simpleCallableNodeTraverser; $this->staticTypeMapper = $staticTypeMapper; $this->classMethodReturnTypeOverrideGuard = $classMethodReturnTypeOverrideGuard; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Add return type declarations from yields', [new CodeSample(<<<'CODE_SAMPLE' class SomeClass { public function provide() { yield 1; } } CODE_SAMPLE , <<<'CODE_SAMPLE' class SomeClass { /** * @return Iterator */ public function provide(): Iterator { yield 1; } } CODE_SAMPLE )]); } /** * @return array> */ public function getNodeTypes() : array { return [Function_::class, ClassMethod::class, Closure::class]; } /** * @param Function_|ClassMethod|Closure $node */ public function refactorWithScope(Node $node, Scope $scope) : ?Node { $yieldNodes = $this->findCurrentScopeYieldNodes($node); if ($yieldNodes === []) { return null; } // skip already filled type if ($node->returnType instanceof Node && $this->isNames($node->returnType, ['Iterator', 'Generator', 'Traversable', 'iterable'])) { return null; } if ($node instanceof ClassMethod && $this->classMethodReturnTypeOverrideGuard->shouldSkipClassMethod($node, $scope)) { return null; } $yieldType = $this->resolveYieldType($yieldNodes, $node); $returnTypeNode = $this->staticTypeMapper->mapPHPStanTypeToPhpParserNode($yieldType, TypeKind::RETURN); if (!$returnTypeNode instanceof Node) { return null; } $node->returnType = $returnTypeNode; return $node; } public function provideMinPhpVersion() : int { return PhpVersionFeature::SCALAR_TYPES; } /** * @return Yield_[]|YieldFrom[] */ private function findCurrentScopeYieldNodes(FunctionLike $functionLike) : array { $yieldNodes = []; $this->simpleCallableNodeTraverser->traverseNodesWithCallable((array) $functionLike->getStmts(), static function (Node $node) use(&$yieldNodes) : ?int { // skip anonymous class and inner function if ($node instanceof Class_) { return NodeTraverser::DONT_TRAVERSE_CURRENT_AND_CHILDREN; } // skip nested scope if ($node instanceof FunctionLike) { return NodeTraverser::DONT_TRAVERSE_CURRENT_AND_CHILDREN; } if ($node instanceof Stmt && !$node instanceof Expression) { $yieldNodes = []; return NodeTraverser::STOP_TRAVERSAL; } if (!$node instanceof Yield_ && !$node instanceof YieldFrom) { return null; } $yieldNodes[] = $node; return null; }); return $yieldNodes; } /** * @param \PhpParser\Node\Expr\Yield_|\PhpParser\Node\Expr\YieldFrom $yield */ private function resolveYieldValue($yield) : ?Expr { if ($yield instanceof Yield_) { return $yield->value; } return $yield->expr; } /** * @param array $yieldNodes * @return Type[] */ private function resolveYieldedTypes(array $yieldNodes) : array { $yieldedTypes = []; foreach ($yieldNodes as $yieldNode) { $value = $this->resolveYieldValue($yieldNode); if (!$value instanceof Expr) { // one of the yields is empty return []; } $resolvedType = $this->nodeTypeResolver->getType($value); if ($resolvedType instanceof MixedType) { continue; } $yieldedTypes[] = $resolvedType; } return $yieldedTypes; } /** * @param array $yieldNodes * @param \PhpParser\Node\Stmt\ClassMethod|\PhpParser\Node\Stmt\Function_|\PhpParser\Node\Expr\Closure $functionLike * @return \Rector\StaticTypeMapper\ValueObject\Type\FullyQualifiedObjectType|\Rector\StaticTypeMapper\ValueObject\Type\FullyQualifiedGenericObjectType */ private function resolveYieldType(array $yieldNodes, $functionLike) { $yieldedTypes = $this->resolveYieldedTypes($yieldNodes); $className = $this->resolveClassName($functionLike); if ($yieldedTypes === []) { return new FullyQualifiedObjectType($className); } $yieldedTypes = $this->typeFactory->createMixedPassedOrUnionType($yieldedTypes); return new FullyQualifiedGenericObjectType($className, [$yieldedTypes]); } /** * @param \PhpParser\Node\Stmt\Function_|\PhpParser\Node\Stmt\ClassMethod|\PhpParser\Node\Expr\Closure $functionLike */ private function resolveClassName($functionLike) : string { $returnTypeNode = $functionLike->getReturnType(); if ($returnTypeNode instanceof Identifier && $returnTypeNode->name === 'iterable') { return 'Iterator'; } if ($returnTypeNode instanceof Name && !$this->nodeNameResolver->isName($returnTypeNode, 'Generator')) { return $this->nodeNameResolver->getName($returnTypeNode); } return 'Generator'; } }