eslint-config-typed 4.2.1 → 4.3.1

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 (170) hide show
  1. package/README.md +4 -0
  2. package/dist/configs/immer.d.mts +3 -0
  3. package/dist/configs/immer.d.mts.map +1 -0
  4. package/dist/configs/immer.mjs +11 -0
  5. package/dist/configs/immer.mjs.map +1 -0
  6. package/dist/configs/index.d.mts +2 -0
  7. package/dist/configs/index.d.mts.map +1 -1
  8. package/dist/configs/index.mjs +2 -0
  9. package/dist/configs/index.mjs.map +1 -1
  10. package/dist/configs/plugins.d.mts +1 -1
  11. package/dist/configs/plugins.d.mts.map +1 -1
  12. package/dist/configs/plugins.mjs +2 -0
  13. package/dist/configs/plugins.mjs.map +1 -1
  14. package/dist/configs/ts-data-forge.d.mts +3 -0
  15. package/dist/configs/ts-data-forge.d.mts.map +1 -0
  16. package/dist/configs/ts-data-forge.mjs +11 -0
  17. package/dist/configs/ts-data-forge.mjs.map +1 -0
  18. package/dist/configs/typescript.d.mts.map +1 -1
  19. package/dist/configs/typescript.mjs +0 -2
  20. package/dist/configs/typescript.mjs.map +1 -1
  21. package/dist/entry-point.mjs +4 -0
  22. package/dist/entry-point.mjs.map +1 -1
  23. package/dist/index.mjs +4 -0
  24. package/dist/index.mjs.map +1 -1
  25. package/dist/plugins/index.d.mts +1 -0
  26. package/dist/plugins/index.d.mts.map +1 -1
  27. package/dist/plugins/index.mjs +1 -0
  28. package/dist/plugins/index.mjs.map +1 -1
  29. package/dist/plugins/ts-data-forge/index.d.mts +2 -0
  30. package/dist/plugins/ts-data-forge/index.d.mts.map +1 -0
  31. package/dist/plugins/ts-data-forge/index.mjs +2 -0
  32. package/dist/plugins/ts-data-forge/index.mjs.map +1 -0
  33. package/dist/plugins/ts-data-forge/plugin.d.mts +3 -0
  34. package/dist/plugins/ts-data-forge/plugin.d.mts.map +1 -0
  35. package/dist/plugins/ts-data-forge/plugin.mjs +8 -0
  36. package/dist/plugins/ts-data-forge/plugin.mjs.map +1 -0
  37. package/dist/plugins/ts-data-forge/rules/branded-number-types.d.mts +5 -0
  38. package/dist/plugins/ts-data-forge/rules/branded-number-types.d.mts.map +1 -0
  39. package/dist/plugins/ts-data-forge/rules/branded-number-types.mjs +35 -0
  40. package/dist/plugins/ts-data-forge/rules/branded-number-types.mjs.map +1 -0
  41. package/dist/plugins/ts-data-forge/rules/constants.d.mts +2 -0
  42. package/dist/plugins/ts-data-forge/rules/constants.d.mts.map +1 -0
  43. package/dist/plugins/ts-data-forge/rules/constants.mjs +4 -0
  44. package/dist/plugins/ts-data-forge/rules/constants.mjs.map +1 -0
  45. package/dist/plugins/ts-data-forge/rules/import-utils.d.mts +5 -0
  46. package/dist/plugins/ts-data-forge/rules/import-utils.d.mts.map +1 -0
  47. package/dist/plugins/ts-data-forge/rules/import-utils.mjs +31 -0
  48. package/dist/plugins/ts-data-forge/rules/import-utils.mjs.map +1 -0
  49. package/dist/plugins/ts-data-forge/rules/index.d.mts +2 -0
  50. package/dist/plugins/ts-data-forge/rules/index.d.mts.map +1 -0
  51. package/dist/plugins/ts-data-forge/rules/index.mjs +2 -0
  52. package/dist/plugins/ts-data-forge/rules/index.mjs.map +1 -0
  53. package/dist/plugins/ts-data-forge/rules/prefer-arr-is-array-at-least-length.d.mts +6 -0
  54. package/dist/plugins/ts-data-forge/rules/prefer-arr-is-array-at-least-length.d.mts.map +1 -0
  55. package/dist/plugins/ts-data-forge/rules/prefer-arr-is-array-at-least-length.mjs +94 -0
  56. package/dist/plugins/ts-data-forge/rules/prefer-arr-is-array-at-least-length.mjs.map +1 -0
  57. package/dist/plugins/ts-data-forge/rules/prefer-arr-is-array-of-length.d.mts +6 -0
  58. package/dist/plugins/ts-data-forge/rules/prefer-arr-is-array-of-length.d.mts.map +1 -0
  59. package/dist/plugins/ts-data-forge/rules/prefer-arr-is-array-of-length.mjs +95 -0
  60. package/dist/plugins/ts-data-forge/rules/prefer-arr-is-array-of-length.mjs.map +1 -0
  61. package/dist/plugins/ts-data-forge/rules/prefer-arr-is-array.d.mts +6 -0
  62. package/dist/plugins/ts-data-forge/rules/prefer-arr-is-array.d.mts.map +1 -0
  63. package/dist/plugins/ts-data-forge/rules/prefer-arr-is-array.mjs +72 -0
  64. package/dist/plugins/ts-data-forge/rules/prefer-arr-is-array.mjs.map +1 -0
  65. package/dist/plugins/ts-data-forge/rules/prefer-arr-is-non-empty.d.mts +6 -0
  66. package/dist/plugins/ts-data-forge/rules/prefer-arr-is-non-empty.d.mts.map +1 -0
  67. package/dist/plugins/ts-data-forge/rules/prefer-arr-is-non-empty.mjs +71 -0
  68. package/dist/plugins/ts-data-forge/rules/prefer-arr-is-non-empty.mjs.map +1 -0
  69. package/dist/plugins/ts-data-forge/rules/prefer-arr-sum.d.mts +6 -0
  70. package/dist/plugins/ts-data-forge/rules/prefer-arr-sum.d.mts.map +1 -0
  71. package/dist/plugins/ts-data-forge/rules/prefer-arr-sum.mjs +183 -0
  72. package/dist/plugins/ts-data-forge/rules/prefer-arr-sum.mjs.map +1 -0
  73. package/dist/plugins/ts-data-forge/rules/prefer-as-int.d.mts +6 -0
  74. package/dist/plugins/ts-data-forge/rules/prefer-as-int.d.mts.map +1 -0
  75. package/dist/plugins/ts-data-forge/rules/prefer-as-int.mjs +86 -0
  76. package/dist/plugins/ts-data-forge/rules/prefer-as-int.mjs.map +1 -0
  77. package/dist/plugins/ts-data-forge/rules/prefer-is-non-null-object.d.mts +6 -0
  78. package/dist/plugins/ts-data-forge/rules/prefer-is-non-null-object.d.mts.map +1 -0
  79. package/dist/plugins/ts-data-forge/rules/prefer-is-non-null-object.mjs +103 -0
  80. package/dist/plugins/ts-data-forge/rules/prefer-is-non-null-object.mjs.map +1 -0
  81. package/dist/plugins/ts-data-forge/rules/prefer-is-record-and-has-key.d.mts +6 -0
  82. package/dist/plugins/ts-data-forge/rules/prefer-is-record-and-has-key.d.mts.map +1 -0
  83. package/dist/plugins/ts-data-forge/rules/prefer-is-record-and-has-key.mjs +105 -0
  84. package/dist/plugins/ts-data-forge/rules/prefer-is-record-and-has-key.mjs.map +1 -0
  85. package/dist/plugins/ts-data-forge/rules/prefer-range-for-loop.d.mts +6 -0
  86. package/dist/plugins/ts-data-forge/rules/prefer-range-for-loop.d.mts.map +1 -0
  87. package/dist/plugins/ts-data-forge/rules/prefer-range-for-loop.mjs +130 -0
  88. package/dist/plugins/ts-data-forge/rules/prefer-range-for-loop.mjs.map +1 -0
  89. package/dist/plugins/ts-data-forge/rules/rules.d.mts +12 -0
  90. package/dist/plugins/ts-data-forge/rules/rules.d.mts.map +1 -0
  91. package/dist/plugins/ts-data-forge/rules/rules.mjs +24 -0
  92. package/dist/plugins/ts-data-forge/rules/rules.mjs.map +1 -0
  93. package/dist/plugins/ts-restrictions/rules/check-destructuring-completeness.d.mts.map +1 -1
  94. package/dist/plugins/ts-restrictions/rules/check-destructuring-completeness.mjs +6 -8
  95. package/dist/plugins/ts-restrictions/rules/check-destructuring-completeness.mjs.map +1 -1
  96. package/dist/plugins/ts-restrictions/rules/no-restricted-cast-name.d.mts.map +1 -1
  97. package/dist/plugins/ts-restrictions/rules/no-restricted-cast-name.mjs +11 -15
  98. package/dist/plugins/ts-restrictions/rules/no-restricted-cast-name.mjs.map +1 -1
  99. package/dist/rules/eslint-jest-rules.d.mts +3 -0
  100. package/dist/rules/eslint-jest-rules.d.mts.map +1 -1
  101. package/dist/rules/eslint-jest-rules.mjs +3 -0
  102. package/dist/rules/eslint-jest-rules.mjs.map +1 -1
  103. package/dist/rules/eslint-ts-data-forge-rules.d.mts +12 -0
  104. package/dist/rules/eslint-ts-data-forge-rules.d.mts.map +1 -0
  105. package/dist/rules/eslint-ts-data-forge-rules.mjs +14 -0
  106. package/dist/rules/eslint-ts-data-forge-rules.mjs.map +1 -0
  107. package/dist/rules/eslint-vitest-rules.d.mts +2 -2
  108. package/dist/rules/eslint-vitest-rules.mjs +2 -2
  109. package/dist/rules/eslint-vitest-rules.mjs.map +1 -1
  110. package/dist/rules/index.d.mts +1 -0
  111. package/dist/rules/index.d.mts.map +1 -1
  112. package/dist/rules/index.mjs +1 -0
  113. package/dist/rules/index.mjs.map +1 -1
  114. package/dist/types/define-known-rules.d.mts +2 -2
  115. package/dist/types/define-known-rules.d.mts.map +1 -1
  116. package/dist/types/define-known-rules.mjs.map +1 -1
  117. package/dist/types/rules/eslint-jest-rules.d.mts +119 -67
  118. package/dist/types/rules/eslint-jest-rules.d.mts.map +1 -1
  119. package/dist/types/rules/eslint-ts-data-forge-rules.d.mts +147 -0
  120. package/dist/types/rules/eslint-ts-data-forge-rules.d.mts.map +1 -0
  121. package/dist/types/rules/eslint-ts-data-forge-rules.mjs +2 -0
  122. package/dist/types/rules/eslint-ts-data-forge-rules.mjs.map +1 -0
  123. package/dist/types/rules/eslint-vitest-rules.d.mts +41 -20
  124. package/dist/types/rules/eslint-vitest-rules.d.mts.map +1 -1
  125. package/dist/types/rules/index.d.mts +1 -0
  126. package/dist/types/rules/index.d.mts.map +1 -1
  127. package/package.json +15 -15
  128. package/src/configs/immer.mts +9 -0
  129. package/src/configs/index.mts +2 -0
  130. package/src/configs/plugins.mts +3 -0
  131. package/src/configs/ts-data-forge.mts +11 -0
  132. package/src/configs/typescript.mts +0 -2
  133. package/src/plugins/index.mts +1 -0
  134. package/src/plugins/strict-dependencies/rules/resolve-import-path.test.mts +7 -9
  135. package/src/plugins/ts-data-forge/index.mts +1 -0
  136. package/src/plugins/ts-data-forge/plugin.mts +6 -0
  137. package/src/plugins/ts-data-forge/rules/branded-number-types.mts +36 -0
  138. package/src/plugins/ts-data-forge/rules/constants.mts +1 -0
  139. package/src/plugins/ts-data-forge/rules/import-utils.mts +56 -0
  140. package/src/plugins/ts-data-forge/rules/index.mts +1 -0
  141. package/src/plugins/ts-data-forge/rules/prefer-arr-is-array-at-least-length.mts +140 -0
  142. package/src/plugins/ts-data-forge/rules/prefer-arr-is-array-at-least-length.test.mts +175 -0
  143. package/src/plugins/ts-data-forge/rules/prefer-arr-is-array-of-length.mts +144 -0
  144. package/src/plugins/ts-data-forge/rules/prefer-arr-is-array-of-length.test.mts +183 -0
  145. package/src/plugins/ts-data-forge/rules/prefer-arr-is-array.mts +97 -0
  146. package/src/plugins/ts-data-forge/rules/prefer-arr-is-array.test.mts +62 -0
  147. package/src/plugins/ts-data-forge/rules/prefer-arr-is-non-empty.mts +106 -0
  148. package/src/plugins/ts-data-forge/rules/prefer-arr-is-non-empty.test.mts +83 -0
  149. package/src/plugins/ts-data-forge/rules/prefer-arr-sum.mts +269 -0
  150. package/src/plugins/ts-data-forge/rules/prefer-arr-sum.test.mts +171 -0
  151. package/src/plugins/ts-data-forge/rules/prefer-as-int.mts +130 -0
  152. package/src/plugins/ts-data-forge/rules/prefer-as-int.test.mts +267 -0
  153. package/src/plugins/ts-data-forge/rules/prefer-is-non-null-object.mts +144 -0
  154. package/src/plugins/ts-data-forge/rules/prefer-is-non-null-object.test.mts +156 -0
  155. package/src/plugins/ts-data-forge/rules/prefer-is-record-and-has-key.mts +158 -0
  156. package/src/plugins/ts-data-forge/rules/prefer-is-record-and-has-key.test.mts +191 -0
  157. package/src/plugins/ts-data-forge/rules/prefer-range-for-loop.mts +181 -0
  158. package/src/plugins/ts-data-forge/rules/prefer-range-for-loop.test.mts +156 -0
  159. package/src/plugins/ts-data-forge/rules/rules.mts +22 -0
  160. package/src/plugins/ts-restrictions/rules/check-destructuring-completeness.mts +6 -8
  161. package/src/plugins/ts-restrictions/rules/no-restricted-cast-name.mts +11 -15
  162. package/src/rules/eslint-jest-rules.mts +3 -0
  163. package/src/rules/eslint-ts-data-forge-rules.mts +13 -0
  164. package/src/rules/eslint-vitest-rules.mts +2 -2
  165. package/src/rules/index.mts +1 -0
  166. package/src/types/define-known-rules.mts +2 -0
  167. package/src/types/rules/eslint-jest-rules.mts +122 -67
  168. package/src/types/rules/eslint-ts-data-forge-rules.mts +156 -0
  169. package/src/types/rules/eslint-vitest-rules.mts +46 -21
  170. package/src/types/rules/index.mts +1 -0
