eslint-plugin-ts-data-forge 0.8.5 → 0.9.0

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 (135) hide show
  1. package/README.md +20 -19
  2. package/dist/plugin.d.mts +2 -0
  3. package/dist/plugin.d.mts.map +1 -1
  4. package/dist/plugin.mjs +1 -0
  5. package/dist/plugin.mjs.map +1 -1
  6. package/dist/rule-types.d.mts +16 -1
  7. package/dist/rule-types.d.mts.map +1 -1
  8. package/dist/rule-types.mjs.map +1 -1
  9. package/dist/rules/ast-utils.d.mts +21 -34
  10. package/dist/rules/ast-utils.d.mts.map +1 -1
  11. package/dist/rules/ast-utils.mjs +53 -58
  12. package/dist/rules/ast-utils.mjs.map +1 -1
  13. package/dist/rules/constants.d.mts +2 -0
  14. package/dist/rules/constants.d.mts.map +1 -1
  15. package/dist/rules/constants.mjs +9 -0
  16. package/dist/rules/constants.mjs.map +1 -1
  17. package/dist/rules/mutable-result.d.mts +10 -0
  18. package/dist/rules/mutable-result.d.mts.map +1 -0
  19. package/dist/rules/mutable-result.mjs +145 -0
  20. package/dist/rules/mutable-result.mjs.map +1 -0
  21. package/dist/rules/no-unnecessary-type-guard.d.mts.map +1 -1
  22. package/dist/rules/no-unnecessary-type-guard.mjs +6 -6
  23. package/dist/rules/no-unnecessary-type-guard.mjs.map +1 -1
  24. package/dist/rules/prefer-arr-generate.d.mts +57 -0
  25. package/dist/rules/prefer-arr-generate.d.mts.map +1 -0
  26. package/dist/rules/prefer-arr-generate.mjs +588 -0
  27. package/dist/rules/prefer-arr-generate.mjs.map +1 -0
  28. package/dist/rules/prefer-arr-is-array.d.mts.map +1 -1
  29. package/dist/rules/prefer-arr-is-array.mjs +7 -5
  30. package/dist/rules/prefer-arr-is-array.mjs.map +1 -1
  31. package/dist/rules/prefer-arr-is-bounded-length-array.d.mts.map +1 -1
  32. package/dist/rules/prefer-arr-is-bounded-length-array.mjs +9 -6
  33. package/dist/rules/prefer-arr-is-bounded-length-array.mjs.map +1 -1
  34. package/dist/rules/prefer-arr-is-fixed-length-array.d.mts.map +1 -1
  35. package/dist/rules/prefer-arr-is-fixed-length-array.mjs +9 -6
  36. package/dist/rules/prefer-arr-is-fixed-length-array.mjs.map +1 -1
  37. package/dist/rules/prefer-arr-is-max-length-array.d.mts.map +1 -1
  38. package/dist/rules/prefer-arr-is-max-length-array.mjs +9 -6
  39. package/dist/rules/prefer-arr-is-max-length-array.mjs.map +1 -1
  40. package/dist/rules/prefer-arr-is-min-length-array.d.mts.map +1 -1
  41. package/dist/rules/prefer-arr-is-min-length-array.mjs +9 -6
  42. package/dist/rules/prefer-arr-is-min-length-array.mjs.map +1 -1
  43. package/dist/rules/prefer-arr-is-non-empty.d.mts.map +1 -1
  44. package/dist/rules/prefer-arr-is-non-empty.mjs +9 -6
  45. package/dist/rules/prefer-arr-is-non-empty.mjs.map +1 -1
  46. package/dist/rules/prefer-arr-scan.d.mts.map +1 -1
  47. package/dist/rules/prefer-arr-scan.mjs +8 -11
  48. package/dist/rules/prefer-arr-scan.mjs.map +1 -1
  49. package/dist/rules/prefer-arr-sum.d.mts.map +1 -1
  50. package/dist/rules/prefer-arr-sum.mjs +8 -6
  51. package/dist/rules/prefer-arr-sum.mjs.map +1 -1
  52. package/dist/rules/prefer-arr-uniq.d.mts.map +1 -1
  53. package/dist/rules/prefer-arr-uniq.mjs +12 -166
  54. package/dist/rules/prefer-arr-uniq.mjs.map +1 -1
  55. package/dist/rules/prefer-as-int.d.mts.map +1 -1
  56. package/dist/rules/prefer-as-int.mjs +6 -8
  57. package/dist/rules/prefer-as-int.mjs.map +1 -1
  58. package/dist/rules/prefer-canonical-array-slicing.d.mts.map +1 -1
  59. package/dist/rules/prefer-canonical-array-slicing.mjs +57 -34
  60. package/dist/rules/prefer-canonical-array-slicing.mjs.map +1 -1
  61. package/dist/rules/prefer-canonical-length-cast.d.mts.map +1 -1
  62. package/dist/rules/prefer-canonical-length-cast.mjs +3 -3
  63. package/dist/rules/prefer-canonical-length-cast.mjs.map +1 -1
  64. package/dist/rules/prefer-canonical-length-guard.d.mts.map +1 -1
  65. package/dist/rules/prefer-canonical-length-guard.mjs +4 -4
  66. package/dist/rules/prefer-canonical-length-guard.mjs.map +1 -1
  67. package/dist/rules/prefer-comparison-over-nullish-guard.d.mts.map +1 -1
  68. package/dist/rules/prefer-comparison-over-nullish-guard.mjs +3 -2
  69. package/dist/rules/prefer-comparison-over-nullish-guard.mjs.map +1 -1
  70. package/dist/rules/prefer-is-non-null-object.d.mts.map +1 -1
  71. package/dist/rules/prefer-is-non-null-object.mjs +113 -43
  72. package/dist/rules/prefer-is-non-null-object.mjs.map +1 -1
  73. package/dist/rules/prefer-is-record-and-has-key.d.mts.map +1 -1
  74. package/dist/rules/prefer-is-record-and-has-key.mjs +105 -48
  75. package/dist/rules/prefer-is-record-and-has-key.mjs.map +1 -1
  76. package/dist/rules/prefer-num-safe-parse-float.d.mts.map +1 -1
  77. package/dist/rules/prefer-num-safe-parse-float.mjs +6 -6
  78. package/dist/rules/prefer-num-safe-parse-float.mjs.map +1 -1
  79. package/dist/rules/prefer-num-safe-parse-int.d.mts.map +1 -1
  80. package/dist/rules/prefer-num-safe-parse-int.mjs +6 -6
  81. package/dist/rules/prefer-num-safe-parse-int.mjs.map +1 -1
  82. package/dist/rules/prefer-obj-over-entries-round-trip.d.mts.map +1 -1
  83. package/dist/rules/prefer-obj-over-entries-round-trip.mjs +9 -12
  84. package/dist/rules/prefer-obj-over-entries-round-trip.mjs.map +1 -1
  85. package/dist/rules/prefer-range-for-loop.d.mts.map +1 -1
  86. package/dist/rules/prefer-range-for-loop.mjs +104 -23
  87. package/dist/rules/prefer-range-for-loop.mjs.map +1 -1
  88. package/dist/rules/rules.d.mts +1 -0
  89. package/dist/rules/rules.d.mts.map +1 -1
  90. package/dist/rules/rules.mjs +2 -0
  91. package/dist/rules/rules.mjs.map +1 -1
  92. package/package.json +11 -10
  93. package/src/plugin.mts +1 -0
  94. package/src/rule-types.mts +17 -1
  95. package/src/rules/ast-utils.mts +73 -80
  96. package/src/rules/constants.mts +12 -0
  97. package/src/rules/mutable-result.mts +204 -0
  98. package/src/rules/no-unnecessary-type-guard.mts +14 -10
  99. package/src/rules/prefer-arr-generate.mts +918 -0
  100. package/src/rules/prefer-arr-generate.test.mts +967 -0
  101. package/src/rules/prefer-arr-is-array.mts +14 -7
  102. package/src/rules/prefer-arr-is-array.test.mts +14 -0
  103. package/src/rules/prefer-arr-is-bounded-length-array.mts +16 -10
  104. package/src/rules/prefer-arr-is-bounded-length-array.test.mts +19 -0
  105. package/src/rules/prefer-arr-is-fixed-length-array.mts +17 -11
  106. package/src/rules/prefer-arr-is-max-length-array.mts +17 -11
  107. package/src/rules/prefer-arr-is-max-length-array.test.mts +8 -0
  108. package/src/rules/prefer-arr-is-min-length-array.mts +17 -11
  109. package/src/rules/prefer-arr-is-min-length-array.test.mts +8 -0
  110. package/src/rules/prefer-arr-is-non-empty.mts +16 -13
  111. package/src/rules/prefer-arr-scan.mts +24 -19
  112. package/src/rules/prefer-arr-sum.mts +18 -14
  113. package/src/rules/prefer-arr-uniq.mts +25 -234
  114. package/src/rules/prefer-as-int.mts +19 -12
  115. package/src/rules/prefer-canonical-array-slicing.mts +79 -52
  116. package/src/rules/prefer-canonical-array-slicing.test.mts +55 -0
  117. package/src/rules/prefer-canonical-length-cast.mts +7 -3
  118. package/src/rules/prefer-canonical-length-guard.mts +9 -4
  119. package/src/rules/prefer-comparison-over-nullish-guard.mts +3 -2
  120. package/src/rules/prefer-comparison-over-nullish-guard.test.mts +16 -0
  121. package/src/rules/prefer-is-non-null-object.mts +171 -64
  122. package/src/rules/prefer-is-non-null-object.test.mts +138 -0
  123. package/src/rules/prefer-is-record-and-has-key.mts +159 -74
  124. package/src/rules/prefer-is-record-and-has-key.test.mts +118 -23
  125. package/src/rules/prefer-num-safe-parse-float.mts +15 -10
  126. package/src/rules/prefer-num-safe-parse-int.mts +15 -13
  127. package/src/rules/prefer-obj-over-entries-round-trip.mts +19 -22
  128. package/src/rules/prefer-range-for-loop.mts +157 -31
  129. package/src/rules/prefer-range-for-loop.test.mts +127 -5
  130. package/src/rules/rules.mts +2 -0
  131. package/dist/rules/import-utils.d.mts +0 -30
  132. package/dist/rules/import-utils.d.mts.map +0 -1
  133. package/dist/rules/import-utils.mjs +0 -98
  134. package/dist/rules/import-utils.mjs.map +0 -1
  135. package/src/rules/import-utils.mts +0 -175
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eslint-plugin-ts-data-forge",
3
- "version": "0.8.5",
3
+ "version": "0.9.0",
4
4
  "private": false,
