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.
Files changed (139) hide show
  1. package/dist/plugin.d.mts +1 -1
  2. package/dist/plugin.d.mts.map +1 -1
  3. package/dist/plugin.mjs +1 -0
  4. package/dist/plugin.mjs.map +1 -1
  5. package/dist/rule-types.d.mts +3 -3
  6. package/dist/rule-types.d.mts.map +1 -1
  7. package/dist/rules/ast-utils.d.mts +59 -3
  8. package/dist/rules/ast-utils.d.mts.map +1 -1
  9. package/dist/rules/ast-utils.mjs +142 -31
  10. package/dist/rules/ast-utils.mjs.map +1 -1
  11. package/dist/rules/branded-number-types.mjs.map +1 -1
  12. package/dist/rules/import-utils.d.mts +1 -1
  13. package/dist/rules/import-utils.d.mts.map +1 -1
  14. package/dist/rules/import-utils.mjs +26 -13
  15. package/dist/rules/import-utils.mjs.map +1 -1
  16. package/dist/rules/no-side-effect-import.d.mts +1 -1
  17. package/dist/rules/no-side-effect-import.d.mts.map +1 -1
  18. package/dist/rules/no-unnecessary-type-guard.d.mts.map +1 -1
  19. package/dist/rules/no-unnecessary-type-guard.mjs +161 -101
  20. package/dist/rules/no-unnecessary-type-guard.mjs.map +1 -1
  21. package/dist/rules/prefer-arr-is-array.d.mts.map +1 -1
  22. package/dist/rules/prefer-arr-is-array.mjs +25 -11
  23. package/dist/rules/prefer-arr-is-array.mjs.map +1 -1
  24. package/dist/rules/prefer-arr-is-bounded-length-array.d.mts +1 -1
  25. package/dist/rules/prefer-arr-is-bounded-length-array.d.mts.map +1 -1
  26. package/dist/rules/prefer-arr-is-bounded-length-array.mjs +11 -21
  27. package/dist/rules/prefer-arr-is-bounded-length-array.mjs.map +1 -1
  28. package/dist/rules/prefer-arr-is-fixed-length-array.d.mts +1 -1
  29. package/dist/rules/prefer-arr-is-fixed-length-array.d.mts.map +1 -1
  30. package/dist/rules/prefer-arr-is-fixed-length-array.mjs +16 -28
  31. package/dist/rules/prefer-arr-is-fixed-length-array.mjs.map +1 -1
  32. package/dist/rules/prefer-arr-is-max-length-array.d.mts +1 -1
  33. package/dist/rules/prefer-arr-is-max-length-array.d.mts.map +1 -1
  34. package/dist/rules/prefer-arr-is-max-length-array.mjs +16 -24
  35. package/dist/rules/prefer-arr-is-max-length-array.mjs.map +1 -1
  36. package/dist/rules/prefer-arr-is-min-length-array.d.mts +1 -1
  37. package/dist/rules/prefer-arr-is-min-length-array.d.mts.map +1 -1
  38. package/dist/rules/prefer-arr-is-min-length-array.mjs +16 -24
  39. package/dist/rules/prefer-arr-is-min-length-array.mjs.map +1 -1
  40. package/dist/rules/prefer-arr-is-non-empty.d.mts.map +1 -1
  41. package/dist/rules/prefer-arr-is-non-empty.mjs +9 -22
  42. package/dist/rules/prefer-arr-is-non-empty.mjs.map +1 -1
  43. package/dist/rules/prefer-arr-scan.d.mts +6 -5
  44. package/dist/rules/prefer-arr-scan.d.mts.map +1 -1
  45. package/dist/rules/prefer-arr-scan.mjs +68 -31
  46. package/dist/rules/prefer-arr-scan.mjs.map +1 -1
  47. package/dist/rules/prefer-arr-sum.d.mts.map +1 -1
  48. package/dist/rules/prefer-arr-sum.mjs +57 -40
  49. package/dist/rules/prefer-arr-sum.mjs.map +1 -1
  50. package/dist/rules/prefer-arr-uniq.d.mts.map +1 -1
  51. package/dist/rules/prefer-arr-uniq.mjs +59 -30
  52. package/dist/rules/prefer-arr-uniq.mjs.map +1 -1
  53. package/dist/rules/prefer-as-int.d.mts.map +1 -1
  54. package/dist/rules/prefer-as-int.mjs +27 -24
  55. package/dist/rules/prefer-as-int.mjs.map +1 -1
  56. package/dist/rules/prefer-canonical-array-slicing.d.mts.map +1 -1
  57. package/dist/rules/prefer-canonical-array-slicing.mjs +59 -28
  58. package/dist/rules/prefer-canonical-array-slicing.mjs.map +1 -1
  59. package/dist/rules/prefer-canonical-length-cast.d.mts.map +1 -1
  60. package/dist/rules/prefer-canonical-length-cast.mjs +41 -16
  61. package/dist/rules/prefer-canonical-length-cast.mjs.map +1 -1
  62. package/dist/rules/prefer-canonical-length-guard.d.mts.map +1 -1
  63. package/dist/rules/prefer-canonical-length-guard.mjs +18 -8
  64. package/dist/rules/prefer-canonical-length-guard.mjs.map +1 -1
  65. package/dist/rules/prefer-comparison-over-nullish-guard.d.mts.map +1 -1
  66. package/dist/rules/prefer-comparison-over-nullish-guard.mjs +31 -6
  67. package/dist/rules/prefer-comparison-over-nullish-guard.mjs.map +1 -1
  68. package/dist/rules/prefer-is-non-null-object.d.mts.map +1 -1
  69. package/dist/rules/prefer-is-non-null-object.mjs +56 -40
  70. package/dist/rules/prefer-is-non-null-object.mjs.map +1 -1
  71. package/dist/rules/prefer-is-record-and-has-key.d.mts.map +1 -1
  72. package/dist/rules/prefer-is-record-and-has-key.mjs +18 -8
  73. package/dist/rules/prefer-is-record-and-has-key.mjs.map +1 -1
  74. package/dist/rules/prefer-num-safe-parse-float.d.mts +3 -1
  75. package/dist/rules/prefer-num-safe-parse-float.d.mts.map +1 -1
  76. package/dist/rules/prefer-num-safe-parse-float.mjs +27 -10
  77. package/dist/rules/prefer-num-safe-parse-float.mjs.map +1 -1
  78. package/dist/rules/prefer-num-safe-parse-int.d.mts.map +1 -1
  79. package/dist/rules/prefer-num-safe-parse-int.mjs +24 -11
  80. package/dist/rules/prefer-num-safe-parse-int.mjs.map +1 -1
  81. package/dist/rules/prefer-obj-over-entries-round-trip.d.mts.map +1 -1
  82. package/dist/rules/prefer-obj-over-entries-round-trip.mjs +22 -11
  83. package/dist/rules/prefer-obj-over-entries-round-trip.mjs.map +1 -1
  84. package/dist/rules/prefer-range-for-loop.d.mts.map +1 -1
  85. package/dist/rules/prefer-range-for-loop.mjs +15 -8
  86. package/dist/rules/prefer-range-for-loop.mjs.map +1 -1
  87. package/dist/types.d.mts +2 -2
  88. package/dist/types.d.mts.map +1 -1
  89. package/package.json +25 -23
  90. package/src/plugin.mts +5 -4
  91. package/src/plugin.test.mts +0 -13
  92. package/src/rule-types.mts +3 -3
  93. package/src/rules/ast-utils.mts +192 -37
  94. package/src/rules/branded-number-types.mts +1 -1
  95. package/src/rules/import-utils.mts +41 -17
  96. package/src/rules/no-side-effect-import.mts +1 -1
  97. package/src/rules/no-unnecessary-type-guard.mts +249 -148
  98. package/src/rules/no-unnecessary-type-guard.test.mts +100 -0
  99. package/src/rules/prefer-arr-is-array.mts +32 -13
  100. package/src/rules/prefer-arr-is-array.test.mts +40 -0
  101. package/src/rules/prefer-arr-is-bounded-length-array.mts +22 -26
  102. package/src/rules/prefer-arr-is-bounded-length-array.test.mts +68 -0
  103. package/src/rules/prefer-arr-is-fixed-length-array.mts +26 -38
  104. package/src/rules/prefer-arr-is-fixed-length-array.test.mts +63 -0
  105. package/src/rules/prefer-arr-is-max-length-array.mts +26 -27
  106. package/src/rules/prefer-arr-is-max-length-array.test.mts +67 -0
  107. package/src/rules/prefer-arr-is-min-length-array.mts +26 -27
  108. package/src/rules/prefer-arr-is-min-length-array.test.mts +70 -0
  109. package/src/rules/prefer-arr-is-non-empty.mts +19 -24
  110. package/src/rules/prefer-arr-is-non-empty.test.mts +79 -0
  111. package/src/rules/prefer-arr-scan.mts +107 -37
  112. package/src/rules/prefer-arr-scan.test.mts +149 -0
  113. package/src/rules/prefer-arr-sum.mts +87 -50
  114. package/src/rules/prefer-arr-sum.test.mts +97 -0
  115. package/src/rules/prefer-arr-uniq.mts +83 -33
  116. package/src/rules/prefer-arr-uniq.test.mts +71 -0
  117. package/src/rules/prefer-as-int.mts +38 -38
  118. package/src/rules/prefer-as-int.test.mts +51 -0
  119. package/src/rules/prefer-canonical-array-slicing.mts +84 -29
  120. package/src/rules/prefer-canonical-array-slicing.test.mts +92 -0
  121. package/src/rules/prefer-canonical-length-cast.mts +56 -18
  122. package/src/rules/prefer-canonical-length-cast.test.mts +111 -0
  123. package/src/rules/prefer-canonical-length-guard.mts +25 -11
  124. package/src/rules/prefer-canonical-length-guard.test.mts +99 -0
  125. package/src/rules/prefer-comparison-over-nullish-guard.mts +42 -9
  126. package/src/rules/prefer-comparison-over-nullish-guard.test.mts +136 -0
  127. package/src/rules/prefer-is-non-null-object.mts +57 -33
  128. package/src/rules/prefer-is-non-null-object.test.mts +42 -0
  129. package/src/rules/prefer-is-record-and-has-key.mts +27 -12
  130. package/src/rules/prefer-is-record-and-has-key.test.mts +34 -0
  131. package/src/rules/prefer-num-safe-parse-float.mts +37 -13
  132. package/src/rules/prefer-num-safe-parse-float.test.mts +79 -0
  133. package/src/rules/prefer-num-safe-parse-int.mts +41 -15
  134. package/src/rules/prefer-num-safe-parse-int.test.mts +64 -0
  135. package/src/rules/prefer-obj-over-entries-round-trip.mts +34 -12
  136. package/src/rules/prefer-range-for-loop.mts +21 -9
  137. package/src/rules/prefer-range-for-loop.test.mts +75 -0
  138. package/src/rules/rules.mts +1 -1
  139. package/src/types.mts +2 -2
