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 @@
1
+ export * from './plugin.mjs';
@@ -0,0 +1,6 @@
1
+ import { type ESLintPlugin } from '../../types/index.mjs';
2
+ import { tsDataForgeRules } from './rules/index.mjs';
3
+
4
+ export const eslintPluginTsDataForge: ESLintPlugin = {
5
+ rules: tsDataForgeRules,
6
+ } as const;
@@ -0,0 +1,36 @@
1
+ const brandedNumberTypes = [
2
+ 'FiniteNumber',
3
+ 'Int',
4
+ 'Int16',
5
+ 'Int32',
6
+ 'NonNegativeFiniteNumber',
7
+ 'NonNegativeInt16',
8
+ 'NonNegativeInt32',
9
+ 'NonZeroFiniteNumber',
10
+ 'NonZeroInt',
11
+ 'NonZeroInt16',
12
+ 'NonZeroInt32',
13
+ 'NonZeroSafeInt',
14
+ 'NonZeroUint16',
15
+ 'NonZeroUint32',
16
+ 'PositiveFiniteNumber',
17
+ 'PositiveInt',
18
+ 'PositiveInt16',
19
+ 'PositiveInt32',
20
+ 'PositiveSafeInt',
21
+ 'PositiveUint16',
22
+ 'PositiveUint32',
23
+ 'SafeInt',
24
+ 'SafeUint',
25
+ 'Uint',
26
+ 'Uint16',
27
+ 'Uint32',
28
+ ] as const;
29
+
30
+ /**
31
+ * Branded number types from ts-data-forge and their corresponding as* functions
32
+ */
33
+ export const brandedNumberTypeNameToFunctionName: ReadonlyMap<
34
+ string,
35
+ `as${string}`
36
+ > = new Map(brandedNumberTypes.map((t) => [t, `as${t}`]));
@@ -0,0 +1 @@
1
+ export const TS_DATA_FORGE_MODULE = 'ts-data-forge';
@@ -0,0 +1,56 @@
1
+ import {
2
+ AST_NODE_TYPES,
3
+ type TSESLint,
4
+ type TSESTree,
5
+ } from '@typescript-eslint/utils';
6
+ import { TS_DATA_FORGE_MODULE } from './constants.mjs';
7
+
8
+ /* eslint-disable @typescript-eslint/prefer-readonly-parameter-types */
9
+
10
+ export const getTsDataForgeImport = (
11
+ program: TSESTree.Program,
12
+ ): TSESTree.ImportDeclaration | undefined =>
13
+ program.body.find(
14
+ (node): node is TSESTree.ImportDeclaration =>
15
+ node.type === AST_NODE_TYPES.ImportDeclaration &&
16
+ node.source.value === TS_DATA_FORGE_MODULE,
17
+ );
18
+
19
+ export const getNamedImports = (
20
+ node: DeepReadonly<TSESTree.ImportDeclaration> | undefined,
21
+ ): readonly string[] => {
22
+ if (node === undefined) return [];
23
+
24
+ return node.specifiers.flatMap((specifier) =>
25
+ specifier.type === AST_NODE_TYPES.ImportSpecifier
26
+ ? (() => {
27
+ const importedName =
28
+ specifier.imported.type === AST_NODE_TYPES.Identifier
29
+ ? specifier.imported.name
30
+ : specifier.imported.value;
31
+
32
+ return typeof importedName === 'string' ? [importedName] : [];
33
+ })()
34
+ : [],
35
+ );
36
+ };
37
+
38
+ export const buildImportFixes = (
39
+ fixer: TSESLint.RuleFixer,
40
+ program: TSESTree.Program,
41
+ _tsDataForgeImport: TSESTree.ImportDeclaration | undefined,
42
+ requiredNames: readonly string[],
43
+ ): readonly TSESLint.RuleFix[] => {
44
+ const specifierText = requiredNames.join(', ');
45
+
46
+ const importStatement = `import { ${specifierText} } from '${TS_DATA_FORGE_MODULE}';`;
47
+
48
+ const newLine = '\n';
49
+
50
+ const insertionText = `${importStatement}${newLine}`;
51
+
52
+ // Always insert at the beginning of the file
53
+ // (organize-imports will handle merging/deduplication)
54
+ return [fixer.insertTextBefore(program, insertionText)];
55
+ };
56
+ /* eslint-enable @typescript-eslint/prefer-readonly-parameter-types */
@@ -0,0 +1 @@
1
+ export * from './rules.mjs';
@@ -0,0 +1,140 @@
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 = 'useIsArrayAtLeastLength';
15
+
16
+ export const preferArrIsArrayAtLeastLength: TSESLint.RuleModule<
17
+ MessageIds,
18
+ Options
19
+ > = {
20
+ meta: {
21
+ type: 'suggestion',
22
+ docs: {
23
+ description:
24
+ 'Replace `xs.length >= n` with `Arr.isArrayAtLeastLength(xs, n)` from ts-data-forge.',
25
+ },
26
+ fixable: 'code',
27
+ schema: [],
28
+ messages: {
29
+ useIsArrayAtLeastLength:
30
+ 'Replace `{{original}}` with `Arr.isArrayAtLeastLength({{arrayName}}, {{length}})` from ts-data-forge.',
31
+ },
32
+ },
33
+
34
+ create: (context) => {
35
+ const sourceCode = context.sourceCode;
36
+
37
+ const program = sourceCode.ast;
38
+
39
+ const tsDataForgeImport = getTsDataForgeImport(program);
40
+
41
+ const services = context.sourceCode.parserServices;
42
+
43
+ const mut_nodesToFix: {
44
+ node: TSESTree.BinaryExpression;
45
+ arrayExpression: TSESTree.Expression;
46
+ lengthExpression: TSESTree.Expression;
47
+ }[] = [];
48
+
49
+ return {
50
+ BinaryExpression: (node) => {
51
+ // Check for `xs.length >= n` or `n <= xs.length`
52
+ if (node.operator !== '>=' && node.operator !== '<=') return;
53
+
54
+ // xs.length >= n or n <= xs.length
55
+ const isLengthOnLeft = node.operator === '>=';
56
+
57
+ const lengthSide = isLengthOnLeft ? node.left : node.right;
58
+
59
+ const valueSide = isLengthOnLeft ? node.right : node.left;
60
+
61
+ // Check if lengthSide is accessing .length
62
+ if (!isLengthAccess(lengthSide)) return;
63
+
64
+ // lengthSide is MemberExpression accessing .length
65
+ const arrayExpression = lengthSide.object;
66
+
67
+ // Check if arrayExpression is actually an array type
68
+ if (services?.program !== undefined && services.program !== null) {
69
+ const checker = services.program.getTypeChecker();
70
+
71
+ const tsNode = services.esTreeNodeToTSNodeMap?.get(arrayExpression);
72
+
73
+ if (tsNode !== undefined) {
74
+ const type = checker.getTypeAtLocation(tsNode);
75
+
76
+ // Check if it's an array type or tuple type
77
+ const isArrayType =
78
+ checker.isArrayType(type) || checker.isTupleType(type);
79
+
80
+ if (!isArrayType) return;
81
+ } else {
82
+ return;
83
+ }
84
+ } else {
85
+ return;
86
+ }
87
+
88
+ mut_nodesToFix.push({
89
+ node,
90
+ arrayExpression,
91
+ lengthExpression: valueSide,
92
+ });
93
+ },
94
+ 'Program:exit': () => {
95
+ const namedImports = getNamedImports(tsDataForgeImport);
96
+
97
+ const hasArrImport = namedImports.includes('Arr');
98
+
99
+ for (const [
100
+ index,
101
+ { node, arrayExpression, lengthExpression },
102
+ ] of mut_nodesToFix.entries()) {
103
+ const arrayText = sourceCode.getText(arrayExpression);
104
+
105
+ const lengthText = sourceCode.getText(lengthExpression);
106
+
107
+ const originalText = sourceCode.getText(node);
108
+
109
+ context.report({
110
+ node,
111
+ messageId: 'useIsArrayAtLeastLength',
112
+ data: {
113
+ original: originalText,
114
+ arrayName: arrayText,
115
+ length: lengthText,
116
+ },
117
+ fix: (fixer) => {
118
+ const replacement = `Arr.isArrayAtLeastLength(${arrayText}, ${lengthText})`;
119
+
120
+ const importFixes =
121
+ index === 0 && !hasArrImport
122
+ ? buildImportFixes(fixer, program, tsDataForgeImport, ['Arr'])
123
+ : [];
124
+
125
+ return [...importFixes, fixer.replaceText(node, replacement)];
126
+ },
127
+ });
128
+ }
129
+ },
130
+ };
131
+ },
132
+ defaultOptions: [],
133
+ };
134
+
135
+ const isLengthAccess = (
136
+ node: DeepReadonly<TSESTree.Expression>,
137
+ ): node is TSESTree.MemberExpression =>
138
+ node.type === AST_NODE_TYPES.MemberExpression &&
139
+ node.property.type === AST_NODE_TYPES.Identifier &&
140
+ node.property.name === 'length';
@@ -0,0 +1,175 @@
1
+ import parser from '@typescript-eslint/parser';
2
+ import { RuleTester } from '@typescript-eslint/rule-tester';
3
+ import dedent from 'dedent';
4
+ import { preferArrIsArrayAtLeastLength } from './prefer-arr-is-array-at-least-length.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-arr-is-array-at-least-length', () => {
21
+ tester.run(
22
+ 'prefer-arr-is-array-at-least-length',
23
+ preferArrIsArrayAtLeastLength,
24
+ {
25
+ valid: [
26
+ {
27
+ name: 'ignores non-array types',
28
+ code: dedent`
29
+ const str = "hello";
30
+ const ok = str.length >= 5;
31
+ `,
32
+ },
33
+ {
34
+ name: 'ignores < comparisons',
35
+ code: dedent`
36
+ const xs = [1, 2, 3];
37
+ const ok = xs.length < 5;
38
+ `,
39
+ },
40
+ {
41
+ name: 'ignores > comparisons',
42
+ code: dedent`
43
+ const xs = [1, 2, 3];
44
+ const ok = xs.length > 0;
45
+ `,
46
+ },
47
+ {
48
+ name: 'ignores === comparisons',
49
+ code: dedent`
50
+ const xs = [1, 2, 3];
51
+ const ok = xs.length === 3;
52
+ `,
53
+ },
54
+ ],
55
+ invalid: [
56
+ {
57
+ name: 'replaces xs.length >= n with Arr.isArrayAtLeastLength',
58
+ code: dedent`
59
+ const xs: readonly number[] = [1, 2, 3];
60
+ const ok = xs.length >= 3;
61
+ `,
62
+ output: dedent`
63
+ import { Arr } from 'ts-data-forge';
64
+ const xs: readonly number[] = [1, 2, 3];
65
+ const ok = Arr.isArrayAtLeastLength(xs, 3);
66
+ `,
67
+ errors: [{ messageId: 'useIsArrayAtLeastLength' }],
68
+ },
69
+ {
70
+ name: 'replaces n <= xs.length with Arr.isArrayAtLeastLength',
71
+ code: dedent`
72
+ const xs: readonly number[] = [1, 2, 3];
73
+ const ok = 3 <= xs.length;
74
+ `,
75
+ output: dedent`
76
+ import { Arr } from 'ts-data-forge';
77
+ const xs: readonly number[] = [1, 2, 3];
78
+ const ok = Arr.isArrayAtLeastLength(xs, 3);
79
+ `,
80
+ errors: [{ messageId: 'useIsArrayAtLeastLength' }],
81
+ },
82
+ {
83
+ name: 'works with no type annotation',
84
+ code: dedent`
85
+ const xs = [1, 2, 3];
86
+ const ok = xs.length >= 1;
87
+ `,
88
+ output: dedent`
89
+ import { Arr } from 'ts-data-forge';
90
+ const xs = [1, 2, 3];
91
+ const ok = Arr.isArrayAtLeastLength(xs, 1);
92
+ `,
93
+ errors: [{ messageId: 'useIsArrayAtLeastLength' }],
94
+ },
95
+ {
96
+ name: 'works with const assertion',
97
+ code: dedent`
98
+ const xs = [1, 2, 3] as const;
99
+ const ok = xs.length >= 2;
100
+ `,
101
+ output: dedent`
102
+ import { Arr } from 'ts-data-forge';
103
+ const xs = [1, 2, 3] as const;
104
+ const ok = Arr.isArrayAtLeastLength(xs, 2);
105
+ `,
106
+ errors: [{ messageId: 'useIsArrayAtLeastLength' }],
107
+ },
108
+ {
109
+ name: 'works with variable length',
110
+ code: dedent`
111
+ const xs = [1, 2, 3];
112
+ const n = 2;
113
+ const ok = xs.length >= n;
114
+ `,
115
+ output: dedent`
116
+ import { Arr } from 'ts-data-forge';
117
+ const xs = [1, 2, 3];
118
+ const n = 2;
119
+ const ok = Arr.isArrayAtLeastLength(xs, n);
120
+ `,
121
+ errors: [{ messageId: 'useIsArrayAtLeastLength' }],
122
+ },
123
+ {
124
+ name: 'keeps existing Arr import',
125
+ code: dedent`
126
+ import { Arr } from 'ts-data-forge';
127
+
128
+ const xs = [1, 2, 3];
129
+ const ok = xs.length >= 1;
130
+ `,
131
+ output: dedent`
132
+ import { Arr } from 'ts-data-forge';
133
+
134
+ const xs = [1, 2, 3];
135
+ const ok = Arr.isArrayAtLeastLength(xs, 1);
136
+ `,
137
+ errors: [{ messageId: 'useIsArrayAtLeastLength' }],
138
+ },
139
+ {
140
+ name: 'replaces multiple checks',
141
+ code: dedent`
142
+ const xs = [1, 2, 3];
143
+ const ys = [4, 5];
144
+ const ok1 = xs.length >= 2;
145
+ const ok2 = ys.length >= 1;
146
+ `,
147
+ output: dedent`
148
+ import { Arr } from 'ts-data-forge';
149
+ const xs = [1, 2, 3];
150
+ const ys = [4, 5];
151
+ const ok1 = Arr.isArrayAtLeastLength(xs, 2);
152
+ const ok2 = Arr.isArrayAtLeastLength(ys, 1);
153
+ `,
154
+ errors: [
155
+ { messageId: 'useIsArrayAtLeastLength' },
156
+ { messageId: 'useIsArrayAtLeastLength' },
157
+ ],
158
+ },
159
+ {
160
+ name: 'works with >= 0 (checking non-empty)',
161
+ code: dedent`
162
+ const xs = [1, 2, 3];
163
+ const ok = xs.length >= 1;
164
+ `,
165
+ output: dedent`
166
+ import { Arr } from 'ts-data-forge';
167
+ const xs = [1, 2, 3];
168
+ const ok = Arr.isArrayAtLeastLength(xs, 1);
169
+ `,
170
+ errors: [{ messageId: 'useIsArrayAtLeastLength' }],
171
+ },
172
+ ],
173
+ },
174
+ );
175
+ }, 20000);
@@ -0,0 +1,144 @@
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 = 'useIsArrayOfLength';
15
+
16
+ export const preferArrIsArrayOfLength: TSESLint.RuleModule<
17
+ MessageIds,
18
+ Options
19
+ > = {
20
+ meta: {
21
+ type: 'suggestion',
22
+ docs: {
23
+ description:
24
+ 'Replace `xs.length === n` with `Arr.isArrayOfLength(xs, n)` from ts-data-forge.',
25
+ },
26
+ fixable: 'code',
27
+ schema: [],
28
+ messages: {
29
+ useIsArrayOfLength:
30
+ 'Replace `{{original}}` with `Arr.isArrayOfLength({{arrayName}}, {{length}})` from ts-data-forge.',
31
+ },
32
+ },
33
+
34
+ create: (context) => {
35
+ const sourceCode = context.sourceCode;
36
+
37
+ const program = sourceCode.ast;
38
+
39
+ const tsDataForgeImport = getTsDataForgeImport(program);
40
+
41
+ const services = context.sourceCode.parserServices;
42
+
43
+ const mut_nodesToFix: {
44
+ node: TSESTree.BinaryExpression;
45
+ arrayExpression: TSESTree.Expression;
46
+ lengthExpression: TSESTree.Expression;
47
+ isNegated: boolean;
48
+ }[] = [];
49
+
50
+ return {
51
+ BinaryExpression: (node) => {
52
+ // Check for `xs.length === n` or `n === xs.length` or `xs.length !== n`
53
+ if (node.operator !== '===' && node.operator !== '!==') return;
54
+
55
+ const isNegated = node.operator === '!==';
56
+
57
+ const isLengthOnLeft = isLengthAccess(node.left);
58
+
59
+ const lengthSide = isLengthOnLeft ? node.left : node.right;
60
+
61
+ const valueSide = isLengthOnLeft ? node.right : node.left;
62
+
63
+ if (!isLengthAccess(lengthSide)) return;
64
+
65
+ // lengthSide is MemberExpression accessing .length
66
+ const arrayExpression = lengthSide.object;
67
+
68
+ // Check if arrayExpression is actually an array type
69
+ if (services?.program !== undefined && services.program !== null) {
70
+ const checker = services.program.getTypeChecker();
71
+
72
+ const tsNode = services.esTreeNodeToTSNodeMap?.get(arrayExpression);
73
+
74
+ if (tsNode !== undefined) {
75
+ const type = checker.getTypeAtLocation(tsNode);
76
+
77
+ // Check if it's an array type or tuple type
78
+ const isArrayType =
79
+ checker.isArrayType(type) || checker.isTupleType(type);
80
+
81
+ if (!isArrayType) return;
82
+ } else {
83
+ return;
84
+ }
85
+ } else {
86
+ return;
87
+ }
88
+
89
+ mut_nodesToFix.push({
90
+ node,
91
+ arrayExpression,
92
+ lengthExpression: valueSide,
93
+ isNegated,
94
+ });
95
+ },
96
+ 'Program:exit': () => {
97
+ const namedImports = getNamedImports(tsDataForgeImport);
98
+
99
+ const hasArrImport = namedImports.includes('Arr');
100
+
101
+ for (const [
102
+ index,
103
+ { node, arrayExpression, lengthExpression, isNegated },
104
+ ] of mut_nodesToFix.entries()) {
105
+ const arrayText = sourceCode.getText(arrayExpression);
106
+
107
+ const lengthText = sourceCode.getText(lengthExpression);
108
+
109
+ const originalText = sourceCode.getText(node);
110
+
111
+ context.report({
112
+ node,
113
+ messageId: 'useIsArrayOfLength',
114
+ data: {
115
+ original: originalText,
116
+ arrayName: arrayText,
117
+ length: lengthText,
118
+ },
119
+ fix: (fixer) => {
120
+ const baseCall = `Arr.isArrayOfLength(${arrayText}, ${lengthText})`;
121
+
122
+ const replacement = isNegated ? `!${baseCall}` : baseCall;
123
+
124
+ const importFixes =
125
+ index === 0 && !hasArrImport
126
+ ? buildImportFixes(fixer, program, tsDataForgeImport, ['Arr'])
127
+ : [];
128
+
129
+ return [...importFixes, fixer.replaceText(node, replacement)];
130
+ },
131
+ });
132
+ }
133
+ },
134
+ };
135
+ },
136
+ defaultOptions: [],
137
+ };
138
+
139
+ const isLengthAccess = (
140
+ node: DeepReadonly<TSESTree.Expression>,
141
+ ): node is TSESTree.MemberExpression =>
142
+ node.type === AST_NODE_TYPES.MemberExpression &&
143
+ node.property.type === AST_NODE_TYPES.Identifier &&
144
+ node.property.name === 'length';