@@ -0,0 +1,158 @@
1
+ import {
2
+ AST_NODE_TYPES,
3
+ type TSESLint,
4
+ type TSESTree,
5
+ } from '@typescript-eslint/utils';
6
+ import {
7
+ buildImportFixes,
8
+ getNamedImports,
9
+ getTsDataForgeImport,
10
+ } from './import-utils.mjs';
11
+
12
+ type Options = readonly [];
13
+
14
+ type MessageIds = 'useIsRecordAndHasKey';
15
+
16
+ export const preferIsRecordAndHasKey: TSESLint.RuleModule<MessageIds, Options> =
17
+ {
18
+ meta: {
19
+ type: 'suggestion',
20
+ docs: {
21
+ description:
22
+ 'Replace `Object.hasOwn(obj, key)` or `key in obj` with `isRecord(obj) && hasKey(obj, key)` from ts-data-forge',
23
+ },
24
+ fixable: 'code',
25
+ schema: [],
26
+ messages: {
27
+ useIsRecordAndHasKey:
28
+ 'Replace `{{original}}` with `isRecord({{objName}}) && hasKey({{objName}}, {{keyName}})` from ts-data-forge.',
29
+ },
30
+ },
31
+
32
+ create: (context) => {
33
+ const sourceCode = context.sourceCode;
34
+
35
+ const program = sourceCode.ast;
36
+
37
+ const tsDataForgeImport = getTsDataForgeImport(program);
38
+
39
+ const mut_nodesToFix: {
40
+ node: TSESTree.CallExpression | TSESTree.BinaryExpression;
41
+ objExpression: TSESTree.Expression;
42
+ keyExpression: TSESTree.Expression;
43
+ }[] = [];
44
+
45
+ return {
46
+ // Handle Object.hasOwn(obj, key)
47
+ CallExpression: (node) => {
48
+ if (
49
+ node.callee.type !== AST_NODE_TYPES.MemberExpression ||
50
+ node.callee.object.type !== AST_NODE_TYPES.Identifier ||
51
+ node.callee.object.name !== 'Object' ||
52
+ node.callee.property.type !== AST_NODE_TYPES.Identifier ||
53
+ node.callee.property.name !== 'hasOwn'
54
+ ) {
55
+ return;
56
+ }
57
+
58
+ // Check arguments: Object.hasOwn(obj, key)
59
+ if (node.arguments.length !== 2) return;
60
+
61
+ const objArg = node.arguments[0];
62
+
63
+ const keyArg = node.arguments[1];
64
+
65
+ if (objArg === undefined || keyArg === undefined) return;
66
+
67
+ // Type guard: ensure arguments are Expressions (not SpreadElement)
68
+ if (
69
+ objArg.type === AST_NODE_TYPES.SpreadElement ||
70
+ keyArg.type === AST_NODE_TYPES.SpreadElement
71
+ ) {
72
+ return;
73
+ }
74
+
75
+ mut_nodesToFix.push({
76
+ node,
77
+ objExpression: objArg,
78
+ keyExpression: keyArg,
79
+ });
80
+ },
81
+
82
+ // Handle key in obj
83
+ BinaryExpression: (node) => {
84
+ if (node.operator !== 'in') return;
85
+
86
+ const keyExpression = node.left;
87
+
88
+ const objExpression = node.right;
89
+
90
+ // Type guard: ensure keyExpression is Expression (not PrivateIdentifier)
91
+ if (keyExpression.type === AST_NODE_TYPES.PrivateIdentifier) return;
92
+
93
+ mut_nodesToFix.push({
94
+ node,
95
+ objExpression,
96
+ keyExpression,
97
+ });
98
+ },
99
+
100
+ 'Program:exit': () => {
101
+ const namedImports = getNamedImports(tsDataForgeImport);
102
+
103
+ const hasIsRecordImport = namedImports.includes('isRecord');
104
+
105
+ const hasHasKeyImport = namedImports.includes('hasKey');
106
+
107
+ for (const [
108
+ index,
109
+ { node, objExpression, keyExpression },
110
+ ] of mut_nodesToFix.entries()) {
111
+ const objText = sourceCode.getText(objExpression);
112
+
113
+ const keyText = sourceCode.getText(keyExpression);
114
+
115
+ const originalText = sourceCode.getText(node);
116
+
117
+ context.report({
118
+ node,
119
+ messageId: 'useIsRecordAndHasKey',
120
+ data: {
121
+ original: originalText,
122
+ objName: objText,
123
+ keyName: keyText,
124
+ },
125
+ fix: (fixer) => {
126
+ const replacement = `isRecord(${objText}) && hasKey(${objText}, ${keyText})`;
127
+
128
+ const importsToAdd: string[] = [];
129
+
130
+ if (!hasIsRecordImport) {
131
+ // eslint-disable-next-line functional/immutable-data
132
+ importsToAdd.push('isRecord');
133
+ }
134
+
135
+ if (!hasHasKeyImport) {
136
+ // eslint-disable-next-line functional/immutable-data
137
+ importsToAdd.push('hasKey');
138
+ }
139
+
140
+ const importFixes =
141
+ index === 0 && importsToAdd.length > 0
142
+ ? buildImportFixes(
143
+ fixer,
144
+ program,
145
+ tsDataForgeImport,
146
+ importsToAdd,
147
+ )
148
+ : [];
149
+
150
+ return [...importFixes, fixer.replaceText(node, replacement)];
151
+ },
152
+ });
153
+ }
154
+ },
155
+ };
156
+ },
157
+ defaultOptions: [],
158
+ };
@@ -0,0 +1,191 @@
1
+ import parser from '@typescript-eslint/parser';
2
+ import { RuleTester } from '@typescript-eslint/rule-tester';
3
+ import dedent from 'dedent';
4
+ import { preferIsRecordAndHasKey } from './prefer-is-record-and-has-key.mjs';
5
+
6
+ const tester = new RuleTester({
7
+ languageOptions: {
8
+ parser,
9
+ parserOptions: {
10
+ ecmaVersion: 2020,
11
+ sourceType: 'module',
12
+ projectService: {
13
+ allowDefaultProject: ['*.ts*'],
14
+ },
15
+ tsconfigRootDir: `${import.meta.dirname}/../..`,
16
+ },
17
+ },
18
+ });
19
+
20
+ describe('prefer-is-record-and-has-key', () => {
21
+ tester.run('prefer-is-record-and-has-key', preferIsRecordAndHasKey, {
22
+ valid: [
23
+ {
24
+ name: 'ignores other Object methods',
25
+ code: dedent`
26
+ const obj = { a: 1 };
27
+ const keys = Object.keys(obj);
28
+ `,
29
+ },
30
+ {
31
+ name: 'ignores hasOwn with wrong number of arguments',
32
+ code: dedent`
33
+ const obj = { a: 1 };
34
+ const ok = Object.hasOwn(obj);
35
+ `,
36
+ },
37
+ ],
38
+ invalid: [
39
+ {
40
+ name: 'replaces Object.hasOwn(obj, key) with isRecord && hasKey',
41
+ code: dedent`
42
+ const obj = { a: 1 };
43
+ const ok = Object.hasOwn(obj, 'a');
44
+ `,
45
+ output: dedent`
46
+ import { isRecord, hasKey } from 'ts-data-forge';
47
+ const obj = { a: 1 };
48
+ const ok = isRecord(obj) && hasKey(obj, 'a');
49
+ `,
50
+ errors: [{ messageId: 'useIsRecordAndHasKey' }],
51
+ },
52
+ {
53
+ name: 'replaces key in obj with isRecord && hasKey',
54
+ code: dedent`
55
+ const obj = { a: 1 };
56
+ const ok = 'a' in obj;
57
+ `,
58
+ output: dedent`
59
+ import { isRecord, hasKey } from 'ts-data-forge';
60
+ const obj = { a: 1 };
61
+ const ok = isRecord(obj) && hasKey(obj, 'a');
62
+ `,
63
+ errors: [{ messageId: 'useIsRecordAndHasKey' }],
64
+ },
65
+ {
66
+ name: 'works with variable key',
67
+ code: dedent`
68
+ const obj = { a: 1 };
69
+ const key = 'a';
70
+ const ok = Object.hasOwn(obj, key);
71
+ `,
72
+ output: dedent`
73
+ import { isRecord, hasKey } from 'ts-data-forge';
74
+ const obj = { a: 1 };
75
+ const key = 'a';
76
+ const ok = isRecord(obj) && hasKey(obj, key);
77
+ `,
78
+ errors: [{ messageId: 'useIsRecordAndHasKey' }],
79
+ },
80
+ {
81
+ name: 'works with in and variable key',
82
+ code: dedent`
83
+ const obj = { a: 1 };
84
+ const key = 'a';
85
+ const ok = key in obj;
86
+ `,
87
+ output: dedent`
88
+ import { isRecord, hasKey } from 'ts-data-forge';
89
+ const obj = { a: 1 };
90
+ const key = 'a';
91
+ const ok = isRecord(obj) && hasKey(obj, key);
92
+ `,
93
+ errors: [{ messageId: 'useIsRecordAndHasKey' }],
94
+ },
95
+ {
96
+ name: 'keeps existing isRecord import',
97
+ code: dedent`
98
+ import { isRecord } from 'ts-data-forge';
99
+
100
+ const obj = { a: 1 };
101
+ const ok = Object.hasOwn(obj, 'a');
102
+ `,
103
+ output: dedent`
104
+ import { hasKey } from 'ts-data-forge';
105
+ import { isRecord } from 'ts-data-forge';
106
+
107
+ const obj = { a: 1 };
108
+ const ok = isRecord(obj) && hasKey(obj, 'a');
109
+ `,
110
+ errors: [{ messageId: 'useIsRecordAndHasKey' }],
111
+ },
112
+ {
113
+ name: 'keeps existing hasKey import',
114
+ code: dedent`
115
+ import { hasKey } from 'ts-data-forge';
116
+
117
+ const obj = { a: 1 };
118
+ const ok = 'a' in obj;
119
+ `,
120
+ output: dedent`
121
+ import { isRecord } from 'ts-data-forge';
122
+ import { hasKey } from 'ts-data-forge';
123
+
124
+ const obj = { a: 1 };
125
+ const ok = isRecord(obj) && hasKey(obj, 'a');
126
+ `,
127
+ errors: [{ messageId: 'useIsRecordAndHasKey' }],
128
+ },
129
+ {
130
+ name: 'keeps both existing imports',
131
+ code: dedent`
132
+ import { isRecord, hasKey } from 'ts-data-forge';
133
+
134
+ const obj = { a: 1 };
135
+ const ok = Object.hasOwn(obj, 'a');
136
+ `,
137
+ output: dedent`
138
+ import { isRecord, hasKey } from 'ts-data-forge';
139
+
140
+ const obj = { a: 1 };
141
+ const ok = isRecord(obj) && hasKey(obj, 'a');
142
+ `,
143
+ errors: [{ messageId: 'useIsRecordAndHasKey' }],
144
+ },
145
+ {
146
+ name: 'replaces multiple checks',
147
+ code: dedent`
148
+ const obj = { a: 1 };
149
+ const ok1 = Object.hasOwn(obj, 'a');
150
+ const ok2 = 'b' in obj;
151
+ `,
152
+ output: dedent`
153
+ import { isRecord, hasKey } from 'ts-data-forge';
154
+ const obj = { a: 1 };
155
+ const ok1 = isRecord(obj) && hasKey(obj, 'a');
156
+ const ok2 = isRecord(obj) && hasKey(obj, 'b');
157
+ `,
158
+ errors: [
159
+ { messageId: 'useIsRecordAndHasKey' },
160
+ { messageId: 'useIsRecordAndHasKey' },
161
+ ],
162
+ },
163
+ {
164
+ name: 'works with complex object expressions',
165
+ code: dedent`
166
+ const data = { nested: { value: 1 } };
167
+ const ok = Object.hasOwn(data.nested, 'value');
168
+ `,
169
+ output: dedent`
170
+ import { isRecord, hasKey } from 'ts-data-forge';
171
+ const data = { nested: { value: 1 } };
172
+ const ok = isRecord(data.nested) && hasKey(data.nested, 'value');
173
+ `,
174
+ errors: [{ messageId: 'useIsRecordAndHasKey' }],
175
+ },
176
+ {
177
+ name: 'works with in and complex object expressions',
178
+ code: dedent`
179
+ const data = { nested: { value: 1 } };
180
+ const ok = 'value' in data.nested;
181
+ `,
182
+ output: dedent`
183
+ import { isRecord, hasKey } from 'ts-data-forge';
184
+ const data = { nested: { value: 1 } };
185
+ const ok = isRecord(data.nested) && hasKey(data.nested, 'value');
186
+ `,
187
+ errors: [{ messageId: 'useIsRecordAndHasKey' }],
188
+ },
189
+ ],
190
+ });
191
+ }, 20000);
@@ -0,0 +1,181 @@
1
+ import {
2
+ AST_NODE_TYPES,
3
+ type TSESLint,
4
+ type TSESTree,
5
+ } from '@typescript-eslint/utils';
6
+ import { Arr } from 'ts-data-forge';
7
+ import {
8
+ buildImportFixes,
9
+ getNamedImports,
10
+ getTsDataForgeImport,
11
+ } from './import-utils.mjs';
12
+
13
+ type Options = readonly [];
14
+
15
+ type MessageIds = 'useRangeForLoop';
16
+
17
+ export const preferRangeForLoop: TSESLint.RuleModule<MessageIds, Options> = {
18
+ meta: {
19
+ type: 'suggestion',
20
+ docs: {
21
+ description:
22
+ 'Replace `for (let i = begin; i < end; ++i)` with `for (const i of range(begin, end))` from ts-data-forge.',
23
+ },
24
+ fixable: 'code',
25
+ schema: [],
26
+ messages: {
27
+ useRangeForLoop:
28
+ 'Replace with `for (const {{varName}} of range({{begin}}, {{end}}))` from ts-data-forge.',
29
+ },
30
+ },
31
+
32
+ create: (context) => {
33
+ const sourceCode = context.sourceCode;
34
+
35
+ const program = sourceCode.ast;
36
+
37
+ const tsDataForgeImport = getTsDataForgeImport(program);
38
+
39
+ const mut_nodesToFix: {
40
+ node: TSESTree.ForStatement;
41
+ varName: string;
42
+ begin: string;
43
+ end: string;
44
+ step?: string;
45
+ }[] = [];
46
+
47
+ return {
48
+ ForStatement: (node) => {
49
+ // Check init: let i = begin
50
+ if (
51
+ node.init === null ||
52
+ node.init.type !== AST_NODE_TYPES.VariableDeclaration ||
53
+ node.init.kind !== 'let' ||
54
+ !Arr.isArrayOfLength(node.init.declarations, 1)
55
+ ) {
56
+ return;
57
+ }
58
+
59
+ const declaration = node.init.declarations[0];
60
+
61
+ if (
62
+ declaration.id.type !== AST_NODE_TYPES.Identifier ||
63
+ declaration.init === null
64
+ ) {
65
+ return;
66
+ }
67
+
68
+ const varName = declaration.id.name;
69
+
70
+ const beginExpr = declaration.init;
71
+
72
+ // Check test: i < end
73
+ if (
74
+ node.test === null ||
75
+ node.test.type !== AST_NODE_TYPES.BinaryExpression ||
76
+ node.test.operator !== '<' ||
77
+ node.test.left.type !== AST_NODE_TYPES.Identifier ||
78
+ node.test.left.name !== varName
79
+ ) {
80
+ return;
81
+ }
82
+
83
+ const endExpr = node.test.right;
84
+
85
+ // Check update: ++i, i++, i += 1, or i += step
86
+ if (node.update === null) return;
87
+
88
+ let mut_step: string | undefined;
89
+
90
+ if (node.update.type === AST_NODE_TYPES.UpdateExpression) {
91
+ // ++i or i++
92
+ if (
93
+ node.update.operator === '++' &&
94
+ node.update.argument.type === AST_NODE_TYPES.Identifier &&
95
+ node.update.argument.name === varName
96
+ ) {
97
+ mut_step = undefined; // step defaults to 1
98
+ } else {
99
+ return;
100
+ }
101
+ } else if (
102
+ node.update.type === AST_NODE_TYPES.AssignmentExpression &&
103
+ node.update.operator === '+=' &&
104
+ node.update.left.type === AST_NODE_TYPES.Identifier &&
105
+ node.update.left.name === varName
106
+ ) {
107
+ // i += step
108
+ const stepValue = node.update.right;
109
+
110
+ if (stepValue.type === AST_NODE_TYPES.Literal) {
111
+ const stepNum = stepValue.value;
112
+
113
+ // Only support positive integer steps
114
+ if (typeof stepNum === 'number' && stepNum > 0) {
115
+ mut_step = stepNum === 1 ? undefined : String(stepNum);
116
+ } else {
117
+ return;
118
+ }
119
+ } else {
120
+ // Variable step like i += step
121
+ mut_step = sourceCode.getText(stepValue);
122
+ }
123
+ } else {
124
+ return;
125
+ }
126
+
127
+ const beginText = sourceCode.getText(beginExpr);
128
+
129
+ const endText = sourceCode.getText(endExpr);
130
+
131
+ mut_nodesToFix.push({
132
+ node,
133
+ varName,
134
+ begin: beginText,
135
+ end: endText,
136
+ step: mut_step,
137
+ });
138
+ },
139
+ 'Program:exit': () => {
140
+ const namedImports = getNamedImports(tsDataForgeImport);
141
+
142
+ const hasRangeImport = namedImports.includes('range');
143
+
144
+ for (const [
145
+ index,
146
+ { node, varName, begin, end, step },
147
+ ] of mut_nodesToFix.entries()) {
148
+ context.report({
149
+ node: node.init ?? node,
150
+ messageId: 'useRangeForLoop',
151
+ data: {
152
+ varName,
153
+ begin,
154
+ end,
155
+ },
156
+ fix: (fixer) => {
157
+ const bodyText = sourceCode.getText(node.body);
158
+
159
+ const rangeArgs =
160
+ step === undefined
161
+ ? `${begin}, ${end}`
162
+ : `${begin}, ${end}, ${step}`;
163
+
164
+ const replacement = `for (const ${varName} of range(${rangeArgs})) ${bodyText}`;
165
+
166
+ const importFixes =
167
+ index === 0 && !hasRangeImport
168
+ ? buildImportFixes(fixer, program, tsDataForgeImport, [
169
+ 'range',
170
+ ])
171
+ : [];
172
+
173
+ return [...importFixes, fixer.replaceText(node, replacement)];
174
+ },
175
+ });
176
+ }
177
+ },
178
+ };
179
+ },
180
+ defaultOptions: [],
181
+ };
@@ -0,0 +1,156 @@
1
+ import parser from '@typescript-eslint/parser';
2
+ import { RuleTester } from '@typescript-eslint/rule-tester';
3
+ import dedent from 'dedent';
4
+ import { preferRangeForLoop } from './prefer-range-for-loop.mjs';
5
+
6
+ const tester = new RuleTester({
7
+ languageOptions: {
8
+ parser,
9
+ parserOptions: {
10
+ ecmaVersion: 2020,
11
+ sourceType: 'module',
12
+ projectService: {
13
+ allowDefaultProject: ['*.ts*'],
14
+ },
15
+ tsconfigRootDir: `${import.meta.dirname}/../..`,
16
+ },
17
+ },
18
+ });
19
+
20
+ describe('prefer-range-for-loop', () => {
21
+ tester.run('prefer-range-for-loop', preferRangeForLoop, {
22
+ valid: [
23
+ {
24
+ name: 'ignores for loop with const',
25
+ code: dedent`
26
+ for (const i = 0; i < 10; ++i) {
27
+ console.log(i);
28
+ }
29
+ `,
30
+ },
31
+ {
32
+ name: 'ignores for loop with different condition',
33
+ code: dedent`
34
+ for (let i = 0; i <= 10; ++i) {
35
+ console.log(i);
36
+ }
37
+ `,
38
+ },
39
+ ],
40
+ invalid: [
41
+ {
42
+ name: 'replaces for loop with ++i',
43
+ code: dedent`
44
+ for (let i = 0; i < 10; ++i) {
45
+ console.log(i);
46
+ }
47
+ `,
48
+ output: dedent`
49
+ import { range } from 'ts-data-forge';
50
+ for (const i of range(0, 10)) {
51
+ console.log(i);
52
+ }
53
+ `,
54
+ errors: [{ messageId: 'useRangeForLoop' }],
55
+ },
56
+ {
57
+ name: 'replaces for loop with i++',
58
+ code: dedent`
59
+ for (let i = 0; i < 10; i++) {
60
+ console.log(i);
61
+ }
62
+ `,
63
+ output: dedent`
64
+ import { range } from 'ts-data-forge';
65
+ for (const i of range(0, 10)) {
66
+ console.log(i);
67
+ }
68
+ `,
69
+ errors: [{ messageId: 'useRangeForLoop' }],
70
+ },
71
+ {
72
+ name: 'replaces for loop with i += 1',
73
+ code: dedent`
74
+ for (let i = 0; i < 10; i += 1) {
75
+ console.log(i);
76
+ }
77
+ `,
78
+ output: dedent`
79
+ import { range } from 'ts-data-forge';
80
+ for (const i of range(0, 10)) {
81
+ console.log(i);
82
+ }
83
+ `,
84
+ errors: [{ messageId: 'useRangeForLoop' }],
85
+ },
86
+ {
87
+ name: 'replaces for loop with variable bounds',
88
+ code: dedent`
89
+ const begin = 5;
90
+ const end = 15;
91
+ for (let j = begin; j < end; ++j) {
92
+ console.log(j);
93
+ }
94
+ `,
95
+ output: dedent`
96
+ import { range } from 'ts-data-forge';
97
+ const begin = 5;
98
+ const end = 15;
99
+ for (const j of range(begin, end)) {
100
+ console.log(j);
101
+ }
102
+ `,
103
+ errors: [{ messageId: 'useRangeForLoop' }],
104
+ },
105
+ // {
106
+ // name: 'replaces for loop with different condition',
107
+ // code: dedent`
108
+ // for (let i = 0; i <= 10; ++i) {
109
+ // console.log(i);
110
+ // }
111
+ // `,
112
+ // output: dedent`
113
+ // import { range } from 'ts-data-forge';
114
+ // for (const i of range(0, 11)) {
115
+ // console.log(i);
116
+ // }
117
+ // `,
118
+ // errors: [{ messageId: 'useRangeForLoop' }],
119
+ // },
120
+ {
121
+ name: 'replaces for loop with i += 2',
122
+ code: dedent`
123
+ for (let i = 0; i < 10; i += 2) {
124
+ console.log(i);
125
+ }
126
+ `,
127
+ output: dedent`
128
+ import { range } from 'ts-data-forge';
129
+ for (const i of range(0, 10, 2)) {
130
+ console.log(i);
131
+ }
132
+ `,
133
+ errors: [{ messageId: 'useRangeForLoop' }],
134
+ },
135
+
136
+ {
137
+ name: 'keeps existing range import',
138
+ code: dedent`
139
+ import { range } from 'ts-data-forge';
140
+
141
+ for (let i = 0; i < 10; ++i) {
142
+ console.log(i);
143
+ }
144
+ `,
145
+ output: dedent`
146
+ import { range } from 'ts-data-forge';
147
+
148
+ for (const i of range(0, 10)) {
149
+ console.log(i);
150
+ }
151
+ `,
152
+ errors: [{ messageId: 'useRangeForLoop' }],
153
+ },
154
+ ],
155
+ });
156
+ }, 20000);
@@ -0,0 +1,22 @@
1
+ import { type ESLintPlugin } from '../../../types/index.mjs';
2
+ import { preferArrIsArrayAtLeastLength } from './prefer-arr-is-array-at-least-length.mjs';
3
+ import { preferArrIsArrayOfLength } from './prefer-arr-is-array-of-length.mjs';
4
+ import { preferArrIsArray } from './prefer-arr-is-array.mjs';
5
+ import { preferArrIsNonEmpty } from './prefer-arr-is-non-empty.mjs';
6
+ import { preferArrSum } from './prefer-arr-sum.mjs';
7
+ import { preferAsInt } from './prefer-as-int.mjs';
8
+ import { preferIsNonNullObject } from './prefer-is-non-null-object.mjs';
9
+ import { preferIsRecordAndHasKey } from './prefer-is-record-and-has-key.mjs';
10
+ import { preferRangeForLoop } from './prefer-range-for-loop.mjs';
11
+
12
+ export const tsDataForgeRules = {
13
+ 'prefer-arr-is-array-at-least-length': preferArrIsArrayAtLeastLength,
14
+ 'prefer-arr-is-array-of-length': preferArrIsArrayOfLength,
15
+ 'prefer-arr-is-array': preferArrIsArray,
16
+ 'prefer-arr-is-non-empty': preferArrIsNonEmpty,
17
+ 'prefer-arr-sum': preferArrSum,
18
+ 'prefer-as-int': preferAsInt,
19
+ 'prefer-is-non-null-object': preferIsNonNullObject,
20
+ 'prefer-range-for-loop': preferRangeForLoop,
21
+ 'prefer-is-record-and-has-key': preferIsRecordAndHasKey,
22
+ } as const satisfies ESLintPlugin['rules'];