eslint-plugin-ts-std-forge 0.1.3 → 0.1.5

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.
Files changed (54) hide show
  1. package/dist/plugin.d.mts +1 -1
  2. package/dist/plugin.d.mts.map +1 -1
  3. package/dist/plugin.mjs +1 -0
  4. package/dist/plugin.mjs.map +1 -1
  5. package/dist/rule-types.d.mts +1 -1
  6. package/dist/rule-types.d.mts.map +1 -1
  7. package/dist/rules/ast-utils.d.mts +41 -0
  8. package/dist/rules/ast-utils.d.mts.map +1 -0
  9. package/dist/rules/ast-utils.mjs +101 -0
  10. package/dist/rules/ast-utils.mjs.map +1 -0
  11. package/dist/rules/import-utils.d.mts +1 -1
  12. package/dist/rules/import-utils.d.mts.map +1 -1
  13. package/dist/rules/import-utils.mjs +2 -1
  14. package/dist/rules/import-utils.mjs.map +1 -1
  15. package/dist/rules/prefer-is-non-null-object.d.mts.map +1 -1
  16. package/dist/rules/prefer-is-non-null-object.mjs +55 -43
  17. package/dist/rules/prefer-is-non-null-object.mjs.map +1 -1
  18. package/dist/rules/prefer-is-record-and-has-key.d.mts.map +1 -1
  19. package/dist/rules/prefer-is-record-and-has-key.mjs +22 -17
  20. package/dist/rules/prefer-is-record-and-has-key.mjs.map +1 -1
  21. package/dist/rules/prefer-safe-array-is-array.d.mts.map +1 -1
  22. package/dist/rules/prefer-safe-array-is-array.mjs +23 -14
  23. package/dist/rules/prefer-safe-array-is-array.mjs.map +1 -1
  24. package/dist/rules/prefer-safe-array-length-guard.d.mts.map +1 -1
  25. package/dist/rules/prefer-safe-array-length-guard.mjs +19 -15
  26. package/dist/rules/prefer-safe-array-length-guard.mjs.map +1 -1
  27. package/dist/rules/prefer-safe-number-parse-integer.d.mts.map +1 -1
  28. package/dist/rules/prefer-safe-number-parse-integer.mjs +16 -17
  29. package/dist/rules/prefer-safe-number-parse-integer.mjs.map +1 -1
  30. package/dist/rules/prefer-safe-number-parse.d.mts.map +1 -1
  31. package/dist/rules/prefer-safe-number-parse.mjs +14 -16
  32. package/dist/rules/prefer-safe-number-parse.mjs.map +1 -1
  33. package/dist/types.d.mts +2 -2
  34. package/dist/types.d.mts.map +1 -1
  35. package/package.json +24 -22
  36. package/src/plugin.mts +5 -4
  37. package/src/plugin.test.mts +0 -13
  38. package/src/rule-types.mts +2 -2
  39. package/src/rules/ast-utils.mts +147 -0
  40. package/src/rules/import-utils.mts +4 -2
  41. package/src/rules/prefer-is-non-null-object.mts +63 -38
  42. package/src/rules/prefer-is-non-null-object.test.mts +54 -0
  43. package/src/rules/prefer-is-record-and-has-key.mts +34 -21
  44. package/src/rules/prefer-is-record-and-has-key.test.mts +128 -0
  45. package/src/rules/prefer-safe-array-is-array.mts +34 -16
  46. package/src/rules/prefer-safe-array-is-array.test.mts +93 -0
  47. package/src/rules/prefer-safe-array-length-guard.mts +32 -19
  48. package/src/rules/prefer-safe-array-length-guard.test.mts +98 -0
  49. package/src/rules/prefer-safe-number-parse-integer.mts +29 -23
  50. package/src/rules/prefer-safe-number-parse-integer.test.mts +104 -0
  51. package/src/rules/prefer-safe-number-parse.mts +25 -21
  52. package/src/rules/prefer-safe-number-parse.test.mts +97 -0
  53. package/src/rules/rules.mts +1 -1
  54. package/src/types.mts +2 -2