@@ -3,57 +3,197 @@ import {
3
3
  type TSESLint,
4
4
  type TSESTree,
5
5
  } from '@typescript-eslint/utils';
6
- import { type DeepReadonly } from 'ts-type-forge';
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)) return true;
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) return false;
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) return false;
140
+ if (variable === undefined) {
141
+ return false;
142
+ }
30
143
 
31
144
  // Must have exactly one definition (const)
32
- if (variable.defs.length !== 1) return false;
145
+ if (variable.defs.length !== 1) {
146
+ return false;
147
+ }
33
148
 
34
149
  const def = variable.defs[0];
35
150
 
36
- if (def === undefined) return false;
37
-
38
- if (!isVariableDefinition(def)) return false;
39
-
40
- if (def.parent.kind !== 'const') return false;
41
-
42
- if (def.node.init == null) return false;
43
-
44
- // Reject if there is an explicit type annotation (e.g., `const n: number = 3`)
45
- // because the literal type is widened and type guard cannot narrow it.
46
- if (def.node.id.typeAnnotation !== undefined) return false;
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
- return isIntegerLiteral(def.node.init);
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
- export const isLengthAccess = (
52
- node: DeepReadonly<TSESTree.Expression>,
53
- ): node is TSESTree.MemberExpression =>
54
- node.type === AST_NODE_TYPES.MemberExpression &&
55
- node.property.type === AST_NODE_TYPES.Identifier &&
56
- node.property.name === 'length';
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 lengthOnLeft = isLengthAccess(left);
230
+ const leftLength = asLengthAccess(left);
89
231
 
