typeNodeUnwrapper = $typeNodeUnwrapper; $this->returnStrictTypeAnalyzer = $returnStrictTypeAnalyzer; $this->returnTypeInferer = $returnTypeInferer; $this->phpVersionProvider = $phpVersionProvider; $this->classMethodReturnTypeOverrideGuard = $classMethodReturnTypeOverrideGuard; } public function getRuleDefinition() : RuleDefinition { return new RuleDefinition('Add return type from strict return type of call', [new CodeSample(<<<'CODE_SAMPLE' final class SomeClass { public function getData() { return $this->getNumber(); } private function getNumber(): int { return 1000; } } CODE_SAMPLE , <<<'CODE_SAMPLE' final class SomeClass { public function getData(): int { return $this->getNumber(); } private function getNumber(): int { return 1000; } } CODE_SAMPLE )]); } /** * @return array> */ public function getNodeTypes() : array { return [ClassMethod::class, Function_::class, Closure::class]; } public function provideMinPhpVersion() : int { return PhpVersionFeature::SCALAR_TYPES; } /** * @param ClassMethod|Function_|Closure $node */ public function refactorWithScope(Node $node, Scope $scope) : ?Node { if ($node->stmts === null) { return null; } if ($this->shouldSkip($node, $scope)) { return null; } $currentScopeReturns = $this->findCurrentScopeReturns($node); $returnedStrictTypes = $this->returnStrictTypeAnalyzer->collectStrictReturnTypes($currentScopeReturns, $scope); if ($returnedStrictTypes === []) { return null; } if (\count($returnedStrictTypes) === 1) { return $this->refactorSingleReturnType($currentScopeReturns[0], $returnedStrictTypes[0], $node); } if ($this->phpVersionProvider->isAtLeastPhpVersion(PhpVersionFeature::UNION_TYPES)) { /** @var PhpParserUnionType[] $returnedStrictTypes */ $unwrappedTypes = $this->typeNodeUnwrapper->unwrapNullableUnionTypes($returnedStrictTypes); $node->returnType = new PhpParserUnionType($unwrappedTypes); return $node; } return null; } /** * @param \PhpParser\Node\Stmt\ClassMethod|\PhpParser\Node\Stmt\Function_|\PhpParser\Node\Expr\Closure $node */ private function isUnionPossibleReturnsVoid($node) : bool { $inferReturnType = $this->returnTypeInferer->inferFunctionLike($node); if ($inferReturnType instanceof UnionType) { foreach ($inferReturnType->getTypes() as $type) { if ($type->isVoid()->yes()) { return \true; } } } return \false; } /** * @param \PhpParser\Node\Stmt\ClassMethod|\PhpParser\Node\Stmt\Function_|\PhpParser\Node\Expr\Closure $node * @return \PhpParser\Node\Expr\Closure|\PhpParser\Node\Stmt\ClassMethod|\PhpParser\Node\Stmt\Function_ */ private function processSingleUnionType($node, UnionType $unionType, NullableType $nullableType) { $types = $unionType->getTypes(); $returnType = $types[0] instanceof ObjectType && $types[1] instanceof NullType ? new NullableType(new FullyQualified($types[0]->getClassName())) : $nullableType; $node->returnType = $returnType; return $node; } /** * @param \PhpParser\Node\Stmt\ClassMethod|\PhpParser\Node\Stmt\Function_|\PhpParser\Node\Expr\Closure $node */ private function shouldSkip($node, Scope $scope) : bool { if ($node->returnType !== null) { return \true; } if ($node instanceof ClassMethod && $this->classMethodReturnTypeOverrideGuard->shouldSkipClassMethod($node, $scope)) { return \true; } return $this->isUnionPossibleReturnsVoid($node); } /** * @param \PhpParser\Node\Identifier|\PhpParser\Node\Name|\PhpParser\Node\NullableType|\PhpParser\Node\ComplexType $returnedStrictTypeNode * @param \PhpParser\Node\Stmt\ClassMethod|\PhpParser\Node\Stmt\Function_|\PhpParser\Node\Expr\Closure $functionLike * @return \PhpParser\Node\Expr\Closure|\PhpParser\Node\Stmt\ClassMethod|\PhpParser\Node\Stmt\Function_ */ private function refactorSingleReturnType(Return_ $return, $returnedStrictTypeNode, $functionLike) { $resolvedType = $this->nodeTypeResolver->getType($return); if ($resolvedType instanceof UnionType) { if (!$returnedStrictTypeNode instanceof NullableType) { return $functionLike; } return $this->processSingleUnionType($functionLike, $resolvedType, $returnedStrictTypeNode); } /** @var Name $returnType */ $returnType = $resolvedType instanceof ObjectType ? new FullyQualified($resolvedType->getClassName()) : $returnedStrictTypeNode; $functionLike->returnType = $returnType; return $functionLike; } /** * @return Return_[] * @param \PhpParser\Node\Stmt\ClassMethod|\PhpParser\Node\Stmt\Function_|\PhpParser\Node\Expr\Closure $node */ private function findCurrentScopeReturns($node) : array { $currentScopeReturns = []; if ($node->stmts === null) { return []; } $this->traverseNodesWithCallable($node->stmts, static function (Node $node) use(&$currentScopeReturns) : ?int { // skip scope nesting if ($node instanceof FunctionLike) { return NodeTraverser::DONT_TRAVERSE_CURRENT_AND_CHILDREN; } if (!$node instanceof Return_) { return null; } if (!$node->expr instanceof Expr) { return null; } $currentScopeReturns[] = $node; return null; }); return $currentScopeReturns; } }