eslint-plugin-ts-data-forge 0.6.0 → 0.7.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 (77) hide show
  1. package/README.md +1 -0
  2. package/dist/index.mjs +2 -2
  3. package/dist/index.mjs.map +1 -1
  4. package/dist/plugin.d.mts +2 -0
  5. package/dist/plugin.d.mts.map +1 -1
  6. package/dist/plugin.mjs +45 -41
  7. package/dist/plugin.mjs.map +1 -1
  8. package/dist/rule-types.d.mts +15 -0
  9. package/dist/rule-types.d.mts.map +1 -1
  10. package/dist/rule-types.mjs +6 -2
  11. package/dist/rule-types.mjs.map +1 -1
  12. package/dist/rules/ast-utils.mjs +107 -50
  13. package/dist/rules/ast-utils.mjs.map +1 -1
  14. package/dist/rules/branded-number-types.mjs +41 -37
  15. package/dist/rules/branded-number-types.mjs.map +1 -1
  16. package/dist/rules/constants.mjs +2 -4
  17. package/dist/rules/constants.mjs.map +1 -1
  18. package/dist/rules/import-utils.mjs +80 -56
  19. package/dist/rules/import-utils.mjs.map +1 -1
  20. package/dist/rules/index.mjs +2 -2
  21. package/dist/rules/index.mjs.map +1 -1
  22. package/dist/rules/no-side-effect-import.mjs +46 -34
  23. package/dist/rules/no-side-effect-import.mjs.map +1 -1
  24. package/dist/rules/no-unnecessary-type-guard.mjs +376 -265
  25. package/dist/rules/no-unnecessary-type-guard.mjs.map +1 -1
  26. package/dist/rules/prefer-arr-is-array.mjs +64 -52
  27. package/dist/rules/prefer-arr-is-array.mjs.map +1 -1
  28. package/dist/rules/prefer-arr-is-bounded-length-array.mjs +91 -85
  29. package/dist/rules/prefer-arr-is-bounded-length-array.mjs.map +1 -1
  30. package/dist/rules/prefer-arr-is-fixed-length-array.mjs +92 -83
  31. package/dist/rules/prefer-arr-is-fixed-length-array.mjs.map +1 -1
  32. package/dist/rules/prefer-arr-is-max-length-array.mjs +92 -80
  33. package/dist/rules/prefer-arr-is-max-length-array.mjs.map +1 -1
  34. package/dist/rules/prefer-arr-is-min-length-array.mjs +92 -80
  35. package/dist/rules/prefer-arr-is-min-length-array.mjs.map +1 -1
  36. package/dist/rules/prefer-arr-is-non-empty.mjs +85 -77
  37. package/dist/rules/prefer-arr-is-non-empty.mjs.map +1 -1
  38. package/dist/rules/prefer-arr-scan.d.mts +96 -0
  39. package/dist/rules/prefer-arr-scan.d.mts.map +1 -0
  40. package/dist/rules/prefer-arr-scan.mjs +482 -0
  41. package/dist/rules/prefer-arr-scan.mjs.map +1 -0
  42. package/dist/rules/prefer-arr-sum.mjs +176 -132
  43. package/dist/rules/prefer-arr-sum.mjs.map +1 -1
  44. package/dist/rules/prefer-as-int.mjs +77 -71
  45. package/dist/rules/prefer-as-int.mjs.map +1 -1
  46. package/dist/rules/prefer-canonical-array-slicing.mjs +503 -343
  47. package/dist/rules/prefer-canonical-array-slicing.mjs.map +1 -1
  48. package/dist/rules/prefer-canonical-length-cast.mjs +217 -144
  49. package/dist/rules/prefer-canonical-length-cast.mjs.map +1 -1
  50. package/dist/rules/prefer-canonical-length-guard.mjs +160 -109
  51. package/dist/rules/prefer-canonical-length-guard.mjs.map +1 -1
  52. package/dist/rules/prefer-comparison-over-nullish-guard.mjs +89 -71
  53. package/dist/rules/prefer-comparison-over-nullish-guard.mjs.map +1 -1
  54. package/dist/rules/prefer-is-non-null-object.mjs +91 -72
  55. package/dist/rules/prefer-is-non-null-object.mjs.map +1 -1
  56. package/dist/rules/prefer-is-record-and-has-key.mjs +148 -105
  57. package/dist/rules/prefer-is-record-and-has-key.mjs.map +1 -1
  58. package/dist/rules/prefer-num-safe-parse-float.mjs +101 -77
  59. package/dist/rules/prefer-num-safe-parse-float.mjs.map +1 -1
  60. package/dist/rules/prefer-num-safe-parse-int.mjs +103 -81
  61. package/dist/rules/prefer-num-safe-parse-int.mjs.map +1 -1
  62. package/dist/rules/prefer-obj-over-entries-round-trip.mjs +299 -194
  63. package/dist/rules/prefer-obj-over-entries-round-trip.mjs.map +1 -1
  64. package/dist/rules/prefer-range-for-loop.mjs +121 -99
  65. package/dist/rules/prefer-range-for-loop.mjs.map +1 -1
  66. package/dist/rules/rules.d.mts +1 -0
  67. package/dist/rules/rules.d.mts.map +1 -1
  68. package/dist/rules/rules.mjs +19 -20
  69. package/dist/rules/rules.mjs.map +1 -1
  70. package/dist/types.mjs +2 -2
  71. package/dist/types.mjs.map +1 -1
  72. package/package.json +6 -7
  73. package/src/plugin.mts +1 -0
  74. package/src/rule-types.mts +16 -0
  75. package/src/rules/prefer-arr-scan.mts +693 -0
  76. package/src/rules/prefer-arr-scan.test.mts +295 -0
  77. package/src/rules/rules.mts +2 -0
