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.
Files changed (40) hide show
  1. package/README.md +32 -33
  2. package/dist/comments.d.mts +10 -0
  3. package/dist/comments.d.mts.map +1 -0
  4. package/dist/comments.mjs +11 -0
  5. package/dist/comments.mjs.map +1 -0
  6. package/dist/imports.d.mts +47 -6
  7. package/dist/imports.d.mts.map +1 -1
  8. package/dist/imports.mjs +123 -6
  9. package/dist/imports.mjs.map +1 -1
  10. package/dist/index.d.mts +1 -0
  11. package/dist/index.d.mts.map +1 -1
  12. package/dist/index.mjs +1 -0
  13. package/dist/index.mjs.map +1 -1
  14. package/dist/negation.mjs.map +1 -1
  15. package/dist/rules/no-side-effect-import.mjs +1 -1
  16. package/dist/rules/no-side-effect-import.mjs.map +1 -1
  17. package/dist/rules/prefer-array-is-array.d.mts.map +1 -1
  18. package/dist/rules/prefer-array-is-array.mjs +14 -20
  19. package/dist/rules/prefer-array-is-array.mjs.map +1 -1
  20. package/dist/rules/prefer-is-non-null-object.d.mts.map +1 -1
  21. package/dist/rules/prefer-is-non-null-object.mjs +25 -26
  22. package/dist/rules/prefer-is-non-null-object.mjs.map +1 -1
  23. package/dist/rules/prefer-is-record-and-has-key.d.mts.map +1 -1
  24. package/dist/rules/prefer-is-record-and-has-key.mjs +33 -45
  25. package/dist/rules/prefer-is-record-and-has-key.mjs.map +1 -1
  26. package/dist/rules/prefer-safe-parse.d.mts.map +1 -1
  27. package/dist/rules/prefer-safe-parse.mjs +17 -16
  28. package/dist/rules/prefer-safe-parse.mjs.map +1 -1
  29. package/dist/type-wrappers.mjs.map +1 -1
  30. package/package.json +7 -7
  31. package/src/comments.mts +23 -0
  32. package/src/imports.mts +217 -17
  33. package/src/index.mts +1 -0
  34. package/src/negation.mts +26 -24
  35. package/src/rules/no-side-effect-import.mts +1 -1
  36. package/src/rules/prefer-array-is-array.mts +24 -37
  37. package/src/rules/prefer-is-non-null-object.mts +38 -43
  38. package/src/rules/prefer-is-record-and-has-key.mts +43 -67
  39. package/src/rules/prefer-safe-parse.mts +32 -39
  40. 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, or no fix when `specifiers` is empty. Each specifier is written
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
- * The placement is deliberately naive: an import sorter
147
- * (prettier-plugin-organize-imports or any equivalent) merges and orders it
148
- * afterwards. Put every name one report needs into a single call: ESLint
149
- * merges the fixes of one report into one edit, and two fixes inserting at the
150
- * start of the file conflict, so that one of them is dropped.
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
- ? ([] as const)
160
- : ([
161
- fixer.insertTextBefore(
162
- program,
163
- `import { ${specifiers.join(', ')} } from '${moduleName}';\n`,
164
- ),
165
- ] as const);
166
-
167
- const importedNameOf = (
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
@@ -1,4 +1,5 @@
1
1
  export * from './bindings.mjs';
2
+ export * from './comments.mjs';
2
3
  export * from './imports.mjs';
3
4
  export * from './negation.mjs';
4
5
  export * from './parentheses.mjs';
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> = new Set([
283
- AST_NODE_TYPES.ArrowFunctionExpression,
284
- AST_NODE_TYPES.AssignmentExpression,
285
- AST_NODE_TYPES.ConditionalExpression,
286
- AST_NODE_TYPES.SequenceExpression,
287
- AST_NODE_TYPES.YieldExpression,
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> = new Set([
292
- AST_NODE_TYPES.ArrayExpression,
293
- AST_NODE_TYPES.AwaitExpression,
294
- AST_NODE_TYPES.CallExpression,
295
- AST_NODE_TYPES.ChainExpression,
296
- AST_NODE_TYPES.Identifier,
297
- AST_NODE_TYPES.Literal,
298
- AST_NODE_TYPES.MemberExpression,
299
- AST_NODE_TYPES.MetaProperty,
300
- AST_NODE_TYPES.NewExpression,
301
- AST_NODE_TYPES.ObjectExpression,
302
- AST_NODE_TYPES.TaggedTemplateExpression,
303
- AST_NODE_TYPES.TemplateLiteral,
304
- AST_NODE_TYPES.ThisExpression,
305
- AST_NODE_TYPES.TSNonNullExpression,
306
- AST_NODE_TYPES.UnaryExpression,
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
- buildNamedImportFix,
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 hasNamespaceImport =
62
- getNamedImports(moduleImports).includes(namespace);
55
+ const binding = bindValueImports(sourceCode, moduleName, [namespace]);
63
56
 
64
- // The import fix lives on the first report only:
65
- // `insertTextBefore(program, …)` produces overlapping ranges
66
- // otherwise and only one of them would survive a pass. A first
67
- // report silenced by an `eslint-disable` comment therefore takes
68
- // the import with it.
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: (fixer) => {
74
- const importFixes =
75
- index === 0 && !hasNamespaceImport
76
- ? buildNamedImportFix(fixer, program, moduleName, [
77
- namespace,
78
- ])
79
- : ([] as const);
80
-
81
- // Only the callee is replaced, type wrappers included, along
82
- // with the parentheses around it, which its range leaves out;
83
- // the arguments stay as written.
84
- return [
85
- ...importFixes,
86
- fixer.replaceTextRange(
87
- getRangeWithParens(sourceCode, node.callee),
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
- buildNamedImportFix,
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 program = sourceCode.ast;
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(...findNonNullObjectPairs(sourceCode, node));
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 hasIsNonNullObjectImport =
58
- getNamedImports(moduleImports).includes('isNonNullObject');
59
-
60
- // The import fix lives on the first report only:
61
- // `insertTextBefore(program, …)` produces overlapping ranges
62
- // otherwise and only one of them would survive a pass. A first
63
- // report silenced by an `eslint-disable` comment therefore takes
64
- // the import with it.
65
- for (const [
66
- index,
67
- { range, identifierName },
68
- ] of mut_pairsToFix.entries()) {
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: (fixer) => {
76
- const replacement = `isNonNullObject(${identifierName})`;
77
-
78
- const importFixes =
79
- index === 0 && !hasIsNonNullObjectImport
80
- ? buildNamedImportFix(fixer, program, moduleName, [
81
- 'isNonNullObject',
82
- ])
83
- : [];
84
-
85
- return [
86
- ...importFixes,
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
  /**