Skip to content
Open
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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ Slevomat Coding Standard for [PHP_CodeSniffer](https://github.com/PHPCSStandards
- [SlevomatCodingStandard.Classes.DisallowStringExpressionPropertyFetch](doc/classes.md#slevomatcodingstandardclassesdisallowstringexpressionpropertyfetch-) 🔧
- [SlevomatCodingStandard.Classes.EmptyLinesAroundClassBraces](doc/classes.md#slevomatcodingstandardclassesemptylinesaroundclassbraces-) 🔧
- [SlevomatCodingStandard.Classes.EnumCaseSpacing](doc/classes.md#slevomatcodingstandardclassesenumcasespacing-) 🔧
- [SlevomatCodingStandard.Classes.ClassKeywordOrder](doc/classes.md#slevomatcodingstandardclassesclasskeywordorder-) 🔧
- [SlevomatCodingStandard.Classes.ForbiddenPublicProperty](doc/classes.md#slevomatcodingstandardclassesforbiddenpublicproperty)
- [SlevomatCodingStandard.Classes.MethodSpacing](doc/classes.md#slevomatcodingstandardclassesmethodspacing-) 🔧
- [SlevomatCodingStandard.Classes.ModernClassNameReference](doc/classes.md#slevomatcodingstandardclassesmodernclassnamereference-) 🔧
Expand Down
112 changes: 112 additions & 0 deletions SlevomatCodingStandard/Sniffs/Classes/ClassKeywordOrderSniff.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
<?php declare(strict_types = 1);

namespace SlevomatCodingStandard\Sniffs\Classes;

use PHP_CodeSniffer\Files\File;
use PHP_CodeSniffer\Sniffs\Sniff;
use SlevomatCodingStandard\Helpers\TokenHelper;
use function array_key_first;
use function array_keys;
use function array_map;
use function array_values;
use function count;
use function implode;
use function ksort;
use function uasort;
use const T_ABSTRACT;
use const T_CLASS;
use const T_FINAL;
use const T_READONLY;
use const T_WHITESPACE;

class ClassKeywordOrderSniff implements Sniff
{

public const CODE_WRONG_CLASS_KEYWORD_ORDER = 'WrongClassKeywordOrder';

/**
* @return array<int, (int|string)>
*/
public function register(): array
{
return [
T_CLASS,
];
}

public function process(File $phpcsFile, int $stackPtr): void
{
$tokens = $phpcsFile->getTokens();

$modifierTokens = [
T_ABSTRACT => 'abstract',
T_FINAL => 'final',
T_READONLY => 'readonly',
];

$foundModifiers = [];
$currentIndex = TokenHelper::findPreviousEffective($phpcsFile, $stackPtr - 1);

while ($currentIndex !== null && isset($modifierTokens[$tokens[$currentIndex]['code']])) {
$foundModifiers[$currentIndex] = $tokens[$currentIndex]['code'];
$currentIndex = TokenHelper::findPreviousEffective($phpcsFile, $currentIndex - 1);
}

if (count($foundModifiers) === 0) {
return;
}

ksort($foundModifiers);

$actualOrderCodes = array_values($foundModifiers);
$actualOrderText = array_map(static fn ($code) => $modifierTokens[$code], $actualOrderCodes);

$sortedModifiers = $foundModifiers;
uasort($sortedModifiers, static function ($a, $b) {
$priority = [
T_ABSTRACT => 0,
T_FINAL => 0,
T_READONLY => 1,
];
return $priority[$a] <=> $priority[$b];
});

$expectedOrderCodes = array_values($sortedModifiers);
$expectedOrderText = array_map(static fn ($code) => $modifierTokens[$code], $expectedOrderCodes);

if ($actualOrderCodes === $expectedOrderCodes) {
return;
}

$error = 'Class keywords are not in the correct order. Found: "%s class"; Expected: "%s class"';
$data = [
implode(' ', $actualOrderText),
implode(' ', $expectedOrderText),
];

$fix = $phpcsFile->addFixableError($error, $stackPtr, self::CODE_WRONG_CLASS_KEYWORD_ORDER, $data);

if ($fix !== true) {
return;
}

$phpcsFile->fixer->beginChangeset();

foreach (array_keys($foundModifiers) as $ptr) {
$phpcsFile->fixer->replaceToken($ptr, '');

if ($tokens[$ptr + 1]['code'] === T_WHITESPACE) {
$phpcsFile->fixer->replaceToken($ptr + 1, '');
}
}

$firstModifierPtr = array_key_first($foundModifiers);

$newContent = implode(' ', $expectedOrderText) . ' ';

$phpcsFile->fixer->addContentBefore($firstModifierPtr, $newContent);

$phpcsFile->fixer->endChangeset();
}

}
1 change: 1 addition & 0 deletions build/phpcs.xml
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,7 @@
<rule ref="SlevomatCodingStandard.Classes.DisallowMultiPropertyDefinition"/>
<rule ref="SlevomatCodingStandard.Classes.EmptyLinesAroundClassBraces"/>
<rule ref="SlevomatCodingStandard.Classes.EnumCaseSpacing"/>
<rule ref="SlevomatCodingStandard.Classes.ClassKeywordOrder"/>
<rule ref="SlevomatCodingStandard.Classes.MethodSpacing"/>
<rule ref="SlevomatCodingStandard.Classes.ModernClassNameReference"/>
<rule ref="SlevomatCodingStandard.Classes.ParentCallSpacing"/>
Expand Down
6 changes: 6 additions & 0 deletions doc/classes.md
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,12 @@ Sniff provides the following settings:
* `minLinesCountBeforeWithoutComment`: minimum number of lines before enum case without a documentation comment or attribute
* `maxLinesCountBeforeWithoutComment`: maximum number of lines before enum case without a documentation comment or attribute

#### SlevomatCodingStandard.Classes.ClassKeywordOrder 🔧

Enforces the correct order of class modifiers (e.g., `final`, `abstract`, `readonly`).

Required order is (final | abstract) readonly class. That is, use either `final` or `abstract` (never both), then `readonly` if present, then `class`.

#### SlevomatCodingStandard.Classes.ForbiddenPublicProperty

Disallows using public properties.
Expand Down
39 changes: 39 additions & 0 deletions tests/Sniffs/Classes/ClassKeywordOrderSniffTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<?php declare(strict_types = 1);

namespace SlevomatCodingStandard\Sniffs\Classes;

use SlevomatCodingStandard\Sniffs\TestCase;

class ClassKeywordOrderSniffTest extends TestCase
{

public function testNoErrors(): void
{
$report = self::checkFile(__DIR__ . '/data/classKeywordOrderNoErrors.php');
self::assertNoSniffErrorInFile($report);
}

public function testErrors(): void
{
$report = self::checkFile(__DIR__ . '/data/classKeywordOrderErrors.php');

self::assertSame(2, $report->getErrorCount());

self::assertSniffError(
$report,
30,
ClassKeywordOrderSniff::CODE_WRONG_CLASS_KEYWORD_ORDER,
'Class keywords are not in the correct order. Found: "readonly final class"; Expected: "final readonly class"',
);

self::assertSniffError(
$report,
49,
ClassKeywordOrderSniff::CODE_WRONG_CLASS_KEYWORD_ORDER,
'Class keywords are not in the correct order. Found: "readonly abstract class"; Expected: "abstract readonly class"',
);

self::assertAllFixedInFile($report);
}

}
56 changes: 56 additions & 0 deletions tests/Sniffs/Classes/data/classKeywordOrderErrors.fixed.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
<?php // lint >= 8.2

class Foo1
{

public function bar()
{

}
}

final class Foo2
{

public function bar()
{

}
}

readonly class Foo3
{

public function bar()
{

}
}

final readonly class Foo4
{

public function bar()
{

}
}


abstract class Foo5
{

public function bar()
{

}
}

abstract readonly class Foo6
{

public function bar()
{

}
}
56 changes: 56 additions & 0 deletions tests/Sniffs/Classes/data/classKeywordOrderErrors.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
<?php // lint >= 8.2

class Foo1
{

public function bar()
{

}
}

final class Foo2
{

public function bar()
{

}
}

readonly class Foo3
{

public function bar()
{

}
}

readonly final class Foo4
{

public function bar()
{

}
}


abstract class Foo5
{

public function bar()
{

}
}

readonly abstract class Foo6
{

public function bar()
{

}
}
56 changes: 56 additions & 0 deletions tests/Sniffs/Classes/data/classKeywordOrderNoErrors.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
<?php // lint >= 8.2

class Foo1
{

public function bar()
{

}
}

final class Foo2
{

public function bar()
{

}
}

readonly class Foo3
{

public function bar()
{

}
}

final readonly class Foo4
{

public function bar()
{

}
}


abstract class Foo5
{

public function bar()
{

}
}

abstract readonly class Foo6
{

public function bar()
{

}
}