eslint-plugin-ts-std-forge 0.1.7 → 0.1.9

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 (44) hide show
  1. package/dist/rule-types.d.mts +2 -1
  2. package/dist/rule-types.d.mts.map +1 -1
  3. package/dist/rule-types.mjs.map +1 -1
  4. package/dist/rules/ast-utils.d.mts +8 -31
  5. package/dist/rules/ast-utils.d.mts.map +1 -1
  6. package/dist/rules/ast-utils.mjs +13 -75
  7. package/dist/rules/ast-utils.mjs.map +1 -1
  8. package/dist/rules/prefer-is-non-null-object.d.mts.map +1 -1
  9. package/dist/rules/prefer-is-non-null-object.mjs +7 -5
  10. package/dist/rules/prefer-is-non-null-object.mjs.map +1 -1
  11. package/dist/rules/prefer-is-record-and-has-key.d.mts.map +1 -1
  12. package/dist/rules/prefer-is-record-and-has-key.mjs +9 -13
  13. package/dist/rules/prefer-is-record-and-has-key.mjs.map +1 -1
  14. package/dist/rules/prefer-safe-array-is-array.d.mts.map +1 -1
  15. package/dist/rules/prefer-safe-array-is-array.mjs +8 -5
  16. package/dist/rules/prefer-safe-array-is-array.mjs.map +1 -1
  17. package/dist/rules/prefer-safe-array-length-guard.d.mts.map +1 -1
  18. package/dist/rules/prefer-safe-array-length-guard.mjs +11 -14
  19. package/dist/rules/prefer-safe-array-length-guard.mjs.map +1 -1
  20. package/dist/rules/prefer-safe-number-parse-integer.d.mts.map +1 -1
  21. package/dist/rules/prefer-safe-number-parse-integer.mjs +9 -7
  22. package/dist/rules/prefer-safe-number-parse-integer.mjs.map +1 -1
  23. package/dist/rules/prefer-safe-number-parse.d.mts.map +1 -1
  24. package/dist/rules/prefer-safe-number-parse.mjs +9 -7
  25. package/dist/rules/prefer-safe-number-parse.mjs.map +1 -1
  26. package/package.json +5 -4
  27. package/src/rule-types.mts +2 -1
  28. package/src/rules/ast-utils.mts +20 -110
  29. package/src/rules/prefer-is-non-null-object.mts +13 -7
  30. package/src/rules/prefer-is-record-and-has-key.mts +28 -27
  31. package/src/rules/prefer-is-record-and-has-key.test.mts +30 -3
  32. package/src/rules/prefer-safe-array-is-array.mts +14 -10
  33. package/src/rules/prefer-safe-array-is-array.test.mts +14 -0
  34. package/src/rules/prefer-safe-array-length-guard.mts +25 -26
  35. package/src/rules/prefer-safe-array-length-guard.test.mts +19 -5
  36. package/src/rules/prefer-safe-number-parse-integer.mts +25 -19
  37. package/src/rules/prefer-safe-number-parse-integer.test.mts +19 -5
  38. package/src/rules/prefer-safe-number-parse.mts +25 -19
  39. package/src/rules/prefer-safe-number-parse.test.mts +19 -5
  40. package/dist/rules/import-utils.d.mts +0 -6
  41. package/dist/rules/import-utils.d.mts.map +0 -1
  42. package/dist/rules/import-utils.mjs +0 -28
  43. package/dist/rules/import-utils.mjs.map +0 -1
  44. package/src/rules/import-utils.mts +0 -58
