eslint-plugin-ts-data-forge 0.10.5 → 0.10.6

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 (88) hide show
  1. package/dist/rules/ast-utils.d.mts +5 -5
  2. package/dist/rules/ast-utils.d.mts.map +1 -1
  3. package/dist/rules/ast-utils.mjs +3 -3
  4. package/dist/rules/ast-utils.mjs.map +1 -1
  5. package/dist/rules/import-binding.d.mts +38 -0
  6. package/dist/rules/import-binding.d.mts.map +1 -0
  7. package/dist/rules/import-binding.mjs +53 -0
  8. package/dist/rules/import-binding.mjs.map +1 -0
  9. package/dist/rules/mutable-result.mjs +37 -37
  10. package/dist/rules/mutable-result.mjs.map +1 -1
  11. package/dist/rules/no-unnecessary-type-guard.d.mts.map +1 -1
  12. package/dist/rules/no-unnecessary-type-guard.mjs +146 -143
  13. package/dist/rules/no-unnecessary-type-guard.mjs.map +1 -1
  14. package/dist/rules/prefer-arr-generate.d.mts.map +1 -1
  15. package/dist/rules/prefer-arr-generate.mjs +129 -131
  16. package/dist/rules/prefer-arr-generate.mjs.map +1 -1
  17. package/dist/rules/prefer-arr-is-bounded-length-array.d.mts.map +1 -1
  18. package/dist/rules/prefer-arr-is-bounded-length-array.mjs +14 -16
  19. package/dist/rules/prefer-arr-is-bounded-length-array.mjs.map +1 -1
  20. package/dist/rules/prefer-arr-is-fixed-length-array.d.mts.map +1 -1
  21. package/dist/rules/prefer-arr-is-fixed-length-array.mjs +15 -17
  22. package/dist/rules/prefer-arr-is-fixed-length-array.mjs.map +1 -1
  23. package/dist/rules/prefer-arr-is-max-length-array.d.mts.map +1 -1
  24. package/dist/rules/prefer-arr-is-max-length-array.mjs +14 -16
  25. package/dist/rules/prefer-arr-is-max-length-array.mjs.map +1 -1
  26. package/dist/rules/prefer-arr-is-min-length-array.d.mts.map +1 -1
  27. package/dist/rules/prefer-arr-is-min-length-array.mjs +14 -16
  28. package/dist/rules/prefer-arr-is-min-length-array.mjs.map +1 -1
  29. package/dist/rules/prefer-arr-is-non-empty.d.mts.map +1 -1
  30. package/dist/rules/prefer-arr-is-non-empty.mjs +14 -16
  31. package/dist/rules/prefer-arr-is-non-empty.mjs.map +1 -1
  32. package/dist/rules/prefer-arr-scan.d.mts.map +1 -1
  33. package/dist/rules/prefer-arr-scan.mjs +9 -18
  34. package/dist/rules/prefer-arr-scan.mjs.map +1 -1
  35. package/dist/rules/prefer-arr-sum.d.mts.map +1 -1
  36. package/dist/rules/prefer-arr-sum.mjs +16 -21
  37. package/dist/rules/prefer-arr-sum.mjs.map +1 -1
  38. package/dist/rules/prefer-arr-uniq.d.mts.map +1 -1
  39. package/dist/rules/prefer-arr-uniq.mjs +38 -43
  40. package/dist/rules/prefer-arr-uniq.mjs.map +1 -1
  41. package/dist/rules/prefer-as-int.d.mts.map +1 -1
  42. package/dist/rules/prefer-as-int.mjs +19 -26
  43. package/dist/rules/prefer-as-int.mjs.map +1 -1
  44. package/dist/rules/prefer-canonical-array-slicing.d.mts.map +1 -1
  45. package/dist/rules/prefer-canonical-array-slicing.mjs +49 -15
  46. package/dist/rules/prefer-canonical-array-slicing.mjs.map +1 -1
  47. package/dist/rules/prefer-canonical-length-cast.mjs +15 -15
  48. package/dist/rules/prefer-canonical-length-cast.mjs.map +1 -1
  49. package/dist/rules/prefer-obj-over-entries-round-trip.d.mts.map +1 -1
  50. package/dist/rules/prefer-obj-over-entries-round-trip.mjs +100 -99
  51. package/dist/rules/prefer-obj-over-entries-round-trip.mjs.map +1 -1
  52. package/dist/rules/prefer-pipe-map-nullable.d.mts.map +1 -1
  53. package/dist/rules/prefer-pipe-map-nullable.mjs +2 -4
  54. package/dist/rules/prefer-pipe-map-nullable.mjs.map +1 -1
  55. package/dist/rules/prefer-range-for-loop.d.mts.map +1 -1
  56. package/dist/rules/prefer-range-for-loop.mjs +18 -24
  57. package/dist/rules/prefer-range-for-loop.mjs.map +1 -1
  58. package/package.json +4 -4
  59. package/src/rules/ast-utils.mts +11 -11
  60. package/src/rules/import-binding.mts +94 -0
  61. package/src/rules/mutable-result.mts +56 -56
  62. package/src/rules/no-unnecessary-type-guard.mts +199 -203
  63. package/src/rules/no-unnecessary-type-guard.test.mts +62 -2
  64. package/src/rules/prefer-arr-generate.mts +184 -194
  65. package/src/rules/prefer-arr-generate.test.mts +262 -134
  66. package/src/rules/prefer-arr-is-bounded-length-array.mts +22 -33
  67. package/src/rules/prefer-arr-is-fixed-length-array.mts +24 -36
  68. package/src/rules/prefer-arr-is-fixed-length-array.test.mts +16 -7
  69. package/src/rules/prefer-arr-is-max-length-array.mts +21 -34
  70. package/src/rules/prefer-arr-is-max-length-array.test.mts +16 -7
  71. package/src/rules/prefer-arr-is-min-length-array.mts +21 -34
  72. package/src/rules/prefer-arr-is-min-length-array.test.mts +16 -7
  73. package/src/rules/prefer-arr-is-non-empty.mts +17 -34
  74. package/src/rules/prefer-arr-is-non-empty.test.mts +110 -0
  75. package/src/rules/prefer-arr-scan.mts +11 -36
  76. package/src/rules/prefer-arr-scan.test.mts +55 -0
  77. package/src/rules/prefer-arr-sum.mts +19 -33
  78. package/src/rules/prefer-arr-uniq.mts +49 -60
  79. package/src/rules/prefer-arr-uniq.test.mts +46 -20
  80. package/src/rules/prefer-as-int.mts +27 -52
  81. package/src/rules/prefer-as-int.test.mts +79 -29
  82. package/src/rules/prefer-canonical-array-slicing.mts +86 -24
  83. package/src/rules/prefer-canonical-array-slicing.test.mts +133 -34
  84. package/src/rules/prefer-canonical-length-cast.mts +18 -18
  85. package/src/rules/prefer-obj-over-entries-round-trip.mts +152 -170
  86. package/src/rules/prefer-obj-over-entries-round-trip.test.mts +48 -6
  87. package/src/rules/prefer-pipe-map-nullable.mts +6 -8
  88. package/src/rules/prefer-range-for-loop.mts +31 -55