@@ -3,7 +3,12 @@ import {
3
3
  type TSESLint,
4
4
  type TSESTree,
5
5
  } from '@typescript-eslint/utils';
6
- import { type ReadonlyRecord } from 'ts-type-forge';
6
+ import type { ReadonlyRecord } from 'ts-type-forge';
7
+ import {
8
+ getArgumentText,
9
+ getValueType,
10
+ skipTypeWrappers,
11
+ } from './ast-utils.mjs';
7
12
  import {
8
13
  buildImportFixes,
9
14
  getNamedImports,
@@ -79,28 +84,28 @@ export const preferSafeArrayLengthGuard: TSESLint.RuleModule<
79
84
 
80
85
  const tsStdForgeImport = getTsStdForgeImport(program);
81
86
 
82
- const services = sourceCode.parserServices;
83
-
84
- const checker = services?.program?.getTypeChecker();
87
+ const checker = sourceCode.parserServices?.program?.getTypeChecker();
85
88
 
86
89
  /**
87
90
  * Whether `expression` is an array or tuple. Without type information the
88
91
  * answer is no: `.length` is also a string's, a function's and a
89
- * `TypedArray`'s, and none of those can be handed to the guard.
92
+ * `TypedArray`'s, and none of those can be handed to the guard. The type is
93
+ * the value's, not a cast's: a string cast to an array is still a string.
90
94
  */
91
95
  const isArrayLike = (
92
96
  // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
93
97
  expression: TSESTree.Expression,
94
98
  ): boolean => {
95
- if (checker === undefined) return false;
96
-
97
- const tsNode = services?.esTreeNodeToTSNodeMap?.get(expression);
98
-
99
- if (tsNode === undefined) return false;
99
+ if (checker === undefined) {
100
+ return false;
101
+ }
100
102
 
101
- const type = checker.getTypeAtLocation(tsNode);
103
+ const type = getValueType(sourceCode, expression);
102
104
 
103
- return checker.isArrayType(type) || checker.isTupleType(type);
105
+ return (
106
+ type !== undefined &&
107
+ (checker.isArrayType(type) || checker.isTupleType(type))
108
+ );
104
109
  };
105
110
 
106
111
  const mut_nodesToFix: {
@@ -113,9 +118,13 @@ export const preferSafeArrayLengthGuard: TSESLint.RuleModule<
113
118
  BinaryExpression: (node) => {
114
119
  const matched = matchLengthComparison(node);
115
120
 
116
- if (matched === undefined) return;
121
+ if (matched === undefined) {
122
+ return;
123
+ }
117
124
 
118
- if (!isArrayLike(matched.arrayExpression)) return;
125
+ if (!isArrayLike(matched.arrayExpression)) {
126
+ return;
127
+ }
119
128
 
120
129
  mut_nodesToFix.push({ node, ...matched });
121
130
  },
@@ -132,7 +141,7 @@ export const preferSafeArrayLengthGuard: TSESLint.RuleModule<
132
141
  index,
133
142
  { node, arrayExpression, guard },
134
143
  ] of mut_nodesToFix.entries()) {
135
- const arrayText = sourceCode.getText(arrayExpression);
144
+ const arrayText = getArgumentText(sourceCode, arrayExpression);
136
145
 
137
146
  context.report({
138
147
  node,
@@ -172,9 +181,11 @@ const matchLengthComparison = (
172
181
  ):
173
182
  | Readonly<{ arrayExpression: TSESTree.Expression; guard: Guard }>
174
183
  | undefined => {
175
- const { left, right } = node;
184
+ // Both operands are read through their type wrappers, which change neither
185
+ // the length nor the bound: `(xs.length as number) > (0 as const)`.
186
+ const left = skipTypeWrappers(node.left);
176
187
 
177
- if (left.type === AST_NODE_TYPES.PrivateIdentifier) return undefined;
188
+ const right = skipTypeWrappers(node.right);
178
189
 
179
190
  const lengthOnLeft = isLengthAccess(left);
180
191
 
@@ -182,7 +193,9 @@ const matchLengthComparison = (
182
193
 
183
194
  const boundSide = lengthOnLeft ? right : left;
184
195
 
185
- if (!isLengthAccess(lengthSide)) return undefined;
196
+ if (!isLengthAccess(lengthSide)) {
197
+ return undefined;
198
+ }
186
199
 
187
200
  // `xs.length === ys.length` has `.length` on both sides and no literal
188
201
  // bound, so the bound check below rejects it.
@@ -206,7 +219,7 @@ const matchLengthComparison = (
206
219
 
207
220
  const isLengthAccess = (
208
221
  // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
209
- node: TSESTree.Expression,
222
+ node: TSESTree.Node,
210
223
  ): node is TSESTree.MemberExpression =>
211
224
  node.type === AST_NODE_TYPES.MemberExpression &&
212
225
  !node.computed &&
@@ -150,3 +150,101 @@ describe('prefer-safe-array-length-guard', () => {
150
150
  ],
151
151
  });
152
152
  });
153
+
154
+ describe('prefer-safe-array-length-guard through type wrappers', () => {
155
+ tester.run('prefer-safe-array-length-guard', preferSafeArrayLengthGuard, {
156
+ valid: [
157
+ {
158
+ name: 'a string cast to an array is still a string',
159
+ code: dedent`
160
+ declare const s: string;
161
+ const ok = (s as unknown as readonly string[]).length > 0;
162
+ `,
163
+ },
164
+ {
165
+ name: 'a bound with no named guard behind `as const` is left alone',
166
+ code: dedent`
167
+ declare const xs: readonly number[];
168
+ const ok = xs.length > (2 as const);
169
+ `,
170
+ },
171
+ ],
172
+ invalid: [
173
+ {
174
+ name: 'reads the bound through `as const`',
175
+ code: dedent`
176
+ declare const xs: readonly number[];
177
+ const ok = xs.length > (0 as const);
178
+ `,
179
+ output: dedent`
180
+ import { SafeArray } from 'ts-std-forge';
181
+ declare const xs: readonly number[];
182
+ const ok = SafeArray.isNonEmpty(xs);
183
+ `,
184
+ errors: [{ messageId: 'useIsNonEmpty' }],
185
+ },
186
+ {
187
+ name: 'reads the length through `as`',
188
+ code: dedent`
189
+ declare const xs: readonly number[];
190
+ const ok = (xs.length as number) > 0;
191
+ `,
192
+ output: dedent`
193
+ import { SafeArray } from 'ts-std-forge';
194
+ declare const xs: readonly number[];
195
+ const ok = SafeArray.isNonEmpty(xs);
196
+ `,
197
+ errors: [{ messageId: 'useIsNonEmpty' }],
198
+ },
199
+ {
200
+ name: 'reads the length through `!`, with the bound on the left',
201
+ code: dedent`
202
+ declare const xs: readonly number[];
203
+ const ok = (0 satisfies number) === xs.length!;
204
+ `,
205
+ output: dedent`
206
+ import { SafeArray } from 'ts-std-forge';
207
+ declare const xs: readonly number[];
208
+ const ok = SafeArray.isEmpty(xs);
209
+ `,
210
+ errors: [{ messageId: 'useIsEmpty' }],
211
+ },
212
+ {
213
+ name: 'keeps a wrapper on the array and reads `!` as the array it asserts',
214
+ code: dedent`
215
+ declare const xs: readonly number[] | undefined;
216
+ const ok = xs!.length !== 0;
217
+ `,
218
+ output: dedent`
219
+ import { SafeArray } from 'ts-std-forge';
220
+ declare const xs: readonly number[] | undefined;
221
+ const ok = SafeArray.isNonEmpty(xs!);
222
+ `,
223
+ errors: [{ messageId: 'useIsNonEmpty' }],
224
+ },
225
+ ],
226
+ });
227
+ });
228
+
229
+ describe('prefer-safe-array-length-guard with parenthesized operands', () => {
230
+ tester.run('prefer-safe-array-length-guard', preferSafeArrayLengthGuard, {
231
+ valid: [],
232
+ invalid: [
233
+ {
234
+ name: 'keeps the parentheses of a sequence array',
235
+ code: dedent`
236
+ declare const a: number;
237
+ declare const xs: readonly number[];
238
+ const ok = (a, xs).length > 0;
239
+ `,
240
+ output: dedent`
241
+ import { SafeArray } from 'ts-std-forge';
242
+ declare const a: number;
243
+ declare const xs: readonly number[];
244
+ const ok = SafeArray.isNonEmpty((a, xs));
245
+ `,
246
+ errors: [{ messageId: 'useIsNonEmpty' }],
247
+ },
248
+ ],
249
+ });
250
+ });
@@ -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,
@@ -62,8 +68,6 @@ export const preferSafeNumberParseInteger: TSESLint.RuleModule<
62
68
 
63
69
  const tsStdForgeImport = getTsStdForgeImport(program);
64
70
 
65
- const services = sourceCode.parserServices;
66
-
67
71
  const mut_nodesToFix: {
68
72
  node: TSESTree.CallExpression;
69
73
  argExpression: TSESTree.Expression;
@@ -71,7 +75,7 @@ export const preferSafeNumberParseInteger: TSESLint.RuleModule<
71
75
 
72
76
  return {
73
77
  CallExpression: (node) => {
74
- const { callee } = node;
78
+ const callee = skipTypeWrappers(node.callee);
75
79
 
76
80
  // Match `parseInt(...)` or `Number.parseInt(...)`.
77
81
  const isGlobalParseInt =
@@ -81,25 +85,32 @@ export const preferSafeNumberParseInteger: TSESLint.RuleModule<
81
85
  const isNumberParseInt =
82
86
  callee.type === AST_NODE_TYPES.MemberExpression &&
83
87
  !callee.computed &&
84
- callee.object.type === AST_NODE_TYPES.Identifier &&
85
- callee.object.name === 'Number' &&
88
+ isIdentifierNamed(callee.object, 'Number') &&
86
89
  callee.property.type === AST_NODE_TYPES.Identifier &&
87
90
  callee.property.name === 'parseInt';
88
91
 
89
- if (!isGlobalParseInt && !isNumberParseInt) return;
92
+ if (!isGlobalParseInt && !isNumberParseInt) {
93
+ return;
94
+ }
90
95
 
91
96
  const args = node.arguments;
92
97
 
93
98
  const firstArg = args[0];
94
99
 
95
- if (firstArg === undefined) return;
100
+ if (firstArg === undefined) {
101
+ return;
102
+ }
96
103
 
97
104
  // Spread argument (`parseInt(...rest)`) cannot be rewritten safely.
98
- if (firstArg.type === AST_NODE_TYPES.SpreadElement) return;
105
+ if (firstArg.type === AST_NODE_TYPES.SpreadElement) {
106
+ return;
107
+ }
99
108
 
100
- // Only base 10: accept a missing radix or an explicit literal `10`.
109
+ // Only base 10: accept a missing radix or an explicit literal `10`,
110
+ // read through its type wrappers, which leave the value as it is.
101
111
  if (args.length >= 2) {
102
- const radix = args[1];
112
+ const radix =
113
+ args[1] === undefined ? undefined : skipTypeWrappers(args[1]);
103
114
 
104
115
  if (radix?.type !== AST_NODE_TYPES.Literal || radix.value !== 10) {
105
116
  return;
@@ -107,19 +118,14 @@ export const preferSafeNumberParseInteger: TSESLint.RuleModule<
107
118
  }
108
119
 
109
120
  // The argument must be a `string` so the autofix is type-safe against
110
- // `SafeNumber.parseInteger(value: string)`. Without type information,
111
- // skip.
112
- if (services?.program == null) return;
121
+ // `SafeNumber.parseInteger(value: string)`, and that of the value, not
122
+ // of a cast: `parseInt(1e21 as unknown as string)` is 1, the rewrite
123
+ // 1e21. Without type information, skip.
124
+ const argType = getValueType(sourceCode, firstArg);
113
125
 
114
- const checker = services.program.getTypeChecker();
115
-
116
- const tsNode = services.esTreeNodeToTSNodeMap?.get(firstArg);
117
-
118
- if (tsNode === undefined) return;
119
-
120
- const argType = checker.getTypeAtLocation(tsNode);
121
-
122
- if (!isStringType(argType)) return;
126
+ if (argType === undefined || !isStringType(argType)) {
127
+ return;
128
+ }
123
129
 
124
130
  mut_nodesToFix.push({ node, argExpression: firstArg });
125
131
  },
@@ -134,7 +140,7 @@ export const preferSafeNumberParseInteger: TSESLint.RuleModule<
134
140
  index,
135
141
  { node, argExpression },
136
142
  ] of mut_nodesToFix.entries()) {
137
- const argText = sourceCode.getText(argExpression);
143
+ const argText = getArgumentText(sourceCode, argExpression);
138
144
 
139
145
  context.report({
140
146
  node,
@@ -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 { callee } = node;
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.type === AST_NODE_TYPES.Identifier &&
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
 
@@ -94,31 +97,32 @@ export const preferSafeNumberParse: TSESLint.RuleModule<MessageIds, Options> = {
94
97
  const isNumberCall =
95
98
  callee.type === AST_NODE_TYPES.Identifier && callee.name === 'Number';
96
99
 
97
- if (!isGlobalParseFloat && !isNumberParseFloat && !isNumberCall) return;
100
+ if (!isGlobalParseFloat && !isNumberParseFloat && !isNumberCall) {
101
+ return;
102
+ }
98
103
 
99
104
  const args = node.arguments;
100
105
 
101
106
  const firstArg = args[0];
102
107
 
103
- if (firstArg === undefined) return;
108
+ if (firstArg === undefined) {
109
+ return;
110
+ }
104
111
 
105
112
  // Spread argument (`parseFloat(...rest)`) cannot be rewritten safely.
106
- if (firstArg.type === AST_NODE_TYPES.SpreadElement) return;
113
+ if (firstArg.type === AST_NODE_TYPES.SpreadElement) {
114
+ return;
115
+ }
107
116
 
108
117
  // The argument must be purely `string` so the autofix is type-safe
109
- // against `SafeNumber.parse(value: string)`. Without type information,
110
- // skip.
111
- if (services?.program == null) return;
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);
112
122
 
113
- const checker = services.program.getTypeChecker();
114
-
115
- const tsNode = services.esTreeNodeToTSNodeMap?.get(firstArg);
116
-
117
- if (tsNode === undefined) return;
118
-
119
- const argType = checker.getTypeAtLocation(tsNode);
120
-
121
- if (!isStringType(argType)) return;
123
+ if (argType === undefined || !isStringType(argType)) {
124
+ return;
125
+ }
122
126
 
123
127
  mut_nodesToFix.push({ node, argExpression: firstArg });
124
128
  },
@@ -133,7 +137,7 @@ export const preferSafeNumberParse: TSESLint.RuleModule<MessageIds, Options> = {
133
137
  index,
134
138
  { node, argExpression },
135
139
  ] of mut_nodesToFix.entries()) {
136
- const argText = sourceCode.getText(argExpression);
140
+ const argText = getArgumentText(sourceCode, argExpression);
137
141
 
138
142
  context.report({
139
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
+ });
@@ -1,4 +1,4 @@
1
- import { type ESLintPlugin } from '../types.mjs';
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 { type TSESLint } from '@typescript-eslint/utils';
2
- import { type DeepReadonly } from 'ts-type-forge';
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