eslint-config-typed 2.3.2 → 3.1.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 (152) hide show
  1. package/README.md +319 -156
  2. package/dist/configs/plugins.d.mts +1 -1
  3. package/dist/configs/plugins.d.mts.map +1 -1
  4. package/dist/configs/plugins.mjs +5 -2
  5. package/dist/configs/plugins.mjs.map +1 -1
  6. package/dist/configs/react-base.d.mts.map +1 -1
  7. package/dist/configs/react-base.mjs +2 -0
  8. package/dist/configs/react-base.mjs.map +1 -1
  9. package/dist/configs/typescript-without-rules.mjs +4 -4
  10. package/dist/configs/typescript-without-rules.mjs.map +1 -1
  11. package/dist/configs/typescript.mjs +8 -8
  12. package/dist/configs/typescript.mjs.map +1 -1
  13. package/dist/entry-point.d.mts +1 -1
  14. package/dist/entry-point.d.mts.map +1 -1
  15. package/dist/entry-point.mjs +2 -1
  16. package/dist/entry-point.mjs.map +1 -1
  17. package/dist/index.mjs +3 -1
  18. package/dist/index.mjs.map +1 -1
  19. package/dist/plugins/index.d.mts +1 -0
  20. package/dist/plugins/index.d.mts.map +1 -1
  21. package/dist/plugins/index.mjs +1 -0
  22. package/dist/plugins/index.mjs.map +1 -1
  23. package/dist/plugins/react-coding-style/index.d.mts +2 -0
  24. package/dist/plugins/react-coding-style/index.d.mts.map +1 -0
  25. package/dist/plugins/react-coding-style/index.mjs +2 -0
  26. package/dist/plugins/react-coding-style/index.mjs.map +1 -0
  27. package/dist/plugins/react-coding-style/plugin.d.mts +3 -0
  28. package/dist/plugins/react-coding-style/plugin.d.mts.map +1 -0
  29. package/dist/plugins/react-coding-style/plugin.mjs +8 -0
  30. package/dist/plugins/react-coding-style/plugin.mjs.map +1 -0
  31. package/dist/plugins/react-coding-style/rules/ban-use-imperative-handle-hook.d.mts +5 -0
  32. package/dist/plugins/react-coding-style/rules/ban-use-imperative-handle-hook.d.mts.map +1 -0
  33. package/dist/plugins/react-coding-style/rules/ban-use-imperative-handle-hook.mjs +29 -0
  34. package/dist/plugins/react-coding-style/rules/ban-use-imperative-handle-hook.mjs.map +1 -0
  35. package/dist/plugins/react-coding-style/rules/component-name.d.mts +10 -0
  36. package/dist/plugins/react-coding-style/rules/component-name.d.mts.map +1 -0
  37. package/dist/plugins/react-coding-style/rules/component-name.mjs +78 -0
  38. package/dist/plugins/react-coding-style/rules/component-name.mjs.map +1 -0
  39. package/dist/plugins/react-coding-style/rules/component-var-type-annotation.d.mts +5 -0
  40. package/dist/plugins/react-coding-style/rules/component-var-type-annotation.d.mts.map +1 -0
  41. package/dist/plugins/react-coding-style/rules/component-var-type-annotation.mjs +41 -0
  42. package/dist/plugins/react-coding-style/rules/component-var-type-annotation.mjs.map +1 -0
  43. package/dist/plugins/react-coding-style/rules/import-style.d.mts +5 -0
  44. package/dist/plugins/react-coding-style/rules/import-style.d.mts.map +1 -0
  45. package/dist/plugins/react-coding-style/rules/import-style.mjs +45 -0
  46. package/dist/plugins/react-coding-style/rules/import-style.mjs.map +1 -0
  47. package/dist/plugins/react-coding-style/rules/index.d.mts +2 -0
  48. package/dist/plugins/react-coding-style/rules/index.d.mts.map +1 -0
  49. package/dist/plugins/react-coding-style/rules/index.mjs +2 -0
  50. package/dist/plugins/react-coding-style/rules/index.mjs.map +1 -0
  51. package/dist/plugins/react-coding-style/rules/props-type-annotation-style.d.mts +5 -0
  52. package/dist/plugins/react-coding-style/rules/props-type-annotation-style.d.mts.map +1 -0
  53. package/dist/plugins/react-coding-style/rules/props-type-annotation-style.mjs +44 -0
  54. package/dist/plugins/react-coding-style/rules/props-type-annotation-style.mjs.map +1 -0
  55. package/dist/plugins/react-coding-style/rules/react-memo-props-argument-name.d.mts +5 -0
  56. package/dist/plugins/react-coding-style/rules/react-memo-props-argument-name.d.mts.map +1 -0
  57. package/dist/plugins/react-coding-style/rules/react-memo-props-argument-name.mjs +55 -0
  58. package/dist/plugins/react-coding-style/rules/react-memo-props-argument-name.mjs.map +1 -0
  59. package/dist/plugins/react-coding-style/rules/react-memo-type-parameter.d.mts +5 -0
  60. package/dist/plugins/react-coding-style/rules/react-memo-type-parameter.d.mts.map +1 -0
  61. package/dist/plugins/react-coding-style/rules/react-memo-type-parameter.mjs +78 -0
  62. package/dist/plugins/react-coding-style/rules/react-memo-type-parameter.mjs.map +1 -0
  63. package/dist/plugins/react-coding-style/rules/rules.d.mts +14 -0
  64. package/dist/plugins/react-coding-style/rules/rules.d.mts.map +1 -0
  65. package/dist/plugins/react-coding-style/rules/rules.mjs +22 -0
  66. package/dist/plugins/react-coding-style/rules/rules.mjs.map +1 -0
  67. package/dist/plugins/react-coding-style/rules/shared.d.mts +5 -0
  68. package/dist/plugins/react-coding-style/rules/shared.d.mts.map +1 -0
  69. package/dist/plugins/react-coding-style/rules/shared.mjs +22 -0
  70. package/dist/plugins/react-coding-style/rules/shared.mjs.map +1 -0
  71. package/dist/plugins/react-coding-style/rules/use-memo-hooks-style.d.mts +5 -0
  72. package/dist/plugins/react-coding-style/rules/use-memo-hooks-style.d.mts.map +1 -0
  73. package/dist/plugins/react-coding-style/rules/use-memo-hooks-style.mjs +42 -0
  74. package/dist/plugins/react-coding-style/rules/use-memo-hooks-style.mjs.map +1 -0
  75. package/dist/plugins/total-functions/rules/common.d.mts +2 -1
  76. package/dist/plugins/total-functions/rules/common.d.mts.map +1 -1
  77. package/dist/plugins/total-functions/rules/fp-ts.d.mts +2 -1
  78. package/dist/plugins/total-functions/rules/fp-ts.d.mts.map +1 -1
  79. package/dist/plugins/total-functions/rules/unsafe-assignment-rule.d.mts +2 -1
  80. package/dist/plugins/total-functions/rules/unsafe-assignment-rule.d.mts.map +1 -1
  81. package/dist/rules/eslint-import-rules.d.mts +50 -47
  82. package/dist/rules/eslint-import-rules.d.mts.map +1 -1
  83. package/dist/rules/eslint-import-rules.mjs +55 -54
  84. package/dist/rules/eslint-import-rules.mjs.map +1 -1
  85. package/dist/rules/eslint-react-coding-style-rules.d.mts +13 -0
  86. package/dist/rules/eslint-react-coding-style-rules.d.mts.map +1 -0
  87. package/dist/rules/eslint-react-coding-style-rules.mjs +14 -0
  88. package/dist/rules/eslint-react-coding-style-rules.mjs.map +1 -0
  89. package/dist/rules/eslint-rules.d.mts +0 -57
  90. package/dist/rules/eslint-rules.d.mts.map +1 -1
  91. package/dist/rules/eslint-rules.mjs +1 -151
  92. package/dist/rules/eslint-rules.mjs.map +1 -1
  93. package/dist/rules/index.d.mts +1 -0
  94. package/dist/rules/index.d.mts.map +1 -1
  95. package/dist/rules/index.mjs +2 -1
  96. package/dist/rules/index.mjs.map +1 -1
  97. package/dist/rules/typescript-eslint-rules.mjs +1 -1
  98. package/dist/types/define-known-rules.d.mts +2 -2
  99. package/dist/types/define-known-rules.d.mts.map +1 -1
  100. package/dist/types/define-known-rules.mjs.map +1 -1
  101. package/dist/types/flat-config.d.mts.map +1 -1
  102. package/dist/types/rules/eslint-import-rules.d.mts +295 -336
  103. package/dist/types/rules/eslint-import-rules.d.mts.map +1 -1
  104. package/dist/types/rules/eslint-react-coding-style-rules.d.mts +148 -0
  105. package/dist/types/rules/eslint-react-coding-style-rules.d.mts.map +1 -0
  106. package/dist/types/rules/eslint-react-coding-style-rules.mjs +2 -0
  107. package/dist/types/rules/eslint-react-coding-style-rules.mjs.map +1 -0
  108. package/dist/types/rules/index.d.mts +1 -0
  109. package/dist/types/rules/index.d.mts.map +1 -1
  110. package/package.json +2 -5
  111. package/src/configs/plugins.mts +8 -3
  112. package/src/configs/react-base.mts +2 -0
  113. package/src/configs/typescript-without-rules.mts +4 -4
  114. package/src/configs/typescript.mts +8 -8
  115. package/src/entry-point.mts +2 -1
  116. package/src/plugins/index.mts +1 -0
  117. package/src/plugins/react-coding-style/README.md +36 -0
  118. package/src/plugins/react-coding-style/index.mts +1 -0
  119. package/src/plugins/react-coding-style/plugin.mts +6 -0
  120. package/src/plugins/react-coding-style/rules/ban-use-imperative-handle-hook.mts +31 -0
  121. package/src/plugins/react-coding-style/rules/ban-use-imperative-handle-hook.test.mts +40 -0
  122. package/src/plugins/react-coding-style/rules/component-name.mts +93 -0
  123. package/src/plugins/react-coding-style/rules/component-name.test.mts +72 -0
  124. package/src/plugins/react-coding-style/rules/component-var-type-annotation.mts +54 -0
  125. package/src/plugins/react-coding-style/rules/component-var-type-annotation.test.mts +62 -0
  126. package/src/plugins/react-coding-style/rules/import-style.mts +53 -0
  127. package/src/plugins/react-coding-style/rules/import-style.test.mts +63 -0
  128. package/src/plugins/react-coding-style/rules/index.mts +1 -0
  129. package/src/plugins/react-coding-style/rules/props-type-annotation-style.mts +59 -0
  130. package/src/plugins/react-coding-style/rules/props-type-annotation-style.test.mts +71 -0
  131. package/src/plugins/react-coding-style/rules/react-memo-props-argument-name.mts +70 -0
  132. package/src/plugins/react-coding-style/rules/react-memo-props-argument-name.test.mts +56 -0
  133. package/src/plugins/react-coding-style/rules/react-memo-type-parameter.mts +102 -0
  134. package/src/plugins/react-coding-style/rules/react-memo-type-parameter.test.mts +98 -0
  135. package/src/plugins/react-coding-style/rules/rules.mts +20 -0
  136. package/src/plugins/react-coding-style/rules/shared.mts +33 -0
  137. package/src/plugins/react-coding-style/rules/use-memo-hooks-style.mts +52 -0
  138. package/src/plugins/react-coding-style/rules/use-memo-hooks-style.test.mts +72 -0
  139. package/src/plugins/total-functions/rules/common.mts +1 -1
  140. package/src/plugins/total-functions/rules/fp-ts.mts +1 -1
  141. package/src/plugins/total-functions/rules/no-enums.mts +1 -1
  142. package/src/plugins/total-functions/rules/unsafe-assignment-rule.mts +1 -1
  143. package/src/rules/eslint-import-rules.mts +58 -55
  144. package/src/rules/eslint-react-coding-style-rules.mts +14 -0
  145. package/src/rules/eslint-rules.mts +0 -181
  146. package/src/rules/index.mts +1 -0
  147. package/src/rules/typescript-eslint-rules.mts +1 -1
  148. package/src/types/define-known-rules.mts +2 -0
  149. package/src/types/flat-config.mts +0 -1
  150. package/src/types/rules/eslint-import-rules.mts +305 -362
  151. package/src/types/rules/eslint-react-coding-style-rules.mts +166 -0
  152. package/src/types/rules/index.mts +1 -0
