eslint-plugin-ts-data-forge 0.8.1 → 0.8.3
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/dist/plugin.d.mts +1 -1
- package/dist/plugin.d.mts.map +1 -1
- package/dist/plugin.mjs +1 -0
- package/dist/plugin.mjs.map +1 -1
- package/dist/rule-types.d.mts +3 -3
- package/dist/rule-types.d.mts.map +1 -1
- package/dist/rules/ast-utils.d.mts +59 -3
- package/dist/rules/ast-utils.d.mts.map +1 -1
- package/dist/rules/ast-utils.mjs +142 -31
- package/dist/rules/ast-utils.mjs.map +1 -1
- package/dist/rules/branded-number-types.mjs.map +1 -1
- package/dist/rules/import-utils.d.mts +1 -1
- package/dist/rules/import-utils.d.mts.map +1 -1
- package/dist/rules/import-utils.mjs +26 -13
- package/dist/rules/import-utils.mjs.map +1 -1
- package/dist/rules/no-side-effect-import.d.mts +1 -1
- package/dist/rules/no-side-effect-import.d.mts.map +1 -1
- package/dist/rules/no-unnecessary-type-guard.d.mts.map +1 -1
- package/dist/rules/no-unnecessary-type-guard.mjs +161 -101
- package/dist/rules/no-unnecessary-type-guard.mjs.map +1 -1
- package/dist/rules/prefer-arr-is-array.d.mts.map +1 -1
- package/dist/rules/prefer-arr-is-array.mjs +25 -11
- package/dist/rules/prefer-arr-is-array.mjs.map +1 -1
- package/dist/rules/prefer-arr-is-bounded-length-array.d.mts +1 -1
- package/dist/rules/prefer-arr-is-bounded-length-array.d.mts.map +1 -1
- package/dist/rules/prefer-arr-is-bounded-length-array.mjs +11 -21
- package/dist/rules/prefer-arr-is-bounded-length-array.mjs.map +1 -1
- package/dist/rules/prefer-arr-is-fixed-length-array.d.mts +1 -1
- package/dist/rules/prefer-arr-is-fixed-length-array.d.mts.map +1 -1
- package/dist/rules/prefer-arr-is-fixed-length-array.mjs +16 -28
- package/dist/rules/prefer-arr-is-fixed-length-array.mjs.map +1 -1
- package/dist/rules/prefer-arr-is-max-length-array.d.mts +1 -1
- package/dist/rules/prefer-arr-is-max-length-array.d.mts.map +1 -1
- package/dist/rules/prefer-arr-is-max-length-array.mjs +16 -24
- package/dist/rules/prefer-arr-is-max-length-array.mjs.map +1 -1
- package/dist/rules/prefer-arr-is-min-length-array.d.mts +1 -1
- package/dist/rules/prefer-arr-is-min-length-array.d.mts.map +1 -1
- package/dist/rules/prefer-arr-is-min-length-array.mjs +16 -24
- package/dist/rules/prefer-arr-is-min-length-array.mjs.map +1 -1
- package/dist/rules/prefer-arr-is-non-empty.d.mts.map +1 -1
- package/dist/rules/prefer-arr-is-non-empty.mjs +9 -22
- package/dist/rules/prefer-arr-is-non-empty.mjs.map +1 -1
- package/dist/rules/prefer-arr-scan.d.mts +6 -5
- package/dist/rules/prefer-arr-scan.d.mts.map +1 -1
- package/dist/rules/prefer-arr-scan.mjs +68 -31
- package/dist/rules/prefer-arr-scan.mjs.map +1 -1
- package/dist/rules/prefer-arr-sum.d.mts.map +1 -1
- package/dist/rules/prefer-arr-sum.mjs +57 -40
- package/dist/rules/prefer-arr-sum.mjs.map +1 -1
- package/dist/rules/prefer-arr-uniq.d.mts.map +1 -1
- package/dist/rules/prefer-arr-uniq.mjs +59 -30
- package/dist/rules/prefer-arr-uniq.mjs.map +1 -1
- package/dist/rules/prefer-as-int.d.mts.map +1 -1
- package/dist/rules/prefer-as-int.mjs +27 -24
- package/dist/rules/prefer-as-int.mjs.map +1 -1
- package/dist/rules/prefer-canonical-array-slicing.d.mts.map +1 -1
- package/dist/rules/prefer-canonical-array-slicing.mjs +59 -28
- package/dist/rules/prefer-canonical-array-slicing.mjs.map +1 -1
- package/dist/rules/prefer-canonical-length-cast.d.mts.map +1 -1
- package/dist/rules/prefer-canonical-length-cast.mjs +41 -16
- package/dist/rules/prefer-canonical-length-cast.mjs.map +1 -1
- package/dist/rules/prefer-canonical-length-guard.d.mts.map +1 -1
- package/dist/rules/prefer-canonical-length-guard.mjs +18 -8
- package/dist/rules/prefer-canonical-length-guard.mjs.map +1 -1
- package/dist/rules/prefer-comparison-over-nullish-guard.d.mts.map +1 -1
- package/dist/rules/prefer-comparison-over-nullish-guard.mjs +31 -6
- package/dist/rules/prefer-comparison-over-nullish-guard.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 +56 -40
- 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 +18 -8
- package/dist/rules/prefer-is-record-and-has-key.mjs.map +1 -1
- package/dist/rules/prefer-num-safe-parse-float.d.mts +3 -1
- package/dist/rules/prefer-num-safe-parse-float.d.mts.map +1 -1
- package/dist/rules/prefer-num-safe-parse-float.mjs +27 -10
- package/dist/rules/prefer-num-safe-parse-float.mjs.map +1 -1
- package/dist/rules/prefer-num-safe-parse-int.d.mts.map +1 -1
- package/dist/rules/prefer-num-safe-parse-int.mjs +24 -11
- package/dist/rules/prefer-num-safe-parse-int.mjs.map +1 -1
- package/dist/rules/prefer-obj-over-entries-round-trip.d.mts.map +1 -1
- package/dist/rules/prefer-obj-over-entries-round-trip.mjs +22 -11
- package/dist/rules/prefer-obj-over-entries-round-trip.mjs.map +1 -1
- package/dist/rules/prefer-range-for-loop.d.mts.map +1 -1
- package/dist/rules/prefer-range-for-loop.mjs +15 -8
- package/dist/rules/prefer-range-for-loop.mjs.map +1 -1
- package/dist/types.d.mts +2 -2
- package/dist/types.d.mts.map +1 -1
- package/package.json +25 -23
- package/src/plugin.mts +5 -4
- package/src/plugin.test.mts +0 -13
- package/src/rule-types.mts +3 -3
- package/src/rules/ast-utils.mts +192 -37
- package/src/rules/branded-number-types.mts +1 -1
- package/src/rules/import-utils.mts +41 -17
- package/src/rules/no-side-effect-import.mts +1 -1
- package/src/rules/no-unnecessary-type-guard.mts +249 -148
- package/src/rules/no-unnecessary-type-guard.test.mts +100 -0
- package/src/rules/prefer-arr-is-array.mts +32 -13
- package/src/rules/prefer-arr-is-array.test.mts +40 -0
- package/src/rules/prefer-arr-is-bounded-length-array.mts +22 -26
- package/src/rules/prefer-arr-is-bounded-length-array.test.mts +68 -0
- package/src/rules/prefer-arr-is-fixed-length-array.mts +26 -38
- package/src/rules/prefer-arr-is-fixed-length-array.test.mts +63 -0
- package/src/rules/prefer-arr-is-max-length-array.mts +26 -27
- package/src/rules/prefer-arr-is-max-length-array.test.mts +67 -0
- package/src/rules/prefer-arr-is-min-length-array.mts +26 -27
- package/src/rules/prefer-arr-is-min-length-array.test.mts +70 -0
- package/src/rules/prefer-arr-is-non-empty.mts +19 -24
- package/src/rules/prefer-arr-is-non-empty.test.mts +79 -0
- package/src/rules/prefer-arr-scan.mts +107 -37
- package/src/rules/prefer-arr-scan.test.mts +149 -0
- package/src/rules/prefer-arr-sum.mts +87 -50
- package/src/rules/prefer-arr-sum.test.mts +97 -0
- package/src/rules/prefer-arr-uniq.mts +83 -33
- package/src/rules/prefer-arr-uniq.test.mts +71 -0
- package/src/rules/prefer-as-int.mts +38 -38
- package/src/rules/prefer-as-int.test.mts +51 -0
- package/src/rules/prefer-canonical-array-slicing.mts +84 -29
- package/src/rules/prefer-canonical-array-slicing.test.mts +92 -0
- package/src/rules/prefer-canonical-length-cast.mts +56 -18
- package/src/rules/prefer-canonical-length-cast.test.mts +111 -0
- package/src/rules/prefer-canonical-length-guard.mts +25 -11
- package/src/rules/prefer-canonical-length-guard.test.mts +99 -0
- package/src/rules/prefer-comparison-over-nullish-guard.mts +42 -9
- package/src/rules/prefer-comparison-over-nullish-guard.test.mts +136 -0
- package/src/rules/prefer-is-non-null-object.mts +57 -33
- package/src/rules/prefer-is-non-null-object.test.mts +42 -0
- package/src/rules/prefer-is-record-and-has-key.mts +27 -12
- package/src/rules/prefer-is-record-and-has-key.test.mts +34 -0
- package/src/rules/prefer-num-safe-parse-float.mts +37 -13
- package/src/rules/prefer-num-safe-parse-float.test.mts +79 -0
- package/src/rules/prefer-num-safe-parse-int.mts +41 -15
- package/src/rules/prefer-num-safe-parse-int.test.mts +64 -0
- package/src/rules/prefer-obj-over-entries-round-trip.mts +34 -12
- package/src/rules/prefer-range-for-loop.mts +21 -9
- package/src/rules/prefer-range-for-loop.test.mts +75 -0
- package/src/rules/rules.mts +1 -1
- package/src/types.mts +2 -2
|
@@ -4,8 +4,9 @@ import {
|
|
|
4
4
|
type TSESLint,
|
|
5
5
|
type TSESTree,
|
|
6
6
|
} from '@typescript-eslint/utils';
|
|
7
|
-
import {
|
|
7
|
+
import type { DeepReadonly, ReadonlyRecord } from 'ts-type-forge';
|
|
8
8
|
import * as ts from 'typescript';
|
|
9
|
+
import { typeWrapperLayers } from './ast-utils.mjs';
|
|
9
10
|
import {
|
|
10
11
|
buildCalleeResolver,
|
|
11
12
|
buildImportFixes,
|
|
@@ -156,7 +157,9 @@ export const noUnnecessaryTypeGuard: TSESLint.RuleModule<MessageIds, Options> =
|
|
|
156
157
|
const strictNullChecks =
|
|
157
158
|
compilerOptions.strictNullChecks ?? compilerOptions.strict ?? false;
|
|
158
159
|
|
|
159
|
-
if (!strictNullChecks)
|
|
160
|
+
if (!strictNullChecks) {
|
|
161
|
+
return {};
|
|
162
|
+
}
|
|
160
163
|
|
|
161
164
|
const checker = parserServices.program.getTypeChecker();
|
|
162
165
|
|
|
@@ -249,7 +252,9 @@ export const noUnnecessaryTypeGuard: TSESLint.RuleModule<MessageIds, Options> =
|
|
|
249
252
|
|
|
250
253
|
return {
|
|
251
254
|
CallExpression: (node) => {
|
|
252
|
-
if (node.arguments.length !== 1)
|
|
255
|
+
if (node.arguments.length !== 1) {
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
253
258
|
|
|
254
259
|
const argument = node.arguments[0];
|
|
255
260
|
|
|
@@ -262,140 +267,57 @@ export const noUnnecessaryTypeGuard: TSESLint.RuleModule<MessageIds, Options> =
|
|
|
262
267
|
|
|
263
268
|
const resolved = resolveGuard(node.callee);
|
|
264
269
|
|
|
265
|
-
if (resolved === undefined)
|
|
270
|
+
if (resolved === undefined) {
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
266
273
|
|
|
267
274
|
const { canonicalName, propertyNode, isNamespace } = resolved;
|
|
268
275
|
|
|
269
|
-
if (ignored.has(canonicalName))
|
|
276
|
+
if (ignored.has(canonicalName)) {
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
270
279
|
|
|
271
280
|
const spec = GUARD_SPECS[canonicalName];
|
|
272
281
|
|
|
273
|
-
if (spec === undefined)
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
propertyNode,
|
|
313
|
-
);
|
|
314
|
-
}
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
break;
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
case 'narrowTo': {
|
|
321
|
-
const present = spec.atoms.filter((a) => inputAtoms.has(a));
|
|
322
|
-
|
|
323
|
-
if (present.length === 0) {
|
|
324
|
-
// No member is in the target set → the guard never holds.
|
|
325
|
-
reportConstant(node, canonicalName, false, argument);
|
|
326
|
-
} else if (!hasMemberOutsideTarget(spec.atoms)) {
|
|
327
|
-
// Input ⊆ target → the guard always holds and narrows nothing.
|
|
328
|
-
reportConstant(node, canonicalName, true, argument);
|
|
329
|
-
} else if (present.length < spec.atoms.length) {
|
|
330
|
-
const replacement = spec.replacements?.[joinAtoms(present)];
|
|
331
|
-
|
|
332
|
-
if (replacement !== undefined) {
|
|
333
|
-
reportReplace(
|
|
334
|
-
node,
|
|
335
|
-
canonicalName,
|
|
336
|
-
replacement,
|
|
337
|
-
isNamespace,
|
|
338
|
-
propertyNode,
|
|
339
|
-
);
|
|
340
|
-
}
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
break;
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
case 'record': {
|
|
347
|
-
if (parts.every((p) => isGuaranteedRecord(p, checker))) {
|
|
348
|
-
// Every member already satisfies `UnknownRecord`.
|
|
349
|
-
reportConstant(node, canonicalName, true, argument);
|
|
350
|
-
} else if (
|
|
351
|
-
parts.every((p) => isGuaranteedNotRecord(p, checker))
|
|
352
|
-
) {
|
|
353
|
-
// No member can ever be a non-null, non-array object.
|
|
354
|
-
reportConstant(node, canonicalName, false, argument);
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
break;
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
case 'nonEmptyString': {
|
|
361
|
-
const hasNullish =
|
|
362
|
-
inputAtoms.has('undefined') || inputAtoms.has('null');
|
|
363
|
-
|
|
364
|
-
const nonNullishParts = parts.filter((p) => {
|
|
365
|
-
const atom = classifyAtom(p);
|
|
366
|
-
|
|
367
|
-
return atom !== 'undefined' && atom !== 'null';
|
|
368
|
-
});
|
|
369
|
-
|
|
370
|
-
if (!nonNullishParts.some(couldBeNonEmptyString)) {
|
|
371
|
-
// No member can be a non-empty string → the guard never holds.
|
|
372
|
-
reportConstant(node, canonicalName, false, argument);
|
|
373
|
-
|
|
374
|
-
break;
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
if (
|
|
378
|
-
nonNullishParts.some(
|
|
379
|
-
(p) => !isGuaranteedNonEmptyString(p, checker),
|
|
380
|
-
)
|
|
381
|
-
) {
|
|
382
|
-
break; // still does real string/empty work
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
if (hasNullish) {
|
|
386
|
-
reportReplace(
|
|
387
|
-
node,
|
|
388
|
-
canonicalName,
|
|
389
|
-
'isNonNullish',
|
|
390
|
-
isNamespace,
|
|
391
|
-
propertyNode,
|
|
392
|
-
);
|
|
393
|
-
} else {
|
|
394
|
-
reportConstant(node, canonicalName, true, argument);
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
break;
|
|
398
|
-
}
|
|
282
|
+
if (spec === undefined) {
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// A conclusion drawn from the argument's type has to hold at every
|
|
287
|
+
// layer of type wrappers around it (see typeWrapperLayers):
|
|
288
|
+
// `isNull(x as string | null)` is the author saying `x` may be
|
|
289
|
+
// `null`, and `isNullish(x as string | undefined)` still sees the
|
|
290
|
+
// `null` that `x` may hold.
|
|
291
|
+
const layerTypes = typeWrapperLayers(argument).map((layer) =>
|
|
292
|
+
checker.getTypeAtLocation(
|
|
293
|
+
parserServices.esTreeNodeToTSNodeMap.get(layer),
|
|
294
|
+
),
|
|
295
|
+
);
|
|
296
|
+
|
|
297
|
+
const verdict = layerTypes
|
|
298
|
+
.map((type) => verdictOf(spec, type, checker))
|
|
299
|
+
.find(
|
|
300
|
+
(candidate): candidate is Verdict =>
|
|
301
|
+
candidate !== undefined &&
|
|
302
|
+
layerTypes.every((type) =>
|
|
303
|
+
holdsOn(candidate, spec, type, checker),
|
|
304
|
+
),
|
|
305
|
+
);
|
|
306
|
+
|
|
307
|
+
if (verdict === undefined) {
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
if (verdict.kind === 'constant') {
|
|
312
|
+
reportConstant(node, canonicalName, verdict.value, argument);
|
|
313
|
+
} else {
|
|
314
|
+
reportReplace(
|
|
315
|
+
node,
|
|
316
|
+
canonicalName,
|
|
317
|
+
verdict.replacement,
|
|
318
|
+
isNamespace,
|
|
319
|
+
propertyNode,
|
|
320
|
+
);
|
|
399
321
|
}
|
|
400
322
|
},
|
|
401
323
|
};
|
|
@@ -403,6 +325,158 @@ export const noUnnecessaryTypeGuard: TSESLint.RuleModule<MessageIds, Options> =
|
|
|
403
325
|
defaultOptions: [{}],
|
|
404
326
|
} as const;
|
|
405
327
|
|
|
328
|
+
/** What the rule concludes about a guard call from one type of its argument. */
|
|
329
|
+
type Verdict = DeepReadonly<
|
|
330
|
+
| { kind: 'constant'; value: boolean }
|
|
331
|
+
| { kind: 'replace'; replacement: string }
|
|
332
|
+
>;
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* What a guard with `spec` does on an argument of `type`: always the same
|
|
336
|
+
* boolean, the same as a narrower guard, or nothing the rule can say.
|
|
337
|
+
*/
|
|
338
|
+
const verdictOf = (
|
|
339
|
+
spec: GuardSpec,
|
|
340
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
341
|
+
type: ts.Type,
|
|
342
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
343
|
+
checker: ts.TypeChecker,
|
|
344
|
+
): Verdict | undefined => {
|
|
345
|
+
const parts = collectUnionParts(type);
|
|
346
|
+
|
|
347
|
+
if (parts === undefined) {
|
|
348
|
+
return undefined; // opaque / generic → bail conservatively
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
const inputAtoms = new Set(parts.map(classifyAtom));
|
|
352
|
+
|
|
353
|
+
const hasMemberOutsideTarget = (atoms: readonly AtomKey[]): boolean => {
|
|
354
|
+
const targetSet = new Set<Atom>(atoms);
|
|
355
|
+
|
|
356
|
+
return parts.some((p) => !targetSet.has(classifyAtom(p)));
|
|
357
|
+
};
|
|
358
|
+
|
|
359
|
+
switch (spec.kind) {
|
|
360
|
+
case 'excludeFrom': {
|
|
361
|
+
const removable = spec.atoms.filter((a) => inputAtoms.has(a));
|
|
362
|
+
|
|
363
|
+
if (removable.length === 0) {
|
|
364
|
+
// Nothing to exclude → the guard always holds.
|
|
365
|
+
return { kind: 'constant', value: true } as const;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
if (!hasMemberOutsideTarget(spec.atoms)) {
|
|
369
|
+
// Every member is excluded → the guard never holds.
|
|
370
|
+
return { kind: 'constant', value: false } as const;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
return replacementVerdict(
|
|
374
|
+
removable.length < spec.atoms.length
|
|
375
|
+
? spec.replacements?.[joinAtoms(removable)]
|
|
376
|
+
: undefined,
|
|
377
|
+
);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
case 'narrowTo': {
|
|
381
|
+
const present = spec.atoms.filter((a) => inputAtoms.has(a));
|
|
382
|
+
|
|
383
|
+
if (present.length === 0) {
|
|
384
|
+
// No member is in the target set → the guard never holds.
|
|
385
|
+
return { kind: 'constant', value: false } as const;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
if (!hasMemberOutsideTarget(spec.atoms)) {
|
|
389
|
+
// Input ⊆ target → the guard always holds and narrows nothing.
|
|
390
|
+
return { kind: 'constant', value: true } as const;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
return replacementVerdict(
|
|
394
|
+
present.length < spec.atoms.length
|
|
395
|
+
? spec.replacements?.[joinAtoms(present)]
|
|
396
|
+
: undefined,
|
|
397
|
+
);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
case 'record':
|
|
401
|
+
// Every member already satisfies `UnknownRecord`, or none can ever be
|
|
402
|
+
// a non-null, non-array object.
|
|
403
|
+
return parts.every((p) => isGuaranteedRecord(p, checker))
|
|
404
|
+
? ({ kind: 'constant', value: true } as const)
|
|
405
|
+
: parts.every((p) => isGuaranteedNotRecord(p, checker))
|
|
406
|
+
? ({ kind: 'constant', value: false } as const)
|
|
407
|
+
: undefined;
|
|
408
|
+
|
|
409
|
+
case 'nonEmptyString': {
|
|
410
|
+
const nonNullishParts = parts.filter((p) => {
|
|
411
|
+
const atom = classifyAtom(p);
|
|
412
|
+
|
|
413
|
+
return atom !== 'undefined' && atom !== 'null';
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
if (!nonNullishParts.some(couldBeNonEmptyString)) {
|
|
417
|
+
// No member can be a non-empty string → the guard never holds.
|
|
418
|
+
return { kind: 'constant', value: false } as const;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
if (
|
|
422
|
+
nonNullishParts.some((p) => !isGuaranteedNonEmptyString(p, checker))
|
|
423
|
+
) {
|
|
424
|
+
return undefined; // still does real string/empty work
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
return inputAtoms.has('undefined') || inputAtoms.has('null')
|
|
428
|
+
? ({ kind: 'replace', replacement: 'isNonNullish' } as const)
|
|
429
|
+
: ({ kind: 'constant', value: true } as const);
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
};
|
|
433
|
+
|
|
434
|
+
const replacementVerdict = (
|
|
435
|
+
replacement: string | undefined,
|
|
436
|
+
): Verdict | undefined =>
|
|
437
|
+
replacement === undefined
|
|
438
|
+
? undefined
|
|
439
|
+
: ({ kind: 'replace', replacement } as const);
|
|
440
|
+
|
|
441
|
+
/**
|
|
442
|
+
* Whether `verdict`, drawn from one layer's type, also holds for `type`. A
|
|
443
|
+
* constant has to come out the same. A replacement holds where the rule would
|
|
444
|
+
* suggest the same one, or where the guard and its replacement are the same
|
|
445
|
+
* constant: under `x!` on a `string | null`, `isNonNullish` and `isNotNull`
|
|
446
|
+
* are both always `true` on `string`.
|
|
447
|
+
*/
|
|
448
|
+
const holdsOn = (
|
|
449
|
+
verdict: Verdict,
|
|
450
|
+
spec: GuardSpec,
|
|
451
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
452
|
+
type: ts.Type,
|
|
453
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
454
|
+
checker: ts.TypeChecker,
|
|
455
|
+
): boolean => {
|
|
456
|
+
const onType = verdictOf(spec, type, checker);
|
|
457
|
+
|
|
458
|
+
if (verdict.kind === 'constant') {
|
|
459
|
+
return onType?.kind === 'constant' && onType.value === verdict.value;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
if (onType?.kind === 'replace') {
|
|
463
|
+
return onType.replacement === verdict.replacement;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
const replacementSpec = GUARD_SPECS[verdict.replacement];
|
|
467
|
+
|
|
468
|
+
const replacementOnType =
|
|
469
|
+
replacementSpec === undefined
|
|
470
|
+
? undefined
|
|
471
|
+
: verdictOf(replacementSpec, type, checker);
|
|
472
|
+
|
|
473
|
+
return (
|
|
474
|
+
onType?.kind === 'constant' &&
|
|
475
|
+
replacementOnType?.kind === 'constant' &&
|
|
476
|
+
replacementOnType.value === onType.value
|
|
477
|
+
);
|
|
478
|
+
};
|
|
479
|
+
|
|
406
480
|
/**
|
|
407
481
|
* Decomposes a type into its union members. Returns `undefined` when any member
|
|
408
482
|
* is opaque (`any` / `unknown` / `never`) or deferred (type parameter, indexed
|
|
@@ -413,7 +487,9 @@ const collectUnionParts = (type: ts.Type): readonly ts.Type[] | undefined => {
|
|
|
413
487
|
const parts = type.isUnion() ? type.types : ([type] as const);
|
|
414
488
|
|
|
415
489
|
for (const part of parts) {
|
|
416
|
-
if ((part.flags & DEFERRED_OR_OPAQUE_FLAGS) !== 0)
|
|
490
|
+
if ((part.flags & DEFERRED_OR_OPAQUE_FLAGS) !== 0) {
|
|
491
|
+
return undefined;
|
|
492
|
+
}
|
|
417
493
|
}
|
|
418
494
|
|
|
419
495
|
return parts;
|
|
@@ -473,19 +549,33 @@ const isCallableType = (
|
|
|
473
549
|
const classifyAtom = (type: ts.Type): Atom => {
|
|
474
550
|
const { flags } = type;
|
|
475
551
|
|
|
476
|
-
if ((flags & ts.TypeFlags.Undefined) !== 0)
|
|
552
|
+
if ((flags & ts.TypeFlags.Undefined) !== 0) {
|
|
553
|
+
return 'undefined';
|
|
554
|
+
}
|
|
477
555
|
|
|
478
|
-
if ((flags & ts.TypeFlags.Null) !== 0)
|
|
556
|
+
if ((flags & ts.TypeFlags.Null) !== 0) {
|
|
557
|
+
return 'null';
|
|
558
|
+
}
|
|
479
559
|
|
|
480
|
-
if ((flags & ts.TypeFlags.BooleanLike) !== 0)
|
|
560
|
+
if ((flags & ts.TypeFlags.BooleanLike) !== 0) {
|
|
561
|
+
return 'boolean';
|
|
562
|
+
}
|
|
481
563
|
|
|
482
|
-
if ((flags & ts.TypeFlags.NumberLike) !== 0)
|
|
564
|
+
if ((flags & ts.TypeFlags.NumberLike) !== 0) {
|
|
565
|
+
return 'number';
|
|
566
|
+
}
|
|
483
567
|
|
|
484
|
-
if ((flags & ts.TypeFlags.StringLike) !== 0)
|
|
568
|
+
if ((flags & ts.TypeFlags.StringLike) !== 0) {
|
|
569
|
+
return 'string';
|
|
570
|
+
}
|
|
485
571
|
|
|
486
|
-
if ((flags & ts.TypeFlags.BigIntLike) !== 0)
|
|
572
|
+
if ((flags & ts.TypeFlags.BigIntLike) !== 0) {
|
|
573
|
+
return 'bigint';
|
|
574
|
+
}
|
|
487
575
|
|
|
488
|
-
if ((flags & ts.TypeFlags.ESSymbolLike) !== 0)
|
|
576
|
+
if ((flags & ts.TypeFlags.ESSymbolLike) !== 0) {
|
|
577
|
+
return 'symbol';
|
|
578
|
+
}
|
|
489
579
|
|
|
490
580
|
// Branded primitives (e.g. `NonEmptyString = string & {...}`) are
|
|
491
581
|
// intersections; classify them by their underlying primitive constituent.
|
|
@@ -493,7 +583,9 @@ const classifyAtom = (type: ts.Type): Atom => {
|
|
|
493
583
|
for (const constituent of type.types) {
|
|
494
584
|
const atom = classifyAtom(constituent);
|
|
495
585
|
|
|
496
|
-
if (atom !== 'other')
|
|
586
|
+
if (atom !== 'other') {
|
|
587
|
+
return atom;
|
|
588
|
+
}
|
|
497
589
|
}
|
|
498
590
|
}
|
|
499
591
|
|
|
@@ -525,13 +617,19 @@ const isGuaranteedNonEmptyString = (
|
|
|
525
617
|
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
526
618
|
checker: ts.TypeChecker,
|
|
527
619
|
): boolean => {
|
|
528
|
-
if (type.isStringLiteral())
|
|
620
|
+
if (type.isStringLiteral()) {
|
|
621
|
+
return type.value !== '';
|
|
622
|
+
}
|
|
529
623
|
|
|
530
|
-
if (type.getProperty(BRAND_MARKER_PROPERTY) === undefined)
|
|
624
|
+
if (type.getProperty(BRAND_MARKER_PROPERTY) === undefined) {
|
|
625
|
+
return false;
|
|
626
|
+
}
|
|
531
627
|
|
|
532
628
|
const minLengthSymbol = type.getProperty(MIN_LENGTH_BRAND_KEY);
|
|
533
629
|
|
|
534
|
-
if (minLengthSymbol === undefined)
|
|
630
|
+
if (minLengthSymbol === undefined) {
|
|
631
|
+
return false;
|
|
632
|
+
}
|
|
535
633
|
|
|
536
634
|
// The `MinLength` brand value is `MinLengthTuple<N, 0>`; it has a required
|
|
537
635
|
// first element (`'0'`) exactly when `N >= 1`, i.e. the string is non-empty.
|
|
@@ -547,11 +645,8 @@ const isGuaranteedNonEmptyString = (
|
|
|
547
645
|
* literal and non-string types cannot.
|
|
548
646
|
*/
|
|
549
647
|
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
550
|
-
const couldBeNonEmptyString = (type: ts.Type): boolean =>
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
return classifyAtom(type) === 'string';
|
|
554
|
-
};
|
|
648
|
+
const couldBeNonEmptyString = (type: ts.Type): boolean =>
|
|
649
|
+
type.isStringLiteral() ? type.value !== '' : classifyAtom(type) === 'string';
|
|
555
650
|
|
|
556
651
|
/**
|
|
557
652
|
* Returns `true` when evaluating the expression cannot have observable side
|
|
@@ -574,6 +669,12 @@ const isSideEffectFreeArg = (node: DeepReadonly<TSESTree.Node>): boolean => {
|
|
|
574
669
|
(!node.computed || isSideEffectFreeArg(node.property))
|
|
575
670
|
);
|
|
576
671
|
|
|
672
|
+
case AST_NODE_TYPES.TSAsExpression:
|
|
673
|
+
case AST_NODE_TYPES.TSNonNullExpression:
|
|
674
|
+
case AST_NODE_TYPES.TSSatisfiesExpression:
|
|
675
|
+
case AST_NODE_TYPES.TSTypeAssertion:
|
|
676
|
+
return isSideEffectFreeArg(node.expression);
|
|
677
|
+
|
|
577
678
|
default:
|
|
578
679
|
return false;
|
|
579
680
|
}
|
|
@@ -460,3 +460,103 @@ describe('no-unnecessary-type-guard', () => {
|
|
|
460
460
|
],
|
|
461
461
|
});
|
|
462
462
|
});
|
|
463
|
+
|
|
464
|
+
describe('no-unnecessary-type-guard through type wrappers', () => {
|
|
465
|
+
tester.run('no-unnecessary-type-guard', noUnnecessaryTypeGuard, {
|
|
466
|
+
valid: [
|
|
467
|
+
{
|
|
468
|
+
name: 'isNullish on a value cast away from null',
|
|
469
|
+
code: dedent`
|
|
470
|
+
import { isNullish } from 'ts-data-forge';
|
|
471
|
+
declare const x: string | null | undefined;
|
|
472
|
+
const y = isNullish(x as string | undefined);
|
|
473
|
+
`,
|
|
474
|
+
},
|
|
475
|
+
{
|
|
476
|
+
name: 'isNonNullish on a value cast away from null',
|
|
477
|
+
code: dedent`
|
|
478
|
+
import { isNonNullish } from 'ts-data-forge';
|
|
479
|
+
declare const x: string | null | undefined;
|
|
480
|
+
const y = isNonNullish(x as string | undefined);
|
|
481
|
+
`,
|
|
482
|
+
},
|
|
483
|
+
{
|
|
484
|
+
name: 'isNonEmptyString on a string cast to NonEmptyString',
|
|
485
|
+
code: dedent`
|
|
486
|
+
import { type NonEmptyString } from 'ts-type-forge';
|
|
487
|
+
import { isNonEmptyString } from 'ts-data-forge';
|
|
488
|
+
declare const x: string | null | undefined;
|
|
489
|
+
const y = isNonEmptyString(x as NonEmptyString | null | undefined);
|
|
490
|
+
`,
|
|
491
|
+
},
|
|
492
|
+
{
|
|
493
|
+
name: 'isNull on a value whose `!` does not remove the null',
|
|
494
|
+
code: dedent`
|
|
495
|
+
import { isNull } from 'ts-data-forge';
|
|
496
|
+
declare const x: string | null;
|
|
497
|
+
const y = isNull(x!);
|
|
498
|
+
`,
|
|
499
|
+
},
|
|
500
|
+
{
|
|
501
|
+
name: 'isNull on a value widened to include null: the `as` is the author saying it may be null',
|
|
502
|
+
code: dedent`
|
|
503
|
+
import { isNull } from 'ts-data-forge';
|
|
504
|
+
declare const x: string;
|
|
505
|
+
const y = isNull(x as string | null);
|
|
506
|
+
`,
|
|
507
|
+
},
|
|
508
|
+
{
|
|
509
|
+
name: 'isNullish on a value widened to include null is not narrowed to isUndefined',
|
|
510
|
+
code: dedent`
|
|
511
|
+
import { isNullish } from 'ts-data-forge';
|
|
512
|
+
declare const x: string | undefined;
|
|
513
|
+
const y = isNullish(x as string | null | undefined);
|
|
514
|
+
`,
|
|
515
|
+
},
|
|
516
|
+
],
|
|
517
|
+
invalid: [
|
|
518
|
+
{
|
|
519
|
+
name: 'isNonNullish on a value whose `!` does not remove the null',
|
|
520
|
+
code: dedent`
|
|
521
|
+
import { isNonNullish, isNotNull } from 'ts-data-forge';
|
|
522
|
+
declare const x: string | null;
|
|
523
|
+
const y = isNonNullish(x!);
|
|
524
|
+
`,
|
|
525
|
+
output: dedent`
|
|
526
|
+
import { isNonNullish, isNotNull } from 'ts-data-forge';
|
|
527
|
+
declare const x: string | null;
|
|
528
|
+
const y = isNotNull(x!);
|
|
529
|
+
`,
|
|
530
|
+
errors: [{ messageId: 'replaceTypeGuard' }],
|
|
531
|
+
},
|
|
532
|
+
{
|
|
533
|
+
name: 'isNull on a value widened to exclude null still, at every layer',
|
|
534
|
+
code: dedent`
|
|
535
|
+
import { isNull } from 'ts-data-forge';
|
|
536
|
+
declare const x: string;
|
|
537
|
+
const y = isNull(x as string | undefined);
|
|
538
|
+
`,
|
|
539
|
+
output: dedent`
|
|
540
|
+
import { isNull } from 'ts-data-forge';
|
|
541
|
+
declare const x: string;
|
|
542
|
+
const y = false;
|
|
543
|
+
`,
|
|
544
|
+
errors: [{ messageId: 'alwaysFalse' }],
|
|
545
|
+
},
|
|
546
|
+
{
|
|
547
|
+
name: 'isNullish replaced on the value under `satisfies`, keeping the wrapper',
|
|
548
|
+
code: dedent`
|
|
549
|
+
import { isNullish, isUndefined } from 'ts-data-forge';
|
|
550
|
+
declare const x: string | undefined;
|
|
551
|
+
const y = isNullish(x satisfies string | null | undefined);
|
|
552
|
+
`,
|
|
553
|
+
output: dedent`
|
|
554
|
+
import { isNullish, isUndefined } from 'ts-data-forge';
|
|
555
|
+
declare const x: string | undefined;
|
|
556
|
+
const y = isUndefined(x satisfies string | null | undefined);
|
|
557
|
+
`,
|
|
558
|
+
errors: [{ messageId: 'replaceTypeGuard' }],
|
|
559
|
+
},
|
|
560
|
+
],
|
|
561
|
+
});
|
|
562
|
+
});
|
|
@@ -3,7 +3,7 @@ import {
|
|
|
3
3
|
type TSESLint,
|
|
4
4
|
type TSESTree,
|
|
5
5
|
} from '@typescript-eslint/utils';
|
|
6
|
-
import {
|
|
6
|
+
import { skipTypeWrappers } from './ast-utils.mjs';
|
|
7
7
|
import {
|
|
8
8
|
buildImportFixes,
|
|
9
9
|
getNamedImports,
|
|
@@ -40,7 +40,9 @@ export const preferArrIsArray: TSESLint.RuleModule<MessageIds, Options> = {
|
|
|
40
40
|
|
|
41
41
|
return {
|
|
42
42
|
CallExpression: (node) => {
|
|
43
|
-
if (!isArrayIsArrayCall(node))
|
|
43
|
+
if (!isArrayIsArrayCall(node)) {
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
44
46
|
|
|
45
47
|
mut_nodesToFix.push(node);
|
|
46
48
|
},
|
|
@@ -59,17 +61,19 @@ export const preferArrIsArray: TSESLint.RuleModule<MessageIds, Options> = {
|
|
|
59
61
|
fix: (fixer) => {
|
|
60
62
|
const callee = node.callee;
|
|
61
63
|
|
|
62
|
-
if (callee.type !== AST_NODE_TYPES.MemberExpression)
|
|
63
|
-
|
|
64
|
-
|
|
64
|
+
if (callee.type !== AST_NODE_TYPES.MemberExpression) {
|
|
65
|
+
return [];
|
|
66
|
+
}
|
|
65
67
|
|
|
68
|
+
// Only the callee is replaced: parentheses the source put around
|
|
69
|
+
// it are outside its range, and stay balanced.
|
|
66
70
|
// Add import only for the first node and only if not already imported
|
|
67
71
|
const importFixes =
|
|
68
72
|
index === 0 && !hasArrImport
|
|
69
73
|
? buildImportFixes(fixer, program, tsDataForgeImport, ['Arr'])
|
|
70
74
|
: [];
|
|
71
75
|
|
|
72
|
-
return [...importFixes, fixer.replaceText(
|
|
76
|
+
return [...importFixes, fixer.replaceText(callee, 'Arr.isArray')];
|
|
73
77
|
},
|
|
74
78
|
});
|
|
75
79
|
}
|
|
@@ -80,19 +84,34 @@ export const preferArrIsArray: TSESLint.RuleModule<MessageIds, Options> = {
|
|
|
80
84
|
} as const;
|
|
81
85
|
|
|
82
86
|
const isArrayIsArrayCall = (
|
|
83
|
-
|
|
87
|
+
// AST nodes hold mutable child arrays, so they are not deeply readonly.
|
|
88
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
89
|
+
node: TSESTree.CallExpression,
|
|
84
90
|
): boolean => {
|
|
85
|
-
if (node.callee.type !== AST_NODE_TYPES.MemberExpression)
|
|
91
|
+
if (node.callee.type !== AST_NODE_TYPES.MemberExpression) {
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const { property } = node.callee;
|
|
86
96
|
|
|
87
|
-
|
|
97
|
+
// `(Array as ArrayConstructor).isArray` is still `Array.isArray`.
|
|
98
|
+
const object = skipTypeWrappers(node.callee.object);
|
|
88
99
|
|
|
89
|
-
if (object.type !== AST_NODE_TYPES.Identifier)
|
|
100
|
+
if (object.type !== AST_NODE_TYPES.Identifier) {
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
90
103
|
|
|
91
|
-
if (object.name !== 'Array')
|
|
104
|
+
if (object.name !== 'Array') {
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
92
107
|
|
|
93
|
-
if (property.type !== AST_NODE_TYPES.Identifier)
|
|
108
|
+
if (property.type !== AST_NODE_TYPES.Identifier) {
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
94
111
|
|
|
95
|
-
if (property.name !== 'isArray')
|
|
112
|
+
if (property.name !== 'isArray') {
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
96
115
|
|
|
97
116
|
return true;
|
|
98
117
|
};
|