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
|
@@ -5,6 +5,12 @@ import {
|
|
|
5
5
|
} from '@typescript-eslint/utils';
|
|
6
6
|
import { SafeArray } from 'ts-std-forge';
|
|
7
7
|
import * as ts from 'typescript';
|
|
8
|
+
import {
|
|
9
|
+
getArgumentText,
|
|
10
|
+
getValueType,
|
|
11
|
+
isIdentifierNamed,
|
|
12
|
+
skipTypeWrappers,
|
|
13
|
+
} from './ast-utils.mjs';
|
|
8
14
|
import {
|
|
9
15
|
buildImportFixes,
|
|
10
16
|
getNamedImports,
|
|
@@ -40,15 +46,14 @@ export const preferIsRecordAndHasKey: TSESLint.RuleModule<MessageIds, Options> =
|
|
|
40
46
|
|
|
41
47
|
const tsStdForgeImport = getTsStdForgeImport(program);
|
|
42
48
|
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
const checker = services?.program?.getTypeChecker();
|
|
49
|
+
const checker = sourceCode.parserServices?.program?.getTypeChecker();
|
|
46
50
|
|
|
47
51
|
/**
|
|
48
52
|
* Whether the `isRecord(...)` half of the rewrite would be dead code
|
|
49
53
|
* because the object already satisfies `hasKey`'s
|
|
50
54
|
* `R extends UnknownRecord` constraint. Without type information nothing
|
|
51
|
-
* is known, so the guard is kept.
|
|
55
|
+
* is known, so the guard is kept. The type is the value's, not a cast's:
|
|
56
|
+
* an array cast to a record is still rejected by `isRecord`.
|
|
52
57
|
*/
|
|
53
58
|
const isKnownRecord = (
|
|
54
59
|
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
@@ -58,12 +63,9 @@ export const preferIsRecordAndHasKey: TSESLint.RuleModule<MessageIds, Options> =
|
|
|
58
63
|
return false;
|
|
59
64
|
}
|
|
60
65
|
|
|
61
|
-
const
|
|
66
|
+
const type = getValueType(sourceCode, expression);
|
|
62
67
|
|
|
63
|
-
return (
|
|
64
|
-
tsNode !== undefined &&
|
|
65
|
-
isAlreadyRecordType(checker, checker.getTypeAtLocation(tsNode))
|
|
66
|
-
);
|
|
68
|
+
return type !== undefined && isAlreadyRecordType(checker, type);
|
|
67
69
|
};
|
|
68
70
|
|
|
69
71
|
const mut_nodesToFix: {
|
|
@@ -75,12 +77,13 @@ export const preferIsRecordAndHasKey: TSESLint.RuleModule<MessageIds, Options> =
|
|
|
75
77
|
return {
|
|
76
78
|
// Handle Object.hasOwn(obj, key)
|
|
77
79
|
CallExpression: (node) => {
|
|
80
|
+
const callee = skipTypeWrappers(node.callee);
|
|
81
|
+
|
|
78
82
|
if (
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
node.callee.property.name !== 'hasOwn'
|
|
83
|
+
callee.type !== AST_NODE_TYPES.MemberExpression ||
|
|
84
|
+
!isIdentifierNamed(callee.object, 'Object') ||
|
|
85
|
+
callee.property.type !== AST_NODE_TYPES.Identifier ||
|
|
86
|
+
callee.property.name !== 'hasOwn'
|
|
84
87
|
) {
|
|
85
88
|
return;
|
|
86
89
|
}
|
|
@@ -157,9 +160,9 @@ export const preferIsRecordAndHasKey: TSESLint.RuleModule<MessageIds, Options> =
|
|
|
157
160
|
index,
|
|
158
161
|
{ node, objExpression, keyExpression, needsIsRecord },
|
|
159
162
|
] of rewrites.entries()) {
|
|
160
|
-
const objText = sourceCode
|
|
163
|
+
const objText = getArgumentText(sourceCode, objExpression);
|
|
161
164
|
|
|
162
|
-
const keyText = sourceCode
|
|
165
|
+
const keyText = getArgumentText(sourceCode, keyExpression);
|
|
163
166
|
|
|
164
167
|
const originalText = sourceCode.getText(node);
|
|
165
168
|
|
|
@@ -78,3 +78,131 @@ describe('prefer-is-record-and-has-key', () => {
|
|
|
78
78
|
],
|
|
79
79
|
});
|
|
80
80
|
});
|
|
81
|
+
|
|
82
|
+
describe('prefer-is-record-and-has-key through type wrappers', () => {
|
|
83
|
+
tester.run('prefer-is-record-and-has-key', preferIsRecordAndHasKey, {
|
|
84
|
+
valid: [],
|
|
85
|
+
invalid: [
|
|
86
|
+
{
|
|
87
|
+
// `isRecord` rejects an array, so dropping it on the cast's word
|
|
88
|
+
// would turn `false` into `true`.
|
|
89
|
+
name: 'keeps isRecord for an array cast to a record',
|
|
90
|
+
code: dedent`
|
|
91
|
+
declare const arr: string[];
|
|
92
|
+
const ok = Object.hasOwn(arr as unknown as Record<string, unknown>, '0');
|
|
93
|
+
`,
|
|
94
|
+
output: dedent`
|
|
95
|
+
import { isRecord, hasKey } from 'ts-std-forge';
|
|
96
|
+
declare const arr: string[];
|
|
97
|
+
const ok = (isRecord(arr as unknown as Record<string, unknown>) && hasKey(arr as unknown as Record<string, unknown>, '0'));
|
|
98
|
+
`,
|
|
99
|
+
errors: [{ messageId: 'useIsRecordAndHasKey' }],
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
name: 'keeps isRecord for an unknown value cast to a record in `in`',
|
|
103
|
+
code: dedent`
|
|
104
|
+
declare const u: unknown;
|
|
105
|
+
const ok = 'a' in (<Record<string, unknown>>u);
|
|
106
|
+
`,
|
|
107
|
+
output: dedent`
|
|
108
|
+
import { isRecord, hasKey } from 'ts-std-forge';
|
|
109
|
+
declare const u: unknown;
|
|
110
|
+
const ok = (isRecord(<Record<string, unknown>>u) && hasKey(<Record<string, unknown>>u, 'a'));
|
|
111
|
+
`,
|
|
112
|
+
errors: [{ messageId: 'useIsRecordAndHasKey' }],
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
name: 'drops isRecord for a record behind `satisfies` and keeps the wrapper',
|
|
116
|
+
code: dedent`
|
|
117
|
+
declare const obj: Readonly<Record<string, unknown>>;
|
|
118
|
+
const ok = Object.hasOwn(obj satisfies object, 'a');
|
|
119
|
+
`,
|
|
120
|
+
output: dedent`
|
|
121
|
+
import { hasKey } from 'ts-std-forge';
|
|
122
|
+
declare const obj: Readonly<Record<string, unknown>>;
|
|
123
|
+
const ok = hasKey(obj satisfies object, 'a');
|
|
124
|
+
`,
|
|
125
|
+
errors: [{ messageId: 'useHasKey' }],
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
name: 'reads `!` on an optional record as the record it asserts',
|
|
129
|
+
code: dedent`
|
|
130
|
+
declare const obj: Readonly<Record<string, unknown>> | undefined;
|
|
131
|
+
const ok = Object.hasOwn(obj!, 'a');
|
|
132
|
+
`,
|
|
133
|
+
output: dedent`
|
|
134
|
+
import { hasKey } from 'ts-std-forge';
|
|
135
|
+
declare const obj: Readonly<Record<string, unknown>> | undefined;
|
|
136
|
+
const ok = hasKey(obj!, 'a');
|
|
137
|
+
`,
|
|
138
|
+
errors: [{ messageId: 'useHasKey' }],
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
name: 'reads `Object` through `as`',
|
|
142
|
+
code: dedent`
|
|
143
|
+
declare const obj: Readonly<Record<string, unknown>>;
|
|
144
|
+
const ok = (Object as ObjectConstructor).hasOwn(obj, 'a');
|
|
145
|
+
`,
|
|
146
|
+
output: dedent`
|
|
147
|
+
import { hasKey } from 'ts-std-forge';
|
|
148
|
+
declare const obj: Readonly<Record<string, unknown>>;
|
|
149
|
+
const ok = hasKey(obj, 'a');
|
|
150
|
+
`,
|
|
151
|
+
errors: [{ messageId: 'useHasKey' }],
|
|
152
|
+
},
|
|
153
|
+
],
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
describe('prefer-is-record-and-has-key with parenthesized operands', () => {
|
|
158
|
+
tester.run('prefer-is-record-and-has-key', preferIsRecordAndHasKey, {
|
|
159
|
+
valid: [],
|
|
160
|
+
invalid: [
|
|
161
|
+
{
|
|
162
|
+
name: 'keeps the parentheses of a sequence object in `in`',
|
|
163
|
+
code: dedent`
|
|
164
|
+
declare const a: number;
|
|
165
|
+
declare const o: unknown;
|
|
166
|
+
const ok = 'k' in (a, o);
|
|
167
|
+
`,
|
|
168
|
+
output: dedent`
|
|
169
|
+
import { isRecord, hasKey } from 'ts-std-forge';
|
|
170
|
+
declare const a: number;
|
|
171
|
+
declare const o: unknown;
|
|
172
|
+
const ok = (isRecord((a, o)) && hasKey((a, o), 'k'));
|
|
173
|
+
`,
|
|
174
|
+
errors: [{ messageId: 'useIsRecordAndHasKey' }],
|
|
175
|
+
},
|
|
176
|
+
{
|
|
177
|
+
name: 'keeps the parentheses of a sequence object in Object.hasOwn',
|
|
178
|
+
code: dedent`
|
|
179
|
+
declare const a: number;
|
|
180
|
+
declare const o: unknown;
|
|
181
|
+
const ok = Object.hasOwn((a, o), 'k');
|
|
182
|
+
`,
|
|
183
|
+
output: dedent`
|
|
184
|
+
import { isRecord, hasKey } from 'ts-std-forge';
|
|
185
|
+
declare const a: number;
|
|
186
|
+
declare const o: unknown;
|
|
187
|
+
const ok = (isRecord((a, o)) && hasKey((a, o), 'k'));
|
|
188
|
+
`,
|
|
189
|
+
errors: [{ messageId: 'useIsRecordAndHasKey' }],
|
|
190
|
+
},
|
|
191
|
+
{
|
|
192
|
+
name: 'keeps the parentheses of a sequence key',
|
|
193
|
+
code: dedent`
|
|
194
|
+
declare const a: number;
|
|
195
|
+
declare const o: Readonly<Record<string, unknown>>;
|
|
196
|
+
const ok = (a, 'k') in o;
|
|
197
|
+
`,
|
|
198
|
+
output: dedent`
|
|
199
|
+
import { hasKey } from 'ts-std-forge';
|
|
200
|
+
declare const a: number;
|
|
201
|
+
declare const o: Readonly<Record<string, unknown>>;
|
|
202
|
+
const ok = hasKey(o, (a, 'k'));
|
|
203
|
+
`,
|
|
204
|
+
errors: [{ messageId: 'useHasKey' }],
|
|
205
|
+
},
|
|
206
|
+
],
|
|
207
|
+
});
|
|
208
|
+
});
|
|
@@ -3,7 +3,11 @@ import {
|
|
|
3
3
|
type TSESLint,
|
|
4
4
|
type TSESTree,
|
|
5
5
|
} from '@typescript-eslint/utils';
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
getRangeWithParens,
|
|
8
|
+
isIdentifierNamed,
|
|
9
|
+
skipTypeWrappers,
|
|
10
|
+
} from './ast-utils.mjs';
|
|
7
11
|
import {
|
|
8
12
|
buildImportFixes,
|
|
9
13
|
getNamedImports,
|
|
@@ -61,20 +65,21 @@ export const preferSafeArrayIsArray: TSESLint.RuleModule<MessageIds, Options> =
|
|
|
61
65
|
node,
|
|
62
66
|
messageId: 'useSafeArrayIsArray',
|
|
63
67
|
fix: (fixer) => {
|
|
64
|
-
const callee = node.callee;
|
|
65
|
-
|
|
66
|
-
if (callee.type !== AST_NODE_TYPES.MemberExpression) {
|
|
67
|
-
return [];
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
const replacement = `SafeArray.isArray${sourceCode.getText(node).slice(sourceCode.getText(callee).length)}`;
|
|
71
|
-
|
|
72
68
|
const importFixes =
|
|
73
69
|
index === 0 && !hasSafeArrayImport
|
|
74
70
|
? buildImportFixes(fixer, program, ['SafeArray'])
|
|
75
71
|
: [];
|
|
76
72
|
|
|
77
|
-
|
|
73
|
+
// Only the callee is replaced, type wrappers included, along
|
|
74
|
+
// with the parentheses around it, which its range leaves out;
|
|
75
|
+
// the arguments stay as written.
|
|
76
|
+
return [
|
|
77
|
+
...importFixes,
|
|
78
|
+
fixer.replaceTextRange(
|
|
79
|
+
getRangeWithParens(sourceCode, node.callee),
|
|
80
|
+
'SafeArray.isArray',
|
|
81
|
+
),
|
|
82
|
+
];
|
|
78
83
|
},
|
|
79
84
|
});
|
|
80
85
|
}
|
|
@@ -85,19 +90,18 @@ export const preferSafeArrayIsArray: TSESLint.RuleModule<MessageIds, Options> =
|
|
|
85
90
|
} as const;
|
|
86
91
|
|
|
87
92
|
const isArrayIsArrayCall = (
|
|
88
|
-
|
|
93
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
94
|
+
node: TSESTree.CallExpression,
|
|
89
95
|
): boolean => {
|
|
90
|
-
|
|
91
|
-
return false;
|
|
92
|
-
}
|
|
96
|
+
const callee = skipTypeWrappers(node.callee);
|
|
93
97
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
if (object.type !== AST_NODE_TYPES.Identifier) {
|
|
98
|
+
if (callee.type !== AST_NODE_TYPES.MemberExpression) {
|
|
97
99
|
return false;
|
|
98
100
|
}
|
|
99
101
|
|
|
100
|
-
|
|
102
|
+
const { object, property } = callee;
|
|
103
|
+
|
|
104
|
+
if (!isIdentifierNamed(object, 'Array')) {
|
|
101
105
|
return false;
|
|
102
106
|
}
|
|
103
107
|
|
|
@@ -63,3 +63,96 @@ ruleTester.run('prefer-safe-array-is-array', preferSafeArrayIsArray, {
|
|
|
63
63
|
},
|
|
64
64
|
],
|
|
65
65
|
});
|
|
66
|
+
|
|
67
|
+
describe('prefer-safe-array-is-array through type wrappers', () => {
|
|
68
|
+
ruleTester.run('prefer-safe-array-is-array', preferSafeArrayIsArray, {
|
|
69
|
+
valid: [],
|
|
70
|
+
invalid: [
|
|
71
|
+
{
|
|
72
|
+
name: 'reads `Array` through `as`',
|
|
73
|
+
code: dedent`
|
|
74
|
+
const result = (Array as ArrayConstructor).isArray(value);
|
|
75
|
+
`,
|
|
76
|
+
output: dedent`
|
|
77
|
+
import { SafeArray } from 'ts-std-forge';
|
|
78
|
+
const result = SafeArray.isArray(value);
|
|
79
|
+
`,
|
|
80
|
+
errors: [{ messageId: 'useSafeArrayIsArray' }],
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
name: 'reads `Array` through `!`',
|
|
84
|
+
code: dedent`
|
|
85
|
+
const result = Array!.isArray(value);
|
|
86
|
+
`,
|
|
87
|
+
output: dedent`
|
|
88
|
+
import { SafeArray } from 'ts-std-forge';
|
|
89
|
+
const result = SafeArray.isArray(value);
|
|
90
|
+
`,
|
|
91
|
+
errors: [{ messageId: 'useSafeArrayIsArray' }],
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
name: 'reads the callee through `satisfies`',
|
|
95
|
+
code: dedent`
|
|
96
|
+
const result = (Array.isArray satisfies unknown)(value);
|
|
97
|
+
`,
|
|
98
|
+
output: dedent`
|
|
99
|
+
import { SafeArray } from 'ts-std-forge';
|
|
100
|
+
const result = SafeArray.isArray(value);
|
|
101
|
+
`,
|
|
102
|
+
errors: [{ messageId: 'useSafeArrayIsArray' }],
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
name: 'keeps a wrapper on the argument',
|
|
106
|
+
code: dedent`
|
|
107
|
+
const result = Array.isArray(value satisfies unknown);
|
|
108
|
+
`,
|
|
109
|
+
output: dedent`
|
|
110
|
+
import { SafeArray } from 'ts-std-forge';
|
|
111
|
+
const result = SafeArray.isArray(value satisfies unknown);
|
|
112
|
+
`,
|
|
113
|
+
errors: [{ messageId: 'useSafeArrayIsArray' }],
|
|
114
|
+
},
|
|
115
|
+
],
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
describe('prefer-safe-array-is-array with parenthesized operands', () => {
|
|
120
|
+
ruleTester.run('prefer-safe-array-is-array', preferSafeArrayIsArray, {
|
|
121
|
+
valid: [],
|
|
122
|
+
invalid: [
|
|
123
|
+
{
|
|
124
|
+
name: 'replaces a parenthesized callee with its parentheses',
|
|
125
|
+
code: dedent`
|
|
126
|
+
const result = (Array.isArray)(x);
|
|
127
|
+
`,
|
|
128
|
+
output: dedent`
|
|
129
|
+
import { SafeArray } from 'ts-std-forge';
|
|
130
|
+
const result = SafeArray.isArray(x);
|
|
131
|
+
`,
|
|
132
|
+
errors: [{ messageId: 'useSafeArrayIsArray' }],
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
name: 'replaces a doubly parenthesized callee of an optional call',
|
|
136
|
+
code: dedent`
|
|
137
|
+
const result = ((Array.isArray))?.(x);
|
|
138
|
+
`,
|
|
139
|
+
output: dedent`
|
|
140
|
+
import { SafeArray } from 'ts-std-forge';
|
|
141
|
+
const result = SafeArray.isArray?.(x);
|
|
142
|
+
`,
|
|
143
|
+
errors: [{ messageId: 'useSafeArrayIsArray' }],
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
name: 'keeps a parenthesized argument',
|
|
147
|
+
code: dedent`
|
|
148
|
+
const result = Array.isArray((a, x));
|
|
149
|
+
`,
|
|
150
|
+
output: dedent`
|
|
151
|
+
import { SafeArray } from 'ts-std-forge';
|
|
152
|
+
const result = SafeArray.isArray((a, x));
|
|
153
|
+
`,
|
|
154
|
+
errors: [{ messageId: 'useSafeArrayIsArray' }],
|
|
155
|
+
},
|
|
156
|
+
],
|
|
157
|
+
});
|
|
158
|
+
});
|
|
@@ -3,7 +3,12 @@ import {
|
|
|
3
3
|
type TSESLint,
|
|
4
4
|
type TSESTree,
|
|
5
5
|
} from '@typescript-eslint/utils';
|
|
6
|
-
import {
|
|
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,14 +84,13 @@ export const preferSafeArrayLengthGuard: TSESLint.RuleModule<
|
|
|
79
84
|
|
|
80
85
|
const tsStdForgeImport = getTsStdForgeImport(program);
|
|
81
86
|
|
|
82
|
-
const
|
|
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
|
|
@@ -96,15 +100,12 @@ export const preferSafeArrayLengthGuard: TSESLint.RuleModule<
|
|
|
96
100
|
return false;
|
|
97
101
|
}
|
|
98
102
|
|
|
99
|
-
const
|
|
100
|
-
|
|
101
|
-
if (tsNode === undefined) {
|
|
102
|
-
return false;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
const type = checker.getTypeAtLocation(tsNode);
|
|
103
|
+
const type = getValueType(sourceCode, expression);
|
|
106
104
|
|
|
107
|
-
return
|
|
105
|
+
return (
|
|
106
|
+
type !== undefined &&
|
|
107
|
+
(checker.isArrayType(type) || checker.isTupleType(type))
|
|
108
|
+
);
|
|
108
109
|
};
|
|
109
110
|
|
|
110
111
|
const mut_nodesToFix: {
|
|
@@ -140,7 +141,7 @@ export const preferSafeArrayLengthGuard: TSESLint.RuleModule<
|
|
|
140
141
|
index,
|
|
141
142
|
{ node, arrayExpression, guard },
|
|
142
143
|
] of mut_nodesToFix.entries()) {
|
|
143
|
-
const arrayText = sourceCode
|
|
144
|
+
const arrayText = getArgumentText(sourceCode, arrayExpression);
|
|
144
145
|
|
|
145
146
|
context.report({
|
|
146
147
|
node,
|
|
@@ -180,11 +181,11 @@ const matchLengthComparison = (
|
|
|
180
181
|
):
|
|
181
182
|
| Readonly<{ arrayExpression: TSESTree.Expression; guard: Guard }>
|
|
182
183
|
| undefined => {
|
|
183
|
-
|
|
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);
|
|
184
187
|
|
|
185
|
-
|
|
186
|
-
return undefined;
|
|
187
|
-
}
|
|
188
|
+
const right = skipTypeWrappers(node.right);
|
|
188
189
|
|
|
189
190
|
const lengthOnLeft = isLengthAccess(left);
|
|
190
191
|
|
|
@@ -218,7 +219,7 @@ const matchLengthComparison = (
|
|
|
218
219
|
|
|
219
220
|
const isLengthAccess = (
|
|
220
221
|
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
221
|
-
node: TSESTree.
|
|
222
|
+
node: TSESTree.Node,
|
|
222
223
|
): node is TSESTree.MemberExpression =>
|
|
223
224
|
node.type === AST_NODE_TYPES.MemberExpression &&
|
|
224
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
|
|
78
|
+
const callee = skipTypeWrappers(node.callee);
|
|
75
79
|
|
|
76
80
|
// Match `parseInt(...)` or `Number.parseInt(...)`.
|
|
77
81
|
const isGlobalParseInt =
|
|
@@ -81,8 +85,7 @@ export const preferSafeNumberParseInteger: TSESLint.RuleModule<
|
|
|
81
85
|
const isNumberParseInt =
|
|
82
86
|
callee.type === AST_NODE_TYPES.MemberExpression &&
|
|
83
87
|
!callee.computed &&
|
|
84
|
-
callee.object
|
|
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
|
|
|
@@ -103,9 +106,11 @@ export const preferSafeNumberParseInteger: TSESLint.RuleModule<
|
|
|
103
106
|
return;
|
|
104
107
|
}
|
|
105
108
|
|
|
106
|
-
// 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.
|
|
107
111
|
if (args.length >= 2) {
|
|
108
|
-
const radix =
|
|
112
|
+
const radix =
|
|
113
|
+
args[1] === undefined ? undefined : skipTypeWrappers(args[1]);
|
|
109
114
|
|
|
110
115
|
if (radix?.type !== AST_NODE_TYPES.Literal || radix.value !== 10) {
|
|
111
116
|
return;
|
|
@@ -113,23 +118,12 @@ export const preferSafeNumberParseInteger: TSESLint.RuleModule<
|
|
|
113
118
|
}
|
|
114
119
|
|
|
115
120
|
// The argument must be a `string` so the autofix is type-safe against
|
|
116
|
-
// `SafeNumber.parseInteger(value: string)
|
|
117
|
-
//
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
const checker = services.program.getTypeChecker();
|
|
123
|
-
|
|
124
|
-
const tsNode = services.esTreeNodeToTSNodeMap?.get(firstArg);
|
|
125
|
-
|
|
126
|
-
if (tsNode === undefined) {
|
|
127
|
-
return;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
const argType = checker.getTypeAtLocation(tsNode);
|
|
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);
|
|
131
125
|
|
|
132
|
-
if (!isStringType(argType)) {
|
|
126
|
+
if (argType === undefined || !isStringType(argType)) {
|
|
133
127
|
return;
|
|
134
128
|
}
|
|
135
129
|
|
|
@@ -146,7 +140,7 @@ export const preferSafeNumberParseInteger: TSESLint.RuleModule<
|
|
|
146
140
|
index,
|
|
147
141
|
{ node, argExpression },
|
|
148
142
|
] of mut_nodesToFix.entries()) {
|
|
149
|
-
const argText = sourceCode
|
|
143
|
+
const argText = getArgumentText(sourceCode, argExpression);
|
|
150
144
|
|
|
151
145
|
context.report({
|
|
152
146
|
node,
|