eslint-plugin-ts-data-forge 0.8.5 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +20 -19
- package/dist/plugin.d.mts +2 -0
- package/dist/plugin.d.mts.map +1 -1
- package/dist/plugin.mjs +1 -0
- package/dist/plugin.mjs.map +1 -1
- package/dist/rule-types.d.mts +16 -1
- package/dist/rule-types.d.mts.map +1 -1
- package/dist/rule-types.mjs.map +1 -1
- package/dist/rules/ast-utils.d.mts +21 -34
- package/dist/rules/ast-utils.d.mts.map +1 -1
- package/dist/rules/ast-utils.mjs +53 -58
- package/dist/rules/ast-utils.mjs.map +1 -1
- package/dist/rules/constants.d.mts +2 -0
- package/dist/rules/constants.d.mts.map +1 -1
- package/dist/rules/constants.mjs +9 -0
- package/dist/rules/constants.mjs.map +1 -1
- package/dist/rules/mutable-result.d.mts +10 -0
- package/dist/rules/mutable-result.d.mts.map +1 -0
- package/dist/rules/mutable-result.mjs +145 -0
- package/dist/rules/mutable-result.mjs.map +1 -0
- package/dist/rules/no-unnecessary-type-guard.d.mts.map +1 -1
- package/dist/rules/no-unnecessary-type-guard.mjs +6 -6
- package/dist/rules/no-unnecessary-type-guard.mjs.map +1 -1
- package/dist/rules/prefer-arr-generate.d.mts +57 -0
- package/dist/rules/prefer-arr-generate.d.mts.map +1 -0
- package/dist/rules/prefer-arr-generate.mjs +588 -0
- package/dist/rules/prefer-arr-generate.mjs.map +1 -0
- package/dist/rules/prefer-arr-is-array.d.mts.map +1 -1
- package/dist/rules/prefer-arr-is-array.mjs +7 -5
- package/dist/rules/prefer-arr-is-array.mjs.map +1 -1
- package/dist/rules/prefer-arr-is-bounded-length-array.d.mts.map +1 -1
- package/dist/rules/prefer-arr-is-bounded-length-array.mjs +9 -6
- package/dist/rules/prefer-arr-is-bounded-length-array.mjs.map +1 -1
- package/dist/rules/prefer-arr-is-fixed-length-array.d.mts.map +1 -1
- package/dist/rules/prefer-arr-is-fixed-length-array.mjs +9 -6
- package/dist/rules/prefer-arr-is-fixed-length-array.mjs.map +1 -1
- package/dist/rules/prefer-arr-is-max-length-array.d.mts.map +1 -1
- package/dist/rules/prefer-arr-is-max-length-array.mjs +9 -6
- package/dist/rules/prefer-arr-is-max-length-array.mjs.map +1 -1
- package/dist/rules/prefer-arr-is-min-length-array.d.mts.map +1 -1
- package/dist/rules/prefer-arr-is-min-length-array.mjs +9 -6
- package/dist/rules/prefer-arr-is-min-length-array.mjs.map +1 -1
- package/dist/rules/prefer-arr-is-non-empty.d.mts.map +1 -1
- package/dist/rules/prefer-arr-is-non-empty.mjs +9 -6
- package/dist/rules/prefer-arr-is-non-empty.mjs.map +1 -1
- package/dist/rules/prefer-arr-scan.d.mts.map +1 -1
- package/dist/rules/prefer-arr-scan.mjs +8 -11
- package/dist/rules/prefer-arr-scan.mjs.map +1 -1
- package/dist/rules/prefer-arr-sum.d.mts.map +1 -1
- package/dist/rules/prefer-arr-sum.mjs +8 -6
- package/dist/rules/prefer-arr-sum.mjs.map +1 -1
- package/dist/rules/prefer-arr-uniq.d.mts.map +1 -1
- package/dist/rules/prefer-arr-uniq.mjs +12 -166
- package/dist/rules/prefer-arr-uniq.mjs.map +1 -1
- package/dist/rules/prefer-as-int.d.mts.map +1 -1
- package/dist/rules/prefer-as-int.mjs +6 -8
- package/dist/rules/prefer-as-int.mjs.map +1 -1
- package/dist/rules/prefer-canonical-array-slicing.d.mts.map +1 -1
- package/dist/rules/prefer-canonical-array-slicing.mjs +57 -34
- package/dist/rules/prefer-canonical-array-slicing.mjs.map +1 -1
- package/dist/rules/prefer-canonical-length-cast.d.mts.map +1 -1
- package/dist/rules/prefer-canonical-length-cast.mjs +3 -3
- package/dist/rules/prefer-canonical-length-cast.mjs.map +1 -1
- package/dist/rules/prefer-canonical-length-guard.d.mts.map +1 -1
- package/dist/rules/prefer-canonical-length-guard.mjs +4 -4
- package/dist/rules/prefer-canonical-length-guard.mjs.map +1 -1
- package/dist/rules/prefer-comparison-over-nullish-guard.d.mts.map +1 -1
- package/dist/rules/prefer-comparison-over-nullish-guard.mjs +3 -2
- package/dist/rules/prefer-comparison-over-nullish-guard.mjs.map +1 -1
- package/dist/rules/prefer-is-non-null-object.d.mts.map +1 -1
- package/dist/rules/prefer-is-non-null-object.mjs +113 -43
- package/dist/rules/prefer-is-non-null-object.mjs.map +1 -1
- package/dist/rules/prefer-is-record-and-has-key.d.mts.map +1 -1
- package/dist/rules/prefer-is-record-and-has-key.mjs +105 -48
- package/dist/rules/prefer-is-record-and-has-key.mjs.map +1 -1
- package/dist/rules/prefer-num-safe-parse-float.d.mts.map +1 -1
- package/dist/rules/prefer-num-safe-parse-float.mjs +6 -6
- package/dist/rules/prefer-num-safe-parse-float.mjs.map +1 -1
- package/dist/rules/prefer-num-safe-parse-int.d.mts.map +1 -1
- package/dist/rules/prefer-num-safe-parse-int.mjs +6 -6
- package/dist/rules/prefer-num-safe-parse-int.mjs.map +1 -1
- package/dist/rules/prefer-obj-over-entries-round-trip.d.mts.map +1 -1
- package/dist/rules/prefer-obj-over-entries-round-trip.mjs +9 -12
- package/dist/rules/prefer-obj-over-entries-round-trip.mjs.map +1 -1
- package/dist/rules/prefer-range-for-loop.d.mts.map +1 -1
- package/dist/rules/prefer-range-for-loop.mjs +104 -23
- package/dist/rules/prefer-range-for-loop.mjs.map +1 -1
- package/dist/rules/rules.d.mts +1 -0
- package/dist/rules/rules.d.mts.map +1 -1
- package/dist/rules/rules.mjs +2 -0
- package/dist/rules/rules.mjs.map +1 -1
- package/package.json +11 -10
- package/src/plugin.mts +1 -0
- package/src/rule-types.mts +17 -1
- package/src/rules/ast-utils.mts +73 -80
- package/src/rules/constants.mts +12 -0
- package/src/rules/mutable-result.mts +204 -0
- package/src/rules/no-unnecessary-type-guard.mts +14 -10
- package/src/rules/prefer-arr-generate.mts +918 -0
- package/src/rules/prefer-arr-generate.test.mts +967 -0
- package/src/rules/prefer-arr-is-array.mts +14 -7
- package/src/rules/prefer-arr-is-array.test.mts +14 -0
- package/src/rules/prefer-arr-is-bounded-length-array.mts +16 -10
- package/src/rules/prefer-arr-is-bounded-length-array.test.mts +19 -0
- package/src/rules/prefer-arr-is-fixed-length-array.mts +17 -11
- package/src/rules/prefer-arr-is-max-length-array.mts +17 -11
- package/src/rules/prefer-arr-is-max-length-array.test.mts +8 -0
- package/src/rules/prefer-arr-is-min-length-array.mts +17 -11
- package/src/rules/prefer-arr-is-min-length-array.test.mts +8 -0
- package/src/rules/prefer-arr-is-non-empty.mts +16 -13
- package/src/rules/prefer-arr-scan.mts +24 -19
- package/src/rules/prefer-arr-sum.mts +18 -14
- package/src/rules/prefer-arr-uniq.mts +25 -234
- package/src/rules/prefer-as-int.mts +19 -12
- package/src/rules/prefer-canonical-array-slicing.mts +79 -52
- package/src/rules/prefer-canonical-array-slicing.test.mts +55 -0
- package/src/rules/prefer-canonical-length-cast.mts +7 -3
- package/src/rules/prefer-canonical-length-guard.mts +9 -4
- package/src/rules/prefer-comparison-over-nullish-guard.mts +3 -2
- package/src/rules/prefer-comparison-over-nullish-guard.test.mts +16 -0
- package/src/rules/prefer-is-non-null-object.mts +171 -64
- package/src/rules/prefer-is-non-null-object.test.mts +138 -0
- package/src/rules/prefer-is-record-and-has-key.mts +159 -74
- package/src/rules/prefer-is-record-and-has-key.test.mts +118 -23
- package/src/rules/prefer-num-safe-parse-float.mts +15 -10
- package/src/rules/prefer-num-safe-parse-int.mts +15 -13
- package/src/rules/prefer-obj-over-entries-round-trip.mts +19 -22
- package/src/rules/prefer-range-for-loop.mts +157 -31
- package/src/rules/prefer-range-for-loop.test.mts +127 -5
- package/src/rules/rules.mts +2 -0
- package/dist/rules/import-utils.d.mts +0 -30
- package/dist/rules/import-utils.d.mts.map +0 -1
- package/dist/rules/import-utils.mjs +0 -98
- package/dist/rules/import-utils.mjs.map +0 -1
- package/src/rules/import-utils.mts +0 -175
|
@@ -3,14 +3,17 @@ import {
|
|
|
3
3
|
type TSESLint,
|
|
4
4
|
type TSESTree,
|
|
5
5
|
} from '@typescript-eslint/utils';
|
|
6
|
-
import { Arr } from 'ts-data-forge';
|
|
7
|
-
import * as ts from 'typescript';
|
|
8
|
-
import { typeWrapperLayers } from './ast-utils.mjs';
|
|
9
6
|
import {
|
|
10
|
-
|
|
7
|
+
buildNamedImportFix,
|
|
8
|
+
getArgumentText,
|
|
9
|
+
getImportDeclarations,
|
|
11
10
|
getNamedImports,
|
|
12
|
-
|
|
13
|
-
} from '
|
|
11
|
+
skipTypeWrappers,
|
|
12
|
+
} from 'eslint-rule-forge';
|
|
13
|
+
import { Arr } from 'ts-data-forge';
|
|
14
|
+
import * as ts from 'typescript';
|
|
15
|
+
import { isIdentifierNamed, typeHoldsAtEveryLayer } from './ast-utils.mjs';
|
|
16
|
+
import { TS_DATA_FORGE_MODULE } from './constants.mjs';
|
|
14
17
|
|
|
15
18
|
type Options = readonly [];
|
|
16
19
|
|
|
@@ -39,37 +42,45 @@ export const preferIsRecordAndHasKey: TSESLint.RuleModule<MessageIds, Options> =
|
|
|
39
42
|
|
|
40
43
|
const program = sourceCode.ast;
|
|
41
44
|
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
const checker = services?.program?.getTypeChecker();
|
|
45
|
+
const tsDataForgeImports = getImportDeclarations(
|
|
46
|
+
program,
|
|
47
|
+
TS_DATA_FORGE_MODULE,
|
|
48
|
+
);
|
|
47
49
|
|
|
48
50
|
/**
|
|
49
51
|
* Whether the `isRecord(...)` half of the rewrite would be dead code
|
|
50
52
|
* because the object already satisfies `hasKey`'s
|
|
51
|
-
* `R extends UnknownRecord` constraint
|
|
52
|
-
*
|
|
53
|
+
* `R extends UnknownRecord` constraint, at every layer of type
|
|
54
|
+
* wrappers: an array cast to a record is still rejected by `isRecord`,
|
|
55
|
+
* and a record widened to one that may be an array is the author saying
|
|
56
|
+
* it may need the guard. Without type information nothing is known, so
|
|
57
|
+
* the guard is kept.
|
|
53
58
|
*/
|
|
54
59
|
const isKnownRecord = (
|
|
55
60
|
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
56
61
|
expression: TSESTree.Expression,
|
|
57
|
-
): boolean =>
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
62
|
+
): boolean =>
|
|
63
|
+
typeHoldsAtEveryLayer(sourceCode, expression, (type, checker) =>
|
|
64
|
+
isAlreadyRecordType(checker, type),
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Whether the object may be an array at some layer of type wrappers.
|
|
69
|
+
* `isRecord` rejects an array, so `'0' in xs` would turn from `true` to
|
|
70
|
+
* `false`; a widening cast to a type that may be an array is the author
|
|
71
|
+
* saying it may be one. Without type information nothing is known, and
|
|
72
|
+
* the rule reports as it always has.
|
|
73
|
+
*/
|
|
74
|
+
const mayBeArray = (
|
|
75
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
76
|
+
expression: TSESTree.Expression,
|
|
77
|
+
): boolean =>
|
|
78
|
+
sourceCode.parserServices?.program != null &&
|
|
79
|
+
!typeHoldsAtEveryLayer(
|
|
80
|
+
sourceCode,
|
|
81
|
+
expression,
|
|
82
|
+
(type, checker) => !mayBeArrayType(checker, type),
|
|
83
|
+
);
|
|
73
84
|
|
|
74
85
|
const mut_nodesToFix: {
|
|
75
86
|
node: TSESTree.CallExpression | TSESTree.BinaryExpression;
|
|
@@ -80,12 +91,13 @@ export const preferIsRecordAndHasKey: TSESLint.RuleModule<MessageIds, Options> =
|
|
|
80
91
|
return {
|
|
81
92
|
// Handle Object.hasOwn(obj, key)
|
|
82
93
|
CallExpression: (node) => {
|
|
94
|
+
const callee = skipTypeWrappers(node.callee);
|
|
95
|
+
|
|
83
96
|
if (
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
node.callee.property.name !== 'hasOwn'
|
|
97
|
+
callee.type !== AST_NODE_TYPES.MemberExpression ||
|
|
98
|
+
!isIdentifierNamed(callee.object, 'Object') ||
|
|
99
|
+
callee.property.type !== AST_NODE_TYPES.Identifier ||
|
|
100
|
+
callee.property.name !== 'hasOwn'
|
|
89
101
|
) {
|
|
90
102
|
return;
|
|
91
103
|
}
|
|
@@ -106,7 +118,8 @@ export const preferIsRecordAndHasKey: TSESLint.RuleModule<MessageIds, Options> =
|
|
|
106
118
|
// Type guard: ensure arguments are Expressions (not SpreadElement)
|
|
107
119
|
if (
|
|
108
120
|
objArg.type === AST_NODE_TYPES.SpreadElement ||
|
|
109
|
-
keyArg.type === AST_NODE_TYPES.SpreadElement
|
|
121
|
+
keyArg.type === AST_NODE_TYPES.SpreadElement ||
|
|
122
|
+
mayBeArray(objArg)
|
|
110
123
|
) {
|
|
111
124
|
return;
|
|
112
125
|
}
|
|
@@ -129,7 +142,10 @@ export const preferIsRecordAndHasKey: TSESLint.RuleModule<MessageIds, Options> =
|
|
|
129
142
|
const objExpression = node.right;
|
|
130
143
|
|
|
131
144
|
// Type guard: ensure keyExpression is Expression (not PrivateIdentifier)
|
|
132
|
-
if (
|
|
145
|
+
if (
|
|
146
|
+
keyExpression.type === AST_NODE_TYPES.PrivateIdentifier ||
|
|
147
|
+
mayBeArray(objExpression)
|
|
148
|
+
) {
|
|
133
149
|
return;
|
|
134
150
|
}
|
|
135
151
|
|
|
@@ -141,30 +157,43 @@ export const preferIsRecordAndHasKey: TSESLint.RuleModule<MessageIds, Options> =
|
|
|
141
157
|
},
|
|
142
158
|
|
|
143
159
|
'Program:exit': () => {
|
|
144
|
-
const namedImports = getNamedImports(
|
|
160
|
+
const namedImports = getNamedImports(tsDataForgeImports);
|
|
145
161
|
|
|
146
|
-
const rewrites = mut_nodesToFix.map((entry) =>
|
|
147
|
-
...entry,
|
|
162
|
+
const rewrites = mut_nodesToFix.map((entry) => {
|
|
148
163
|
// A record-typed object makes the `isRecord(...)` conjunct always
|
|
149
164
|
// true, so it is left out of the replacement entirely.
|
|
150
|
-
needsIsRecord
|
|
151
|
-
|
|
165
|
+
const needsIsRecord = !isKnownRecord(entry.objExpression);
|
|
166
|
+
|
|
167
|
+
return {
|
|
168
|
+
...entry,
|
|
169
|
+
needsIsRecord,
|
|
170
|
+
// `isRecord(obj) && hasKey(obj, key)` evaluates `obj` twice.
|
|
171
|
+
fixable: !needsIsRecord || isSideEffectFree(entry.objExpression),
|
|
172
|
+
};
|
|
173
|
+
});
|
|
152
174
|
|
|
153
|
-
const
|
|
154
|
-
...(rewrites.some(({ needsIsRecord }) => needsIsRecord) &&
|
|
155
|
-
!namedImports.includes('isRecord')
|
|
156
|
-
? ['isRecord']
|
|
157
|
-
: []),
|
|
158
|
-
...(namedImports.includes('hasKey') ? [] : ['hasKey']),
|
|
159
|
-
];
|
|
175
|
+
const fixableRewrites = rewrites.filter(({ fixable }) => fixable);
|
|
160
176
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
177
|
+
const importsToAdd: readonly string[] = Arr.isEmpty(fixableRewrites)
|
|
178
|
+
? []
|
|
179
|
+
: [
|
|
180
|
+
...(fixableRewrites.some(
|
|
181
|
+
({ needsIsRecord }) => needsIsRecord,
|
|
182
|
+
) && !namedImports.includes('isRecord')
|
|
183
|
+
? ['isRecord']
|
|
184
|
+
: []),
|
|
185
|
+
...(namedImports.includes('hasKey') ? [] : ['hasKey']),
|
|
186
|
+
];
|
|
166
187
|
|
|
167
|
-
|
|
188
|
+
const importCarrier = fixableRewrites[0];
|
|
189
|
+
|
|
190
|
+
for (const rewrite of rewrites) {
|
|
191
|
+
const { node, objExpression, keyExpression, needsIsRecord } =
|
|
192
|
+
rewrite;
|
|
193
|
+
|
|
194
|
+
const objText = getArgumentText(sourceCode, objExpression);
|
|
195
|
+
|
|
196
|
+
const keyText = getArgumentText(sourceCode, keyExpression);
|
|
168
197
|
|
|
169
198
|
const originalText = sourceCode.getText(node);
|
|
170
199
|
|
|
@@ -176,26 +205,32 @@ export const preferIsRecordAndHasKey: TSESLint.RuleModule<MessageIds, Options> =
|
|
|
176
205
|
objName: objText,
|
|
177
206
|
keyName: keyText,
|
|
178
207
|
},
|
|
179
|
-
fix:
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
208
|
+
fix: !rewrite.fixable
|
|
209
|
+
? undefined
|
|
210
|
+
: (fixer) => {
|
|
211
|
+
const replacement = needsIsRecord
|
|
212
|
+
? `(isRecord(${objText}) && hasKey(${objText}, ${keyText}))`
|
|
213
|
+
: `hasKey(${objText}, ${keyText})`;
|
|
214
|
+
|
|
215
|
+
// All import fixes have to live on the first fixable
|
|
216
|
+
// report: `insertTextBefore(program, …)` produces
|
|
217
|
+
// overlapping ranges otherwise and only one of them would
|
|
218
|
+
// survive a pass.
|
|
219
|
+
const importFixes =
|
|
220
|
+
rewrite === importCarrier && importsToAdd.length > 0
|
|
221
|
+
? buildNamedImportFix(
|
|
222
|
+
fixer,
|
|
223
|
+
program,
|
|
224
|
+
TS_DATA_FORGE_MODULE,
|
|
225
|
+
importsToAdd,
|
|
226
|
+
)
|
|
227
|
+
: [];
|
|
228
|
+
|
|
229
|
+
return [
|
|
230
|
+
...importFixes,
|
|
231
|
+
fixer.replaceText(node, replacement),
|
|
232
|
+
];
|
|
233
|
+
},
|
|
199
234
|
});
|
|
200
235
|
}
|
|
201
236
|
},
|
|
@@ -204,6 +239,56 @@ export const preferIsRecordAndHasKey: TSESLint.RuleModule<MessageIds, Options> =
|
|
|
204
239
|
defaultOptions: [],
|
|
205
240
|
} as const;
|
|
206
241
|
|
|
242
|
+
/**
|
|
243
|
+
* Whether reading `node` again has no side effects: a variable, `this`, or a
|
|
244
|
+
* non-computed property chain on one, type wrappers aside.
|
|
245
|
+
*/
|
|
246
|
+
const isSideEffectFree = (
|
|
247
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
248
|
+
wrapped: TSESTree.Node,
|
|
249
|
+
): boolean => {
|
|
250
|
+
const node = skipTypeWrappers(wrapped);
|
|
251
|
+
|
|
252
|
+
return (
|
|
253
|
+
node.type === AST_NODE_TYPES.Identifier ||
|
|
254
|
+
node.type === AST_NODE_TYPES.ThisExpression ||
|
|
255
|
+
(node.type === AST_NODE_TYPES.MemberExpression &&
|
|
256
|
+
!node.computed &&
|
|
257
|
+
isSideEffectFree(node.object))
|
|
258
|
+
);
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Whether a value of `type` may be an array or a tuple: some member of a union
|
|
263
|
+
* or an intersection is one, or the constraint of a deferred type is.
|
|
264
|
+
* `object` and `unknown` say nothing either way and are not counted.
|
|
265
|
+
*/
|
|
266
|
+
const mayBeArrayType = (
|
|
267
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
268
|
+
checker: ts.TypeChecker,
|
|
269
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
270
|
+
type: ts.Type,
|
|
271
|
+
): boolean => {
|
|
272
|
+
if (type.isUnionOrIntersection()) {
|
|
273
|
+
return type.types.some((member) => mayBeArrayType(checker, member));
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
if (
|
|
277
|
+
(type.flags &
|
|
278
|
+
(ts.TypeFlags.TypeParameter |
|
|
279
|
+
ts.TypeFlags.IndexedAccess |
|
|
280
|
+
ts.TypeFlags.Conditional |
|
|
281
|
+
ts.TypeFlags.Substitution)) !==
|
|
282
|
+
0
|
|
283
|
+
) {
|
|
284
|
+
const constraint = checker.getBaseConstraintOfType(type);
|
|
285
|
+
|
|
286
|
+
return constraint !== undefined && mayBeArrayType(checker, constraint);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
return checker.isArrayType(type) || checker.isTupleType(type);
|
|
290
|
+
};
|
|
291
|
+
|
|
207
292
|
/**
|
|
208
293
|
* Whether `type` already satisfies `hasKey`'s `R extends UnknownRecord`
|
|
209
294
|
* constraint, so that guarding the call with `isRecord` adds nothing.
|
|
@@ -62,19 +62,6 @@ describe('prefer-is-record-and-has-key', () => {
|
|
|
62
62
|
`,
|
|
63
63
|
errors: [{ messageId: 'useHasKey' }],
|
|
64
64
|
},
|
|
65
|
-
{
|
|
66
|
-
name: 'keeps isRecord when only part of a union is a record',
|
|
67
|
-
code: dedent`
|
|
68
|
-
declare const obj: Record<string, unknown> | readonly unknown[];
|
|
69
|
-
const ok = Object.hasOwn(obj, 'a');
|
|
70
|
-
`,
|
|
71
|
-
output: dedent`
|
|
72
|
-
import { isRecord, hasKey } from 'ts-data-forge';
|
|
73
|
-
declare const obj: Record<string, unknown> | readonly unknown[];
|
|
74
|
-
const ok = (isRecord(obj) && hasKey(obj, 'a'));
|
|
75
|
-
`,
|
|
76
|
-
errors: [{ messageId: 'useIsRecordAndHasKey' }],
|
|
77
|
-
},
|
|
78
65
|
{
|
|
79
66
|
name: 'keeps isRecord for an index-signature-carrying callable',
|
|
80
67
|
code: dedent`
|
|
@@ -295,28 +282,136 @@ describe('prefer-is-record-and-has-key through type wrappers', () => {
|
|
|
295
282
|
valid: [],
|
|
296
283
|
invalid: [
|
|
297
284
|
{
|
|
298
|
-
name: '
|
|
285
|
+
name: 'reads `Object` through `as`',
|
|
299
286
|
code: dedent`
|
|
300
|
-
declare const
|
|
301
|
-
const ok = Object.hasOwn(
|
|
287
|
+
declare const obj: Readonly<Record<string, unknown>>;
|
|
288
|
+
const ok = (Object as ObjectConstructor).hasOwn(obj, 'a');
|
|
302
289
|
`,
|
|
303
290
|
output: dedent`
|
|
304
|
-
import {
|
|
291
|
+
import { hasKey } from 'ts-data-forge';
|
|
292
|
+
declare const obj: Readonly<Record<string, unknown>>;
|
|
293
|
+
const ok = hasKey(obj, 'a');
|
|
294
|
+
`,
|
|
295
|
+
errors: [{ messageId: 'useHasKey' }],
|
|
296
|
+
},
|
|
297
|
+
],
|
|
298
|
+
});
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
describe('prefer-is-record-and-has-key with parenthesized operands', () => {
|
|
302
|
+
tester.run('prefer-is-record-and-has-key', preferIsRecordAndHasKey, {
|
|
303
|
+
valid: [],
|
|
304
|
+
invalid: [
|
|
305
|
+
{
|
|
306
|
+
name: 'keeps the parentheses of a sequence object',
|
|
307
|
+
code: dedent`
|
|
308
|
+
declare const a: number;
|
|
309
|
+
declare const o: Readonly<Record<string, unknown>>;
|
|
310
|
+
const ok = 'k' in (a, o);
|
|
311
|
+
`,
|
|
312
|
+
output: dedent`
|
|
313
|
+
import { hasKey } from 'ts-data-forge';
|
|
314
|
+
declare const a: number;
|
|
315
|
+
declare const o: Readonly<Record<string, unknown>>;
|
|
316
|
+
const ok = hasKey((a, o), 'k');
|
|
317
|
+
`,
|
|
318
|
+
errors: [{ messageId: 'useHasKey' }],
|
|
319
|
+
},
|
|
320
|
+
{
|
|
321
|
+
name: 'keeps the parentheses of a sequence key',
|
|
322
|
+
code: dedent`
|
|
323
|
+
declare const a: number;
|
|
324
|
+
declare const o: Readonly<Record<string, unknown>>;
|
|
325
|
+
const ok = (a, 'k') in o;
|
|
326
|
+
`,
|
|
327
|
+
output: dedent`
|
|
328
|
+
import { hasKey } from 'ts-data-forge';
|
|
329
|
+
declare const a: number;
|
|
330
|
+
declare const o: Readonly<Record<string, unknown>>;
|
|
331
|
+
const ok = hasKey(o, (a, 'k'));
|
|
332
|
+
`,
|
|
333
|
+
errors: [{ messageId: 'useHasKey' }],
|
|
334
|
+
},
|
|
335
|
+
],
|
|
336
|
+
});
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
describe('prefer-is-record-and-has-key where the rewrite would change behavior', () => {
|
|
340
|
+
tester.run('prefer-is-record-and-has-key', preferIsRecordAndHasKey, {
|
|
341
|
+
valid: [
|
|
342
|
+
{
|
|
343
|
+
// `isRecord` rejects an array, so `'0' in xs` would turn false.
|
|
344
|
+
name: 'an array',
|
|
345
|
+
code: dedent`
|
|
346
|
+
declare const xs: readonly number[];
|
|
347
|
+
const a = '0' in xs;
|
|
348
|
+
const b = Object.hasOwn(xs, 'length');
|
|
349
|
+
`,
|
|
350
|
+
},
|
|
351
|
+
{
|
|
352
|
+
name: 'a union with an array',
|
|
353
|
+
code: dedent`
|
|
354
|
+
declare const v: Readonly<Record<string, unknown>> | readonly number[];
|
|
355
|
+
const ok = 'k' in v;
|
|
356
|
+
`,
|
|
357
|
+
},
|
|
358
|
+
{
|
|
359
|
+
name: 'an array cast to a record',
|
|
360
|
+
code: dedent`
|
|
305
361
|
declare const arr: readonly string[];
|
|
306
|
-
const ok = (
|
|
362
|
+
const ok = Object.hasOwn(arr as unknown as Record<string, unknown>, '0');
|
|
307
363
|
`,
|
|
308
|
-
errors: [{ messageId: 'useIsRecordAndHasKey' }],
|
|
309
364
|
},
|
|
310
365
|
{
|
|
311
|
-
name: '
|
|
366
|
+
name: 'a record widened by `as` to one that may be an array: the author says it may be one',
|
|
312
367
|
code: dedent`
|
|
313
368
|
declare const obj: Readonly<Record<string, unknown>>;
|
|
314
|
-
const ok = Object.hasOwn(obj as Readonly<Record<string, unknown>> | readonly
|
|
369
|
+
const ok = Object.hasOwn(obj as Readonly<Record<string, unknown>> | readonly string[], 'a');
|
|
370
|
+
`,
|
|
371
|
+
},
|
|
372
|
+
],
|
|
373
|
+
invalid: [
|
|
374
|
+
{
|
|
375
|
+
// `isRecord(…) && hasKey(…)` would evaluate the object twice.
|
|
376
|
+
name: 'an object with side effects is reported without a fix',
|
|
377
|
+
code: dedent`
|
|
378
|
+
declare const getObj: () => object;
|
|
379
|
+
declare const log: () => void;
|
|
380
|
+
declare const o: object;
|
|
381
|
+
const a = 'k' in getObj();
|
|
382
|
+
const b = Object.hasOwn(getObj(), 'k');
|
|
383
|
+
const c = 'k' in (log(), o);
|
|
384
|
+
`,
|
|
385
|
+
output: null,
|
|
386
|
+
errors: [
|
|
387
|
+
{ messageId: 'useIsRecordAndHasKey' },
|
|
388
|
+
{ messageId: 'useIsRecordAndHasKey' },
|
|
389
|
+
{ messageId: 'useIsRecordAndHasKey' },
|
|
390
|
+
],
|
|
391
|
+
},
|
|
392
|
+
{
|
|
393
|
+
name: 'an object with side effects is fixed when isRecord is dropped: it is read once',
|
|
394
|
+
code: dedent`
|
|
395
|
+
declare const getRec: () => Readonly<Record<string, unknown>>;
|
|
396
|
+
const ok = 'k' in getRec();
|
|
397
|
+
`,
|
|
398
|
+
output: dedent`
|
|
399
|
+
import { hasKey } from 'ts-data-forge';
|
|
400
|
+
declare const getRec: () => Readonly<Record<string, unknown>>;
|
|
401
|
+
const ok = hasKey(getRec(), 'k');
|
|
402
|
+
`,
|
|
403
|
+
errors: [{ messageId: 'useHasKey' }],
|
|
404
|
+
},
|
|
405
|
+
{
|
|
406
|
+
name: 'a reference read through type wrappers is still read without side effects',
|
|
407
|
+
code: dedent`
|
|
408
|
+
declare const o: { readonly p: object };
|
|
409
|
+
const ok = 'k' in (o.p satisfies object);
|
|
315
410
|
`,
|
|
316
411
|
output: dedent`
|
|
317
412
|
import { isRecord, hasKey } from 'ts-data-forge';
|
|
318
|
-
declare const
|
|
319
|
-
const ok = (isRecord(
|
|
413
|
+
declare const o: { readonly p: object };
|
|
414
|
+
const ok = (isRecord(o.p satisfies object) && hasKey(o.p satisfies object, 'k'));
|
|
320
415
|
`,
|
|
321
416
|
errors: [{ messageId: 'useIsRecordAndHasKey' }],
|
|
322
417
|
},
|
|
@@ -3,13 +3,15 @@ import {
|
|
|
3
3
|
type TSESLint,
|
|
4
4
|
type TSESTree,
|
|
5
5
|
} from '@typescript-eslint/utils';
|
|
6
|
-
import * as ts from 'typescript';
|
|
7
|
-
import { toArgumentText, typeWrapperLayers } from './ast-utils.mjs';
|
|
8
6
|
import {
|
|
9
|
-
|
|
7
|
+
buildNamedImportFix,
|
|
8
|
+
getArgumentText,
|
|
9
|
+
getImportDeclarations,
|
|
10
10
|
getNamedImports,
|
|
11
|
-
|
|
12
|
-
} from '
|
|
11
|
+
typeWrapperLayers,
|
|
12
|
+
} from 'eslint-rule-forge';
|
|
13
|
+
import * as ts from 'typescript';
|
|
14
|
+
import { TS_DATA_FORGE_MODULE } from './constants.mjs';
|
|
13
15
|
|
|
14
16
|
type Options = readonly [];
|
|
15
17
|
|
|
@@ -62,7 +64,10 @@ export const preferNumSafeParseFloat: TSESLint.RuleModule<MessageIds, Options> =
|
|
|
62
64
|
|
|
63
65
|
const program = sourceCode.ast;
|
|
64
66
|
|
|
65
|
-
const
|
|
67
|
+
const tsDataForgeImports = getImportDeclarations(
|
|
68
|
+
program,
|
|
69
|
+
TS_DATA_FORGE_MODULE,
|
|
70
|
+
);
|
|
66
71
|
|
|
67
72
|
const services = context.sourceCode.parserServices;
|
|
68
73
|
|
|
@@ -145,7 +150,7 @@ export const preferNumSafeParseFloat: TSESLint.RuleModule<MessageIds, Options> =
|
|
|
145
150
|
mut_nodesToFix.push({ node, argExpression: firstArg });
|
|
146
151
|
},
|
|
147
152
|
'Program:exit': () => {
|
|
148
|
-
const namedImports = getNamedImports(
|
|
153
|
+
const namedImports = getNamedImports(tsDataForgeImports);
|
|
149
154
|
|
|
150
155
|
const missingImports = (['Num', 'Result'] as const).filter(
|
|
151
156
|
(name) => !namedImports.includes(name),
|
|
@@ -155,7 +160,7 @@ export const preferNumSafeParseFloat: TSESLint.RuleModule<MessageIds, Options> =
|
|
|
155
160
|
index,
|
|
156
161
|
{ node, argExpression },
|
|
157
162
|
] of mut_nodesToFix.entries()) {
|
|
158
|
-
const argText =
|
|
163
|
+
const argText = getArgumentText(sourceCode, argExpression);
|
|
159
164
|
|
|
160
165
|
const originalText = sourceCode.getText(node);
|
|
161
166
|
|
|
@@ -171,10 +176,10 @@ export const preferNumSafeParseFloat: TSESLint.RuleModule<MessageIds, Options> =
|
|
|
171
176
|
|
|
172
177
|
const importFixes =
|
|
173
178
|
index === 0 && missingImports.length > 0
|
|
174
|
-
?
|
|
179
|
+
? buildNamedImportFix(
|
|
175
180
|
fixer,
|
|
176
181
|
program,
|
|
177
|
-
|
|
182
|
+
TS_DATA_FORGE_MODULE,
|
|
178
183
|
missingImports,
|
|
179
184
|
)
|
|
180
185
|
: [];
|
|
@@ -3,17 +3,16 @@ import {
|
|
|
3
3
|
type TSESLint,
|
|
4
4
|
type TSESTree,
|
|
5
5
|
} from '@typescript-eslint/utils';
|
|
6
|
-
import * as ts from 'typescript';
|
|
7
6
|
import {
|
|
7
|
+
buildNamedImportFix,
|
|
8
|
+
getArgumentText,
|
|
9
|
+
getImportDeclarations,
|
|
10
|
+
getNamedImports,
|
|
8
11
|
skipTypeWrappers,
|
|
9
|
-
toArgumentText,
|
|
10
12
|
typeWrapperLayers,
|
|
11
|
-
} from '
|
|
12
|
-
import
|
|
13
|
-
|
|
14
|
-
getNamedImports,
|
|
15
|
-
getTsDataForgeImport,
|
|
16
|
-
} from './import-utils.mjs';
|
|
13
|
+
} from 'eslint-rule-forge';
|
|
14
|
+
import * as ts from 'typescript';
|
|
15
|
+
import { TS_DATA_FORGE_MODULE } from './constants.mjs';
|
|
17
16
|
|
|
18
17
|
type Options = readonly [];
|
|
19
18
|
|
|
@@ -63,7 +62,10 @@ export const preferNumSafeParseInt: TSESLint.RuleModule<MessageIds, Options> = {
|
|
|
63
62
|
|
|
64
63
|
const program = sourceCode.ast;
|
|
65
64
|
|
|
66
|
-
const
|
|
65
|
+
const tsDataForgeImports = getImportDeclarations(
|
|
66
|
+
program,
|
|
67
|
+
TS_DATA_FORGE_MODULE,
|
|
68
|
+
);
|
|
67
69
|
|
|
68
70
|
const services = context.sourceCode.parserServices;
|
|
69
71
|
|
|
@@ -147,7 +149,7 @@ export const preferNumSafeParseInt: TSESLint.RuleModule<MessageIds, Options> = {
|
|
|
147
149
|
mut_nodesToFix.push({ node, argExpression: firstArg });
|
|
148
150
|
},
|
|
149
151
|
'Program:exit': () => {
|
|
150
|
-
const namedImports = getNamedImports(
|
|
152
|
+
const namedImports = getNamedImports(tsDataForgeImports);
|
|
151
153
|
|
|
152
154
|
const missingImports = (['Num', 'Result'] as const).filter(
|
|
153
155
|
(name) => !namedImports.includes(name),
|
|
@@ -157,7 +159,7 @@ export const preferNumSafeParseInt: TSESLint.RuleModule<MessageIds, Options> = {
|
|
|
157
159
|
index,
|
|
158
160
|
{ node, argExpression },
|
|
159
161
|
] of mut_nodesToFix.entries()) {
|
|
160
|
-
const argText =
|
|
162
|
+
const argText = getArgumentText(sourceCode, argExpression);
|
|
161
163
|
|
|
162
164
|
const originalText = sourceCode.getText(node);
|
|
163
165
|
|
|
@@ -173,10 +175,10 @@ export const preferNumSafeParseInt: TSESLint.RuleModule<MessageIds, Options> = {
|
|
|
173
175
|
|
|
174
176
|
const importFixes =
|
|
175
177
|
index === 0 && missingImports.length > 0
|
|
176
|
-
?
|
|
178
|
+
? buildNamedImportFix(
|
|
177
179
|
fixer,
|
|
178
180
|
program,
|
|
179
|
-
|
|
181
|
+
TS_DATA_FORGE_MODULE,
|
|
180
182
|
missingImports,
|
|
181
183
|
)
|
|
182
184
|
: [];
|