Skip to content

Commit 845a82f

Browse files
committed
Introduce rule SlevomatCodingStandard.Classes.ClassKeywordOrder
1 parent 3ce4daf commit 845a82f

File tree

8 files changed

+326
-0
lines changed

8 files changed

+326
-0
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ Slevomat Coding Standard for [PHP_CodeSniffer](https://github.com/PHPCSStandards
5454
- [SlevomatCodingStandard.Classes.DisallowStringExpressionPropertyFetch](doc/classes.md#slevomatcodingstandardclassesdisallowstringexpressionpropertyfetch-) 🔧
5555
- [SlevomatCodingStandard.Classes.EmptyLinesAroundClassBraces](doc/classes.md#slevomatcodingstandardclassesemptylinesaroundclassbraces-) 🔧
5656
- [SlevomatCodingStandard.Classes.EnumCaseSpacing](doc/classes.md#slevomatcodingstandardclassesenumcasespacing-) 🔧
57+
- [SlevomatCodingStandard.Classes.ClassKeywordOrder](doc/classes.md#slevomatcodingstandardclassesclasskeywordorder-) 🔧
5758
- [SlevomatCodingStandard.Classes.ForbiddenPublicProperty](doc/classes.md#slevomatcodingstandardclassesforbiddenpublicproperty)
5859
- [SlevomatCodingStandard.Classes.MethodSpacing](doc/classes.md#slevomatcodingstandardclassesmethodspacing-) 🔧
5960
- [SlevomatCodingStandard.Classes.ModernClassNameReference](doc/classes.md#slevomatcodingstandardclassesmodernclassnamereference-) 🔧
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace SlevomatCodingStandard\Sniffs\Classes;
4+
5+
use PHP_CodeSniffer\Files\File;
6+
use PHP_CodeSniffer\Sniffs\Sniff;
7+
use SlevomatCodingStandard\Helpers\TokenHelper;
8+
use function array_key_first;
9+
use function array_keys;
10+
use function array_map;
11+
use function array_values;
12+
use function count;
13+
use function implode;
14+
use function ksort;
15+
use function uasort;
16+
use const T_ABSTRACT;
17+
use const T_CLASS;
18+
use const T_FINAL;
19+
use const T_READONLY;
20+
use const T_WHITESPACE;
21+
22+
class ClassKeywordOrderSniff implements Sniff
23+
{
24+
25+
public const CODE_WRONG_CLASS_KEYWORD_ORDER = 'WrongClassKeywordOrder';
26+
27+
/**
28+
* @return array<int, (int|string)>
29+
*/
30+
public function register(): array
31+
{
32+
return [
33+
T_CLASS,
34+
];
35+
}
36+
37+
public function process(File $phpcsFile, int $stackPtr): void
38+
{
39+
$tokens = $phpcsFile->getTokens();
40+
41+
$modifierTokens = [
42+
T_ABSTRACT => 'abstract',
43+
T_FINAL => 'final',
44+
T_READONLY => 'readonly',
45+
];
46+
47+
$foundModifiers = [];
48+
$currentIndex = TokenHelper::findPreviousEffective($phpcsFile, $stackPtr - 1);
49+
50+
while ($currentIndex !== null && isset($modifierTokens[$tokens[$currentIndex]['code']])) {
51+
$foundModifiers[$currentIndex] = $tokens[$currentIndex]['code'];
52+
$currentIndex = TokenHelper::findPreviousEffective($phpcsFile, $currentIndex - 1);
53+
}
54+
55+
if (count($foundModifiers) === 0) {
56+
return;
57+
}
58+
59+
ksort($foundModifiers);
60+
61+
$actualOrderCodes = array_values($foundModifiers);
62+
$actualOrderText = array_map(static fn ($code) => $modifierTokens[$code], $actualOrderCodes);
63+
64+
$sortedModifiers = $foundModifiers;
65+
uasort($sortedModifiers, static function ($a, $b) {
66+
$priority = [
67+
T_ABSTRACT => 0,
68+
T_FINAL => 0,
69+
T_READONLY => 1,
70+
];
71+
return $priority[$a] <=> $priority[$b];
72+
});
73+
74+
$expectedOrderCodes = array_values($sortedModifiers);
75+
$expectedOrderText = array_map(static fn ($code) => $modifierTokens[$code], $expectedOrderCodes);
76+
77+
if ($actualOrderCodes === $expectedOrderCodes) {
78+
return;
79+
}
80+
81+
$error = 'Class keywords are not in the correct order. Found: "%s class"; Expected: "%s class"';
82+
$data = [
83+
implode(' ', $actualOrderText),
84+
implode(' ', $expectedOrderText),
85+
];
86+
87+
$fix = $phpcsFile->addFixableError($error, $stackPtr, self::CODE_WRONG_CLASS_KEYWORD_ORDER, $data);
88+
89+
if ($fix !== true) {
90+
return;
91+
}
92+
93+
$phpcsFile->fixer->beginChangeset();
94+
95+
foreach (array_keys($foundModifiers) as $ptr) {
96+
$phpcsFile->fixer->replaceToken($ptr, '');
97+
98+
if ($tokens[$ptr + 1]['code'] === T_WHITESPACE) {
99+
$phpcsFile->fixer->replaceToken($ptr + 1, '');
100+
}
101+
}
102+
103+
$firstModifierPtr = array_key_first($foundModifiers);
104+
105+
$newContent = implode(' ', $expectedOrderText) . ' ';
106+
107+
$phpcsFile->fixer->addContentBefore($firstModifierPtr, $newContent);
108+
109+
$phpcsFile->fixer->endChangeset();
110+
}
111+
112+
}

build/phpcs.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,7 @@
222222
<rule ref="SlevomatCodingStandard.Classes.DisallowMultiPropertyDefinition"/>
223223
<rule ref="SlevomatCodingStandard.Classes.EmptyLinesAroundClassBraces"/>
224224
<rule ref="SlevomatCodingStandard.Classes.EnumCaseSpacing"/>
225+
<rule ref="SlevomatCodingStandard.Classes.ClassKeywordOrder"/>
225226
<rule ref="SlevomatCodingStandard.Classes.MethodSpacing"/>
226227
<rule ref="SlevomatCodingStandard.Classes.ModernClassNameReference"/>
227228
<rule ref="SlevomatCodingStandard.Classes.ParentCallSpacing"/>

doc/classes.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,12 @@ Sniff provides the following settings:
161161
* `minLinesCountBeforeWithoutComment`: minimum number of lines before enum case without a documentation comment or attribute
162162
* `maxLinesCountBeforeWithoutComment`: maximum number of lines before enum case without a documentation comment or attribute
163163

164+
#### SlevomatCodingStandard.Classes.ClassKeywordOrder 🔧
165+
166+
Enforces the correct order of class modifiers (e.g., `final`, `abstract`, `readonly`).
167+
168+
Required order is (final | abstract) readonly class. That is, use either `final` or `abstract` (never both), then `readonly` if present, then `class`.
169+
164170
#### SlevomatCodingStandard.Classes.ForbiddenPublicProperty
165171

166172
Disallows using public properties.
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace SlevomatCodingStandard\Sniffs\Classes;
4+
5+
use SlevomatCodingStandard\Sniffs\TestCase;
6+
7+
class ClassKeywordOrderSniffTest extends TestCase
8+
{
9+
10+
public function testNoErrors(): void
11+
{
12+
$report = self::checkFile(__DIR__ . '/data/classKeywordOrderNoErrors.php');
13+
self::assertNoSniffErrorInFile($report);
14+
}
15+
16+
public function testErrors(): void
17+
{
18+
$report = self::checkFile(__DIR__ . '/data/classKeywordOrderErrors.php');
19+
20+
self::assertSame(2, $report->getErrorCount());
21+
22+
self::assertSniffError(
23+
$report,
24+
30,
25+
ClassKeywordOrderSniff::CODE_WRONG_CLASS_KEYWORD_ORDER,
26+
'Class keywords are not in the correct order. Found: "readonly final class"; Expected: "final readonly class"',
27+
);
28+
29+
self::assertSniffError(
30+
$report,
31+
49,
32+
ClassKeywordOrderSniff::CODE_WRONG_CLASS_KEYWORD_ORDER,
33+
'Class keywords are not in the correct order. Found: "readonly abstract class"; Expected: "abstract readonly class"',
34+
);
35+
}
36+
37+
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
<?php // lint >= 8.2
2+
3+
class Foo1
4+
{
5+
6+
public function bar()
7+
{
8+
9+
}
10+
}
11+
12+
final class Foo2
13+
{
14+
15+
public function bar()
16+
{
17+
18+
}
19+
}
20+
21+
readonly class Foo3
22+
{
23+
24+
public function bar()
25+
{
26+
27+
}
28+
}
29+
30+
final readonly class Foo4
31+
{
32+
33+
public function bar()
34+
{
35+
36+
}
37+
}
38+
39+
40+
abstract class Foo5
41+
{
42+
43+
public function bar()
44+
{
45+
46+
}
47+
}
48+
49+
abstract readonly class Foo6
50+
{
51+
52+
public function bar()
53+
{
54+
55+
}
56+
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
<?php // lint >= 8.2
2+
3+
class Foo1
4+
{
5+
6+
public function bar()
7+
{
8+
9+
}
10+
}
11+
12+
final class Foo2
13+
{
14+
15+
public function bar()
16+
{
17+
18+
}
19+
}
20+
21+
readonly class Foo3
22+
{
23+
24+
public function bar()
25+
{
26+
27+
}
28+
}
29+
30+
readonly final class Foo4
31+
{
32+
33+
public function bar()
34+
{
35+
36+
}
37+
}
38+
39+
40+
abstract class Foo5
41+
{
42+
43+
public function bar()
44+
{
45+
46+
}
47+
}
48+
49+
readonly abstract class Foo6
50+
{
51+
52+
public function bar()
53+
{
54+
55+
}
56+
}
57+
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
<?php // lint >= 8.2
2+
3+
class Foo1
4+
{
5+
6+
public function bar()
7+
{
8+
9+
}
10+
}
11+
12+
final class Foo2
13+
{
14+
15+
public function bar()
16+
{
17+
18+
}
19+
}
20+
21+
readonly class Foo3
22+
{
23+
24+
public function bar()
25+
{
26+
27+
}
28+
}
29+
30+
final readonly class Foo4
31+
{
32+
33+
public function bar()
34+
{
35+
36+
}
37+
}
38+
39+
40+
abstract class Foo5
41+
{
42+
43+
public function bar()
44+
{
45+
46+
}
47+
}
48+
49+
abstract readonly class Foo6
50+
{
51+
52+
public function bar()
53+
{
54+
55+
}
56+
}

0 commit comments

Comments
 (0)