eslint-rule-forge 0.3.2 → 0.5.0
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/README.md +47 -40
- package/dist/bindings.d.mts +4 -5
- package/dist/bindings.d.mts.map +1 -1
- package/dist/bindings.mjs +0 -1
- package/dist/bindings.mjs.map +1 -1
- package/dist/comments.d.mts +10 -0
- package/dist/comments.d.mts.map +1 -0
- package/dist/comments.mjs +11 -0
- package/dist/comments.mjs.map +1 -0
- package/dist/imports.d.mts +68 -8
- package/dist/imports.d.mts.map +1 -1
- package/dist/imports.mjs +202 -6
- package/dist/imports.mjs.map +1 -1
- package/dist/index.d.mts +2 -0
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +2 -0
- package/dist/index.mjs.map +1 -1
- package/dist/negation.d.mts +8 -9
- package/dist/negation.d.mts.map +1 -1
- package/dist/negation.mjs +18 -18
- package/dist/negation.mjs.map +1 -1
- package/dist/rules/prefer-array-is-array.d.mts.map +1 -1
- package/dist/rules/prefer-array-is-array.mjs +13 -19
- package/dist/rules/prefer-array-is-array.mjs.map +1 -1
- package/dist/rules/prefer-is-non-null-object.d.mts.map +1 -1
- package/dist/rules/prefer-is-non-null-object.mjs +75 -76
- 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 +32 -44
- package/dist/rules/prefer-is-record-and-has-key.mjs.map +1 -1
- package/dist/rules/prefer-safe-parse.d.mts +10 -7
- package/dist/rules/prefer-safe-parse.d.mts.map +1 -1
- package/dist/rules/prefer-safe-parse.mjs +50 -34
- package/dist/rules/prefer-safe-parse.mjs.map +1 -1
- package/dist/statements.d.mts +12 -0
- package/dist/statements.d.mts.map +1 -0
- package/dist/statements.mjs +39 -0
- package/dist/statements.mjs.map +1 -0
- package/dist/type-wrappers.d.mts +3 -3
- package/package.json +5 -5
- package/src/bindings.mts +4 -5
- package/src/comments.mts +23 -0
- package/src/imports.mts +363 -19
- package/src/index.mts +2 -0
- package/src/negation.mts +36 -37
- package/src/rules/prefer-array-is-array.mts +23 -36
- package/src/rules/prefer-is-non-null-object.mts +107 -112
- package/src/rules/prefer-is-record-and-has-key.mts +47 -71
- package/src/rules/prefer-safe-parse.mts +92 -77
- package/src/statements.mts +57 -0
- package/src/type-wrappers.mts +3 -3
|
@@ -3,11 +3,8 @@ import {
|
|
|
3
3
|
type TSESLint,
|
|
4
4
|
type TSESTree,
|
|
5
5
|
} from '@typescript-eslint/utils';
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
getImportDeclarations,
|
|
9
|
-
getNamedImports,
|
|
10
|
-
} from '../imports.mjs';
|
|
6
|
+
import { hasForeignBinding } from '../bindings.mjs';
|
|
7
|
+
import { bindValueImports } from '../imports.mjs';
|
|
11
8
|
import { getRangeWithParens, parenthesesDepth } from '../parentheses.mjs';
|
|
12
9
|
import { skipTypeWrappers } from '../type-wrappers.mjs';
|
|
13
10
|
|
|
@@ -39,11 +36,8 @@ export const createPreferIsNonNullObject = ({
|
|
|
39
36
|
create: (context) => {
|
|
40
37
|
const sourceCode = context.sourceCode;
|
|
41
38
|
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
const moduleImports = getImportDeclarations(program, moduleName);
|
|
45
|
-
|
|
46
|
-
const mut_pairsToFix: NonNullObjectPair[] = [];
|
|
39
|
+
const mut_pairsToFix: (NonNullObjectPair &
|
|
40
|
+
Readonly<{ chain: TSESTree.LogicalExpression }>)[] = [];
|
|
47
41
|
|
|
48
42
|
return {
|
|
49
43
|
LogicalExpression: (node) => {
|
|
@@ -52,42 +46,43 @@ export const createPreferIsNonNullObject = ({
|
|
|
52
46
|
return;
|
|
53
47
|
}
|
|
54
48
|
|
|
55
|
-
mut_pairsToFix.push(
|
|
49
|
+
mut_pairsToFix.push(
|
|
50
|
+
...findNonNullObjectPairs(sourceCode, node).map((pair) => ({
|
|
51
|
+
...pair,
|
|
52
|
+
chain: node,
|
|
53
|
+
})),
|
|
54
|
+
);
|
|
56
55
|
},
|
|
57
56
|
'Program:exit': () => {
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
//
|
|
65
|
-
//
|
|
66
|
-
for (const
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
57
|
+
const binding = bindValueImports(sourceCode, moduleName, [
|
|
58
|
+
'isNonNullObject',
|
|
59
|
+
]);
|
|
60
|
+
|
|
61
|
+
const localName = binding?.localNames[0];
|
|
62
|
+
|
|
63
|
+
// Every fix brings `isNonNullObject` into scope itself, so that one
|
|
64
|
+
// applied alone (an editor's quick fix) is complete.
|
|
65
|
+
for (const { range, identifierName, chain } of mut_pairsToFix) {
|
|
66
|
+
const loc = {
|
|
67
|
+
start: sourceCode.getLocFromIndex(range[0]),
|
|
68
|
+
end: sourceCode.getLocFromIndex(range[1]),
|
|
69
|
+
} as const;
|
|
70
|
+
|
|
70
71
|
context.report({
|
|
71
|
-
loc
|
|
72
|
-
start: sourceCode.getLocFromIndex(range[0]),
|
|
73
|
-
end: sourceCode.getLocFromIndex(range[1]),
|
|
74
|
-
},
|
|
72
|
+
loc,
|
|
75
73
|
messageId: 'useIsNonNullObject',
|
|
76
|
-
fix:
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
fixer.replaceTextRange(range, replacement),
|
|
89
|
-
];
|
|
90
|
-
},
|
|
74
|
+
fix:
|
|
75
|
+
binding === undefined ||
|
|
76
|
+
localName === undefined ||
|
|
77
|
+
hasForeignBinding(sourceCode, chain, localName, moduleName)
|
|
78
|
+
? null
|
|
79
|
+
: (fixer) => [
|
|
80
|
+
...binding.edits(fixer),
|
|
81
|
+
fixer.replaceTextRange(
|
|
82
|
+
range,
|
|
83
|
+
`${localName}(${identifierName})`,
|
|
84
|
+
),
|
|
85
|
+
],
|
|
91
86
|
});
|
|
92
87
|
}
|
|
93
88
|
},
|
|
@@ -120,51 +115,8 @@ const isChainOperand = (
|
|
|
120
115
|
parenthesesDepth(sourceCode, node) === 0;
|
|
121
116
|
|
|
122
117
|
/**
|
|
123
|
-
*
|
|
124
|
-
*
|
|
125
|
-
* and after it: `a && u !== null && typeof u === 'object' && b`. They have to
|
|
126
|
-
* be next to each other, so that the pair is one range of the source.
|
|
127
|
-
*/
|
|
128
|
-
const findNonNullObjectPairs = (
|
|
129
|
-
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
130
|
-
sourceCode: TSESLint.SourceCode,
|
|
131
|
-
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
132
|
-
node: TSESTree.LogicalExpression,
|
|
133
|
-
): readonly NonNullObjectPair[] => {
|
|
134
|
-
const operands = chainOperands(sourceCode, node);
|
|
135
|
-
|
|
136
|
-
const pairsFrom = (index: number): readonly NonNullObjectPair[] => {
|
|
137
|
-
const first = operands[index];
|
|
138
|
-
|
|
139
|
-
const second = operands[index + 1];
|
|
140
|
-
|
|
141
|
-
if (first === undefined || second === undefined) {
|
|
142
|
-
return [];
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
const identifierName =
|
|
146
|
-
pairIdentifierName(first, second) ?? pairIdentifierName(second, first);
|
|
147
|
-
|
|
148
|
-
return identifierName === undefined
|
|
149
|
-
? pairsFrom(index + 1)
|
|
150
|
-
: [
|
|
151
|
-
{
|
|
152
|
-
range: [
|
|
153
|
-
getRangeWithParens(sourceCode, first)[0],
|
|
154
|
-
getRangeWithParens(sourceCode, second)[1],
|
|
155
|
-
],
|
|
156
|
-
identifierName,
|
|
157
|
-
},
|
|
158
|
-
...pairsFrom(index + 2),
|
|
159
|
-
];
|
|
160
|
-
};
|
|
161
|
-
|
|
162
|
-
return pairsFrom(0);
|
|
163
|
-
};
|
|
164
|
-
|
|
165
|
-
/**
|
|
166
|
-
* The operands of the `&&` chain `node`, left to right. A parenthesized `&&`
|
|
167
|
-
* is one operand: its halves cannot be replaced without its parentheses.
|
|
118
|
+
* The operands of the `&&` chain `node`, left to right. A parenthesized `&&` is
|
|
119
|
+
* one operand: its halves cannot be replaced without its parentheses.
|
|
168
120
|
*/
|
|
169
121
|
const chainOperands = (
|
|
170
122
|
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
@@ -183,26 +135,11 @@ const chainOperands = (
|
|
|
183
135
|
// Every operand below is read through its type wrappers: `typeof (u satisfies
|
|
184
136
|
// unknown) === 'object' && u! !== null` is the same check.
|
|
185
137
|
|
|
186
|
-
|
|
187
|
-
* The name of `u` when `typeofCheck` is `typeof u === 'object'` and
|
|
188
|
-
* `nullCheck` is `u !== null` for the same `u`.
|
|
189
|
-
*/
|
|
190
|
-
const pairIdentifierName = (
|
|
191
|
-
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
192
|
-
typeofCheck: TSESTree.Expression,
|
|
138
|
+
const isLiteralOf = (
|
|
193
139
|
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
skipTypeWrappers(typeofCheck),
|
|
198
|
-
);
|
|
199
|
-
|
|
200
|
-
return identifierName !== undefined &&
|
|
201
|
-
identifierName ===
|
|
202
|
-
getNonNullCheckIdentifierName(skipTypeWrappers(nullCheck))
|
|
203
|
-
? identifierName
|
|
204
|
-
: undefined;
|
|
205
|
-
};
|
|
140
|
+
node: TSESTree.Node,
|
|
141
|
+
value: string | null,
|
|
142
|
+
): boolean => node.type === AST_NODE_TYPES.Literal && node.value === value;
|
|
206
143
|
|
|
207
144
|
/** `u` of `typeof u === 'object'`, in either operand order. */
|
|
208
145
|
const getTypeofObjectIdentifierName = (
|
|
@@ -242,8 +179,8 @@ const getTypeofObjectIdentifierName = (
|
|
|
242
179
|
|
|
243
180
|
/**
|
|
244
181
|
* `u` of `u !== null`, in either operand order. `u != null` also excludes
|
|
245
|
-
* `undefined`, which `typeof u === 'object'` has already excluded, so it is
|
|
246
|
-
*
|
|
182
|
+
* `undefined`, which `typeof u === 'object'` has already excluded, so it is the
|
|
183
|
+
* same check here.
|
|
247
184
|
*/
|
|
248
185
|
const getNonNullCheckIdentifierName = (
|
|
249
186
|
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
@@ -269,8 +206,66 @@ const getNonNullCheckIdentifierName = (
|
|
|
269
206
|
return checked?.type === AST_NODE_TYPES.Identifier ? checked.name : undefined;
|
|
270
207
|
};
|
|
271
208
|
|
|
272
|
-
|
|
209
|
+
/**
|
|
210
|
+
* The name of `u` when `typeofCheck` is `typeof u === 'object'` and `nullCheck`
|
|
211
|
+
* is `u !== null` for the same `u`.
|
|
212
|
+
*/
|
|
213
|
+
const pairIdentifierName = (
|
|
273
214
|
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
215
|
+
typeofCheck: TSESTree.Expression,
|
|
216
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
217
|
+
nullCheck: TSESTree.Expression,
|
|
218
|
+
): string | undefined => {
|
|
219
|
+
const identifierName = getTypeofObjectIdentifierName(
|
|
220
|
+
skipTypeWrappers(typeofCheck),
|
|
221
|
+
);
|
|
222
|
+
|
|
223
|
+
return identifierName !== undefined &&
|
|
224
|
+
identifierName ===
|
|
225
|
+
getNonNullCheckIdentifierName(skipTypeWrappers(nullCheck))
|
|
226
|
+
? identifierName
|
|
227
|
+
: undefined;
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Every non-overlapping pair in the `&&` chain `node` heads, left to right. The
|
|
232
|
+
* two halves may come in either order, and the pair may have operands before
|
|
233
|
+
* and after it: `a && u !== null && typeof u === 'object' && b`. They have to
|
|
234
|
+
* be next to each other, so that the pair is one range of the source.
|
|
235
|
+
*/
|
|
236
|
+
const findNonNullObjectPairs = (
|
|
237
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
238
|
+
sourceCode: TSESLint.SourceCode,
|
|
239
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
240
|
+
node: TSESTree.LogicalExpression,
|
|
241
|
+
): readonly NonNullObjectPair[] => {
|
|
242
|
+
const operands = chainOperands(sourceCode, node);
|
|
243
|
+
|
|
244
|
+
const pairsFrom = (index: number): readonly NonNullObjectPair[] => {
|
|
245
|
+
const first = operands[index];
|
|
246
|
+
|
|
247
|
+
const second = operands[index + 1];
|
|
248
|
+
|
|
249
|
+
if (first === undefined || second === undefined) {
|
|
250
|
+
return [];
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const identifierName =
|
|
254
|
+
pairIdentifierName(first, second) ?? pairIdentifierName(second, first);
|
|
255
|
+
|
|
256
|
+
return identifierName === undefined
|
|
257
|
+
? pairsFrom(index + 1)
|
|
258
|
+
: [
|
|
259
|
+
{
|
|
260
|
+
range: [
|
|
261
|
+
getRangeWithParens(sourceCode, first)[0],
|
|
262
|
+
getRangeWithParens(sourceCode, second)[1],
|
|
263
|
+
],
|
|
264
|
+
identifierName,
|
|
265
|
+
},
|
|
266
|
+
...pairsFrom(index + 2),
|
|
267
|
+
];
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
return pairsFrom(0);
|
|
271
|
+
};
|
|
@@ -4,11 +4,8 @@ import {
|
|
|
4
4
|
type TSESTree,
|
|
5
5
|
} from '@typescript-eslint/utils';
|
|
6
6
|
import * as ts from 'typescript';
|
|
7
|
-
import {
|
|
8
|
-
|
|
9
|
-
getImportDeclarations,
|
|
10
|
-
getNamedImports,
|
|
11
|
-
} from '../imports.mjs';
|
|
7
|
+
import { hasForeignBinding } from '../bindings.mjs';
|
|
8
|
+
import { bindValueImports } from '../imports.mjs';
|
|
12
9
|
import { getArgumentText } from '../parentheses.mjs';
|
|
13
10
|
import {
|
|
14
11
|
isIdentifierNamed,
|
|
@@ -45,18 +42,14 @@ export const createPreferIsRecordAndHasKey = ({
|
|
|
45
42
|
create: (context) => {
|
|
46
43
|
const sourceCode = context.sourceCode;
|
|
47
44
|
|
|
48
|
-
const program = sourceCode.ast;
|
|
49
|
-
|
|
50
|
-
const moduleImports = getImportDeclarations(program, moduleName);
|
|
51
|
-
|
|
52
45
|
/**
|
|
53
46
|
* Whether the `isRecord(...)` half of the rewrite would be dead code
|
|
54
47
|
* because the object already satisfies `hasKey`'s
|
|
55
|
-
* `R extends UnknownRecord` constraint, at every layer of type
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
48
|
+
* `R extends UnknownRecord` constraint, at every layer of type wrappers:
|
|
49
|
+
* an array cast to a record is still rejected by `isRecord`, and a record
|
|
50
|
+
* widened to one that may be an array is the author saying it may need
|
|
51
|
+
* the guard. Without type information nothing is known, so the guard is
|
|
52
|
+
* kept.
|
|
60
53
|
*/
|
|
61
54
|
const isKnownRecord = (
|
|
62
55
|
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
@@ -159,81 +152,64 @@ export const createPreferIsRecordAndHasKey = ({
|
|
|
159
152
|
},
|
|
160
153
|
|
|
161
154
|
'Program:exit': () => {
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
const
|
|
155
|
+
// Bound once for each shape of rewrite: a file may import `hasKey`
|
|
156
|
+
// but not `isRecord`, which only the rewrites that need it add.
|
|
157
|
+
const withIsRecord = bindValueImports(sourceCode, moduleName, [
|
|
158
|
+
'isRecord',
|
|
159
|
+
'hasKey',
|
|
160
|
+
]);
|
|
161
|
+
|
|
162
|
+
const hasKeyOnly = bindValueImports(sourceCode, moduleName, [
|
|
163
|
+
'hasKey',
|
|
164
|
+
]);
|
|
165
|
+
|
|
166
|
+
for (const { node, objExpression, keyExpression } of mut_nodesToFix) {
|
|
165
167
|
// A record-typed object makes the `isRecord(...)` conjunct always
|
|
166
168
|
// true, so it is left out of the replacement entirely.
|
|
167
|
-
const needsIsRecord = !isKnownRecord(
|
|
169
|
+
const needsIsRecord = !isKnownRecord(objExpression);
|
|
168
170
|
|
|
169
|
-
|
|
170
|
-
...entry,
|
|
171
|
-
needsIsRecord,
|
|
172
|
-
// `isRecord(obj) && hasKey(obj, key)` evaluates `obj` twice.
|
|
173
|
-
fixable: !needsIsRecord || isSideEffectFree(entry.objExpression),
|
|
174
|
-
};
|
|
175
|
-
});
|
|
176
|
-
|
|
177
|
-
const fixableRewrites = rewrites.filter(({ fixable }) => fixable);
|
|
171
|
+
const binding = needsIsRecord ? withIsRecord : hasKeyOnly;
|
|
178
172
|
|
|
179
|
-
|
|
180
|
-
fixableRewrites.length === 0
|
|
181
|
-
? []
|
|
182
|
-
: [
|
|
183
|
-
...(fixableRewrites.some(
|
|
184
|
-
({ needsIsRecord }) => needsIsRecord,
|
|
185
|
-
) && !namedImports.includes('isRecord')
|
|
186
|
-
? ['isRecord']
|
|
187
|
-
: []),
|
|
188
|
-
...(namedImports.includes('hasKey') ? [] : ['hasKey']),
|
|
189
|
-
];
|
|
173
|
+
const hasKeyName = binding?.localNames.at(-1);
|
|
190
174
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
const { node, objExpression, keyExpression, needsIsRecord } =
|
|
195
|
-
rewrite;
|
|
175
|
+
const isRecordName = needsIsRecord
|
|
176
|
+
? binding?.localNames[0]
|
|
177
|
+
: undefined;
|
|
196
178
|
|
|
197
179
|
const objText = getArgumentText(sourceCode, objExpression);
|
|
198
180
|
|
|
199
181
|
const keyText = getArgumentText(sourceCode, keyExpression);
|
|
200
182
|
|
|
201
|
-
const
|
|
183
|
+
const fixable =
|
|
184
|
+
binding !== undefined &&
|
|
185
|
+
hasKeyName !== undefined &&
|
|
186
|
+
(!needsIsRecord || isRecordName !== undefined) &&
|
|
187
|
+
// `isRecord(obj) && hasKey(obj, key)` evaluates `obj` twice.
|
|
188
|
+
(!needsIsRecord || isSideEffectFree(objExpression)) &&
|
|
189
|
+
binding.localNames.every(
|
|
190
|
+
(name) =>
|
|
191
|
+
!hasForeignBinding(sourceCode, node, name, moduleName),
|
|
192
|
+
);
|
|
202
193
|
|
|
203
194
|
context.report({
|
|
204
195
|
node,
|
|
205
196
|
messageId: needsIsRecord ? 'useIsRecordAndHasKey' : 'useHasKey',
|
|
206
197
|
data: {
|
|
207
|
-
original:
|
|
198
|
+
original: sourceCode.getText(node),
|
|
208
199
|
objName: objText,
|
|
209
200
|
keyName: keyText,
|
|
210
201
|
},
|
|
211
|
-
fix:
|
|
212
|
-
?
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
const importFixes =
|
|
223
|
-
rewrite === importCarrier && importsToAdd.length > 0
|
|
224
|
-
? buildNamedImportFix(
|
|
225
|
-
fixer,
|
|
226
|
-
program,
|
|
227
|
-
moduleName,
|
|
228
|
-
importsToAdd,
|
|
229
|
-
)
|
|
230
|
-
: [];
|
|
231
|
-
|
|
232
|
-
return [
|
|
233
|
-
...importFixes,
|
|
234
|
-
fixer.replaceText(node, replacement),
|
|
235
|
-
];
|
|
236
|
-
},
|
|
202
|
+
fix: fixable
|
|
203
|
+
? (fixer) => [
|
|
204
|
+
...binding.edits(fixer),
|
|
205
|
+
fixer.replaceText(
|
|
206
|
+
node,
|
|
207
|
+
needsIsRecord
|
|
208
|
+
? `(${isRecordName}(${objText}) && ${hasKeyName}(${objText}, ${keyText}))`
|
|
209
|
+
: `${hasKeyName}(${objText}, ${keyText})`,
|
|
210
|
+
),
|
|
211
|
+
]
|
|
212
|
+
: null,
|
|
237
213
|
});
|
|
238
214
|
}
|
|
239
215
|
},
|