Skip to content

Commit 6943861

Browse files
NevossStyleShit
andauthored
feat: add require-type-annotations rule (#48)
Closes #10 --------- Co-authored-by: StyleShit <32631382+StyleShit@users.noreply.github.com>
1 parent 4230f6f commit 6943861

7 files changed

Lines changed: 254 additions & 7 deletions

File tree

.changeset/big-rockets-dress.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'eslint-plugin-php': minor
3+
---
4+
5+
Add `require-type-annotations` rule

cspell.json

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
{
22
"$schema": "https://raw.githubusercontent.com/streetsidesoftware/cspell/main/cspell.schema.json",
33
"words": [
4+
"arrowfunc",
5+
"assignref",
6+
"classconstant",
47
"commentblock",
58
"commentline",
6-
"classconstant",
7-
"propertystatement",
8-
"assignref"
9+
"propertystatement"
910
],
1011
"enableGlobDot": true,
1112
"ignorePaths": [

src/configs/recommended.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,10 @@ import { type Linter } from 'eslint';
33
export const recommended: Linter.Config = {
44
language: 'php/php',
55
rules: {
6-
'php/eqeqeq': 'error',
76
'php/disallow-references': 'error',
7+
'php/eqeqeq': 'error',
88
'php/no-array-keyword': 'error',
9+
'php/require-type-annotations': 'error',
910
'php/require-visibility': 'error',
1011
},
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 { requireTypeAnnotations } from './rules/require-type-annotations';
89
import { requireVisibility } from './rules/require-visibility';
910

1011
import { recommended } from './configs/recommended';
@@ -25,6 +26,7 @@ const plugin = {
2526
'disallow-references': disallowReferences,
2627
eqeqeq,
2728
'no-array-keyword': noArrayKeyword,
29+
'require-type-annotations': requireTypeAnnotations,
2830
'require-visibility': requireVisibility,
2931
},
3032
} satisfies ESLint.Plugin;
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
import { RuleTester } from 'eslint';
2+
3+
import php from '../../index';
4+
import { requireTypeAnnotations } from '../require-type-annotations';
5+
6+
const ruleTester = new RuleTester({
7+
plugins: { php },
8+
language: 'php/php',
9+
});
10+
11+
ruleTester.run('require-type-annotations', requireTypeAnnotations, {
12+
valid: [
13+
'<?php class Foo { public int $a = 1; }',
14+
'<?php class Foo { public function bar(): int {} }',
15+
'<?php class Foo { public function __construct() {} }',
16+
'<?php class Foo { public function bar( int $param ): int {} }',
17+
'<?php function foo(): int {}',
18+
'<?php function foo( int $param ): int {}',
19+
'<?php array_map(function(): int {}, []);',
20+
'<?php array_map(function( int $param ): int {}, []);',
21+
'<?php array_map(fn(): int => 1, []);',
22+
'<?php array_map(fn( int $param ): int => 1, []);',
23+
],
24+
invalid: [
25+
{
26+
code: '<?php class Foo { public $bar; }',
27+
errors: [
28+
{
29+
messageId: 'requireTypesForProperty',
30+
data: { name: 'bar' },
31+
line: 1,
32+
column: 26,
33+
endLine: 1,
34+
endColumn: 30,
35+
},
36+
],
37+
},
38+
{
39+
code: '<?php class Foo { public function bar() {} }',
40+
errors: [
41+
{
42+
messageId: 'requireTypesForMethodReturnType',
43+
data: { name: 'bar' },
44+
line: 1,
45+
column: 35,
46+
endLine: 1,
47+
endColumn: 38,
48+
},
49+
],
50+
},
51+
{
52+
code: '<?php class Foo { public function bar( $param ): int {} }',
53+
errors: [
54+
{
55+
messageId: 'requireTypesForParameter',
56+
data: { name: 'param' },
57+
line: 1,
58+
column: 40,
59+
endLine: 1,
60+
endColumn: 46,
61+
},
62+
],
63+
},
64+
{
65+
code: '<?php function foo() {}',
66+
errors: [
67+
{
68+
messageId: 'requireTypesForFunctionReturnType',
69+
data: { name: 'foo' },
70+
line: 1,
71+
column: 16,
72+
endLine: 1,
73+
endColumn: 19,
74+
},
75+
],
76+
},
77+
{
78+
code: '<?php function foo( $param ): int {}',
79+
errors: [
80+
{
81+
messageId: 'requireTypesForParameter',
82+
data: { name: 'param' },
83+
line: 1,
84+
column: 21,
85+
endLine: 1,
86+
endColumn: 27,
87+
},
88+
],
89+
},
90+
{
91+
code: '<?php array_map(function() {}, []);',
92+
errors: [
93+
{
94+
messageId: 'requireTypesForClosureReturnType',
95+
line: 1,
96+
column: 17,
97+
endLine: 1,
98+
endColumn: 25,
99+
},
100+
],
101+
},
102+
{
103+
code: '<?php array_map(function( $param ): int {}, []);',
104+
errors: [
105+
{
106+
messageId: 'requireTypesForParameter',
107+
line: 1,
108+
column: 27,
109+
endLine: 1,
110+
endColumn: 33,
111+
},
112+
],
113+
},
114+
{
115+
code: '<?php array_map(fn() => 1, []);',
116+
errors: [
117+
{
118+
messageId: 'requireTypesForClosureReturnType',
119+
line: 1,
120+
column: 17,
121+
endLine: 1,
122+
endColumn: 19,
123+
},
124+
],
125+
},
126+
{
127+
code: '<?php array_map(fn( $param ): int => 1, []);',
128+
errors: [
129+
{
130+
messageId: 'requireTypesForParameter',
131+
line: 1,
132+
column: 21,
133+
endLine: 1,
134+
endColumn: 27,
135+
},
136+
],
137+
},
138+
],
139+
});
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
import {
2+
type ArrowFunc,
3+
type Closure,
4+
type Function,
5+
type Method,
6+
type Parameter,
7+
type Property,
8+
} from 'php-parser';
9+
10+
import { createRule } from '../utils/create-rule';
11+
import { extractName } from '../utils/extract-names';
12+
13+
type MessageIds =
14+
| 'requireTypesForProperty'
15+
| 'requireTypesForParameter'
16+
| 'requireTypesForMethodReturnType'
17+
| 'requireTypesForFunctionReturnType'
18+
| 'requireTypesForClosureReturnType';
19+
20+
type Options = [];
21+
22+
export const requireTypeAnnotations = createRule<MessageIds, Options>({
23+
meta: {
24+
type: 'suggestion',
25+
docs: {
26+
description:
27+
'Require types for properties, parameters, methods return types, and closures return types',
28+
},
29+
messages: {
30+
requireTypesForProperty:
31+
"Type must be declared on property '{{name}}'.",
32+
requireTypesForParameter:
33+
"Type must be declared on parameter '{{name}}'.",
34+
requireTypesForMethodReturnType:
35+
"Return type must be declared on method '{{name}}'.",
36+
requireTypesForFunctionReturnType:
37+
"Return type must be declared on function '{{name}}'.",
38+
requireTypesForClosureReturnType:
39+
'Return type must be declared on closure.',
40+
},
41+
schema: [],
42+
},
43+
create(context) {
44+
return {
45+
'property[type=null]'(node: Property) {
46+
context.report({
47+
node,
48+
messageId: 'requireTypesForProperty',
49+
data: { name: extractName(node) },
50+
});
51+
},
52+
'parameter[type=null]'(node: Parameter) {
53+
context.report({
54+
node,
55+
messageId: 'requireTypesForParameter',
56+
data: { name: extractName(node) },
57+
});
58+
},
59+
'method[type=null][name.name!=__construct]'(node: Method) {
60+
context.report({
61+
node: typeof node.name === 'string' ? node : node.name,
62+
messageId: 'requireTypesForMethodReturnType',
63+
data: { name: extractName(node) },
64+
});
65+
},
66+
'function[type=null]'(node: Function) {
67+
context.report({
68+
node: typeof node.name === 'string' ? node : node.name,
69+
messageId: 'requireTypesForFunctionReturnType',
70+
data: { name: extractName(node) },
71+
});
72+
},
73+
'closure[type=null], arrowfunc[type=null]'(
74+
node: Closure | ArrowFunc,
75+
) {
76+
const nodeLoc = context.sourceCode.getLoc(node);
77+
78+
const endColumn =
79+
node.kind === 'closure'
80+
? nodeLoc.start.column + 'function'.length
81+
: nodeLoc.start.column + 'fn'.length;
82+
83+
context.report({
84+
node,
85+
loc: {
86+
start: nodeLoc.start,
87+
end: {
88+
line: nodeLoc.start.line,
89+
column: endColumn,
90+
},
91+
},
92+
messageId: 'requireTypesForClosureReturnType',
93+
});
94+
},
95+
};
96+
},
97+
});

src/utils/extract-names.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@ type Nameable = {
55
};
66

77
export function extractNames(names: Nameable[]) {
8-
return names.map(({ name }) =>
9-
typeof name === 'string' ? name : name.name,
10-
);
8+
return names.map(extractName);
9+
}
10+
11+
export function extractName(name: Nameable) {
12+
return typeof name.name === 'string' ? name.name : name.name.name;
1113
}

0 commit comments

Comments
 (0)