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.
Files changed (51) hide show
  1. package/dist/plugins/react-coding-style/rules/ban-use-imperative-handle-hook.mjs +4 -4
  2. package/dist/plugins/react-coding-style/rules/ban-use-imperative-handle-hook.mjs.map +1 -1
  3. package/dist/plugins/react-coding-style/rules/component-name.d.mts.map +1 -1
  4. package/dist/plugins/react-coding-style/rules/component-name.mjs +5 -3
  5. package/dist/plugins/react-coding-style/rules/component-name.mjs.map +1 -1
  6. package/dist/plugins/react-coding-style/rules/import-style.d.mts +8 -2
  7. package/dist/plugins/react-coding-style/rules/import-style.d.mts.map +1 -1
  8. package/dist/plugins/react-coding-style/rules/import-style.mjs +63 -26
  9. package/dist/plugins/react-coding-style/rules/import-style.mjs.map +1 -1
  10. package/dist/plugins/react-coding-style/rules/props-type-annotation-style.mjs +2 -2
  11. package/dist/plugins/react-coding-style/rules/props-type-annotation-style.mjs.map +1 -1
  12. package/dist/plugins/react-coding-style/rules/react-memo-props-argument-name.mjs +2 -2
  13. package/dist/plugins/react-coding-style/rules/react-memo-props-argument-name.mjs.map +1 -1
  14. package/dist/plugins/react-coding-style/rules/react-memo-type-parameter.mjs +2 -2
  15. package/dist/plugins/react-coding-style/rules/react-memo-type-parameter.mjs.map +1 -1
  16. package/dist/plugins/react-coding-style/rules/rules.d.mts +3 -1
  17. package/dist/plugins/react-coding-style/rules/rules.d.mts.map +1 -1
  18. package/dist/plugins/react-coding-style/rules/shared.d.mts +7 -3
  19. package/dist/plugins/react-coding-style/rules/shared.d.mts.map +1 -1
  20. package/dist/plugins/react-coding-style/rules/shared.mjs +61 -3
  21. package/dist/plugins/react-coding-style/rules/shared.mjs.map +1 -1
  22. package/dist/plugins/react-coding-style/rules/use-memo-hooks-style.d.mts.map +1 -1
  23. package/dist/plugins/react-coding-style/rules/use-memo-hooks-style.mjs +45 -3
  24. package/dist/plugins/react-coding-style/rules/use-memo-hooks-style.mjs.map +1 -1
  25. package/dist/rules/eslint-import-rules.d.mts +1 -3
  26. package/dist/rules/eslint-import-rules.d.mts.map +1 -1
  27. package/dist/rules/eslint-import-rules.mjs +3 -1
  28. package/dist/rules/eslint-import-rules.mjs.map +1 -1
  29. package/dist/rules/eslint-react-coding-style-rules.d.mts +3 -1
  30. package/dist/rules/eslint-react-coding-style-rules.d.mts.map +1 -1
  31. package/dist/rules/eslint-react-coding-style-rules.mjs +2 -2
  32. package/dist/rules/eslint-react-coding-style-rules.mjs.map +1 -1
  33. package/dist/types/rules/eslint-react-coding-style-rules.d.mts +32 -2
  34. package/dist/types/rules/eslint-react-coding-style-rules.d.mts.map +1 -1
  35. package/package.json +1 -1
  36. package/src/plugins/react-coding-style/rules/ban-use-imperative-handle-hook.mts +4 -4
  37. package/src/plugins/react-coding-style/rules/ban-use-imperative-handle-hook.test.mts +50 -18
  38. package/src/plugins/react-coding-style/rules/component-name.mts +6 -3
  39. package/src/plugins/react-coding-style/rules/import-style.mts +92 -34
  40. package/src/plugins/react-coding-style/rules/import-style.test.mts +81 -34
  41. package/src/plugins/react-coding-style/rules/props-type-annotation-style.mts +2 -2
  42. package/src/plugins/react-coding-style/rules/react-memo-props-argument-name.mts +2 -2
  43. package/src/plugins/react-coding-style/rules/react-memo-type-parameter.mts +2 -2
  44. package/src/plugins/react-coding-style/rules/shared.mts +92 -7
  45. package/src/plugins/react-coding-style/rules/use-memo-hooks-style-named.test.mts +162 -0
  46. package/src/plugins/react-coding-style/rules/use-memo-hooks-style-namespace.test.mts +162 -0
  47. package/src/plugins/react-coding-style/rules/use-memo-hooks-style.mts +68 -3
  48. package/src/rules/eslint-import-rules.mts +4 -1
  49. package/src/rules/eslint-react-coding-style-rules.mts +2 -2
  50. package/src/types/rules/eslint-react-coding-style-rules.mts +36 -2
  51. 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 single namespace import named 'React'.
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
- type RuleEntry = Linter.StringSeverity;
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,SAAS,GAAG,MAAM,CAAC,cAAc,CAAC;CAC/C;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;CACrE,CAAC"}
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
  {
2
2
  "name": "eslint-config-typed",
3
- "version": "3.8.1",
3
+ "version": "3.9.0",
4
4
  "private": false,
5
5
  "keywords": [
6
6
  "typescript"
@@ -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 { isReactMemberExpression } from './shared.mjs';
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
- MemberExpression: (node: DeepReadonly<TSESTree.MemberExpression>) => {
22
- if (isReactMemberExpression(node, 'useImperativeHandle')) {
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
- tester.run(ruleName, banUseImperativeHandleHook, {
19
- valid: [],
20
- invalid: [
21
- {
22
- name: 'Disallow React.useImperativeHandle',
23
- code: dedent`
24
- type Props = Readonly<{
25
- readonly ref: unknown;
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
- const Component = React.memo<Props>((props) => {
29
- React.useImperativeHandle(props.ref, () => ({}));
30
- return null;
31
- });
32
- `,
33
- errors: [
45
+ describe('named import (useImperativeHandle)', () => {
46
+ tester.run(ruleName, banUseImperativeHandleHook, {
47
+ valid: [],
48
+ invalid: [
34
49
  {
35
- messageId: 'disallowUseImperativeHandle',
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 { isReactCallExpression } from './shared.mjs';
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 MessageIds = 'namespaceImportRequired' | 'namespaceNameMustBeReact';
3
+ type ImportStyle = 'namespace' | 'named';
4
4
 
5
- // NOTE: React import 方法を `import * as React from 'react'` と namespace import のみに限定するルール。
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
- "Enforces importing React with a single namespace import named 'React'.",
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
- ImportDeclaration: (node) => {
26
- if (node.source.value !== 'react') {
27
- return;
28
- }
29
-
30
- const [firstSpecifier] = node.specifiers;
31
-
32
- if (
33
- firstSpecifier === undefined ||
34
- firstSpecifier.type !== AST_NODE_TYPES.ImportNamespaceSpecifier ||
35
- node.specifiers.length !== 1
36
- ) {
37
- context.report({
38
- node,
39
- messageId: 'namespaceImportRequired',
40
- });
41
-
42
- return;
43
- }
44
-
45
- if (firstSpecifier.local.name !== 'React') {
46
- context.report({
47
- node: firstSpecifier.local,
48
- messageId: 'namespaceNameMustBeReact',
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
- tester.run(ruleName, importStyleRule, {
18
- valid: [
19
- {
20
- code: "import * as React from 'react';",
21
- },
22
- {
23
- code: "import type * as React from 'react';",
24
- },
25
- {
26
- code: "import { useMemo } from 'react-use';",
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
- messageId: 'namespaceImportRequired',
28
+ code: "import { useMemo } from 'react-use';",
35
29
  },
36
30
  ],
37
- },
38
- {
39
- code: "import { useState } from 'react';",
40
- errors: [
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
- messageId: 'namespaceImportRequired',
57
+ code: "import React, * as R from 'react';",
58
+ errors: [
59
+ {
60
+ messageId: 'namespaceImportRequired',
61
+ },
62
+ ],
43
63
  },
44
64
  ],
45
- },
46
- {
47
- code: "import * as R from 'react';",
48
- errors: [
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
- messageId: 'namespaceNameMustBeReact',
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
- messageId: 'namespaceImportRequired',
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, isReactCallExpression } from './shared.mjs';
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 (!isReactCallExpression(node, 'memo')) {
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, isReactCallExpression } from './shared.mjs';
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 (!isReactCallExpression(node, 'memo')) {
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, isReactCallExpression } from './shared.mjs';
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 (!isReactCallExpression(node, 'memo')) {
34
+ if (!isReactApiCall(context, node, 'memo')) {
35
35
  return;
36
36
  }
37
37
 
@@ -1,6 +1,10 @@
1
- import { AST_NODE_TYPES, type TSESTree } from '@typescript-eslint/utils';
1
+ import {
2
+ AST_NODE_TYPES,
3
+ type TSESLint,
4
+ type TSESTree,
5
+ } from '@typescript-eslint/utils';
2
6
 
3
- export const isReactMemberExpression = (
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
- export const isReactCallExpression = (
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
- propertyName: string,
16
- ): boolean =>
17
- node.callee.type === AST_NODE_TYPES.MemberExpression &&
18
- isReactMemberExpression(node.callee, propertyName);
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>,