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/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
- * a named import, the property for a namespace import.
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
- * 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.
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 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
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
- * 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
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 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];
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
- : host !== undefined
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
- /** The declaration is not `import type`. */
311
- const isNotTypeOnly = ({ declaration }: NamedSpecifier): boolean =>
312
- declaration.importKind !== 'type';
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
@@ -4,4 +4,5 @@ export * from './imports.mjs';
4
4
  export * from './negation.mjs';
5
5
  export * from './parentheses.mjs';
6
6
  export * from './rules/index.mjs';
7
+ export * from './statements.mjs';
7
8
  export * from './type-wrappers.mjs';
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
- * be `node` itself. Where only the truthiness of the negation is read — the
142
- * test of an `if` — any value will do, and this may answer `true` for every
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
- * write it (with {@link prefixedNegationOf}) or to give up. Inside `&&` and
164
- * `||`, a side with no short negation is written with `!`.
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
- * looked into, since inverting it would drop the wrapper.
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
- * Every non-overlapping pair in the `&&` chain `node` heads, left to right. The
119
- * two halves may come in either order, and the pair may have operands before
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
- nullCheck: TSESTree.Expression,
190
- ): string | undefined => {
191
- const identifierName = getTypeofObjectIdentifierName(
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
- * the same check here.
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
- const isLiteralOf = (
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
- node: TSESTree.Node,
270
- value: string | null,
271
- ): boolean => node.type === AST_NODE_TYPES.Literal && node.value === value;
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
- * wrappers: an array cast to a record is still rejected by `isRecord`,
50
- * and a record widened to one that may be an array is the author saying
51
- * it may need the guard. Without type information nothing is known, so
52
- * the guard is kept.
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