eslint-config-typed 4.0.6 → 4.0.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/configs/typescript.d.mts.map +1 -1
- package/dist/configs/typescript.mjs +2 -0
- package/dist/configs/typescript.mjs.map +1 -1
- package/dist/plugins/react-coding-style/rules/display-name.d.mts +2 -2
- package/dist/plugins/react-coding-style/rules/display-name.d.mts.map +1 -1
- package/dist/plugins/react-coding-style/rules/display-name.mjs +110 -30
- package/dist/plugins/react-coding-style/rules/display-name.mjs.map +1 -1
- package/dist/plugins/react-coding-style/rules/rules.d.mts +2 -2
- package/dist/plugins/react-coding-style/rules/use-memo-hooks-style.mjs +18 -9
- package/dist/plugins/react-coding-style/rules/use-memo-hooks-style.mjs.map +1 -1
- package/dist/rules/eslint-react-coding-style-rules.d.mts +1 -3
- package/dist/rules/eslint-react-coding-style-rules.d.mts.map +1 -1
- package/dist/rules/eslint-react-coding-style-rules.mjs +1 -1
- package/dist/rules/eslint-react-coding-style-rules.mjs.map +1 -1
- package/dist/rules/eslint-react-rules.d.mts +1 -1
- package/dist/rules/eslint-react-rules.mjs +1 -1
- package/dist/rules/eslint-react-rules.mjs.map +1 -1
- package/dist/types/rules/eslint-react-coding-style-rules.d.mts +21 -6
- package/dist/types/rules/eslint-react-coding-style-rules.d.mts.map +1 -1
- package/package.json +11 -9
- package/src/configs/typescript.mts +4 -0
- package/src/plugins/react-coding-style/README.md +4 -3
- package/src/plugins/react-coding-style/rules/display-name.mts +160 -38
- package/src/plugins/react-coding-style/rules/display-name.test.mts +70 -6
- package/src/plugins/react-coding-style/rules/shared.test.mts +148 -0
- package/src/plugins/react-coding-style/rules/use-memo-hooks-style-named.test.mts +47 -11
- package/src/plugins/react-coding-style/rules/use-memo-hooks-style-namespace.test.mts +47 -11
- package/src/plugins/react-coding-style/rules/use-memo-hooks-style.mts +24 -9
- package/src/rules/eslint-react-coding-style-rules.mts +1 -1
- package/src/rules/eslint-react-rules.mts +1 -1
- package/src/types/rules/eslint-react-coding-style-rules.mts +21 -6
|
@@ -3,16 +3,16 @@ import {
|
|
|
3
3
|
type TSESLint,
|
|
4
4
|
type TSESTree,
|
|
5
5
|
} from '@typescript-eslint/utils';
|
|
6
|
-
import {
|
|
6
|
+
import { castDeepMutable } from 'ts-data-forge';
|
|
7
7
|
import { isReactApiCall } from './shared.mjs';
|
|
8
8
|
|
|
9
9
|
type Options = readonly [
|
|
10
10
|
Readonly<{
|
|
11
|
-
|
|
11
|
+
ignoreName?: string | readonly string[];
|
|
12
12
|
}>?,
|
|
13
13
|
];
|
|
14
14
|
|
|
15
|
-
type MessageIds = 'missingDisplayName';
|
|
15
|
+
type MessageIds = 'missingDisplayName' | 'mismatchedDisplayName';
|
|
16
16
|
|
|
17
17
|
/**
|
|
18
18
|
* Rule to require displayName property for React components
|
|
@@ -23,16 +23,23 @@ export const displayNameRule: TSESLint.RuleModule<MessageIds, Options> = {
|
|
|
23
23
|
type: 'suggestion',
|
|
24
24
|
docs: {
|
|
25
25
|
description:
|
|
26
|
-
'Require
|
|
26
|
+
'Require React.memo components to define displayName matching the component name',
|
|
27
27
|
},
|
|
28
28
|
schema: [
|
|
29
29
|
{
|
|
30
30
|
type: 'object',
|
|
31
31
|
properties: {
|
|
32
|
-
|
|
33
|
-
type: 'boolean',
|
|
32
|
+
ignoreName: {
|
|
34
33
|
description:
|
|
35
|
-
'
|
|
34
|
+
'Component names allowed to have displayName different from the variable name.',
|
|
35
|
+
oneOf: [
|
|
36
|
+
{ type: 'string' },
|
|
37
|
+
{
|
|
38
|
+
type: 'array',
|
|
39
|
+
items: { type: 'string' },
|
|
40
|
+
minItems: 0,
|
|
41
|
+
},
|
|
42
|
+
],
|
|
36
43
|
},
|
|
37
44
|
},
|
|
38
45
|
additionalProperties: false,
|
|
@@ -41,12 +48,17 @@ export const displayNameRule: TSESLint.RuleModule<MessageIds, Options> = {
|
|
|
41
48
|
messages: {
|
|
42
49
|
missingDisplayName:
|
|
43
50
|
'Component should have a displayName property for better debugging',
|
|
51
|
+
mismatchedDisplayName:
|
|
52
|
+
'displayName should match the component name "{{componentName}}"',
|
|
44
53
|
},
|
|
45
54
|
},
|
|
46
55
|
create: (context) => {
|
|
47
56
|
const options = context.options[0] ?? {};
|
|
48
57
|
|
|
49
|
-
const
|
|
58
|
+
const ignoreNameSet = normalizeNames(options.ignoreName);
|
|
59
|
+
|
|
60
|
+
const shouldIgnoreMismatch = (componentName: string): boolean =>
|
|
61
|
+
ignoreNameSet.has(componentName);
|
|
50
62
|
|
|
51
63
|
const checkComponent = (
|
|
52
64
|
node: DeepReadonly<TSESTree.VariableDeclarator>,
|
|
@@ -65,48 +77,47 @@ export const displayNameRule: TSESLint.RuleModule<MessageIds, Options> = {
|
|
|
65
77
|
|
|
66
78
|
const componentName = node.id.name;
|
|
67
79
|
|
|
68
|
-
|
|
69
|
-
return;
|
|
70
|
-
}
|
|
80
|
+
const assignment = getDisplayNameAssignment(node);
|
|
71
81
|
|
|
72
|
-
|
|
82
|
+
if (assignment === undefined) {
|
|
83
|
+
context.report({
|
|
84
|
+
node: castDeepMutable(node),
|
|
85
|
+
messageId: 'missingDisplayName',
|
|
86
|
+
});
|
|
73
87
|
|
|
74
|
-
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
75
90
|
|
|
76
|
-
|
|
91
|
+
if (!isComponentDisplayNameAssignment(assignment, componentName)) {
|
|
92
|
+
context.report({
|
|
93
|
+
node: castDeepMutable(node),
|
|
94
|
+
messageId: 'missingDisplayName',
|
|
95
|
+
});
|
|
77
96
|
|
|
78
|
-
if (grandParent.type !== AST_NODE_TYPES.Program) {
|
|
79
97
|
return;
|
|
80
98
|
}
|
|
81
99
|
|
|
82
|
-
const
|
|
100
|
+
const displayName = extractDisplayName(assignment.right);
|
|
83
101
|
|
|
84
|
-
|
|
102
|
+
if (displayName === undefined) {
|
|
103
|
+
context.report({
|
|
104
|
+
node: assignment.right,
|
|
105
|
+
messageId: 'mismatchedDisplayName',
|
|
106
|
+
data: { componentName },
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
85
111
|
|
|
86
|
-
if (
|
|
112
|
+
if (shouldIgnoreMismatch(componentName)) {
|
|
87
113
|
return;
|
|
88
114
|
}
|
|
89
115
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
const hasDisplayName =
|
|
93
|
-
nextStatement !== undefined &&
|
|
94
|
-
nextStatement.type === AST_NODE_TYPES.ExpressionStatement &&
|
|
95
|
-
nextStatement.expression.type === AST_NODE_TYPES.AssignmentExpression &&
|
|
96
|
-
nextStatement.expression.left.type ===
|
|
97
|
-
AST_NODE_TYPES.MemberExpression &&
|
|
98
|
-
nextStatement.expression.left.object.type ===
|
|
99
|
-
AST_NODE_TYPES.Identifier &&
|
|
100
|
-
nextStatement.expression.left.object.name === componentName &&
|
|
101
|
-
nextStatement.expression.left.property.type ===
|
|
102
|
-
AST_NODE_TYPES.Identifier &&
|
|
103
|
-
nextStatement.expression.left.property.name === 'displayName';
|
|
104
|
-
|
|
105
|
-
if (!hasDisplayName) {
|
|
116
|
+
if (displayName !== componentName) {
|
|
106
117
|
context.report({
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
118
|
+
node: assignment.right,
|
|
119
|
+
messageId: 'mismatchedDisplayName',
|
|
120
|
+
data: { componentName },
|
|
110
121
|
});
|
|
111
122
|
}
|
|
112
123
|
};
|
|
@@ -115,5 +126,116 @@ export const displayNameRule: TSESLint.RuleModule<MessageIds, Options> = {
|
|
|
115
126
|
VariableDeclarator: checkComponent,
|
|
116
127
|
};
|
|
117
128
|
},
|
|
118
|
-
defaultOptions: [{
|
|
129
|
+
defaultOptions: [{ ignoreName: [] }],
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
const normalizeNames = (
|
|
133
|
+
names: string | readonly string[] | undefined,
|
|
134
|
+
): ReadonlySet<string> => {
|
|
135
|
+
if (names === undefined) {
|
|
136
|
+
return new Set();
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (typeof names === 'string') {
|
|
140
|
+
return new Set([names]);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return new Set(names);
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
const getDisplayNameAssignment = (
|
|
147
|
+
node: DeepReadonly<TSESTree.VariableDeclarator>,
|
|
148
|
+
): DeepReadonly<TSESTree.AssignmentExpression> | undefined => {
|
|
149
|
+
let mut_current = node.parent as DeepReadonly<TSESTree.Node> | undefined;
|
|
150
|
+
|
|
151
|
+
let mut_statement: DeepReadonly<TSESTree.Statement> | undefined = undefined;
|
|
152
|
+
|
|
153
|
+
while (mut_current !== undefined) {
|
|
154
|
+
if (
|
|
155
|
+
mut_current.type === AST_NODE_TYPES.VariableDeclaration ||
|
|
156
|
+
mut_current.type === AST_NODE_TYPES.ExportNamedDeclaration
|
|
157
|
+
) {
|
|
158
|
+
mut_statement = mut_current as DeepReadonly<TSESTree.Statement>;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (mut_current.type === AST_NODE_TYPES.Program) {
|
|
162
|
+
break;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
mut_current = mut_current.parent;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (mut_current === undefined || mut_statement === undefined) {
|
|
169
|
+
return undefined;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const program = mut_current;
|
|
173
|
+
|
|
174
|
+
const componentIndex = program.body.indexOf(
|
|
175
|
+
// eslint-disable-next-line total-functions/no-unsafe-type-assertion
|
|
176
|
+
mut_statement as TSESTree.Statement,
|
|
177
|
+
);
|
|
178
|
+
|
|
179
|
+
if (componentIndex === -1) {
|
|
180
|
+
return undefined;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const nextStatement = program.body[componentIndex + 1];
|
|
184
|
+
|
|
185
|
+
if (nextStatement === undefined) {
|
|
186
|
+
return undefined;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (nextStatement.type !== AST_NODE_TYPES.ExpressionStatement) {
|
|
190
|
+
return undefined;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (nextStatement.expression.type !== AST_NODE_TYPES.AssignmentExpression) {
|
|
194
|
+
return undefined;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return nextStatement.expression;
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
const isComponentDisplayNameAssignment = (
|
|
201
|
+
assignment: DeepReadonly<TSESTree.AssignmentExpression>,
|
|
202
|
+
componentName: string,
|
|
203
|
+
): assignment is TSESTree.AssignmentExpression => {
|
|
204
|
+
if (assignment.left.type !== AST_NODE_TYPES.MemberExpression) {
|
|
205
|
+
return false;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if (assignment.left.object.type !== AST_NODE_TYPES.Identifier) {
|
|
209
|
+
return false;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (assignment.left.object.name !== componentName) {
|
|
213
|
+
return false;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
return (
|
|
217
|
+
assignment.left.property.type === AST_NODE_TYPES.Identifier &&
|
|
218
|
+
assignment.left.property.name === 'displayName'
|
|
219
|
+
);
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
const extractDisplayName = (
|
|
223
|
+
expression: DeepReadonly<TSESTree.Expression>,
|
|
224
|
+
): string | undefined => {
|
|
225
|
+
if (
|
|
226
|
+
expression.type === AST_NODE_TYPES.Literal &&
|
|
227
|
+
typeof expression.value === 'string'
|
|
228
|
+
) {
|
|
229
|
+
return expression.value;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (
|
|
233
|
+
expression.type === AST_NODE_TYPES.TemplateLiteral &&
|
|
234
|
+
expression.expressions.length === 0 &&
|
|
235
|
+
expression.quasis.length === 1
|
|
236
|
+
) {
|
|
237
|
+
return expression.quasis[0]?.value.cooked ?? undefined;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
return undefined;
|
|
119
241
|
};
|
|
@@ -41,6 +41,13 @@ describe('display-name', () => {
|
|
|
41
41
|
const notAComponent = someFunction();
|
|
42
42
|
`,
|
|
43
43
|
},
|
|
44
|
+
{
|
|
45
|
+
name: 'Exported component with displayName',
|
|
46
|
+
code: dedent`
|
|
47
|
+
export const MyComponent = React.memo(() => <div>Hello</div>);
|
|
48
|
+
MyComponent.displayName = 'MyComponent';
|
|
49
|
+
`,
|
|
50
|
+
},
|
|
44
51
|
],
|
|
45
52
|
invalid: [
|
|
46
53
|
{
|
|
@@ -50,6 +57,26 @@ describe('display-name', () => {
|
|
|
50
57
|
`,
|
|
51
58
|
errors: [{ messageId: 'missingDisplayName' }],
|
|
52
59
|
},
|
|
60
|
+
{
|
|
61
|
+
name: 'Exported component without displayName',
|
|
62
|
+
code: dedent`
|
|
63
|
+
export const MyComponent = React.memo(() => <div>Hello</div>);
|
|
64
|
+
`,
|
|
65
|
+
errors: [{ messageId: 'missingDisplayName' }],
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
name: 'Component with mismatched displayName',
|
|
69
|
+
code: dedent`
|
|
70
|
+
const MyComponent = React.memo(() => <div>Hello</div>);
|
|
71
|
+
MyComponent.displayName = 'Other';
|
|
72
|
+
`,
|
|
73
|
+
errors: [
|
|
74
|
+
{
|
|
75
|
+
messageId: 'mismatchedDisplayName',
|
|
76
|
+
data: { componentName: 'MyComponent' },
|
|
77
|
+
},
|
|
78
|
+
],
|
|
79
|
+
},
|
|
53
80
|
{
|
|
54
81
|
name: 'Named import without displayName',
|
|
55
82
|
code: dedent`
|
|
@@ -58,19 +85,47 @@ describe('display-name', () => {
|
|
|
58
85
|
`,
|
|
59
86
|
errors: [{ messageId: 'missingDisplayName' }],
|
|
60
87
|
},
|
|
88
|
+
{
|
|
89
|
+
name: 'Named import with mismatched displayName',
|
|
90
|
+
code: dedent`
|
|
91
|
+
import { memo } from 'react';
|
|
92
|
+
const MyComponent = memo(() => <div>Hello</div>);
|
|
93
|
+
MyComponent.displayName = 'Component';
|
|
94
|
+
`,
|
|
95
|
+
errors: [
|
|
96
|
+
{
|
|
97
|
+
messageId: 'mismatchedDisplayName',
|
|
98
|
+
data: { componentName: 'MyComponent' },
|
|
99
|
+
},
|
|
100
|
+
],
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
name: 'Exported component with mismatched displayName',
|
|
104
|
+
code: dedent`
|
|
105
|
+
export const MyComponent = React.memo(() => <div>Hello</div>);
|
|
106
|
+
MyComponent.displayName = 'Component';
|
|
107
|
+
`,
|
|
108
|
+
errors: [
|
|
109
|
+
{
|
|
110
|
+
messageId: 'mismatchedDisplayName',
|
|
111
|
+
data: { componentName: 'MyComponent' },
|
|
112
|
+
},
|
|
113
|
+
],
|
|
114
|
+
},
|
|
61
115
|
],
|
|
62
116
|
});
|
|
63
117
|
});
|
|
64
118
|
|
|
65
|
-
describe('
|
|
66
|
-
tester.run('display-name with
|
|
119
|
+
describe('ignoreName option', () => {
|
|
120
|
+
tester.run('display-name with ignoreName', displayNameRule, {
|
|
67
121
|
valid: [
|
|
68
122
|
{
|
|
69
|
-
name: 'Component
|
|
123
|
+
name: 'Component with mismatched displayName (ignored)',
|
|
70
124
|
code: dedent`
|
|
71
125
|
const MyComponent = React.memo(() => <div>Hello</div>);
|
|
126
|
+
MyComponent.displayName = 'Other';
|
|
72
127
|
`,
|
|
73
|
-
options: [{
|
|
128
|
+
options: [{ ignoreName: 'MyComponent' }],
|
|
74
129
|
},
|
|
75
130
|
{
|
|
76
131
|
name: 'Component with displayName',
|
|
@@ -78,10 +133,19 @@ describe('display-name', () => {
|
|
|
78
133
|
const MyComponent = React.memo(() => <div>Hello</div>);
|
|
79
134
|
MyComponent.displayName = 'MyComponent';
|
|
80
135
|
`,
|
|
81
|
-
options: [{
|
|
136
|
+
options: [{ ignoreName: ['MyComponent'] }],
|
|
137
|
+
},
|
|
138
|
+
],
|
|
139
|
+
invalid: [
|
|
140
|
+
{
|
|
141
|
+
name: 'Component without displayName is still reported',
|
|
142
|
+
code: dedent`
|
|
143
|
+
const MyComponent = React.memo(() => <div>Hello</div>);
|
|
144
|
+
`,
|
|
145
|
+
options: [{ ignoreName: ['MyComponent'] }],
|
|
146
|
+
errors: [{ messageId: 'missingDisplayName' }],
|
|
82
147
|
},
|
|
83
148
|
],
|
|
84
|
-
invalid: [],
|
|
85
149
|
});
|
|
86
150
|
});
|
|
87
151
|
});
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import parser from '@typescript-eslint/parser';
|
|
2
|
+
import { RuleTester } from '@typescript-eslint/rule-tester';
|
|
3
|
+
import { type TSESLint } from '@typescript-eslint/utils';
|
|
4
|
+
import { getReactMemoArrowFunction, isReactApiCall } from './shared.mjs';
|
|
5
|
+
|
|
6
|
+
const tester = new RuleTester({
|
|
7
|
+
languageOptions: {
|
|
8
|
+
parser,
|
|
9
|
+
parserOptions: {
|
|
10
|
+
ecmaVersion: 2020,
|
|
11
|
+
sourceType: 'module',
|
|
12
|
+
ecmaFeatures: { jsx: true },
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
const reactApiRule: TSESLint.RuleModule<'reactApiDetected', readonly []> = {
|
|
18
|
+
meta: {
|
|
19
|
+
type: 'problem',
|
|
20
|
+
docs: { description: 'test helper isReactApiCall' },
|
|
21
|
+
schema: [],
|
|
22
|
+
messages: {
|
|
23
|
+
reactApiDetected: 'React API call detected',
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
defaultOptions: [],
|
|
27
|
+
create: (context) => ({
|
|
28
|
+
CallExpression: (node) => {
|
|
29
|
+
if (isReactApiCall(context, node, 'memo')) {
|
|
30
|
+
context.report({ node, messageId: 'reactApiDetected' });
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
}),
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const reactMemoArrowRule: TSESLint.RuleModule<'arrowDetected', readonly []> = {
|
|
37
|
+
meta: {
|
|
38
|
+
type: 'problem',
|
|
39
|
+
docs: { description: 'test helper getReactMemoArrowFunction' },
|
|
40
|
+
schema: [],
|
|
41
|
+
messages: {
|
|
42
|
+
arrowDetected: 'React.memo received arrow function',
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
defaultOptions: [],
|
|
46
|
+
create: (context) => ({
|
|
47
|
+
CallExpression: (node) => {
|
|
48
|
+
if (!isReactApiCall(context, node, 'memo')) {
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const arrow = getReactMemoArrowFunction(node);
|
|
53
|
+
|
|
54
|
+
if (arrow !== undefined) {
|
|
55
|
+
assert.strictEqual(arrow.type, 'ArrowFunctionExpression');
|
|
56
|
+
|
|
57
|
+
context.report({ node, messageId: 'arrowDetected' });
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
}),
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
describe('shared helpers', () => {
|
|
64
|
+
tester.run('isReactApiCall', reactApiRule, {
|
|
65
|
+
valid: [
|
|
66
|
+
{
|
|
67
|
+
name: 'non React call',
|
|
68
|
+
code: 'const x = fn();',
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
name: 'memo imported from non-react',
|
|
72
|
+
code: `
|
|
73
|
+
import { memo } from 'not-react';
|
|
74
|
+
const Component = memo(() => null);
|
|
75
|
+
`,
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
name: 'React member but different method',
|
|
79
|
+
code: `
|
|
80
|
+
import * as React from 'react';
|
|
81
|
+
const Component = React.useMemo(() => null, []);
|
|
82
|
+
`,
|
|
83
|
+
},
|
|
84
|
+
],
|
|
85
|
+
invalid: [
|
|
86
|
+
{
|
|
87
|
+
name: 'named memo import from react',
|
|
88
|
+
code: `
|
|
89
|
+
import { memo } from 'react';
|
|
90
|
+
const Component = memo(() => null);
|
|
91
|
+
`,
|
|
92
|
+
errors: [{ messageId: 'reactApiDetected' }],
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
name: 'namespace React memo call',
|
|
96
|
+
code: `
|
|
97
|
+
import * as React from 'react';
|
|
98
|
+
const Component = React.memo(() => null);
|
|
99
|
+
`,
|
|
100
|
+
errors: [{ messageId: 'reactApiDetected' }],
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
name: 'global React memo call without import',
|
|
104
|
+
code: `
|
|
105
|
+
const Component = React.memo(() => null);
|
|
106
|
+
`,
|
|
107
|
+
errors: [{ messageId: 'reactApiDetected' }],
|
|
108
|
+
},
|
|
109
|
+
],
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
tester.run('getReactMemoArrowFunction', reactMemoArrowRule, {
|
|
113
|
+
valid: [
|
|
114
|
+
{
|
|
115
|
+
name: 'memo with non-arrow first argument',
|
|
116
|
+
code: `
|
|
117
|
+
import { memo } from 'react';
|
|
118
|
+
function Component() { return null; }
|
|
119
|
+
const Wrapped = memo(Component);
|
|
120
|
+
`,
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
name: 'non React call with arrow argument',
|
|
124
|
+
code: `
|
|
125
|
+
const Wrapped = wrap(() => null);
|
|
126
|
+
`,
|
|
127
|
+
},
|
|
128
|
+
],
|
|
129
|
+
invalid: [
|
|
130
|
+
{
|
|
131
|
+
name: 'memo with arrow function argument',
|
|
132
|
+
code: `
|
|
133
|
+
import { memo } from 'react';
|
|
134
|
+
const Wrapped = memo(() => null);
|
|
135
|
+
`,
|
|
136
|
+
errors: [{ messageId: 'arrowDetected' }],
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
name: 'React namespace memo with arrow function argument',
|
|
140
|
+
code: `
|
|
141
|
+
import * as React from 'react';
|
|
142
|
+
const Wrapped = React.memo(() => null);
|
|
143
|
+
`,
|
|
144
|
+
errors: [{ messageId: 'arrowDetected' }],
|
|
145
|
+
},
|
|
146
|
+
],
|
|
147
|
+
});
|
|
148
|
+
});
|
|
@@ -28,12 +28,14 @@ describe('use-memo-hooks-style', () => {
|
|
|
28
28
|
import { memo, useMemo } from 'react';
|
|
29
29
|
|
|
30
30
|
type Props = Readonly<{
|
|
31
|
-
|
|
31
|
+
value: number;
|
|
32
32
|
}>;
|
|
33
33
|
|
|
34
34
|
const Component = memo<Props>((props) => {
|
|
35
35
|
const memoized = useMemo<number>(() => props.value, [props.value]);
|
|
36
|
+
|
|
36
37
|
const typed: number = useMemo(() => props.value, [props.value]);
|
|
38
|
+
|
|
37
39
|
return <div />;
|
|
38
40
|
});
|
|
39
41
|
`,
|
|
@@ -54,6 +56,40 @@ describe('use-memo-hooks-style', () => {
|
|
|
54
56
|
const value = useMemo(() => 42) as number;
|
|
55
57
|
`,
|
|
56
58
|
},
|
|
59
|
+
{
|
|
60
|
+
name: 'Allow satisfies expression',
|
|
61
|
+
code: dedent`
|
|
62
|
+
import { memo, useMemo } from 'react';
|
|
63
|
+
|
|
64
|
+
type Props = Readonly<{
|
|
65
|
+
value: number;
|
|
66
|
+
}>;
|
|
67
|
+
|
|
68
|
+
const Component = memo<Props>((props) => {
|
|
69
|
+
const value = useMemo(() => props.value, [props.value]) satisfies number;
|
|
70
|
+
|
|
71
|
+
const inner = useMemo(() => props.value satisfies number, [props.value]);
|
|
72
|
+
|
|
73
|
+
return inner + value;
|
|
74
|
+
});
|
|
75
|
+
`,
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
name: 'Allow const assertion',
|
|
79
|
+
code: dedent`
|
|
80
|
+
import { memo, useMemo } from 'react';
|
|
81
|
+
|
|
82
|
+
type Props = Readonly<{
|
|
83
|
+
value: number;
|
|
84
|
+
}>;
|
|
85
|
+
|
|
86
|
+
const Component = memo<Props>((props) => {
|
|
87
|
+
const value = useMemo(() => ({ v: props.value }) as const, [props.value]);
|
|
88
|
+
|
|
89
|
+
return value;
|
|
90
|
+
});
|
|
91
|
+
`,
|
|
92
|
+
},
|
|
57
93
|
],
|
|
58
94
|
invalid: [
|
|
59
95
|
{
|
|
@@ -62,7 +98,7 @@ describe('use-memo-hooks-style', () => {
|
|
|
62
98
|
import { memo, useMemo } from 'react';
|
|
63
99
|
|
|
64
100
|
type Props = Readonly<{
|
|
65
|
-
|
|
101
|
+
value: number;
|
|
66
102
|
}>;
|
|
67
103
|
|
|
68
104
|
const Component = memo<Props>((props) => {
|
|
@@ -82,7 +118,7 @@ describe('use-memo-hooks-style', () => {
|
|
|
82
118
|
import { memo, useMemo } from 'react';
|
|
83
119
|
|
|
84
120
|
type Props = Readonly<{
|
|
85
|
-
|
|
121
|
+
value: number;
|
|
86
122
|
}>;
|
|
87
123
|
|
|
88
124
|
const Component = memo<Props>((props) => {
|
|
@@ -102,11 +138,11 @@ describe('use-memo-hooks-style', () => {
|
|
|
102
138
|
import { memo, useMemo } from 'react';
|
|
103
139
|
|
|
104
140
|
type Props = Readonly<{
|
|
105
|
-
|
|
141
|
+
value: number;
|
|
106
142
|
}>;
|
|
107
143
|
|
|
108
144
|
const Component = memo<Props>((props) => {
|
|
109
|
-
const value = useMemo(() => props.value as number, [props.value])
|
|
145
|
+
const value = useMemo(() => props.value as number, [props.value]);
|
|
110
146
|
return value;
|
|
111
147
|
});
|
|
112
148
|
`,
|
|
@@ -117,16 +153,16 @@ describe('use-memo-hooks-style', () => {
|
|
|
117
153
|
],
|
|
118
154
|
},
|
|
119
155
|
{
|
|
120
|
-
name: 'Disallow satisfies
|
|
156
|
+
name: 'Disallow return type annotation even with satisfies',
|
|
121
157
|
code: dedent`
|
|
122
158
|
import { memo, useMemo } from 'react';
|
|
123
159
|
|
|
124
160
|
type Props = Readonly<{
|
|
125
|
-
|
|
161
|
+
value: number;
|
|
126
162
|
}>;
|
|
127
163
|
|
|
128
164
|
const Component = memo<Props>((props) => {
|
|
129
|
-
const value = useMemo(() => props.value, [props.value]) satisfies number;
|
|
165
|
+
const value = useMemo((): number => props.value, [props.value]) satisfies number;
|
|
130
166
|
return value;
|
|
131
167
|
});
|
|
132
168
|
`,
|
|
@@ -137,16 +173,16 @@ describe('use-memo-hooks-style', () => {
|
|
|
137
173
|
],
|
|
138
174
|
},
|
|
139
175
|
{
|
|
140
|
-
name: 'Disallow
|
|
176
|
+
name: 'Disallow return type annotation even with const assertion',
|
|
141
177
|
code: dedent`
|
|
142
178
|
import { memo, useMemo } from 'react';
|
|
143
179
|
|
|
144
180
|
type Props = Readonly<{
|
|
145
|
-
|
|
181
|
+
value: number;
|
|
146
182
|
}>;
|
|
147
183
|
|
|
148
184
|
const Component = memo<Props>((props) => {
|
|
149
|
-
const value = useMemo(() => props.value
|
|
185
|
+
const value = useMemo((): number => props.value, [props.value]) as const;
|
|
150
186
|
return value;
|
|
151
187
|
});
|
|
152
188
|
`,
|