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
package/src/rules/ast-utils.mts
CHANGED
|
@@ -3,57 +3,197 @@ import {
|
|
|
3
3
|
type TSESLint,
|
|
4
4
|
type TSESTree,
|
|
5
5
|
} from '@typescript-eslint/utils';
|
|
6
|
-
import {
|
|
6
|
+
import type { DeepReadonly } from 'ts-type-forge';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* `node` with every `as`, `satisfies`, `!` and `<T>` around it taken off: the
|
|
10
|
+
* expression whose value it is. A wrapper changes no value, so a rule judges
|
|
11
|
+
* syntax on this — `xs.length > (0 as number)` compares with the literal `0`
|
|
12
|
+
* — and writes its fix from the original node so the wrapper is kept. A type
|
|
13
|
+
* is another matter: see {@link typeWrapperLayers}. See
|
|
14
|
+
* `docs/writing-lint-rules.md` at the repository root. A copy of
|
|
15
|
+
* `eslint-config-typed`'s `type-wrapper-utils.mts`, since this package is
|
|
16
|
+
* published on its own.
|
|
17
|
+
*/
|
|
18
|
+
export const skipTypeWrappers = <N extends TSESTree.Node>(
|
|
19
|
+
node: N,
|
|
20
|
+
): N | TSESTree.Expression =>
|
|
21
|
+
isTypeWrapper(node) ? skipTypeWrappers(node.expression) : node;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* `node` and every expression inside the type wrappers around it, outermost
|
|
25
|
+
* first: `x as A as B` gives `x as A as B`, `x as A` and `x`.
|
|
26
|
+
*
|
|
27
|
+
* A conclusion a rule draws from a type — that a guard always holds, that a
|
|
28
|
+
* value is an array, that `?? 0` is dead — has to hold for the type of every
|
|
29
|
+
* layer. An assertion therefore only ever makes a rule more cautious: one
|
|
30
|
+
* that widens (`x as string | null`) is the author saying the value may be
|
|
31
|
+
* `null`, and is respected; one that narrows (`x!`, `v as string` on an
|
|
32
|
+
* `unknown`) is a claim the checker cannot verify, and the value's own type
|
|
33
|
+
* still decides.
|
|
34
|
+
*/
|
|
35
|
+
export const typeWrapperLayers = <N extends TSESTree.Node>(
|
|
36
|
+
node: N,
|
|
37
|
+
): readonly (N | TSESTree.Expression)[] =>
|
|
38
|
+
isTypeWrapper(node)
|
|
39
|
+
? ([node, ...typeWrapperLayers(node.expression)] as const)
|
|
40
|
+
: ([node] as const);
|
|
41
|
+
|
|
42
|
+
export const isTypeWrapper = (
|
|
43
|
+
node: DeepReadonly<TSESTree.Node>,
|
|
44
|
+
): node is TypeWrapper => TYPE_WRAPPERS.has(node.type);
|
|
45
|
+
|
|
46
|
+
export type TypeWrapper =
|
|
47
|
+
| TSESTree.TSAsExpression
|
|
48
|
+
| TSESTree.TSNonNullExpression
|
|
49
|
+
| TSESTree.TSSatisfiesExpression
|
|
50
|
+
| TSESTree.TSTypeAssertion;
|
|
51
|
+
|
|
52
|
+
const TYPE_WRAPPERS: ReadonlySet<AST_NODE_TYPES> = new Set([
|
|
53
|
+
AST_NODE_TYPES.TSAsExpression,
|
|
54
|
+
AST_NODE_TYPES.TSNonNullExpression,
|
|
55
|
+
AST_NODE_TYPES.TSSatisfiesExpression,
|
|
56
|
+
AST_NODE_TYPES.TSTypeAssertion,
|
|
57
|
+
]);
|
|
7
58
|
|
|
8
59
|
const isIntegerLiteral = (node: DeepReadonly<TSESTree.Expression>): boolean =>
|
|
9
60
|
node.type === AST_NODE_TYPES.Literal &&
|
|
10
61
|
typeof node.value === 'number' &&
|
|
11
62
|
Number.isInteger(node.value);
|
|
12
63
|
|
|
64
|
+
/**
|
|
65
|
+
* The source text of `node` for an argument position. A node's text never
|
|
66
|
+
* includes the parentheses the source wrote around it, so a comma (sequence)
|
|
67
|
+
* expression gets them back rather than spilling into the next argument.
|
|
68
|
+
*/
|
|
69
|
+
export const toArgumentText = (
|
|
70
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
71
|
+
node: TSESTree.Node,
|
|
72
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
73
|
+
sourceCode: TSESLint.SourceCode,
|
|
74
|
+
): string => {
|
|
75
|
+
const text = sourceCode.getText(node);
|
|
76
|
+
|
|
77
|
+
return node.type === AST_NODE_TYPES.SequenceExpression ? `(${text})` : text;
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Whether `node` is an array or a tuple at every layer of type wrappers (see
|
|
82
|
+
* {@link typeWrapperLayers}): a cast does not make a string an array, and the
|
|
83
|
+
* fix, which keeps the wrapper, has to type-check too. `false` without type
|
|
84
|
+
* information.
|
|
85
|
+
*/
|
|
86
|
+
export const isArrayOrTupleExpression = (
|
|
87
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
88
|
+
services: TSESLint.SourceCode['parserServices'],
|
|
89
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
90
|
+
node: TSESTree.Expression,
|
|
91
|
+
): boolean => {
|
|
92
|
+
const checker = services?.program?.getTypeChecker();
|
|
93
|
+
|
|
94
|
+
if (checker === undefined) {
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return typeWrapperLayers(node).every((expression) => {
|
|
99
|
+
const tsNode = services?.esTreeNodeToTSNodeMap?.get(expression);
|
|
100
|
+
|
|
101
|
+
if (tsNode === undefined) {
|
|
102
|
+
return false;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const type = checker.getTypeAtLocation(tsNode);
|
|
106
|
+
|
|
107
|
+
return checker.isArrayType(type) || checker.isTupleType(type);
|
|
108
|
+
});
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Whether `node`, under any type wrapper, is an integer literal or a `const`
|
|
113
|
+
* initialized with one.
|
|
114
|
+
*/
|
|
13
115
|
export const isIntegerLiteralOrConstant = (
|
|
14
116
|
node: DeepReadonly<TSESTree.Expression>,
|
|
15
117
|
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
16
118
|
sourceCode: TSESLint.SourceCode,
|
|
17
119
|
): boolean => {
|
|
120
|
+
// `(3 as number)`, `3 satisfies number` are still the bound 3.
|
|
121
|
+
if (isTypeWrapper(node)) {
|
|
122
|
+
return isIntegerLiteralOrConstant(node.expression, sourceCode);
|
|
123
|
+
}
|
|
124
|
+
|
|
18
125
|
// Direct integer literal (e.g., 3)
|
|
19
|
-
if (isIntegerLiteral(node))
|
|
126
|
+
if (isIntegerLiteral(node)) {
|
|
127
|
+
return true;
|
|
128
|
+
}
|
|
20
129
|
|
|
21
130
|
// Identifier referencing a const variable initialized with an integer literal
|
|
22
|
-
if (node.type !== AST_NODE_TYPES.Identifier)
|
|
131
|
+
if (node.type !== AST_NODE_TYPES.Identifier) {
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
23
134
|
|
|
24
135
|
// eslint-disable-next-line total-functions/no-unsafe-type-assertion
|
|
25
136
|
const scope = sourceCode.getScope(node as TSESTree.Node);
|
|
26
137
|
|
|
27
138
|
const variable = findVariable(scope, node.name);
|
|
28
139
|
|
|
29
|
-
if (variable === undefined)
|
|
140
|
+
if (variable === undefined) {
|
|
141
|
+
return false;
|
|
142
|
+
}
|
|
30
143
|
|
|
31
144
|
// Must have exactly one definition (const)
|
|
32
|
-
if (variable.defs.length !== 1)
|
|
145
|
+
if (variable.defs.length !== 1) {
|
|
146
|
+
return false;
|
|
147
|
+
}
|
|
33
148
|
|
|
34
149
|
const def = variable.defs[0];
|
|
35
150
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
151
|
+
return (
|
|
152
|
+
def !== undefined &&
|
|
153
|
+
isVariableDefinition(def) &&
|
|
154
|
+
def.parent.kind === 'const' &&
|
|
155
|
+
def.node.init != null &&
|
|
156
|
+
// Reject if there is an explicit type annotation (e.g., `const n: number = 3`)
|
|
157
|
+
// because the literal type is widened and type guard cannot narrow it.
|
|
158
|
+
def.node.id.typeAnnotation === undefined &&
|
|
159
|
+
isIntegerLiteral(def.node.init)
|
|
160
|
+
);
|
|
161
|
+
};
|
|
47
162
|
|
|
48
|
-
|
|
163
|
+
/**
|
|
164
|
+
* `node` as a `<array>.length` read, looked for under any type wrapper
|
|
165
|
+
* (`(xs.length satisfies number)`, `xs.length!`), or `undefined`. The property
|
|
166
|
+
* must be written `.length`: `xs[length]` reads whatever index the variable
|
|
167
|
+
* `length` holds.
|
|
168
|
+
*/
|
|
169
|
+
export const asLengthAccess = (
|
|
170
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
171
|
+
node: TSESTree.Expression,
|
|
172
|
+
): TSESTree.MemberExpression | undefined => {
|
|
173
|
+
const inner = skipTypeWrappers(node);
|
|
174
|
+
|
|
175
|
+
return inner.type === AST_NODE_TYPES.MemberExpression &&
|
|
176
|
+
!inner.computed &&
|
|
177
|
+
inner.property.type === AST_NODE_TYPES.Identifier &&
|
|
178
|
+
inner.property.name === 'length'
|
|
179
|
+
? inner
|
|
180
|
+
: undefined;
|
|
49
181
|
};
|
|
50
182
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
)
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
183
|
+
/**
|
|
184
|
+
* Whether two array expressions are written the same way, type wrappers aside:
|
|
185
|
+
* `xs.length >= 1 && (xs as readonly T[]).length <= 3` bounds one array.
|
|
186
|
+
*/
|
|
187
|
+
export const isSameArrayText = (
|
|
188
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
189
|
+
a: TSESTree.Expression,
|
|
190
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
191
|
+
b: TSESTree.Expression,
|
|
192
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
193
|
+
sourceCode: TSESLint.SourceCode,
|
|
194
|
+
): boolean =>
|
|
195
|
+
sourceCode.getText(skipTypeWrappers(a)) ===
|
|
196
|
+
sourceCode.getText(skipTypeWrappers(b));
|
|
57
197
|
|
|
58
198
|
type LengthComparison = Readonly<{
|
|
59
199
|
array: TSESTree.Expression;
|
|
@@ -65,7 +205,9 @@ type LengthComparison = Readonly<{
|
|
|
65
205
|
* Parses a `<array>.length <op> <bound>` comparison (in either operand order)
|
|
66
206
|
* into its array expression, integer-literal/`const` `bound`, and whether it is
|
|
67
207
|
* a lower (`min`) or upper (`max`) length bound. Returns `undefined` for
|
|
68
|
-
* anything else.
|
|
208
|
+
* anything else. The length and the bound are read through type wrappers; the
|
|
209
|
+
* `bound` returned is the literal or constant underneath, since a wrapper such
|
|
210
|
+
* as `as number` would only widen the length a guard narrows to.
|
|
69
211
|
*
|
|
70
212
|
* - `xs.length >= n` / `n <= xs.length` → `min`
|
|
71
213
|
* - `xs.length <= n` / `n >= xs.length` → `max`
|
|
@@ -85,15 +227,21 @@ export const parseLengthComparison = (
|
|
|
85
227
|
|
|
86
228
|
const { left, right, operator } = node;
|
|
87
229
|
|
|
88
|
-
const
|
|
230
|
+
const leftLength = asLengthAccess(left);
|
|
89
231
|
|
|
90
|
-
const
|
|
232
|
+
const lengthOnLeft = leftLength !== undefined;
|
|
233
|
+
|
|
234
|
+
const lengthSide = leftLength ?? asLengthAccess(right);
|
|
91
235
|
|
|
92
236
|
const boundSide = lengthOnLeft ? right : left;
|
|
93
237
|
|
|
94
|
-
if (
|
|
238
|
+
if (lengthSide === undefined) {
|
|
239
|
+
return undefined;
|
|
240
|
+
}
|
|
95
241
|
|
|
96
|
-
if (!isIntegerLiteralOrConstant(boundSide, sourceCode))
|
|
242
|
+
if (!isIntegerLiteralOrConstant(boundSide, sourceCode)) {
|
|
243
|
+
return undefined;
|
|
244
|
+
}
|
|
97
245
|
|
|
98
246
|
const kind: LengthComparison['kind'] = (
|
|
99
247
|
lengthOnLeft ? operator === '>=' : operator === '<='
|
|
@@ -101,7 +249,11 @@ export const parseLengthComparison = (
|
|
|
101
249
|
? 'min'
|
|
102
250
|
: 'max';
|
|
103
251
|
|
|
104
|
-
return {
|
|
252
|
+
return {
|
|
253
|
+
array: lengthSide.object,
|
|
254
|
+
bound: skipTypeWrappers(boundSide),
|
|
255
|
+
kind,
|
|
256
|
+
};
|
|
105
257
|
};
|
|
106
258
|
|
|
107
259
|
/**
|
|
@@ -128,17 +280,21 @@ export const isPartOfBoundedLengthCheck = (
|
|
|
128
280
|
|
|
129
281
|
const self = parseLengthComparison(node, sourceCode);
|
|
130
282
|
|
|
131
|
-
if (self === undefined)
|
|
283
|
+
if (self === undefined) {
|
|
284
|
+
return false;
|
|
285
|
+
}
|
|
132
286
|
|
|
133
287
|
const sibling = parent.left === node ? parent.right : parent.left;
|
|
134
288
|
|
|
135
289
|
const other = parseLengthComparison(sibling, sourceCode);
|
|
136
290
|
|
|
137
|
-
if (other === undefined)
|
|
291
|
+
if (other === undefined) {
|
|
292
|
+
return false;
|
|
293
|
+
}
|
|
138
294
|
|
|
139
295
|
return (
|
|
140
296
|
self.kind !== other.kind &&
|
|
141
|
-
|
|
297
|
+
isSameArrayText(self.array, other.array, sourceCode)
|
|
142
298
|
);
|
|
143
299
|
};
|
|
144
300
|
|
|
@@ -161,9 +317,8 @@ const findVariable = (
|
|
|
161
317
|
): TSESLint.Scope.Variable | undefined => {
|
|
162
318
|
const variable = scope.set.get(name);
|
|
163
319
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
return findVariable(scope.upper, name);
|
|
320
|
+
return (
|
|
321
|
+
variable ??
|
|
322
|
+
(scope.upper === null ? undefined : findVariable(scope.upper, name))
|
|
323
|
+
);
|
|
169
324
|
};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// Keep this list in sync with the `as<Type>` cast functions exported by
|
|
2
|
-
// ts-data-forge. `pnpm run check:branded-number-types` (also run during the
|
|
2
|
+
// ts-data-forge. `pnpm run check:dist:branded-number-types` (also run during the
|
|
3
3
|
// build) fails if a branded number type is missing here or no longer exists.
|
|
4
4
|
const brandedNumberTypes = [
|
|
5
5
|
'FiniteNumber',
|
|
@@ -3,7 +3,8 @@ import {
|
|
|
3
3
|
type TSESLint,
|
|
4
4
|
type TSESTree,
|
|
5
5
|
} from '@typescript-eslint/utils';
|
|
6
|
-
import {
|
|
6
|
+
import type { DeepReadonly } from 'ts-type-forge';
|
|
7
|
+
import { isTypeWrapper } from './ast-utils.mjs';
|
|
7
8
|
import { TS_DATA_FORGE_MODULE } from './constants.mjs';
|
|
8
9
|
|
|
9
10
|
/* eslint-disable @typescript-eslint/prefer-readonly-parameter-types */
|
|
@@ -20,7 +21,9 @@ export const getTsDataForgeImport = (
|
|
|
20
21
|
export const getNamedImports = (
|
|
21
22
|
node: DeepReadonly<TSESTree.ImportDeclaration> | undefined,
|
|
22
23
|
): readonly string[] => {
|
|
23
|
-
if (node === undefined)
|
|
24
|
+
if (node === undefined) {
|
|
25
|
+
return [];
|
|
26
|
+
}
|
|
24
27
|
|
|
25
28
|
return node.specifiers.flatMap((specifier) =>
|
|
26
29
|
specifier.type === AST_NODE_TYPES.ImportSpecifier
|
|
@@ -83,7 +86,9 @@ export const buildCalleeResolver = (
|
|
|
83
86
|
|
|
84
87
|
const localToCanonical = new Map<string, string>(
|
|
85
88
|
specifiers.flatMap((specifier) => {
|
|
86
|
-
if (specifier.type !== AST_NODE_TYPES.ImportSpecifier)
|
|
89
|
+
if (specifier.type !== AST_NODE_TYPES.ImportSpecifier) {
|
|
90
|
+
return [];
|
|
91
|
+
}
|
|
87
92
|
|
|
88
93
|
const importedName =
|
|
89
94
|
specifier.imported.type === AST_NODE_TYPES.Identifier
|
|
@@ -96,7 +101,16 @@ export const buildCalleeResolver = (
|
|
|
96
101
|
}),
|
|
97
102
|
);
|
|
98
103
|
|
|
99
|
-
|
|
104
|
+
// `(isNull as (u: unknown) => boolean)(x)` and `(tf as typeof tf).isNull(x)`
|
|
105
|
+
// still call `isNull`: both the callee and a namespace object are read
|
|
106
|
+
// through type wrappers.
|
|
107
|
+
const resolve = (
|
|
108
|
+
callee: DeepReadonly<TSESTree.Expression>,
|
|
109
|
+
): ResolvedCallee | undefined => {
|
|
110
|
+
if (isTypeWrapper(callee)) {
|
|
111
|
+
return resolve(callee.expression);
|
|
112
|
+
}
|
|
113
|
+
|
|
100
114
|
if (callee.type === AST_NODE_TYPES.Identifier) {
|
|
101
115
|
const canonicalName = localToCanonical.get(callee.name);
|
|
102
116
|
|
|
@@ -111,25 +125,35 @@ export const buildCalleeResolver = (
|
|
|
111
125
|
}
|
|
112
126
|
|
|
113
127
|
if (
|
|
114
|
-
namespaceName
|
|
115
|
-
callee.type
|
|
116
|
-
|
|
117
|
-
callee.
|
|
118
|
-
callee.object.name === namespaceName &&
|
|
119
|
-
callee.property.type === AST_NODE_TYPES.Identifier
|
|
128
|
+
namespaceName === undefined ||
|
|
129
|
+
callee.type !== AST_NODE_TYPES.MemberExpression ||
|
|
130
|
+
callee.computed ||
|
|
131
|
+
callee.property.type !== AST_NODE_TYPES.Identifier
|
|
120
132
|
) {
|
|
121
|
-
return
|
|
122
|
-
canonicalName: callee.property.name,
|
|
123
|
-
// eslint-disable-next-line total-functions/no-unsafe-type-assertion
|
|
124
|
-
propertyNode: callee.property as TSESTree.Node,
|
|
125
|
-
isNamespace: true,
|
|
126
|
-
};
|
|
133
|
+
return undefined;
|
|
127
134
|
}
|
|
128
135
|
|
|
129
|
-
|
|
136
|
+
const object = skipReadonlyTypeWrappers(callee.object);
|
|
137
|
+
|
|
138
|
+
return object.type === AST_NODE_TYPES.Identifier &&
|
|
139
|
+
object.name === namespaceName
|
|
140
|
+
? {
|
|
141
|
+
canonicalName: callee.property.name,
|
|
142
|
+
// eslint-disable-next-line total-functions/no-unsafe-type-assertion
|
|
143
|
+
propertyNode: callee.property as TSESTree.Node,
|
|
144
|
+
isNamespace: true,
|
|
145
|
+
}
|
|
146
|
+
: undefined;
|
|
130
147
|
};
|
|
148
|
+
|
|
149
|
+
return resolve;
|
|
131
150
|
};
|
|
132
151
|
|
|
152
|
+
const skipReadonlyTypeWrappers = (
|
|
153
|
+
node: DeepReadonly<TSESTree.Expression>,
|
|
154
|
+
): DeepReadonly<TSESTree.Expression> =>
|
|
155
|
+
isTypeWrapper(node) ? skipReadonlyTypeWrappers(node.expression) : node;
|
|
156
|
+
|
|
133
157
|
export const buildImportFixes = (
|
|
134
158
|
fixer: TSESLint.RuleFixer,
|
|
135
159
|
program: TSESTree.Program,
|