Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
# CHANGELOG for PHP CS Fixer: custom fixers

## v3.25.0
- ReadonlyPromotedPropertiesFixer - support asymmetric visibility

## v3.24.0
- Add PhpUnitRequiresConstraintFixer

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
[![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)
[![PHP version](https://img.shields.io/packagist/php-v/kubawerlos/php-cs-fixer-custom-fixers.svg)](https://php.net)
[![License](https://img.shields.io/github/license/kubawerlos/php-cs-fixer-custom-fixers.svg)](LICENSE)
![Tests](https://img.shields.io/badge/tests-3610-brightgreen.svg)
![Tests](https://img.shields.io/badge/tests-3613-brightgreen.svg)
[![Downloads](https://img.shields.io/packagist/dt/kubawerlos/php-cs-fixer-custom-fixers.svg)](https://packagist.org/packages/kubawerlos/php-cs-fixer-custom-fixers)

[![CI status](https://github.com/kubawerlos/php-cs-fixer-custom-fixers/actions/workflows/ci.yaml/badge.svg)](https://github.com/kubawerlos/php-cs-fixer-custom-fixers/actions/workflows/ci.yaml)
Expand Down
69 changes: 45 additions & 24 deletions src/Fixer/ReadonlyPromotedPropertiesFixer.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,23 @@

final class ReadonlyPromotedPropertiesFixer extends AbstractFixer
{
/** @var list<int> */
private array $promotedPropertyVisibilityKinds;

public function __construct()
{
$this->promotedPropertyVisibilityKinds = [
CT::T_CONSTRUCTOR_PROPERTY_PROMOTION_PRIVATE,
CT::T_CONSTRUCTOR_PROPERTY_PROMOTION_PROTECTED,
CT::T_CONSTRUCTOR_PROPERTY_PROMOTION_PUBLIC,
];
if (\defined('T_PUBLIC_SET')) {
$this->promotedPropertyVisibilityKinds[] = \T_PUBLIC_SET;
$this->promotedPropertyVisibilityKinds[] = \T_PROTECTED_SET;
$this->promotedPropertyVisibilityKinds[] = \T_PRIVATE_SET;
}
}

public function getDefinition(): FixerDefinitionInterface
{
return new FixerDefinition(
Expand Down Expand Up @@ -53,11 +70,7 @@ public function getPriority(): int

public function isCandidate(Tokens $tokens): bool
{
return \defined('T_READONLY') && $tokens->isAnyTokenKindsFound([
CT::T_CONSTRUCTOR_PROPERTY_PROMOTION_PRIVATE,
CT::T_CONSTRUCTOR_PROPERTY_PROMOTION_PROTECTED,
CT::T_CONSTRUCTOR_PROPERTY_PROMOTION_PUBLIC,
]);
return \defined('T_READONLY') && $tokens->isAnyTokenKindsFound($this->promotedPropertyVisibilityKinds);
}

public function isRisky(): bool
Expand All @@ -83,6 +96,8 @@ public function fix(\SplFileInfo $file, Tokens $tokens): void
continue;
}

$constructorNameIndex = $tokens->getNextMeaningfulToken($constructorAnalysis->getConstructorIndex());

$classOpenBraceIndex = $tokens->getNextTokenOfKind($index, ['{']);
\assert(\is_int($classOpenBraceIndex));
$classCloseBraceIndex = $tokens->findBlockEnd(Tokens::BLOCK_TYPE_CURLY_BRACE, $classOpenBraceIndex);
Expand Down Expand Up @@ -119,34 +134,20 @@ private function fixParameters(
int $constructorCloseParenthesisIndex
): void {
for ($index = $constructorCloseParenthesisIndex; $index > $constructorOpenParenthesisIndex; $index--) {
if (
!$tokens[$index]->isGivenKind([
CT::T_CONSTRUCTOR_PROPERTY_PROMOTION_PRIVATE,
CT::T_CONSTRUCTOR_PROPERTY_PROMOTION_PROTECTED,
CT::T_CONSTRUCTOR_PROPERTY_PROMOTION_PUBLIC,
])
) {
continue;
}

$nextIndex = $tokens->getNextMeaningfulToken($index);
if ($tokens[$nextIndex]->isGivenKind(\T_READONLY)) {
if (!$tokens[$index]->isGivenKind(\T_VARIABLE)) {
continue;
}

$prevIndex = $tokens->getPrevMeaningfulToken($index);
if ($tokens[$prevIndex]->isGivenKind(\T_READONLY)) {
$insertIndex = $this->getInsertIndex($tokens, $index);
if ($insertIndex === null) {
continue;
}

$propertyIndex = $tokens->getNextTokenOfKind($index, [[\T_VARIABLE]]);
\assert(\is_int($propertyIndex));

$propertyAssignment = $tokens->findSequence(
[
[\T_VARIABLE, '$this'],
[\T_OBJECT_OPERATOR],
[\T_STRING, \substr($tokens[$propertyIndex]->getContent(), 1)],
[\T_STRING, \substr($tokens[$index]->getContent(), 1)],
],
$classOpenBraceIndex,
$classCloseBraceIndex,
Expand All @@ -156,12 +157,32 @@ private function fixParameters(
}

$tokens->insertAt(
$index + 1,
$insertIndex + 1,
[
new Token([\T_WHITESPACE, ' ']),
new Token([\T_READONLY, 'readonly']),
],
);
}
}

private function getInsertIndex(Tokens $tokens, int $index): ?int
{
$insertIndex = null;

$index = $tokens->getPrevMeaningfulToken($index);
\assert(\is_int($index));
while (!$tokens[$index]->equalsAny([',', '('])) {
$index = $tokens->getPrevMeaningfulToken($index);
\assert(\is_int($index));
if ($tokens[$index]->isGivenKind(\T_READONLY)) {
return null;
}
if ($insertIndex === null && $tokens[$index]->isGivenKind($this->promotedPropertyVisibilityKinds)) {
$insertIndex = $index;
}
}

return $insertIndex;
}
}
1 change: 1 addition & 0 deletions tests/Fixer/AbstractFixerTestCase.php
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ abstract class AbstractFixerTestCase extends TestCase
'testFix80',
'testFix81',
'testFix82',
'testFix84',
'testIsRisky',
'testReversingCodeSample',
'testStringIsTheSame',
Expand Down
83 changes: 83 additions & 0 deletions tests/Fixer/ReadonlyPromotedPropertiesFixerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -173,4 +173,87 @@ class Baz { public function __construct(public int $x) {} }
',
];
}

/**
* @dataProvider provideFix84Cases
*
* @requires PHP >= 8.4
*/
public function testFix84(string $expected, ?string $input = null): void
{
$this->doTest($expected, $input);
}

/**
* @return iterable<string, array{0: string, 1?: string}>
*/
public static function provideFix84Cases(): iterable
{
yield 'asymmetric visibility with both visibilities' => [
<<<'PHP'
<?php
final class Foo {
public function __construct(
public public(set) readonly int $a,
public protected(set) readonly int $b,
public private(set) readonly int $c,
protected protected(set) readonly int $d,
protected private(set) readonly int $e,
private private(set) readonly int $f,
) {}
}
PHP,
<<<'PHP'
<?php
final class Foo {
public function __construct(
public public(set) int $a,
public protected(set) int $b,
public private(set) int $c,
protected protected(set) int $d,
protected private(set) int $e,
private private(set) int $f,
) {}
}
PHP,
];

yield 'asymmetric visibility with only write visibility' => [
<<<'PHP'
<?php
final class Foo {
public function __construct(
public(set) readonly int $a,
protected(set) readonly int $b,
private(set) readonly int $c,
) {}
}
PHP,
<<<'PHP'
<?php
final class Foo {
public function __construct(
public(set) int $a,
protected(set) int $b,
private(set) int $c,
) {}
}
PHP,
];

yield 'readonly asymmetric visibility' => [
<<<'PHP'
<?php
final class Foo {
public function __construct(
readonly public public(set) int $a,
public readonly protected(set) int $b,
public private(set) readonly int $c,
readonly private(set) int $e,
private(set) readonly int $f,
) {}
}
PHP,
];
}
}
2 changes: 1 addition & 1 deletion tests/Readme/ReadmeCommandTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
/**
* @internal
*
* @requires PHP >= 8.2
* @requires PHP >= 8.4
*
* @covers \PhpCsFixerCustomFixersDev\Readme\ReadmeCommand
*/
Expand Down