eslint-rule-forge 0.4.0 → 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 +17 -9
- 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/imports.d.mts +36 -17
- package/dist/imports.d.mts.map +1 -1
- package/dist/imports.mjs +92 -13
- package/dist/imports.mjs.map +1 -1
- package/dist/index.d.mts +1 -0
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +1 -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-is-non-null-object.mjs +51 -51
- package/dist/rules/prefer-is-non-null-object.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 +41 -26
- 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/imports.mts +181 -37
- package/src/index.mts +1 -0
- package/src/negation.mts +36 -37
- package/src/rules/prefer-is-non-null-object.mts +70 -70
- package/src/rules/prefer-is-record-and-has-key.mts +5 -5
- package/src/rules/prefer-safe-parse.mts +72 -50
- package/src/statements.mts +57 -0
- package/src/type-wrappers.mts +3 -3
package/src/imports.mts
CHANGED
|
@@ -61,8 +61,8 @@ export type ResolvedCallee = Readonly<{
|
|
|
61
61
|
canonicalName: string;
|
|
62
62
|
|
|
63
63
|
/**
|
|
64
|
-
* The identifier to rewrite to change what is called: the callee itself for
|
|
65
|
-
*
|
|
64
|
+
* The identifier to rewrite to change what is called: the callee itself for a
|
|
65
|
+
* named import, the property for a namespace import.
|
|
66
66
|
*/
|
|
67
67
|
propertyNode: TSESTree.Identifier;
|
|
68
68
|
|
|
@@ -145,12 +145,11 @@ export const buildCalleeResolver = (
|
|
|
145
145
|
* as given, so `type T` imports a type.
|
|
146
146
|
*
|
|
147
147
|
* Use it only when the file does not import from `moduleName` by name;
|
|
148
|
-
* {@link bindValueImports} adds to an existing declaration instead, which
|
|
149
|
-
*
|
|
150
|
-
*
|
|
151
|
-
*
|
|
152
|
-
*
|
|
153
|
-
* dropped.
|
|
148
|
+
* {@link bindValueImports} adds to an existing declaration instead, which keeps
|
|
149
|
+
* `import-x/no-duplicates` satisfied, and falls back to this. An import sorter
|
|
150
|
+
* orders the declaration afterwards. Put every name one report needs into a
|
|
151
|
+
* single call: ESLint merges the fixes of one report into one edit, and two
|
|
152
|
+
* fixes inserting at the same place conflict, so that one of them is dropped.
|
|
154
153
|
*/
|
|
155
154
|
export const buildNamedImportFix = (
|
|
156
155
|
fixer: TSESLint.RuleFixer,
|
|
@@ -199,10 +198,12 @@ export const buildNamedImportFix = (
|
|
|
199
198
|
* - Imported as a value: that local name, and no edit.
|
|
200
199
|
* - Imported by `import { type x }`: the edit drops `type`, keeping a comment
|
|
201
200
|
* after it.
|
|
202
|
-
* - Not imported: the edit adds it to the first
|
|
203
|
-
* that has a value specifier, or else
|
|
204
|
-
* not `import type
|
|
205
|
-
* `import
|
|
201
|
+
* - Not imported: the edit adds it to the braces of the first import from the
|
|
202
|
+
* module that has a value specifier, or else of the first other one with
|
|
203
|
+
* braces that is not `import type`, empty braces included (`import {}`,
|
|
204
|
+
* `import D, {}`).
|
|
205
|
+
* Adding it there rather than in a new declaration keeps
|
|
206
|
+
* `import-x/no-duplicates` satisfied. Failing that, it goes into new braces
|
|
206
207
|
* after a lone default import (`import D, { x }`), and when the module has
|
|
207
208
|
* neither, a new declaration goes after the directive prologue
|
|
208
209
|
* ({@link buildNamedImportFix}).
|
|
@@ -212,11 +213,11 @@ export const buildNamedImportFix = (
|
|
|
212
213
|
*
|
|
213
214
|
* ESLint's default parser leaves `importKind` out, which counts as a value
|
|
214
215
|
* import. Every name a fix writes is bound by that fix's own edits rather than
|
|
215
|
-
* left to another report's: an editor applies one fix at a time, and a fix
|
|
216
|
-
*
|
|
217
|
-
*
|
|
218
|
-
*
|
|
219
|
-
*
|
|
216
|
+
* left to another report's: an editor applies one fix at a time, and a fix that
|
|
217
|
+
* loses a conflict with another in the same pass is dropped. Fixes that all add
|
|
218
|
+
* a name overlap, so `--fix` applies one per pass, and the rest in the next, by
|
|
219
|
+
* when the file imports it. Whether something other than an import binds the
|
|
220
|
+
* name where it is written is the caller's to check
|
|
220
221
|
* ({@link hasForeignBinding}).
|
|
221
222
|
*/
|
|
222
223
|
export const bindValueImports = (
|
|
@@ -253,18 +254,7 @@ export const bindValueImports = (
|
|
|
253
254
|
|
|
254
255
|
const toAdd = defined.flatMap((b) => (b.isAdded ? [b.localName] : []));
|
|
255
256
|
|
|
256
|
-
const
|
|
257
|
-
named.find(isValueImport) ?? named.find(isNotTypeOnly)
|
|
258
|
-
)?.declaration.specifiers.at(-1);
|
|
259
|
-
|
|
260
|
-
// `import D from 'm'` takes `, { x }`: a second declaration beside it would
|
|
261
|
-
// be a duplicate.
|
|
262
|
-
const defaultHost = declarations.find(
|
|
263
|
-
(declaration) =>
|
|
264
|
-
declaration.importKind !== 'type' &&
|
|
265
|
-
declaration.specifiers.length === 1 &&
|
|
266
|
-
declaration.specifiers[0]?.type === AST_NODE_TYPES.ImportDefaultSpecifier,
|
|
267
|
-
)?.specifiers[0];
|
|
257
|
+
const hosts = hostsFor(declarations);
|
|
268
258
|
|
|
269
259
|
return {
|
|
270
260
|
localNames: defined.map((b) => b.localName),
|
|
@@ -272,15 +262,86 @@ export const bindValueImports = (
|
|
|
272
262
|
...defined.flatMap((b) => b.edits(fixer)),
|
|
273
263
|
...(toAdd.length === 0
|
|
274
264
|
? []
|
|
275
|
-
:
|
|
276
|
-
? [fixer.insertTextAfter(host, `, ${toAdd.join(', ')}`)]
|
|
277
|
-
: defaultHost !== undefined
|
|
278
|
-
? [fixer.insertTextAfter(defaultHost, `, { ${toAdd.join(', ')} }`)]
|
|
279
|
-
: buildNamedImportFix(fixer, program, moduleName, toAdd)),
|
|
265
|
+
: addToFirstHost(fixer, sourceCode, moduleName, hosts, toAdd)),
|
|
280
266
|
],
|
|
281
267
|
};
|
|
282
268
|
};
|
|
283
269
|
|
|
270
|
+
/**
|
|
271
|
+
* The edits that import `moduleName`'s types `names` the file does not import
|
|
272
|
+
* yet: added to an existing `import type { … }` from the module (`, X`), else
|
|
273
|
+
* as `type X` to another import from it, chosen as {@link bindValueImports}
|
|
274
|
+
* chooses one, and only failing those as a new
|
|
275
|
+
* `import { type X } from '<module>'` after the directive prologue. Adding to
|
|
276
|
+
* an existing declaration keeps `import-x/no-duplicates` satisfied. Whether the
|
|
277
|
+
* file binds a name to something else is the caller's to check.
|
|
278
|
+
*/
|
|
279
|
+
export const addTypeImports = (
|
|
280
|
+
fixer: TSESLint.RuleFixer,
|
|
281
|
+
sourceCode: TSESLint.SourceCode,
|
|
282
|
+
moduleName: string,
|
|
283
|
+
names: readonly string[],
|
|
284
|
+
): readonly TSESLint.RuleFix[] => {
|
|
285
|
+
if (names.length === 0) {
|
|
286
|
+
return [];
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
const declarations = getImportDeclarations(sourceCode.ast, moduleName);
|
|
290
|
+
|
|
291
|
+
const typeOnlyFix = firstFix(
|
|
292
|
+
declarations.filter(isTypeOnlyDeclaration),
|
|
293
|
+
(declaration) => addToImportBraces(fixer, sourceCode, declaration, names),
|
|
294
|
+
);
|
|
295
|
+
|
|
296
|
+
return typeOnlyFix !== undefined
|
|
297
|
+
? [typeOnlyFix]
|
|
298
|
+
: addToFirstHost(
|
|
299
|
+
fixer,
|
|
300
|
+
sourceCode,
|
|
301
|
+
moduleName,
|
|
302
|
+
hostsFor(declarations),
|
|
303
|
+
names.map((name) => `type ${name}`),
|
|
304
|
+
);
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Adds `specifiers` to the `{ … }` of `declaration`: after its last named
|
|
309
|
+
* specifier, or inside empty braces (`import d, {} from …`). `undefined` when
|
|
310
|
+
* the declaration has no braces (a default or namespace import alone) or there
|
|
311
|
+
* is nothing to add. The braces of import attributes (`with { … }`) are not
|
|
312
|
+
* taken for them.
|
|
313
|
+
*/
|
|
314
|
+
export const addToImportBraces = (
|
|
315
|
+
fixer: TSESLint.RuleFixer,
|
|
316
|
+
sourceCode: TSESLint.SourceCode,
|
|
317
|
+
declaration: TSESTree.ImportDeclaration,
|
|
318
|
+
specifiers: readonly string[],
|
|
319
|
+
): TSESLint.RuleFix | undefined => {
|
|
320
|
+
if (specifiers.length === 0) {
|
|
321
|
+
return undefined;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
const lastNamed = declaration.specifiers
|
|
325
|
+
.filter(
|
|
326
|
+
(specifier): specifier is TSESTree.ImportSpecifier =>
|
|
327
|
+
specifier.type === AST_NODE_TYPES.ImportSpecifier,
|
|
328
|
+
)
|
|
329
|
+
.at(-1);
|
|
330
|
+
|
|
331
|
+
if (lastNamed !== undefined) {
|
|
332
|
+
return fixer.insertTextAfter(lastNamed, `, ${specifiers.join(', ')}`);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
const openingBrace = sourceCode.getFirstToken(declaration, {
|
|
336
|
+
filter: (token) =>
|
|
337
|
+
token.value === '{' && token.range[1] <= declaration.source.range[0],
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
return openingBrace === null
|
|
341
|
+
? undefined
|
|
342
|
+
: fixer.insertTextAfter(openingBrace, ` ${specifiers.join(', ')} `);
|
|
343
|
+
};
|
|
344
|
+
|
|
284
345
|
/** The exported name `specifier` imports: `x` for `import { x as y }`. */
|
|
285
346
|
export const importedNameOf = (
|
|
286
347
|
specifier: DeepReadonly<TSESTree.ImportSpecifier>,
|
|
@@ -289,6 +350,56 @@ export const importedNameOf = (
|
|
|
289
350
|
? specifier.imported.name
|
|
290
351
|
: specifier.imported.value;
|
|
291
352
|
|
|
353
|
+
/** The first fix `toFix` gives for `declarations`, trying them in order. */
|
|
354
|
+
const firstFix = (
|
|
355
|
+
declarations: readonly TSESTree.ImportDeclaration[],
|
|
356
|
+
toFix: (
|
|
357
|
+
declaration: TSESTree.ImportDeclaration,
|
|
358
|
+
) => TSESLint.RuleFix | undefined,
|
|
359
|
+
): TSESLint.RuleFix | undefined => {
|
|
360
|
+
for (const declaration of declarations) {
|
|
361
|
+
const fix = toFix(declaration);
|
|
362
|
+
|
|
363
|
+
if (fix !== undefined) {
|
|
364
|
+
return fix;
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
return undefined;
|
|
369
|
+
};
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* Adds `specifiers` to the braces of the first of `hosts` that has them, else
|
|
373
|
+
* puts braces after a lone default import among them (`import D, { x }`): a
|
|
374
|
+
* second declaration beside it would be a duplicate. Failing both, a new
|
|
375
|
+
* declaration importing them from `moduleName`.
|
|
376
|
+
*/
|
|
377
|
+
const addToFirstHost = (
|
|
378
|
+
fixer: TSESLint.RuleFixer,
|
|
379
|
+
sourceCode: TSESLint.SourceCode,
|
|
380
|
+
moduleName: string,
|
|
381
|
+
hosts: readonly TSESTree.ImportDeclaration[],
|
|
382
|
+
specifiers: readonly string[],
|
|
383
|
+
): readonly TSESLint.RuleFix[] => {
|
|
384
|
+
const intoBraces = firstFix(hosts, (declaration) =>
|
|
385
|
+
addToImportBraces(fixer, sourceCode, declaration, specifiers),
|
|
386
|
+
);
|
|
387
|
+
|
|
388
|
+
if (intoBraces !== undefined) {
|
|
389
|
+
return [intoBraces];
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
const defaultHost = hosts.find(
|
|
393
|
+
(declaration) =>
|
|
394
|
+
declaration.specifiers.length === 1 &&
|
|
395
|
+
declaration.specifiers[0]?.type === AST_NODE_TYPES.ImportDefaultSpecifier,
|
|
396
|
+
)?.specifiers[0];
|
|
397
|
+
|
|
398
|
+
return defaultHost !== undefined
|
|
399
|
+
? [fixer.insertTextAfter(defaultHost, `, { ${specifiers.join(', ')} }`)]
|
|
400
|
+
: buildNamedImportFix(fixer, sourceCode.ast, moduleName, specifiers);
|
|
401
|
+
};
|
|
402
|
+
|
|
292
403
|
/** What {@link bindValueImports} gives. */
|
|
293
404
|
export type ValueImportBinding = Readonly<{
|
|
294
405
|
/** The names to write, in the order of the export names asked for. */
|
|
@@ -303,13 +414,46 @@ type NamedSpecifier = Readonly<{
|
|
|
303
414
|
specifier: TSESTree.ImportSpecifier;
|
|
304
415
|
}>;
|
|
305
416
|
|
|
417
|
+
/** `import type …`. */
|
|
418
|
+
const isTypeOnlyDeclaration = (
|
|
419
|
+
declaration: TSESTree.ImportDeclaration,
|
|
420
|
+
): boolean => declaration.importKind === 'type';
|
|
421
|
+
|
|
422
|
+
/** The declaration is not `import type`. */
|
|
423
|
+
const isNotTypeOnly = ({ declaration }: NamedSpecifier): boolean =>
|
|
424
|
+
!isTypeOnlyDeclaration(declaration);
|
|
425
|
+
|
|
306
426
|
/** Neither `specifier` nor its declaration says `type`. */
|
|
307
427
|
const isValueImport = ({ declaration, specifier }: NamedSpecifier): boolean =>
|
|
308
428
|
isNotTypeOnly({ declaration, specifier }) && specifier.importKind !== 'type';
|
|
309
429
|
|
|
310
|
-
/**
|
|
311
|
-
|
|
312
|
-
|
|
430
|
+
/**
|
|
431
|
+
* The declarations a name can be added to, in the order to try them: those that
|
|
432
|
+
* are not `import type`, the ones with a value specifier first, since one
|
|
433
|
+
* holding only `type` specifiers is erased (and flagged by
|
|
434
|
+
* `no-import-type-side-effects`) unless a value joins it.
|
|
435
|
+
*/
|
|
436
|
+
const hostsFor = (
|
|
437
|
+
declarations: readonly TSESTree.ImportDeclaration[],
|
|
438
|
+
): readonly TSESTree.ImportDeclaration[] => {
|
|
439
|
+
const notTypeOnly = declarations.filter(
|
|
440
|
+
(declaration) => !isTypeOnlyDeclaration(declaration),
|
|
441
|
+
);
|
|
442
|
+
|
|
443
|
+
const hasValueSpecifier = (
|
|
444
|
+
declaration: TSESTree.ImportDeclaration,
|
|
445
|
+
): boolean =>
|
|
446
|
+
declaration.specifiers.some(
|
|
447
|
+
(specifier) =>
|
|
448
|
+
specifier.type === AST_NODE_TYPES.ImportSpecifier &&
|
|
449
|
+
isValueImport({ declaration, specifier }),
|
|
450
|
+
);
|
|
451
|
+
|
|
452
|
+
return [
|
|
453
|
+
...notTypeOnly.filter(hasValueSpecifier),
|
|
454
|
+
...notTypeOnly.filter((declaration) => !hasValueSpecifier(declaration)),
|
|
455
|
+
];
|
|
456
|
+
};
|
|
313
457
|
|
|
314
458
|
const bindingOf = (
|
|
315
459
|
sourceCode: TSESLint.SourceCode,
|
package/src/index.mts
CHANGED
package/src/negation.mts
CHANGED
|
@@ -137,10 +137,9 @@ export type NegationContext = Readonly<{
|
|
|
137
137
|
checker: ts.TypeChecker;
|
|
138
138
|
getLayerTypes: GetLayerTypes;
|
|
139
139
|
/**
|
|
140
|
-
* Whether `node`'s value is a boolean, so that the negation of `!node` may
|
|
141
|
-
*
|
|
142
|
-
*
|
|
143
|
-
* node.
|
|
140
|
+
* Whether `node`'s value is a boolean, so that the negation of `!node` may be
|
|
141
|
+
* `node` itself. Where only the truthiness of the negation is read — the test
|
|
142
|
+
* of an `if` — any value will do, and this may answer `true` for every node.
|
|
144
143
|
*/
|
|
145
144
|
isBoolean: (node: DeepReadonly<TSESTree.Expression>) => boolean;
|
|
146
145
|
}>;
|
|
@@ -159,12 +158,12 @@ export type NegationContext = Readonly<{
|
|
|
159
158
|
* | anything else | `!a`, `!(a)` | always |
|
|
160
159
|
*
|
|
161
160
|
* `undefined` when the first two rows' condition does not hold: then the only
|
|
162
|
-
* exact negation is `!!a` or `!(a < b)`, and a caller decides whether to
|
|
163
|
-
*
|
|
164
|
-
*
|
|
161
|
+
* exact negation is `!!a` or `!(a < b)`, and a caller decides whether to write
|
|
162
|
+
* it (with {@link prefixedNegationOf}) or to give up. Inside `&&` and `||`, a
|
|
163
|
+
* side with no short negation is written with `!`.
|
|
165
164
|
*
|
|
166
|
-
* The node is taken as written: a comparison under a type wrapper is not
|
|
167
|
-
*
|
|
165
|
+
* The node is taken as written: a comparison under a type wrapper is not looked
|
|
166
|
+
* into, since inverting it would drop the wrapper.
|
|
168
167
|
*/
|
|
169
168
|
export const negationOf = (
|
|
170
169
|
node: DeepReadonly<TSESTree.Expression>,
|
|
@@ -210,6 +209,34 @@ type Negation = Readonly<{
|
|
|
210
209
|
precedence: Precedence;
|
|
211
210
|
}>;
|
|
212
211
|
|
|
212
|
+
const isLooserThan = (precedence: Precedence, operator: '&&' | '||'): boolean =>
|
|
213
|
+
precedence === 'looser' || (operator === '&&' && precedence === '||');
|
|
214
|
+
|
|
215
|
+
const asOperand = (
|
|
216
|
+
negation: Negation,
|
|
217
|
+
operator: '&&' | '||' | undefined,
|
|
218
|
+
): string =>
|
|
219
|
+
operator !== undefined && isLooserThan(negation.precedence, operator)
|
|
220
|
+
? (`(${negation.text})` as const)
|
|
221
|
+
: negation.text;
|
|
222
|
+
|
|
223
|
+
const precedenceOf = (node: DeepReadonly<TSESTree.Node>): Precedence => {
|
|
224
|
+
if (node.type === AST_NODE_TYPES.LogicalExpression) {
|
|
225
|
+
// `??` cannot be mixed with `&&` or `||` without parentheses.
|
|
226
|
+
return node.operator === '??' ? 'looser' : node.operator;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
return LOOSER_THAN_LOGICAL.has(node.type) ? 'looser' : 'tighter';
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* The parser's node, which the source code and the token store take as mutable,
|
|
234
|
+
* for one that this package only reads.
|
|
235
|
+
*/
|
|
236
|
+
const asNode = <T extends TSESTree.Node>(node: DeepReadonly<T>): T =>
|
|
237
|
+
// eslint-disable-next-line total-functions/no-unsafe-type-assertion
|
|
238
|
+
node as T;
|
|
239
|
+
|
|
213
240
|
const shortNegationOf = (
|
|
214
241
|
node: DeepReadonly<TSESTree.Expression>,
|
|
215
242
|
context: NegationContext,
|
|
@@ -259,26 +286,6 @@ const shortNegationOf = (
|
|
|
259
286
|
};
|
|
260
287
|
};
|
|
261
288
|
|
|
262
|
-
const asOperand = (
|
|
263
|
-
negation: Negation,
|
|
264
|
-
operator: '&&' | '||' | undefined,
|
|
265
|
-
): string =>
|
|
266
|
-
operator !== undefined && isLooserThan(negation.precedence, operator)
|
|
267
|
-
? (`(${negation.text})` as const)
|
|
268
|
-
: negation.text;
|
|
269
|
-
|
|
270
|
-
const isLooserThan = (precedence: Precedence, operator: '&&' | '||'): boolean =>
|
|
271
|
-
precedence === 'looser' || (operator === '&&' && precedence === '||');
|
|
272
|
-
|
|
273
|
-
const precedenceOf = (node: DeepReadonly<TSESTree.Node>): Precedence => {
|
|
274
|
-
if (node.type === AST_NODE_TYPES.LogicalExpression) {
|
|
275
|
-
// `??` cannot be mixed with `&&` or `||` without parentheses.
|
|
276
|
-
return node.operator === '??' ? 'looser' : node.operator;
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
return LOOSER_THAN_LOGICAL.has(node.type) ? 'looser' : 'tighter';
|
|
280
|
-
};
|
|
281
|
-
|
|
282
289
|
const LOOSER_THAN_LOGICAL: ReadonlySet<AST_NODE_TYPES> =
|
|
283
290
|
new Set<AST_NODE_TYPES>([
|
|
284
291
|
AST_NODE_TYPES.ArrowFunctionExpression,
|
|
@@ -348,11 +355,3 @@ const isNeverNaN = (type: ts.Type, checker: ts.TypeChecker): boolean => {
|
|
|
348
355
|
checker.typeToString(brandType) === 'false'
|
|
349
356
|
);
|
|
350
357
|
};
|
|
351
|
-
|
|
352
|
-
/**
|
|
353
|
-
* The parser's node, which the source code and the token store take as
|
|
354
|
-
* mutable, for one that this package only reads.
|
|
355
|
-
*/
|
|
356
|
-
const asNode = <T extends TSESTree.Node>(node: DeepReadonly<T>): T =>
|
|
357
|
-
// eslint-disable-next-line total-functions/no-unsafe-type-assertion
|
|
358
|
-
node as T;
|
|
@@ -115,51 +115,8 @@ const isChainOperand = (
|
|
|
115
115
|
parenthesesDepth(sourceCode, node) === 0;
|
|
116
116
|
|
|
117
117
|
/**
|
|
118
|
-
*
|
|
119
|
-
*
|
|
120
|
-
* and after it: `a && u !== null && typeof u === 'object' && b`. They have to
|
|
121
|
-
* be next to each other, so that the pair is one range of the source.
|
|
122
|
-
*/
|
|
123
|
-
const findNonNullObjectPairs = (
|
|
124
|
-
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
125
|
-
sourceCode: TSESLint.SourceCode,
|
|
126
|
-
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
127
|
-
node: TSESTree.LogicalExpression,
|
|
128
|
-
): readonly NonNullObjectPair[] => {
|
|
129
|
-
const operands = chainOperands(sourceCode, node);
|
|
130
|
-
|
|
131
|
-
const pairsFrom = (index: number): readonly NonNullObjectPair[] => {
|
|
132
|
-
const first = operands[index];
|
|
133
|
-
|
|
134
|
-
const second = operands[index + 1];
|
|
135
|
-
|
|
136
|
-
if (first === undefined || second === undefined) {
|
|
137
|
-
return [];
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
const identifierName =
|
|
141
|
-
pairIdentifierName(first, second) ?? pairIdentifierName(second, first);
|
|
142
|
-
|
|
143
|
-
return identifierName === undefined
|
|
144
|
-
? pairsFrom(index + 1)
|
|
145
|
-
: [
|
|
146
|
-
{
|
|
147
|
-
range: [
|
|
148
|
-
getRangeWithParens(sourceCode, first)[0],
|
|
149
|
-
getRangeWithParens(sourceCode, second)[1],
|
|
150
|
-
],
|
|
151
|
-
identifierName,
|
|
152
|
-
},
|
|
153
|
-
...pairsFrom(index + 2),
|
|
154
|
-
];
|
|
155
|
-
};
|
|
156
|
-
|
|
157
|
-
return pairsFrom(0);
|
|
158
|
-
};
|
|
159
|
-
|
|
160
|
-
/**
|
|
161
|
-
* The operands of the `&&` chain `node`, left to right. A parenthesized `&&`
|
|
162
|
-
* 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.
|
|
163
120
|
*/
|
|
164
121
|
const chainOperands = (
|
|
165
122
|
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
@@ -178,26 +135,11 @@ const chainOperands = (
|
|
|
178
135
|
// Every operand below is read through its type wrappers: `typeof (u satisfies
|
|
179
136
|
// unknown) === 'object' && u! !== null` is the same check.
|
|
180
137
|
|
|
181
|
-
|
|
182
|
-
* The name of `u` when `typeofCheck` is `typeof u === 'object'` and
|
|
183
|
-
* `nullCheck` is `u !== null` for the same `u`.
|
|
184
|
-
*/
|
|
185
|
-
const pairIdentifierName = (
|
|
186
|
-
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
187
|
-
typeofCheck: TSESTree.Expression,
|
|
138
|
+
const isLiteralOf = (
|
|
188
139
|
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
skipTypeWrappers(typeofCheck),
|
|
193
|
-
);
|
|
194
|
-
|
|
195
|
-
return identifierName !== undefined &&
|
|
196
|
-
identifierName ===
|
|
197
|
-
getNonNullCheckIdentifierName(skipTypeWrappers(nullCheck))
|
|
198
|
-
? identifierName
|
|
199
|
-
: undefined;
|
|
200
|
-
};
|
|
140
|
+
node: TSESTree.Node,
|
|
141
|
+
value: string | null,
|
|
142
|
+
): boolean => node.type === AST_NODE_TYPES.Literal && node.value === value;
|
|
201
143
|
|
|
202
144
|
/** `u` of `typeof u === 'object'`, in either operand order. */
|
|
203
145
|
const getTypeofObjectIdentifierName = (
|
|
@@ -237,8 +179,8 @@ const getTypeofObjectIdentifierName = (
|
|
|
237
179
|
|
|
238
180
|
/**
|
|
239
181
|
* `u` of `u !== null`, in either operand order. `u != null` also excludes
|
|
240
|
-
* `undefined`, which `typeof u === 'object'` has already excluded, so it is
|
|
241
|
-
*
|
|
182
|
+
* `undefined`, which `typeof u === 'object'` has already excluded, so it is the
|
|
183
|
+
* same check here.
|
|
242
184
|
*/
|
|
243
185
|
const getNonNullCheckIdentifierName = (
|
|
244
186
|
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
@@ -264,8 +206,66 @@ const getNonNullCheckIdentifierName = (
|
|
|
264
206
|
return checked?.type === AST_NODE_TYPES.Identifier ? checked.name : undefined;
|
|
265
207
|
};
|
|
266
208
|
|
|
267
|
-
|
|
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 = (
|
|
268
214
|
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
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
|
+
};
|
|
@@ -45,11 +45,11 @@ export const createPreferIsRecordAndHasKey = ({
|
|
|
45
45
|
/**
|
|
46
46
|
* Whether the `isRecord(...)` half of the rewrite would be dead code
|
|
47
47
|
* because the object already satisfies `hasKey`'s
|
|
48
|
-
* `R extends UnknownRecord` constraint, at every layer of type
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
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.
|
|
53
53
|
*/
|
|
54
54
|
const isKnownRecord = (
|
|
55
55
|
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|