@@ -168,6 +168,20 @@ describe('prefer-safe-array-length-guard through type wrappers', () => {
168
168
  const ok = xs.length > (2 as const);
169
169
  `,
170
170
  },
171
+ {
172
+ name: 'an array widened by `as` to one that may be a string',
173
+ code: dedent`
174
+ declare const xs: readonly string[];
175
+ const ok = (xs as readonly string[] | string).length > 0;
176
+ `,
177
+ },
178
+ {
179
+ name: '`!` on an optional array, which leaves undefined in place',
180
+ code: dedent`
181
+ declare const xs: readonly number[] | undefined;
182
+ const ok = xs!.length !== 0;
183
+ `,
184
+ },
171
185
  ],
172
186
  invalid: [
173
187
  {
@@ -210,15 +224,15 @@ describe('prefer-safe-array-length-guard through type wrappers', () => {
210
224
  errors: [{ messageId: 'useIsEmpty' }],
211
225
  },
212
226
  {
213
- name: 'keeps a wrapper on the array and reads `!` as the array it asserts',
227
+ name: 'keeps a wrapper on an array that is an array at every layer',
214
228
  code: dedent`
215
- declare const xs: readonly number[] | undefined;
216
- const ok = xs!.length !== 0;
229
+ declare const xs: readonly number[];
230
+ const ok = (xs! as readonly number[]).length !== 0;
217
231
  `,
218
232
  output: dedent`
219
233
  import { SafeArray } from 'ts-std-forge';
220
- declare const xs: readonly number[] | undefined;
221
- const ok = SafeArray.isNonEmpty(xs!);
234
+ declare const xs: readonly number[];
235
+ const ok = SafeArray.isNonEmpty(xs! as readonly number[]);
222
236
  `,
223
237
  errors: [{ messageId: 'useIsNonEmpty' }],
224
238
  },
@@ -3,18 +3,16 @@ import {
3
3
  type TSESLint,
4
4
  type TSESTree,
5
5
  } from '@typescript-eslint/utils';
6
- import * as ts from 'typescript';
7
6
  import {
7
+ buildNamedImportFix,
8
8
  getArgumentText,
9
- getValueType,
10
- isIdentifierNamed,
11
- skipTypeWrappers,
12
- } from './ast-utils.mjs';
13
- import {
14
- buildImportFixes,
9
+ getImportDeclarations,
15
10
  getNamedImports,
16
- getTsStdForgeImport,
17
- } from './import-utils.mjs';
11
+ skipTypeWrappers,
12
+ } from 'eslint-rule-forge';
13
+ import * as ts from 'typescript';
14
+ import { isIdentifierNamed, typeHoldsAtEveryLayer } from './ast-utils.mjs';
15
+ import { TS_STD_FORGE_MODULE } from './constants.mjs';
18
16
 
19
17
  type Options = readonly [];
20
18
 
@@ -66,7 +64,10 @@ export const preferSafeNumberParseInteger: TSESLint.RuleModule<
66
64
 
67
65
  const program = sourceCode.ast;
68
66
 
69
- const tsStdForgeImport = getTsStdForgeImport(program);
67
+ const tsStdForgeImports = getImportDeclarations(
68
+ program,
69
+ TS_STD_FORGE_MODULE,
70
+ );
70
71
 
71
72
  const mut_nodesToFix: {
72
73
  node: TSESTree.CallExpression;
@@ -117,20 +118,20 @@ export const preferSafeNumberParseInteger: TSESLint.RuleModule<
117
118
  }
118
119
  }
119
120
 
120
- // The argument must be a `string` so the autofix is type-safe against
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);
125
-
126
- if (argType === undefined || !isStringType(argType)) {
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)) {
127
128
  return;
128
129
  }
129
130
 
130
131
  mut_nodesToFix.push({ node, argExpression: firstArg });
131
132
  },
132
133
  'Program:exit': () => {
133
- const namedImports = getNamedImports(tsStdForgeImport);
134
+ const namedImports = getNamedImports(tsStdForgeImports);
134
135
 
135
136
  const missingImports = (['Result', 'SafeNumber'] as const).filter(
136
137
  (name) => !namedImports.includes(name),
@@ -154,7 +155,12 @@ export const preferSafeNumberParseInteger: TSESLint.RuleModule<
154
155
 
155
156
  const importFixes =
156
157
  index === 0 && missingImports.length > 0
157
- ? buildImportFixes(fixer, program, missingImports)
158
+ ? buildNamedImportFix(
159
+ fixer,
160
+ program,
161
+ TS_STD_FORGE_MODULE,
162
+ missingImports,
163
+ )
158
164
  : [];
159
165
 
160
166
  return [...importFixes, fixer.replaceText(node, core)];
@@ -84,6 +84,20 @@ describe('prefer-safe-number-parse-integer through type wrappers', () => {
84
84
  const n = parseInt(s, 16 as const);
85
85
  `,
86
86
  },
87
+ {
88
+ name: 'ignores a string widened by `as` to one that may be a number',
89
+ code: dedent`
90
+ declare const s: string;
91
+ const n = parseInt(s as string | number as string);
92
+ `,
93
+ },
94
+ {
95
+ name: 'ignores `!` on an optional string, which leaves undefined in place',
96
+ code: dedent`
97
+ declare const s: string | undefined;
98
+ const n = parseInt(s!);
99
+ `,
100
+ },
87
101
  ],
