Skip to content

Commit d444598

Browse files
authored
feat: add no-final rule (#52)
Closes #13
1 parent 6943861 commit d444598

4 files changed

Lines changed: 205 additions & 0 deletions

File tree

src/configs/recommended.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ export const recommended: Linter.Config = {
66
'php/disallow-references': 'error',
77
'php/eqeqeq': 'error',
88
'php/no-array-keyword': 'error',
9+
'php/no-final': 'error',
910
'php/require-type-annotations': 'error',
1011
'php/require-visibility': 'error',
1112
},

src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { type ESLint } from 'eslint';
55
import { disallowReferences } from './rules/disallow-references';
66
import { eqeqeq } from './rules/eqeqeq';
77
import { noArrayKeyword } from './rules/no-array-keyword';
8+
import { noFinal } from './rules/no-final';
89
import { requireTypeAnnotations } from './rules/require-type-annotations';
910
import { requireVisibility } from './rules/require-visibility';
1011

@@ -26,6 +27,7 @@ const plugin = {
2627
'disallow-references': disallowReferences,
2728
eqeqeq,
2829
'no-array-keyword': noArrayKeyword,
30+
'no-final': noFinal,
2931
'require-type-annotations': requireTypeAnnotations,
3032
'require-visibility': requireVisibility,
3133
},
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
import { RuleTester } from 'eslint';
2+
3+
import php from '../../index';
4+
import { noFinal } from '../no-final';
5+
6+
const ruleTester = new RuleTester({
7+
plugins: { php },
8+
language: 'php/php',
9+
});
10+
11+
ruleTester.run('no-final', noFinal, {
12+
valid: [
13+
{
14+
name: 'methods',
15+
code: `
16+
<?php
17+
class Test {
18+
public function method1() {}
19+
protected function method2() {}
20+
private function method3() {}
21+
}
22+
`,
23+
},
24+
25+
{
26+
name: 'properties',
27+
code: `
28+
<?php
29+
class Test {
30+
public $property1;
31+
protected $property2;
32+
private $property3;
33+
}
34+
`,
35+
},
36+
],
37+
invalid: [
38+
{
39+
name: 'final class',
40+
code: '<?php final class Test {}',
41+
errors: [
42+
{
43+
messageId: 'noFinal',
44+
line: 1,
45+
column: 19,
46+
endLine: 1,
47+
endColumn: 23,
48+
},
49+
],
50+
},
51+
52+
{
53+
name: 'final class constant',
54+
code: `
55+
<?php
56+
class Test {
57+
final const CONSTANT = 'value';
58+
}
59+
`,
60+
errors: [
61+
{
62+
messageId: 'noFinal',
63+
line: 4,
64+
column: 6,
65+
endLine: 4,
66+
endColumn: 36,
67+
suggestions: [
68+
{
69+
messageId: 'removeFinal',
70+
output: `
71+
<?php
72+
class Test {
73+
const CONSTANT = 'value';
74+
}
75+
`,
76+
},
77+
],
78+
},
79+
],
80+
},
81+
82+
{
83+
name: 'final class property',
84+
code: `
85+
<?php
86+
class Test {
87+
final public $property = 'value',
88+
$property2 = 'value-1';
89+
}
90+
`,
91+
errors: [
92+
{
93+
messageId: 'noFinal',
94+
line: 4,
95+
column: 6,
96+
endLine: 5,
97+
endColumn: 29,
98+
suggestions: [
99+
{
100+
messageId: 'removeFinal',
101+
output: `
102+
<?php
103+
class Test {
104+
public $property = 'value',
105+
$property2 = 'value-1';
106+
}
107+
`,
108+
},
109+
],
110+
},
111+
],
112+
},
113+
114+
{
115+
name: 'final class method',
116+
code: `
117+
<?php
118+
class Test {
119+
final public function method() {
120+
// Code
121+
}
122+
}
123+
`,
124+
errors: [
125+
{
126+
messageId: 'noFinal',
127+
line: 4,
128+
column: 28,
129+
endLine: 4,
130+
endColumn: 34,
131+
},
132+
],
133+
},
134+
],
135+
});

src/rules/no-final.ts

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import type { ClassConstant, Identifier, PropertyStatement } from 'php-parser';
2+
3+
import { createRule } from '../utils/create-rule';
4+
5+
type MessageIds = 'noFinal' | 'removeFinal';
6+
type Options = [];
7+
8+
export const noFinal = createRule<MessageIds, Options>({
9+
meta: {
10+
hasSuggestions: true,
11+
type: 'suggestion',
12+
docs: {
13+
description: 'Disallow using the `final` keyword',
14+
},
15+
messages: {
16+
noFinal: 'The `final` keyword is not allowed.',
17+
removeFinal: 'Remove the `final` keyword`.',
18+
},
19+
schema: [],
20+
},
21+
22+
create(context) {
23+
return {
24+
'class[isFinal=true] > .name, method[isFinal=true] > .name'(
25+
node: Identifier,
26+
) {
27+
context.report({
28+
node,
29+
messageId: 'noFinal',
30+
});
31+
},
32+
33+
'classconstant, propertystatement'(
34+
node: ClassConstant | PropertyStatement,
35+
) {
36+
const finalKeyword = context.sourceCode.findClosestKeyword(
37+
node,
38+
'final',
39+
);
40+
41+
if (!finalKeyword) {
42+
return;
43+
}
44+
45+
context.report({
46+
node,
47+
loc: {
48+
start: finalKeyword.start,
49+
end: context.sourceCode.getLoc(node).end,
50+
},
51+
messageId: 'noFinal',
52+
suggest: [
53+
{
54+
messageId: 'removeFinal',
55+
fix(fixer) {
56+
return fixer.removeRange([
57+
finalKeyword.start.offset,
58+
finalKeyword.end.offset,
59+
]);
60+
},
61+
},
62+
],
63+
});
64+
},
65+
};
66+
},
67+
});

0 commit comments

Comments
 (0)