@@ -0,0 +1,93 @@
1
+ import { AST_NODE_TYPES, type TSESLint } from '@typescript-eslint/utils';
2
+ import { isReactCallExpression } from './shared.mjs';
3
+
4
+ type ComponentNameOption = Readonly<{
5
+ maxLength?: number;
6
+ pattern?: RegExp;
7
+ }>;
8
+
9
+ type Options = readonly [ComponentNameOption] | readonly [];
10
+
11
+ type MessageIds = 'componentNameTooLong' | 'componentNameDoesNotMatch';
12
+
13
+ // NOTE:
14
+ // `const Name = React.memo<Props>((props) => {`
15
+ // が1行に収まるようにするためのルール。1行に収まらないとインデントが増えてコンポーネント実装の可読性が下がりやすくなるため。
16
+ // component props の型名や引数名の制約と併せて、 prettier のデフォルト print-width = 80 で export を省いた場合の最大長としてデフォルト値 42 を設定している。
17
+ // 抵触する場合、以下のように書き換える。
18
+ //
19
+ // const Component = React.memo<Props>((props) => {
20
+ // ...
21
+ // });
22
+ //
23
+ // export { Component as SomeComponent };
24
+
25
+ export const componentNameRule: TSESLint.RuleModule<MessageIds, Options> = {
26
+ meta: {
27
+ type: 'suggestion',
28
+ docs: {
29
+ description:
30
+ 'Enforces naming conventions for variables assigned to React.memo(...) components.',
31
+ },
32
+ schema: [
33
+ {
34
+ type: 'object',
35
+ properties: {
36
+ maxLength: {
37
+ type: 'integer',
38
+ minimum: 1,
39
+ },
40
+ pattern: {
41
+ type: 'object',
42
+ },
43
+ },
44
+ additionalProperties: false,
45
+ },
46
+ ],
47
+ messages: {
48
+ componentNameTooLong:
49
+ 'The component name length should be less than {{ maxLength }}. Consider rewrite as `const Component = React.memo<Props>((props) => { }); export { Component as SomeComponent };`.',
50
+ componentNameDoesNotMatch:
51
+ 'The component name should match {{ pattern }}.',
52
+ },
53
+ },
54
+ create: (context) => {
55
+ const option = context.options[0];
56
+ const maxLength = option?.maxLength ?? 42;
57
+ const pattern = option?.pattern;
58
+
59
+ return {
60
+ VariableDeclarator: (node) => {
61
+ if (
62
+ node.id.type !== AST_NODE_TYPES.Identifier ||
63
+ node.init?.type !== AST_NODE_TYPES.CallExpression ||
64
+ !isReactCallExpression(node.init, 'memo')
65
+ ) {
66
+ return;
67
+ }
68
+
69
+ if (node.id.name.length >= maxLength) {
70
+ context.report({
71
+ node: node.id,
72
+ messageId: 'componentNameTooLong',
73
+ data: {
74
+ maxLength: String(maxLength),
75
+ },
76
+ });
77
+ return;
78
+ }
79
+
80
+ if (pattern !== undefined && !pattern.test(node.id.name)) {
81
+ context.report({
82
+ node: node.id,
83
+ messageId: 'componentNameDoesNotMatch',
84
+ data: {
85
+ pattern: String(pattern),
86
+ },
87
+ });
88
+ }
89
+ },
90
+ };
91
+ },
92
+ defaultOptions: [],
93
+ };
@@ -0,0 +1,72 @@
1
+ import parser from '@typescript-eslint/parser';
2
+ import { RuleTester } from '@typescript-eslint/rule-tester';
3
+ import dedent from 'dedent';
4
+ import { componentNameRule } from './component-name.mjs';
5
+
6
+ const ruleName = 'component-name';
7
+
8
+ const tester = new RuleTester({
9
+ languageOptions: {
10
+ parser,
11
+ parserOptions: {
12
+ ecmaVersion: 2020,
13
+ sourceType: 'module',
14
+ },
15
+ },
16
+ });
17
+
18
+ tester.run(ruleName, componentNameRule, {
19
+ valid: [
20
+ {
21
+ code: dedent`
22
+ const Component = React.memo<Props>((props) => {
23
+ return React.createElement('div', props);
24
+ });
25
+ `,
26
+ options: [
27
+ {
28
+ maxLength: 20,
29
+ pattern: /^Component$/u,
30
+ },
31
+ ],
32
+ },
33
+ ],
34
+ invalid: [
35
+ {
36
+ name: 'Name too long',
37
+ code: dedent`
38
+ const VeryLongComponentName = React.memo<Props>((props) => {
39
+ return React.createElement('div', props);
40
+ });
41
+ `,
42
+ options: [
43
+ {
44
+ maxLength: 10,
45
+ },
46
+ ],
47
+ errors: [
48
+ {
49
+ messageId: 'componentNameTooLong',
50
+ },
51
+ ],
52
+ },
53
+ {
54
+ name: 'Name does not match pattern',
55
+ code: dedent`
56
+ const component = React.memo<Props>((props) => {
57
+ return React.createElement('div', props);
58
+ });
59
+ `,
60
+ options: [
61
+ {
62
+ pattern: /^Component$/u,
63
+ },
64
+ ],
65
+ errors: [
66
+ {
67
+ messageId: 'componentNameDoesNotMatch',
68
+ },
69
+ ],
70
+ },
71
+ ],
72
+ });
@@ -0,0 +1,54 @@
1
+ import {
2
+ AST_NODE_TYPES,
3
+ type TSESLint,
4
+ type TSESTree,
5
+ } from '@typescript-eslint/utils';
6
+ import { castDeepMutable } from 'ts-data-forge';
7
+
8
+ type MessageIds = 'disallowReactFunctionalComponentTypes';
9
+
10
+ // NOTE: React.FC による型注釈があれば React.memo を使うように促すルール。
11
+ // React.FC で型注釈されていない React.memo 化されていないコンポーネントは別途検出する必要がある。
12
+
13
+ export const componentVarTypeAnnotationRule: TSESLint.RuleModule<MessageIds> = {
14
+ meta: {
15
+ type: 'suggestion',
16
+ docs: {
17
+ description:
18
+ 'Disallows using React.FC / React.FunctionComponent type annotations.',
19
+ },
20
+ schema: [],
21
+ messages: {
22
+ disallowReactFunctionalComponentTypes:
23
+ 'Use React.memo<Props>((props) => { ... }) instead.',
24
+ },
25
+ },
26
+ create: (context) => ({
27
+ TSTypeReference: (node: DeepReadonly<TSESTree.TSTypeReference>) => {
28
+ if (isReactFunctionalComponentReference(node.typeName)) {
29
+ context.report({
30
+ node: castDeepMutable(node),
31
+ messageId: 'disallowReactFunctionalComponentTypes',
32
+ });
33
+ }
34
+ },
35
+ }),
36
+ defaultOptions: [],
37
+ };
38
+
39
+ const isReactFunctionalComponentReference = (
40
+ node: DeepReadonly<TSESTree.EntityName>,
41
+ ): boolean => {
42
+ if (node.type !== AST_NODE_TYPES.TSQualifiedName) {
43
+ return false;
44
+ }
45
+
46
+ const left = node.left;
47
+ const right = node.right;
48
+
49
+ return (
50
+ left.type === AST_NODE_TYPES.Identifier &&
51
+ left.name === 'React' &&
52
+ (right.name === 'FC' || right.name === 'FunctionComponent')
53
+ );
54
+ };
@@ -0,0 +1,62 @@
1
+ import parser from '@typescript-eslint/parser';
2
+ import { RuleTester } from '@typescript-eslint/rule-tester';
3
+ import dedent from 'dedent';
4
+ import { componentVarTypeAnnotationRule } from './component-var-type-annotation.mjs';
5
+
6
+ const ruleName = 'component-var-type-annotation';
7
+
8
+ const tester = new RuleTester({
9
+ languageOptions: {
10
+ parser,
11
+ parserOptions: {
12
+ ecmaVersion: 2020,
13
+ sourceType: 'module',
14
+ },
15
+ },
16
+ });
17
+
18
+ tester.run(ruleName, componentVarTypeAnnotationRule, {
19
+ valid: [
20
+ {
21
+ code: dedent`
22
+ type Props = Readonly<{
23
+ readonly value: number;
24
+ }>;
25
+
26
+ const Component = React.memo<Props>((props) => {
27
+ return React.createElement('div', props);
28
+ });
29
+ `,
30
+ },
31
+ ],
32
+ invalid: [
33
+ {
34
+ code: dedent`
35
+ type Props = Readonly<{
36
+ readonly value: number;
37
+ }>;
38
+
39
+ type Component = React.FC<Props>;
40
+ `,
41
+ errors: [
42
+ {
43
+ messageId: 'disallowReactFunctionalComponentTypes',
44
+ },
45
+ ],
46
+ },
47
+ {
48
+ code: dedent`
49
+ type Props = Readonly<{
50
+ readonly value: number;
51
+ }>;
52
+
53
+ type Component = React.FunctionComponent<Props>;
54
+ `,
55
+ errors: [
56
+ {
57
+ messageId: 'disallowReactFunctionalComponentTypes',
58
+ },
59
+ ],
60
+ },
61
+ ],
62
+ });
@@ -0,0 +1,53 @@
1
+ import { AST_NODE_TYPES, type TSESLint } from '@typescript-eslint/utils';
2
+
3
+ type MessageIds = 'namespaceImportRequired' | 'namespaceNameMustBeReact';
4
+
5
+ // NOTE: React の import 方法を `import * as React from 'react'` と namespace import のみに限定するルール。
6
+ // import を1回で済ませられて便利なのと、 React.* に対する以降のルールを書きやすくするため。
7
+ // tree-shaking に悪影響は無い。
8
+
9
+ export const importStyleRule: TSESLint.RuleModule<MessageIds> = {
10
+ meta: {
11
+ type: 'suggestion',
12
+ docs: {
13
+ description:
14
+ "Enforces importing React with a single namespace import named 'React'.",
15
+ },
16
+ schema: [],
17
+ messages: {
18
+ namespaceImportRequired:
19
+ "React should be imported as `import * as React from 'react'`.",
20
+ namespaceNameMustBeReact:
21
+ "The namespace name imported from 'react' must be 'React'.",
22
+ },
23
+ },
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
+ return;
42
+ }
43
+
44
+ if (firstSpecifier.local.name !== 'React') {
45
+ context.report({
46
+ node: firstSpecifier.local,
47
+ messageId: 'namespaceNameMustBeReact',
48
+ });
49
+ }
50
+ },
51
+ }),
52
+ defaultOptions: [],
53
+ };
@@ -0,0 +1,63 @@
1
+ import parser from '@typescript-eslint/parser';
2
+ import { RuleTester } from '@typescript-eslint/rule-tester';
3
+ import { importStyleRule } from './import-style.mjs';
4
+
5
+ const ruleName = 'import-style';
6
+
7
+ const tester = new RuleTester({
8
+ languageOptions: {
9
+ parser,
10
+ parserOptions: {
11
+ ecmaVersion: 2020,
12
+ sourceType: 'module',
13
+ },
14
+ },
15
+ });
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: [
33
+ {
34
+ messageId: 'namespaceImportRequired',
35
+ },
36
+ ],
37
+ },
38
+ {
39
+ code: "import { useState } from 'react';",
40
+ errors: [
41
+ {
42
+ messageId: 'namespaceImportRequired',
43
+ },
44
+ ],
45
+ },
46
+ {
47
+ code: "import * as R from 'react';",
48
+ errors: [
49
+ {
50
+ messageId: 'namespaceNameMustBeReact',
51
+ },
52
+ ],
53
+ },
54
+ {
55
+ code: "import React, * as R from 'react';",
56
+ errors: [
57
+ {
58
+ messageId: 'namespaceImportRequired',
59
+ },
60
+ ],
61
+ },
62
+ ],
63
+ });
@@ -0,0 +1 @@
1
+ export * from './rules.mjs';
@@ -0,0 +1,59 @@
1
+ import {
2
+ AST_NODE_TYPES,
3
+ type TSESLint,
4
+ type TSESTree,
5
+ } from '@typescript-eslint/utils';
6
+ import { castDeepMutable } from 'ts-data-forge';
7
+ import { getReactMemoArrowFunction, isReactCallExpression } from './shared.mjs';
8
+
9
+ type MessageIds = 'disallowPropsTypeAnnotation';
10
+
11
+ // 前提: Arrow function の使用が強制されていること。
12
+
13
+ export const propsTypeAnnotationStyleRule: TSESLint.RuleModule<MessageIds> = {
14
+ meta: {
15
+ type: 'suggestion',
16
+ docs: {
17
+ description:
18
+ 'Forbids annotating props directly in the arrow function passed to React.memo.',
19
+ },
20
+ schema: [],
21
+ messages: {
22
+ disallowPropsTypeAnnotation:
23
+ 'Replace `React.memo((props: Props) => { ... })` with `React.memo<Props>((props) => { ... })`.',
24
+ },
25
+ },
26
+ create: (context) => ({
27
+ CallExpression: (node: DeepReadonly<TSESTree.CallExpression>) => {
28
+ if (!isReactCallExpression(node, 'memo')) {
29
+ return;
30
+ }
31
+
32
+ const arrowFunction = getReactMemoArrowFunction(node);
33
+
34
+ if (
35
+ arrowFunction === undefined ||
36
+ arrowFunction.body.type !== AST_NODE_TYPES.BlockStatement
37
+ ) {
38
+ return;
39
+ }
40
+
41
+ const [firstParam] = arrowFunction.params;
42
+
43
+ if (
44
+ firstParam === undefined ||
45
+ firstParam.type !== AST_NODE_TYPES.Identifier
46
+ ) {
47
+ return;
48
+ }
49
+
50
+ if (firstParam.typeAnnotation !== undefined) {
51
+ context.report({
52
+ node: castDeepMutable(firstParam.typeAnnotation),
53
+ messageId: 'disallowPropsTypeAnnotation',
54
+ });
55
+ }
56
+ },
57
+ }),
58
+ defaultOptions: [],
59
+ };
@@ -0,0 +1,71 @@
1
+ import parser from '@typescript-eslint/parser';
2
+ import { RuleTester } from '@typescript-eslint/rule-tester';
3
+ import dedent from 'dedent';
4
+ import { propsTypeAnnotationStyleRule } from './props-type-annotation-style.mjs';
5
+
6
+ const ruleName = 'props-type-annotation-style';
7
+
8
+ const tester = new RuleTester({
9
+ languageOptions: {
10
+ parser,
11
+ parserOptions: {
12
+ ecmaVersion: 2020,
13
+ sourceType: 'module',
14
+ },
15
+ },
16
+ });
17
+
18
+ tester.run(ruleName, propsTypeAnnotationStyleRule, {
19
+ valid: [
20
+ {
21
+ code: dedent`
22
+ type Props = Readonly<{
23
+ readonly value: number;
24
+ }>;
25
+
26
+ const Component = React.memo<Props>((props) => {
27
+ return React.createElement('div', props);
28
+ });
29
+ `,
30
+ },
31
+ {
32
+ code: dedent`
33
+ const NonComponent = someFunction((props: Props) => props.value);
34
+ `,
35
+ },
36
+ ],
37
+ invalid: [
38
+ {
39
+ code: dedent`
40
+ type Props = Readonly<{
41
+ readonly value: number;
42
+ }>;
43
+
44
+ const Component = React.memo((props: Props) => {
45
+ return React.createElement('div', props);
46
+ });
47
+ `,
48
+ errors: [
49
+ {
50
+ messageId: 'disallowPropsTypeAnnotation',
51
+ },
52
+ ],
53
+ },
54
+ {
55
+ code: dedent`
56
+ type Props = Readonly<{
57
+ readonly value: number;
58
+ }>;
59
+
60
+ const Component = React.memo<Props>((props: Props) => {
61
+ return React.createElement('div', props);
62
+ });
63
+ `,
64
+ errors: [
65
+ {
66
+ messageId: 'disallowPropsTypeAnnotation',
67
+ },
68
+ ],
69
+ },
70
+ ],
71
+ });
@@ -0,0 +1,70 @@
1
+ import {
2
+ AST_NODE_TYPES,
3
+ type TSESLint,
4
+ type TSESTree,
5
+ } from '@typescript-eslint/utils';
6
+ import { castDeepMutable } from 'ts-data-forge';
7
+ import { getReactMemoArrowFunction, isReactCallExpression } from './shared.mjs';
8
+
9
+ type MessageIds = 'propsParamMustBeNamedProps' | 'propsParamMustBeIdentifier';
10
+
11
+ // NOTE: component props を "props" という名前に限定する。
12
+ // return 文を含む component の props を "props" という名前の変数に限定する。
13
+ // 前提: component が React.memo でラップされていること。Arrow function の使用が強制されていること。
14
+
15
+ export const reactMemoPropsArgumentNameRule: TSESLint.RuleModule<MessageIds> = {
16
+ meta: {
17
+ type: 'suggestion',
18
+ docs: {
19
+ description:
20
+ "Enforces the arrow function passed to React.memo to use a single argument named 'props'.",
21
+ },
22
+ schema: [],
23
+ messages: {
24
+ propsParamMustBeNamedProps:
25
+ "The argument name of the arrow function passed to React.memo should be 'props'.",
26
+ propsParamMustBeIdentifier:
27
+ "The props of a component containing a return statement are limited to a variable named 'props'.",
28
+ },
29
+ },
30
+ create: (context) => ({
31
+ CallExpression: (node: DeepReadonly<TSESTree.CallExpression>) => {
32
+ if (!isReactCallExpression(node, 'memo')) {
33
+ return;
34
+ }
35
+
36
+ const arrowFunction = getReactMemoArrowFunction(node);
37
+
38
+ // Detect `React.memo<Props>(({ prop1, prop2 }) => { ... })`
39
+ if (
40
+ arrowFunction === undefined ||
41
+ arrowFunction.body.type !== AST_NODE_TYPES.BlockStatement
42
+ ) {
43
+ return;
44
+ }
45
+
46
+ const [firstParam] = arrowFunction.params;
47
+
48
+ if (firstParam === undefined) {
49
+ return;
50
+ }
51
+
52
+ if (firstParam.type === AST_NODE_TYPES.Identifier) {
53
+ // Restrict props argument name to be "props"
54
+ if (firstParam.name !== 'props') {
55
+ context.report({
56
+ node: castDeepMutable(firstParam),
57
+ messageId: 'propsParamMustBeNamedProps',
58
+ });
59
+ }
60
+ return;
61
+ }
62
+
63
+ context.report({
64
+ node: castDeepMutable(firstParam),
65
+ messageId: 'propsParamMustBeIdentifier',
66
+ });
67
+ },
68
+ }),
69
+ defaultOptions: [],
70
+ };
@@ -0,0 +1,56 @@
1
+ import parser from '@typescript-eslint/parser';
2
+ import { RuleTester } from '@typescript-eslint/rule-tester';
3
+ import dedent from 'dedent';
4
+ import { reactMemoPropsArgumentNameRule } from './react-memo-props-argument-name.mjs';
5
+
6
+ const ruleName = 'react-memo-props-argument-name';
7
+
8
+ const tester = new RuleTester({
9
+ languageOptions: {
10
+ parser,
11
+ parserOptions: {
12
+ ecmaVersion: 2020,
13
+ sourceType: 'module',
14
+ },
15
+ },
16
+ });
17
+
18
+ tester.run(ruleName, reactMemoPropsArgumentNameRule, {
19
+ valid: [
20
+ {
21
+ code: dedent`
22
+ const Component = React.memo<Props>((props) => {
23
+ return React.createElement('div', props);
24
+ });
25
+ `,
26
+ },
27
+ ],
28
+ invalid: [
29
+ {
30
+ name: 'Require identifier named props',
31
+ code: dedent`
32
+ const Component = React.memo<Props>((properties) => {
33
+ return React.createElement('div', properties);
34
+ });
35
+ `,
36
+ errors: [
37
+ {
38
+ messageId: 'propsParamMustBeNamedProps',
39
+ },
40
+ ],
41
+ },
42
+ {
43
+ name: 'Disallow destructuring',
44
+ code: dedent`
45
+ const Component = React.memo<Props>(({ value }) => {
46
+ return React.createElement('div', { value });
47
+ });
48
+ `,
49
+ errors: [
50
+ {
51
+ messageId: 'propsParamMustBeIdentifier',
52
+ },
53
+ ],
54
+ },
55
+ ],
56
+ });