@@ -74,62 +74,6 @@ const outermostTypePreservingWrapper = (
74
74
  : node;
75
75
  };
76
76
 
77
- const isUsedAsMutable = (
78
- // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
79
- checker: ts.TypeChecker,
80
- // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
81
- getTsNode: (esNode: TSESTree.Node) => ts.Node | undefined,
82
- // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
83
- node: TSESTree.Node,
84
- ): boolean => {
85
- const parent = node.parent;
86
-
87
- if (
88
- parent?.type === AST_NODE_TYPES.MemberExpression &&
89
- parent.object === node
90
- ) {
91
- // xs.push(...), xs.sort(), ...
92
- if (
93
- !parent.computed &&
94
- parent.property.type === AST_NODE_TYPES.Identifier &&
95
- mutatingMethods.has(parent.property.name)
96
- ) {
97
- return true;
98
- }
99
-
100
- // xs[0] = ..., xs.length = ..., xs[0]++, delete xs[0]
101
- const grandParent = parent.parent;
102
-
103
- return (
104
- (grandParent.type === AST_NODE_TYPES.AssignmentExpression &&
105
- grandParent.left === parent) ||
106
- grandParent.type === AST_NODE_TYPES.UpdateExpression ||
107
- (grandParent.type === AST_NODE_TYPES.UnaryExpression &&
108
- grandParent.operator === 'delete')
109
- );
110
- }
111
-
112
- // Passed to a parameter, assigned to a variable, or returned from a function
113
- // whose declared type is a mutable array.
114
- const tsNode = getTsNode(node);
115
-
116
- if (tsNode === undefined) {
117
- return false;
118
- }
119
-
120
- if (!ts.isExpression(tsNode)) {
121
- return false;
122
- }
123
-
124
- const expectedType =
125
- getDeclaredParameterType(checker, tsNode) ??
126
- checker.getContextualType(tsNode);
127
-
128
- return (
129
- expectedType !== undefined && isMutableArrayOnly(checker, expectedType)
130
- );
131
- };
132
-
133
77
  /**
134
78
  * The declared type of the parameter `argument` is passed to. The contextual
135
79
  * type of an argument to a generic function is instantiated from that very
@@ -190,6 +134,62 @@ const isMutableArrayOnly = (
190
134
  );
191
135
  };
192
136
 
137
+ const isUsedAsMutable = (
138
+ // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
139
+ checker: ts.TypeChecker,
140
+ // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
141
+ getTsNode: (esNode: TSESTree.Node) => ts.Node | undefined,
142
+ // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
143
+ node: TSESTree.Node,
144
+ ): boolean => {
145
+ const parent = node.parent;
146
+
147
+ if (
148
+ parent?.type === AST_NODE_TYPES.MemberExpression &&
149
+ parent.object === node
150
+ ) {
151
+ // xs.push(...), xs.sort(), ...
152
+ if (
153
+ !parent.computed &&
154
+ parent.property.type === AST_NODE_TYPES.Identifier &&
155
+ mutatingMethods.has(parent.property.name)
156
+ ) {
157
+ return true;
158
+ }
159
+
160
+ // xs[0] = ..., xs.length = ..., xs[0]++, delete xs[0]
161
+ const grandParent = parent.parent;
162
+
163
+ return (
164
+ (grandParent.type === AST_NODE_TYPES.AssignmentExpression &&
165
+ grandParent.left === parent) ||
166
+ grandParent.type === AST_NODE_TYPES.UpdateExpression ||
167
+ (grandParent.type === AST_NODE_TYPES.UnaryExpression &&
168
+ grandParent.operator === 'delete')
169
+ );
170
+ }
171
+
172
+ // Passed to a parameter, assigned to a variable, or returned from a function
173
+ // whose declared type is a mutable array.
174
+ const tsNode = getTsNode(node);
175
+
176
+ if (tsNode === undefined) {
177
+ return false;
178
+ }
179
+
180
+ if (!ts.isExpression(tsNode)) {
181
+ return false;
182
+ }
183
+
184
+ const expectedType =
185
+ getDeclaredParameterType(checker, tsNode) ??
186
+ checker.getContextualType(tsNode);
187
+
188
+ return (
189
+ expectedType !== undefined && isMutableArrayOnly(checker, expectedType)
190
+ );
191
+ };
192
+
193
193
  /** Methods that `ReadonlyArray` lacks and that mutate the receiver. */
