Skip to content

Commit 1513363

Browse files
kamil-zacekkukulich
authored andcommitted
SlevomatCodingStandard.Classes.ClassKeywordOrder: New sniff
1 parent 4875bfe commit 1513363

File tree

8 files changed

+327
-0
lines changed

8 files changed

+327
-0
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ Slevomat Coding Standard for [PHP_CodeSniffer](https://github.com/PHPCSStandards
4343
- [SlevomatCodingStandard.Attributes.RequireAttributeAfterDocComment](doc/attributes.md#slevomatcodingstandardattributesrequireattributeafterdoccomment-) 🔧
4444
- [SlevomatCodingStandard.Classes.BackedEnumTypeSpacing](doc/classes.md#slevomatcodingstandardclassesbackedenumtypespacing-) 🔧
4545
- [SlevomatCodingStandard.Classes.ClassConstantVisibility](doc/classes.md#slevomatcodingstandardclassesclassconstantvisibility-) 🔧
46+
- [SlevomatCodingStandard.Classes.ClassKeywordOrder](doc/classes.md#slevomatcodingstandardclassesclasskeywordorder-) 🔧
4647
- [SlevomatCodingStandard.Classes.ClassLength](doc/classes.md#slevomatcodingstandardclassesclasslength)
4748
- [SlevomatCodingStandard.Classes.ClassMemberSpacing](doc/classes.md#slevomatcodingstandardclassesclassmemberspacing-) 🔧
4849
- [SlevomatCodingStandard.Classes.ClassStructure](doc/classes.md#slevomatcodingstandardclassesclassstructure-) 🔧
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
@@ -187,6 +187,7 @@
187187
<rule ref="SlevomatCodingStandard.Arrays.TrailingArrayComma"/>
188188

189189
<rule ref="SlevomatCodingStandard.Classes.ClassConstantVisibility"/>
190+
<rule ref="SlevomatCodingStandard.Classes.ClassKeywordOrder"/>
190191
<rule ref="SlevomatCodingStandard.Classes.ClassMemberSpacing"/>
191192
<rule ref="SlevomatCodingStandard.Classes.ClassStructure">
192193
<properties>

doc/classes.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,12 @@ const FOO = 1; // visibility missing!
2222
public const BAR = 2; // correct
2323
```
2424

25+
#### SlevomatCodingStandard.Classes.ClassKeywordOrder 🔧
26+
27+
Enforces the correct order of class modifiers (e.g., `final`, `abstract`, `readonly`).
28+
29+
Required order is (final | abstract) readonly class. That is, use either `final` or `abstract` (never both), then `readonly` if present, then `class`.
30+
2531
#### SlevomatCodingStandard.Classes.ClassLength
2632

2733
Disallows long classes. This sniff provides the following settings:
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
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+
self::assertAllFixedInFile($report);
37+
}
38+
39+
}
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: 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+
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+
}
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)