90
- const lengthSide = lengthOnLeft ? left : right;
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 (!isLengthAccess(lengthSide)) return undefined;
238
+ if (lengthSide === undefined) {
239
+ return undefined;
240
+ }
95
241
 
96
- if (!isIntegerLiteralOrConstant(boundSide, sourceCode)) return undefined;
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 { array: lengthSide.object, bound: boundSide, kind };
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) return false;
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) return false;
291
+ if (other === undefined) {
292
+ return false;
293
+ }
138
294
 
139
295
  return (
140
296
  self.kind !== other.kind &&
141
- sourceCode.getText(self.array) === sourceCode.getText(other.array)
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
- if (variable !== undefined) return variable;
165
-
166
- if (scope.upper === null) return undefined;
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 { type DeepReadonly } from 'ts-type-forge';
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) return [];
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) return [];
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
- return (callee) => {
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 !== undefined &&
115
- callee.type === AST_NODE_TYPES.MemberExpression &&
116
- !callee.computed &&
117
- callee.object.type === AST_NODE_TYPES.Identifier &&
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
- return undefined;
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,
@@ -1,4 +1,4 @@
1
- import { type TSESLint, type TSESTree } from '@typescript-eslint/utils';
1
+ import type { TSESLint, TSESTree } from '@typescript-eslint/utils';
2
2
  import { Arr } from 'ts-data-forge';
3
3
  import { TS_DATA_FORGE_MODULE } from './constants.mjs';
4
4