eslint-rule-forge 0.3.1 → 0.4.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 +32 -33
- 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 +47 -6
- package/dist/imports.d.mts.map +1 -1
- package/dist/imports.mjs +123 -6
- 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.mjs.map +1 -1
- package/dist/rules/no-side-effect-import.mjs +1 -1
- package/dist/rules/no-side-effect-import.mjs.map +1 -1
- package/dist/rules/prefer-array-is-array.d.mts.map +1 -1
- package/dist/rules/prefer-array-is-array.mjs +14 -20
- 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 +25 -26
- 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 +33 -45
- package/dist/rules/prefer-is-record-and-has-key.mjs.map +1 -1
- package/dist/rules/prefer-safe-parse.d.mts.map +1 -1
- package/dist/rules/prefer-safe-parse.mjs +17 -16
- package/dist/rules/prefer-safe-parse.mjs.map +1 -1
- package/dist/type-wrappers.mjs.map +1 -1
- package/package.json +7 -7
- package/src/comments.mts +23 -0
- package/src/imports.mts +217 -17
- package/src/index.mts +1 -0
- package/src/negation.mts +26 -24
- package/src/rules/no-side-effect-import.mts +1 -1
- package/src/rules/prefer-array-is-array.mts +24 -37
- package/src/rules/prefer-is-non-null-object.mts +38 -43
- package/src/rules/prefer-is-record-and-has-key.mts +43 -67
- package/src/rules/prefer-safe-parse.mts +32 -39
- package/src/type-wrappers.mts +1 -1
package/src/imports.mts
CHANGED
|
@@ -140,33 +140,233 @@ export const buildCalleeResolver = (
|
|
|
140
140
|
|
|
141
141
|
/**
|
|
142
142
|
* A fix that inserts `import { <specifiers> } from '<moduleName>';` at the top
|
|
143
|
-
* of the file,
|
|
143
|
+
* of the file, after its directive prologue (`'use client';` stays a
|
|
144
|
+
* directive), or no fix when `specifiers` is empty. Each specifier is written
|
|
144
145
|
* as given, so `type T` imports a type.
|
|
145
146
|
*
|
|
146
|
-
*
|
|
147
|
-
*
|
|
148
|
-
*
|
|
149
|
-
*
|
|
150
|
-
*
|
|
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
|
+
* keeps `import-x/no-duplicates` satisfied, and falls back to this. An import
|
|
150
|
+
* sorter orders the declaration afterwards. Put every name one report needs
|
|
151
|
+
* into a single call: ESLint merges the fixes of one report into one edit,
|
|
152
|
+
* and two fixes inserting at the same place conflict, so that one of them is
|
|
153
|
+
* dropped.
|
|
151
154
|
*/
|
|
152
155
|
export const buildNamedImportFix = (
|
|
153
156
|
fixer: TSESLint.RuleFixer,
|
|
154
157
|
program: TSESTree.Program,
|
|
155
158
|
moduleName: string,
|
|
156
159
|
specifiers: readonly string[],
|
|
157
|
-
): readonly TSESLint.RuleFix[] =>
|
|
158
|
-
specifiers.length === 0
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
160
|
+
): readonly TSESLint.RuleFix[] => {
|
|
161
|
+
if (specifiers.length === 0) {
|
|
162
|
+
return [] as const;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const declaration =
|
|
166
|
+
`import { ${specifiers.join(', ')} } from '${moduleName}';` as const;
|
|
167
|
+
|
|
168
|
+
const lastDirective = program.body
|
|
169
|
+
.filter(
|
|
170
|
+
(statement): statement is TSESTree.ExpressionStatement =>
|
|
171
|
+
statement.type === AST_NODE_TYPES.ExpressionStatement &&
|
|
172
|
+
statement.directive !== undefined,
|
|
173
|
+
)
|
|
174
|
+
.at(-1);
|
|
175
|
+
|
|
176
|
+
if (lastDirective === undefined) {
|
|
177
|
+
return [fixer.insertTextBefore(program, `${declaration}\n`)] as const;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// After the comments that start on the directive's last line as well: one
|
|
181
|
+
// such as `// eslint-disable-line` is about that line.
|
|
182
|
+
const anchor =
|
|
183
|
+
(program.comments ?? [])
|
|
184
|
+
.filter(
|
|
185
|
+
(comment) =>
|
|
186
|
+
comment.range[0] >= lastDirective.range[1] &&
|
|
187
|
+
comment.loc.start.line === lastDirective.loc.end.line,
|
|
188
|
+
)
|
|
189
|
+
.at(-1) ?? lastDirective;
|
|
190
|
+
|
|
191
|
+
return [fixer.insertTextAfter(anchor, `\n${declaration}`)] as const;
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* The local names `moduleName`'s exports `exportNames` are bound to as values
|
|
196
|
+
* once `edits` are made, in the same order, or `undefined` when one of them
|
|
197
|
+
* cannot be bound.
|
|
198
|
+
*
|
|
199
|
+
* - Imported as a value: that local name, and no edit.
|
|
200
|
+
* - Imported by `import { type x }`: the edit drops `type`, keeping a comment
|
|
201
|
+
* after it.
|
|
202
|
+
* - Not imported: the edit adds it to the first named import from the module
|
|
203
|
+
* that has a value specifier, or else to any named import from it that is
|
|
204
|
+
* not `import type`. Adding it there rather than in a new declaration keeps
|
|
205
|
+
* `import-x/no-duplicates` satisfied. Failing that, it goes into braces
|
|
206
|
+
* after a lone default import (`import D, { x }`), and when the module has
|
|
207
|
+
* neither, a new declaration goes after the directive prologue
|
|
208
|
+
* ({@link buildNamedImportFix}).
|
|
209
|
+
* - Refused when an import already binds the name to something else
|
|
210
|
+
* (`import type { x }`, `import { y as x }`, `import { x } from 'other'`),
|
|
211
|
+
* where adding it would be a duplicate binding.
|
|
212
|
+
*
|
|
213
|
+
* ESLint's default parser leaves `importKind` out, which counts as a value
|
|
214
|
+
* 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
|
+
* that loses a conflict with another in the same pass is dropped. Fixes that
|
|
217
|
+
* all add a name overlap, so `--fix` applies one per pass, and the rest in the
|
|
218
|
+
* next, by when the file imports it. Whether something other than an import
|
|
219
|
+
* binds the name where it is written is the caller's to check
|
|
220
|
+
* ({@link hasForeignBinding}).
|
|
221
|
+
*/
|
|
222
|
+
export const bindValueImports = (
|
|
223
|
+
sourceCode: TSESLint.SourceCode,
|
|
224
|
+
moduleName: string,
|
|
225
|
+
exportNames: readonly string[],
|
|
226
|
+
): ValueImportBinding | undefined => {
|
|
227
|
+
const program = sourceCode.ast;
|
|
228
|
+
|
|
229
|
+
const allImports = program.body.filter(
|
|
230
|
+
(node): node is TSESTree.ImportDeclaration =>
|
|
231
|
+
node.type === AST_NODE_TYPES.ImportDeclaration,
|
|
232
|
+
);
|
|
233
|
+
|
|
234
|
+
const declarations = getImportDeclarations(program, moduleName);
|
|
235
|
+
|
|
236
|
+
const named = declarations.flatMap((declaration) =>
|
|
237
|
+
declaration.specifiers.flatMap((specifier) =>
|
|
238
|
+
specifier.type === AST_NODE_TYPES.ImportSpecifier
|
|
239
|
+
? [{ declaration, specifier }]
|
|
240
|
+
: [],
|
|
241
|
+
),
|
|
242
|
+
);
|
|
243
|
+
|
|
244
|
+
const bindings = exportNames.map((exportName) =>
|
|
245
|
+
bindingOf(sourceCode, allImports, named, exportName),
|
|
246
|
+
);
|
|
247
|
+
|
|
248
|
+
if (bindings.includes(undefined)) {
|
|
249
|
+
return undefined;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const defined = bindings.filter((b) => b !== undefined);
|
|
253
|
+
|
|
254
|
+
const toAdd = defined.flatMap((b) => (b.isAdded ? [b.localName] : []));
|
|
255
|
+
|
|
256
|
+
const host = (
|
|
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];
|
|
268
|
+
|
|
269
|
+
return {
|
|
270
|
+
localNames: defined.map((b) => b.localName),
|
|
271
|
+
edits: (fixer) => [
|
|
272
|
+
...defined.flatMap((b) => b.edits(fixer)),
|
|
273
|
+
...(toAdd.length === 0
|
|
274
|
+
? []
|
|
275
|
+
: host !== undefined
|
|
276
|
+
? [fixer.insertTextAfter(host, `, ${toAdd.join(', ')}`)]
|
|
277
|
+
: defaultHost !== undefined
|
|
278
|
+
? [fixer.insertTextAfter(defaultHost, `, { ${toAdd.join(', ')} }`)]
|
|
279
|
+
: buildNamedImportFix(fixer, program, moduleName, toAdd)),
|
|
280
|
+
],
|
|
281
|
+
};
|
|
282
|
+
};
|
|
283
|
+
|
|
284
|
+
/** The exported name `specifier` imports: `x` for `import { x as y }`. */
|
|
285
|
+
export const importedNameOf = (
|
|
168
286
|
specifier: DeepReadonly<TSESTree.ImportSpecifier>,
|
|
169
287
|
): string =>
|
|
170
288
|
specifier.imported.type === AST_NODE_TYPES.Identifier
|
|
171
289
|
? specifier.imported.name
|
|
172
290
|
: specifier.imported.value;
|
|
291
|
+
|
|
292
|
+
/** What {@link bindValueImports} gives. */
|
|
293
|
+
export type ValueImportBinding = Readonly<{
|
|
294
|
+
/** The names to write, in the order of the export names asked for. */
|
|
295
|
+
localNames: readonly string[];
|
|
296
|
+
|
|
297
|
+
/** The edits that bring every one of them into scope. */
|
|
298
|
+
edits: (fixer: TSESLint.RuleFixer) => readonly TSESLint.RuleFix[];
|
|
299
|
+
}>;
|
|
300
|
+
|
|
301
|
+
type NamedSpecifier = Readonly<{
|
|
302
|
+
declaration: TSESTree.ImportDeclaration;
|
|
303
|
+
specifier: TSESTree.ImportSpecifier;
|
|
304
|
+
}>;
|
|
305
|
+
|
|
306
|
+
/** Neither `specifier` nor its declaration says `type`. */
|
|
307
|
+
const isValueImport = ({ declaration, specifier }: NamedSpecifier): boolean =>
|
|
308
|
+
isNotTypeOnly({ declaration, specifier }) && specifier.importKind !== 'type';
|
|
309
|
+
|
|
310
|
+
/** The declaration is not `import type`. */
|
|
311
|
+
const isNotTypeOnly = ({ declaration }: NamedSpecifier): boolean =>
|
|
312
|
+
declaration.importKind !== 'type';
|
|
313
|
+
|
|
314
|
+
const bindingOf = (
|
|
315
|
+
sourceCode: TSESLint.SourceCode,
|
|
316
|
+
allImports: readonly TSESTree.ImportDeclaration[],
|
|
317
|
+
named: readonly NamedSpecifier[],
|
|
318
|
+
exportName: string,
|
|
319
|
+
):
|
|
320
|
+
| Readonly<{
|
|
321
|
+
localName: string;
|
|
322
|
+
isAdded: boolean;
|
|
323
|
+
edits: (fixer: TSESLint.RuleFixer) => readonly TSESLint.RuleFix[];
|
|
324
|
+
}>
|
|
325
|
+
| undefined => {
|
|
326
|
+
const matching = named.filter(
|
|
327
|
+
({ specifier }) => importedNameOf(specifier) === exportName,
|
|
328
|
+
);
|
|
329
|
+
|
|
330
|
+
const value = matching.find(isValueImport);
|
|
331
|
+
|
|
332
|
+
if (value !== undefined) {
|
|
333
|
+
return {
|
|
334
|
+
localName: value.specifier.local.name,
|
|
335
|
+
isAdded: false,
|
|
336
|
+
edits: () => [],
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
const inlineType = matching.find(isNotTypeOnly)?.specifier;
|
|
341
|
+
|
|
342
|
+
if (inlineType !== undefined) {
|
|
343
|
+
const typeKeyword = sourceCode.getFirstToken(inlineType);
|
|
344
|
+
|
|
345
|
+
// Up to whatever follows `type`, a comment included, so that the comment
|
|
346
|
+
// stays.
|
|
347
|
+
const next =
|
|
348
|
+
typeKeyword === null
|
|
349
|
+
? null
|
|
350
|
+
: sourceCode.getTokenAfter(typeKeyword, { includeComments: true });
|
|
351
|
+
|
|
352
|
+
return typeKeyword === null || next === null
|
|
353
|
+
? undefined
|
|
354
|
+
: {
|
|
355
|
+
localName: inlineType.local.name,
|
|
356
|
+
isAdded: false,
|
|
357
|
+
edits: (fixer) => [
|
|
358
|
+
fixer.removeRange([typeKeyword.range[0], next.range[0]]),
|
|
359
|
+
],
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
const isTaken = allImports.some((declaration) =>
|
|
364
|
+
declaration.specifiers.some(
|
|
365
|
+
(specifier) => specifier.local.name === exportName,
|
|
366
|
+
),
|
|
367
|
+
);
|
|
368
|
+
|
|
369
|
+
return isTaken
|
|
370
|
+
? undefined
|
|
371
|
+
: { localName: exportName, isAdded: true, edits: () => [] };
|
|
372
|
+
};
|
package/src/index.mts
CHANGED
package/src/negation.mts
CHANGED
|
@@ -279,32 +279,34 @@ const precedenceOf = (node: DeepReadonly<TSESTree.Node>): Precedence => {
|
|
|
279
279
|
return LOOSER_THAN_LOGICAL.has(node.type) ? 'looser' : 'tighter';
|
|
280
280
|
};
|
|
281
281
|
|
|
282
|
-
const LOOSER_THAN_LOGICAL: ReadonlySet<AST_NODE_TYPES> =
|
|
283
|
-
AST_NODE_TYPES
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
282
|
+
const LOOSER_THAN_LOGICAL: ReadonlySet<AST_NODE_TYPES> =
|
|
283
|
+
new Set<AST_NODE_TYPES>([
|
|
284
|
+
AST_NODE_TYPES.ArrowFunctionExpression,
|
|
285
|
+
AST_NODE_TYPES.AssignmentExpression,
|
|
286
|
+
AST_NODE_TYPES.ConditionalExpression,
|
|
287
|
+
AST_NODE_TYPES.SequenceExpression,
|
|
288
|
+
AST_NODE_TYPES.YieldExpression,
|
|
289
|
+
]);
|
|
289
290
|
|
|
290
291
|
/** Expressions `!` can be put in front of without parentheses. */
|
|
291
|
-
const BINDS_AS_TIGHT_AS_UNARY: ReadonlySet<AST_NODE_TYPES> =
|
|
292
|
-
AST_NODE_TYPES
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
292
|
+
const BINDS_AS_TIGHT_AS_UNARY: ReadonlySet<AST_NODE_TYPES> =
|
|
293
|
+
new Set<AST_NODE_TYPES>([
|
|
294
|
+
AST_NODE_TYPES.ArrayExpression,
|
|
295
|
+
AST_NODE_TYPES.AwaitExpression,
|
|
296
|
+
AST_NODE_TYPES.CallExpression,
|
|
297
|
+
AST_NODE_TYPES.ChainExpression,
|
|
298
|
+
AST_NODE_TYPES.Identifier,
|
|
299
|
+
AST_NODE_TYPES.Literal,
|
|
300
|
+
AST_NODE_TYPES.MemberExpression,
|
|
301
|
+
AST_NODE_TYPES.MetaProperty,
|
|
302
|
+
AST_NODE_TYPES.NewExpression,
|
|
303
|
+
AST_NODE_TYPES.ObjectExpression,
|
|
304
|
+
AST_NODE_TYPES.TaggedTemplateExpression,
|
|
305
|
+
AST_NODE_TYPES.TemplateLiteral,
|
|
306
|
+
AST_NODE_TYPES.ThisExpression,
|
|
307
|
+
AST_NODE_TYPES.TSNonNullExpression,
|
|
308
|
+
AST_NODE_TYPES.UnaryExpression,
|
|
309
|
+
]);
|
|
308
310
|
|
|
309
311
|
const RELATIONAL_OPERATORS: ReadonlySet<ComparisonOperator> =
|
|
310
312
|
new Set<ComparisonOperator>(['<', '<=', '>', '>=']);
|
|
@@ -28,6 +28,7 @@ export const createNoSideEffectImport = ({
|
|
|
28
28
|
fixable: 'code',
|
|
29
29
|
schema: [],
|
|
30
30
|
messages: { removeSideEffectImport: message },
|
|
31
|
+
defaultOptions: [],
|
|
31
32
|
},
|
|
32
33
|
|
|
33
34
|
create: (context) => ({
|
|
@@ -43,7 +44,6 @@ export const createNoSideEffectImport = ({
|
|
|
43
44
|
});
|
|
44
45
|
},
|
|
45
46
|
}),
|
|
46
|
-
defaultOptions: [],
|
|
47
47
|
}) as const;
|
|
48
48
|
|
|
49
49
|
/* eslint-disable @typescript-eslint/prefer-readonly-parameter-types */
|
|
@@ -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 } from '../parentheses.mjs';
|
|
12
9
|
import { isIdentifierNamed, skipTypeWrappers } from '../type-wrappers.mjs';
|
|
13
10
|
|
|
@@ -38,15 +35,12 @@ export const createPreferArrayIsArray = ({
|
|
|
38
35
|
fixable: 'code',
|
|
39
36
|
schema: [],
|
|
40
37
|
messages: { useIsArray: text },
|
|
38
|
+
defaultOptions: [],
|
|
41
39
|
},
|
|
42
40
|
|
|
43
41
|
create: (context) => {
|
|
44
42
|
const sourceCode = context.sourceCode;
|
|
45
43
|
|
|
46
|
-
const program = sourceCode.ast;
|
|
47
|
-
|
|
48
|
-
const moduleImports = getImportDeclarations(program, moduleName);
|
|
49
|
-
|
|
50
44
|
const mut_nodesToFix: TSESTree.CallExpression[] = [];
|
|
51
45
|
|
|
52
46
|
return {
|
|
@@ -58,43 +52,36 @@ export const createPreferArrayIsArray = ({
|
|
|
58
52
|
mut_nodesToFix.push(node);
|
|
59
53
|
},
|
|
60
54
|
'Program:exit': () => {
|
|
61
|
-
const
|
|
62
|
-
getNamedImports(moduleImports).includes(namespace);
|
|
55
|
+
const binding = bindValueImports(sourceCode, moduleName, [namespace]);
|
|
63
56
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
//
|
|
67
|
-
//
|
|
68
|
-
|
|
69
|
-
for (const [index, node] of mut_nodesToFix.entries()) {
|
|
57
|
+
const localName = binding?.localNames[0];
|
|
58
|
+
|
|
59
|
+
// Every fix brings the namespace into scope itself, so that one
|
|
60
|
+
// applied alone (an editor's quick fix) is complete.
|
|
61
|
+
for (const node of mut_nodesToFix) {
|
|
70
62
|
context.report({
|
|
71
63
|
node,
|
|
72
64
|
messageId: 'useIsArray',
|
|
73
|
-
fix:
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
replacement,
|
|
89
|
-
),
|
|
90
|
-
];
|
|
91
|
-
},
|
|
65
|
+
fix:
|
|
66
|
+
binding === undefined ||
|
|
67
|
+
localName === undefined ||
|
|
68
|
+
hasForeignBinding(sourceCode, node, localName, moduleName)
|
|
69
|
+
? null
|
|
70
|
+
: (fixer) => [
|
|
71
|
+
...binding.edits(fixer),
|
|
72
|
+
// Only the callee is replaced, type wrappers included,
|
|
73
|
+
// along with the parentheses around it, which its range
|
|
74
|
+
// leaves out; the arguments stay as written.
|
|
75
|
+
fixer.replaceTextRange(
|
|
76
|
+
getRangeWithParens(sourceCode, node.callee),
|
|
77
|
+
`${localName}.isArray`,
|
|
78
|
+
),
|
|
79
|
+
],
|
|
92
80
|
});
|
|
93
81
|
}
|
|
94
82
|
},
|
|
95
83
|
};
|
|
96
84
|
},
|
|
97
|
-
defaultOptions: [],
|
|
98
85
|
} as const;
|
|
99
86
|
};
|
|
100
87
|
|
|
@@ -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
|
|
|
@@ -33,16 +30,14 @@ export const createPreferIsNonNullObject = ({
|
|
|
33
30
|
messages: {
|
|
34
31
|
useIsNonNullObject: `Replace the object/null check with \`isNonNullObject()\` from ${moduleName}.`,
|
|
35
32
|
},
|
|
33
|
+
defaultOptions: [],
|
|
36
34
|
},
|
|
37
35
|
|
|
38
36
|
create: (context) => {
|
|
39
37
|
const sourceCode = context.sourceCode;
|
|
40
38
|
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
const moduleImports = getImportDeclarations(program, moduleName);
|
|
44
|
-
|
|
45
|
-
const mut_pairsToFix: NonNullObjectPair[] = [];
|
|
39
|
+
const mut_pairsToFix: (NonNullObjectPair &
|
|
40
|
+
Readonly<{ chain: TSESTree.LogicalExpression }>)[] = [];
|
|
46
41
|
|
|
47
42
|
return {
|
|
48
43
|
LogicalExpression: (node) => {
|
|
@@ -51,48 +46,48 @@ export const createPreferIsNonNullObject = ({
|
|
|
51
46
|
return;
|
|
52
47
|
}
|
|
53
48
|
|
|
54
|
-
mut_pairsToFix.push(
|
|
49
|
+
mut_pairsToFix.push(
|
|
50
|
+
...findNonNullObjectPairs(sourceCode, node).map((pair) => ({
|
|
51
|
+
...pair,
|
|
52
|
+
chain: node,
|
|
53
|
+
})),
|
|
54
|
+
);
|
|
55
55
|
},
|
|
56
56
|
'Program:exit': () => {
|
|
57
|
-
const
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
//
|
|
64
|
-
//
|
|
65
|
-
for (const
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|
+
|
|
69
71
|
context.report({
|
|
70
|
-
loc
|
|
71
|
-
start: sourceCode.getLocFromIndex(range[0]),
|
|
72
|
-
end: sourceCode.getLocFromIndex(range[1]),
|
|
73
|
-
},
|
|
72
|
+
loc,
|
|
74
73
|
messageId: 'useIsNonNullObject',
|
|
75
|
-
fix:
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
fixer.replaceTextRange(range, replacement),
|
|
88
|
-
];
|
|
89
|
-
},
|
|
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
|
+
],
|
|
90
86
|
});
|
|
91
87
|
}
|
|
92
88
|
},
|
|
93
89
|
};
|
|
94
90
|
},
|
|
95
|
-
defaultOptions: [],
|
|
96
91
|
}) as const;
|
|
97
92
|
|
|
98
93
|
/**
|