eslint-plugin-ts-std-forge 0.1.11 → 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 +5 -5
- 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,174 +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
|
-
skipTypeWrappers,
|
|
12
|
-
} from 'eslint-rule-forge';
|
|
13
|
-
import * as ts from 'typescript';
|
|
14
|
-
import { isIdentifierNamed, typeHoldsAtEveryLayer } from './ast-utils.mjs';
|
|
1
|
+
import { createPreferSafeParseInt } from 'eslint-rule-forge';
|
|
15
2
|
import { TS_STD_FORGE_MODULE } from './constants.mjs';
|
|
16
3
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
* Returns `true` when every constituent of `type` is string-like, i.e. the
|
|
23
|
-
* type is assignable to the `string` parameter of `SafeNumber.parseInteger`.
|
|
24
|
-
* A union such as `string | undefined` is rejected so the autofix never
|
|
25
|
-
* produces a type error.
|
|
26
|
-
*/
|
|
27
|
-
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
28
|
-
const isStringType = (type: ts.Type): boolean => {
|
|
29
|
-
const parts = type.isUnion() ? type.types : ([type] as const);
|
|
30
|
-
|
|
31
|
-
return (
|
|
32
|
-
parts.length > 0 &&
|
|
33
|
-
parts.every((t) => (t.flags & ts.TypeFlags.StringLike) !== 0)
|
|
34
|
-
);
|
|
35
|
-
};
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* Detects `parseInt(x)` / `Number.parseInt(x, 10)` and replaces them with
|
|
39
|
-
* `Result.unwrapOkOr(SafeNumber.parseInteger(x), Number.NaN)` (ts-std-forge).
|
|
40
|
-
*
|
|
41
|
-
* Only base 10 is rewritten: `SafeNumber.parseInteger` takes no radix, so a
|
|
42
|
-
* call that passes another one means something the replacement cannot say.
|
|
43
|
-
*/
|
|
44
|
-
export const preferSafeNumberParseInteger: TSESLint.RuleModule<
|
|
45
|
-
MessageIds,
|
|
46
|
-
Options
|
|
47
|
-
> = {
|
|
48
|
-
meta: {
|
|
49
|
-
type: 'suggestion',
|
|
50
|
-
docs: {
|
|
51
|
-
description:
|
|
52
|
-
'Replace `parseInt(x)` or `Number.parseInt(x, 10)` with `Result.unwrapOkOr(SafeNumber.parseInteger(x), Number.NaN)` from ts-std-forge.',
|
|
53
|
-
},
|
|
54
|
-
fixable: 'code',
|
|
55
|
-
schema: [],
|
|
56
|
-
messages: {
|
|
57
|
-
useSafeNumberParseInteger:
|
|
58
|
-
'Replace `{{original}}` with `Result.unwrapOkOr(SafeNumber.parseInteger({{argName}}), Number.NaN)` from ts-std-forge.',
|
|
59
|
-
},
|
|
60
|
-
},
|
|
61
|
-
|
|
62
|
-
create: (context) => {
|
|
63
|
-
const sourceCode = context.sourceCode;
|
|
64
|
-
|
|
65
|
-
const program = sourceCode.ast;
|
|
66
|
-
|
|
67
|
-
const tsStdForgeImports = getImportDeclarations(
|
|
68
|
-
program,
|
|
69
|
-
TS_STD_FORGE_MODULE,
|
|
70
|
-
);
|
|
71
|
-
|
|
72
|
-
const mut_nodesToFix: {
|
|
73
|
-
node: TSESTree.CallExpression;
|
|
74
|
-
argExpression: TSESTree.Expression;
|
|
75
|
-
}[] = [];
|
|
76
|
-
|
|
77
|
-
return {
|
|
78
|
-
CallExpression: (node) => {
|
|
79
|
-
const callee = skipTypeWrappers(node.callee);
|
|
80
|
-
|
|
81
|
-
// Match `parseInt(...)` or `Number.parseInt(...)`.
|
|
82
|
-
const isGlobalParseInt =
|
|
83
|
-
callee.type === AST_NODE_TYPES.Identifier &&
|
|
84
|
-
callee.name === 'parseInt';
|
|
85
|
-
|
|
86
|
-
const isNumberParseInt =
|
|
87
|
-
callee.type === AST_NODE_TYPES.MemberExpression &&
|
|
88
|
-
!callee.computed &&
|
|
89
|
-
isIdentifierNamed(callee.object, 'Number') &&
|
|
90
|
-
callee.property.type === AST_NODE_TYPES.Identifier &&
|
|
91
|
-
callee.property.name === 'parseInt';
|
|
92
|
-
|
|
93
|
-
if (!isGlobalParseInt && !isNumberParseInt) {
|
|
94
|
-
return;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
const args = node.arguments;
|
|
98
|
-
|
|
99
|
-
const firstArg = args[0];
|
|
100
|
-
|
|
101
|
-
if (firstArg === undefined) {
|
|
102
|
-
return;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
// Spread argument (`parseInt(...rest)`) cannot be rewritten safely.
|
|
106
|
-
if (firstArg.type === AST_NODE_TYPES.SpreadElement) {
|
|
107
|
-
return;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
// Only base 10: accept a missing radix or an explicit literal `10`,
|
|
111
|
-
// read through its type wrappers, which leave the value as it is.
|
|
112
|
-
if (args.length >= 2) {
|
|
113
|
-
const radix =
|
|
114
|
-
args[1] === undefined ? undefined : skipTypeWrappers(args[1]);
|
|
115
|
-
|
|
116
|
-
if (radix?.type !== AST_NODE_TYPES.Literal || radix.value !== 10) {
|
|
117
|
-
return;
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
// The argument must be a `string` at every layer of type wrappers:
|
|
122
|
-
// the value's own type is what `parseInt` sees at run time
|
|
123
|
-
// (`parseInt(1e21 as unknown as string)` is 1, the rewrite 1e21), and
|
|
124
|
-
// the type a wrapper states is what the fix, which keeps the wrapper,
|
|
125
|
-
// hands `SafeNumber.parseInteger(value: string)`. Without type
|
|
126
|
-
// information, skip.
|
|
127
|
-
if (!typeHoldsAtEveryLayer(sourceCode, firstArg, isStringType)) {
|
|
128
|
-
return;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
mut_nodesToFix.push({ node, argExpression: firstArg });
|
|
132
|
-
},
|
|
133
|
-
'Program:exit': () => {
|
|
134
|
-
const namedImports = getNamedImports(tsStdForgeImports);
|
|
135
|
-
|
|
136
|
-
const missingImports = (['Result', 'SafeNumber'] as const).filter(
|
|
137
|
-
(name) => !namedImports.includes(name),
|
|
138
|
-
);
|
|
139
|
-
|
|
140
|
-
for (const [
|
|
141
|
-
index,
|
|
142
|
-
{ node, argExpression },
|
|
143
|
-
] of mut_nodesToFix.entries()) {
|
|
144
|
-
const argText = getArgumentText(sourceCode, argExpression);
|
|
145
|
-
|
|
146
|
-
context.report({
|
|
147
|
-
node,
|
|
148
|
-
messageId: 'useSafeNumberParseInteger',
|
|
149
|
-
data: {
|
|
150
|
-
original: sourceCode.getText(node),
|
|
151
|
-
argName: argText,
|
|
152
|
-
},
|
|
153
|
-
fix: (fixer) => {
|
|
154
|
-
const core = `Result.unwrapOkOr(SafeNumber.parseInteger(${argText}), Number.NaN)`;
|
|
155
|
-
|
|
156
|
-
const importFixes =
|
|
157
|
-
index === 0 && missingImports.length > 0
|
|
158
|
-
? buildNamedImportFix(
|
|
159
|
-
fixer,
|
|
160
|
-
program,
|
|
161
|
-
TS_STD_FORGE_MODULE,
|
|
162
|
-
missingImports,
|
|
163
|
-
)
|
|
164
|
-
: [];
|
|
165
|
-
|
|
166
|
-
return [...importFixes, fixer.replaceText(node, core)];
|
|
167
|
-
},
|
|
168
|
-
});
|
|
169
|
-
}
|
|
170
|
-
},
|
|
171
|
-
};
|
|
172
|
-
},
|
|
173
|
-
defaultOptions: [],
|
|
174
|
-
} as const;
|
|
4
|
+
export const preferSafeNumberParseInteger = createPreferSafeParseInt({
|
|
5
|
+
moduleName: TS_STD_FORGE_MODULE,
|
|
6
|
+
namespace: 'SafeNumber',
|
|
7
|
+
method: 'parseInteger',
|
|
8
|
+
});
|
|
@@ -34,6 +34,20 @@ describe('prefer-safe-number-parse-integer', () => {
|
|
|
34
34
|
const n = parseInt(x as unknown as string & never, 10);
|
|
35
35
|
`,
|
|
36
36
|
},
|
|
37
|
+
{
|
|
38
|
+
name: 'ignores never-typed arguments',
|
|
39
|
+
code: dedent`
|
|
40
|
+
declare const x: string & number;
|
|
41
|
+
const n = parseInt(x, 10);
|
|
42
|
+
`,
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
name: 'ignores unrelated calls',
|
|
46
|
+
code: dedent`
|
|
47
|
+
const s = "1";
|
|
48
|
+
const n = Number(s);
|
|
49
|
+
`,
|
|
50
|
+
},
|
|
37
51
|
],
|
|
38
52
|
invalid: [
|
|
39
53
|
{
|
|
@@ -47,7 +61,7 @@ describe('prefer-safe-number-parse-integer', () => {
|
|
|
47
61
|
declare const s: string;
|
|
48
62
|
const n = Result.unwrapOkOr(SafeNumber.parseInteger(s), Number.NaN);
|
|
49
63
|
`,
|
|
50
|
-
errors: [{ messageId: '
|
|
64
|
+
errors: [{ messageId: 'useSafeParseInt' }],
|
|
51
65
|
},
|
|
52
66
|
{
|
|
53
67
|
name: 'rewrites Number.parseInt(x, 10)',
|
|
@@ -60,7 +74,7 @@ describe('prefer-safe-number-parse-integer', () => {
|
|
|
60
74
|
declare const s: string;
|
|
61
75
|
const n = Result.unwrapOkOr(SafeNumber.parseInteger(s), Number.NaN);
|
|
62
76
|
`,
|
|
63
|
-
errors: [{ messageId: '
|
|
77
|
+
errors: [{ messageId: 'useSafeParseInt' }],
|
|
64
78
|
},
|
|
65
79
|
],
|
|
66
80
|
});
|
|
@@ -111,7 +125,7 @@ describe('prefer-safe-number-parse-integer through type wrappers', () => {
|
|
|
111
125
|
declare const s: string;
|
|
112
126
|
const n = Result.unwrapOkOr(SafeNumber.parseInteger(s), Number.NaN);
|
|
113
127
|
`,
|
|
114
|
-
errors: [{ messageId: '
|
|
128
|
+
errors: [{ messageId: 'useSafeParseInt' }],
|
|
115
129
|
},
|
|
116
130
|
{
|
|
117
131
|
name: 'reads a radix of 10 through `satisfies` and keeps `satisfies` on the argument',
|
|
@@ -124,7 +138,7 @@ describe('prefer-safe-number-parse-integer through type wrappers', () => {
|
|
|
124
138
|
declare const s: string;
|
|
125
139
|
const n = Result.unwrapOkOr(SafeNumber.parseInteger(s satisfies string), Number.NaN);
|
|
126
140
|
`,
|
|
127
|
-
errors: [{ messageId: '
|
|
141
|
+
errors: [{ messageId: 'useSafeParseInt' }],
|
|
128
142
|
},
|
|
129
143
|
{
|
|
130
144
|
name: 'reads the callee through `as` and `!`',
|
|
@@ -140,8 +154,8 @@ describe('prefer-safe-number-parse-integer through type wrappers', () => {
|
|
|
140
154
|
const b = Result.unwrapOkOr(SafeNumber.parseInteger(s), Number.NaN);
|
|
141
155
|
`,
|
|
142
156
|
errors: [
|
|
143
|
-
{ messageId: '
|
|
144
|
-
{ messageId: '
|
|
157
|
+
{ messageId: 'useSafeParseInt' },
|
|
158
|
+
{ messageId: 'useSafeParseInt' },
|
|
145
159
|
],
|
|
146
160
|
},
|
|
147
161
|
{
|
|
@@ -155,7 +169,7 @@ describe('prefer-safe-number-parse-integer through type wrappers', () => {
|
|
|
155
169
|
declare const s: string;
|
|
156
170
|
const n = Result.unwrapOkOr(SafeNumber.parseInteger(s! as string), Number.NaN);
|
|
157
171
|
`,
|
|
158
|
-
errors: [{ messageId: '
|
|
172
|
+
errors: [{ messageId: 'useSafeParseInt' }],
|
|
159
173
|
},
|
|
160
174
|
],
|
|
161
175
|
});
|
|
@@ -178,7 +192,7 @@ describe('prefer-safe-number-parse-integer with parenthesized operands', () => {
|
|
|
178
192
|
declare const s: string;
|
|
179
193
|
const n = Result.unwrapOkOr(SafeNumber.parseInteger((a, s)), Number.NaN);
|
|
180
194
|
`,
|
|
181
|
-
errors: [{ messageId: '
|
|
195
|
+
errors: [{ messageId: 'useSafeParseInt' }],
|
|
182
196
|
},
|
|
183
197
|
],
|
|
184
198
|
});
|
|
@@ -1,171 +1,14 @@
|
|
|
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 * as ts from 'typescript';
|
|
14
|
-
import { isIdentifierNamed, typeHoldsAtEveryLayer } from './ast-utils.mjs';
|
|
1
|
+
import { createPreferSafeParseFloat } from 'eslint-rule-forge';
|
|
15
2
|
import { TS_STD_FORGE_MODULE } from './constants.mjs';
|
|
16
3
|
|
|
17
|
-
type Options = readonly [];
|
|
18
|
-
|
|
19
|
-
type MessageIds = 'useSafeNumberParse';
|
|
20
|
-
|
|
21
4
|
/**
|
|
22
|
-
* Returns `true` when every constituent of `type` is string-like, i.e. the
|
|
23
|
-
* type is assignable to the `string` parameter of `SafeNumber.parse`. A union
|
|
24
|
-
* such as `string | undefined` is rejected so the autofix never produces a
|
|
25
|
-
* type error.
|
|
26
|
-
*/
|
|
27
|
-
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
28
|
-
const isStringType = (type: ts.Type): boolean => {
|
|
29
|
-
const parts = type.isUnion() ? type.types : ([type] as const);
|
|
30
|
-
|
|
31
|
-
return (
|
|
32
|
-
parts.length > 0 &&
|
|
33
|
-
parts.every((t) => (t.flags & ts.TypeFlags.StringLike) !== 0)
|
|
34
|
-
);
|
|
35
|
-
};
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* Detects `parseFloat(x)`, `Number.parseFloat(x)`, and `Number(x)` (when x
|
|
39
|
-
* is typed as `string`) and replaces them with
|
|
40
|
-
* `Result.unwrapOkOr(SafeNumber.parse(x), Number.NaN)` (ts-std-forge).
|
|
41
|
-
*
|
|
42
|
-
* `Number(x)` is only flagged when x is purely a string type to avoid false
|
|
43
|
-
* positives for `Number(someBoolean)` or `Number(someNumber)` uses.
|
|
44
|
-
*
|
|
45
5
|
* The ts-data-forge rule of the same shape names `Num.safeParseFloat`, which
|
|
46
6
|
* has delegated to `SafeNumber.parse` since the D-49 inversion and wraps the
|
|
47
7
|
* result in a `FiniteNumber` brand. This one names the implementation, for
|
|
48
8
|
* callers who do not want the brand or the extra dependency.
|
|
49
9
|
*/
|
|
50
|
-
export const preferSafeNumberParse
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
'Replace `parseFloat(x)`, `Number.parseFloat(x)`, or `Number(x)` (when x is a string) with `Result.unwrapOkOr(SafeNumber.parse(x), Number.NaN)` from ts-std-forge.',
|
|
56
|
-
},
|
|
57
|
-
fixable: 'code',
|
|
58
|
-
schema: [],
|
|
59
|
-
messages: {
|
|
60
|
-
useSafeNumberParse:
|
|
61
|
-
'Replace `{{original}}` with `Result.unwrapOkOr(SafeNumber.parse({{argName}}), Number.NaN)` from ts-std-forge.',
|
|
62
|
-
},
|
|
63
|
-
},
|
|
64
|
-
|
|
65
|
-
create: (context) => {
|
|
66
|
-
const sourceCode = context.sourceCode;
|
|
67
|
-
|
|
68
|
-
const program = sourceCode.ast;
|
|
69
|
-
|
|
70
|
-
const tsStdForgeImports = getImportDeclarations(
|
|
71
|
-
program,
|
|
72
|
-
TS_STD_FORGE_MODULE,
|
|
73
|
-
);
|
|
74
|
-
|
|
75
|
-
const mut_nodesToFix: {
|
|
76
|
-
node: TSESTree.CallExpression;
|
|
77
|
-
argExpression: TSESTree.Expression;
|
|
78
|
-
}[] = [];
|
|
79
|
-
|
|
80
|
-
return {
|
|
81
|
-
CallExpression: (node) => {
|
|
82
|
-
const callee = skipTypeWrappers(node.callee);
|
|
83
|
-
|
|
84
|
-
// Match `parseFloat(...)` (global function).
|
|
85
|
-
const isGlobalParseFloat =
|
|
86
|
-
callee.type === AST_NODE_TYPES.Identifier &&
|
|
87
|
-
callee.name === 'parseFloat';
|
|
88
|
-
|
|
89
|
-
// Match `Number.parseFloat(...)`.
|
|
90
|
-
const isNumberParseFloat =
|
|
91
|
-
callee.type === AST_NODE_TYPES.MemberExpression &&
|
|
92
|
-
!callee.computed &&
|
|
93
|
-
isIdentifierNamed(callee.object, '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 && callee.name === 'Number';
|
|
100
|
-
|
|
101
|
-
if (!isGlobalParseFloat && !isNumberParseFloat && !isNumberCall) {
|
|
102
|
-
return;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
const args = node.arguments;
|
|
106
|
-
|
|
107
|
-
const firstArg = args[0];
|
|
108
|
-
|
|
109
|
-
if (firstArg === undefined) {
|
|
110
|
-
return;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
// Spread argument (`parseFloat(...rest)`) cannot be rewritten safely.
|
|
114
|
-
if (firstArg.type === AST_NODE_TYPES.SpreadElement) {
|
|
115
|
-
return;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
// The argument must be purely `string` at every layer of type
|
|
119
|
-
// wrappers: the value's own type is what `Number` sees at run time
|
|
120
|
-
// (`Number(true as unknown as string)` is 1, the rewrite NaN), and the
|
|
121
|
-
// type a wrapper states is what the fix, which keeps the wrapper,
|
|
122
|
-
// hands `SafeNumber.parse(value: string)`. Without type information,
|
|
123
|
-
// skip.
|
|
124
|
-
if (!typeHoldsAtEveryLayer(sourceCode, firstArg, isStringType)) {
|
|
125
|
-
return;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
mut_nodesToFix.push({ node, argExpression: firstArg });
|
|
129
|
-
},
|
|
130
|
-
'Program:exit': () => {
|
|
131
|
-
const namedImports = getNamedImports(tsStdForgeImports);
|
|
132
|
-
|
|
133
|
-
const missingImports = (['Result', 'SafeNumber'] as const).filter(
|
|
134
|
-
(name) => !namedImports.includes(name),
|
|
135
|
-
);
|
|
136
|
-
|
|
137
|
-
for (const [
|
|
138
|
-
index,
|
|
139
|
-
{ node, argExpression },
|
|
140
|
-
] of mut_nodesToFix.entries()) {
|
|
141
|
-
const argText = getArgumentText(sourceCode, argExpression);
|
|
142
|
-
|
|
143
|
-
context.report({
|
|
144
|
-
node,
|
|
145
|
-
messageId: 'useSafeNumberParse',
|
|
146
|
-
data: {
|
|
147
|
-
original: sourceCode.getText(node),
|
|
148
|
-
argName: argText,
|
|
149
|
-
},
|
|
150
|
-
fix: (fixer) => {
|
|
151
|
-
const core = `Result.unwrapOkOr(SafeNumber.parse(${argText}), Number.NaN)`;
|
|
152
|
-
|
|
153
|
-
const importFixes =
|
|
154
|
-
index === 0 && missingImports.length > 0
|
|
155
|
-
? buildNamedImportFix(
|
|
156
|
-
fixer,
|
|
157
|
-
program,
|
|
158
|
-
TS_STD_FORGE_MODULE,
|
|
159
|
-
missingImports,
|
|
160
|
-
)
|
|
161
|
-
: [];
|
|
162
|
-
|
|
163
|
-
return [...importFixes, fixer.replaceText(node, core)];
|
|
164
|
-
},
|
|
165
|
-
});
|
|
166
|
-
}
|
|
167
|
-
},
|
|
168
|
-
};
|
|
169
|
-
},
|
|
170
|
-
defaultOptions: [],
|
|
171
|
-
} as const;
|
|
10
|
+
export const preferSafeNumberParse = createPreferSafeParseFloat({
|
|
11
|
+
moduleName: TS_STD_FORGE_MODULE,
|
|
12
|
+
namespace: 'SafeNumber',
|
|
13
|
+
method: 'parse',
|
|
14
|
+
});
|
|
@@ -34,6 +34,27 @@ describe('prefer-safe-number-parse', () => {
|
|
|
34
34
|
const n = Number(b);
|
|
35
35
|
`,
|
|
36
36
|
},
|
|
37
|
+
{
|
|
38
|
+
name: 'ignores never-typed arguments (string & number = never)',
|
|
39
|
+
code: dedent`
|
|
40
|
+
declare const x: string & number;
|
|
41
|
+
const n = parseFloat(x);
|
|
42
|
+
`,
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
name: 'ignores spread arguments',
|
|
46
|
+
code: dedent`
|
|
47
|
+
declare const args: [string];
|
|
48
|
+
const n = parseFloat(...args);
|
|
49
|
+
`,
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
name: 'ignores parseInt (handled by prefer-safe-number-parse-integer)',
|
|
53
|
+
code: dedent`
|
|
54
|
+
const s = "123";
|
|
55
|
+
const n = parseInt(s, 10);
|
|
56
|
+
`,
|
|
57
|
+
},
|
|
37
58
|
{
|
|
38
59
|
name: 'ignores a union that is not purely string',
|
|
39
60
|
code: dedent`
|
|
@@ -61,7 +82,7 @@ describe('prefer-safe-number-parse', () => {
|
|
|
61
82
|
declare const s: string;
|
|
62
83
|
const n = Result.unwrapOkOr(SafeNumber.parse(s), Number.NaN);
|
|
63
84
|
`,
|
|
64
|
-
errors: [{ messageId: '
|
|
85
|
+
errors: [{ messageId: 'useSafeParseFloat' }],
|
|
65
86
|
},
|
|
66
87
|
{
|
|
67
88
|
name: 'rewrites Number.parseFloat on a string',
|
|
@@ -74,7 +95,7 @@ describe('prefer-safe-number-parse', () => {
|
|
|
74
95
|
declare const s: string;
|
|
75
96
|
const n = Result.unwrapOkOr(SafeNumber.parse(s), Number.NaN);
|
|
76
97
|
`,
|
|
77
|
-
errors: [{ messageId: '
|
|
98
|
+
errors: [{ messageId: 'useSafeParseFloat' }],
|
|
78
99
|
},
|
|
79
100
|
{
|
|
80
101
|
name: 'rewrites Number(x) when x is a string',
|
|
@@ -87,7 +108,7 @@ describe('prefer-safe-number-parse', () => {
|
|
|
87
108
|
declare const s: string;
|
|
88
109
|
const n = Result.unwrapOkOr(SafeNumber.parse(s), Number.NaN);
|
|
89
110
|
`,
|
|
90
|
-
errors: [{ messageId: '
|
|
111
|
+
errors: [{ messageId: 'useSafeParseFloat' }],
|
|
91
112
|
},
|
|
92
113
|
{
|
|
93
114
|
name: 'adds only the missing import',
|
|
@@ -102,7 +123,7 @@ describe('prefer-safe-number-parse', () => {
|
|
|
102
123
|
declare const s: string;
|
|
103
124
|
const n = Result.unwrapOkOr(SafeNumber.parse(s), Number.NaN);
|
|
104
125
|
`,
|
|
105
|
-
errors: [{ messageId: '
|
|
126
|
+
errors: [{ messageId: 'useSafeParseFloat' }],
|
|
106
127
|
},
|
|
107
128
|
],
|
|
108
129
|
});
|
|
@@ -159,7 +180,7 @@ describe('prefer-safe-number-parse through type wrappers', () => {
|
|
|
159
180
|
declare const s: string;
|
|
160
181
|
const n = Result.unwrapOkOr(SafeNumber.parse(s satisfies string), Number.NaN);
|
|
161
182
|
`,
|
|
162
|
-
errors: [{ messageId: '
|
|
183
|
+
errors: [{ messageId: 'useSafeParseFloat' }],
|
|
163
184
|
},
|
|
164
185
|
{
|
|
165
186
|
name: 'reads the callee through `as` and `!`',
|
|
@@ -175,8 +196,8 @@ describe('prefer-safe-number-parse through type wrappers', () => {
|
|
|
175
196
|
const b = Result.unwrapOkOr(SafeNumber.parse(s), Number.NaN);
|
|
176
197
|
`,
|
|
177
198
|
errors: [
|
|
178
|
-
{ messageId: '
|
|
179
|
-
{ messageId: '
|
|
199
|
+
{ messageId: 'useSafeParseFloat' },
|
|
200
|
+
{ messageId: 'useSafeParseFloat' },
|
|
180
201
|
],
|
|
181
202
|
},
|
|
182
203
|
{
|
|
@@ -190,7 +211,7 @@ describe('prefer-safe-number-parse through type wrappers', () => {
|
|
|
190
211
|
declare const s: string;
|
|
191
212
|
const n = Result.unwrapOkOr(SafeNumber.parse(s! as string), Number.NaN);
|
|
192
213
|
`,
|
|
193
|
-
errors: [{ messageId: '
|
|
214
|
+
errors: [{ messageId: 'useSafeParseFloat' }],
|
|
194
215
|
},
|
|
195
216
|
],
|
|
196
217
|
});
|
|
@@ -213,7 +234,7 @@ describe('prefer-safe-number-parse with parenthesized operands', () => {
|
|
|
213
234
|
declare const s: string;
|
|
214
235
|
const n = Result.unwrapOkOr(SafeNumber.parse((a, s)), Number.NaN);
|
|
215
236
|
`,
|
|
216
|
-
errors: [{ messageId: '
|
|
237
|
+
errors: [{ messageId: 'useSafeParseFloat' }],
|
|
217
238
|
},
|
|
218
239
|
],
|
|
219
240
|
});
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
import { type TSESLint, type TSESTree } from '@typescript-eslint/utils';
|
|
2
|
-
import type * as ts from 'typescript';
|
|
3
|
-
/**
|
|
4
|
-
* Whether `node`, read through its type wrappers, is the identifier `name`:
|
|
5
|
-
* `Number`, `(Number as NumberConstructor)` and `Number!` are the same object.
|
|
6
|
-
*/
|
|
7
|
-
export declare const isIdentifierNamed: (node: TSESTree.Node, name: string) => boolean;
|
|
8
|
-
/**
|
|
9
|
-
* Whether `holds` is true of the type of every layer of type wrappers around
|
|
10
|
-
* `node` (see {@link typeWrapperLayers}), outermost to innermost; `false`
|
|
11
|
-
* without type information. A widening cast (`s as string | number`) is the
|
|
12
|
-
* author saying the value may be more than it is, and is respected; a
|
|
13
|
-
* narrowing one (`s!`, `v as unknown as string`) cannot vouch for what the
|
|
14
|
-
* value's own type does not, and the fix, which keeps the wrapper, has to
|
|
15
|
-
* type-check at every layer too.
|
|
16
|
-
*/
|
|
17
|
-
export declare const typeHoldsAtEveryLayer: (sourceCode: TSESLint.SourceCode, node: TSESTree.Node, holds: (type: ts.Type, typeChecker: ts.TypeChecker) => boolean) => boolean;
|
|
18
|
-
//# sourceMappingURL=ast-utils.d.mts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"ast-utils.d.mts","sourceRoot":"","sources":["../../src/rules/ast-utils.mts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,QAAQ,EACb,KAAK,QAAQ,EACd,MAAM,0BAA0B,CAAC;AAElC,OAAO,KAAK,KAAK,EAAE,MAAM,YAAY,CAAC;AAEtC;;;GAGG;AACH,eAAO,MAAM,iBAAiB,SAEtB,QAAQ,CAAC,IAAI,QACb,MAAM,KACX,OAMF,CAAC;AAEF;;;;;;;;GAQG;AACH,eAAO,MAAM,qBAAqB,eAEpB,QAAQ,CAAC,UAAU,QAEzB,QAAQ,CAAC,IAAI,SAEZ,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,EAAE,WAAW,EAAE,EAAE,CAAC,WAAW,KAAK,OAAO,KAC7D,OAgBF,CAAC"}
|
package/dist/rules/ast-utils.mjs
DELETED
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
import { AST_NODE_TYPES, } from '@typescript-eslint/utils';
|
|
2
|
-
import { skipTypeWrappers, typeWrapperLayers } from 'eslint-rule-forge';
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
export const isIdentifierNamed = (
|
|
8
|
-
|
|
9
|
-
node, name) => {
|
|
10
|
-
const unwrapped = skipTypeWrappers(node);
|
|
11
|
-
return (unwrapped.type === AST_NODE_TYPES.Identifier && unwrapped.name === name);
|
|
12
|
-
};
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
export const typeHoldsAtEveryLayer = (
|
|
23
|
-
|
|
24
|
-
sourceCode,
|
|
25
|
-
|
|
26
|
-
node,
|
|
27
|
-
|
|
28
|
-
holds) => {
|
|
29
|
-
const services = sourceCode.parserServices;
|
|
30
|
-
const checker = services?.program?.getTypeChecker();
|
|
31
|
-
if (checker === undefined) {
|
|
32
|
-
return false;
|
|
33
|
-
}
|
|
34
|
-
return typeWrapperLayers(node).every((layer) => {
|
|
35
|
-
const tsNode = services?.esTreeNodeToTSNodeMap?.get(layer);
|
|
36
|
-
return (tsNode !== undefined && holds(checker.getTypeAtLocation(tsNode), checker));
|
|
37
|
-
});
|
|
38
|
-
};
|
|
39
|
-
//# sourceMappingURL=ast-utils.mjs.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"ast-utils.mjs","sourceRoot":"","sources":["../../src/rules/ast-utils.mts"],"names":[],"mappings":"AAAA,OAAO,EACL,cAAc,GAGf,MAAM,0BAA0B,CAAC;AAClC,OAAO,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AAGxE;;;GAGG;AACH,MAAM,CAAC,MAAM,iBAAiB,GAAG;AAC/B,8EAA8E;AAC9E,IAAmB,EACnB,IAAY,EACH,EAAE;IACX,MAAM,SAAS,GAAG,gBAAgB,CAAC,IAAI,CAAC,CAAC;IAEzC,OAAO,CACL,SAAS,CAAC,IAAI,KAAK,cAAc,CAAC,UAAU,IAAI,SAAS,CAAC,IAAI,KAAK,IAAI,CACxE,CAAC;AACJ,CAAC,CAAC;AAEF;;;;;;;;GAQG;AACH,MAAM,CAAC,MAAM,qBAAqB,GAAG;AACnC,8EAA8E;AAC9E,UAA+B;AAC/B,8EAA8E;AAC9E,IAAmB;AACnB,8EAA8E;AAC9E,KAA8D,EACrD,EAAE;IACX,MAAM,QAAQ,GAAG,UAAU,CAAC,cAAc,CAAC;IAE3C,MAAM,OAAO,GAAG,QAAQ,EAAE,OAAO,EAAE,cAAc,EAAE,CAAC;IAEpD,IAAI,OAAO,KAAK,SAAS,EAAE,CAAC;QAC1B,OAAO,KAAK,CAAC;IACf,CAAC;IAED,OAAO,iBAAiB,CAAC,IAAI,CAAC,CAAC,KAAK,CAAC,CAAC,KAAK,EAAE,EAAE;QAC7C,MAAM,MAAM,GAAG,QAAQ,EAAE,qBAAqB,EAAE,GAAG,CAAC,KAAK,CAAC,CAAC;QAE3D,OAAO,CACL,MAAM,KAAK,SAAS,IAAI,KAAK,CAAC,OAAO,CAAC,iBAAiB,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC,CAC1E,CAAC;IACJ,CAAC,CAAC,CAAC;AACL,CAAC,CAAC"}
|
package/src/rules/ast-utils.mts
DELETED
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
AST_NODE_TYPES,
|
|
3
|
-
type TSESLint,
|
|
4
|
-
type TSESTree,
|
|
5
|
-
} from '@typescript-eslint/utils';
|
|
6
|
-
import { skipTypeWrappers, typeWrapperLayers } from 'eslint-rule-forge';
|
|
7
|
-
import type * as ts from 'typescript';
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Whether `node`, read through its type wrappers, is the identifier `name`:
|
|
11
|
-
* `Number`, `(Number as NumberConstructor)` and `Number!` are the same object.
|
|
12
|
-
*/
|
|
13
|
-
export const isIdentifierNamed = (
|
|
14
|
-
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
15
|
-
node: TSESTree.Node,
|
|
16
|
-
name: string,
|
|
17
|
-
): boolean => {
|
|
18
|
-
const unwrapped = skipTypeWrappers(node);
|
|
19
|
-
|
|
20
|
-
return (
|
|
21
|
-
unwrapped.type === AST_NODE_TYPES.Identifier && unwrapped.name === name
|
|
22
|
-
);
|
|
23
|
-
};
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* Whether `holds` is true of the type of every layer of type wrappers around
|
|
27
|
-
* `node` (see {@link typeWrapperLayers}), outermost to innermost; `false`
|
|
28
|
-
* without type information. A widening cast (`s as string | number`) is the
|
|
29
|
-
* author saying the value may be more than it is, and is respected; a
|
|
30
|
-
* narrowing one (`s!`, `v as unknown as string`) cannot vouch for what the
|
|
31
|
-
* value's own type does not, and the fix, which keeps the wrapper, has to
|
|
32
|
-
* type-check at every layer too.
|
|
33
|
-
*/
|
|
34
|
-
export const typeHoldsAtEveryLayer = (
|
|
35
|
-
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
36
|
-
sourceCode: TSESLint.SourceCode,
|
|
37
|
-
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
38
|
-
node: TSESTree.Node,
|
|
39
|
-
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
40
|
-
holds: (type: ts.Type, typeChecker: ts.TypeChecker) => boolean,
|
|
41
|
-
): boolean => {
|
|
42
|
-
const services = sourceCode.parserServices;
|
|
43
|
-
|
|
44
|
-
const checker = services?.program?.getTypeChecker();
|
|
45
|
-
|
|
46
|
-
if (checker === undefined) {
|
|
47
|
-
return false;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
return typeWrapperLayers(node).every((layer) => {
|
|
51
|
-
const tsNode = services?.esTreeNodeToTSNodeMap?.get(layer);
|
|
52
|
-
|
|
53
|
-
return (
|
|
54
|
-
tsNode !== undefined && holds(checker.getTypeAtLocation(tsNode), checker)
|
|
55
|
-
);
|
|
56
|
-
});
|
|
57
|
-
};
|