eslint-plugin-ts-std-forge 0.1.10 → 0.1.12
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/dist/plugin.d.mts +3 -3
- package/dist/rules/prefer-is-non-null-object.d.mts +1 -5
- package/dist/rules/prefer-is-non-null-object.d.mts.map +1 -1
- package/dist/rules/prefer-is-non-null-object.mjs +4 -184
- package/dist/rules/prefer-is-non-null-object.mjs.map +1 -1
- package/dist/rules/prefer-is-record-and-has-key.d.mts +1 -5
- package/dist/rules/prefer-is-record-and-has-key.d.mts.map +1 -1
- package/dist/rules/prefer-is-record-and-has-key.mjs +4 -219
- package/dist/rules/prefer-is-record-and-has-key.mjs.map +1 -1
- package/dist/rules/prefer-safe-array-is-array.d.mts +1 -5
- package/dist/rules/prefer-safe-array-is-array.d.mts.map +1 -1
- package/dist/rules/prefer-safe-array-is-array.mjs +5 -78
- package/dist/rules/prefer-safe-array-is-array.mjs.map +1 -1
- package/dist/rules/prefer-safe-array-length-guard.mjs +1 -2
- package/dist/rules/prefer-safe-array-length-guard.mjs.map +1 -1
- package/dist/rules/prefer-safe-number-parse-integer.d.mts +1 -12
- package/dist/rules/prefer-safe-number-parse-integer.d.mts.map +1 -1
- package/dist/rules/prefer-safe-number-parse-integer.mjs +6 -108
- package/dist/rules/prefer-safe-number-parse-integer.mjs.map +1 -1
- package/dist/rules/prefer-safe-number-parse.d.mts +1 -12
- package/dist/rules/prefer-safe-number-parse.d.mts.map +1 -1
- package/dist/rules/prefer-safe-number-parse.mjs +6 -103
- package/dist/rules/prefer-safe-number-parse.mjs.map +1 -1
- package/dist/rules/rules.d.mts +3 -3
- package/package.json +6 -6
- package/src/rules/prefer-is-non-null-object.mts +4 -274
- package/src/rules/prefer-is-record-and-has-key.mts +4 -322
- package/src/rules/prefer-is-record-and-has-key.test.mts +48 -0
- package/src/rules/prefer-safe-array-is-array.mts +5 -119
- package/src/rules/prefer-safe-array-is-array.test.mts +18 -14
- package/src/rules/prefer-safe-array-length-guard.mts +1 -1
- package/src/rules/prefer-safe-number-parse-integer.mts +6 -172
- package/src/rules/prefer-safe-number-parse-integer.test.mts +22 -8
- package/src/rules/prefer-safe-number-parse.mts +6 -163
- package/src/rules/prefer-safe-number-parse.test.mts +30 -9
- package/dist/rules/ast-utils.d.mts +0 -18
- package/dist/rules/ast-utils.d.mts.map +0 -1
- package/dist/rules/ast-utils.mjs +0 -39
- package/dist/rules/ast-utils.mjs.map +0 -1
- package/src/rules/ast-utils.mts +0 -57
|
@@ -1,324 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
AST_NODE_TYPES,
|
|
3
|
-
type TSESLint,
|
|
4
|
-
type TSESTree,
|
|
5
|
-
} from '@typescript-eslint/utils';
|
|
6
|
-
import {
|
|
7
|
-
buildNamedImportFix,
|
|
8
|
-
getArgumentText,
|
|
9
|
-
getImportDeclarations,
|
|
10
|
-
getNamedImports,
|
|
11
|
-
skipTypeWrappers,
|
|
12
|
-
} from 'eslint-rule-forge';
|
|
13
|
-
import { SafeArray } from 'ts-std-forge';
|
|
14
|
-
import * as ts from 'typescript';
|
|
15
|
-
import { isIdentifierNamed, typeHoldsAtEveryLayer } from './ast-utils.mjs';
|
|
1
|
+
import { createPreferIsRecordAndHasKey } from 'eslint-rule-forge';
|
|
16
2
|
import { TS_STD_FORGE_MODULE } from './constants.mjs';
|
|
17
3
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
export const preferIsRecordAndHasKey: TSESLint.RuleModule<MessageIds, Options> =
|
|
23
|
-
{
|
|
24
|
-
meta: {
|
|
25
|
-
type: 'suggestion',
|
|
26
|
-
docs: {
|
|
27
|
-
description:
|
|
28
|
-
'Replace `Object.hasOwn(obj, key)` or `key in obj` with `isRecord(obj) && hasKey(obj, key)` from ts-std-forge',
|
|
29
|
-
},
|
|
30
|
-
fixable: 'code',
|
|
31
|
-
schema: [],
|
|
32
|
-
messages: {
|
|
33
|
-
useIsRecordAndHasKey:
|
|
34
|
-
'Replace `{{original}}` with `isRecord({{objName}}) && hasKey({{objName}}, {{keyName}})` from ts-std-forge.',
|
|
35
|
-
useHasKey:
|
|
36
|
-
'Replace `{{original}}` with `hasKey({{objName}}, {{keyName}})` from ts-std-forge.',
|
|
37
|
-
},
|
|
38
|
-
},
|
|
39
|
-
|
|
40
|
-
create: (context) => {
|
|
41
|
-
const sourceCode = context.sourceCode;
|
|
42
|
-
|
|
43
|
-
const program = sourceCode.ast;
|
|
44
|
-
|
|
45
|
-
const tsStdForgeImports = getImportDeclarations(
|
|
46
|
-
program,
|
|
47
|
-
TS_STD_FORGE_MODULE,
|
|
48
|
-
);
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* Whether the `isRecord(...)` half of the rewrite would be dead code
|
|
52
|
-
* because the object already satisfies `hasKey`'s
|
|
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.
|
|
58
|
-
*/
|
|
59
|
-
const isKnownRecord = (
|
|
60
|
-
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
61
|
-
expression: TSESTree.Expression,
|
|
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
|
-
);
|
|
84
|
-
|
|
85
|
-
const mut_nodesToFix: {
|
|
86
|
-
node: TSESTree.CallExpression | TSESTree.BinaryExpression;
|
|
87
|
-
objExpression: TSESTree.Expression;
|
|
88
|
-
keyExpression: TSESTree.Expression;
|
|
89
|
-
}[] = [];
|
|
90
|
-
|
|
91
|
-
return {
|
|
92
|
-
// Handle Object.hasOwn(obj, key)
|
|
93
|
-
CallExpression: (node) => {
|
|
94
|
-
const callee = skipTypeWrappers(node.callee);
|
|
95
|
-
|
|
96
|
-
if (
|
|
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'
|
|
101
|
-
) {
|
|
102
|
-
return;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
// Check arguments: Object.hasOwn(obj, key)
|
|
106
|
-
if (node.arguments.length !== 2) {
|
|
107
|
-
return;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
const objArg = node.arguments[0];
|
|
111
|
-
|
|
112
|
-
const keyArg = node.arguments[1];
|
|
113
|
-
|
|
114
|
-
if (objArg === undefined || keyArg === undefined) {
|
|
115
|
-
return;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
// Type guard: ensure arguments are Expressions (not SpreadElement)
|
|
119
|
-
if (
|
|
120
|
-
objArg.type === AST_NODE_TYPES.SpreadElement ||
|
|
121
|
-
keyArg.type === AST_NODE_TYPES.SpreadElement ||
|
|
122
|
-
mayBeArray(objArg)
|
|
123
|
-
) {
|
|
124
|
-
return;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
mut_nodesToFix.push({
|
|
128
|
-
node,
|
|
129
|
-
objExpression: objArg,
|
|
130
|
-
keyExpression: keyArg,
|
|
131
|
-
});
|
|
132
|
-
},
|
|
133
|
-
|
|
134
|
-
// Handle key in obj
|
|
135
|
-
BinaryExpression: (node) => {
|
|
136
|
-
if (node.operator !== 'in') {
|
|
137
|
-
return;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
const keyExpression = node.left;
|
|
141
|
-
|
|
142
|
-
const objExpression = node.right;
|
|
143
|
-
|
|
144
|
-
// Type guard: ensure keyExpression is Expression (not PrivateIdentifier)
|
|
145
|
-
if (
|
|
146
|
-
keyExpression.type === AST_NODE_TYPES.PrivateIdentifier ||
|
|
147
|
-
mayBeArray(objExpression)
|
|
148
|
-
) {
|
|
149
|
-
return;
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
mut_nodesToFix.push({
|
|
153
|
-
node,
|
|
154
|
-
objExpression,
|
|
155
|
-
keyExpression,
|
|
156
|
-
});
|
|
157
|
-
},
|
|
158
|
-
|
|
159
|
-
'Program:exit': () => {
|
|
160
|
-
const namedImports = getNamedImports(tsStdForgeImports);
|
|
161
|
-
|
|
162
|
-
const rewrites = mut_nodesToFix.map((entry) => {
|
|
163
|
-
// A record-typed object makes the `isRecord(...)` conjunct always
|
|
164
|
-
// true, so it is left out of the replacement entirely.
|
|
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
|
-
});
|
|
174
|
-
|
|
175
|
-
const fixableRewrites = rewrites.filter(({ fixable }) => fixable);
|
|
176
|
-
|
|
177
|
-
const importsToAdd: readonly string[] = SafeArray.isEmpty(
|
|
178
|
-
fixableRewrites,
|
|
179
|
-
)
|
|
180
|
-
? []
|
|
181
|
-
: [
|
|
182
|
-
...(fixableRewrites.some(
|
|
183
|
-
({ needsIsRecord }) => needsIsRecord,
|
|
184
|
-
) && !namedImports.includes('isRecord')
|
|
185
|
-
? ['isRecord']
|
|
186
|
-
: []),
|
|
187
|
-
...(namedImports.includes('hasKey') ? [] : ['hasKey']),
|
|
188
|
-
];
|
|
189
|
-
|
|
190
|
-
const importCarrier = fixableRewrites[0];
|
|
191
|
-
|
|
192
|
-
for (const rewrite of rewrites) {
|
|
193
|
-
const { node, objExpression, keyExpression, needsIsRecord } =
|
|
194
|
-
rewrite;
|
|
195
|
-
|
|
196
|
-
const objText = getArgumentText(sourceCode, objExpression);
|
|
197
|
-
|
|
198
|
-
const keyText = getArgumentText(sourceCode, keyExpression);
|
|
199
|
-
|
|
200
|
-
const originalText = sourceCode.getText(node);
|
|
201
|
-
|
|
202
|
-
context.report({
|
|
203
|
-
node,
|
|
204
|
-
messageId: needsIsRecord ? 'useIsRecordAndHasKey' : 'useHasKey',
|
|
205
|
-
data: {
|
|
206
|
-
original: originalText,
|
|
207
|
-
objName: objText,
|
|
208
|
-
keyName: keyText,
|
|
209
|
-
},
|
|
210
|
-
fix: !rewrite.fixable
|
|
211
|
-
? undefined
|
|
212
|
-
: (fixer) => {
|
|
213
|
-
const replacement = needsIsRecord
|
|
214
|
-
? `(isRecord(${objText}) && hasKey(${objText}, ${keyText}))`
|
|
215
|
-
: `hasKey(${objText}, ${keyText})`;
|
|
216
|
-
|
|
217
|
-
// All import fixes have to live on the first fixable
|
|
218
|
-
// report: `insertTextBefore(program, …)` produces
|
|
219
|
-
// overlapping ranges otherwise and only one of them would
|
|
220
|
-
// survive a pass.
|
|
221
|
-
const importFixes =
|
|
222
|
-
rewrite === importCarrier && importsToAdd.length > 0
|
|
223
|
-
? buildNamedImportFix(
|
|
224
|
-
fixer,
|
|
225
|
-
program,
|
|
226
|
-
TS_STD_FORGE_MODULE,
|
|
227
|
-
importsToAdd,
|
|
228
|
-
)
|
|
229
|
-
: [];
|
|
230
|
-
|
|
231
|
-
return [
|
|
232
|
-
...importFixes,
|
|
233
|
-
fixer.replaceText(node, replacement),
|
|
234
|
-
];
|
|
235
|
-
},
|
|
236
|
-
});
|
|
237
|
-
}
|
|
238
|
-
},
|
|
239
|
-
};
|
|
240
|
-
},
|
|
241
|
-
defaultOptions: [],
|
|
242
|
-
} as const;
|
|
243
|
-
|
|
244
|
-
/**
|
|
245
|
-
* Whether reading `node` again has no side effects: a variable, `this`, or a
|
|
246
|
-
* non-computed property chain on one, type wrappers aside.
|
|
247
|
-
*/
|
|
248
|
-
const isSideEffectFree = (
|
|
249
|
-
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
250
|
-
wrapped: TSESTree.Node,
|
|
251
|
-
): boolean => {
|
|
252
|
-
const node = skipTypeWrappers(wrapped);
|
|
253
|
-
|
|
254
|
-
return (
|
|
255
|
-
node.type === AST_NODE_TYPES.Identifier ||
|
|
256
|
-
node.type === AST_NODE_TYPES.ThisExpression ||
|
|
257
|
-
(node.type === AST_NODE_TYPES.MemberExpression &&
|
|
258
|
-
!node.computed &&
|
|
259
|
-
isSideEffectFree(node.object))
|
|
260
|
-
);
|
|
261
|
-
};
|
|
262
|
-
|
|
263
|
-
/**
|
|
264
|
-
* Whether a value of `type` may be an array or a tuple: some member of a union
|
|
265
|
-
* or an intersection is one, or the constraint of a deferred type is.
|
|
266
|
-
* `object` and `unknown` say nothing either way and are not counted.
|
|
267
|
-
*/
|
|
268
|
-
const mayBeArrayType = (
|
|
269
|
-
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
270
|
-
checker: ts.TypeChecker,
|
|
271
|
-
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
272
|
-
type: ts.Type,
|
|
273
|
-
): boolean => {
|
|
274
|
-
if (type.isUnionOrIntersection()) {
|
|
275
|
-
return type.types.some((member) => mayBeArrayType(checker, member));
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
if (
|
|
279
|
-
(type.flags &
|
|
280
|
-
(ts.TypeFlags.TypeParameter |
|
|
281
|
-
ts.TypeFlags.IndexedAccess |
|
|
282
|
-
ts.TypeFlags.Conditional |
|
|
283
|
-
ts.TypeFlags.Substitution)) !==
|
|
284
|
-
0
|
|
285
|
-
) {
|
|
286
|
-
const constraint = checker.getBaseConstraintOfType(type);
|
|
287
|
-
|
|
288
|
-
return constraint !== undefined && mayBeArrayType(checker, constraint);
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
return checker.isArrayType(type) || checker.isTupleType(type);
|
|
292
|
-
};
|
|
293
|
-
|
|
294
|
-
/**
|
|
295
|
-
* Whether `type` already satisfies `hasKey`'s `R extends UnknownRecord`
|
|
296
|
-
* constraint, so that guarding the call with `isRecord` adds nothing.
|
|
297
|
-
*
|
|
298
|
-
* `UnknownRecord` is `ReadonlyRecord<string, unknown>`, so a string index
|
|
299
|
-
* signature is exactly what it asks for and its value type is `unknown`, which
|
|
300
|
-
* anything satisfies. Callables and arrays are excluded because `isRecord`
|
|
301
|
-
* rejects them at runtime (`typeof fn === 'object'` is false, `Array.isArray`
|
|
302
|
-
* is true), so dropping the guard there would change behavior.
|
|
303
|
-
*
|
|
304
|
-
* Deliberately conservative: a type TypeScript would accept through an
|
|
305
|
-
* *implicit* index signature (an object type alias without one of its own)
|
|
306
|
-
* keeps the guard, because the checker does not report such a signature here.
|
|
307
|
-
*/
|
|
308
|
-
const isAlreadyRecordType = (
|
|
309
|
-
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
310
|
-
checker: ts.TypeChecker,
|
|
311
|
-
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
312
|
-
type: ts.Type,
|
|
313
|
-
): boolean =>
|
|
314
|
-
type.isUnion()
|
|
315
|
-
? type.types.every((member) => isAlreadyRecordType(checker, member))
|
|
316
|
-
: checker.getIndexInfoOfType(type, ts.IndexKind.String) !== undefined &&
|
|
317
|
-
SafeArray.isEmpty(
|
|
318
|
-
checker.getSignaturesOfType(type, ts.SignatureKind.Call),
|
|
319
|
-
) &&
|
|
320
|
-
SafeArray.isEmpty(
|
|
321
|
-
checker.getSignaturesOfType(type, ts.SignatureKind.Construct),
|
|
322
|
-
) &&
|
|
323
|
-
!checker.isArrayType(type) &&
|
|
324
|
-
!checker.isTupleType(type);
|
|
4
|
+
export const preferIsRecordAndHasKey = createPreferIsRecordAndHasKey({
|
|
5
|
+
moduleName: TS_STD_FORGE_MODULE,
|
|
6
|
+
});
|
|
@@ -62,6 +62,54 @@ describe('prefer-is-record-and-has-key', () => {
|
|
|
62
62
|
`,
|
|
63
63
|
errors: [{ messageId: 'useHasKey' }],
|
|
64
64
|
},
|
|
65
|
+
{
|
|
66
|
+
name: 'keeps isRecord for an index-signature-carrying callable',
|
|
67
|
+
code: dedent`
|
|
68
|
+
type Callable = { (): void; [key: string]: unknown };
|
|
69
|
+
declare const obj: Callable;
|
|
70
|
+
const ok = Object.hasOwn(obj, 'a');
|
|
71
|
+
`,
|
|
72
|
+
output: dedent`
|
|
73
|
+
import { isRecord, hasKey } from 'ts-std-forge';
|
|
74
|
+
type Callable = { (): void; [key: string]: unknown };
|
|
75
|
+
declare const obj: Callable;
|
|
76
|
+
const ok = (isRecord(obj) && hasKey(obj, 'a'));
|
|
77
|
+
`,
|
|
78
|
+
errors: [{ messageId: 'useIsRecordAndHasKey' }],
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
name: 'imports isRecord once when both forms appear in a file',
|
|
82
|
+
code: dedent`
|
|
83
|
+
declare const rec: Record<string, unknown>;
|
|
84
|
+
declare const unk: unknown;
|
|
85
|
+
const a = Object.hasOwn(rec, 'a');
|
|
86
|
+
const b = Object.hasOwn(unk, 'b');
|
|
87
|
+
`,
|
|
88
|
+
output: dedent`
|
|
89
|
+
import { isRecord, hasKey } from 'ts-std-forge';
|
|
90
|
+
declare const rec: Record<string, unknown>;
|
|
91
|
+
declare const unk: unknown;
|
|
92
|
+
const a = hasKey(rec, 'a');
|
|
93
|
+
const b = (isRecord(unk) && hasKey(unk, 'b'));
|
|
94
|
+
`,
|
|
95
|
+
errors: [
|
|
96
|
+
{ messageId: 'useHasKey' },
|
|
97
|
+
{ messageId: 'useIsRecordAndHasKey' },
|
|
98
|
+
],
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
name: 'wraps a negated Object.hasOwn in parentheses',
|
|
102
|
+
code: dedent`
|
|
103
|
+
const obj = { a: 1 };
|
|
104
|
+
const ok = !Object.hasOwn(obj, 'a');
|
|
105
|
+
`,
|
|
106
|
+
output: dedent`
|
|
107
|
+
import { isRecord, hasKey } from 'ts-std-forge';
|
|
108
|
+
const obj = { a: 1 };
|
|
109
|
+
const ok = !(isRecord(obj) && hasKey(obj, 'a'));
|
|
110
|
+
`,
|
|
111
|
+
errors: [{ messageId: 'useIsRecordAndHasKey' }],
|
|
112
|
+
},
|
|
65
113
|
{
|
|
66
114
|
name: 'rewrites `key in obj` too',
|
|
67
115
|
code: dedent`
|
|
@@ -1,121 +1,7 @@
|
|
|
1
|
-
import {
|
|
2
|
-
AST_NODE_TYPES,
|
|
3
|
-
type TSESLint,
|
|
4
|
-
type TSESTree,
|
|
5
|
-
} from '@typescript-eslint/utils';
|
|
6
|
-
import {
|
|
7
|
-
buildNamedImportFix,
|
|
8
|
-
getImportDeclarations,
|
|
9
|
-
getNamedImports,
|
|
10
|
-
getRangeWithParens,
|
|
11
|
-
skipTypeWrappers,
|
|
12
|
-
} from 'eslint-rule-forge';
|
|
13
|
-
import { isIdentifierNamed } from './ast-utils.mjs';
|
|
1
|
+
import { createPreferArrayIsArray } from 'eslint-rule-forge';
|
|
14
2
|
import { TS_STD_FORGE_MODULE } from './constants.mjs';
|
|
15
3
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
export const preferSafeArrayIsArray: TSESLint.RuleModule<MessageIds, Options> =
|
|
21
|
-
{
|
|
22
|
-
meta: {
|
|
23
|
-
type: 'suggestion',
|
|
24
|
-
docs: {
|
|
25
|
-
description:
|
|
26
|
-
'Replace `Array.isArray` with `SafeArray.isArray` from ts-std-forge.',
|
|
27
|
-
},
|
|
28
|
-
fixable: 'code',
|
|
29
|
-
schema: [],
|
|
30
|
-
messages: {
|
|
31
|
-
useSafeArrayIsArray:
|
|
32
|
-
'Replace `Array.isArray` with `SafeArray.isArray` from ts-std-forge.',
|
|
33
|
-
},
|
|
34
|
-
},
|
|
35
|
-
|
|
36
|
-
create: (context) => {
|
|
37
|
-
const sourceCode = context.sourceCode;
|
|
38
|
-
|
|
39
|
-
const program = sourceCode.ast;
|
|
40
|
-
|
|
41
|
-
const tsStdForgeImports = getImportDeclarations(
|
|
42
|
-
program,
|
|
43
|
-
TS_STD_FORGE_MODULE,
|
|
44
|
-
);
|
|
45
|
-
|
|
46
|
-
const mut_nodesToFix: TSESTree.CallExpression[] = [];
|
|
47
|
-
|
|
48
|
-
return {
|
|
49
|
-
CallExpression: (node) => {
|
|
50
|
-
if (!isArrayIsArrayCall(node)) {
|
|
51
|
-
return;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
mut_nodesToFix.push(node);
|
|
55
|
-
},
|
|
56
|
-
'Program:exit': () => {
|
|
57
|
-
const hasSafeArrayImport =
|
|
58
|
-
getNamedImports(tsStdForgeImports).includes('SafeArray');
|
|
59
|
-
|
|
60
|
-
// The import fix lives on the first report only:
|
|
61
|
-
// `insertTextBefore(program, …)` produces overlapping ranges
|
|
62
|
-
// otherwise and only one of them would survive a pass. A first
|
|
63
|
-
// report silenced by an `eslint-disable` comment therefore takes
|
|
64
|
-
// the import with it.
|
|
65
|
-
for (const [index, node] of mut_nodesToFix.entries()) {
|
|
66
|
-
context.report({
|
|
67
|
-
node,
|
|
68
|
-
messageId: 'useSafeArrayIsArray',
|
|
69
|
-
fix: (fixer) => {
|
|
70
|
-
const importFixes =
|
|
71
|
-
index === 0 && !hasSafeArrayImport
|
|
72
|
-
? buildNamedImportFix(fixer, program, TS_STD_FORGE_MODULE, [
|
|
73
|
-
'SafeArray',
|
|
74
|
-
])
|
|
75
|
-
: [];
|
|
76
|
-
|
|
77
|
-
// Only the callee is replaced, type wrappers included, along
|
|
78
|
-
// with the parentheses around it, which its range leaves out;
|
|
79
|
-
// the arguments stay as written.
|
|
80
|
-
return [
|
|
81
|
-
...importFixes,
|
|
82
|
-
fixer.replaceTextRange(
|
|
83
|
-
getRangeWithParens(sourceCode, node.callee),
|
|
84
|
-
'SafeArray.isArray',
|
|
85
|
-
),
|
|
86
|
-
];
|
|
87
|
-
},
|
|
88
|
-
});
|
|
89
|
-
}
|
|
90
|
-
},
|
|
91
|
-
};
|
|
92
|
-
},
|
|
93
|
-
defaultOptions: [],
|
|
94
|
-
} as const;
|
|
95
|
-
|
|
96
|
-
const isArrayIsArrayCall = (
|
|
97
|
-
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
98
|
-
node: TSESTree.CallExpression,
|
|
99
|
-
): boolean => {
|
|
100
|
-
const callee = skipTypeWrappers(node.callee);
|
|
101
|
-
|
|
102
|
-
if (callee.type !== AST_NODE_TYPES.MemberExpression) {
|
|
103
|
-
return false;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
const { object, property } = callee;
|
|
107
|
-
|
|
108
|
-
if (!isIdentifierNamed(object, 'Array')) {
|
|
109
|
-
return false;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
if (property.type !== AST_NODE_TYPES.Identifier) {
|
|
113
|
-
return false;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
if (property.name !== 'isArray') {
|
|
117
|
-
return false;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
return true;
|
|
121
|
-
};
|
|
4
|
+
export const preferSafeArrayIsArray = createPreferArrayIsArray({
|
|
5
|
+
moduleName: TS_STD_FORGE_MODULE,
|
|
6
|
+
namespace: 'SafeArray',
|
|
7
|
+
});
|
|
@@ -20,6 +20,13 @@ ruleTester.run('prefer-safe-array-is-array', preferSafeArrayIsArray, {
|
|
|
20
20
|
const result = obj.isArray(value);
|
|
21
21
|
`,
|
|
22
22
|
},
|
|
23
|
+
{
|
|
24
|
+
name: 'A computed member named by a variable is not `isArray`',
|
|
25
|
+
code: dedent`
|
|
26
|
+
declare const isArray: 'from';
|
|
27
|
+
const result = Array[isArray](value);
|
|
28
|
+
`,
|
|
29
|
+
},
|
|
23
30
|
],
|
|
24
31
|
invalid: [
|
|
25
32
|
{
|
|
@@ -31,7 +38,7 @@ ruleTester.run('prefer-safe-array-is-array', preferSafeArrayIsArray, {
|
|
|
31
38
|
import { SafeArray } from 'ts-std-forge';
|
|
32
39
|
const result = SafeArray.isArray(value);
|
|
33
40
|
`,
|
|
34
|
-
errors: [{ messageId: '
|
|
41
|
+
errors: [{ messageId: 'useIsArray' }],
|
|
35
42
|
},
|
|
36
43
|
{
|
|
37
44
|
name: 'Replace Array.isArray when SafeArray is already imported',
|
|
@@ -43,7 +50,7 @@ ruleTester.run('prefer-safe-array-is-array', preferSafeArrayIsArray, {
|
|
|
43
50
|
import { SafeArray } from 'ts-std-forge';
|
|
44
51
|
const result = SafeArray.isArray(value);
|
|
45
52
|
`,
|
|
46
|
-
errors: [{ messageId: '
|
|
53
|
+
errors: [{ messageId: 'useIsArray' }],
|
|
47
54
|
},
|
|
48
55
|
{
|
|
49
56
|
name: 'Sees SafeArray imported by a second ts-std-forge declaration',
|
|
@@ -57,7 +64,7 @@ ruleTester.run('prefer-safe-array-is-array', preferSafeArrayIsArray, {
|
|
|
57
64
|
import { SafeArray } from 'ts-std-forge';
|
|
58
65
|
const result = SafeArray.isArray(value);
|
|
59
66
|
`,
|
|
60
|
-
errors: [{ messageId: '
|
|
67
|
+
errors: [{ messageId: 'useIsArray' }],
|
|
61
68
|
},
|
|
62
69
|
{
|
|
63
70
|
name: 'Replace multiple Array.isArray calls, importing once',
|
|
@@ -70,10 +77,7 @@ ruleTester.run('prefer-safe-array-is-array', preferSafeArrayIsArray, {
|
|
|
70
77
|
const result1 = SafeArray.isArray(value1);
|
|
71
78
|
const result2 = SafeArray.isArray(value2);
|
|
72
79
|
`,
|
|
73
|
-
errors: [
|
|
74
|
-
{ messageId: 'useSafeArrayIsArray' },
|
|
75
|
-
{ messageId: 'useSafeArrayIsArray' },
|
|
76
|
-
],
|
|
80
|
+
errors: [{ messageId: 'useIsArray' }, { messageId: 'useIsArray' }],
|
|
77
81
|
},
|
|
78
82
|
],
|
|
79
83
|
});
|
|
@@ -91,7 +95,7 @@ describe('prefer-safe-array-is-array through type wrappers', () => {
|
|
|
91
95
|
import { SafeArray } from 'ts-std-forge';
|
|
92
96
|
const result = SafeArray.isArray(value);
|
|
93
97
|
`,
|
|
94
|
-
errors: [{ messageId: '
|
|
98
|
+
errors: [{ messageId: 'useIsArray' }],
|
|
95
99
|
},
|
|
96
100
|
{
|
|
97
101
|
name: 'reads `Array` through `!`',
|
|
@@ -102,7 +106,7 @@ describe('prefer-safe-array-is-array through type wrappers', () => {
|
|
|
102
106
|
import { SafeArray } from 'ts-std-forge';
|
|
103
107
|
const result = SafeArray.isArray(value);
|
|
104
108
|
`,
|
|
105
|
-
errors: [{ messageId: '
|
|
109
|
+
errors: [{ messageId: 'useIsArray' }],
|
|
106
110
|
},
|
|
107
111
|
{
|
|
108
112
|
name: 'reads the callee through `satisfies`',
|
|
@@ -113,7 +117,7 @@ describe('prefer-safe-array-is-array through type wrappers', () => {
|
|
|
113
117
|
import { SafeArray } from 'ts-std-forge';
|
|
114
118
|
const result = SafeArray.isArray(value);
|
|
115
119
|
`,
|
|
116
|
-
errors: [{ messageId: '
|
|
120
|
+
errors: [{ messageId: 'useIsArray' }],
|
|
117
121
|
},
|
|
118
122
|
{
|
|
119
123
|
name: 'keeps a wrapper on the argument',
|
|
@@ -124,7 +128,7 @@ describe('prefer-safe-array-is-array through type wrappers', () => {
|
|
|
124
128
|
import { SafeArray } from 'ts-std-forge';
|
|
125
129
|
const result = SafeArray.isArray(value satisfies unknown);
|
|
126
130
|
`,
|
|
127
|
-
errors: [{ messageId: '
|
|
131
|
+
errors: [{ messageId: 'useIsArray' }],
|
|
128
132
|
},
|
|
129
133
|
],
|
|
130
134
|
});
|
|
@@ -143,7 +147,7 @@ describe('prefer-safe-array-is-array with parenthesized operands', () => {
|
|
|
143
147
|
import { SafeArray } from 'ts-std-forge';
|
|
144
148
|
const result = SafeArray.isArray(x);
|
|
145
149
|
`,
|
|
146
|
-
errors: [{ messageId: '
|
|
150
|
+
errors: [{ messageId: 'useIsArray' }],
|
|
147
151
|
},
|
|
148
152
|
{
|
|
149
153
|
name: 'replaces a doubly parenthesized callee of an optional call',
|
|
@@ -154,7 +158,7 @@ describe('prefer-safe-array-is-array with parenthesized operands', () => {
|
|
|
154
158
|
import { SafeArray } from 'ts-std-forge';
|
|
155
159
|
const result = SafeArray.isArray?.(x);
|
|
156
160
|
`,
|
|
157
|
-
errors: [{ messageId: '
|
|
161
|
+
errors: [{ messageId: 'useIsArray' }],
|
|
158
162
|
},
|
|
159
163
|
{
|
|
160
164
|
name: 'keeps a parenthesized argument',
|
|
@@ -165,7 +169,7 @@ describe('prefer-safe-array-is-array with parenthesized operands', () => {
|
|
|
165
169
|
import { SafeArray } from 'ts-std-forge';
|
|
166
170
|
const result = SafeArray.isArray((a, x));
|
|
167
171
|
`,
|
|
168
|
-
errors: [{ messageId: '
|
|
172
|
+
errors: [{ messageId: 'useIsArray' }],
|
|
169
173
|
},
|
|
170
174
|
],
|
|
171
175
|
});
|
|
@@ -9,9 +9,9 @@ import {
|
|
|
9
9
|
getImportDeclarations,
|
|
10
10
|
getNamedImports,
|
|
11
11
|
skipTypeWrappers,
|
|
12
|
+
typeHoldsAtEveryLayer,
|
|
12
13
|
} from 'eslint-rule-forge';
|
|
13
14
|
import type { ReadonlyRecord } from 'ts-type-forge';
|
|
14
|
-
import { typeHoldsAtEveryLayer } from './ast-utils.mjs';
|
|
15
15
|
import { TS_STD_FORGE_MODULE } from './constants.mjs';
|
|
16
16
|
|
|
17
17
|
type Options = readonly [];
|