194
194
  const mutatingMethods: ReadonlySet<string> = new Set<string>([
195
195
  'copyWithin',
@@ -6,14 +6,13 @@ import {
6
6
  } from '@typescript-eslint/utils';
7
7
  import {
8
8
  buildCalleeResolver,
9
- buildNamedImportFix,
10
9
  getImportDeclarations,
11
- getImportedLocalName,
12
10
  typeWrapperLayers,
13
11
  } from 'eslint-rule-forge';
14
12
  import type { DeepReadonly, ReadonlyRecord } from 'ts-type-forge';
15
13
  import * as ts from 'typescript';
16
14
  import { TS_DATA_FORGE_MODULE } from './constants.mjs';
15
+ import { tsDataForgeValueAtByName } from './import-binding.mjs';
17
16
 
18
17
  type Options = readonly [
19
18
  Readonly<{
@@ -178,39 +177,32 @@ export const noUnnecessaryTypeGuard: TSESLint.RuleModule<MessageIds, Options> =
178
177
 
179
178
  const resolveGuard = buildCalleeResolver(tsDataForgeImports);
180
179
 
181
- const buildReplaceFix =
182
- (
183
- isNamespace: boolean,
184
- // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
185
- propertyNode: TSESTree.Node,
186
- replacement: string,
187
- ): TSESLint.ReportFixFunction =>
188
- (fixer) => {
189
- // A namespace member access (`tf.isX`) exposes every export, so the
190
- // canonical name is always in scope.
191
- if (isNamespace) {
192
- return [fixer.replaceText(propertyNode, replacement)];
193
- }
194
-
195
- // If the replacement guard is already imported (possibly under an
196
- // alias), rename to its in-scope local name. Otherwise add a new
197
- // import and rename to the canonical name.
198
- const localName = getImportedLocalName(
199
- tsDataForgeImports,
200
- replacement,
201
- );
180
+ // One binding per replacement guard, however many reports use it.
181
+ const guardAt = tsDataForgeValueAtByName(sourceCode);
202
182
 
203
- if (localName !== undefined) {
204
- return [fixer.replaceText(propertyNode, localName)];
205
- }
206
-
207
- return [
208
- ...buildNamedImportFix(fixer, program, TS_DATA_FORGE_MODULE, [
209
- replacement,
210
- ]),
211
- fixer.replaceText(propertyNode, replacement),
212
- ];
213
- };
183
+ const buildReplaceFix = (
184
+ isNamespace: boolean,
185
+ // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
186
+ propertyNode: TSESTree.Node,
187
+ replacement: string,
188
+ ): TSESLint.ReportFixFunction | null => {
189
+ // A namespace member access (`tf.isX`) exposes every export, so the
190
+ // canonical name is always in scope.
191
+ if (isNamespace) {
192
+ return (fixer) => [fixer.replaceText(propertyNode, replacement)];
193
+ }
194
+
195
+ // The replacement guard's local name, added to the imports when the
196
+ // file does not import it; no fix when it cannot be bound there.
197
+ const guard = guardAt(replacement)(propertyNode);
198
+
199
+ return guard === undefined
200
+ ? null
201
+ : (fixer) => [
202
+ ...guard.edits(fixer),
203
+ fixer.replaceText(propertyNode, guard.localName),
204
+ ];
205
+ };
214
206
 
215
207
  const reportConstant = (
216
208
  node: DeepReadonly<TSESTree.CallExpression>,
@@ -250,7 +242,11 @@ export const noUnnecessaryTypeGuard: TSESLint.RuleModule<MessageIds, Options> =
250
242
  // eslint-disable-next-line total-functions/no-unsafe-type-assertion
251
243
  node: node as TSESTree.Node,
252
244
  messageId: 'replaceTypeGuard',
253
- data: { from, to },
245
+ // The name the fix writes: an aliased import's local name.
246
+ data: {
247
+ from,
248
+ to: isNamespace ? to : (guardAt(to)(propertyNode)?.localName ?? to),
249
+ },
254
250
  fix: buildReplaceFix(isNamespace, propertyNode, to),
255
251
  });
256
252
  };
@@ -335,6 +331,174 @@ type Verdict = DeepReadonly<
335
331
  | { kind: 'replace'; replacement: string }
336
332
  >;
337
333
 
334
+ const replacementVerdict = (
335
+ replacement: string | undefined,
336
+ ): Verdict | undefined =>
337
+ replacement === undefined
338
+ ? undefined
339
+ : ({ kind: 'replace', replacement } as const);
340
+
341
+ /**
342
+ * Decomposes a type into its union members. Returns `undefined` when any member
343
+ * is opaque (`any` / `unknown` / `never`) or deferred (type parameter, indexed
344
+ * access, …), signalling that the rule should not draw conclusions.
345
+ */
346
+ // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
347
+ const collectUnionParts = (type: ts.Type): readonly ts.Type[] | undefined => {
348
+ const parts = type.isUnion() ? type.types : ([type] as const);
349
+
350
+ for (const part of parts) {
351
+ if ((part.flags & DEFERRED_OR_OPAQUE_FLAGS) !== 0) {
352
+ return undefined;
353
+ }
354
+ }
355
+
356
+ return parts;
357
+ };
358
+
359
+ const isCallableType = (
360
+ // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
361
+ type: ts.Type,
362
+ // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
363
+ checker: ts.TypeChecker,
364
+ ): boolean =>
365
+ checker.getSignaturesOfType(type, ts.SignatureKind.Call).length > 0 ||
366
+ checker.getSignaturesOfType(type, ts.SignatureKind.Construct).length > 0;
367
+
368
+ /**
369
+ * Whether `isRecord` is guaranteed to accept this (non-union) type.
370
+ *
371
+ * `isRecord` narrows to `UnknownRecord` (`ReadonlyRecord<string, unknown>`), so
372
+ * a string index signature is exactly what it asks for and its value type is
373
+ * `unknown`, which anything satisfies. Arrays and callables are excluded even
374
+ * when they carry one, because `isRecord` rejects them at runtime
375
+ * (`Array.isArray` is true; `typeof fn === 'object'` is false).
376
+ *
377
+ * Deliberately conservative: an object type without an index signature of its
378
+ * own is reported as neither guaranteed-accepted nor guaranteed-rejected, even
379
+ * where TypeScript would accept it through an implicit index signature.
380
+ */
381
+ const isGuaranteedRecord = (
382
+ // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
383
+ type: ts.Type,
384
+ // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
385
+ checker: ts.TypeChecker,
386
+ ): boolean =>
387
+ checker.getIndexInfoOfType(type, ts.IndexKind.String) !== undefined &&
388
+ !isCallableType(type, checker) &&
389
+ !checker.isArrayType(type) &&
390
+ !checker.isTupleType(type);
391
+
392
+ /** Classifies a single (non-union) type into a primitive {@link Atom}. */
393
+ // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
394
+ const classifyAtom = (type: ts.Type): Atom => {
395
+ const { flags } = type;
396
+
397
+ if ((flags & ts.TypeFlags.Undefined) !== 0) {
398
+ return 'undefined';
399
+ }
400
+
401
+ if ((flags & ts.TypeFlags.Null) !== 0) {
402
+ return 'null';
403
+ }
404
+
405
+ if ((flags & ts.TypeFlags.BooleanLike) !== 0) {
406
+ return 'boolean';
407
+ }
408
+
409
+ if ((flags & ts.TypeFlags.NumberLike) !== 0) {
410
+ return 'number';
411
+ }
412
+
413
+ if ((flags & ts.TypeFlags.StringLike) !== 0) {
414
+ return 'string';
415
+ }
416
+
417
+ if ((flags & ts.TypeFlags.BigIntLike) !== 0) {
418
+ return 'bigint';
419
+ }
420
+
421
+ if ((flags & ts.TypeFlags.ESSymbolLike) !== 0) {
422
+ return 'symbol';
423
+ }
424
+
425
+ // Branded primitives (e.g. `NonEmptyString = string & {...}`) are
426
+ // intersections; classify them by their underlying primitive constituent.
427
+ if (type.isIntersection()) {
428
+ for (const constituent of type.types) {
429
+ const atom = classifyAtom(constituent);
430
+
431
+ if (atom !== 'other') {
432
+ return atom;
433
+ }
434
+ }
435
+ }
436
+
437
+ return 'other';
438
+ };
439
+
440
+ /**
441
+ * Whether `isRecord` is guaranteed to reject this (non-union) type: every
442
+ * primitive (`null` included), every array or tuple, and every callable fails
443
+ * its `typeof x === 'object' && x !== null && !Array.isArray(x)` check.
444
+ */
445
+ const isGuaranteedNotRecord = (
446
+ // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
447
+ type: ts.Type,
448
+ // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
449
+ checker: ts.TypeChecker,
450
+ ): boolean =>
451
+ classifyAtom(type) !== 'other' ||
452
+ checker.isArrayType(type) ||
453
+ checker.isTupleType(type) ||
454
+ isCallableType(type, checker);
455
+
456
+ /** A stable key for a set of atoms, used to look up replacement guards. */
457
+ const joinAtoms = (atoms: readonly AtomKey[]): string =>
458
+ ATOM_ORDER.filter((atom) => atoms.includes(atom)).join('|');
459
+
460
+ /**
461
+ * Returns `true` when the type is guaranteed to be a non-empty string: either a
462
+ * non-empty string literal or a value branded as `NonEmptyString`
463
+ * (`MinLengthString<N>` with `N >= 1`).
464
+ */
465
+ const isGuaranteedNonEmptyString = (
466
+ // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
467
+ type: ts.Type,
468
+ // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
469
+ checker: ts.TypeChecker,
470
+ ): boolean => {
471
+ if (type.isStringLiteral()) {
472
+ return type.value !== '';
473
+ }
474
+
475
+ if (type.getProperty(BRAND_MARKER_PROPERTY) === undefined) {
476
+ return false;
477
+ }
478
+
479
+ const minLengthSymbol = type.getProperty(MIN_LENGTH_BRAND_KEY);
480
+
481
+ if (minLengthSymbol === undefined) {
482
+ return false;
483
+ }
484
+
485
+ // The `MinLength` brand value is `MinLengthTuple<N, 0>`; it has a required
486
+ // first element (`'0'`) exactly when `N >= 1`, i.e. the string is non-empty.
487
+ // `MinLengthString<0>` brands with a plain array and has no such element.
488
+ return (
489
+ checker.getTypeOfSymbol(minLengthSymbol).getProperty('0') !== undefined
490
+ );
491
+ };
492
+
493
+ /**
494
+ * Returns `true` when the type can possibly be a non-empty string (a general
495
+ * `string`, a branded string, or a non-empty string literal). The empty string
496
+ * literal and non-string types cannot.
497
+ */
498
+ // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
499
+ const couldBeNonEmptyString = (type: ts.Type): boolean =>
500
+ type.isStringLiteral() ? type.value !== '' : classifyAtom(type) === 'string';
501
+
338
502
  /**
339
503
  * What a guard with `spec` does on an argument of `type`: always the same
340
504
  * boolean, the same as a narrower guard, or nothing the rule can say.
@@ -435,13 +599,6 @@ const verdictOf = (
435
599
  }
436
600
  };
437
601
 
438
- const replacementVerdict = (
439
- replacement: string | undefined,
440
- ): Verdict | undefined =>
441
- replacement === undefined
442
- ? undefined
443
- : ({ kind: 'replace', replacement } as const);
444
-
445
602
  /**
446
603
  * Whether `verdict`, drawn from one layer's type, also holds for `type`. A
447
604
  * constant has to come out the same. A replacement holds where the rule would
@@ -481,121 +638,6 @@ const holdsOn = (
481
638
  );
482
639
  };
483
640
 
484
- /**
485
- * Decomposes a type into its union members. Returns `undefined` when any member
486
- * is opaque (`any` / `unknown` / `never`) or deferred (type parameter, indexed
487
- * access, …), signalling that the rule should not draw conclusions.
488
- */
489
- // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
490
- const collectUnionParts = (type: ts.Type): readonly ts.Type[] | undefined => {
491
- const parts = type.isUnion() ? type.types : ([type] as const);
492
-
493
- for (const part of parts) {
494
- if ((part.flags & DEFERRED_OR_OPAQUE_FLAGS) !== 0) {
495
- return undefined;
496
- }
497
- }
498
-
499
- return parts;
500
- };
501
-
502
- /**
503
- * Whether `isRecord` is guaranteed to accept this (non-union) type.
504
- *
505
- * `isRecord` narrows to `UnknownRecord` (`ReadonlyRecord<string, unknown>`), so
506
- * a string index signature is exactly what it asks for and its value type is
507
- * `unknown`, which anything satisfies. Arrays and callables are excluded even
508
- * when they carry one, because `isRecord` rejects them at runtime
509
- * (`Array.isArray` is true; `typeof fn === 'object'` is false).
510
- *
511
- * Deliberately conservative: an object type without an index signature of its
512
- * own is reported as neither guaranteed-accepted nor guaranteed-rejected, even
513
- * where TypeScript would accept it through an implicit index signature.
514
- */
515
- const isGuaranteedRecord = (
516
- // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
517
- type: ts.Type,
518
- // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
519
- checker: ts.TypeChecker,
520
- ): boolean =>
521
- checker.getIndexInfoOfType(type, ts.IndexKind.String) !== undefined &&
522
- !isCallableType(type, checker) &&
523
- !checker.isArrayType(type) &&
524
- !checker.isTupleType(type);
525
-
526
- /**
527
- * Whether `isRecord` is guaranteed to reject this (non-union) type: every
528
- * primitive (`null` included), every array or tuple, and every callable fails
529
- * its `typeof x === 'object' && x !== null && !Array.isArray(x)` check.
530
- */
531
- const isGuaranteedNotRecord = (
532
- // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
533
- type: ts.Type,
534
- // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
535
- checker: ts.TypeChecker,
536
- ): boolean =>
537
- classifyAtom(type) !== 'other' ||
538
- checker.isArrayType(type) ||
539
- checker.isTupleType(type) ||
540
- isCallableType(type, checker);
541
-
542
- const isCallableType = (
543
- // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
544
- type: ts.Type,
545
- // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
546
- checker: ts.TypeChecker,
547
- ): boolean =>
548
- checker.getSignaturesOfType(type, ts.SignatureKind.Call).length > 0 ||
549
- checker.getSignaturesOfType(type, ts.SignatureKind.Construct).length > 0;
550
-
551
- /** Classifies a single (non-union) type into a primitive {@link Atom}. */
552
- // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
553
- const classifyAtom = (type: ts.Type): Atom => {
554
- const { flags } = type;
555
-
556
- if ((flags & ts.TypeFlags.Undefined) !== 0) {
557
- return 'undefined';
558
- }
559
-
560
- if ((flags & ts.TypeFlags.Null) !== 0) {
561
- return 'null';
562
- }
563
-
564
- if ((flags & ts.TypeFlags.BooleanLike) !== 0) {
565
- return 'boolean';
566
- }
567
-
568
- if ((flags & ts.TypeFlags.NumberLike) !== 0) {
569
- return 'number';
570
- }
571
-
572
- if ((flags & ts.TypeFlags.StringLike) !== 0) {
573
- return 'string';
574
- }
575
-
576
- if ((flags & ts.TypeFlags.BigIntLike) !== 0) {
577
- return 'bigint';
578
- }
579
-
580
- if ((flags & ts.TypeFlags.ESSymbolLike) !== 0) {
581
- return 'symbol';
582
- }
583
-
584
- // Branded primitives (e.g. `NonEmptyString = string & {...}`) are
585
- // intersections; classify them by their underlying primitive constituent.
586
- if (type.isIntersection()) {
587
- for (const constituent of type.types) {
588
- const atom = classifyAtom(constituent);
589
-
590
- if (atom !== 'other') {
591
- return atom;
592
- }
593
- }
594
- }
595
-
596
- return 'other';
597
- };
598
-
599
641
  const ATOM_ORDER: readonly AtomKey[] = [
600
642
  'undefined',
601
643
  'null',
@@ -606,52 +648,6 @@ const ATOM_ORDER: readonly AtomKey[] = [
606
648
  'symbol',
607
649
  ] as const;
608
650
 
609
- /** A stable key for a set of atoms, used to look up replacement guards. */
610
- const joinAtoms = (atoms: readonly AtomKey[]): string =>
611
- ATOM_ORDER.filter((atom) => atoms.includes(atom)).join('|');
612
-
613
- /**
614
- * Returns `true` when the type is guaranteed to be a non-empty string: either a
615
- * non-empty string literal or a value branded as `NonEmptyString`
616
- * (`MinLengthString<N>` with `N >= 1`).
617
- */
618
- const isGuaranteedNonEmptyString = (
619
- // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
620
- type: ts.Type,
621
- // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
622
- checker: ts.TypeChecker,
623
- ): boolean => {
624
- if (type.isStringLiteral()) {
625
- return type.value !== '';
626
- }
627
-
628
- if (type.getProperty(BRAND_MARKER_PROPERTY) === undefined) {
629
- return false;
630
- }
631
-
632
- const minLengthSymbol = type.getProperty(MIN_LENGTH_BRAND_KEY);
633
-
634
- if (minLengthSymbol === undefined) {
635
- return false;
636
- }
637
-
638
- // The `MinLength` brand value is `MinLengthTuple<N, 0>`; it has a required
639
- // first element (`'0'`) exactly when `N >= 1`, i.e. the string is non-empty.
640
- // `MinLengthString<0>` brands with a plain array and has no such element.
641
- return (
642
- checker.getTypeOfSymbol(minLengthSymbol).getProperty('0') !== undefined
643
- );
644
- };
645
-
646
- /**
647
- * Returns `true` when the type can possibly be a non-empty string (a general
648
- * `string`, a branded string, or a non-empty string literal). The empty string
649
- * literal and non-string types cannot.
650
- */
651
- // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
652
- const couldBeNonEmptyString = (type: ts.Type): boolean =>
653
- type.isStringLiteral() ? type.value !== '' : classifyAtom(type) === 'string';
654
-
655
651
  /**
656
652
  * Returns `true` when evaluating the expression cannot have observable side
657
653
  * effects, so replacing the surrounding call with a literal is safe. Limited to
@@ -406,13 +406,73 @@ describe('no-unnecessary-type-guard', () => {
406
406
  const y = isNonNullish(x);
407
407
  `,
408
408
  output: dedent`
409
- import { isNotNull } from 'ts-data-forge';
410
- import { isNonNullish } from 'ts-data-forge';
409
+ import { isNonNullish, isNotNull } from 'ts-data-forge';
411
410
  declare const x: string | null;
412
411
  const y = isNotNull(x);
413
412
  `,
414
413
  errors: [{ messageId: 'replaceTypeGuard' }],
415
414
  },
415
+ {
416
+ name: 'the message names an aliased replacement guard by its local name',
417
+ code: dedent`
418
+ import { isNonNullish, isNotNull as inn } from 'ts-data-forge';
419
+ declare const x: string | null;
420
+ const y = isNonNullish(x);
421
+ `,
422
+ output: dedent`
423
+ import { isNonNullish, isNotNull as inn } from 'ts-data-forge';
424
+ declare const x: string | null;
425
+ const y = inn(x);
426
+ `,
427
+ errors: [
428
+ {
429
+ messageId: 'replaceTypeGuard',
430
+ data: { from: 'isNonNullish', to: 'inn' },
431
+ },
432
+ ],
433
+ },
434
+ {
435
+ name: 'several reports bind each replacement guard they write',
436
+ code: dedent`
437
+ import { isNonNullish } from 'ts-data-forge';
438
+ declare const x: string | null;
439
+ declare const z: string | undefined;
440
+ const a = isNonNullish(x);
441
+ const b = isNonNullish(x);
442
+ const c = isNonNullish(z);
443
+ `,
444
+ output: [
445
+ dedent`
446
+ import { isNonNullish, isNotNull } from 'ts-data-forge';
447
+ declare const x: string | null;
448
+ declare const z: string | undefined;
449
+ const a = isNotNull(x);
450
+ const b = isNonNullish(x);
451
+ const c = isNonNullish(z);
452
+ `,
453
+ dedent`
454
+ import { isNonNullish, isNotNull, isNotUndefined } from 'ts-data-forge';
455
+ declare const x: string | null;
456
+ declare const z: string | undefined;
457
+ const a = isNotNull(x);
458
+ const b = isNonNullish(x);
459
+ const c = isNotUndefined(z);
460
+ `,
461
+ dedent`
462
+ import { isNonNullish, isNotNull, isNotUndefined } from 'ts-data-forge';
463
+ declare const x: string | null;
464
+ declare const z: string | undefined;
465
+ const a = isNotNull(x);
466
+ const b = isNotNull(x);
467
+ const c = isNotUndefined(z);
468
+ `,
469
+ ],
470
+ errors: [
471
+ { messageId: 'replaceTypeGuard' },
472
+ { messageId: 'replaceTypeGuard' },
473
+ { messageId: 'replaceTypeGuard' },
474
+ ],
475
+ },
416
476
  {
417
477
  name: 'isNullish that only matches undefined -> isUndefined',
418
478
  code: dedent`