diff --git a/config/set/code-quality.php b/config/set/code-quality.php index f7417d7f2e47..56f3f3c95b79 100644 --- a/config/set/code-quality.php +++ b/config/set/code-quality.php @@ -10,6 +10,7 @@ use Rector\CodeQuality\Rector\BooleanNot\SimplifyDeMorganBinaryRector; use Rector\CodeQuality\Rector\Catch_\ThrowWithPreviousExceptionRector; use Rector\CodeQuality\Rector\Class_\CompleteDynamicPropertiesRector; +use Rector\CodeQuality\Rector\ClassMethod\DateTimeToDateTimeInterfaceRector; use Rector\CodeQuality\Rector\Concat\JoinStringConcatRector; use Rector\CodeQuality\Rector\Equal\UseIdenticalOverEqualWithSameTypeRector; use Rector\CodeQuality\Rector\Expression\InlineIfToExplicitIfRector; @@ -163,4 +164,5 @@ $services->set(FixClassCaseSensitivityNameRector::class); $services->set(IssetOnPropertyObjectToPropertyExistsRector::class); $services->set(NewStaticToNewSelfRector::class); + $services->set(DateTimeToDateTimeInterfaceRector::class); }; diff --git a/rules/code-quality/src/Rector/ClassMethod/DateTimeToDateTimeInterfaceRector.php b/rules/code-quality/src/Rector/ClassMethod/DateTimeToDateTimeInterfaceRector.php new file mode 100644 index 000000000000..af3855514364 --- /dev/null +++ b/rules/code-quality/src/Rector/ClassMethod/DateTimeToDateTimeInterfaceRector.php @@ -0,0 +1,201 @@ +nodeTypeResolver = $nodeTypeResolver; + } + + public function getRuleDefinition(): RuleDefinition + { + return new RuleDefinition('Changes DateTime type-hint to DateTimeInterface', [ + new CodeSample( + <<<'CODE_SAMPLE' +class SomeClass { + public function methodWithDateTime(\DateTime $dateTime) + { + return true; + } +} +CODE_SAMPLE + , + <<<'CODE_SAMPLE' +class SomeClass { + /** + * @param \DateTime|\DateTimeImmutable $dateTime + */ + public function methodWithDateTime(\DateTimeInterface $dateTime) + { + return true; + } +} +CODE_SAMPLE + ), + ]); + } + + /** + * @return string[] + */ + public function getNodeTypes(): array + { + return [ClassMethod::class]; + } + + /** + * @param ClassMethod $node + */ + public function refactor(Node $node): ?Node + { + if (! $this->isAtLeastPhpVersion(PhpVersionFeature::DATE_TIME_INTERFACE)) { + return null; + } + + $isModifiedNode = false; + foreach ($node->getParams() as $param) { + if (! $this->isDateTimeParam($param)) { + continue; + } + + $this->refactorParamTypeHint($param); + $this->refactorParamDocBlock($param, $node); + $this->refactorMethodCalls($param, $node); + $isModifiedNode = true; + } + + if (! $isModifiedNode) { + return null; + } + + return $node; + } + + private function refactorParamTypeHint(Param $param): void + { + $dateTimeInterfaceType = new FullyQualified(DateTimeInterface::class); + if ($param->type instanceof NullableType) { + $param->type = new NullableType($dateTimeInterfaceType); + return; + } + + $param->type = $dateTimeInterfaceType; + } + + private function refactorParamDocBlock(Param $param, ClassMethod $classMethod): void + { + /** @var PhpDocInfo|null $phpDocInfo */ + $phpDocInfo = $classMethod->getAttribute(AttributeKey::PHP_DOC_INFO); + if ($phpDocInfo === null) { + $phpDocInfo = $this->phpDocInfoFactory->createEmpty($classMethod); + } + + $types = [new PHPStanObjectType(DateTime::class), new PHPStanObjectType(DateTimeImmutable::class)]; + if ($param->type instanceof NullableType) { + $types[] = new PHPStanNullType(); + } + + $paramName = $this->getName($param->var); + if ($paramName === null) { + throw new ShouldNotHappenException(); + } + $phpDocInfo->changeParamType(new PHPStanUnionType($types), $param, $paramName); + } + + private function refactorMethodCalls(Param $param, ClassMethod $classMethod): void + { + if ($classMethod->stmts === null) { + return; + } + + $this->traverseNodesWithCallable($classMethod->stmts, function (Node $node) use ($param) { + if (!($node instanceof MethodCall)){ + return; + } + + $this->refactorMethodCall($param, $node); + }); + } + + private function refactorMethodCall(Param $param, MethodCall $methodCall): void + { + $paramName = $this->getName($param->var); + if ($paramName === null || $this->shouldSkipMethodCallRefactor($paramName, $methodCall)) { + return; + } + + $newAssignNode = new Assign(new Variable($paramName), $methodCall); + + /** @var Node $parentNode */ + $parentNode = $methodCall->getAttribute(AttributeKey::PARENT_NODE); + if ($parentNode instanceof Arg) { + $parentNode->value = $newAssignNode; + return; + } + + $parentNode->expr = $newAssignNode; + } + + private function shouldSkipMethodCallRefactor(string $paramName, MethodCall $methodCall): bool + { + if (! $this->isName($methodCall->var, $paramName)) { + return true; + } + + if (! $this->isNames($methodCall->name, self::METHODS_RETURNING_CLASS_INSTANCE_MAP)) { + return true; + } + + $parentNode = $methodCall->getAttribute(AttributeKey::PARENT_NODE) ; + if ($parentNode === null) { + return true; + } + + return $parentNode instanceof Assign; + } + + private function isDateTimeParam(Param $param): bool + { + return $this->nodeTypeResolver->isObjectTypeOrNullableObjectType($param, DateTime::class); + } +} diff --git a/rules/code-quality/tests/Rector/ClassMethod/DateTimeToDateTimeInterfaceRector/DateTimeToDateTimeInterfaceRectorTest.php b/rules/code-quality/tests/Rector/ClassMethod/DateTimeToDateTimeInterfaceRector/DateTimeToDateTimeInterfaceRectorTest.php new file mode 100644 index 000000000000..808dfd7dbea7 --- /dev/null +++ b/rules/code-quality/tests/Rector/ClassMethod/DateTimeToDateTimeInterfaceRector/DateTimeToDateTimeInterfaceRectorTest.php @@ -0,0 +1,31 @@ +doTestFileInfo($fileInfo); + } + + public function provideData(): Iterator + { + return $this->yieldFilesFromDirectory(__DIR__ . '/Fixture'); + } + + protected function getRectorClass(): string + { + return DateTimeToDateTimeInterfaceRector::class; + } +} diff --git a/rules/code-quality/tests/Rector/ClassMethod/DateTimeToDateTimeInterfaceRector/Fixture/fixture_fqn.php.inc b/rules/code-quality/tests/Rector/ClassMethod/DateTimeToDateTimeInterfaceRector/Fixture/fixture_fqn.php.inc new file mode 100644 index 000000000000..b9f29d2e5908 --- /dev/null +++ b/rules/code-quality/tests/Rector/ClassMethod/DateTimeToDateTimeInterfaceRector/Fixture/fixture_fqn.php.inc @@ -0,0 +1,28 @@ + +----- + diff --git a/rules/code-quality/tests/Rector/ClassMethod/DateTimeToDateTimeInterfaceRector/Fixture/fixture_nullable.php.inc b/rules/code-quality/tests/Rector/ClassMethod/DateTimeToDateTimeInterfaceRector/Fixture/fixture_nullable.php.inc new file mode 100644 index 000000000000..5d01253b32a3 --- /dev/null +++ b/rules/code-quality/tests/Rector/ClassMethod/DateTimeToDateTimeInterfaceRector/Fixture/fixture_nullable.php.inc @@ -0,0 +1,28 @@ + +----- + diff --git a/rules/code-quality/tests/Rector/ClassMethod/DateTimeToDateTimeInterfaceRector/Fixture/fixture_use.php.inc b/rules/code-quality/tests/Rector/ClassMethod/DateTimeToDateTimeInterfaceRector/Fixture/fixture_use.php.inc new file mode 100644 index 000000000000..28bcacf30c68 --- /dev/null +++ b/rules/code-quality/tests/Rector/ClassMethod/DateTimeToDateTimeInterfaceRector/Fixture/fixture_use.php.inc @@ -0,0 +1,32 @@ + +----- + diff --git a/rules/code-quality/tests/Rector/ClassMethod/DateTimeToDateTimeInterfaceRector/Fixture/fixture_variable_usage.php.inc b/rules/code-quality/tests/Rector/ClassMethod/DateTimeToDateTimeInterfaceRector/Fixture/fixture_variable_usage.php.inc new file mode 100644 index 000000000000..bc44f3bd5b8f --- /dev/null +++ b/rules/code-quality/tests/Rector/ClassMethod/DateTimeToDateTimeInterfaceRector/Fixture/fixture_variable_usage.php.inc @@ -0,0 +1,36 @@ +modify('+1 day'); + + return $dateTime; + } +} + +?> +----- +modify('+1 day'); + + return $dateTime; + } +} + +?> diff --git a/rules/code-quality/tests/Rector/ClassMethod/DateTimeToDateTimeInterfaceRector/Fixture/fixture_variable_usage_assign.php.inc b/rules/code-quality/tests/Rector/ClassMethod/DateTimeToDateTimeInterfaceRector/Fixture/fixture_variable_usage_assign.php.inc new file mode 100644 index 000000000000..586b789e9114 --- /dev/null +++ b/rules/code-quality/tests/Rector/ClassMethod/DateTimeToDateTimeInterfaceRector/Fixture/fixture_variable_usage_assign.php.inc @@ -0,0 +1,36 @@ +modify('+1 day'); + + return $dateTime; + } +} + +?> +----- +modify('+1 day'); + + return $dateTime; + } +} + +?> diff --git a/rules/code-quality/tests/Rector/ClassMethod/DateTimeToDateTimeInterfaceRector/Fixture/fixture_variable_usage_param.php.inc b/rules/code-quality/tests/Rector/ClassMethod/DateTimeToDateTimeInterfaceRector/Fixture/fixture_variable_usage_param.php.inc new file mode 100644 index 000000000000..efe4bf5482a6 --- /dev/null +++ b/rules/code-quality/tests/Rector/ClassMethod/DateTimeToDateTimeInterfaceRector/Fixture/fixture_variable_usage_param.php.inc @@ -0,0 +1,36 @@ +modify('+1 day')); + + return $dateTime; + } +} + +?> +----- +modify('+1 day')); + + return $dateTime; + } +} + +?> diff --git a/src/ValueObject/PhpVersionFeature.php b/src/ValueObject/PhpVersionFeature.php index c5c707ac01c8..fffa75dc50e8 100644 --- a/src/ValueObject/PhpVersionFeature.php +++ b/src/ValueObject/PhpVersionFeature.php @@ -16,6 +16,8 @@ final class PhpVersionFeature */ public const ELVIS_OPERATOR = 50300; + public const DATE_TIME_INTERFACE = 50500; + /** * @var int */