eslint-plugin-ts-data-forge 0.10.0 → 0.10.2
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 +1 -1
- package/dist/rule-types.d.mts +1 -1
- package/dist/rules/ast-utils.d.mts +0 -16
- package/dist/rules/ast-utils.d.mts.map +1 -1
- package/dist/rules/ast-utils.mjs +0 -36
- package/dist/rules/ast-utils.mjs.map +1 -1
- package/dist/rules/no-side-effect-import.d.mts +1 -8
- package/dist/rules/no-side-effect-import.d.mts.map +1 -1
- package/dist/rules/no-side-effect-import.mjs +7 -43
- package/dist/rules/no-side-effect-import.mjs.map +1 -1
- package/dist/rules/prefer-arr-is-array.d.mts +1 -5
- package/dist/rules/prefer-arr-is-array.d.mts.map +1 -1
- package/dist/rules/prefer-arr-is-array.mjs +5 -83
- package/dist/rules/prefer-arr-is-array.mjs.map +1 -1
- 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-num-safe-parse-float.d.mts +1 -15
- package/dist/rules/prefer-num-safe-parse-float.d.mts.map +1 -1
- package/dist/rules/prefer-num-safe-parse-float.mjs +6 -123
- package/dist/rules/prefer-num-safe-parse-float.mjs.map +1 -1
- package/dist/rules/prefer-num-safe-parse-int.d.mts +1 -13
- package/dist/rules/prefer-num-safe-parse-int.d.mts.map +1 -1
- package/dist/rules/prefer-num-safe-parse-int.mjs +6 -121
- package/dist/rules/prefer-num-safe-parse-int.mjs.map +1 -1
- package/dist/rules/rules.d.mts +1 -1
- package/package.json +6 -6
- package/src/rule-types.mts +1 -1
- package/src/rules/ast-utils.mts +0 -51
- package/src/rules/no-side-effect-import.mts +7 -61
- package/src/rules/prefer-arr-is-array.mts +5 -122
- package/src/rules/prefer-arr-is-array.test.mts +50 -7
- package/src/rules/prefer-is-non-null-object.mts +4 -274
- package/src/rules/prefer-is-record-and-has-key.mts +4 -318
- package/src/rules/prefer-num-safe-parse-float.mts +6 -193
- package/src/rules/prefer-num-safe-parse-float.test.mts +18 -0
- package/src/rules/prefer-num-safe-parse-int.mts +6 -192
- package/src/rules/prefer-num-safe-parse-int.test.mts +18 -0
|
@@ -1,320 +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 { Arr } from 'ts-data-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_DATA_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-data-forge',
|
|
29
|
-
},
|
|
30
|
-
fixable: 'code',
|
|
31
|
-
schema: [],
|
|
32
|
-
messages: {
|
|
33
|
-
useIsRecordAndHasKey:
|
|
34
|
-
'Replace `{{original}}` with `isRecord({{objName}}) && hasKey({{objName}}, {{keyName}})` from ts-data-forge.',
|
|
35
|
-
useHasKey:
|
|
36
|
-
'Replace `{{original}}` with `hasKey({{objName}}, {{keyName}})` from ts-data-forge.',
|
|
37
|
-
},
|
|
38
|
-
},
|
|
39
|
-
|
|
40
|
-
create: (context) => {
|
|
41
|
-
const sourceCode = context.sourceCode;
|
|
42
|
-
|
|
43
|
-
const program = sourceCode.ast;
|
|
44
|
-
|
|
45
|
-
const tsDataForgeImports = getImportDeclarations(
|
|
46
|
-
program,
|
|
47
|
-
TS_DATA_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(tsDataForgeImports);
|
|
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[] = Arr.isEmpty(fixableRewrites)
|
|
178
|
-
? []
|
|
179
|
-
: [
|
|
180
|
-
...(fixableRewrites.some(
|
|
181
|
-
({ needsIsRecord }) => needsIsRecord,
|
|
182
|
-
) && !namedImports.includes('isRecord')
|
|
183
|
-
? ['isRecord']
|
|
184
|
-
: []),
|
|
185
|
-
...(namedImports.includes('hasKey') ? [] : ['hasKey']),
|
|
186
|
-
];
|
|
187
|
-
|
|
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);
|
|
197
|
-
|
|
198
|
-
const originalText = sourceCode.getText(node);
|
|
199
|
-
|
|
200
|
-
context.report({
|
|
201
|
-
node,
|
|
202
|
-
messageId: needsIsRecord ? 'useIsRecordAndHasKey' : 'useHasKey',
|
|
203
|
-
data: {
|
|
204
|
-
original: originalText,
|
|
205
|
-
objName: objText,
|
|
206
|
-
keyName: keyText,
|
|
207
|
-
},
|
|
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
|
-
},
|
|
234
|
-
});
|
|
235
|
-
}
|
|
236
|
-
},
|
|
237
|
-
};
|
|
238
|
-
},
|
|
239
|
-
defaultOptions: [],
|
|
240
|
-
} as const;
|
|
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
|
-
|
|
292
|
-
/**
|
|
293
|
-
* Whether `type` already satisfies `hasKey`'s `R extends UnknownRecord`
|
|
294
|
-
* constraint, so that guarding the call with `isRecord` adds nothing.
|
|
295
|
-
*
|
|
296
|
-
* `UnknownRecord` is `ReadonlyRecord<string, unknown>`, so a string index
|
|
297
|
-
* signature is exactly what it asks for and its value type is `unknown`, which
|
|
298
|
-
* anything satisfies. Callables and arrays are excluded because `isRecord`
|
|
299
|
-
* rejects them at runtime (`typeof fn === 'object'` is false, `Array.isArray`
|
|
300
|
-
* is true), so dropping the guard there would change behavior.
|
|
301
|
-
*
|
|
302
|
-
* Deliberately conservative: a type TypeScript would accept through an
|
|
303
|
-
* *implicit* index signature (an object type alias without one of its own)
|
|
304
|
-
* keeps the guard, because the checker does not report such a signature here.
|
|
305
|
-
*/
|
|
306
|
-
const isAlreadyRecordType = (
|
|
307
|
-
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
308
|
-
checker: ts.TypeChecker,
|
|
309
|
-
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
310
|
-
type: ts.Type,
|
|
311
|
-
): boolean =>
|
|
312
|
-
type.isUnion()
|
|
313
|
-
? type.types.every((member) => isAlreadyRecordType(checker, member))
|
|
314
|
-
: checker.getIndexInfoOfType(type, ts.IndexKind.String) !== undefined &&
|
|
315
|
-
Arr.isEmpty(checker.getSignaturesOfType(type, ts.SignatureKind.Call)) &&
|
|
316
|
-
Arr.isEmpty(
|
|
317
|
-
checker.getSignaturesOfType(type, ts.SignatureKind.Construct),
|
|
318
|
-
) &&
|
|
319
|
-
!checker.isArrayType(type) &&
|
|
320
|
-
!checker.isTupleType(type);
|
|
4
|
+
export const preferIsRecordAndHasKey = createPreferIsRecordAndHasKey({
|
|
5
|
+
moduleName: TS_DATA_FORGE_MODULE,
|
|
6
|
+
});
|
|
@@ -1,195 +1,8 @@
|
|
|
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
|
-
typeWrapperLayers,
|
|
12
|
-
} from 'eslint-rule-forge';
|
|
13
|
-
import * as ts from 'typescript';
|
|
1
|
+
import { createPreferSafeParseFloat } from 'eslint-rule-forge';
|
|
14
2
|
import { TS_DATA_FORGE_MODULE } from './constants.mjs';
|
|
15
3
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
* Returns `true` when every constituent of `type` is string-like, i.e. the
|
|
22
|
-
* type is assignable to the `string` parameter of `Num.safeParseFloat`. A
|
|
23
|
-
* union such as `string | undefined` is rejected so the autofix never
|
|
24
|
-
* produces a type error.
|
|
25
|
-
*/
|
|
26
|
-
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
27
|
-
const isStringType = (type: ts.Type): boolean => {
|
|
28
|
-
const parts = type.isUnion() ? type.types : ([type] as const);
|
|
29
|
-
|
|
30
|
-
return (
|
|
31
|
-
parts.length > 0 &&
|
|
32
|
-
parts.every((t) => (t.flags & ts.TypeFlags.StringLike) !== 0)
|
|
33
|
-
);
|
|
34
|
-
};
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* Detects `parseFloat(x)`, `Number.parseFloat(x)`, and `Number(x)` (when x
|
|
38
|
-
* is typed as `string`) and replaces them with
|
|
39
|
-
* `Result.unwrapOkOr(Num.safeParseFloat(x), Number.NaN)` (ts-data-forge).
|
|
40
|
-
*
|
|
41
|
-
* `Number(x)` is only flagged when x is purely a string type to avoid false
|
|
42
|
-
* positives for `Number(someBoolean)` or `Number(someNumber)` uses. The type is
|
|
43
|
-
* asked of the value under any `as` / `!` / `satisfies` too, so a cast does not
|
|
44
|
-
* vouch for a string that is not one.
|
|
45
|
-
*/
|
|
46
|
-
export const preferNumSafeParseFloat: TSESLint.RuleModule<MessageIds, Options> =
|
|
47
|
-
{
|
|
48
|
-
meta: {
|
|
49
|
-
type: 'suggestion',
|
|
50
|
-
docs: {
|
|
51
|
-
description:
|
|
52
|
-
'Replace `parseFloat(x)`, `Number.parseFloat(x)`, or `Number(x)` (when x is a string) with `Result.unwrapOkOr(Num.safeParseFloat(x), Number.NaN)` from ts-data-forge.',
|
|
53
|
-
},
|
|
54
|
-
fixable: 'code',
|
|
55
|
-
schema: [],
|
|
56
|
-
messages: {
|
|
57
|
-
useSafeParseFloat:
|
|
58
|
-
'Replace `{{original}}` with `Result.unwrapOkOr(Num.safeParseFloat({{argName}}), Number.NaN)` from ts-data-forge.',
|
|
59
|
-
},
|
|
60
|
-
},
|
|
61
|
-
|
|
62
|
-
create: (context) => {
|
|
63
|
-
const sourceCode = context.sourceCode;
|
|
64
|
-
|
|
65
|
-
const program = sourceCode.ast;
|
|
66
|
-
|
|
67
|
-
const tsDataForgeImports = getImportDeclarations(
|
|
68
|
-
program,
|
|
69
|
-
TS_DATA_FORGE_MODULE,
|
|
70
|
-
);
|
|
71
|
-
|
|
72
|
-
const services = context.sourceCode.parserServices;
|
|
73
|
-
|
|
74
|
-
const mut_nodesToFix: {
|
|
75
|
-
node: TSESTree.CallExpression;
|
|
76
|
-
argExpression: TSESTree.Expression;
|
|
77
|
-
}[] = [];
|
|
78
|
-
|
|
79
|
-
return {
|
|
80
|
-
CallExpression: (node) => {
|
|
81
|
-
const { callee } = node;
|
|
82
|
-
|
|
83
|
-
// Match `parseFloat(...)` (global function).
|
|
84
|
-
const isGlobalParseFloat =
|
|
85
|
-
callee.type === AST_NODE_TYPES.Identifier &&
|
|
86
|
-
callee.name === 'parseFloat';
|
|
87
|
-
|
|
88
|
-
// Match `Number.parseFloat(...)`.
|
|
89
|
-
const isNumberParseFloat =
|
|
90
|
-
callee.type === AST_NODE_TYPES.MemberExpression &&
|
|
91
|
-
!callee.computed &&
|
|
92
|
-
callee.object.type === AST_NODE_TYPES.Identifier &&
|
|
93
|
-
callee.object.name === 'Number' &&
|
|
94
|
-
callee.property.type === AST_NODE_TYPES.Identifier &&
|
|
95
|
-
callee.property.name === 'parseFloat';
|
|
96
|
-
|
|
97
|
-
// Match `Number(...)` called as a function (not `new Number(...)`).
|
|
98
|
-
const isNumberCall =
|
|
99
|
-
callee.type === AST_NODE_TYPES.Identifier &&
|
|
100
|
-
callee.name === 'Number';
|
|
101
|
-
|
|
102
|
-
if (!isGlobalParseFloat && !isNumberParseFloat && !isNumberCall) {
|
|
103
|
-
return;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
const args = node.arguments;
|
|
107
|
-
|
|
108
|
-
const firstArg = args[0];
|
|
109
|
-
|
|
110
|
-
if (firstArg === undefined) {
|
|
111
|
-
return;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
// Spread argument (`parseFloat(...rest)`) cannot be rewritten safely.
|
|
115
|
-
if (firstArg.type === AST_NODE_TYPES.SpreadElement) {
|
|
116
|
-
return;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
// The argument must be purely `string` so the autofix is type-safe
|
|
120
|
-
// against `Num.safeParseFloat(s: string)`. Without type information,
|
|
121
|
-
// skip.
|
|
122
|
-
if (services?.program == null) {
|
|
123
|
-
return;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
const checker = services.program.getTypeChecker();
|
|
127
|
-
|
|
128
|
-
// Every layer of type wrappers must be `string` (see
|
|
129
|
-
// typeWrapperLayers): the value underneath is what `Number` sees at
|
|
130
|
-
// run time (`Number(v as string)` may still be handed `true`), and
|
|
131
|
-
// the type a wrapper states is what the fix, which keeps the
|
|
132
|
-
// wrapper, passes on.
|
|
133
|
-
const isString = (
|
|
134
|
-
// AST nodes hold mutable child arrays, so they are not deeply readonly.
|
|
135
|
-
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
136
|
-
expression: TSESTree.Node,
|
|
137
|
-
): boolean => {
|
|
138
|
-
const tsNode = services.esTreeNodeToTSNodeMap?.get(expression);
|
|
139
|
-
|
|
140
|
-
return (
|
|
141
|
-
tsNode !== undefined &&
|
|
142
|
-
isStringType(checker.getTypeAtLocation(tsNode))
|
|
143
|
-
);
|
|
144
|
-
};
|
|
145
|
-
|
|
146
|
-
if (!typeWrapperLayers(firstArg).every(isString)) {
|
|
147
|
-
return;
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
mut_nodesToFix.push({ node, argExpression: firstArg });
|
|
151
|
-
},
|
|
152
|
-
'Program:exit': () => {
|
|
153
|
-
const namedImports = getNamedImports(tsDataForgeImports);
|
|
154
|
-
|
|
155
|
-
const missingImports = (['Num', 'Result'] as const).filter(
|
|
156
|
-
(name) => !namedImports.includes(name),
|
|
157
|
-
);
|
|
158
|
-
|
|
159
|
-
for (const [
|
|
160
|
-
index,
|
|
161
|
-
{ node, argExpression },
|
|
162
|
-
] of mut_nodesToFix.entries()) {
|
|
163
|
-
const argText = getArgumentText(sourceCode, argExpression);
|
|
164
|
-
|
|
165
|
-
const originalText = sourceCode.getText(node);
|
|
166
|
-
|
|
167
|
-
context.report({
|
|
168
|
-
node,
|
|
169
|
-
messageId: 'useSafeParseFloat',
|
|
170
|
-
data: {
|
|
171
|
-
original: originalText,
|
|
172
|
-
argName: argText,
|
|
173
|
-
},
|
|
174
|
-
fix: (fixer) => {
|
|
175
|
-
const core = `Result.unwrapOkOr(Num.safeParseFloat(${argText}), Number.NaN)`;
|
|
176
|
-
|
|
177
|
-
const importFixes =
|
|
178
|
-
index === 0 && missingImports.length > 0
|
|
179
|
-
? buildNamedImportFix(
|
|
180
|
-
fixer,
|
|
181
|
-
program,
|
|
182
|
-
TS_DATA_FORGE_MODULE,
|
|
183
|
-
missingImports,
|
|
184
|
-
)
|
|
185
|
-
: [];
|
|
186
|
-
|
|
187
|
-
return [...importFixes, fixer.replaceText(node, core)];
|
|
188
|
-
},
|
|
189
|
-
});
|
|
190
|
-
}
|
|
191
|
-
},
|
|
192
|
-
};
|
|
193
|
-
},
|
|
194
|
-
defaultOptions: [],
|
|
195
|
-
} as const;
|
|
4
|
+
export const preferNumSafeParseFloat = createPreferSafeParseFloat({
|
|
5
|
+
moduleName: TS_DATA_FORGE_MODULE,
|
|
6
|
+
namespace: 'Num',
|
|
7
|
+
method: 'safeParseFloat',
|
|
8
|
+
});
|
|
@@ -214,6 +214,24 @@ describe('prefer-num-safe-parse-float through type wrappers', () => {
|
|
|
214
214
|
{ messageId: 'useSafeParseFloat' },
|
|
215
215
|
],
|
|
216
216
|
},
|
|
217
|
+
{
|
|
218
|
+
name: 'the callee read through `as` and `!`',
|
|
219
|
+
code: dedent`
|
|
220
|
+
declare const s: string;
|
|
221
|
+
const a = (Number as NumberConstructor)(s);
|
|
222
|
+
const b = Number!.parseFloat(s);
|
|
223
|
+
`,
|
|
224
|
+
output: dedent`
|
|
225
|
+
import { Num, Result } from 'ts-data-forge';
|
|
226
|
+
declare const s: string;
|
|
227
|
+
const a = Result.unwrapOkOr(Num.safeParseFloat(s), Number.NaN);
|
|
228
|
+
const b = Result.unwrapOkOr(Num.safeParseFloat(s), Number.NaN);
|
|
229
|
+
`,
|
|
230
|
+
errors: [
|
|
231
|
+
{ messageId: 'useSafeParseFloat' },
|
|
232
|
+
{ messageId: 'useSafeParseFloat' },
|
|
233
|
+
],
|
|
234
|
+
},
|
|
217
235
|
],
|
|
218
236
|
});
|
|
219
237
|
}, 20000);
|