5
5
  "description": "ESLint rules that steer code toward ts-data-forge idioms.",
6
6
  "keywords": [
@@ -35,16 +35,17 @@
35
35
  ],
36
36
  "dependencies": {
37
37
  "@typescript-eslint/utils": "^8.71.1",
38
- "ts-data-forge": "14.7.1",
38
+ "eslint-rule-forge": "^0.2.0",
39
+ "ts-data-forge": "14.8.0",
39
40
  "ts-type-forge": "~9.2.2"
40
41
  },
41
42
  "devDependencies": {
42
43
  "@typescript-eslint/parser": "8.71.1",
43
44
  "@typescript-eslint/rule-tester": "8.71.1",
44
45
  "dedent": "1.7.2",
45
- "eslint-config-typed": "5.17.3",
46
- "eslint-plugin-ts-fortress": "1.3.2",
47
- "eslint-plugin-ts-type-forge": "0.8.7",
46
+ "eslint-config-typed": "5.19.0",
47
+ "eslint-plugin-ts-fortress": "1.3.4",
48
+ "eslint-plugin-ts-type-forge": "0.8.9",
48
49
  "prettier": "3.9.9",
49
50
  "prettier-plugin-organize-imports": "4.3.0",
50
51
  "prettier-plugin-packagejson": "3.0.2",
@@ -66,15 +67,15 @@
66
67
  "build": "tsx --tsconfig ../../tools/configs/tsconfig.tsx.json ./scripts/cmd/build.mts",
67
68
  "check:dist:branded-number-types": "tsx --tsconfig ../../tools/configs/tsconfig.tsx.json ./scripts/cmd/check-branded-number-types.mts",
68
69
  "check:ext": "tsx --tsconfig ../../tools/configs/tsconfig.tsx.json ./scripts/cmd/check-ext.mts",
69
- "check:lint": "oxlint . && eslint .",
70
- "check:test": "pnpm run z:vitest run",
71
- "check:test:cov": "pnpm run z:vitest run --coverage",
72
- "check:types": "node ../../node_modules/typescript-native/bin/tsc --noEmit",
70
+ "check:lint": "node ../../tools/scripts/cmd/assert-deps-built.mts && oxlint . && eslint .",
71
+ "check:test": "node ../../tools/scripts/cmd/assert-deps-built.mts && pnpm run z:vitest run",
72
+ "check:test:cov": "node ../../tools/scripts/cmd/assert-deps-built.mts && pnpm run z:vitest run --coverage",
73
+ "check:types": "node ../../tools/scripts/cmd/assert-deps-built.mts && node ../../node_modules/typescript-native/bin/tsc --noEmit",
73
74
  "doc": "pnpm run gen:readme",
74
75
  "fix:fmt": "tsx --tsconfig ../../tools/configs/tsconfig.tsx.json ../../libs/ts-repo-utils/src/cmd/format-uncommitted.mts --cwd .",
75
76
  "fix:fmt:diff": "tsx --tsconfig ../../tools/configs/tsconfig.tsx.json ../../libs/ts-repo-utils/src/cmd/format-diff-from.mts origin/main --cwd .",
76
77
  "fix:fmt:full": "pnpm run z:prettier .",
77
- "fix:lint": "oxlint . --fix && eslint . --fix",
78
+ "fix:lint": "node ../../tools/scripts/cmd/assert-deps-built.mts && oxlint . --fix && eslint . --fix",
78
79
  "fmt": "pnpm run fix:fmt",
79
80
  "gen": "run-s gen:rule-types gen:readme",
80
81
  "gen:readme": "tsx --tsconfig ../../tools/configs/tsconfig.tsx.json ./scripts/cmd/embed-examples.mts",
package/src/plugin.mts CHANGED
@@ -13,6 +13,7 @@ const recommendedRules = {
13
13
  'ts-data-forge/prefer-canonical-array-slicing': 'error',
14
14
  'ts-data-forge/prefer-canonical-length-cast': 'error',
15
15
  'ts-data-forge/prefer-canonical-length-guard': 'error',
16
+ 'ts-data-forge/prefer-arr-generate': 'error',
16
17
  'ts-data-forge/prefer-arr-is-array': 'error',
17
18
  'ts-data-forge/prefer-arr-scan': 'error',
18
19
  'ts-data-forge/prefer-arr-sum': 'error',
@@ -72,6 +72,21 @@ namespace PreferCanonicalLengthCast {
72
72
  export type RuleEntry = Linter.StringSeverity;
73
73
  }
74
74
 
75
+ /**
76
+ * @description Replace the filter-and-map idiom `xs.flatMap((x) => (cond ? [f(x)] : []))` with a loop in `Arr.generate` from ts-data-forge.
77
+ *
78
+ * ```md
79
+ * | key | value |
80
+ * | :--------- | :--------- |
81
+ * | type | suggestion |
82
+ * | deprecated | false |
83
+ * | fixable | code |
84
+ * ```
85
+ */
86
+ namespace PreferArrGenerate {
87
+ export type RuleEntry = Linter.StringSeverity;
88
+ }
89
+
75
90
  /**
76
91
  * @description Replace `Array.isArray` with `Arr.isArray` from ts-data-forge.
77
92
  *
@@ -148,7 +163,7 @@ namespace PreferAsInt {
148
163
  }
149
164
 
150
165
  /**
151
- * @description Replace `typeof u === "object" && u !== null` with `isNonNullObject(u)` from ts-data-forge.
166
+ * @description Replace `typeof u === "object" && u !== null`, in either order and anywhere in an `&&` chain, with `isNonNullObject(u)` from ts-data-forge.
152
167
  *
153
168
  * ```md
154
169
  * | key | value |
@@ -314,6 +329,7 @@ export type EslintTsDataForgeRules = Readonly<{
314
329
  'ts-data-forge/prefer-canonical-array-slicing': PreferCanonicalArraySlicing.RuleEntry;
315
330
  'ts-data-forge/prefer-canonical-length-guard': PreferCanonicalLengthGuard.RuleEntry;
316
331
  'ts-data-forge/prefer-canonical-length-cast': PreferCanonicalLengthCast.RuleEntry;
332
+ 'ts-data-forge/prefer-arr-generate': PreferArrGenerate.RuleEntry;
317
333
  'ts-data-forge/prefer-arr-is-array': PreferArrIsArray.RuleEntry;
318
334
  'ts-data-forge/prefer-arr-scan': PreferArrScan.RuleEntry;
319
335
  'ts-data-forge/prefer-arr-sum': PreferArrSum.RuleEntry;
@@ -1,82 +1,22 @@
1
1
  import {
2
2
  AST_NODE_TYPES,
3
+ ASTUtils,
3
4
  type TSESLint,
4
5
  type TSESTree,
5
6
  } from '@typescript-eslint/utils';
7
+ import {
8
+ isTypeWrapper,
9
+ skipTypeWrappers,
10
+ typeWrapperLayers,
11
+ } from 'eslint-rule-forge';
6
12
  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
- ]);
13
+ import type * as ts from 'typescript';
58
14
 
59
15
  const isIntegerLiteral = (node: DeepReadonly<TSESTree.Expression>): boolean =>
60
16
  node.type === AST_NODE_TYPES.Literal &&
61
17
  typeof node.value === 'number' &&
62
18
  Number.isInteger(node.value);
63
19
 
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
20
  /**
81
21
  * Whether `node` is an array or a tuple at every layer of type wrappers (see
82
22
  * {@link typeWrapperLayers}): a cast does not make a string an array, and the
@@ -135,9 +75,9 @@ export const isIntegerLiteralOrConstant = (
135
75
  // eslint-disable-next-line total-functions/no-unsafe-type-assertion
136
76
  const scope = sourceCode.getScope(node as TSESTree.Node);
137
77
 
138
- const variable = findVariable(scope, node.name);
78
+ const variable = ASTUtils.findVariable(scope, node.name);
139
79
 
140
- if (variable === undefined) {
80
+ if (variable === null) {
141
81
  return false;
142
82
  }
143
83
 
@@ -205,7 +145,8 @@ type LengthComparison = Readonly<{
205
145
  * Parses a `<array>.length <op> <bound>` comparison (in either operand order)
206
146
  * into its array expression, integer-literal/`const` `bound`, and whether it is
207
147
  * a lower (`min`) or upper (`max`) length bound. Returns `undefined` for
208
- * anything else. The length and the bound are read through type wrappers; the
148
+ * anything else. The comparison, the length and the bound are read through
149
+ * type wrappers; the
209
150
  * `bound` returned is the literal or constant underneath, since a wrapper such
210
151
  * as `as number` would only widen the length a guard narrows to.
211
152
  *
@@ -214,10 +155,12 @@ type LengthComparison = Readonly<{
214
155
  */
215
156
  export const parseLengthComparison = (
216
157
  // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
217
- node: TSESTree.Expression,
158
+ wrapped: TSESTree.Expression,
218
159
  // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
219
160
  sourceCode: TSESLint.SourceCode,
220
161
  ): LengthComparison | undefined => {
162
+ const node = skipTypeWrappers(wrapped);
163
+
221
164
  if (
222
165
  node.type !== AST_NODE_TYPES.BinaryExpression ||
223
166
  (node.operator !== '>=' && node.operator !== '<=')
@@ -262,6 +205,8 @@ export const parseLengthComparison = (
262
205
  * together form the `Arr.isBoundedLengthArray` pattern handled by
263
206
  * `prefer-arr-is-bounded-length-array`. The single-bound rules skip such
264
207
  * operands so the bounded rule rewrites the whole `&&` instead of the parts.
208
+ * A comparison wrapped as a whole (`(xs.length >= 1 satisfies boolean) && …`)
209
+ * is still the operand.
265
210
  */
266
211
  export const isPartOfBoundedLengthCheck = (
267
212
  // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
@@ -269,10 +214,12 @@ export const isPartOfBoundedLengthCheck = (
269
214
  // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
270
215
  sourceCode: TSESLint.SourceCode,
271
216
  ): boolean => {
272
- const { parent } = node;
217
+ const operand = outermostTypeWrapper(node);
218
+
219
+ const { parent } = operand;
273
220
 
274
221
  if (
275
- parent.type !== AST_NODE_TYPES.LogicalExpression ||
222
+ parent?.type !== AST_NODE_TYPES.LogicalExpression ||
276
223
  parent.operator !== '&&'
277
224
  ) {
278
225
  return false;
@@ -284,7 +231,7 @@ export const isPartOfBoundedLengthCheck = (
284
231
  return false;
285
232
  }
286
233
 
287
- const sibling = parent.left === node ? parent.right : parent.left;
234
+ const sibling = parent.left === operand ? parent.right : parent.left;
288
235
 
289
236
  const other = parseLengthComparison(sibling, sourceCode);
290
237
 
@@ -298,6 +245,15 @@ export const isPartOfBoundedLengthCheck = (
298
245
  );
299
246
  };
300
247
 
248
+ /** The outermost of the type wrappers around `node`, or `node` itself. */
249
+ const outermostTypeWrapper = (
250
+ // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
251
+ node: TSESTree.Node,
252
+ ): TSESTree.Node =>
253
+ node.parent !== undefined && isTypeWrapper(node.parent)
254
+ ? outermostTypeWrapper(node.parent)
255
+ : node;
256
+
301
257
  type VariableDefinition = Readonly<{
302
258
  type: string;
303
259
  parent: TSESTree.VariableDeclaration;
@@ -310,15 +266,52 @@ const isVariableDefinition = (
310
266
  ): def is TSESLint.Scope.Definition & VariableDefinition =>
311
267
  (def.type as string) === 'Variable';
312
268
 
313
- const findVariable = (
269
+ /**
270
+ * Whether `node`, read through its type wrappers, is the identifier `name`:
271
+ * `Number`, `(Number as NumberConstructor)` and `Number!` are the same object.
272
+ */
273
+ export const isIdentifierNamed = (
314
274
  // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
315
- scope: TSESLint.Scope.Scope,
275
+ node: TSESTree.Node,
316
276
  name: string,
317
- ): TSESLint.Scope.Variable | undefined => {
318
- const variable = scope.set.get(name);
277
+ ): boolean => {
278
+ const unwrapped = skipTypeWrappers(node);
319
279
 
320
280
  return (
321
- variable ??
322
- (scope.upper === null ? undefined : findVariable(scope.upper, name))
281
+ unwrapped.type === AST_NODE_TYPES.Identifier && unwrapped.name === name
323
282
  );
324
283
  };
284
+
285
+ /**
286
+ * Whether `holds` is true of the type of every layer of type wrappers around
287
+ * `node` (see {@link typeWrapperLayers}), outermost to innermost; `false`
288
+ * without type information. A widening cast (`s as string | number`) is the
289
+ * author saying the value may be more than it is, and is respected; a
290
+ * narrowing one (`s!`, `v as unknown as string`) cannot vouch for what the
291
+ * value's own type does not, and the fix, which keeps the wrapper, has to
292
+ * type-check at every layer too.
293
+ */
294
+ export const typeHoldsAtEveryLayer = (
295
+ // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
296
+ sourceCode: TSESLint.SourceCode,
297
+ // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
298
+ node: TSESTree.Node,
299
+ // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
300
+ holds: (type: ts.Type, typeChecker: ts.TypeChecker) => boolean,
301
+ ): boolean => {
302
+ const services = sourceCode.parserServices;
303
+
304
+ const checker = services?.program?.getTypeChecker();
305
+
306
+ if (checker === undefined) {
307
+ return false;
308
+ }
309
+
310
+ return typeWrapperLayers(node).every((layer) => {
311
+ const tsNode = services?.esTreeNodeToTSNodeMap?.get(layer);
312
+
313
+ return (
314
+ tsNode !== undefined && holds(checker.getTypeAtLocation(tsNode), checker)
315
+ );
316
+ });
317
+ };
@@ -1 +1,13 @@
1
+ import * as ts from 'typescript';
2
+
1
3
  export const TS_DATA_FORGE_MODULE = 'ts-data-forge';
4
+
5
+ /** The type flags of every primitive type but `void`. */
6
+ export const primitiveTypeFlags =
7
+ ts.TypeFlags.StringLike |
8
+ ts.TypeFlags.NumberLike |
9
+ ts.TypeFlags.BigIntLike |
10
+ ts.TypeFlags.BooleanLike |
11
+ ts.TypeFlags.ESSymbolLike |
12
+ ts.TypeFlags.Null |
13
+ ts.TypeFlags.Undefined;
@@ -0,0 +1,204 @@
1
+ import {
2
+ AST_NODE_TYPES,
3
+ type TSESLint,
4
+ type TSESTree,
5
+ } from '@typescript-eslint/utils';
6
+ import * as ts from 'typescript';
7
+ import { primitiveTypeFlags } from './constants.mjs';
8
+
9
+ /**
10
+ * Whether the code around `node` visibly relies on the result being a mutable
11
+ * array, so that swapping in the readonly array `Arr.uniq` or `Arr.generate`
12
+ * returns would not compile. Looks at `node` itself and, when it initializes a variable without a
13
+ * type annotation, at every read of that variable.
14
+ */
15
+ export const needsMutableResult = (
16
+ // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
17
+ sourceCode: TSESLint.SourceCode,
18
+ // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
19
+ checker: ts.TypeChecker,
20
+ // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
21
+ getTsNode: (esNode: TSESTree.Node) => ts.Node | undefined,
22
+ // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
23
+ node: TSESTree.Node,
24
+ ): boolean => {
25
+ const outer = outermostTypePreservingWrapper(node);
26
+
27
+ if (isUsedAsMutable(checker, getTsNode, outer)) {
28
+ return true;
29
+ }
30
+
31
+ const parent = outer.parent;
32
+
33
+ if (
34
+ parent?.type !== AST_NODE_TYPES.VariableDeclarator ||
35
+ parent.init !== outer ||
36
+ parent.id.type !== AST_NODE_TYPES.Identifier ||
37
+ parent.id.typeAnnotation !== undefined
38
+ ) {
39
+ return false;
40
+ }
41
+
42
+ const [variable] = sourceCode.getDeclaredVariables(parent);
43
+
44
+ return (
45
+ variable?.references.some(
46
+ (reference) =>
47
+ reference.init !== true &&
48
+ isUsedAsMutable(
49
+ checker,
50
+ getTsNode,
51
+ outermostTypePreservingWrapper(reference.identifier),
52
+ ),
53
+ ) ?? false
54
+ );
55
+ };
56
+
57
+ /**
58
+ * `node`, or the outermost `!` / `satisfies` around it. Neither changes the
59
+ * type the value is used at, so `Array.from(new Set(xs))!.push(4)` and
60
+ * `ys!.push(4)` still need a mutable array. An `as` does change it, and a
61
+ * readonly array may be cast to a mutable one, so the walk stops there.
62
+ */
63
+ const outermostTypePreservingWrapper = (
64
+ // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
65
+ node: TSESTree.Node,
66
+ ): TSESTree.Node => {
67
+ const { parent } = node;
68
+
69
+ return parent !== undefined &&
70
+ (parent.type === AST_NODE_TYPES.TSNonNullExpression ||
71
+ parent.type === AST_NODE_TYPES.TSSatisfiesExpression) &&
72
+ parent.expression === node
73
+ ? outermostTypePreservingWrapper(parent)
74
+ : node;
75
+ };
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
+ /**
134
+ * The declared type of the parameter `argument` is passed to. The contextual
135
+ * type of an argument to a generic function is instantiated from that very
136
+ * argument — `Arr.toSorted(Array.from(set), cmp)` has `Ar` inferred as
137
+ * `string[]` — so it would read as "a mutable array is required" for any
138
+ * generic parameter. The declared type (`Ar extends readonly unknown[]`) is
139
+ * what the rewritten argument actually has to satisfy.
140
+ */
141
+ const getDeclaredParameterType = (
142
+ // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
143
+ checker: ts.TypeChecker,
144
+ // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
145
+ argument: ts.Expression,
146
+ ): ts.Type | undefined => {
147
+ const call = argument.parent;
148
+
149
+ if (!ts.isCallExpression(call) && !ts.isNewExpression(call)) {
150
+ return undefined;
151
+ }
152
+
153
+ const index = call.arguments?.indexOf(argument) ?? -1;
154
+
155
+ if (index === -1) {
156
+ return undefined;
157
+ }
158
+
159
+ const declaration = checker.getResolvedSignature(call)?.getDeclaration();
160
+
161
+ const parameter = declaration?.parameters[index];
162
+
163
+ // A rest parameter's declared type is the array of arguments, not the type of
164
+ // one; the contextual type is the better answer there.
165
+ return parameter === undefined || parameter.dotDotDotToken !== undefined
166
+ ? undefined
167
+ : checker.getTypeAtLocation(parameter);
168
+ };
169
+
170
+ /**
171
+ * Whether a readonly array cannot be assigned to `type`: it is a mutable array,
172
+ * or a union of mutable arrays and primitives. (A tuple accepted no array to
173
+ * begin with, so it does not matter here.)
174
+ */
175
+ const isMutableArrayOnly = (
176
+ // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
177
+ checker: ts.TypeChecker,
178
+ // eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
179
+ type: ts.Type,
180
+ ): boolean => {
181
+ const members = type.isUnion() ? type.types : ([type] as const);
182
+
183
+ return (
184
+ members.every(
185
+ (t) =>
186
+ // `null`, `undefined` and other primitives accept no array at all.
187
+ (t.flags & primitiveTypeFlags) !== 0 ||
188
+ (checker.isArrayType(t) && t.getSymbol()?.getName() === 'Array'),
189
+ ) && members.some((t) => (t.flags & primitiveTypeFlags) === 0)
190
+ );
191
+ };
192
+
193
+ /** Methods that `ReadonlyArray` lacks and that mutate the receiver. */
194
+ const mutatingMethods: ReadonlySet<string> = new Set([
195
+ 'copyWithin',
196
+ 'fill',
197
+ 'pop',
198
+ 'push',
199
+ 'reverse',
200
+ 'shift',
201
+ 'sort',
202
+ 'splice',
203
+ 'unshift',
204
+ ]);
@@ -4,15 +4,16 @@ import {
4
4
  type TSESLint,
5
5
  type TSESTree,
6
6
  } from '@typescript-eslint/utils';
7
- import type { DeepReadonly, ReadonlyRecord } from 'ts-type-forge';
8
- import * as ts from 'typescript';
9
- import { typeWrapperLayers } from './ast-utils.mjs';
10
7
  import {
11
8
  buildCalleeResolver,
12
- buildImportFixes,
9
+ buildNamedImportFix,
10
+ getImportDeclarations,
13
11
  getImportedLocalName,
14
- getTsDataForgeImport,
15
- } from './import-utils.mjs';
12
+ typeWrapperLayers,
13
+ } from 'eslint-rule-forge';
14
+ import type { DeepReadonly, ReadonlyRecord } from 'ts-type-forge';
15
+ import * as ts from 'typescript';
16
+ import { TS_DATA_FORGE_MODULE } from './constants.mjs';
16
17
 
17
18
  type Options = readonly [
18
19
  Readonly<{
@@ -169,9 +170,12 @@ export const noUnnecessaryTypeGuard: TSESLint.RuleModule<MessageIds, Options> =
169
170
 
170
171
  const ignored = new Set(options?.ignore);
171
172
 
172
- const tsDataForgeImport = getTsDataForgeImport(program);
173
+ const tsDataForgeImports = getImportDeclarations(
174
+ program,
175
+ TS_DATA_FORGE_MODULE,
176
+ );
173
177
 
174
- const resolveGuard = buildCalleeResolver(tsDataForgeImport);
178
+ const resolveGuard = buildCalleeResolver(tsDataForgeImports);
175
179
 
176
180
  const buildReplaceFix =
177
181
  (
@@ -191,7 +195,7 @@ export const noUnnecessaryTypeGuard: TSESLint.RuleModule<MessageIds, Options> =
191
195
  // alias), rename to its in-scope local name. Otherwise add a new
192
196
  // import and rename to the canonical name.
193
197
  const localName = getImportedLocalName(
194
- tsDataForgeImport,
198
+ tsDataForgeImports,
195
199
  replacement,
196
200
  );
197
201
 
@@ -200,7 +204,7 @@ export const noUnnecessaryTypeGuard: TSESLint.RuleModule<MessageIds, Options> =
200
204
  }
201
205
 
202
206
  return [
203
- ...buildImportFixes(fixer, program, tsDataForgeImport, [
207
+ ...buildNamedImportFix(fixer, program, TS_DATA_FORGE_MODULE, [
204
208
  replacement,
205
209
  ]),
206
210
  fixer.replaceText(propertyNode, replacement),