eslint-config-typed 4.0.5 → 4.0.7

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.
Files changed (58) hide show
  1. package/dist/plugins/react-coding-style/rules/display-name.d.mts +2 -2
  2. package/dist/plugins/react-coding-style/rules/display-name.d.mts.map +1 -1
  3. package/dist/plugins/react-coding-style/rules/display-name.mjs +110 -30
  4. package/dist/plugins/react-coding-style/rules/display-name.mjs.map +1 -1
  5. package/dist/plugins/react-coding-style/rules/rules.d.mts +2 -2
  6. package/dist/rules/eslint-react-coding-style-rules.d.mts +1 -3
  7. package/dist/rules/eslint-react-coding-style-rules.d.mts.map +1 -1
  8. package/dist/rules/eslint-react-coding-style-rules.mjs +1 -1
  9. package/dist/rules/eslint-react-coding-style-rules.mjs.map +1 -1
  10. package/dist/rules/eslint-react-rules.d.mts +1 -1
  11. package/dist/rules/eslint-react-rules.mjs +1 -1
  12. package/dist/rules/eslint-react-rules.mjs.map +1 -1
  13. package/dist/types/rules/eslint-cypress-rules.d.mts +2 -2
  14. package/dist/types/rules/eslint-import-rules.d.mts +214 -168
  15. package/dist/types/rules/eslint-import-rules.d.mts.map +1 -1
  16. package/dist/types/rules/eslint-jest-rules.d.mts +47 -45
  17. package/dist/types/rules/eslint-jest-rules.d.mts.map +1 -1
  18. package/dist/types/rules/eslint-playwright-rules.d.mts +41 -34
  19. package/dist/types/rules/eslint-playwright-rules.d.mts.map +1 -1
  20. package/dist/types/rules/eslint-prefer-arrow-functions-rules.d.mts +6 -2
  21. package/dist/types/rules/eslint-prefer-arrow-functions-rules.d.mts.map +1 -1
  22. package/dist/types/rules/eslint-react-coding-style-rules.d.mts +21 -6
  23. package/dist/types/rules/eslint-react-coding-style-rules.d.mts.map +1 -1
  24. package/dist/types/rules/eslint-react-rules.d.mts +208 -185
  25. package/dist/types/rules/eslint-react-rules.d.mts.map +1 -1
  26. package/dist/types/rules/eslint-rules.d.mts +1244 -1073
  27. package/dist/types/rules/eslint-rules.d.mts.map +1 -1
  28. package/dist/types/rules/eslint-stylistic-rules.d.mts +707 -619
  29. package/dist/types/rules/eslint-stylistic-rules.d.mts.map +1 -1
  30. package/dist/types/rules/eslint-ts-restrictions-rules.d.mts +94 -76
  31. package/dist/types/rules/eslint-ts-restrictions-rules.d.mts.map +1 -1
  32. package/dist/types/rules/eslint-unicorn-rules.d.mts +145 -121
  33. package/dist/types/rules/eslint-unicorn-rules.d.mts.map +1 -1
  34. package/dist/types/rules/eslint-vitest-rules.d.mts +43 -42
  35. package/dist/types/rules/eslint-vitest-rules.d.mts.map +1 -1
  36. package/dist/types/rules/typescript-eslint-rules.d.mts +1973 -1763
  37. package/dist/types/rules/typescript-eslint-rules.d.mts.map +1 -1
  38. package/package.json +58 -57
  39. package/src/plugins/react-coding-style/README.md +4 -3
  40. package/src/plugins/react-coding-style/rules/display-name.mts +160 -38
  41. package/src/plugins/react-coding-style/rules/display-name.test.mts +70 -6
  42. package/src/plugins/react-coding-style/rules/shared.test.mts +148 -0
  43. package/src/plugins/vitest-coding-style/rules/original-assert-api-type.test.mts +147 -143
  44. package/src/rules/eslint-react-coding-style-rules.mts +1 -1
  45. package/src/rules/eslint-react-rules.mts +1 -1
  46. package/src/types/rules/eslint-cypress-rules.mts +2 -2
  47. package/src/types/rules/eslint-import-rules.mts +214 -168
  48. package/src/types/rules/eslint-jest-rules.mts +47 -45
  49. package/src/types/rules/eslint-playwright-rules.mts +41 -34
  50. package/src/types/rules/eslint-prefer-arrow-functions-rules.mts +6 -2
  51. package/src/types/rules/eslint-react-coding-style-rules.mts +21 -6
  52. package/src/types/rules/eslint-react-rules.mts +208 -185
  53. package/src/types/rules/eslint-rules.mts +1229 -1058
  54. package/src/types/rules/eslint-stylistic-rules.mts +707 -619
  55. package/src/types/rules/eslint-ts-restrictions-rules.mts +94 -76
  56. package/src/types/rules/eslint-unicorn-rules.mts +145 -121
  57. package/src/types/rules/eslint-vitest-rules.mts +43 -42
  58. package/src/types/rules/typescript-eslint-rules.mts +1973 -1763
@@ -3,16 +3,16 @@ import {
3
3
  type TSESLint,
4
4
  type TSESTree,
5
5
  } from '@typescript-eslint/utils';
6
- import { expectType } from 'ts-data-forge';
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
- ignoreTranspilerName?: boolean;
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 displayName property for React components created with React.memo',
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
- ignoreTranspilerName: {
33
- type: 'boolean',
32
+ ignoreName: {
34
33
  description:
35
- 'When true, ignores components that get displayName from variable name',
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 ignoreTranspilerName = options.ignoreTranspilerName ?? false;
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
- if (ignoreTranspilerName) {
69
- return;
70
- }
80
+ const assignment = getDisplayNameAssignment(node);
71
81
 
72
- const parent = node.parent;
82
+ if (assignment === undefined) {
83
+ context.report({
84
+ node: castDeepMutable(node),
85
+ messageId: 'missingDisplayName',
86
+ });
73
87
 
74
- expectType<typeof parent.type, AST_NODE_TYPES.VariableDeclaration>('=');
88
+ return;
89
+ }
75
90
 
76
- const grandParent = parent.parent;
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 program = grandParent;
100
+ const displayName = extractDisplayName(assignment.right);
83
101
 
84
- const componentIndex = program.body.indexOf(parent);
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 (componentIndex === -1) {
112
+ if (shouldIgnoreMismatch(componentName)) {
87
113
  return;
88
114
  }
89
115
 
90
- const nextStatement = program.body[componentIndex + 1];
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
- // eslint-disable-next-line total-functions/no-unsafe-type-assertion
108
- node: node.id as never,
109
- messageId: 'missingDisplayName',
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: [{ ignoreTranspilerName: false }],
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('ignoreTranspilerName option', () => {
66
- tester.run('display-name with ignoreTranspilerName', displayNameRule, {
119
+ describe('ignoreName option', () => {
120
+ tester.run('display-name with ignoreName', displayNameRule, {
67
121
  valid: [
68
122
  {
69
- name: 'Component without displayName (ignored)',
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: [{ ignoreTranspilerName: true }],
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: [{ ignoreTranspilerName: true }],
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
+ });