Skip to content

Commit a1e78f0

Browse files
authored
Add ReadonlyPromotedPropertiesFixer (#786)
1 parent 5da90c8 commit a1e78f0

File tree

7 files changed

+269
-8
lines changed

7 files changed

+269
-8
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
# CHANGELOG for PHP CS Fixer: custom fixers
22

3+
## v3.11.0
4+
- Add ReadonlyPromotedPropertiesFixer
5+
36
## v3.10.0
47
- Do not require `friendsofphp/php-cs-fixer` as dependency (to allow using `php-cs-fixer/shim`)
58

README.md

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
[![Latest stable version](https://img.shields.io/packagist/v/kubawerlos/php-cs-fixer-custom-fixers.svg?label=current%20version)](https://packagist.org/packages/kubawerlos/php-cs-fixer-custom-fixers)
44
[![PHP version](https://img.shields.io/packagist/php-v/kubawerlos/php-cs-fixer-custom-fixers.svg)](https://php.net)
55
[![License](https://img.shields.io/github/license/kubawerlos/php-cs-fixer-custom-fixers.svg)](LICENSE)
6-
![Tests](https://img.shields.io/badge/tests-3422-brightgreen.svg)
6+
![Tests](https://img.shields.io/badge/tests-3450-brightgreen.svg)
77
[![Downloads](https://img.shields.io/packagist/dt/kubawerlos/php-cs-fixer-custom-fixers.svg)](https://packagist.org/packages/kubawerlos/php-cs-fixer-custom-fixers)
88

99
[![CI Status](https://github.com/kubawerlos/php-cs-fixer-custom-fixers/workflows/CI/badge.svg?branch=main)](https://github.com/kubawerlos/php-cs-fixer-custom-fixers/actions)
@@ -567,6 +567,20 @@ Configuration options:
567567
}
568568
```
569569

570+
#### ReadonlyPromotedPropertiesFixer
571+
Promoted properties must readonly.
572+
*Risky: when property is written.*
573+
```diff
574+
<?php class Foo {
575+
public function __construct(
576+
- public array $a,
577+
- public bool $b,
578+
+ public readonly array $a,
579+
+ public readonly bool $b,
580+
) {}
581+
}
582+
```
583+
570584
#### SingleSpaceAfterStatementFixer
571585
Statements not followed by a semicolon must be followed by a single space.
572586
Configuration options:

src/Fixer/PromotedConstructorPropertyFixer.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ public function configure(array $configuration): void
7878
}
7979

8080
/**
81-
* Must run before BracesFixer, ClassAttributesSeparationFixer, ConstructorEmptyBracesFixer, MultilinePromotedPropertiesFixer, NoExtraBlankLinesFixer.
81+
* Must run before BracesFixer, ClassAttributesSeparationFixer, ConstructorEmptyBracesFixer, MultilinePromotedPropertiesFixer, NoExtraBlankLinesFixer, ReadonlyPromotedPropertiesFixer.
8282
*/
8383
public function getPriority(): int
8484
{
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
<?php declare(strict_types=1);
2+
3+
/*
4+
* This file is part of PHP CS Fixer: custom fixers.
5+
*
6+
* (c) 2018 Kuba Werłos
7+
*
8+
* For the full copyright and license information, please view
9+
* the LICENSE file that was distributed with this source code.
10+
*/
11+
12+
namespace PhpCsFixerCustomFixers\Fixer;
13+
14+
use PhpCsFixer\FixerDefinition\FixerDefinition;
15+
use PhpCsFixer\FixerDefinition\FixerDefinitionInterface;
16+
use PhpCsFixer\FixerDefinition\VersionSpecification;
17+
use PhpCsFixer\FixerDefinition\VersionSpecificCodeSample;
18+
use PhpCsFixer\Tokenizer\CT;
19+
use PhpCsFixer\Tokenizer\Token;
20+
use PhpCsFixer\Tokenizer\Tokens;
21+
use PhpCsFixerCustomFixers\Analyzer\ConstructorAnalyzer;
22+
23+
final class ReadonlyPromotedPropertiesFixer extends AbstractFixer
24+
{
25+
public function getDefinition(): FixerDefinitionInterface
26+
{
27+
return new FixerDefinition(
28+
'Promoted properties must readonly.',
29+
[
30+
new VersionSpecificCodeSample(
31+
'<?php class Foo {
32+
public function __construct(
33+
public array $a,
34+
public bool $b,
35+
) {}
36+
}
37+
',
38+
new VersionSpecification(80100)
39+
),
40+
],
41+
null,
42+
'when property is written'
43+
);
44+
}
45+
46+
/**
47+
* Must run after PromotedConstructorPropertyFixer.
48+
*/
49+
public function getPriority(): int
50+
{
51+
return 0;
52+
}
53+
54+
public function isCandidate(Tokens $tokens): bool
55+
{
56+
return \defined('T_READONLY') && $tokens->isAnyTokenKindsFound([
57+
CT::T_CONSTRUCTOR_PROPERTY_PROMOTION_PRIVATE,
58+
CT::T_CONSTRUCTOR_PROPERTY_PROMOTION_PROTECTED,
59+
CT::T_CONSTRUCTOR_PROPERTY_PROMOTION_PUBLIC,
60+
]);
61+
}
62+
63+
public function isRisky(): bool
64+
{
65+
return true;
66+
}
67+
68+
public function fix(\SplFileInfo $file, Tokens $tokens): void
69+
{
70+
$constructorAnalyzer = new ConstructorAnalyzer();
71+
72+
for ($index = $tokens->count() - 1; $index > 0; $index--) {
73+
if (!$tokens[$index]->isGivenKind(\T_CLASS)) {
74+
continue;
75+
}
76+
77+
$constructorAnalysis = $constructorAnalyzer->findNonAbstractConstructor($tokens, $index);
78+
if ($constructorAnalysis === null) {
79+
continue;
80+
}
81+
82+
$openParenthesis = $tokens->getNextTokenOfKind($constructorAnalysis->getConstructorIndex(), ['(']);
83+
\assert(\is_int($openParenthesis));
84+
$closeParenthesis = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_PARENTHESIS_BRACE, $openParenthesis);
85+
86+
$this->fixParameters($tokens, $openParenthesis, $closeParenthesis);
87+
}
88+
}
89+
90+
private function fixParameters(Tokens $tokens, int $openParenthesis, int $closeParenthesis): void
91+
{
92+
for ($index = $closeParenthesis; $index > $openParenthesis; $index--) {
93+
if (
94+
!$tokens[$index]->isGivenKind([
95+
CT::T_CONSTRUCTOR_PROPERTY_PROMOTION_PRIVATE,
96+
CT::T_CONSTRUCTOR_PROPERTY_PROMOTION_PROTECTED,
97+
CT::T_CONSTRUCTOR_PROPERTY_PROMOTION_PUBLIC,
98+
])
99+
) {
100+
continue;
101+
}
102+
103+
$nextIndex = $tokens->getNextMeaningfulToken($index);
104+
if ($tokens[$nextIndex]->isGivenKind(\T_READONLY)) {
105+
continue;
106+
}
107+
108+
$prevIndex = $tokens->getPrevMeaningfulToken($index);
109+
if ($tokens[$prevIndex]->isGivenKind(\T_READONLY)) {
110+
continue;
111+
}
112+
113+
$tokens->insertAt(
114+
$index + 1,
115+
[
116+
new Token([\T_WHITESPACE, ' ']),
117+
new Token([\T_READONLY, 'readonly']),
118+
]
119+
);
120+
}
121+
}
122+
}
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
<?php declare(strict_types=1);
2+
3+
/*
4+
* This file is part of PHP CS Fixer: custom fixers.
5+
*
6+
* (c) 2018 Kuba Werłos
7+
*
8+
* For the full copyright and license information, please view
9+
* the LICENSE file that was distributed with this source code.
10+
*/
11+
12+
namespace Tests\Fixer;
13+
14+
/**
15+
* @internal
16+
*
17+
* @covers \PhpCsFixerCustomFixers\Fixer\ReadonlyPromotedPropertiesFixer
18+
*
19+
* @requires PHP 8.1
20+
*/
21+
final class ReadonlyPromotedPropertiesFixerTest extends AbstractFixerTestCase
22+
{
23+
public function testIsRisky(): void
24+
{
25+
self::assertTrue($this->fixer->isRisky());
26+
}
27+
28+
/**
29+
* @dataProvider provideFixCases
30+
*/
31+
public function testFix(string $expected, ?string $input = null): void
32+
{
33+
$this->doTest($expected, $input);
34+
}
35+
36+
/**
37+
* @return iterable<array<string>>>
38+
*/
39+
public static function provideFixCases(): iterable
40+
{
41+
yield [
42+
'<?php class Foo {
43+
public function __construct(
44+
int $x
45+
) {}
46+
}',
47+
];
48+
49+
yield [
50+
'<?php class Foo {
51+
public function __construct(
52+
public readonly int $a,
53+
protected readonly int $b,
54+
private readonly int $c,
55+
) {}
56+
}',
57+
'<?php class Foo {
58+
public function __construct(
59+
public int $a,
60+
protected int $b,
61+
private int $c,
62+
) {}
63+
}',
64+
];
65+
66+
yield [
67+
'<?php class Foo {
68+
public function __construct(
69+
public readonly int $a,
70+
readonly public int $b,
71+
public readonly int $c,
72+
readonly public int $d,
73+
public readonly int $e,
74+
readonly public int $f,
75+
public readonly int $f,
76+
) {}
77+
}',
78+
'<?php class Foo {
79+
public function __construct(
80+
public readonly int $a,
81+
readonly public int $b,
82+
public int $c,
83+
readonly public int $d,
84+
public int $e,
85+
readonly public int $f,
86+
public readonly int $f,
87+
) {}
88+
}',
89+
];
90+
yield [
91+
'<?php
92+
class Foo { public function __construct(public readonly int $x) {} }
93+
class Bar { public function notConstruct(public int $x) {} }
94+
class Baz { public function __construct(public readonly int $x) {} }
95+
',
96+
'<?php
97+
class Foo { public function __construct(public int $x) {} }
98+
class Bar { public function notConstruct(public int $x) {} }
99+
class Baz { public function __construct(public int $x) {} }
100+
',
101+
];
102+
}
103+
}

tests/PriorityTest.php

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,15 +25,15 @@
2525
* @internal
2626
*
2727
* @coversNothing
28+
*
29+
* @requires PHP 8.1
2830
*/
2931
final class PriorityTest extends TestCase
3032
{
3133
use AssertSameTokensTrait;
3234

3335
/**
3436
* @dataProvider providePriorityCases
35-
*
36-
* @requires PHP 8.0
3737
*/
3838
public function testPriorities(FixerInterface $firstFixer, FixerInterface $secondFixer): void
3939
{
@@ -42,8 +42,6 @@ public function testPriorities(FixerInterface $firstFixer, FixerInterface $secon
4242

4343
/**
4444
* @dataProvider providePriorityCases
45-
*
46-
* @requires PHP 8.0
4745
*/
4846
public function testInOrder(FixerInterface $firstFixer, FixerInterface $secondFixer, string $expected, string $input): void
4947
{
@@ -64,8 +62,6 @@ public function testInOrder(FixerInterface $firstFixer, FixerInterface $secondFi
6462

6563
/**
6664
* @dataProvider providePriorityCases
67-
*
68-
* @requires PHP 8.0
6965
*/
7066
public function testInRevertedOrder(FixerInterface $firstFixer, FixerInterface $secondFixer, string $expected, string $input): void
7167
{
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
--CONFIGURATION--
2+
{ "PhpCsFixerCustomFixers/promoted_constructor_property": true, "PhpCsFixerCustomFixers/readonly_promoted_properties": true }
3+
--EXPECTED--
4+
<?php class Foo {
5+
public function __construct(
6+
private readonly int $x,
7+
private readonly int $y,
8+
) {
9+
}
10+
}
11+
12+
--INPUT--
13+
<?php class Foo {
14+
private int $x;
15+
private int $y;
16+
public function __construct(
17+
int $x,
18+
int $y,
19+
) {
20+
$this->x = $x;
21+
$this->y = $y;
22+
}
23+
}

0 commit comments

Comments
 (0)