eslint-plugin-ts-std-forge 0.1.4 → 0.1.6
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/index.d.mts +1 -1
- package/dist/index.d.mts.map +1 -1
- package/dist/plugin.d.mts +1 -1
- 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 +1 -1
- package/dist/rule-types.d.mts.map +1 -1
- package/dist/rules/ast-utils.d.mts +41 -0
- package/dist/rules/ast-utils.d.mts.map +1 -0
- package/dist/rules/ast-utils.mjs +101 -0
- package/dist/rules/ast-utils.mjs.map +1 -0
- package/dist/rules/import-utils.d.mts +1 -1
- package/dist/rules/import-utils.d.mts.map +1 -1
- package/dist/rules/prefer-is-non-null-object.mjs +26 -12
- 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 +12 -12
- package/dist/rules/prefer-is-record-and-has-key.mjs.map +1 -1
- package/dist/rules/prefer-safe-array-is-array.d.mts.map +1 -1
- package/dist/rules/prefer-safe-array-is-array.mjs +15 -13
- package/dist/rules/prefer-safe-array-is-array.mjs.map +1 -1
- package/dist/rules/prefer-safe-array-length-guard.d.mts.map +1 -1
- package/dist/rules/prefer-safe-array-length-guard.mjs +11 -13
- package/dist/rules/prefer-safe-array-length-guard.mjs.map +1 -1
- package/dist/rules/prefer-safe-number-parse-integer.d.mts.map +1 -1
- package/dist/rules/prefer-safe-number-parse-integer.mjs +9 -16
- package/dist/rules/prefer-safe-number-parse-integer.mjs.map +1 -1
- package/dist/rules/prefer-safe-number-parse.d.mts.map +1 -1
- package/dist/rules/prefer-safe-number-parse.mjs +7 -15
- package/dist/rules/prefer-safe-number-parse.mjs.map +1 -1
- package/dist/types.d.mts +2 -2
- package/dist/types.d.mts.map +1 -1
- package/package.json +7 -7
- package/src/index.mts +1 -1
- package/src/plugin.mts +5 -4
- package/src/plugin.test.mts +0 -13
- package/src/rule-types.mts +2 -2
- package/src/rules/ast-utils.mts +147 -0
- package/src/rules/import-utils.mts +1 -1
- package/src/rules/prefer-is-non-null-object.mts +34 -13
- package/src/rules/prefer-is-non-null-object.test.mts +54 -0
- package/src/rules/prefer-is-record-and-has-key.mts +19 -16
- package/src/rules/prefer-is-record-and-has-key.test.mts +128 -0
- package/src/rules/prefer-safe-array-is-array.mts +22 -18
- package/src/rules/prefer-safe-array-is-array.test.mts +93 -0
- package/src/rules/prefer-safe-array-length-guard.mts +20 -19
- package/src/rules/prefer-safe-array-length-guard.test.mts +98 -0
- package/src/rules/prefer-safe-number-parse-integer.mts +18 -24
- package/src/rules/prefer-safe-number-parse-integer.test.mts +104 -0
- package/src/rules/prefer-safe-number-parse.mts +14 -22
- package/src/rules/prefer-safe-number-parse.test.mts +97 -0
- package/src/rules/rules.mts +1 -1
- package/src/types.mts +2 -2
|
@@ -65,3 +65,107 @@ describe('prefer-safe-number-parse-integer', () => {
|
|
|
65
65
|
],
|
|
66
66
|
});
|
|
67
67
|
});
|
|
68
|
+
|
|
69
|
+
describe('prefer-safe-number-parse-integer through type wrappers', () => {
|
|
70
|
+
tester.run('prefer-safe-number-parse-integer', preferSafeNumberParseInteger, {
|
|
71
|
+
valid: [
|
|
72
|
+
{
|
|
73
|
+
// `parseInt(1e21)` is 1, `SafeNumber.parseInteger(1e21)` is 1e21.
|
|
74
|
+
name: 'ignores a number cast to string',
|
|
75
|
+
code: dedent`
|
|
76
|
+
declare const x: number;
|
|
77
|
+
const n = parseInt(x as unknown as string);
|
|
78
|
+
`,
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
name: 'ignores a radix other than 10 behind `as const`',
|
|
82
|
+
code: dedent`
|
|
83
|
+
declare const s: string;
|
|
84
|
+
const n = parseInt(s, 16 as const);
|
|
85
|
+
`,
|
|
86
|
+
},
|
|
87
|
+
],
|
|
88
|
+
invalid: [
|
|
89
|
+
{
|
|
90
|
+
name: 'reads a radix of 10 through `as const`',
|
|
91
|
+
code: dedent`
|
|
92
|
+
declare const s: string;
|
|
93
|
+
const n = parseInt(s, 10 as const);
|
|
94
|
+
`,
|
|
95
|
+
output: dedent`
|
|
96
|
+
import { Result, SafeNumber } from 'ts-std-forge';
|
|
97
|
+
declare const s: string;
|
|
98
|
+
const n = Result.unwrapOkOr(SafeNumber.parseInteger(s), Number.NaN);
|
|
99
|
+
`,
|
|
100
|
+
errors: [{ messageId: 'useSafeNumberParseInteger' }],
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
name: 'reads a radix of 10 through `satisfies` and keeps `satisfies` on the argument',
|
|
104
|
+
code: dedent`
|
|
105
|
+
declare const s: string;
|
|
106
|
+
const n = Number.parseInt(s satisfies string, 10 satisfies number);
|
|
107
|
+
`,
|
|
108
|
+
output: dedent`
|
|
109
|
+
import { Result, SafeNumber } from 'ts-std-forge';
|
|
110
|
+
declare const s: string;
|
|
111
|
+
const n = Result.unwrapOkOr(SafeNumber.parseInteger(s satisfies string), Number.NaN);
|
|
112
|
+
`,
|
|
113
|
+
errors: [{ messageId: 'useSafeNumberParseInteger' }],
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
name: 'reads the callee through `as` and `!`',
|
|
117
|
+
code: dedent`
|
|
118
|
+
declare const s: string;
|
|
119
|
+
const a = (parseInt as typeof parseInt)(s);
|
|
120
|
+
const b = (Number as NumberConstructor)!.parseInt(s, 10);
|
|
121
|
+
`,
|
|
122
|
+
output: dedent`
|
|
123
|
+
import { Result, SafeNumber } from 'ts-std-forge';
|
|
124
|
+
declare const s: string;
|
|
125
|
+
const a = Result.unwrapOkOr(SafeNumber.parseInteger(s), Number.NaN);
|
|
126
|
+
const b = Result.unwrapOkOr(SafeNumber.parseInteger(s), Number.NaN);
|
|
127
|
+
`,
|
|
128
|
+
errors: [
|
|
129
|
+
{ messageId: 'useSafeNumberParseInteger' },
|
|
130
|
+
{ messageId: 'useSafeNumberParseInteger' },
|
|
131
|
+
],
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
name: 'reads `!` on an optional string as the string it asserts',
|
|
135
|
+
code: dedent`
|
|
136
|
+
declare const s: string | undefined;
|
|
137
|
+
const n = parseInt(s!);
|
|
138
|
+
`,
|
|
139
|
+
output: dedent`
|
|
140
|
+
import { Result, SafeNumber } from 'ts-std-forge';
|
|
141
|
+
declare const s: string | undefined;
|
|
142
|
+
const n = Result.unwrapOkOr(SafeNumber.parseInteger(s!), Number.NaN);
|
|
143
|
+
`,
|
|
144
|
+
errors: [{ messageId: 'useSafeNumberParseInteger' }],
|
|
145
|
+
},
|
|
146
|
+
],
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
describe('prefer-safe-number-parse-integer with parenthesized operands', () => {
|
|
151
|
+
tester.run('prefer-safe-number-parse-integer', preferSafeNumberParseInteger, {
|
|
152
|
+
valid: [],
|
|
153
|
+
invalid: [
|
|
154
|
+
{
|
|
155
|
+
name: 'keeps the parentheses of a sequence argument',
|
|
156
|
+
code: dedent`
|
|
157
|
+
declare const a: number;
|
|
158
|
+
declare const s: string;
|
|
159
|
+
const n = parseInt((a, s), (10));
|
|
160
|
+
`,
|
|
161
|
+
output: dedent`
|
|
162
|
+
import { Result, SafeNumber } from 'ts-std-forge';
|
|
163
|
+
declare const a: number;
|
|
164
|
+
declare const s: string;
|
|
165
|
+
const n = Result.unwrapOkOr(SafeNumber.parseInteger((a, s)), Number.NaN);
|
|
166
|
+
`,
|
|
167
|
+
errors: [{ messageId: 'useSafeNumberParseInteger' }],
|
|
168
|
+
},
|
|
169
|
+
],
|
|
170
|
+
});
|
|
171
|
+
});
|
|
@@ -4,6 +4,12 @@ import {
|
|
|
4
4
|
type TSESTree,
|
|
5
5
|
} from '@typescript-eslint/utils';
|
|
6
6
|
import * as ts from 'typescript';
|
|
7
|
+
import {
|
|
8
|
+
getArgumentText,
|
|
9
|
+
getValueType,
|
|
10
|
+
isIdentifierNamed,
|
|
11
|
+
skipTypeWrappers,
|
|
12
|
+
} from './ast-utils.mjs';
|
|
7
13
|
import {
|
|
8
14
|
buildImportFixes,
|
|
9
15
|
getNamedImports,
|
|
@@ -65,8 +71,6 @@ export const preferSafeNumberParse: TSESLint.RuleModule<MessageIds, Options> = {
|
|
|
65
71
|
|
|
66
72
|
const tsStdForgeImport = getTsStdForgeImport(program);
|
|
67
73
|
|
|
68
|
-
const services = sourceCode.parserServices;
|
|
69
|
-
|
|
70
74
|
const mut_nodesToFix: {
|
|
71
75
|
node: TSESTree.CallExpression;
|
|
72
76
|
argExpression: TSESTree.Expression;
|
|
@@ -74,7 +78,7 @@ export const preferSafeNumberParse: TSESLint.RuleModule<MessageIds, Options> = {
|
|
|
74
78
|
|
|
75
79
|
return {
|
|
76
80
|
CallExpression: (node) => {
|
|
77
|
-
const
|
|
81
|
+
const callee = skipTypeWrappers(node.callee);
|
|
78
82
|
|
|
79
83
|
// Match `parseFloat(...)` (global function).
|
|
80
84
|
const isGlobalParseFloat =
|
|
@@ -85,8 +89,7 @@ export const preferSafeNumberParse: TSESLint.RuleModule<MessageIds, Options> = {
|
|
|
85
89
|
const isNumberParseFloat =
|
|
86
90
|
callee.type === AST_NODE_TYPES.MemberExpression &&
|
|
87
91
|
!callee.computed &&
|
|
88
|
-
callee.object
|
|
89
|
-
callee.object.name === 'Number' &&
|
|
92
|
+
isIdentifierNamed(callee.object, 'Number') &&
|
|
90
93
|
callee.property.type === AST_NODE_TYPES.Identifier &&
|
|
91
94
|
callee.property.name === 'parseFloat';
|
|
92
95
|
|
|
@@ -112,23 +115,12 @@ export const preferSafeNumberParse: TSESLint.RuleModule<MessageIds, Options> = {
|
|
|
112
115
|
}
|
|
113
116
|
|
|
114
117
|
// The argument must be purely `string` so the autofix is type-safe
|
|
115
|
-
// against `SafeNumber.parse(value: string)
|
|
116
|
-
//
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
const checker = services.program.getTypeChecker();
|
|
122
|
-
|
|
123
|
-
const tsNode = services.esTreeNodeToTSNodeMap?.get(firstArg);
|
|
124
|
-
|
|
125
|
-
if (tsNode === undefined) {
|
|
126
|
-
return;
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
const argType = checker.getTypeAtLocation(tsNode);
|
|
118
|
+
// against `SafeNumber.parse(value: string)`, and that of the value, not
|
|
119
|
+
// of a cast: `Number(true as unknown as string)` is 1, the rewrite NaN.
|
|
120
|
+
// Without type information, skip.
|
|
121
|
+
const argType = getValueType(sourceCode, firstArg);
|
|
130
122
|
|
|
131
|
-
if (!isStringType(argType)) {
|
|
123
|
+
if (argType === undefined || !isStringType(argType)) {
|
|
132
124
|
return;
|
|
133
125
|
}
|
|
134
126
|
|
|
@@ -145,7 +137,7 @@ export const preferSafeNumberParse: TSESLint.RuleModule<MessageIds, Options> = {
|
|
|
145
137
|
index,
|
|
146
138
|
{ node, argExpression },
|
|
147
139
|
] of mut_nodesToFix.entries()) {
|
|
148
|
-
const argText = sourceCode
|
|
140
|
+
const argText = getArgumentText(sourceCode, argExpression);
|
|
149
141
|
|
|
150
142
|
context.report({
|
|
151
143
|
node,
|
|
@@ -107,3 +107,100 @@ describe('prefer-safe-number-parse', () => {
|
|
|
107
107
|
],
|
|
108
108
|
});
|
|
109
109
|
});
|
|
110
|
+
|
|
111
|
+
describe('prefer-safe-number-parse through type wrappers', () => {
|
|
112
|
+
tester.run('prefer-safe-number-parse', preferSafeNumberParse, {
|
|
113
|
+
valid: [
|
|
114
|
+
{
|
|
115
|
+
name: 'ignores a boolean cast to string, which Number() reads as 0 or 1',
|
|
116
|
+
code: dedent`
|
|
117
|
+
declare const b: boolean;
|
|
118
|
+
const n = Number(b as unknown as string);
|
|
119
|
+
`,
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
name: 'ignores a number cast to string with `<T>`',
|
|
123
|
+
code: dedent`
|
|
124
|
+
declare const x: number;
|
|
125
|
+
const n = Number(<string>(<unknown>x));
|
|
126
|
+
`,
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
name: 'ignores a union made to look like a string with `!`',
|
|
130
|
+
code: dedent`
|
|
131
|
+
declare const x: string | number | undefined;
|
|
132
|
+
const n = parseFloat(x! as string);
|
|
133
|
+
`,
|
|
134
|
+
},
|
|
135
|
+
],
|
|
136
|
+
invalid: [
|
|
137
|
+
{
|
|
138
|
+
name: 'keeps `satisfies` on a string argument',
|
|
139
|
+
code: dedent`
|
|
140
|
+
declare const s: string;
|
|
141
|
+
const n = Number(s satisfies string);
|
|
142
|
+
`,
|
|
143
|
+
output: dedent`
|
|
144
|
+
import { Result, SafeNumber } from 'ts-std-forge';
|
|
145
|
+
declare const s: string;
|
|
146
|
+
const n = Result.unwrapOkOr(SafeNumber.parse(s satisfies string), Number.NaN);
|
|
147
|
+
`,
|
|
148
|
+
errors: [{ messageId: 'useSafeNumberParse' }],
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
name: 'reads the callee through `as` and `!`',
|
|
152
|
+
code: dedent`
|
|
153
|
+
declare const s: string;
|
|
154
|
+
const a = (Number as NumberConstructor)(s);
|
|
155
|
+
const b = Number!.parseFloat(s);
|
|
156
|
+
`,
|
|
157
|
+
output: dedent`
|
|
158
|
+
import { Result, SafeNumber } from 'ts-std-forge';
|
|
159
|
+
declare const s: string;
|
|
160
|
+
const a = Result.unwrapOkOr(SafeNumber.parse(s), Number.NaN);
|
|
161
|
+
const b = Result.unwrapOkOr(SafeNumber.parse(s), Number.NaN);
|
|
162
|
+
`,
|
|
163
|
+
errors: [
|
|
164
|
+
{ messageId: 'useSafeNumberParse' },
|
|
165
|
+
{ messageId: 'useSafeNumberParse' },
|
|
166
|
+
],
|
|
167
|
+
},
|
|
168
|
+
{
|
|
169
|
+
name: 'reads `!` on an optional string as the string it asserts',
|
|
170
|
+
code: dedent`
|
|
171
|
+
declare const s: string | undefined;
|
|
172
|
+
const n = parseFloat(s!);
|
|
173
|
+
`,
|
|
174
|
+
output: dedent`
|
|
175
|
+
import { Result, SafeNumber } from 'ts-std-forge';
|
|
176
|
+
declare const s: string | undefined;
|
|
177
|
+
const n = Result.unwrapOkOr(SafeNumber.parse(s!), Number.NaN);
|
|
178
|
+
`,
|
|
179
|
+
errors: [{ messageId: 'useSafeNumberParse' }],
|
|
180
|
+
},
|
|
181
|
+
],
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
describe('prefer-safe-number-parse with parenthesized operands', () => {
|
|
186
|
+
tester.run('prefer-safe-number-parse', preferSafeNumberParse, {
|
|
187
|
+
valid: [],
|
|
188
|
+
invalid: [
|
|
189
|
+
{
|
|
190
|
+
name: 'keeps the parentheses of a sequence argument',
|
|
191
|
+
code: dedent`
|
|
192
|
+
declare const a: number;
|
|
193
|
+
declare const s: string;
|
|
194
|
+
const n = parseFloat((a, s));
|
|
195
|
+
`,
|
|
196
|
+
output: dedent`
|
|
197
|
+
import { Result, SafeNumber } from 'ts-std-forge';
|
|
198
|
+
declare const a: number;
|
|
199
|
+
declare const s: string;
|
|
200
|
+
const n = Result.unwrapOkOr(SafeNumber.parse((a, s)), Number.NaN);
|
|
201
|
+
`,
|
|
202
|
+
errors: [{ messageId: 'useSafeNumberParse' }],
|
|
203
|
+
},
|
|
204
|
+
],
|
|
205
|
+
});
|
|
206
|
+
});
|
package/src/rules/rules.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { ESLintPlugin } from '../types.mjs';
|
|
2
2
|
import { preferIsNonNullObject } from './prefer-is-non-null-object.mjs';
|
|
3
3
|
import { preferIsRecordAndHasKey } from './prefer-is-record-and-has-key.mjs';
|
|
4
4
|
import { preferSafeArrayIsArray } from './prefer-safe-array-is-array.mjs';
|
package/src/types.mts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import type { TSESLint } from '@typescript-eslint/utils';
|
|
2
|
+
import type { DeepReadonly } from 'ts-type-forge';
|
|
3
3
|
|
|
4
4
|
export type ESLintPlugin = DeepReadonly<TSESLint.FlatConfig.Plugin>;
|
|
5
5
|
|