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.
Files changed (54) hide show
  1. package/dist/index.d.mts +1 -1
  2. package/dist/index.d.mts.map +1 -1
  3. package/dist/plugin.d.mts +1 -1
  4. package/dist/plugin.d.mts.map +1 -1
  5. package/dist/plugin.mjs +1 -0
  6. package/dist/plugin.mjs.map +1 -1
  7. package/dist/rule-types.d.mts +1 -1
  8. package/dist/rule-types.d.mts.map +1 -1
  9. package/dist/rules/ast-utils.d.mts +41 -0
  10. package/dist/rules/ast-utils.d.mts.map +1 -0
  11. package/dist/rules/ast-utils.mjs +101 -0
  12. package/dist/rules/ast-utils.mjs.map +1 -0
  13. package/dist/rules/import-utils.d.mts +1 -1
  14. package/dist/rules/import-utils.d.mts.map +1 -1
  15. package/dist/rules/prefer-is-non-null-object.mjs +26 -12
  16. package/dist/rules/prefer-is-non-null-object.mjs.map +1 -1
  17. package/dist/rules/prefer-is-record-and-has-key.d.mts.map +1 -1
  18. package/dist/rules/prefer-is-record-and-has-key.mjs +12 -12
  19. package/dist/rules/prefer-is-record-and-has-key.mjs.map +1 -1
  20. package/dist/rules/prefer-safe-array-is-array.d.mts.map +1 -1
  21. package/dist/rules/prefer-safe-array-is-array.mjs +15 -13
  22. package/dist/rules/prefer-safe-array-is-array.mjs.map +1 -1
  23. package/dist/rules/prefer-safe-array-length-guard.d.mts.map +1 -1
  24. package/dist/rules/prefer-safe-array-length-guard.mjs +11 -13
  25. package/dist/rules/prefer-safe-array-length-guard.mjs.map +1 -1
  26. package/dist/rules/prefer-safe-number-parse-integer.d.mts.map +1 -1
  27. package/dist/rules/prefer-safe-number-parse-integer.mjs +9 -16
  28. package/dist/rules/prefer-safe-number-parse-integer.mjs.map +1 -1
  29. package/dist/rules/prefer-safe-number-parse.d.mts.map +1 -1
  30. package/dist/rules/prefer-safe-number-parse.mjs +7 -15
  31. package/dist/rules/prefer-safe-number-parse.mjs.map +1 -1
  32. package/dist/types.d.mts +2 -2
  33. package/dist/types.d.mts.map +1 -1
  34. package/package.json +7 -7
  35. package/src/index.mts +1 -1
  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 +1 -1
  41. package/src/rules/prefer-is-non-null-object.mts +34 -13
  42. package/src/rules/prefer-is-non-null-object.test.mts +54 -0
  43. package/src/rules/prefer-is-record-and-has-key.mts +19 -16
  44. package/src/rules/prefer-is-record-and-has-key.test.mts +128 -0
  45. package/src/rules/prefer-safe-array-is-array.mts +22 -18
  46. package/src/rules/prefer-safe-array-is-array.test.mts +93 -0
  47. package/src/rules/prefer-safe-array-length-guard.mts +20 -19
  48. package/src/rules/prefer-safe-array-length-guard.test.mts +98 -0
  49. package/src/rules/prefer-safe-number-parse-integer.mts +18 -24
  50. package/src/rules/prefer-safe-number-parse-integer.test.mts +104 -0
  51. package/src/rules/prefer-safe-number-parse.mts +14 -22
  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
@@ -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
 
@@ -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)`. Without type information,
116
- // skip.
117
- if (services?.program == null) {
118
- return;
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.getText(argExpression);
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
+ });
@@ -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