eslint-config-typed 3.8.1 → 3.9.0
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/plugins/react-coding-style/rules/ban-use-imperative-handle-hook.mjs +4 -4
- package/dist/plugins/react-coding-style/rules/ban-use-imperative-handle-hook.mjs.map +1 -1
- package/dist/plugins/react-coding-style/rules/component-name.d.mts.map +1 -1
- package/dist/plugins/react-coding-style/rules/component-name.mjs +5 -3
- package/dist/plugins/react-coding-style/rules/component-name.mjs.map +1 -1
- package/dist/plugins/react-coding-style/rules/import-style.d.mts +8 -2
- package/dist/plugins/react-coding-style/rules/import-style.d.mts.map +1 -1
- package/dist/plugins/react-coding-style/rules/import-style.mjs +63 -26
- package/dist/plugins/react-coding-style/rules/import-style.mjs.map +1 -1
- package/dist/plugins/react-coding-style/rules/props-type-annotation-style.mjs +2 -2
- package/dist/plugins/react-coding-style/rules/props-type-annotation-style.mjs.map +1 -1
- package/dist/plugins/react-coding-style/rules/react-memo-props-argument-name.mjs +2 -2
- package/dist/plugins/react-coding-style/rules/react-memo-props-argument-name.mjs.map +1 -1
- package/dist/plugins/react-coding-style/rules/react-memo-type-parameter.mjs +2 -2
- package/dist/plugins/react-coding-style/rules/react-memo-type-parameter.mjs.map +1 -1
- package/dist/plugins/react-coding-style/rules/rules.d.mts +3 -1
- package/dist/plugins/react-coding-style/rules/rules.d.mts.map +1 -1
- package/dist/plugins/react-coding-style/rules/shared.d.mts +7 -3
- package/dist/plugins/react-coding-style/rules/shared.d.mts.map +1 -1
- package/dist/plugins/react-coding-style/rules/shared.mjs +61 -3
- package/dist/plugins/react-coding-style/rules/shared.mjs.map +1 -1
- package/dist/plugins/react-coding-style/rules/use-memo-hooks-style.d.mts.map +1 -1
- package/dist/plugins/react-coding-style/rules/use-memo-hooks-style.mjs +45 -3
- package/dist/plugins/react-coding-style/rules/use-memo-hooks-style.mjs.map +1 -1
- package/dist/rules/eslint-import-rules.d.mts +1 -3
- package/dist/rules/eslint-import-rules.d.mts.map +1 -1
- package/dist/rules/eslint-import-rules.mjs +3 -1
- package/dist/rules/eslint-import-rules.mjs.map +1 -1
- package/dist/rules/eslint-react-coding-style-rules.d.mts +3 -1
- package/dist/rules/eslint-react-coding-style-rules.d.mts.map +1 -1
- package/dist/rules/eslint-react-coding-style-rules.mjs +2 -2
- package/dist/rules/eslint-react-coding-style-rules.mjs.map +1 -1
- package/dist/types/rules/eslint-react-coding-style-rules.d.mts +32 -2
- package/dist/types/rules/eslint-react-coding-style-rules.d.mts.map +1 -1
- package/package.json +1 -1
- package/src/plugins/react-coding-style/rules/ban-use-imperative-handle-hook.mts +4 -4
- package/src/plugins/react-coding-style/rules/ban-use-imperative-handle-hook.test.mts +50 -18
- package/src/plugins/react-coding-style/rules/component-name.mts +6 -3
- package/src/plugins/react-coding-style/rules/import-style.mts +92 -34
- package/src/plugins/react-coding-style/rules/import-style.test.mts +81 -34
- package/src/plugins/react-coding-style/rules/props-type-annotation-style.mts +2 -2
- package/src/plugins/react-coding-style/rules/react-memo-props-argument-name.mts +2 -2
- package/src/plugins/react-coding-style/rules/react-memo-type-parameter.mts +2 -2
- package/src/plugins/react-coding-style/rules/shared.mts +92 -7
- package/src/plugins/react-coding-style/rules/use-memo-hooks-style-named.test.mts +162 -0
- package/src/plugins/react-coding-style/rules/use-memo-hooks-style-namespace.test.mts +162 -0
- package/src/plugins/react-coding-style/rules/use-memo-hooks-style.mts +68 -3
- package/src/rules/eslint-import-rules.mts +4 -1
- package/src/rules/eslint-react-coding-style-rules.mts +2 -2
- package/src/types/rules/eslint-react-coding-style-rules.mts +36 -2
- package/src/plugins/react-coding-style/rules/use-memo-hooks-style.test.mts +0 -72
|
@@ -53,7 +53,7 @@ declare namespace ComponentVarTypeAnnotation {
|
|
|
53
53
|
type RuleEntry = Linter.StringSeverity;
|
|
54
54
|
}
|
|
55
55
|
/**
|
|
56
|
-
* Enforces importing React with a
|
|
56
|
+
* Enforces importing React with a specific style (namespace or named imports).
|
|
57
57
|
*
|
|
58
58
|
* ```md
|
|
59
59
|
* | key | value |
|
|
@@ -63,7 +63,36 @@ declare namespace ComponentVarTypeAnnotation {
|
|
|
63
63
|
* ```
|
|
64
64
|
*/
|
|
65
65
|
declare namespace ImportStyle {
|
|
66
|
-
|
|
66
|
+
/**
|
|
67
|
+
* ### schema
|
|
68
|
+
*
|
|
69
|
+
* ```json
|
|
70
|
+
* [
|
|
71
|
+
* {
|
|
72
|
+
* "type": "object",
|
|
73
|
+
* "properties": {
|
|
74
|
+
* "importStyle": {
|
|
75
|
+
* "type": "string",
|
|
76
|
+
* "enum": [
|
|
77
|
+
* "namespace",
|
|
78
|
+
* "named"
|
|
79
|
+
* ],
|
|
80
|
+
* "description": "Import style to enforce: \"namespace\" for `import * as React` or \"named\" for `import { ... }`"
|
|
81
|
+
* }
|
|
82
|
+
* },
|
|
83
|
+
* "additionalProperties": false
|
|
84
|
+
* }
|
|
85
|
+
* ]
|
|
86
|
+
* ```
|
|
87
|
+
*/
|
|
88
|
+
type Options = {
|
|
89
|
+
/**
|
|
90
|
+
* Import style to enforce: "namespace" for `import * as React` or "named"
|
|
91
|
+
* for `import { ... }`
|
|
92
|
+
*/
|
|
93
|
+
readonly importStyle?: 'named' | 'namespace';
|
|
94
|
+
};
|
|
95
|
+
type RuleEntry = Linter.Severity | SpreadOptionsIfIsArray<readonly [Linter.StringSeverity, Options]> | 'off';
|
|
67
96
|
}
|
|
68
97
|
/**
|
|
69
98
|
* Forbids annotating props directly in the arrow function passed to React.memo.
|
|
@@ -143,6 +172,7 @@ export type EslintReactCodingStyleRules = {
|
|
|
143
172
|
};
|
|
144
173
|
export type EslintReactCodingStyleRulesOption = {
|
|
145
174
|
readonly 'react-coding-style/component-name': ComponentName.Options;
|
|
175
|
+
readonly 'react-coding-style/import-style': ImportStyle.Options;
|
|
146
176
|
};
|
|
147
177
|
export {};
|
|
148
178
|
//# sourceMappingURL=eslint-react-coding-style-rules.d.mts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"eslint-react-coding-style-rules.d.mts","sourceRoot":"","sources":["../../../src/types/rules/eslint-react-coding-style-rules.mts"],"names":[],"mappings":"AACA,OAAO,EAAE,KAAK,MAAM,EAAE,MAAM,QAAQ,CAAC;AAErC,KAAK,sBAAsB,CACzB,CAAC,SAAS,SAAS,CAAC,MAAM,CAAC,cAAc,EAAE,OAAO,CAAC,IACjD,CAAC,CAAC,CAAC,CAAC,SAAS,SAAS,OAAO,EAAE,GAC/B,SAAS,CAAC,MAAM,CAAC,cAAc,EAAE,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,GACzC,CAAC,CAAC;AAEN;;;;;;;;;;GAUG;AACH,kBAAU,aAAa,CAAC;IACtB;;;;;;;;;;;;;;;;;;;;OAoBG;IACH,KAAY,OAAO,GAAG;QACpB,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;QAC5B,QAAQ,CAAC,OAAO,CAAC,EAAE,aAAa,CAAC;KAClC,CAAC;IAEF,KAAY,SAAS,GACjB,MAAM,CAAC,QAAQ,GACf,sBAAsB,CAAC,SAAS,CAAC,MAAM,CAAC,cAAc,EAAE,OAAO,CAAC,CAAC,GACjE,KAAK,CAAC;CACX;AAED;;;;;;;;;GASG;AACH,kBAAU,0BAA0B,CAAC;IACnC,KAAY,SAAS,GAAG,MAAM,CAAC,cAAc,CAAC;CAC/C;AAED;;;;;;;;;GASG;AACH,kBAAU,WAAW,CAAC;IACpB,KAAY,
|
|
1
|
+
{"version":3,"file":"eslint-react-coding-style-rules.d.mts","sourceRoot":"","sources":["../../../src/types/rules/eslint-react-coding-style-rules.mts"],"names":[],"mappings":"AACA,OAAO,EAAE,KAAK,MAAM,EAAE,MAAM,QAAQ,CAAC;AAErC,KAAK,sBAAsB,CACzB,CAAC,SAAS,SAAS,CAAC,MAAM,CAAC,cAAc,EAAE,OAAO,CAAC,IACjD,CAAC,CAAC,CAAC,CAAC,SAAS,SAAS,OAAO,EAAE,GAC/B,SAAS,CAAC,MAAM,CAAC,cAAc,EAAE,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,GACzC,CAAC,CAAC;AAEN;;;;;;;;;;GAUG;AACH,kBAAU,aAAa,CAAC;IACtB;;;;;;;;;;;;;;;;;;;;OAoBG;IACH,KAAY,OAAO,GAAG;QACpB,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;QAC5B,QAAQ,CAAC,OAAO,CAAC,EAAE,aAAa,CAAC;KAClC,CAAC;IAEF,KAAY,SAAS,GACjB,MAAM,CAAC,QAAQ,GACf,sBAAsB,CAAC,SAAS,CAAC,MAAM,CAAC,cAAc,EAAE,OAAO,CAAC,CAAC,GACjE,KAAK,CAAC;CACX;AAED;;;;;;;;;GASG;AACH,kBAAU,0BAA0B,CAAC;IACnC,KAAY,SAAS,GAAG,MAAM,CAAC,cAAc,CAAC;CAC/C;AAED;;;;;;;;;GASG;AACH,kBAAU,WAAW,CAAC;IACpB;;;;;;;;;;;;;;;;;;;;;OAqBG;IACH,KAAY,OAAO,GAAG;QACpB;;;WAGG;QACH,QAAQ,CAAC,WAAW,CAAC,EAAE,OAAO,GAAG,WAAW,CAAC;KAC9C,CAAC;IAEF,KAAY,SAAS,GACjB,MAAM,CAAC,QAAQ,GACf,sBAAsB,CAAC,SAAS,CAAC,MAAM,CAAC,cAAc,EAAE,OAAO,CAAC,CAAC,GACjE,KAAK,CAAC;CACX;AAED;;;;;;;;;GASG;AACH,kBAAU,wBAAwB,CAAC;IACjC,KAAY,SAAS,GAAG,MAAM,CAAC,cAAc,CAAC;CAC/C;AAED;;;;;;;;;;GAUG;AACH,kBAAU,0BAA0B,CAAC;IACnC,KAAY,SAAS,GAAG,MAAM,CAAC,cAAc,CAAC;CAC/C;AAED;;;;;;;;;GASG;AACH,kBAAU,sBAAsB,CAAC;IAC/B,KAAY,SAAS,GAAG,MAAM,CAAC,cAAc,CAAC;CAC/C;AAED;;;;;;;;;GASG;AACH,kBAAU,gBAAgB,CAAC;IACzB,KAAY,SAAS,GAAG,MAAM,CAAC,cAAc,CAAC;CAC/C;AAED;;;;;;;;;GASG;AACH,kBAAU,0BAA0B,CAAC;IACnC,KAAY,SAAS,GAAG,MAAM,CAAC,cAAc,CAAC;CAC/C;AAED,MAAM,MAAM,2BAA2B,GAAG;IACxC,QAAQ,CAAC,mCAAmC,EAAE,aAAa,CAAC,SAAS,CAAC;IACtE,QAAQ,CAAC,kDAAkD,EAAE,0BAA0B,CAAC,SAAS,CAAC;IAClG,QAAQ,CAAC,iCAAiC,EAAE,WAAW,CAAC,SAAS,CAAC;IAClE,QAAQ,CAAC,gDAAgD,EAAE,wBAAwB,CAAC,SAAS,CAAC;IAC9F,QAAQ,CAAC,mDAAmD,EAAE,0BAA0B,CAAC,SAAS,CAAC;IACnG,QAAQ,CAAC,8CAA8C,EAAE,sBAAsB,CAAC,SAAS,CAAC;IAC1F,QAAQ,CAAC,wCAAwC,EAAE,gBAAgB,CAAC,SAAS,CAAC;IAC9E,QAAQ,CAAC,mDAAmD,EAAE,0BAA0B,CAAC,SAAS,CAAC;CACpG,CAAC;AAEF,MAAM,MAAM,iCAAiC,GAAG;IAC9C,QAAQ,CAAC,mCAAmC,EAAE,aAAa,CAAC,OAAO,CAAC;IACpE,QAAQ,CAAC,iCAAiC,EAAE,WAAW,CAAC,OAAO,CAAC;CACjE,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type TSESLint, type TSESTree } from '@typescript-eslint/utils';
|
|
2
2
|
import { castDeepMutable } from 'ts-data-forge';
|
|
3
|
-
import {
|
|
3
|
+
import { isReactApiCall } from './shared.mjs';
|
|
4
4
|
|
|
5
5
|
type MessageIds = 'disallowUseImperativeHandle';
|
|
6
6
|
|
|
@@ -18,10 +18,10 @@ export const banUseImperativeHandleHook: TSESLint.RuleModule<MessageIds> = {
|
|
|
18
18
|
},
|
|
19
19
|
},
|
|
20
20
|
create: (context) => ({
|
|
21
|
-
|
|
22
|
-
if (
|
|
21
|
+
CallExpression: (node: DeepReadonly<TSESTree.CallExpression>) => {
|
|
22
|
+
if (isReactApiCall(context, node, 'useImperativeHandle')) {
|
|
23
23
|
context.report({
|
|
24
|
-
node: castDeepMutable(node),
|
|
24
|
+
node: castDeepMutable(node.callee),
|
|
25
25
|
messageId: 'disallowUseImperativeHandle',
|
|
26
26
|
});
|
|
27
27
|
}
|
|
@@ -15,26 +15,58 @@ const tester = new RuleTester({
|
|
|
15
15
|
},
|
|
16
16
|
});
|
|
17
17
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
18
|
+
describe('ban-use-imperative-handle-hook', () => {
|
|
19
|
+
describe('namespace import (React.useImperativeHandle)', () => {
|
|
20
|
+
tester.run(ruleName, banUseImperativeHandleHook, {
|
|
21
|
+
valid: [],
|
|
22
|
+
invalid: [
|
|
23
|
+
{
|
|
24
|
+
name: 'Disallow React.useImperativeHandle',
|
|
25
|
+
code: dedent`
|
|
26
|
+
type Props = Readonly<{
|
|
27
|
+
readonly ref: unknown;
|
|
28
|
+
}>;
|
|
29
|
+
|
|
30
|
+
const Component = React.memo<Props>((props) => {
|
|
31
|
+
React.useImperativeHandle(props.ref, () => ({}));
|
|
32
|
+
return null;
|
|
33
|
+
});
|
|
34
|
+
`,
|
|
35
|
+
errors: [
|
|
36
|
+
{
|
|
37
|
+
messageId: 'disallowUseImperativeHandle',
|
|
38
|
+
},
|
|
39
|
+
],
|
|
40
|
+
},
|
|
41
|
+
],
|
|
42
|
+
});
|
|
43
|
+
});
|
|
27
44
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
`,
|
|
33
|
-
errors: [
|
|
45
|
+
describe('named import (useImperativeHandle)', () => {
|
|
46
|
+
tester.run(ruleName, banUseImperativeHandleHook, {
|
|
47
|
+
valid: [],
|
|
48
|
+
invalid: [
|
|
34
49
|
{
|
|
35
|
-
|
|
50
|
+
name: 'Disallow useImperativeHandle',
|
|
51
|
+
code: dedent`
|
|
52
|
+
import { memo, useImperativeHandle } from 'react';
|
|
53
|
+
|
|
54
|
+
type Props = Readonly<{
|
|
55
|
+
readonly ref: unknown;
|
|
56
|
+
}>;
|
|
57
|
+
|
|
58
|
+
const Component = memo<Props>((props) => {
|
|
59
|
+
useImperativeHandle(props.ref, () => ({}));
|
|
60
|
+
return null;
|
|
61
|
+
});
|
|
62
|
+
`,
|
|
63
|
+
errors: [
|
|
64
|
+
{
|
|
65
|
+
messageId: 'disallowUseImperativeHandle',
|
|
66
|
+
},
|
|
67
|
+
],
|
|
36
68
|
},
|
|
37
69
|
],
|
|
38
|
-
}
|
|
39
|
-
|
|
70
|
+
});
|
|
71
|
+
});
|
|
40
72
|
});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { AST_NODE_TYPES, type TSESLint } from '@typescript-eslint/utils';
|
|
2
|
-
import {
|
|
2
|
+
import { isReactApiCall } from './shared.mjs';
|
|
3
3
|
|
|
4
4
|
type ComponentNameOption = Readonly<{
|
|
5
5
|
maxLength?: number;
|
|
@@ -60,12 +60,15 @@ export const componentNameRule: TSESLint.RuleModule<MessageIds, Options> = {
|
|
|
60
60
|
VariableDeclarator: (node) => {
|
|
61
61
|
if (
|
|
62
62
|
node.id.type !== AST_NODE_TYPES.Identifier ||
|
|
63
|
-
node.init?.type !== AST_NODE_TYPES.CallExpression
|
|
64
|
-
!isReactCallExpression(node.init, 'memo')
|
|
63
|
+
node.init?.type !== AST_NODE_TYPES.CallExpression
|
|
65
64
|
) {
|
|
66
65
|
return;
|
|
67
66
|
}
|
|
68
67
|
|
|
68
|
+
if (!isReactApiCall(context, node.init, 'memo')) {
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
|
|
69
72
|
if (node.id.name.length >= maxLength) {
|
|
70
73
|
context.report({
|
|
71
74
|
node: node.id,
|
|
@@ -1,54 +1,112 @@
|
|
|
1
1
|
import { AST_NODE_TYPES, type TSESLint } from '@typescript-eslint/utils';
|
|
2
2
|
|
|
3
|
-
type
|
|
3
|
+
type ImportStyle = 'namespace' | 'named';
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
type Options = readonly [
|
|
6
|
+
Readonly<{
|
|
7
|
+
importStyle?: ImportStyle;
|
|
8
|
+
}>?,
|
|
9
|
+
];
|
|
10
|
+
|
|
11
|
+
type MessageIds =
|
|
12
|
+
| 'namespaceImportRequired'
|
|
13
|
+
| 'namespaceNameMustBeReact'
|
|
14
|
+
| 'namedImportRequired';
|
|
15
|
+
|
|
16
|
+
// NOTE: React の import 方法を指定するルール。
|
|
17
|
+
// デフォルトは `import * as React from 'react'` と namespace import のみに限定。
|
|
6
18
|
// import を1回で済ませられて便利なのと、 React.* に対する以降のルールを書きやすくするため。
|
|
7
19
|
// tree-shaking に悪影響は無い。
|
|
20
|
+
// オプションで named imports `import { useState, useEffect } from 'react'` も許可可能。
|
|
8
21
|
|
|
9
|
-
export const importStyleRule: TSESLint.RuleModule<MessageIds> = {
|
|
22
|
+
export const importStyleRule: TSESLint.RuleModule<MessageIds, Options> = {
|
|
10
23
|
meta: {
|
|
11
24
|
type: 'suggestion',
|
|
12
25
|
docs: {
|
|
13
26
|
description:
|
|
14
|
-
|
|
27
|
+
'Enforces importing React with a specific style (namespace or named imports).',
|
|
15
28
|
},
|
|
16
|
-
schema: [
|
|
29
|
+
schema: [
|
|
30
|
+
{
|
|
31
|
+
type: 'object',
|
|
32
|
+
properties: {
|
|
33
|
+
importStyle: {
|
|
34
|
+
type: 'string',
|
|
35
|
+
enum: ['namespace', 'named'],
|
|
36
|
+
description:
|
|
37
|
+
'Import style to enforce: "namespace" for `import * as React` or "named" for `import { ... }`',
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
additionalProperties: false,
|
|
41
|
+
},
|
|
42
|
+
],
|
|
17
43
|
messages: {
|
|
18
44
|
namespaceImportRequired:
|
|
19
45
|
"React should be imported as `import * as React from 'react'`.",
|
|
20
46
|
namespaceNameMustBeReact:
|
|
21
47
|
"The namespace name imported from 'react' must be 'React'.",
|
|
48
|
+
namedImportRequired:
|
|
49
|
+
"React should be imported as named imports like `import { useState } from 'react'`.",
|
|
22
50
|
},
|
|
23
51
|
},
|
|
24
|
-
create: (context) =>
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
52
|
+
create: (context) => {
|
|
53
|
+
const options = context.options[0] ?? {};
|
|
54
|
+
const importStyle = options.importStyle ?? 'namespace';
|
|
55
|
+
|
|
56
|
+
return {
|
|
57
|
+
ImportDeclaration: (node) => {
|
|
58
|
+
if (node.source.value !== 'react') {
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
switch (importStyle) {
|
|
63
|
+
case 'named': {
|
|
64
|
+
// Check that all specifiers are named imports
|
|
65
|
+
const hasInvalidSpecifier = node.specifiers.some(
|
|
66
|
+
(spec) =>
|
|
67
|
+
spec.type === AST_NODE_TYPES.ImportNamespaceSpecifier ||
|
|
68
|
+
spec.type === AST_NODE_TYPES.ImportDefaultSpecifier,
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
if (hasInvalidSpecifier) {
|
|
72
|
+
context.report({
|
|
73
|
+
node,
|
|
74
|
+
messageId: 'namedImportRequired',
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
break;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
case 'namespace': {
|
|
82
|
+
// namespace import mode (default)
|
|
83
|
+
const [firstSpecifier] = node.specifiers;
|
|
84
|
+
|
|
85
|
+
if (
|
|
86
|
+
firstSpecifier === undefined ||
|
|
87
|
+
firstSpecifier.type !== AST_NODE_TYPES.ImportNamespaceSpecifier ||
|
|
88
|
+
node.specifiers.length !== 1
|
|
89
|
+
) {
|
|
90
|
+
context.report({
|
|
91
|
+
node,
|
|
92
|
+
messageId: 'namespaceImportRequired',
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (firstSpecifier.local.name !== 'React') {
|
|
99
|
+
context.report({
|
|
100
|
+
node: firstSpecifier.local,
|
|
101
|
+
messageId: 'namespaceNameMustBeReact',
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
break;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
},
|
|
109
|
+
};
|
|
110
|
+
},
|
|
53
111
|
defaultOptions: [],
|
|
54
112
|
};
|
|
@@ -14,50 +14,97 @@ const tester = new RuleTester({
|
|
|
14
14
|
},
|
|
15
15
|
});
|
|
16
16
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
{
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
},
|
|
28
|
-
],
|
|
29
|
-
invalid: [
|
|
30
|
-
{
|
|
31
|
-
code: "import React from 'react';",
|
|
32
|
-
errors: [
|
|
17
|
+
describe('import-style', () => {
|
|
18
|
+
describe('namespace import mode (default)', () => {
|
|
19
|
+
tester.run(ruleName, importStyleRule, {
|
|
20
|
+
valid: [
|
|
21
|
+
{
|
|
22
|
+
code: "import * as React from 'react';",
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
code: "import type * as React from 'react';",
|
|
26
|
+
},
|
|
33
27
|
{
|
|
34
|
-
|
|
28
|
+
code: "import { useMemo } from 'react-use';",
|
|
35
29
|
},
|
|
36
30
|
],
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
31
|
+
invalid: [
|
|
32
|
+
{
|
|
33
|
+
code: "import React from 'react';",
|
|
34
|
+
errors: [
|
|
35
|
+
{
|
|
36
|
+
messageId: 'namespaceImportRequired',
|
|
37
|
+
},
|
|
38
|
+
],
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
code: "import { useState } from 'react';",
|
|
42
|
+
errors: [
|
|
43
|
+
{
|
|
44
|
+
messageId: 'namespaceImportRequired',
|
|
45
|
+
},
|
|
46
|
+
],
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
code: "import * as R from 'react';",
|
|
50
|
+
errors: [
|
|
51
|
+
{
|
|
52
|
+
messageId: 'namespaceNameMustBeReact',
|
|
53
|
+
},
|
|
54
|
+
],
|
|
55
|
+
},
|
|
41
56
|
{
|
|
42
|
-
|
|
57
|
+
code: "import React, * as R from 'react';",
|
|
58
|
+
errors: [
|
|
59
|
+
{
|
|
60
|
+
messageId: 'namespaceImportRequired',
|
|
61
|
+
},
|
|
62
|
+
],
|
|
43
63
|
},
|
|
44
64
|
],
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
describe('named import mode', () => {
|
|
69
|
+
tester.run(ruleName, importStyleRule, {
|
|
70
|
+
valid: [
|
|
71
|
+
{
|
|
72
|
+
code: "import { useState } from 'react';",
|
|
73
|
+
options: [{ importStyle: 'named' }],
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
code: "import { useState, useEffect } from 'react';",
|
|
77
|
+
options: [{ importStyle: 'named' }],
|
|
78
|
+
},
|
|
49
79
|
{
|
|
50
|
-
|
|
80
|
+
code: "import { useMemo } from 'react-use';",
|
|
81
|
+
options: [{ importStyle: 'named' }],
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
code: "import type { FC } from 'react';",
|
|
85
|
+
options: [{ importStyle: 'named' }],
|
|
51
86
|
},
|
|
52
87
|
],
|
|
53
|
-
|
|
54
|
-
{
|
|
55
|
-
code: "import React, * as R from 'react';",
|
|
56
|
-
errors: [
|
|
88
|
+
invalid: [
|
|
57
89
|
{
|
|
58
|
-
|
|
90
|
+
code: "import * as React from 'react';",
|
|
91
|
+
options: [{ importStyle: 'named' }],
|
|
92
|
+
errors: [
|
|
93
|
+
{
|
|
94
|
+
messageId: 'namedImportRequired',
|
|
95
|
+
},
|
|
96
|
+
],
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
code: "import React from 'react';",
|
|
100
|
+
options: [{ importStyle: 'named' }],
|
|
101
|
+
errors: [
|
|
102
|
+
{
|
|
103
|
+
messageId: 'namedImportRequired',
|
|
104
|
+
},
|
|
105
|
+
],
|
|
59
106
|
},
|
|
60
107
|
],
|
|
61
|
-
}
|
|
62
|
-
|
|
108
|
+
});
|
|
109
|
+
});
|
|
63
110
|
});
|
|
@@ -4,7 +4,7 @@ import {
|
|
|
4
4
|
type TSESTree,
|
|
5
5
|
} from '@typescript-eslint/utils';
|
|
6
6
|
import { castDeepMutable } from 'ts-data-forge';
|
|
7
|
-
import { getReactMemoArrowFunction,
|
|
7
|
+
import { getReactMemoArrowFunction, isReactApiCall } from './shared.mjs';
|
|
8
8
|
|
|
9
9
|
type MessageIds = 'disallowPropsTypeAnnotation';
|
|
10
10
|
|
|
@@ -25,7 +25,7 @@ export const propsTypeAnnotationStyleRule: TSESLint.RuleModule<MessageIds> = {
|
|
|
25
25
|
},
|
|
26
26
|
create: (context) => ({
|
|
27
27
|
CallExpression: (node: DeepReadonly<TSESTree.CallExpression>) => {
|
|
28
|
-
if (!
|
|
28
|
+
if (!isReactApiCall(context, node, 'memo')) {
|
|
29
29
|
return;
|
|
30
30
|
}
|
|
31
31
|
|
|
@@ -4,7 +4,7 @@ import {
|
|
|
4
4
|
type TSESTree,
|
|
5
5
|
} from '@typescript-eslint/utils';
|
|
6
6
|
import { castDeepMutable } from 'ts-data-forge';
|
|
7
|
-
import { getReactMemoArrowFunction,
|
|
7
|
+
import { getReactMemoArrowFunction, isReactApiCall } from './shared.mjs';
|
|
8
8
|
|
|
9
9
|
type MessageIds = 'propsParamMustBeNamedProps' | 'propsParamMustBeIdentifier';
|
|
10
10
|
|
|
@@ -29,7 +29,7 @@ export const reactMemoPropsArgumentNameRule: TSESLint.RuleModule<MessageIds> = {
|
|
|
29
29
|
},
|
|
30
30
|
create: (context) => ({
|
|
31
31
|
CallExpression: (node: DeepReadonly<TSESTree.CallExpression>) => {
|
|
32
|
-
if (!
|
|
32
|
+
if (!isReactApiCall(context, node, 'memo')) {
|
|
33
33
|
return;
|
|
34
34
|
}
|
|
35
35
|
|
|
@@ -4,7 +4,7 @@ import {
|
|
|
4
4
|
type TSESTree,
|
|
5
5
|
} from '@typescript-eslint/utils';
|
|
6
6
|
import { castDeepMutable } from 'ts-data-forge';
|
|
7
|
-
import { getReactMemoArrowFunction,
|
|
7
|
+
import { getReactMemoArrowFunction, isReactApiCall } from './shared.mjs';
|
|
8
8
|
|
|
9
9
|
type MessageIds =
|
|
10
10
|
| 'requirePropsTypeParameter'
|
|
@@ -31,7 +31,7 @@ export const reactMemoTypeParameterRule: TSESLint.RuleModule<MessageIds> = {
|
|
|
31
31
|
},
|
|
32
32
|
create: (context) => ({
|
|
33
33
|
CallExpression: (node: DeepReadonly<TSESTree.CallExpression>) => {
|
|
34
|
-
if (!
|
|
34
|
+
if (!isReactApiCall(context, node, 'memo')) {
|
|
35
35
|
return;
|
|
36
36
|
}
|
|
37
37
|
|
|
@@ -1,6 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
AST_NODE_TYPES,
|
|
3
|
+
type TSESLint,
|
|
4
|
+
type TSESTree,
|
|
5
|
+
} from '@typescript-eslint/utils';
|
|
2
6
|
|
|
3
|
-
|
|
7
|
+
const isReactMemberExpression = (
|
|
4
8
|
node: DeepReadonly<TSESTree.MemberExpression>,
|
|
5
9
|
propertyName: string,
|
|
6
10
|
): boolean =>
|
|
@@ -10,12 +14,93 @@ export const isReactMemberExpression = (
|
|
|
10
14
|
node.property.name === propertyName &&
|
|
11
15
|
!node.computed;
|
|
12
16
|
|
|
13
|
-
|
|
17
|
+
/**
|
|
18
|
+
* Check if the given identifier is imported from "react"
|
|
19
|
+
*/
|
|
20
|
+
const isImportedFromReact = (
|
|
21
|
+
context: DeepReadonly<TSESLint.RuleContext<string, readonly unknown[]>>,
|
|
22
|
+
identifierName: string,
|
|
23
|
+
): boolean => {
|
|
24
|
+
const sourceCode = context.sourceCode;
|
|
25
|
+
|
|
26
|
+
// Get the global scope to search for imports
|
|
27
|
+
const globalScope = sourceCode.scopeManager?.globalScope ?? undefined;
|
|
28
|
+
|
|
29
|
+
if (globalScope === undefined) {
|
|
30
|
+
// If no scope manager, assume it's React (for backward compatibility)
|
|
31
|
+
return true;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Search through all scopes for the variable
|
|
35
|
+
const scopes = [globalScope, ...globalScope.childScopes];
|
|
36
|
+
|
|
37
|
+
const variables = scopes
|
|
38
|
+
.map((scope) => scope.set.get(identifierName))
|
|
39
|
+
.filter((v): v is NonNullable<typeof v> => v !== undefined);
|
|
40
|
+
|
|
41
|
+
if (variables.length === 0) {
|
|
42
|
+
// If variable is not found in any scope, assume it's a global (React)
|
|
43
|
+
// This handles cases where React is used without explicit import
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Check if any variable is imported from 'react'
|
|
48
|
+
for (const variable of variables) {
|
|
49
|
+
for (const def of variable.defs) {
|
|
50
|
+
// Type narrowing: def.type is a string literal type, not enum
|
|
51
|
+
if (
|
|
52
|
+
Object.hasOwn(def, 'type') &&
|
|
53
|
+
typeof def.type === 'string' &&
|
|
54
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-enum-comparison
|
|
55
|
+
def.type === 'ImportBinding'
|
|
56
|
+
) {
|
|
57
|
+
const importDeclaration = def.parent;
|
|
58
|
+
|
|
59
|
+
if (
|
|
60
|
+
importDeclaration.type === AST_NODE_TYPES.ImportDeclaration &&
|
|
61
|
+
importDeclaration.source.value === 'react'
|
|
62
|
+
) {
|
|
63
|
+
return true;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Found an import, but not from 'react'
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Variable was found but not as an import, it's a local definition
|
|
73
|
+
return false;
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Check if the given CallExpression is a React API call.
|
|
78
|
+
* Supports both namespace imports (React.memo) and named imports (memo).
|
|
79
|
+
* Verifies that the identifier is actually imported from "react".
|
|
80
|
+
*/
|
|
81
|
+
export const isReactApiCall = (
|
|
82
|
+
context: DeepReadonly<TSESLint.RuleContext<string, readonly unknown[]>>,
|
|
14
83
|
node: DeepReadonly<TSESTree.CallExpression>,
|
|
15
|
-
|
|
16
|
-
): boolean =>
|
|
17
|
-
|
|
18
|
-
|
|
84
|
+
apiName: string,
|
|
85
|
+
): boolean => {
|
|
86
|
+
// Check for named import: memo(...)
|
|
87
|
+
if (
|
|
88
|
+
node.callee.type === AST_NODE_TYPES.Identifier &&
|
|
89
|
+
node.callee.name === apiName
|
|
90
|
+
) {
|
|
91
|
+
return isImportedFromReact(context, apiName);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Check for namespace import: React.memo(...)
|
|
95
|
+
if (
|
|
96
|
+
node.callee.type === AST_NODE_TYPES.MemberExpression &&
|
|
97
|
+
isReactMemberExpression(node.callee, apiName)
|
|
98
|
+
) {
|
|
99
|
+
return isImportedFromReact(context, 'React');
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return false;
|
|
103
|
+
};
|
|
19
104
|
|
|
20
105
|
export const getReactMemoArrowFunction = (
|
|
21
106
|
node: DeepReadonly<TSESTree.CallExpression>,
|