Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
9621a2e
[Php82] Add ReadOnlyClassRector
samsonasik May 12, 2022
9aedda7
skip has non-readonly property
samsonasik May 12, 2022
458e0ab
note
samsonasik May 12, 2022
d695419
note
samsonasik May 12, 2022
fd38da9
note
samsonasik May 12, 2022
ea45914
[ci-review] Rector Rectify
actions-user May 12, 2022
e7ff154
skip has AllowDynamicProperties attribute
samsonasik May 12, 2022
ddaec2f
skip already readonly
samsonasik May 12, 2022
9d62247
[ci-review] Rector Rectify
actions-user May 12, 2022
c3bab83
skip property promotion not readonly
samsonasik May 12, 2022
8892d68
no params means no property promotion, skip if no property defined
samsonasik May 12, 2022
bb3e69b
note
samsonasik May 12, 2022
ec62cff
note
samsonasik May 12, 2022
d110c9d
skip final class, possibly extendable
samsonasik May 12, 2022
f4cae77
add fixture
samsonasik May 12, 2022
2252bb1
add @see
samsonasik May 12, 2022
c718d39
visibility union ndoe rules/Privatization/NodeManipulator/VisibilityM…
samsonasik May 12, 2022
01f8f5d
visibility union ndoe rules/Privatization/NodeManipulator/VisibilityM…
samsonasik May 12, 2022
857bf25
remove already readonly fixture
samsonasik May 12, 2022
1dc02f0
comment
samsonasik May 12, 2022
1e57d14
skip anonymous class fixture
samsonasik May 12, 2022
f01e401
skip non-final class fixture
samsonasik May 12, 2022
ff9935a
skip allow dynamic fixture
samsonasik May 12, 2022
43f765a
class check
samsonasik May 12, 2022
2636f90
skip has writable property fixture
samsonasik May 12, 2022
31c4464
skip no properties
samsonasik May 12, 2022
e063a52
skip property promotion writable
samsonasik May 12, 2022
4c6db88
debug
samsonasik May 12, 2022
605a1be
[ci-review] Rector Rectify
actions-user May 12, 2022
a13123f
fix
samsonasik May 12, 2022
6ff7f94
eol
samsonasik May 12, 2022
322c13d
fix
samsonasik May 12, 2022
a9de1ae
[ci-review] Rector Rectify
actions-user May 12, 2022
5d36a73
final touch: add up-to-php82 level setlist
samsonasik May 12, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)
{
}
}

?>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There should be one more fixture with one readonly property and one normal one

Copy link
Member Author

@samsonasik samsonasik May 12, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That willl be skipped, i will add more fixture

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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;
}
Comment on lines +1 to +8
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about public readonly combination?

Copy link
Member Author

@samsonasik samsonasik May 12, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That willl be skipped for writable+readonly, i will add more fixture

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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