Created
April 28, 2025 06:46
-
-
Save mortenscheel/7b90133ba7432ad99dadda70a6d63f80 to your computer and use it in GitHub Desktop.
The purpose of this Rector is to refactor a code base using Carbon V2 to V3 without changing behavior.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
declare(strict_types=1); | |
namespace Dev\Rector; | |
use Carbon\Carbon; | |
use Carbon\CarbonInterface; | |
use PhpParser\Node; | |
use PhpParser\Node\Arg; | |
use PhpParser\Node\Expr\Cast\Int_ as IntCast; | |
use PhpParser\Node\Expr\MethodCall; | |
use PhpParser\Node\Identifier; | |
use PhpParser\Node\Scalar\String_; | |
use PHPStan\Type\MixedType; | |
use PHPStan\Type\ObjectType; | |
use PHPStan\Type\StringType; | |
use Rector\Rector\AbstractRector; | |
use Symplify\RuleDocGenerator\ValueObject\RuleDefinition; | |
class CarbonV3UpgradeRector extends AbstractRector | |
{ | |
private const diffMethodNames = [ | |
'diffInSeconds', | |
'diffInMinutes', | |
'diffInHours', | |
'diffInDays', | |
'diffInWeekdays', | |
'diffInWeeks', | |
'diffInMonths', | |
'diffInYears', | |
]; | |
private const isSameMethodNames = [ | |
'isSameSecond', | |
'isSameMinute', | |
'isSameHour', | |
'isSameDay', | |
'isSameWeek', | |
'isSameMonth', | |
'isSameYear', | |
'isSameDecade', | |
]; | |
private const addSubtractMethodNames = [ | |
'addSeconds', | |
'addMinutes', | |
'addHours', | |
'addDays', | |
'addWeeks', | |
'addMonths', | |
'addYears', | |
'subSeconds', | |
'subMinutes', | |
'subHours', | |
'subDays', | |
'subWeeks', | |
'subMonths', | |
'subYears', | |
]; | |
/** | |
* @return array<class-string<Node>> | |
*/ | |
public function getNodeTypes(): array | |
{ | |
return [Node\Expr\MethodCall::class, Node\Expr\StaticCall::class]; | |
} | |
public function refactor(Node $node): ?Node | |
{ | |
if ($node instanceof Node\Expr\StaticCall) { | |
return $this->refactorStaticCall($node); | |
} | |
/** @var MethodCall $node */ | |
// Only process method calls on Carbon objects | |
$calledType = $this->getType($node->var); | |
if (!$calledType->isSuperTypeOf(new ObjectType(CarbonInterface::class))) { | |
return null; | |
} | |
if ($this->isNames($node->name, self::diffMethodNames)) { | |
return $this->refactorDiffMethodCall($node); | |
} | |
if ($this->isNames($node->name, self::isSameMethodNames)) { | |
return $this->refactorIsSameMethodCall($node); | |
} | |
if ($this->isNames($node->name, self::addSubtractMethodNames)) { | |
return $this->refactorAddOrSubtractUnitsMethodCall($node); | |
} | |
return null; | |
} | |
private function hasCallArgument(int $index, string $name, MethodCall $node): bool | |
{ | |
foreach ($node->args as $arg) { | |
if ($arg instanceof Arg && $arg->name && $this->isName($arg->name, $name)) { | |
return true; | |
} | |
} | |
return isset($node->args[$index]); | |
} | |
public function getRuleDefinition(): RuleDefinition | |
{ | |
/** @noinspection PhpUnhandledExceptionInspection */ | |
return new RuleDefinition('', []); | |
} | |
/** | |
* - default for `absolute` was changed from true to false, so add `absolute: true` unless explicitly set already | |
* - return value was changed from int to float. add int cast if necessary | |
*/ | |
private function refactorDiffMethodCall(MethodCall|Node $node): ?Node | |
{ | |
if (!$this->hasCallArgument(1, 'absolute', $node)) { | |
$arg = new Arg(new Node\Expr\ConstFetch(new Node\Name('true'))); | |
$arg->name = new Identifier('absolute'); | |
$node->args[] = $arg; | |
} | |
return new IntCast($node); | |
} | |
/** | |
* isSame* methods can no longer be called without arguments | |
*/ | |
private function refactorIsSameMethodCall(MethodCall $node): ?Node | |
{ | |
if (!$this->hasCallArgument(0, 'date', $node)) { | |
$node->args[] = $this->nodeFactory->createArg(new Node\Expr\FuncCall(new Node\Name('now'))); | |
return $node; | |
} | |
return null; | |
} | |
/** | |
* Argument must now be int or float (or no argument). Cast possible string arguments | |
*/ | |
private function refactorAddOrSubtractUnitsMethodCall(MethodCall $node): ?Node | |
{ | |
if (!isset($node->args[0])) { | |
// It's still okay to call e.g. addDays() without arguments (default is 1) | |
return null; | |
} | |
$arg = $node->args[0]; | |
$argType = $this->nodeTypeResolver->getType($arg->value); | |
if ($argType instanceof StringType || $argType instanceof MixedType) { | |
$node->args[0] = $this->nodeFactory->createArg(new Node\Expr\Cast\Double($arg->value, ['kind' => Node\Expr\Cast\Double::KIND_FLOAT])); | |
return $node; | |
} | |
return null; | |
} | |
/** | |
* Carbon::createFromTimestamp() now uses UTC as default timezone (previously it used php's cofigured timezone). | |
* Add "Europe/Copenhagen" as second argument if there's only one argument.' | |
*/ | |
private function refactorStaticCall(Node\Expr\StaticCall $node): ?Node | |
{ | |
// Check if this is a call to \Carbon\Carbon::createFromTimestamp() | |
if (!$this->isName($node->name, 'createFromTimestamp') || !$this->isName($node->class, Carbon::class)) { | |
return null; | |
} | |
// If there's only one argument (the timestamp), add the timezone | |
if (\count($node->args) === 1) { | |
$node->args[] = $this->nodeFactory->createArg(new String_('Europe/Copenhagen')); | |
return $node; | |
} | |
return null; | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
declare(strict_types=1); | |
namespace Dev\Rector; | |
use PhpParser\Node; | |
use Rector\Rector\AbstractRector; | |
use Symplify\RuleDocGenerator\ValueObject\RuleDefinition; | |
class RemoveIdenticalConsecutiveCastsRector extends AbstractRector | |
{ | |
/** | |
* @return array<class-string<Node>> | |
*/ | |
public function getNodeTypes(): array | |
{ | |
return [Node\Expr\Cast::class]; | |
} | |
/** | |
* @param Node\Expr\Cast $node | |
*/ | |
public function refactor(Node $node): ?Node | |
{ | |
$wasChanged = false; | |
while ($node::class === $node->expr::class) { | |
$node = $node->expr; | |
$wasChanged = true; | |
} | |
return $wasChanged ? $node : null; | |
} | |
public function getRuleDefinition(): RuleDefinition | |
{ | |
/** @noinspection PhpUnhandledExceptionInspection */ | |
return new RuleDefinition('', []); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
See the docs for a description of all breaking changes in Carbon v3.
Note: The
diffIn*
methods returnfloat
in Carbon 3, so this rector adds an(int)
cast to those method calls to avoid changing return types. I was unable to detect if an int cast was already applied, so the rector will add ´(int)cast every time it's run. To remove consecutive casts, use the
RemoveIdenticalConsecutiveCastsRector`.