@@ -1,362 +1,522 @@
1
- import { ESLintUtils, AST_NODE_TYPES } from '@typescript-eslint/utils';
1
+ import { AST_NODE_TYPES, ESLintUtils, } from '@typescript-eslint/utils';
2
2
  import * as ts from 'typescript';
3
- import { getTsDataForgeImport, getImportedLocalName, buildImportFixes } from './import-utils.mjs';
3
+ import { buildImportFixes, getImportedLocalName, getTsDataForgeImport, } from './import-utils.mjs';
4
+
5
+ const normalizeCountedOp = (fn, count) => count < 1
6
+ ? undefined
7
+ : fn === 'skip' && count === 1
8
+ ? { fn: 'tail' }
9
+ : fn === 'skipLast' && count === 1
10
+ ? { fn: 'butLast' }
11
+ : { fn, count };
12
+ const asNonNegativeIntegerLiteral = (node) => node.type === AST_NODE_TYPES.Literal &&
13
+ typeof node.value === 'number' &&
14
+ Number.isInteger(node.value) &&
15
+ node.value >= 0
16
+ ? node.value
17
+ : undefined;
4
18
 
5
- const normalizeCountedOp = (fn, count) => count < 1 ? void 0 : fn === "skip" && count === 1 ? { fn: "tail" } : fn === "skipLast" && count === 1 ? { fn: "butLast" } : { fn, count };
6
- const asNonNegativeIntegerLiteral = (node) => node.type === AST_NODE_TYPES.Literal && typeof node.value === "number" && Number.isInteger(node.value) && node.value >= 0 ? node.value : void 0;
7
19
  const asNegatedPositiveIntegerLiteral = (node) => {
8
- if (node.type !== AST_NODE_TYPES.UnaryExpression || node.operator !== "-" || node.argument.type !== AST_NODE_TYPES.Literal) {
9
- return void 0;
10
- }
11
- const n = asNonNegativeIntegerLiteral(node.argument);
12
- return n !== void 0 && n >= 1 ? n : void 0;
20
+ if (node.type !== AST_NODE_TYPES.UnaryExpression ||
21
+ node.operator !== '-' ||
22
+ node.argument.type !== AST_NODE_TYPES.Literal) {
23
+ return undefined;
24
+ }
25
+ const n = asNonNegativeIntegerLiteral(node.argument);
26
+ return n !== undefined && n >= 1 ? n : undefined;
13
27
  };
