Skip to content

Commit

Permalink
PHP 8.2: support readonly classes (#23)
Browse files Browse the repository at this point in the history
  • Loading branch information
Slamdunk committed May 24, 2023
1 parent d6b2b6d commit 3866e19
Show file tree
Hide file tree
Showing 7 changed files with 70 additions and 35 deletions.
8 changes: 4 additions & 4 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,12 @@
"php": "~8.1.0 || ~8.2.0",
"ext-mbstring": "*",
"ext-tokenizer": "*",
"friendsofphp/php-cs-fixer": "^3.16"
"friendsofphp/php-cs-fixer": "^3.17"
},
"require-dev": {
"phpstan/phpstan": "^1.10.11",
"phpstan/phpstan-phpunit": "^1.3.11",
"phpunit/phpunit": "^10.0.19",
"phpstan/phpstan": "^1.10.15",
"phpstan/phpstan-phpunit": "^1.3.12",
"phpunit/phpunit": "^10.1.3",
"slam/php-debug-r": "^1.8.0",
"slam/phpstan-extensions": "^6.0.0"
},
Expand Down
5 changes: 2 additions & 3 deletions lib/Config.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,11 @@ final class Config extends PhpCsFixerConfig
{
public const RULES = [
'@DoctrineAnnotation' => true,
'@PhpCsFixer' => true,
'@PhpCsFixer:risky' => true,
'@PHP80Migration:risky' => true,
'@PHP81Migration' => true,
'@PHPUnit84Migration:risky' => true,
'@PhpCsFixer' => true,
'@PhpCsFixer:risky' => true,
'Slam/final_abstract_public' => true,
'Slam/final_internal_class' => true,
'Slam/function_reference_space' => true,
Expand Down Expand Up @@ -65,7 +65,6 @@ final class Config extends PhpCsFixerConfig
// 'psr0' => true,
'random_api_migration' => true,
'regular_callable_call' => true,
'self_static_accessor' => true,
'simple_to_complex_string_variable' => false,
'simplified_if_return' => true,
'simplified_null_return' => false,
Expand Down
5 changes: 5 additions & 0 deletions lib/FinalInternalClassFixer.php
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,11 @@ protected function applyFix(SplFileInfo $file, Tokens $tokens): void
$classes = \array_keys($tokens->findGivenKind(\T_CLASS));

while ($classIndex = \array_pop($classes)) {
$prevTokenIndex = $tokens->getPrevMeaningfulToken($classIndex);
if (\defined('T_READONLY') && $tokens[$prevTokenIndex]->isGivenKind([\T_READONLY])) {
$classIndex = $prevTokenIndex;
}

// ignore class if it is abstract or already final
$prevToken = $tokens[$tokens->getPrevMeaningfulToken($classIndex)];
if ($prevToken->isGivenKind([\T_ABSTRACT, \T_FINAL, \T_NEW])) {
Expand Down
8 changes: 5 additions & 3 deletions phpunit.xml
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,6 @@
cacheDirectory=".phpunit.cache"
>
<coverage>
<include>
<directory suffix=".php">./lib</directory>
</include>
<report>
<text outputFile="php://stdout" showOnlySummary="true"/>
</report>
Expand All @@ -19,4 +16,9 @@
<testsuite name="SlamCsFixer">
<directory>./tests</directory>
</testsuite>
<source>
<include>
<directory>./lib</directory>
</include>
</source>
</phpunit>
30 changes: 14 additions & 16 deletions tests/AbstractFixerTestCase.php
Original file line number Diff line number Diff line change
Expand Up @@ -64,31 +64,29 @@ final protected function doTest(string $expected, ?string $input = null, ?SplFil
$fileIsSupported = $this->fixer->supports($file);

if (null !== $input) {
static::assertNull($this->lintSource($input));
self::assertNull($this->lintSource($input));

Tokens::clearCache();
$tokens = Tokens::fromCode($input);

if ($fileIsSupported) {
static::assertTrue($this->fixer->isCandidate($tokens), 'Fixer must be a candidate for input code.');
static::assertFalse($tokens->isChanged(), 'Fixer must not touch Tokens on candidate check.');
self::assertTrue($this->fixer->isCandidate($tokens), 'Fixer must be a candidate for input code.');
self::assertFalse($tokens->isChanged(), 'Fixer must not touch Tokens on candidate check.');
$this->fixer->fix($file, $tokens);
}

static::assertSame(
self::assertSame(
$expected,
$tokens->generateCode(),
'Code build on input code must match expected code.'
);
static::assertTrue($tokens->isChanged(), 'Tokens collection built on input code must be marked as changed after fixing.');
self::assertTrue($tokens->isChanged(), 'Tokens collection built on input code must be marked as changed after fixing.');

$tokens->clearEmptyTokens();

static::assertSame(
self::assertSame(
\count($tokens),
\count(\array_unique(\array_map(static function (Token $token) {
return \spl_object_hash($token);
}, $tokens->toArray()))),
\count(\array_unique(\array_map(static fn (Token $token) => \spl_object_hash($token), $tokens->toArray()))),
'Token items inside Tokens collection must be unique.'
);

Expand All @@ -97,7 +95,7 @@ final protected function doTest(string $expected, ?string $input = null, ?SplFil
self::assertTokens($expectedTokens, $tokens);
}

static::assertNull($this->lintSource($expected));
self::assertNull($this->lintSource($expected));

Tokens::clearCache();
$tokens = Tokens::fromCode($expected);
Expand All @@ -106,12 +104,12 @@ final protected function doTest(string $expected, ?string $input = null, ?SplFil
$this->fixer->fix($file, $tokens);
}

static::assertSame(
self::assertSame(
$expected,
$tokens->generateCode(),
'Code build on expected code must not change.'
);
static::assertFalse($tokens->isChanged(), 'Tokens collection built on expected code must not be marked as changed after fixing.');
self::assertFalse($tokens->isChanged(), 'Tokens collection built on expected code must not be marked as changed after fixing.');
}

private function lintSource(string $source): ?string
Expand All @@ -129,18 +127,18 @@ private static function assertTokens(Tokens $expectedTokens, Tokens $inputTokens
{
foreach ($expectedTokens as $index => $expectedToken) {
if (! isset($inputTokens[$index])) {
static::fail(\sprintf("The token at index %d must be:\n%s, but is not set in the input collection.", $index, $expectedToken->toJson()));
self::fail(\sprintf("The token at index %d must be:\n%s, but is not set in the input collection.", $index, $expectedToken->toJson()));
}

$inputToken = $inputTokens[$index];

static::assertTrue(
self::assertTrue(
$expectedToken->equals($inputToken),
\sprintf("The token at index %d must be:\n%s,\ngot:\n%s.", $index, $expectedToken->toJson(), $inputToken->toJson())
);

$expectedTokenKind = $expectedToken->isArray() ? $expectedToken->getId() : $expectedToken->getContent();
static::assertTrue(
self::assertTrue(
$inputTokens->isTokenKindFound($expectedTokenKind),
\sprintf(
'The token kind %s (%s) must be found in tokens collection.',
Expand All @@ -150,6 +148,6 @@ private static function assertTokens(Tokens $expectedTokens, Tokens $inputTokens
);
}

static::assertSame($expectedTokens->count(), $inputTokens->count(), 'Both collections must have the same length.');
self::assertSame($expectedTokens->count(), $inputTokens->count(), 'Both collections must have the same length.');
}
}
16 changes: 7 additions & 9 deletions tests/ConfigTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -53,16 +53,14 @@ public function testAllRulesAreSpecifiedAndDifferentFromRuleSets(): void
$fixerFactory->registerCustomFixers($config->getCustomFixers());
$fixers = $fixerFactory->getFixers();

$availableRules = \array_filter($fixers, static function (FixerInterface $fixer): bool {
return ! $fixer instanceof DeprecatedFixerInterface;
});
$availableRules = \array_map(function (FixerInterface $fixer): string {
return $fixer->getName();
}, $availableRules);
$availableRules = \array_filter($fixers, static fn (FixerInterface $fixer): bool => ! $fixer instanceof DeprecatedFixerInterface);
$availableRules = \array_map(fn (FixerInterface $fixer): string => $fixer->getName(), $availableRules);
\sort($availableRules);

/*
$diff = \array_diff($availableRules, $currentRules);
self::assertEmpty($diff, \sprintf("The following fixers are missing:\n- %s", \implode(\PHP_EOL . '- ', $diff)));
*/

$diff = \array_diff($currentRules, $availableRules);
self::assertEmpty($diff, \sprintf("The following fixers do not exist:\n- %s", \implode(\PHP_EOL . '- ', $diff)));
Expand All @@ -77,16 +75,16 @@ public function testAllRulesAreSpecifiedAndDifferentFromRuleSets(): void
}
self::assertSame([], $alreadyDefinedRules, 'These rules are already defined in the respective set');

/*
$currentSets = \array_values(\array_filter(\array_keys($configRules), static function (string $fixerName): bool {
return isset($fixerName[0]) && '@' === $fixerName[0];
}));
$defaultSets = RuleSets::getSetDefinitionNames();
$intersectSets = \array_values(\array_intersect($defaultSets, $currentSets));
self::assertEquals($intersectSets, $currentSets, \sprintf('Rule sets must be ordered as the appear in %s', RuleSet::class));
*/

$currentRules = \array_values(\array_filter(\array_keys($configRules), static function (string $fixerName): bool {
return isset($fixerName[0]) && '@' !== $fixerName[0];
}));
$currentRules = \array_values(\array_filter(\array_keys($configRules), static fn (string $fixerName): bool => isset($fixerName[0]) && '@' !== $fixerName[0]));

$orderedCurrentRules = $currentRules;
\sort($orderedCurrentRules);
Expand Down
33 changes: 33 additions & 0 deletions tests/FinalInternalClassFixerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\RequiresPhp;
use SlamCsFixer\FinalInternalClassFixer;

#[CoversClass(FinalInternalClassFixer::class)]
Expand All @@ -15,6 +16,7 @@ final class FinalInternalClassFixerTest extends AbstractFixerTestCase
public function testFix(string $expected, ?string $input = null): void
{
$this->doTest($expected, $input);
$this->doTest($expected);
}

/** @return string[][] */
Expand Down Expand Up @@ -66,4 +68,35 @@ public static function provideCases(): array
],
];
}

#[RequiresPhp('8.2')]
#[DataProvider('provide82Cases')]
public function test82Fix(string $expected, ?string $input = null): void
{
$this->doTest($expected, $input);
$this->doTest($expected);
}

/** @return string[][] */
public static function provide82Cases(): array
{
return [
[
'<?php final readonly class MyClass {}',
'<?php readonly class MyClass {}',
],
[
'<?php final readonly class MyClass extends MyAbstract {}',
'<?php readonly class MyClass extends MyAbstract {}',
],
[
'<?php final readonly class MyClass implements MyInterface {}',
'<?php readonly class MyClass implements MyInterface {}',
],
[
"<?php\n/**\n * @codeCoverageIgnore\n */\nfinal readonly class MyEntity {}",
"<?php\n/**\n * @codeCoverageIgnore\n */\nreadonly class MyEntity {}",
],
];
}
}

0 comments on commit 3866e19

Please sign in to comment.