Skip to content

Commit

Permalink
[DowngradePhp82] Add DowngradeReadonlyClassRector to downgrade readon…
Browse files Browse the repository at this point in the history
…ly class (#2322)

* [DowngradePhp82] Add DowngradeReadonlyClassRector to downgrade final readonly class

* apply readonly property promotion

* fixture

* phpstan

* [ci-review] Rector Rectify

* add DowngradeLevelSetList::DOWN_TO_PHP_81

* add test to ensure no flip-flop between add and removing readonly when downgrade to php 8.0

* more test

* more test

* combination test

* final touch: typo fix

* not property promotion

* static properties cannot be readonly

* static properties cannot be readonly

* note

* note

* note

* test

Co-authored-by: GitHub Action <action@github.com>
  • Loading branch information
samsonasik and actions-user committed May 31, 2022
1 parent f01725a commit b48271e
Show file tree
Hide file tree
Showing 19 changed files with 441 additions and 3 deletions.
12 changes: 12 additions & 0 deletions config/set/downgrade-php82.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?php

declare(strict_types=1);

use Rector\Config\RectorConfig;
use Rector\Core\ValueObject\PhpVersion;
use Rector\DowngradePhp82\Rector\Class_\DowngradeReadonlyClassRector;

return static function (RectorConfig $rectorConfig): void {
$rectorConfig->phpVersion(PhpVersion::PHP_81);
$rectorConfig->rule(DowngradeReadonlyClassRector::class);
};
3 changes: 2 additions & 1 deletion config/set/level/down-to-php80.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@
declare(strict_types=1);

use Rector\Config\RectorConfig;
use Rector\Set\ValueObject\DowngradeLevelSetList;
use Rector\Set\ValueObject\DowngradeSetList;

return static function (RectorConfig $rectorConfig): void {
$rectorConfig->sets([DowngradeSetList::PHP_81]);
$rectorConfig->sets([DowngradeLevelSetList::DOWN_TO_PHP_81, DowngradeSetList::PHP_81]);
};
10 changes: 10 additions & 0 deletions config/set/level/down-to-php81.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?php

declare(strict_types=1);

use Rector\Config\RectorConfig;
use Rector\Set\ValueObject\DowngradeSetList;

return static function (RectorConfig $rectorConfig): void {
$rectorConfig->sets([DowngradeSetList::PHP_82]);
};
5 changes: 5 additions & 0 deletions packages/Set/ValueObject/DowngradeLevelSetList.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@

final class DowngradeLevelSetList implements SetListInterface
{
/**
* @var string
*/
public const DOWN_TO_PHP_81 = __DIR__ . '/../../../config/set/level/down-to-php81.php';

/**
* @var string
*/
Expand Down
5 changes: 5 additions & 0 deletions packages/Set/ValueObject/DowngradeSetList.php
Original file line number Diff line number Diff line change
Expand Up @@ -62,4 +62,9 @@ final class DowngradeSetList implements SetListInterface
* @var string
*/
public const PHP_81 = __DIR__ . '/../../../config/set/downgrade-php81.php';

/**
* @var string
*/
public const PHP_82 = __DIR__ . '/../../../config/set/downgrade-php82.php';
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?php

declare(strict_types=1);

namespace Rector\Tests\DowngradePhp82\Rector\Class_\DowngradeReadonlyClassRector;

use Iterator;
use Rector\Testing\PHPUnit\AbstractRectorTestCase;
use Symplify\SmartFileSystem\SmartFileInfo;

final class DowngradeReadonlyClassRectorTest extends AbstractRectorTestCase
{
/**
* @dataProvider provideData()
*/
public function test(SmartFileInfo $fileInfo): void
{
$this->doTestFileInfo($fileInfo);
}

/**
* @return Iterator<SmartFileInfo>
*/
public function provideData(): Iterator
{
return $this->yieldFilesFromDirectory(__DIR__ . '/Fixture');
}

public function provideConfigFilePath(): string
{
return __DIR__ . '/config/configured_rule.php';
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<?php

namespace Rector\Tests\DowngradePhp82\Rector\Class_\DowngradeReadonlyClassRector\Fixture;

final readonly class CombinePropertyAndPropertyPromotion
{
private string $foo;

public function __construct(private string $bar)
{
}
}

?>
-----
<?php

namespace Rector\Tests\DowngradePhp82\Rector\Class_\DowngradeReadonlyClassRector\Fixture;

final class CombinePropertyAndPropertyPromotion
{
private readonly string $foo;

public function __construct(private readonly string $bar)
{
}
}

?>
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<?php

namespace Rector\Tests\DowngradePhp82\Rector\Class_\DowngradeReadonlyClassRector\Fixture;

final readonly class NotPropertyPromotion
{
public function __construct(string $bar)
{
}
}

?>
-----
<?php

namespace Rector\Tests\DowngradePhp82\Rector\Class_\DowngradeReadonlyClassRector\Fixture;

final class NotPropertyPromotion
{
public function __construct(string $bar)
{
}
}

?>
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php

namespace Rector\Tests\DowngradePhp82\Rector\Class_\DowngradeReadonlyClassRector\Fixture;

final readonly class ReadonlyClass
{
public string $foo;
}

?>
-----
<?php

namespace Rector\Tests\DowngradePhp82\Rector\Class_\DowngradeReadonlyClassRector\Fixture;

final class ReadonlyClass
{
public readonly string $foo;
}

?>
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<?php

namespace Rector\Tests\DowngradePhp82\Rector\Class_\DowngradeReadonlyClassRector\Fixture;

final readonly class ReadonlyClassWithPropertyPromotion
{
public function __construct(private string $foo)
{
}
}

?>
-----
<?php

namespace Rector\Tests\DowngradePhp82\Rector\Class_\DowngradeReadonlyClassRector\Fixture;

final class ReadonlyClassWithPropertyPromotion
{
public function __construct(private readonly string $foo)
{
}
}

?>
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?php

namespace Rector\Tests\DowngradePhp82\Rector\Class_\DowngradeReadonlyClassRector\Fixture;

class SkipNotReadonly
{
public readonly string $foo;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php

namespace Rector\Tests\DowngradePhp82\Rector\Class_\DowngradeReadonlyClassRector\Fixture;

final readonly class StaticProperty
{
public static string $foo;
}

?>
-----
<?php

namespace Rector\Tests\DowngradePhp82\Rector\Class_\DowngradeReadonlyClassRector\Fixture;

final class StaticProperty
{
public static string $foo;
}

?>
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?php

declare(strict_types=1);

use Rector\Config\RectorConfig;
use Rector\DowngradePhp82\Rector\Class_\DowngradeReadonlyClassRector;

return static function (RectorConfig $rectorConfig): void {
$rectorConfig->rule(DowngradeReadonlyClassRector::class);
};
141 changes: 141 additions & 0 deletions rules/DowngradePhp82/Rector/Class_/DowngradeReadonlyClassRector.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
<?php

declare(strict_types=1);

namespace Rector\DowngradePhp82\Rector\Class_;

use PhpParser\Node;
use PhpParser\Node\Stmt\Class_;
use PhpParser\Node\Stmt\ClassMethod;
use Rector\Core\Rector\AbstractRector;
use Rector\Core\ValueObject\MethodName;
use Rector\Privatization\NodeManipulator\VisibilityManipulator;
use Symplify\RuleDocGenerator\ValueObject\CodeSample\CodeSample;
use Symplify\RuleDocGenerator\ValueObject\RuleDefinition;

/**
* @changelog https://wiki.php.net/rfc/readonly_classes
*
* @see \Rector\Tests\DowngradePhp82\Rector\Class_\DowngradeReadonlyClassRector\DowngradeReadonlyClassRectorTest
*/
final class DowngradeReadonlyClassRector extends AbstractRector
{
public function __construct(private readonly VisibilityManipulator $visibilityManipulator)
{
}

/**
* @return array<class-string<Node>>
*/
public function getNodeTypes(): array
{
return [Class_::class];
}

public function getRuleDefinition(): RuleDefinition
{
return new RuleDefinition(
'Remove "readonly" class type, decorate all properties to "readonly"',
[
new CodeSample(
<<<'CODE_SAMPLE'
final readonly class SomeClass
{
public string $foo;
public function __construct()
{
$this->foo = 'foo';
}
}
CODE_SAMPLE
,
<<<'CODE_SAMPLE'
final readonly class SomeClass
{
public readonly string $foo;
public function __construct()
{
$this->foo = 'foo';
}
}
CODE_SAMPLE
),
]
);
}

/**
* @param Class_ $node
*/
public function refactor(Node $node): ?Node
{
if (! $this->visibilityManipulator->isReadonly($node)) {
return null;
}

$this->visibilityManipulator->removeReadonly($node);
$this->makePropertiesReadonly($node);
$this->makePromotedPropertiesReadonly($node);

return $node;
}

private function makePropertiesReadonly(Class_ $class): void
{
foreach ($class->getProperties() as $property) {
if ($property->isReadonly()) {
continue;
}

/**
* It technically impossible that readonly class has:
*
* - non-typed property
* - static property
*
* but here to ensure no flip-flop when using direct rule for multiple rules applied
*/
if ($property->type === null) {
continue;
}

if ($property->isStatic()) {
continue;
}

$this->visibilityManipulator->makeReadonly($property);
}
}

private function makePromotedPropertiesReadonly(Class_ $class): void
{
$classMethod = $class->getMethod(MethodName::CONSTRUCT);
if (! $classMethod instanceof ClassMethod) {
return;
}

foreach ($classMethod->getParams() as $param) {
if ($this->visibilityManipulator->isReadonly($param)) {
continue;
}

/**
* not property promotion, just param
*/
if ($param->flags === 0) {
continue;
}

/**
* also not typed, just param
*/
if ($param->type === null) {
continue;
}

$this->visibilityManipulator->makeReadonly($param);
}
}
}
4 changes: 2 additions & 2 deletions rules/Privatization/NodeManipulator/VisibilityManipulator.php
Original file line number Diff line number Diff line change
Expand Up @@ -122,12 +122,12 @@ public function makeReadonly(Class_ | Property | Param $node): void
$this->addVisibilityFlag($node, Visibility::READONLY);
}

public function isReadonly(Property | Param $node): bool
public function isReadonly(Class_ | Property | Param $node): bool
{
return $this->hasVisibility($node, Visibility::READONLY);
}

public function removeReadonly(Property | Param $node): void
public function removeReadonly(Class_ | Property | Param $node): void
{
$this->removeVisibilityFlag($node, Visibility::READONLY);
}
Expand Down

0 comments on commit b48271e

Please sign in to comment.