88
102
  invalid: [
89
103
  {
@@ -131,15 +145,15 @@ describe('prefer-safe-number-parse-integer through type wrappers', () => {
131
145
  ],
132
146
  },
133
147
  {
134
- name: 'reads `!` on an optional string as the string it asserts',
148
+ name: 'rewrites a string that is a string at every layer',
135
149
  code: dedent`
136
- declare const s: string | undefined;
137
- const n = parseInt(s!);
150
+ declare const s: string;
151
+ const n = parseInt(s! as string);
138
152
  `,
139
153
  output: dedent`
140
154
  import { Result, SafeNumber } from 'ts-std-forge';
141
- declare const s: string | undefined;
142
- const n = Result.unwrapOkOr(SafeNumber.parseInteger(s!), Number.NaN);
155
+ declare const s: string;
156
+ const n = Result.unwrapOkOr(SafeNumber.parseInteger(s! as string), Number.NaN);
143
157
  `,
144
158
  errors: [{ messageId: 'useSafeNumberParseInteger' }],
145
159
  },
@@ -3,18 +3,16 @@ import {
3
3
  type TSESLint,
4
4
  type TSESTree,
5
5
  } from '@typescript-eslint/utils';
6
- import * as ts from 'typescript';
7
6
  import {
7
+ buildNamedImportFix,
8
8
  getArgumentText,
9
- getValueType,
10
- isIdentifierNamed,
11
- skipTypeWrappers,
12
- } from './ast-utils.mjs';
13
- import {
14
- buildImportFixes,
9
+ getImportDeclarations,
15
10
  getNamedImports,
16
- getTsStdForgeImport,
17
- } from './import-utils.mjs';
11
+ skipTypeWrappers,
12
+ } from 'eslint-rule-forge';
13
+ import * as ts from 'typescript';
14
+ import { isIdentifierNamed, typeHoldsAtEveryLayer } from './ast-utils.mjs';
15
+ import { TS_STD_FORGE_MODULE } from './constants.mjs';
18
16
 
19
17
  type Options = readonly [];
20
18
 
@@ -69,7 +67,10 @@ export const preferSafeNumberParse: TSESLint.RuleModule<MessageIds, Options> = {
69
67
 
70
68
  const program = sourceCode.ast;
71
69
 
72
- const tsStdForgeImport = getTsStdForgeImport(program);
70
+ const tsStdForgeImports = getImportDeclarations(
71
+ program,
72
+ TS_STD_FORGE_MODULE,
73
+ );
73
74
 
74
75
  const mut_nodesToFix: {
75
76
  node: TSESTree.CallExpression;
@@ -114,20 +115,20 @@ export const preferSafeNumberParse: TSESLint.RuleModule<MessageIds, Options> = {
114
115
  return;
115
116
  }
116
117
 
117
- // The argument must be purely `string` so the autofix is type-safe
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);
122
-
123
- if (argType === undefined || !isStringType(argType)) {
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)) {
124
125
  return;
125
126
  }
126
127
 
127
128
  mut_nodesToFix.push({ node, argExpression: firstArg });
128
129
  },
