Skip to content

Commit

Permalink
Fix 6644 (#728)
Browse files Browse the repository at this point in the history
  • Loading branch information
Dominik Peters committed Aug 21, 2021
1 parent 29a6d94 commit 2a6cfb3
Show file tree
Hide file tree
Showing 6 changed files with 410 additions and 99 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
<?php

namespace Rector\Tests\CodeQuality\Rector\ClassMethod\DateTimeToDateTimeInterfaceRector\Fixture;

final class FixtureFull
{
private \DateTime $date;

private ?\DateTime $nullableDate;

public function __construct(\DateTime $date, ?\DateTime $nullableDate)
{
$this->date = $date;
$this->nullableDate = $nullableDate;
}

public function getDate(): \DateTime
{
return $this->date;
}

public function getNullableDate(): ?\DateTime
{
return $this->nullableDate;
}
}

?>
-----
<?php

namespace Rector\Tests\CodeQuality\Rector\ClassMethod\DateTimeToDateTimeInterfaceRector\Fixture;

final class FixtureFull
{
/**
* @var \DateTime|\DateTimeImmutable
*/
private \DateTimeInterface $date;

/**
* @var \DateTime|\DateTimeImmutable|null
*/
private ?\DateTimeInterface $nullableDate;

/**
* @param \DateTime|\DateTimeImmutable $date
* @param \DateTime|\DateTimeImmutable|null $nullableDate
*/
public function __construct(\DateTimeInterface $date, ?\DateTimeInterface $nullableDate)
{
$this->date = $date;
$this->nullableDate = $nullableDate;
}

/**
* @return \DateTime|\DateTimeImmutable
*/
public function getDate(): \DateTimeInterface
{
return $this->date;
}

/**
* @return \DateTime|\DateTimeImmutable|null
*/
public function getNullableDate(): ?\DateTimeInterface
{
return $this->nullableDate;
}
}

?>
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,22 @@ namespace Rector\Tests\CodeQuality\Rector\ClassMethod\DateTimeToDateTimeInterfac

use DateTime as PhpDateTime;

class DateTime extends PhpDateTime
class FunctionDateTime extends PhpDateTime
{
public function getDateTimeCustomFormat(): ?string
{
return $this->format('Y-m-d H:i:s');
}
}

class Foo
class SkipChildClass
{
public static function bar(?DateTime $datetime)
public static function bar(?FunctionDateTime $datetime)
{
// ...
}

public static function baz(DateTime $datetime)
public static function baz(FunctionDateTime $datetime)
{
// ...
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?php

namespace Rector\Tests\CodeQuality\Rector\ClassMethod\DateTimeToDateTimeInterfaceRector\Fixture;

use DateTime as PhpDateTime;

class ReturnDateTime extends PhpDateTime
{
public function getDateTimeCustomFormat(): ?string
{
return $this->format('Y-m-d H:i:s');
}
}

class SkipChildClassReturn
{
public static function bar(): ?ReturnDateTime
{
// ...
}

public static function baz(): ReturnDateTime
{
// ...
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
<?php

declare(strict_types=1);


namespace Rector\CodeQuality\NodeManipulator;

use PhpParser\Node;
use PhpParser\Node\Arg;
use PhpParser\Node\Expr\Assign;
use PhpParser\Node\Expr\MethodCall;
use PhpParser\Node\Expr\Variable;
use PhpParser\Node\Identifier;
use PhpParser\Node\Name;
use PhpParser\Node\NullableType;
use PhpParser\Node\Param;
use PhpParser\Node\Stmt\ClassMethod;
use PhpParser\Node\Stmt\Expression;
use PHPStan\Type\NullType;
use PHPStan\Type\ObjectType;
use PHPStan\Type\Type;
use PHPStan\Type\UnionType;
use Rector\BetterPhpDocParser\PhpDocInfo\PhpDocInfoFactory;
use Rector\BetterPhpDocParser\PhpDocManipulator\PhpDocTypeChanger;
use Rector\Core\Exception\ShouldNotHappenException;
use Rector\Core\NodeAnalyzer\ParamAnalyzer;
use Rector\NodeNameResolver\NodeNameResolver;
use Rector\NodeTypeResolver\Node\AttributeKey;
use Rector\NodeTypeResolver\NodeTypeResolver;
use Symplify\Astral\NodeTraverser\SimpleCallableNodeTraverser;

final class ClassMethodParameterTypeManipulator
{
public function __construct(
private PhpDocInfoFactory $phpDocInfoFactory,
private PhpDocTypeChanger $phpDocTypeChanger,
private NodeTypeResolver $nodeTypeResolver,
private ParamAnalyzer $paramAnalyzer,
private NodeNameResolver $nodeNameResolver,
private SimpleCallableNodeTraverser $simpleCallableNodeTraverser
) {
}

/**
* @param string[] $methodsReturningClassInstance
*/
public function refactorFunctionParameters(
ClassMethod $node,
ObjectType $toReplaceType,
Identifier|Name|NullableType $replaceIntoType,
Type $phpDocType,
array $methodsReturningClassInstance
): void {
foreach ($node->getParams() as $param) {
if (! $this->nodeTypeResolver->isObjectType($param, $toReplaceType)) {
continue;
}

$paramType = $this->nodeTypeResolver->resolve($param);
if (! $paramType->isSuperTypeOf($toReplaceType)->yes()) {
continue;
}

$this->refactorParamTypeHint($param, $replaceIntoType);
$this->refactorParamDocBlock($param, $node, $phpDocType);
$this->refactorMethodCalls($param, $node, $methodsReturningClassInstance);
}
}


private function refactorParamTypeHint(Param $param, Identifier|Name|NullableType $replaceIntoType): void
{
if ($this->paramAnalyzer->isNullable($param) && !$replaceIntoType instanceof NullableType) {
$replaceIntoType = new NullableType($replaceIntoType);
}

$param->type = $replaceIntoType;
}

private function refactorParamDocBlock(Param $param, ClassMethod $classMethod, Type $phpDocType): void
{
$paramName = $this->nodeNameResolver->getName($param->var);
if ($paramName === null) {
throw new ShouldNotHappenException();
}

if ($this->paramAnalyzer->isNullable($param)) {
if ($phpDocType instanceof UnionType) {
// Adding a UnionType into a new UnionType throws an exception so we need to "unpack" the types
$phpDocType = new UnionType([...$phpDocType->getTypes(), new NullType()]);
} else {
$phpDocType = new UnionType([$phpDocType, new NullType()]);
}
}
$phpDocInfo = $this->phpDocInfoFactory->createFromNodeOrEmpty($classMethod);
$this->phpDocTypeChanger->changeParamType($phpDocInfo, $phpDocType, $param, $paramName);
}

/**
* @param string[] $methodsReturningClassInstance
*/
private function refactorMethodCalls(Param $param, ClassMethod $classMethod, array $methodsReturningClassInstance): void
{
if ($classMethod->stmts === null) {
return;
}

$this->simpleCallableNodeTraverser->traverseNodesWithCallable($classMethod->stmts, function (Node $node) use ($param, $methodsReturningClassInstance): void {
if (! ($node instanceof MethodCall)) {
return;
}

$this->refactorMethodCall($param, $node, $methodsReturningClassInstance);
});
}

/**
* @param string[] $methodsReturningClassInstance
*/
private function refactorMethodCall(Param $param, MethodCall $methodCall, array $methodsReturningClassInstance): void
{
$paramName = $this->nodeNameResolver->getName($param->var);
if ($paramName === null) {
return;
}
if ($this->shouldSkipMethodCallRefactor($paramName, $methodCall, $methodsReturningClassInstance)) {
return;
}

$assign = new Assign(new Variable($paramName), $methodCall);

/** @var Node $parent */
$parent = $methodCall->getAttribute(AttributeKey::PARENT_NODE);
if ($parent instanceof Arg) {
$parent->value = $assign;
return;
}

if (! $parent instanceof Expression) {
return;
}

$parent->expr = $assign;
}

/**
* @param string[] $methodsReturningClassInstance
*/
private function shouldSkipMethodCallRefactor(string $paramName, MethodCall $methodCall, array $methodsReturningClassInstance): bool
{
if (! $this->nodeNameResolver->isName($methodCall->var, $paramName)) {
return true;
}

if (! $this->nodeNameResolver->isNames($methodCall->name, $methodsReturningClassInstance)) {
return true;
}

$parentNode = $methodCall->getAttribute(AttributeKey::PARENT_NODE);
if (! $parentNode instanceof Node) {
return true;
}

return $parentNode instanceof Assign;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
<?php

declare(strict_types=1);


namespace Rector\CodeQuality\NodeManipulator;

use PhpParser\Node\Identifier;
use PhpParser\Node\Name;
use PhpParser\Node\NullableType;
use PhpParser\Node\Stmt\ClassMethod;
use PHPStan\Type\NullType;
use PHPStan\Type\ObjectType;
use PHPStan\Type\Type;
use PHPStan\Type\UnionType;
use Rector\BetterPhpDocParser\PhpDocInfo\PhpDocInfoFactory;
use Rector\BetterPhpDocParser\PhpDocManipulator\PhpDocTypeChanger;
use Rector\NodeTypeResolver\NodeTypeResolver;

final class ClassMethodReturnTypeManipulator
{
public function __construct(
private PhpDocInfoFactory $phpDocInfoFactory,
private PhpDocTypeChanger $phpDocTypeChanger,
private NodeTypeResolver $nodeTypeResolver
) {
}

public function refactorFunctionReturnType(
ClassMethod $classMethod,
ObjectType $toReplaceType,
Identifier|Name|NullableType $replaceIntoType,
Type $phpDocType
): void {
$returnType = $classMethod->returnType;
if ($returnType === null) {
return;
}

$isNullable = false;
if ($returnType instanceof NullableType) {
$isNullable = true;
$returnType = $returnType->type;
}
if (! $this->nodeTypeResolver->isObjectType($returnType, $toReplaceType)) {
return;
}

$paramType = $this->nodeTypeResolver->resolve($returnType);
if (! $paramType->isSuperTypeOf($toReplaceType)->yes()) {
return;
}

if ($isNullable) {
if ($phpDocType instanceof UnionType) {
// Adding a UnionType into a new UnionType throws an exception so we need to "unpack" the types
$phpDocType = new UnionType([...$phpDocType->getTypes(), new NullType()]);
} else {
$phpDocType = new UnionType([$phpDocType, new NullType()]);
}
if (!$replaceIntoType instanceof NullableType) {
$replaceIntoType = new NullableType($replaceIntoType);
}
}
$classMethod->returnType = $replaceIntoType;

$phpDocInfo = $this->phpDocInfoFactory->createFromNodeOrEmpty($classMethod);
$this->phpDocTypeChanger->changeReturnType($phpDocInfo, $phpDocType);
}
}

0 comments on commit 2a6cfb3

Please sign in to comment.