14
- const isSimpleReference = (node) => node.type === AST_NODE_TYPES.Identifier || node.type === AST_NODE_TYPES.ThisExpression || node.type === AST_NODE_TYPES.MemberExpression && !node.computed && node.property.type === AST_NODE_TYPES.Identifier && isSimpleReference(node.object);
15
- const rootIdentifierName = (node) => node.type === AST_NODE_TYPES.Identifier ? node.name : node.type === AST_NODE_TYPES.MemberExpression ? rootIdentifierName(node.object) : void 0;
16
- const COMPARISON_OPERATORS = /* @__PURE__ */ new Set([
17
- "!==",
18
- "<",
19
- "<=",
20
- ">",
21
- ">="
28
+
29
+
30
+
31
+
32
+
33
+ const isSimpleReference = (node) => node.type === AST_NODE_TYPES.Identifier ||
34
+ node.type === AST_NODE_TYPES.ThisExpression ||
35
+ (node.type === AST_NODE_TYPES.MemberExpression &&
36
+ !node.computed &&
37
+ node.property.type === AST_NODE_TYPES.Identifier &&
38
+ isSimpleReference(node.object));
39
+ const rootIdentifierName = (node) => node.type === AST_NODE_TYPES.Identifier
40
+ ? node.name
41
+ : node.type === AST_NODE_TYPES.MemberExpression
42
+ ? rootIdentifierName(node.object)
43
+ : undefined;
44
+ const COMPARISON_OPERATORS = new Set([
45
+ '!==',
46
+ '<',
47
+ '<=',
48
+ '>',
49
+ '>=',
22
50
  ]);
51
+
23
52
  const FLIPPED_OPERATORS = {
24
- "!==": "!==",
25
- "<": ">",
26
- "<=": ">=",
27
- ">": "<",
28
- ">=": "<="
53
+ '!==': '!==',
54
+ '<': '>',
55
+ '<=': '>=',
56
+ '>': '<',
57
+ '>=': '<=',
29
58
  };
59
+
30
60
  const classifyLiteralComparison = (op, n) => {
31
- switch (op) {
32
- case ">=":
33
- return normalizeCountedOp("skip", n);
34
- case ">":
35
- return normalizeCountedOp("skip", n + 1);
36
- case "!==":
37
- return n === 0 ? { fn: "tail" } : void 0;
38
- case "<":
39
- return normalizeCountedOp("take", n);
40
- case "<=":
41
- return normalizeCountedOp("take", n + 1);
42
- }
61
+ switch (op) {
62
+ case '>=':
63
+ return normalizeCountedOp('skip', n);
64
+ case '>':
65
+ return normalizeCountedOp('skip', n + 1);
66
+ case '!==':
67
+ return n === 0 ? { fn: 'tail' } : undefined;
68
+ case '<':
69
+ return normalizeCountedOp('take', n);
70
+ case '<=':
71
+ return normalizeCountedOp('take', n + 1);
72
+ }
43
73
  };
74
+
44
75
  const classifyLengthComparison = (op, m) => {
45
- switch (op) {
46
- case "<":
47
- return normalizeCountedOp("skipLast", m);
48
- case "<=":
49
- return normalizeCountedOp("skipLast", m - 1);
50
- case "!==":
51
- return m === 1 ? { fn: "butLast" } : void 0;
52
- case ">=":
53
- return normalizeCountedOp("takeLast", m);
54
- case ">":
55
- return normalizeCountedOp("takeLast", m - 1);
56
- }
76
+ switch (op) {
77
+ case '<':
78
+ return normalizeCountedOp('skipLast', m);
79
+ case '<=':
80
+ return normalizeCountedOp('skipLast', m - 1);
81
+ case '!==':
82
+ return m === 1 ? { fn: 'butLast' } : undefined;
83
+ case '>=':
84
+ return normalizeCountedOp('takeLast', m);
85
+ case '>':
86
+ return normalizeCountedOp('takeLast', m - 1);
87
+ }
57
88
  };
58
- const preferCanonicalArraySlicing = {
59
- meta: {
60
- type: "suggestion",
61
- docs: {
62
- description: "Unify non-mutating array element addition/removal patterns (`slice` / `toSpliced` / `filter` / `concat` / spread) into the corresponding ts-data-forge functions: `Arr.tail`, `Arr.skip`, `Arr.take`, `Arr.butLast`, `Arr.skipLast`, `Arr.takeLast`, `Arr.toUnshifted`, `Arr.toPushed`."
89
+ export const preferCanonicalArraySlicing = {
90
+ meta: {
91
+ type: 'suggestion',
92
+ docs: {
93
+ description: 'Unify non-mutating array element addition/removal patterns (`slice` / `toSpliced` / `filter` / `concat` / spread) into the corresponding ts-data-forge functions: `Arr.tail`, `Arr.skip`, `Arr.take`, `Arr.butLast`, `Arr.skipLast`, `Arr.takeLast`, `Arr.toUnshifted`, `Arr.toPushed`.',
94
+ },
95
+ fixable: 'code',
96
+ schema: [],
97
+ messages: {
98
+ preferCanonicalArraySlicing: 'Replace with `{{replacement}}` from ts-data-forge.',
99
+ },
63
100
  },
64
- fixable: "code",
65
- schema: [],
66
- messages: {
67
- preferCanonicalArraySlicing: "Replace with `{{replacement}}` from ts-data-forge."
68
- }
69
- },
70
- create: (context) => {
71
- const sourceCode = context.sourceCode;
72
- const program = sourceCode.ast;
73
- const tsDataForgeImport = getTsDataForgeImport(program);
74
- const parserServices = ESLintUtils.getParserServices(context);
75
- const checker = parserServices.program.getTypeChecker();
76
- const arrLocalName = getImportedLocalName(tsDataForgeImport, "Arr");
77
- const namespaceLocalName = tsDataForgeImport?.specifiers.find(
78
- (specifier) => specifier.type === AST_NODE_TYPES.ImportNamespaceSpecifier
79
- )?.local.name;
80
- const arrRef = arrLocalName ?? (namespaceLocalName === void 0 ? "Arr" : `${namespaceLocalName}.Arr`);
81
- const needsArrImport = arrLocalName === void 0 && namespaceLocalName === void 0;
82
- const isArrayOrTupleType = (type) => {
83
- if ((type.flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown)) !== 0) {
84
- return false;
85
- }
86
- if (type.isUnion()) {
87
- return type.types.every(isArrayOrTupleType);
88
- }
89
- if ((type.flags & (ts.TypeFlags.TypeParameter | ts.TypeFlags.IndexedAccess | ts.TypeFlags.Conditional | ts.TypeFlags.Substitution)) !== 0) {
90
- const constraint = checker.getBaseConstraintOfType(type);
91
- return constraint !== void 0 && isArrayOrTupleType(constraint);
92
- }
93
- return checker.isArrayType(type) || checker.isTupleType(type);
94
- };
95
- const getTypeOf = (node) => checker.getTypeAtLocation(parserServices.esTreeNodeToTSNodeMap.get(node));
96
- const isDefinitelyNotArrayType = (type) => {
97
- if ((type.flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown)) !== 0) {
98
- return false;
99
- }
100
- if (type.isUnion()) {
101
- return type.types.every(isDefinitelyNotArrayType);
102
- }
103
- if ((type.flags & (ts.TypeFlags.TypeParameter | ts.TypeFlags.IndexedAccess | ts.TypeFlags.Conditional | ts.TypeFlags.Substitution)) !== 0) {
104
- const constraint = checker.getBaseConstraintOfType(type);
105
- return constraint !== void 0 && isDefinitelyNotArrayType(constraint);
106
- }
107
- return !checker.isArrayType(type) && !checker.isTupleType(type);
108
- };
109
- const toArgText = (node) => {
110
- const text = sourceCode.getText(node);
111
- return node.type === AST_NODE_TYPES.SequenceExpression ? `(${text})` : text;
112
- };
113
- const toReplacementText = (op, xsText) => {
114
- switch (op.fn) {
115
- case "tail":
116
- case "butLast":
117
- return `${arrRef}.${op.fn}(${xsText})`;
118
- case "skip":
119
- case "take":
120
- case "skipLast":
121
- case "takeLast":
122
- return `${arrRef}.${op.fn}(${xsText}, ${op.count})`;
123
- // Emit the curried form for `toUnshifted` so the new element stays in
124
- // the same textual position as in the `[item, ...xs]` source, keeping
125
- // the rewrite easy to read.
126
- case "toUnshifted":
127
- return `${arrRef}.toUnshifted(${op.itemText})(${xsText})`;
128
- case "toPushed":
129
- return `${arrRef}.toPushed(${xsText}, ${op.itemText})`;
130
- }
131
- };
132
- const mut_nodesToFix = [];
133
- const report = (node, op, xsText) => {
134
- mut_nodesToFix.push({
135
- node,
136
- replacement: toReplacementText(op, xsText)
137
- });
138
- };
139
- const isReceiverLengthAccess = (node, receiverText) => node.type === AST_NODE_TYPES.MemberExpression && !node.computed && node.property.type === AST_NODE_TYPES.Identifier && node.property.name === "length" && sourceCode.getText(node.object) === receiverText;
140
- const matchLengthMinus = (node, receiverText) => {
141
- if (node.type !== AST_NODE_TYPES.BinaryExpression || node.operator !== "-" || !isReceiverLengthAccess(node.left, receiverText)) {
142
- return void 0;
143
- }
144
- const m = asNonNegativeIntegerLiteral(node.right);
145
- return m !== void 0 && m >= 1 ? m : void 0;
146
- };
147
- const classifyFilterCallback = (callback, receiver, receiverText) => {
148
- if (callback.type !== AST_NODE_TYPES.ArrowFunctionExpression || callback.async || callback.body.type === AST_NODE_TYPES.BlockStatement || callback.params.length !== 2) {
149
- return void 0;
150
- }
151
- const [elementParam, indexParam] = callback.params;
152
- if (elementParam === void 0 || indexParam === void 0 || elementParam.type !== AST_NODE_TYPES.Identifier || indexParam.type !== AST_NODE_TYPES.Identifier) {
153
- return void 0;
154
- }
155
- const receiverRoot = rootIdentifierName(receiver);
156
- if (receiverRoot !== void 0 && (elementParam.name === receiverRoot || indexParam.name === receiverRoot)) {
157
- return void 0;
158
- }
159
- const body = callback.body;
160
- if (body.type !== AST_NODE_TYPES.BinaryExpression || !COMPARISON_OPERATORS.has(body.operator)) {
161
- return void 0;
162
- }
163
- const rawOperator = body.operator;
164
- const isIndex = (side) => side.type === AST_NODE_TYPES.Identifier && side.name === indexParam.name;
165
- const [op, other] = isIndex(body.left) ? [rawOperator, body.right] : isIndex(body.right) ? [FLIPPED_OPERATORS[rawOperator], body.left] : [void 0, void 0];
166
- if (op === void 0 || isIndex(other)) {
167
- return void 0;
168
- }
169
- const n = asNonNegativeIntegerLiteral(other);
170
- if (n !== void 0) {
171
- return classifyLiteralComparison(op, n);
172
- }
173
- if (!isSimpleReference(receiver)) return void 0;
174
- const m = matchLengthMinus(other, receiverText);
175
- return m === void 0 ? void 0 : classifyLengthComparison(op, m);
176
- };
177
- const classifySliceArgs = (args, receiver) => {
178
- if (args.length === 1) {
179
- const [start] = args;
180
- if (start === void 0) return void 0;
181
- const n = asNonNegativeIntegerLiteral(start);
182
- if (n !== void 0) return normalizeCountedOp("skip", n);
183
- const negated = asNegatedPositiveIntegerLiteral(start);
184
- return negated === void 0 ? void 0 : normalizeCountedOp("takeLast", negated);
185
- }
186
- if (args.length === 2) {
187
- const [start, end] = args;
188
- if (start === void 0 || end === void 0 || asNonNegativeIntegerLiteral(start) !== 0) {
189
- return void 0;
190
- }
191
- const n = asNonNegativeIntegerLiteral(end);
192
- if (n !== void 0) return normalizeCountedOp("take", n);
193
- const negated = asNegatedPositiveIntegerLiteral(end);
194
- if (negated !== void 0) {
195
- return normalizeCountedOp("skipLast", negated);
196
- }
197
- if (!isSimpleReference(receiver)) return void 0;
198
- const m = matchLengthMinus(end, sourceCode.getText(receiver));
199
- return m === void 0 ? void 0 : normalizeCountedOp("skipLast", m);
200
- }
201
- return void 0;
202
- };
203
- const classifyToSplicedArgs = (args, receiver, receiverText) => {
204
- if (args.length === 1) {
205
- const [start] = args;
206
- if (start === void 0) return void 0;
207
- const n = asNonNegativeIntegerLiteral(start);
208
- if (n !== void 0) return normalizeCountedOp("take", n);
209
- const negated = asNegatedPositiveIntegerLiteral(start);
210
- return negated === void 0 ? void 0 : normalizeCountedOp("skipLast", negated);
211
- }
212
- if (args.length === 2) {
213
- const [start, deleteCount] = args;
214
- if (start === void 0 || deleteCount === void 0 || asNonNegativeIntegerLiteral(start) !== 0) {
215
- return void 0;
216
- }
217
- const n = asNonNegativeIntegerLiteral(deleteCount);
218
- if (n !== void 0) return normalizeCountedOp("skip", n);
219
- if (!isSimpleReference(receiver)) return void 0;
220
- const m = matchLengthMinus(deleteCount, receiverText);
221
- return m === void 0 ? void 0 : normalizeCountedOp("takeLast", m);
222
- }
223
- if (args.length === 3) {
224
- const [start, deleteCount, item] = args;
225
- if (start === void 0 || deleteCount === void 0 || item === void 0 || item.type === AST_NODE_TYPES.SpreadElement || asNonNegativeIntegerLiteral(deleteCount) !== 0) {
226
- return void 0;
227
- }
228
- if (asNonNegativeIntegerLiteral(start) === 0) {
229
- return { fn: "toUnshifted", itemText: toArgText(item) };
230
- }
231
- if (isSimpleReference(receiver) && isReceiverLengthAccess(start, receiverText)) {
232
- return { fn: "toPushed", itemText: toArgText(item) };
233
- }
234
- return void 0;
235
- }
236
- return void 0;
237
- };
238
- return {
239
- CallExpression: (node) => {
240
- const callee = node.callee;
241
- if (callee.type !== AST_NODE_TYPES.MemberExpression || callee.computed || callee.property.type !== AST_NODE_TYPES.Identifier) {
242
- return;
243
- }
244
- const methodName = callee.property.name;
245
- const receiver = callee.object;
246
- if (methodName === "skip" || methodName === "skipLast") {
247
- const isArrObject = receiver.type === AST_NODE_TYPES.Identifier && receiver.name === arrLocalName || receiver.type === AST_NODE_TYPES.MemberExpression && !receiver.computed && receiver.object.type === AST_NODE_TYPES.Identifier && receiver.object.name === namespaceLocalName && receiver.property.type === AST_NODE_TYPES.Identifier && receiver.property.name === "Arr";
248
- const [arrayArg, countArg] = node.arguments;
249
- if (isArrObject && arrayArg !== void 0 && countArg !== void 0 && node.arguments.length === 2 && arrayArg.type !== AST_NODE_TYPES.SpreadElement && asNonNegativeIntegerLiteral(countArg) === 1) {
250
- report(
251
- node,
252
- { fn: methodName === "skip" ? "tail" : "butLast" },
253
- toArgText(arrayArg)
254
- );
255
- }
256
- return;
257
- }
258
- if (methodName === "concat" && receiver.type === AST_NODE_TYPES.ArrayExpression) {
259
- const [element] = receiver.elements;
260
- const [arg] = node.arguments;
261
- if (element !== null && element !== void 0 && arg !== void 0 && receiver.elements.length === 1 && element.type !== AST_NODE_TYPES.SpreadElement && node.arguments.length === 1 && arg.type !== AST_NODE_TYPES.SpreadElement && isArrayOrTupleType(getTypeOf(arg))) {
262
- report(
263
- node,
264
- { fn: "toUnshifted", itemText: toArgText(element) },
265
- toArgText(arg)
266
- );
267
- }
268
- return;
269
- }
270
- if (methodName !== "slice" && methodName !== "toSpliced" && methodName !== "filter" && methodName !== "concat") {
271
- return;
272
- }
273
- if (!isArrayOrTupleType(getTypeOf(receiver))) return;
274
- const receiverText = sourceCode.getText(receiver);
275
- switch (methodName) {
276
- case "slice": {
277
- const op = classifySliceArgs(node.arguments, receiver);
278
- if (op !== void 0) report(node, op, toArgText(receiver));
279
- return;
280
- }
281
- case "toSpliced": {
282
- const op = classifyToSplicedArgs(
283
- node.arguments,
284
- receiver,
285
- receiverText
286
- );
287
- if (op !== void 0) report(node, op, toArgText(receiver));
288
- return;
289
- }
290
- case "filter": {
291
- const [callback] = node.arguments;
292
- if (callback === void 0 || node.arguments.length !== 1) {
293
- return;
101
+ create: (context) => {
102
+ const sourceCode = context.sourceCode;
103
+ const program = sourceCode.ast;
104
+ const tsDataForgeImport = getTsDataForgeImport(program);
105
+ const parserServices = ESLintUtils.getParserServices(context);
106
+ const checker = parserServices.program.getTypeChecker();
107
+
108
+
109
+
110
+
111
+
112
+ const arrLocalName = getImportedLocalName(tsDataForgeImport, 'Arr');
113
+ const namespaceLocalName = tsDataForgeImport?.specifiers.find((specifier) => specifier.type === AST_NODE_TYPES.ImportNamespaceSpecifier)?.local.name;
114
+ const arrRef = arrLocalName ??
115
+ (namespaceLocalName === undefined ? 'Arr' : `${namespaceLocalName}.Arr`);
116
+ const needsArrImport = arrLocalName === undefined && namespaceLocalName === undefined;
117
+
118
+
119
+
120
+
121
+ const isArrayOrTupleType = (type) => {
122
+ if ((type.flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown)) !== 0) {
123
+ return false;
294
124
  }
295
- const op = classifyFilterCallback(callback, receiver, receiverText);
296
- if (op !== void 0) report(node, op, toArgText(receiver));
297
- return;
298
- }
299
- case "concat": {
300
- const [arg] = node.arguments;
301
- if (arg === void 0 || node.arguments.length !== 1 || arg.type === AST_NODE_TYPES.SpreadElement) {
302
- return;
125
+ if (type.isUnion()) {
126
+ return type.types.every(isArrayOrTupleType);
303
127
  }
304
- if (arg.type === AST_NODE_TYPES.ArrayExpression) {
305
- const [element] = arg.elements;
306
- if (element !== null && element !== void 0 && arg.elements.length === 1 && element.type !== AST_NODE_TYPES.SpreadElement) {
307
- report(
308
- node,
309
- { fn: "toPushed", itemText: toArgText(element) },
310
- toArgText(receiver)
311
- );
312
- }
313
- return;
128
+ if ((type.flags &
129
+ (ts.TypeFlags.TypeParameter |
130
+ ts.TypeFlags.IndexedAccess |
131
+ ts.TypeFlags.Conditional |
132
+ ts.TypeFlags.Substitution)) !==
133
+ 0) {
134
+ const constraint = checker.getBaseConstraintOfType(type);
135
+ return constraint !== undefined && isArrayOrTupleType(constraint);
314
136
  }
315
- if (isDefinitelyNotArrayType(getTypeOf(arg))) {
316
- report(
137
+ return checker.isArrayType(type) || checker.isTupleType(type);
138
+ };
139
+ const getTypeOf = (node) => checker.getTypeAtLocation(parserServices.esTreeNodeToTSNodeMap.get(node));
140
+
141
+
142
+
143
+
144
+
145
+ const isDefinitelyNotArrayType = (type) => {
146
+ if ((type.flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown)) !== 0) {
147
+ return false;
148
+ }
149
+ if (type.isUnion()) {
150
+ return type.types.every(isDefinitelyNotArrayType);
151
+ }
152
+ if ((type.flags &
153
+ (ts.TypeFlags.TypeParameter |
154
+ ts.TypeFlags.IndexedAccess |
155
+ ts.TypeFlags.Conditional |
156
+ ts.TypeFlags.Substitution)) !==
157
+ 0) {
158
+ const constraint = checker.getBaseConstraintOfType(type);
159
+ return constraint !== undefined && isDefinitelyNotArrayType(constraint);
160
+ }
161
+ return !checker.isArrayType(type) && !checker.isTupleType(type);
162
+ };
163
+
164
+ const toArgText = (node) => {
165
+ const text = sourceCode.getText(node);
166
+ return node.type === AST_NODE_TYPES.SequenceExpression
167
+ ? `(${text})`
168
+ : text;
169
+ };
170
+ const toReplacementText = (op, xsText) => {
171
+ switch (op.fn) {
172
+ case 'tail':
173
+ case 'butLast':
174
+ return `${arrRef}.${op.fn}(${xsText})`;
175
+ case 'skip':
176
+ case 'take':
177
+ case 'skipLast':
178
+ case 'takeLast':
179
+ return `${arrRef}.${op.fn}(${xsText}, ${op.count})`;
180
+
181
+
182
+
183
+ case 'toUnshifted':
184
+ return `${arrRef}.toUnshifted(${op.itemText})(${xsText})`;
185
+ case 'toPushed':
186
+ return `${arrRef}.toPushed(${xsText}, ${op.itemText})`;
187
+ }
188
+ };
189
+ const mut_nodesToFix = [];
190
+ const report = (node, op, xsText) => {
191
+ mut_nodesToFix.push({
317
192
  node,
318
- { fn: "toPushed", itemText: toArgText(arg) },
319
- toArgText(receiver)
320
- );
193
+ replacement: toReplacementText(op, xsText),
194
+ });
195
+ };
196
+
197
+ const isReceiverLengthAccess = (node, receiverText) => node.type === AST_NODE_TYPES.MemberExpression &&
198
+ !node.computed &&
199
+ node.property.type === AST_NODE_TYPES.Identifier &&
200
+ node.property.name === 'length' &&
201
+ sourceCode.getText(node.object) === receiverText;
202
+
203
+ const matchLengthMinus = (node, receiverText) => {
204
+ if (node.type !== AST_NODE_TYPES.BinaryExpression ||
205
+ node.operator !== '-' ||
206
+ !isReceiverLengthAccess(node.left, receiverText)) {
207
+ return undefined;
321
208
  }
322
- }
323
- }
324
- },
325
- ArrayExpression: (node) => {
326
- if (node.elements.length !== 2) return;
327
- const [first, second] = node.elements;
328
- if (first === null || first === void 0 || second === null || second === void 0) {
329
- return;
330
- }
331
- const [spread, item, fn] = first.type === AST_NODE_TYPES.SpreadElement && second.type !== AST_NODE_TYPES.SpreadElement ? [first, second, "toPushed"] : first.type !== AST_NODE_TYPES.SpreadElement && second.type === AST_NODE_TYPES.SpreadElement ? [second, first, "toUnshifted"] : [void 0, void 0, void 0];
332
- if (spread === void 0) return;
333
- if (!isArrayOrTupleType(getTypeOf(spread.argument))) return;
334
- report(
335
- node,
336
- { fn, itemText: toArgText(item) },
337
- toArgText(spread.argument)
338
- );
339
- },
340
- "Program:exit": () => {
341
- for (const [index, nodeInfo] of mut_nodesToFix.entries()) {
342
- context.report({
343
- node: nodeInfo.node,
344
- messageId: "preferCanonicalArraySlicing",
345
- data: { replacement: nodeInfo.replacement },
346
- fix: (fixer) => {
347
- const importFixes = index === 0 && needsArrImport ? buildImportFixes(fixer, program, tsDataForgeImport, ["Arr"]) : [];
348
- return [
349
- ...importFixes,
350
- fixer.replaceText(nodeInfo.node, nodeInfo.replacement)
351
- ];
209
+ const m = asNonNegativeIntegerLiteral(node.right);
210
+ return m !== undefined && m >= 1 ? m : undefined;
211
+ };
212
+
213
+
214
+
215
+
216
+
217
+ const classifyFilterCallback = (callback, receiver, receiverText) => {
218
+ if (callback.type !== AST_NODE_TYPES.ArrowFunctionExpression ||
219
+ callback.async ||
220
+ callback.body.type === AST_NODE_TYPES.BlockStatement ||
221
+ callback.params.length !== 2) {
222
+ return undefined;
223
+ }
224
+ const [elementParam, indexParam] = callback.params;
225
+ if (elementParam === undefined ||
226
+ indexParam === undefined ||
227
+ elementParam.type !== AST_NODE_TYPES.Identifier ||
228
+ indexParam.type !== AST_NODE_TYPES.Identifier) {
229
+ return undefined;
352
230
  }
353
- });
354
- }
355
- }
356
- };
357
- },
358
- defaultOptions: []
359
- };
360
231
 
361
- export { preferCanonicalArraySlicing };
362
- //# sourceMappingURL=prefer-canonical-array-slicing.mjs.map
232
+
233
+
234
+ const receiverRoot = rootIdentifierName(receiver);
235
+ if (receiverRoot !== undefined &&
236
+ (elementParam.name === receiverRoot || indexParam.name === receiverRoot)) {
237
+ return undefined;
238
+ }
239
+ const body = callback.body;
240
+ if (body.type !== AST_NODE_TYPES.BinaryExpression ||
241
+ !COMPARISON_OPERATORS.has(body.operator)) {
242
+ return undefined;
243
+ }
244
+
245
+ const rawOperator = body.operator;
246
+ const isIndex = (side) => side.type === AST_NODE_TYPES.Identifier &&
247
+ side.name === indexParam.name;
248
+ const [op, other] = isIndex(body.left)
249
+ ? [rawOperator, body.right]
250
+ : isIndex(body.right)
251
+ ? [FLIPPED_OPERATORS[rawOperator], body.left]
252
+ : [undefined, undefined];
253
+
254
+
255
+ if (op === undefined || isIndex(other)) {
256
+ return undefined;
257
+ }
258
+
259
+
260
+
261
+
262
+ const n = asNonNegativeIntegerLiteral(other);
263
+ if (n !== undefined) {
264
+ return classifyLiteralComparison(op, n);
265
+ }
266
+ if (!isSimpleReference(receiver))
267
+ return undefined;
268
+ const m = matchLengthMinus(other, receiverText);
269
+ return m === undefined ? undefined : classifyLengthComparison(op, m);
270
+ };
271
+ const classifySliceArgs = (args, receiver) => {
272
+ if (args.length === 1) {
273
+ const [start] = args;
274
+ if (start === undefined)
275
+ return undefined;
276
+ const n = asNonNegativeIntegerLiteral(start);
277
+ if (n !== undefined)
278
+ return normalizeCountedOp('skip', n);
279
+ const negated = asNegatedPositiveIntegerLiteral(start);
280
+ return negated === undefined
281
+ ? undefined
282
+ : normalizeCountedOp('takeLast', negated);
283
+ }
284
+ if (args.length === 2) {
285
+ const [start, end] = args;
286
+ if (start === undefined ||
287
+ end === undefined ||
288
+ asNonNegativeIntegerLiteral(start) !== 0) {
289
+ return undefined;
290
+ }
291
+ const n = asNonNegativeIntegerLiteral(end);
292
+ if (n !== undefined)
293
+ return normalizeCountedOp('take', n);
294
+ const negated = asNegatedPositiveIntegerLiteral(end);
295
+ if (negated !== undefined) {
296
+ return normalizeCountedOp('skipLast', negated);
297
+ }
298
+ if (!isSimpleReference(receiver))
299
+ return undefined;
300
+ const m = matchLengthMinus(end, sourceCode.getText(receiver));
301
+ return m === undefined ? undefined : normalizeCountedOp('skipLast', m);
302
+ }
303
+ return undefined;
304
+ };
305
+ const classifyToSplicedArgs = (args, receiver, receiverText) => {
306
+ if (args.length === 1) {
307
+ const [start] = args;
308
+ if (start === undefined)
309
+ return undefined;
310
+ const n = asNonNegativeIntegerLiteral(start);
311
+
312
+ if (n !== undefined)
313
+ return normalizeCountedOp('take', n);
314
+ const negated = asNegatedPositiveIntegerLiteral(start);
315
+
316
+ return negated === undefined
317
+ ? undefined
318
+ : normalizeCountedOp('skipLast', negated);
319
+ }
320
+ if (args.length === 2) {
321
+ const [start, deleteCount] = args;
322
+ if (start === undefined ||
323
+ deleteCount === undefined ||
324
+ asNonNegativeIntegerLiteral(start) !== 0) {
325
+ return undefined;
326
+ }
327
+
328
+ const n = asNonNegativeIntegerLiteral(deleteCount);
329
+ if (n !== undefined)
330
+ return normalizeCountedOp('skip', n);
331
+
332
+
333
+
334
+ if (!isSimpleReference(receiver))
335
+ return undefined;
336
+ const m = matchLengthMinus(deleteCount, receiverText);
337
+ return m === undefined ? undefined : normalizeCountedOp('takeLast', m);
338
+ }
339
+ if (args.length === 3) {
340
+ const [start, deleteCount, item] = args;
341
+ if (start === undefined ||
342
+ deleteCount === undefined ||
343
+ item === undefined ||
344
+ item.type === AST_NODE_TYPES.SpreadElement ||
345
+ asNonNegativeIntegerLiteral(deleteCount) !== 0) {
346
+ return undefined;
347
+ }
348
+
349
+ if (asNonNegativeIntegerLiteral(start) === 0) {
350
+ return { fn: 'toUnshifted', itemText: toArgText(item) };
351
+ }
352
+
353
+ if (isSimpleReference(receiver) &&
354
+ isReceiverLengthAccess(start, receiverText)) {
355
+ return { fn: 'toPushed', itemText: toArgText(item) };
356
+ }
357
+ return undefined;
358
+ }
359
+ return undefined;
360
+ };
361
+ return {
362
+ CallExpression: (node) => {
363
+ const callee = node.callee;
364
+ if (callee.type !== AST_NODE_TYPES.MemberExpression ||
365
+ callee.computed ||
366
+ callee.property.type !== AST_NODE_TYPES.Identifier) {
367
+ return;
368
+ }
369
+ const methodName = callee.property.name;
370
+ const receiver = callee.object;
371
+
372
+
373
+ if (methodName === 'skip' || methodName === 'skipLast') {
374
+ const isArrObject = (receiver.type === AST_NODE_TYPES.Identifier &&
375
+ receiver.name === arrLocalName) ||
376
+ (receiver.type === AST_NODE_TYPES.MemberExpression &&
377
+ !receiver.computed &&
378
+ receiver.object.type === AST_NODE_TYPES.Identifier &&
379
+ receiver.object.name === namespaceLocalName &&
380
+ receiver.property.type === AST_NODE_TYPES.Identifier &&
381
+ receiver.property.name === 'Arr');
382
+ const [arrayArg, countArg] = node.arguments;
383
+ if (isArrObject &&
384
+ arrayArg !== undefined &&
385
+ countArg !== undefined &&
386
+ node.arguments.length === 2 &&
387
+ arrayArg.type !== AST_NODE_TYPES.SpreadElement &&
388
+ asNonNegativeIntegerLiteral(countArg) === 1) {
389
+ report(node, { fn: methodName === 'skip' ? 'tail' : 'butLast' }, toArgText(arrayArg));
390
+ }
391
+ return;
392
+ }
393
+
394
+ if (methodName === 'concat' &&
395
+ receiver.type === AST_NODE_TYPES.ArrayExpression) {
396
+ const [element] = receiver.elements;
397
+ const [arg] = node.arguments;
398
+ if (element !== null &&
399
+ element !== undefined &&
400
+ arg !== undefined &&
401
+ receiver.elements.length === 1 &&
402
+ element.type !== AST_NODE_TYPES.SpreadElement &&
403
+ node.arguments.length === 1 &&
404
+ arg.type !== AST_NODE_TYPES.SpreadElement &&
405
+ isArrayOrTupleType(getTypeOf(arg))) {
406
+ report(node, { fn: 'toUnshifted', itemText: toArgText(element) }, toArgText(arg));
407
+ }
408
+ return;
409
+ }
410
+ if (methodName !== 'slice' &&
411
+ methodName !== 'toSpliced' &&
412
+ methodName !== 'filter' &&
413
+ methodName !== 'concat') {
414
+ return;
415
+ }
416
+
417
+
418
+ if (!isArrayOrTupleType(getTypeOf(receiver)))
419
+ return;
420
+ const receiverText = sourceCode.getText(receiver);
421
+ switch (methodName) {
422
+ case 'slice': {
423
+ const op = classifySliceArgs(node.arguments, receiver);
424
+ if (op !== undefined)
425
+ report(node, op, toArgText(receiver));
426
+ return;
427
+ }
428
+ case 'toSpliced': {
429
+ const op = classifyToSplicedArgs(node.arguments, receiver, receiverText);
430
+ if (op !== undefined)
431
+ report(node, op, toArgText(receiver));
432
+ return;
433
+ }
434
+ case 'filter': {
435
+ const [callback] = node.arguments;
436
+ if (callback === undefined || node.arguments.length !== 1) {
437
+ return;
438
+ }
439
+ const op = classifyFilterCallback(callback, receiver, receiverText);
440
+ if (op !== undefined)
441
+ report(node, op, toArgText(receiver));
442
+ return;
443
+ }
444
+ case 'concat': {
445
+ const [arg] = node.arguments;
446
+ if (arg === undefined ||
447
+ node.arguments.length !== 1 ||
448
+ arg.type === AST_NODE_TYPES.SpreadElement) {
449
+ return;
450
+ }
451
+
452
+ if (arg.type === AST_NODE_TYPES.ArrayExpression) {
453
+ const [element] = arg.elements;
454
+ if (element !== null &&
455
+ element !== undefined &&
456
+ arg.elements.length === 1 &&
457
+ element.type !== AST_NODE_TYPES.SpreadElement) {
458
+ report(node, { fn: 'toPushed', itemText: toArgText(element) }, toArgText(receiver));
459
+ }
460
+ return;
461
+ }
462
+
463
+
464
+ if (isDefinitelyNotArrayType(getTypeOf(arg))) {
465
+ report(node, { fn: 'toPushed', itemText: toArgText(arg) }, toArgText(receiver));
466
+ }
467
+ }
468
+ }
469
+ },
470
+ ArrayExpression: (node) => {
471
+
472
+
473
+ if (node.elements.length !== 2)
474
+ return;
475
+ const [first, second] = node.elements;
476
+ if (first === null ||
477
+ first === undefined ||
478
+ second === null ||
479
+ second === undefined) {
480
+ return;
481
+ }
482
+ const [spread, item, fn] = first.type === AST_NODE_TYPES.SpreadElement &&
483
+ second.type !== AST_NODE_TYPES.SpreadElement
484
+ ? [first, second, 'toPushed']
485
+ : first.type !== AST_NODE_TYPES.SpreadElement &&
486
+ second.type === AST_NODE_TYPES.SpreadElement
487
+ ? [second, first, 'toUnshifted']
488
+ : [undefined, undefined, undefined];
489
+
490
+
491
+
492
+ if (spread === undefined)
493
+ return;
494
+
495
+
496
+ if (!isArrayOrTupleType(getTypeOf(spread.argument)))
497
+ return;
498
+ report(node, { fn, itemText: toArgText(item) }, toArgText(spread.argument));
499
+ },
500
+ 'Program:exit': () => {
501
+ for (const [index, nodeInfo] of mut_nodesToFix.entries()) {
502
+ context.report({
503
+ node: nodeInfo.node,
504
+ messageId: 'preferCanonicalArraySlicing',
505
+ data: { replacement: nodeInfo.replacement },
506
+ fix: (fixer) => {
507
+ const importFixes = index === 0 && needsArrImport
508
+ ? buildImportFixes(fixer, program, tsDataForgeImport, ['Arr'])
509
+ : [];
510
+ return [
511
+ ...importFixes,
512
+ fixer.replaceText(nodeInfo.node, nodeInfo.replacement),
513
+ ];
514
+ },
515
+ });
516
+ }
517
+ },
518
+ };
519
+ },
520
+ defaultOptions: [],
521
+ };
522
+ //# sourceMappingURL=prefer-canonical-array-slicing.mjs.map