Skip to content

Commit

Permalink
[Php82] Add ReadOnlyClassRector (#2296)
Browse files Browse the repository at this point in the history
* [Php82] Add ReadOnlyClassRector

* skip has non-readonly property

* note

* note

* note

* [ci-review] Rector Rectify

* skip has AllowDynamicProperties attribute

* skip already readonly

* [ci-review] Rector Rectify

* skip property promotion not readonly

* no params means no property promotion, skip if no property defined

* note

* note

* skip final class, possibly extendable

* add fixture

* add @see

* visibility union ndoe rules/Privatization/NodeManipulator/VisibilityManipulator.php

* visibility union ndoe rules/Privatization/NodeManipulator/VisibilityManipulator.php

* remove already readonly fixture

* comment

* skip anonymous class fixture

* skip non-final class fixture

* skip allow dynamic fixture

* class check

* skip has writable property fixture

* skip no properties

* skip property promotion writable

* debug

* [ci-review] Rector Rectify

* fix

* eol

* fix

* [ci-review] Rector Rectify

* final touch: add up-to-php82 level setlist

Co-authored-by: GitHub Action <action@github.com>
  • Loading branch information
samsonasik and actions-user committed May 12, 2022
1 parent 93cf392 commit 78aaf7e
Show file tree
Hide file tree
Showing 18 changed files with 363 additions and 2 deletions.
15 changes: 15 additions & 0 deletions config/set/level/up-to-php82.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?php

declare(strict_types=1);

use Rector\Config\RectorConfig;
use Rector\Core\ValueObject\PhpVersion;
use Rector\Set\ValueObject\LevelSetList;
use Rector\Set\ValueObject\SetList;

return static function (RectorConfig $rectorConfig): void {
$rectorConfig->sets([SetList::PHP_82, LevelSetList::UP_TO_PHP_81]);

// parameter must be defined after import, to override imported param version
$rectorConfig->phpVersion(PhpVersion::PHP_82);
};
10 changes: 10 additions & 0 deletions config/set/php82.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\Php82\Rector\Class_\ReadOnlyClassRector;

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

final class LevelSetList implements SetListInterface
{
/**
* @var string
*/
public const UP_TO_PHP_82 = __DIR__ . '/../../../config/set/level/up-to-php82.php';

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

/**
* @var string
*/
public const PHP_82 = __DIR__ . '/../../../config/set/php82.php';

/**
* @var string
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php

namespace Rector\Tests\Php82\Rector\Class_\ReadOnlyClassRector\Fixture;

final class OnlyReadonlyProperty
{
private readonly string $property;
}

?>
-----
<?php

namespace Rector\Tests\Php82\Rector\Class_\ReadOnlyClassRector\Fixture;

final readonly class OnlyReadonlyProperty
{
private string $property;
}

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

namespace Rector\Tests\Php82\Rector\Class_\ReadOnlyClassRector\Fixture;

final class OnlyReadonlyProperty2
{
public function __construct(private readonly string $property)
{
}
}

?>
-----
<?php

namespace Rector\Tests\Php82\Rector\Class_\ReadOnlyClassRector\Fixture;

final readonly class OnlyReadonlyProperty2
{
public function __construct(private string $property)
{
}
}

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

namespace Rector\Tests\Php82\Rector\Class_\ReadOnlyClassRector\Fixture;

#[\AllowDynamicProperties]
final class SkipAllowDynamic
{
private readonly string $property;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php

namespace Rector\Tests\Php82\Rector\Class_\ReadOnlyClassRector\Fixture;

class SkipAnonymousClass
{
public function run()
{
new class {
private readonly string $foo;
};
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?php

namespace Rector\Tests\Php82\Rector\Class_\ReadOnlyClassRector\Fixture;

final class SkipHasWritableProperty
{
private string $property;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<?php

namespace Rector\Tests\Php82\Rector\Class_\ReadOnlyClassRector\Fixture;

final class SkipNoProperties
{
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?php

namespace Rector\Tests\Php82\Rector\Class_\ReadOnlyClassRector\Fixture;

final class SkipNoProperties2
{
public function __construct()
{
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?php

namespace Rector\Tests\Php82\Rector\Class_\ReadOnlyClassRector\Fixture;

class SkipNonFinalClass
{
private readonly string $property;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?php

namespace Rector\Tests\Php82\Rector\Class_\ReadOnlyClassRector\Fixture;

final class SkipPropertyPromotionWritable
{
public function __construct(private string $data)
{
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?php

declare(strict_types=1);

namespace Rector\Tests\Php82\Rector\Class_\ReadOnlyClassRector;

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

final class ReadOnlyClassRectorTest 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,10 @@
<?php

declare(strict_types=1);

use Rector\Config\RectorConfig;
use Rector\Php82\Rector\Class_\ReadOnlyClassRector;

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

declare(strict_types=1);

namespace Rector\Php82\Rector\Class_;

use PhpParser\Node;
use PhpParser\Node\Stmt\Class_;
use PhpParser\Node\Stmt\ClassMethod;
use PhpParser\Node\Stmt\Property;
use Rector\Core\NodeAnalyzer\ClassAnalyzer;
use Rector\Core\Rector\AbstractRector;
use Rector\Core\ValueObject\MethodName;
use Rector\Core\ValueObject\PhpVersionFeature;
use Rector\Core\ValueObject\Visibility;
use Rector\Php80\NodeAnalyzer\PhpAttributeAnalyzer;
use Rector\Privatization\NodeManipulator\VisibilityManipulator;
use Rector\VersionBonding\Contract\MinPhpVersionInterface;
use Symplify\RuleDocGenerator\ValueObject\CodeSample\CodeSample;
use Symplify\RuleDocGenerator\ValueObject\RuleDefinition;

/**
* @changelog https://wiki.php.net/rfc/readonly_classes
*
* @see \Rector\Tests\Php82\Rector\Class_\ReadOnlyClassRector\ReadOnlyClassRectorTest
*/
final class ReadOnlyClassRector extends AbstractRector implements MinPhpVersionInterface
{
/**
* @var string
*/
private const ATTRIBUTE = 'AllowDynamicProperties';

public function __construct(
private readonly ClassAnalyzer $classAnalyzer,
private readonly VisibilityManipulator $visibilityManipulator,
private readonly PhpAttributeAnalyzer $phpAttributeAnalyzer
) {
}

public function getRuleDefinition(): RuleDefinition
{
return new RuleDefinition('Decorate read-only class with `readonly` attribute', [
new CodeSample(
<<<'CODE_SAMPLE'
final class SomeClass
{
public function __construct(
private readonly string $name
) {
}
}
CODE_SAMPLE

,
<<<'CODE_SAMPLE'
final readonly class SomeClass
{
public function __construct(
private string $name
) {
}
}
CODE_SAMPLE
),
]);
}

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

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

$this->visibilityManipulator->makeReadonly($node);

$constructClassMethod = $node->getMethod(MethodName::CONSTRUCT);
if ($constructClassMethod instanceof ClassMethod) {
foreach ($constructClassMethod->getParams() as $param) {
$this->visibilityManipulator->removeReadonly($param);
}
}

foreach ($node->getProperties() as $property) {
$this->visibilityManipulator->removeReadonly($property);
}

return $node;
}

public function provideMinPhpVersion(): int
{
return PhpVersionFeature::READONLY_CLASS;
}

private function shouldSkip(Class_ $class): bool
{
// need to have test fixture once feature added to nikic/PHP-Parser
if ($this->visibilityManipulator->hasVisibility($class, Visibility::READONLY)) {
return true;
}

if ($this->classAnalyzer->isAnonymousClass($class)) {
return true;
}

if (! $class->isFinal()) {
return true;
}

if ($this->phpAttributeAnalyzer->hasPhpAttribute($class, self::ATTRIBUTE)) {
return true;
}

$properties = $class->getProperties();
if ($this->hasWritableProperty($properties)) {
return true;
}

$constructClassMethod = $class->getMethod(MethodName::CONSTRUCT);
if (! $constructClassMethod instanceof ClassMethod) {
// no __construct means no property promotion, skip if class has no property defined
return $properties === [];
}

$params = $constructClassMethod->getParams();
if ($params === []) {
// no params means no property promotion, skip if class has no property defined
return $properties === [];
}

foreach ($params as $param) {
// has non-property promotion, skip
if (! $this->visibilityManipulator->hasVisibility($param, Visibility::READONLY)) {
return true;
}
}

return false;
}

/**
* @param Property[] $properties
*/
private function hasWritableProperty(array $properties): bool
{
foreach ($properties as $property) {
if (! $property->isReadonly()) {
return true;
}
}

return false;
}
}
Loading

0 comments on commit 78aaf7e

Please sign in to comment.