eslint-plugin-ts-data-forge 0.10.5 → 0.10.7
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/README.md +3 -3
- package/dist/plugin.d.mts +2 -2
- 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 +5 -5
- package/dist/rules/ast-utils.d.mts +8 -9
- package/dist/rules/ast-utils.d.mts.map +1 -1
- package/dist/rules/ast-utils.mjs +3 -4
- 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 +2 -2
- package/dist/rules/prefer-arr-generate.d.mts.map +1 -1
- package/dist/rules/prefer-arr-generate.mjs +146 -136
- 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 -16
- 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-num-safe-parse-float.d.mts +1 -1
- package/dist/rules/prefer-num-safe-parse-float.d.mts.map +1 -1
- package/dist/rules/prefer-num-safe-parse-int.d.mts +1 -1
- package/dist/rules/prefer-num-safe-parse-int.d.mts.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 +2 -2
- 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/dist/rules/rules.d.mts +2 -2
- package/package.json +6 -6
- package/src/plugin.mts +4 -3
- package/src/rule-types.mts +5 -5
- package/src/rules/ast-utils.mts +14 -15
- package/src/rules/import-binding.mts +94 -0
- package/src/rules/mutable-result.mts +56 -56
- package/src/rules/no-unnecessary-type-guard.mts +203 -207
- package/src/rules/no-unnecessary-type-guard.test.mts +62 -2
- package/src/rules/prefer-arr-generate.mts +222 -214
- package/src/rules/prefer-arr-generate.test.mts +490 -133
- package/src/rules/prefer-arr-is-bounded-length-array.mts +22 -33
- package/src/rules/prefer-arr-is-fixed-length-array.mts +26 -38
- package/src/rules/prefer-arr-is-fixed-length-array.test.mts +16 -7
- package/src/rules/prefer-arr-is-max-length-array.mts +23 -36
- package/src/rules/prefer-arr-is-max-length-array.test.mts +16 -7
- package/src/rules/prefer-arr-is-min-length-array.mts +23 -36
- 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 +17 -42
- package/src/rules/prefer-arr-scan.test.mts +55 -0
- package/src/rules/prefer-arr-sum.mts +22 -36
- package/src/rules/prefer-arr-uniq.mts +51 -62
- 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 +101 -40
- package/src/rules/prefer-canonical-array-slicing.test.mts +133 -34
- package/src/rules/prefer-canonical-length-cast.mts +29 -29
- package/src/rules/prefer-canonical-length-guard.mts +2 -2
- package/src/rules/prefer-num-safe-parse-float.test.mts +255 -94
- package/src/rules/prefer-num-safe-parse-int.test.mts +212 -76
- 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 +17 -19
- package/src/rules/prefer-range-for-loop.mts +37 -61
- package/src/rules/prefer-range-for-loop.test.mts +2 -2
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import type { TSESLint, TSESTree } from '@typescript-eslint/utils';
|
|
2
|
+
import {
|
|
3
|
+
bindValueImports,
|
|
4
|
+
hasForeignBinding,
|
|
5
|
+
type ValueImportBinding,
|
|
6
|
+
} from 'eslint-rule-forge';
|
|
7
|
+
import { memoizeFunction } from 'ts-data-forge';
|
|
8
|
+
import { TS_DATA_FORGE_MODULE } from './constants.mjs';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Binds ts-data-forge's `exportName` as a value once per file (see
|
|
12
|
+
* `bindValueImports`), and gives, for the code at a node, the name a fix writes
|
|
13
|
+
* and the edits that bring it into scope, or `undefined` when no fix can: an
|
|
14
|
+
* import already binds the name to something else, or something other than the
|
|
15
|
+
* import takes it where the code is.
|
|
16
|
+
*
|
|
17
|
+
* Every fix that writes the name makes these edits itself, so that one applied
|
|
18
|
+
* alone (an editor's quick fix) is complete; `--fix` then takes one pass per
|
|
19
|
+
* report that adds the import.
|
|
20
|
+
*/
|
|
21
|
+
export const tsDataForgeValueAt = (
|
|
22
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
23
|
+
sourceCode: TSESLint.SourceCode,
|
|
24
|
+
exportName: string,
|
|
25
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
26
|
+
): ((node: TSESTree.Node) => TsDataForgeValue | undefined) => {
|
|
27
|
+
const { at } = tsDataForgeValuesAt(sourceCode, [exportName]);
|
|
28
|
+
|
|
29
|
+
return (node) => {
|
|
30
|
+
const binding = at(node);
|
|
31
|
+
|
|
32
|
+
const localName = binding?.localNames[0];
|
|
33
|
+
|
|
34
|
+
return binding === undefined || localName === undefined
|
|
35
|
+
? undefined
|
|
36
|
+
: { localName, edits: binding.edits };
|
|
37
|
+
};
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* {@link tsDataForgeValueAt} for a name that varies from report to report (a
|
|
42
|
+
* cast function, a replacement guard): each name is bound once per file,
|
|
43
|
+
* however many reports write it.
|
|
44
|
+
*/
|
|
45
|
+
export const tsDataForgeValueAtByName = (
|
|
46
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
47
|
+
sourceCode: TSESLint.SourceCode,
|
|
48
|
+
): ((exportName: string) => ReturnType<typeof tsDataForgeValueAt>) =>
|
|
49
|
+
memoizeFunction(
|
|
50
|
+
(exportName: string) => tsDataForgeValueAt(sourceCode, exportName),
|
|
51
|
+
(exportName) => exportName,
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* {@link tsDataForgeValueAt} for several names one fix writes together: their
|
|
56
|
+
* edits are made as one, so that two names the file does not import go into a
|
|
57
|
+
* single new declaration. `localNames` are the names to write, known before any
|
|
58
|
+
* node is: the export names themselves when they cannot be bound.
|
|
59
|
+
*/
|
|
60
|
+
export const tsDataForgeValuesAt = (
|
|
61
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
62
|
+
sourceCode: TSESLint.SourceCode,
|
|
63
|
+
exportNames: readonly string[],
|
|
64
|
+
): Readonly<{
|
|
65
|
+
localNames: readonly string[];
|
|
66
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
67
|
+
at: (node: TSESTree.Node) => ValueImportBinding | undefined;
|
|
68
|
+
}> => {
|
|
69
|
+
const binding = bindValueImports(
|
|
70
|
+
sourceCode,
|
|
71
|
+
TS_DATA_FORGE_MODULE,
|
|
72
|
+
exportNames,
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
localNames: binding?.localNames ?? exportNames,
|
|
77
|
+
at: (node) =>
|
|
78
|
+
binding === undefined ||
|
|
79
|
+
binding.localNames.some((localName) =>
|
|
80
|
+
hasForeignBinding(sourceCode, node, localName, TS_DATA_FORGE_MODULE),
|
|
81
|
+
)
|
|
82
|
+
? undefined
|
|
83
|
+
: binding,
|
|
84
|
+
};
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
/** What {@link tsDataForgeValueAt} gives for one node. */
|
|
88
|
+
export type TsDataForgeValue = Readonly<{
|
|
89
|
+
/** The name to write. */
|
|
90
|
+
localName: string;
|
|
91
|
+
|
|
92
|
+
/** The edits that bind it. */
|
|
93
|
+
edits: ValueImportBinding['edits'];
|
|
94
|
+
}>;
|
|
@@ -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.
|
|
@@ -402,8 +566,8 @@ const verdictOf = (
|
|
|
402
566
|
}
|
|
403
567
|
|
|
404
568
|
case 'record':
|
|
405
|
-
// Every member already satisfies `UnknownRecord`, or none can ever be
|
|
406
|
-
//
|
|
569
|
+
// Every member already satisfies `UnknownRecord`, or none can ever be a
|
|
570
|
+
// non-null, non-array object.
|
|
407
571
|
return parts.every((p) => isGuaranteedRecord(p, checker))
|
|
408
572
|
? ({ kind: 'constant', value: true } as const)
|
|
409
573
|
: parts.every((p) => isGuaranteedNotRecord(p, checker))
|
|
@@ -435,19 +599,12 @@ 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
|
|
448
605
|
* suggest the same one, or where the guard and its replacement are the same
|
|
449
|
-
* constant: under `x!` on a `string | null`, `isNonNullish` and `isNotNull`
|
|
450
|
-
*
|
|
606
|
+
* constant: under `x!` on a `string | null`, `isNonNullish` and `isNotNull` are
|
|
607
|
+
* both always `true` on `string`.
|
|
451
608
|
*/
|
|
452
609
|
const holdsOn = (
|
|
453
610
|
verdict: Verdict,
|
|
@@ -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
|