129
130
  'Program:exit': () => {
130
- const namedImports = getNamedImports(tsStdForgeImport);
131
+ const namedImports = getNamedImports(tsStdForgeImports);
131
132
 
132
133
  const missingImports = (['Result', 'SafeNumber'] as const).filter(
133
134
  (name) => !namedImports.includes(name),
@@ -151,7 +152,12 @@ export const preferSafeNumberParse: TSESLint.RuleModule<MessageIds, Options> = {
151
152
 
152
153
  const importFixes =
153
154
  index === 0 && missingImports.length > 0
154
- ? buildImportFixes(fixer, program, missingImports)
155
+ ? buildNamedImportFix(
156
+ fixer,
157
+ program,
158
+ TS_STD_FORGE_MODULE,
159
+ missingImports,
160
+ )
155
161
  : [];
156
162
 
157
163
  return [...importFixes, fixer.replaceText(node, core)];
@@ -132,6 +132,20 @@ describe('prefer-safe-number-parse through type wrappers', () => {
132
132
  const n = parseFloat(x! as string);
133
133
  `,
134
134
  },
135
+ {
136
+ name: 'ignores a string widened by `as` to a type Number() reads otherwise',
137
+ code: dedent`
138
+ declare const s: string;
139
+ const n = Number(s as string | boolean);
140
+ `,
141
+ },
142
+ {
143
+ name: 'ignores `!` on an optional string, which leaves undefined in place',
144
+ code: dedent`
145
+ declare const s: string | undefined;
146
+ const n = parseFloat(s!);
147
+ `,
148
+ },
135
149
  ],
136
150
  invalid: [
137
151
  {
@@ -166,15 +180,15 @@ describe('prefer-safe-number-parse through type wrappers', () => {
166
180
  ],
167
181
  },
168
182
  {
169
- name: 'reads `!` on an optional string as the string it asserts',
183
+ name: 'rewrites a string that is a string at every layer',
170
184
  code: dedent`
171
- declare const s: string | undefined;
172
- const n = parseFloat(s!);
185
+ declare const s: string;
186
+ const n = parseFloat(s! as string);
173
187
  `,
174
188
  output: dedent`
175
189
  import { Result, SafeNumber } from 'ts-std-forge';
176
- declare const s: string | undefined;
177
- const n = Result.unwrapOkOr(SafeNumber.parse(s!), Number.NaN);
190
+ declare const s: string;
191
+ const n = Result.unwrapOkOr(SafeNumber.parse(s! as string), Number.NaN);
178
192
  `,
179
193
  errors: [{ messageId: 'useSafeNumberParse' }],
180
194
  },
@@ -1,6 +0,0 @@
1
- import { type TSESLint, type TSESTree } from '@typescript-eslint/utils';
2
- import type { DeepReadonly } from 'ts-type-forge';
3
- export declare const getTsStdForgeImport: (program: TSESTree.Program) => TSESTree.ImportDeclaration | undefined;
4
- export declare const getNamedImports: (node: DeepReadonly<TSESTree.ImportDeclaration> | undefined) => readonly string[];
5
- export declare const buildImportFixes: (fixer: TSESLint.RuleFixer, program: TSESTree.Program, requiredNames: readonly string[]) => readonly TSESLint.RuleFix[];
6
- //# sourceMappingURL=import-utils.d.mts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"import-utils.d.mts","sourceRoot":"","sources":["../../src/rules/import-utils.mts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,QAAQ,EACb,KAAK,QAAQ,EACd,MAAM,0BAA0B,CAAC;AAClC,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAKlD,eAAO,MAAM,mBAAmB,YACrB,QAAQ,CAAC,OAAO,KACxB,QAAQ,CAAC,iBAAiB,GAAG,SAK7B,CAAC;AAEJ,eAAO,MAAM,eAAe,SACpB,YAAY,CAAC,QAAQ,CAAC,iBAAiB,CAAC,GAAG,SAAS,KACzD,SAAS,MAAM,EAiBjB,CAAC;AAEF,eAAO,MAAM,gBAAgB,UACpB,QAAQ,CAAC,SAAS,WAChB,QAAQ,CAAC,OAAO,iBACV,SAAS,MAAM,EAAE,KAC/B,SAAS,QAAQ,CAAC,OAAO,EAa3B,CAAC"}
@@ -1,28 +0,0 @@
1
- import { AST_NODE_TYPES, } from '@typescript-eslint/utils';
2
- import { TS_STD_FORGE_MODULE } from './constants.mjs';
3
-
4
- export const getTsStdForgeImport = (program) => program.body.find((node) => node.type === AST_NODE_TYPES.ImportDeclaration &&
5
- node.source.value === TS_STD_FORGE_MODULE);
6
- export const getNamedImports = (node) => {
7
- if (node === undefined) {
8
- return [];
9
- }
10
- return node.specifiers.flatMap((specifier) => specifier.type === AST_NODE_TYPES.ImportSpecifier
11
- ? (() => {
12
- const importedName = specifier.imported.type === AST_NODE_TYPES.Identifier
13
- ? specifier.imported.name
14
- : specifier.imported.value;
15
- return typeof importedName === 'string' ? [importedName] : [];
16
- })()
17
- : []);
18
- };
19
- export const buildImportFixes = (fixer, program, requiredNames) => {
20
- const specifierText = requiredNames.join(', ');
21
- const importStatement = `import { ${specifierText} } from '${TS_STD_FORGE_MODULE}';`;
22
- const newLine = '\n';
23
- const insertionText = `${importStatement}${newLine}`;
24
-
25
-
26
- return [fixer.insertTextBefore(program, insertionText)];
27
- };
28
- //# sourceMappingURL=import-utils.mjs.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"import-utils.mjs","sourceRoot":"","sources":["../../src/rules/import-utils.mts"],"names":[],"mappings":"AAAA,OAAO,EACL,cAAc,GAGf,MAAM,0BAA0B,CAAC;AAElC,OAAO,EAAE,mBAAmB,EAAE,MAAM,iBAAiB,CAAC;AAEtD,uEAAuE;AAEvE,MAAM,CAAC,MAAM,mBAAmB,GAAG,CACjC,OAAyB,EACe,EAAE,CAC1C,OAAO,CAAC,IAAI,CAAC,IAAI,CACf,CAAC,IAAI,EAAsC,EAAE,CAC3C,IAAI,CAAC,IAAI,KAAK,cAAc,CAAC,iBAAiB;IAC9C,IAAI,CAAC,MAAM,CAAC,KAAK,KAAK,mBAAmB,CAC5C,CAAC;AAEJ,MAAM,CAAC,MAAM,eAAe,GAAG,CAC7B,IAA0D,EACvC,EAAE;IACrB,IAAI,IAAI,KAAK,SAAS,EAAE,CAAC;QACvB,OAAO,EAAE,CAAC;IACZ,CAAC;IAED,OAAO,IAAI,CAAC,UAAU,CAAC,OAAO,CAAC,CAAC,SAAS,EAAE,EAAE,CAC3C,SAAS,CAAC,IAAI,KAAK,cAAc,CAAC,eAAe;QAC/C,CAAC,CAAC,CAAC,GAAG,EAAE;YACJ,MAAM,YAAY,GAChB,SAAS,CAAC,QAAQ,CAAC,IAAI,KAAK,cAAc,CAAC,UAAU;gBACnD,CAAC,CAAC,SAAS,CAAC,QAAQ,CAAC,IAAI;gBACzB,CAAC,CAAC,SAAS,CAAC,QAAQ,CAAC,KAAK,CAAC;YAE/B,OAAO,OAAO,YAAY,KAAK,QAAQ,CAAC,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;QAChE,CAAC,CAAC,EAAE;QACN,CAAC,CAAC,EAAE,CACP,CAAC;AACJ,CAAC,CAAC;AAEF,MAAM,CAAC,MAAM,gBAAgB,GAAG,CAC9B,KAAyB,EACzB,OAAyB,EACzB,aAAgC,EACH,EAAE;IAC/B,MAAM,aAAa,GAAG,aAAa,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAE/C,MAAM,eAAe,GACnB,YAAY,aAAa,YAAY,mBAAmB,IAAa,CAAC;IAExE,MAAM,OAAO,GAAG,IAAI,CAAC;IAErB,MAAM,aAAa,GAAG,GAAG,eAAe,GAAG,OAAO,EAAW,CAAC;IAE9D,6CAA6C;IAC7C,uDAAuD;IACvD,OAAO,CAAC,KAAK,CAAC,gBAAgB,CAAC,OAAO,EAAE,aAAa,CAAC,CAAC,CAAC;AAC1D,CAAC,CAAC"}
@@ -1,58 +0,0 @@
1
- import {
2
- AST_NODE_TYPES,
3
- type TSESLint,
4
- type TSESTree,
5
- } from '@typescript-eslint/utils';
6
- import type { DeepReadonly } from 'ts-type-forge';
7
- import { TS_STD_FORGE_MODULE } from './constants.mjs';
8
-
9
- /* eslint-disable @typescript-eslint/prefer-readonly-parameter-types */
10
-
11
- export const getTsStdForgeImport = (
12
- program: TSESTree.Program,
13
- ): TSESTree.ImportDeclaration | undefined =>
14
- program.body.find(
15
- (node): node is TSESTree.ImportDeclaration =>
16
- node.type === AST_NODE_TYPES.ImportDeclaration &&
17
- node.source.value === TS_STD_FORGE_MODULE,
18
- );
19
-
20
- export const getNamedImports = (
21
- node: DeepReadonly<TSESTree.ImportDeclaration> | undefined,
22
- ): readonly string[] => {
23
- if (node === undefined) {
24
- return [];
25
- }
26
-
27
- return node.specifiers.flatMap((specifier) =>
28
- specifier.type === AST_NODE_TYPES.ImportSpecifier
29
- ? (() => {
30
- const importedName =
31
- specifier.imported.type === AST_NODE_TYPES.Identifier
32
- ? specifier.imported.name
33
- : specifier.imported.value;
34
-
35
- return typeof importedName === 'string' ? [importedName] : [];
36
- })()
37
- : [],
38
- );
39
- };
40
-
41
- export const buildImportFixes = (
42
- fixer: TSESLint.RuleFixer,
43
- program: TSESTree.Program,
44
- requiredNames: readonly string[],
45
- ): readonly TSESLint.RuleFix[] => {
46
- const specifierText = requiredNames.join(', ');
47
-
48
- const importStatement =
49
- `import { ${specifierText} } from '${TS_STD_FORGE_MODULE}';` as const;
50
-
51
- const newLine = '\n';
52
-
53
- const insertionText = `${importStatement}${newLine}` as const;
54
-
55
- // Always insert at the beginning of the file
56
- // (organize-imports will handle merging/deduplication)
57
- return [fixer.insertTextBefore(program, insertionText)];
58
- };