eslint-plugin-ts-data-forge 0.10.4 → 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.
- package/dist/rules/ast-utils.d.mts +5 -5
- package/dist/rules/ast-utils.d.mts.map +1 -1
- package/dist/rules/ast-utils.mjs +3 -3
- package/dist/rules/ast-utils.mjs.map +1 -1
- package/dist/rules/import-binding.d.mts +38 -0
- package/dist/rules/import-binding.d.mts.map +1 -0
- package/dist/rules/import-binding.mjs +53 -0
- package/dist/rules/import-binding.mjs.map +1 -0
- package/dist/rules/mutable-result.mjs +37 -37
- package/dist/rules/mutable-result.mjs.map +1 -1
- package/dist/rules/no-unnecessary-type-guard.d.mts.map +1 -1
- package/dist/rules/no-unnecessary-type-guard.mjs +146 -143
- package/dist/rules/no-unnecessary-type-guard.mjs.map +1 -1
- package/dist/rules/prefer-arr-generate.d.mts.map +1 -1
- package/dist/rules/prefer-arr-generate.mjs +129 -131
- package/dist/rules/prefer-arr-generate.mjs.map +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 +14 -16
- package/dist/rules/prefer-arr-is-bounded-length-array.mjs.map +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 +15 -17
- package/dist/rules/prefer-arr-is-fixed-length-array.mjs.map +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 +14 -16
- package/dist/rules/prefer-arr-is-max-length-array.mjs.map +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 +14 -16
- 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 +14 -16
- package/dist/rules/prefer-arr-is-non-empty.mjs.map +1 -1
- package/dist/rules/prefer-arr-scan.d.mts.map +1 -1
- package/dist/rules/prefer-arr-scan.mjs +9 -18
- 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 +16 -21
- 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 +38 -43
- 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 +19 -26
- 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 +49 -15
- package/dist/rules/prefer-canonical-array-slicing.mjs.map +1 -1
- package/dist/rules/prefer-canonical-length-cast.mjs +15 -15
- package/dist/rules/prefer-canonical-length-cast.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 +100 -99
- package/dist/rules/prefer-obj-over-entries-round-trip.mjs.map +1 -1
- package/dist/rules/prefer-pipe-map-nullable.d.mts.map +1 -1
- package/dist/rules/prefer-pipe-map-nullable.mjs +2 -4
- package/dist/rules/prefer-pipe-map-nullable.mjs.map +1 -1
- package/dist/rules/prefer-range-for-loop.d.mts.map +1 -1
- package/dist/rules/prefer-range-for-loop.mjs +18 -24
- package/dist/rules/prefer-range-for-loop.mjs.map +1 -1
- package/package.json +5 -5
- package/src/rules/ast-utils.mts +11 -11
- package/src/rules/import-binding.mts +94 -0
- package/src/rules/mutable-result.mts +56 -56
- package/src/rules/no-unnecessary-type-guard.mts +199 -203
- package/src/rules/no-unnecessary-type-guard.test.mts +62 -2
- package/src/rules/prefer-arr-generate.mts +184 -194
- package/src/rules/prefer-arr-generate.test.mts +262 -134
- package/src/rules/prefer-arr-is-array.test.mts +87 -5
- package/src/rules/prefer-arr-is-bounded-length-array.mts +22 -33
- package/src/rules/prefer-arr-is-fixed-length-array.mts +24 -36
- package/src/rules/prefer-arr-is-fixed-length-array.test.mts +16 -7
- package/src/rules/prefer-arr-is-max-length-array.mts +21 -34
- package/src/rules/prefer-arr-is-max-length-array.test.mts +16 -7
- package/src/rules/prefer-arr-is-min-length-array.mts +21 -34
- package/src/rules/prefer-arr-is-min-length-array.test.mts +16 -7
- package/src/rules/prefer-arr-is-non-empty.mts +17 -34
- package/src/rules/prefer-arr-is-non-empty.test.mts +110 -0
- package/src/rules/prefer-arr-scan.mts +11 -36
- package/src/rules/prefer-arr-scan.test.mts +55 -0
- package/src/rules/prefer-arr-sum.mts +19 -33
- package/src/rules/prefer-arr-uniq.mts +49 -60
- package/src/rules/prefer-arr-uniq.test.mts +46 -20
- package/src/rules/prefer-as-int.mts +27 -52
- package/src/rules/prefer-as-int.test.mts +79 -29
- package/src/rules/prefer-canonical-array-slicing.mts +86 -24
- package/src/rules/prefer-canonical-array-slicing.test.mts +133 -34
- package/src/rules/prefer-canonical-length-cast.mts +18 -18
- package/src/rules/prefer-is-non-null-object.test.mts +62 -37
- package/src/rules/prefer-is-record-and-has-key.test.mts +32 -17
- package/src/rules/prefer-num-safe-parse-float.test.mts +45 -21
- package/src/rules/prefer-num-safe-parse-int.test.mts +29 -14
- package/src/rules/prefer-obj-over-entries-round-trip.mts +152 -170
- package/src/rules/prefer-obj-over-entries-round-trip.test.mts +48 -6
- package/src/rules/prefer-pipe-map-nullable.mts +6 -8
- 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
|
-
|
|
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
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
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
|
-
|
|
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`
|