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.
- package/README.md +20 -19
- package/dist/plugin.d.mts +2 -0
- 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 +16 -1
- package/dist/rule-types.d.mts.map +1 -1
- package/dist/rule-types.mjs.map +1 -1
- package/dist/rules/ast-utils.d.mts +21 -34
- package/dist/rules/ast-utils.d.mts.map +1 -1
- package/dist/rules/ast-utils.mjs +53 -58
- package/dist/rules/ast-utils.mjs.map +1 -1
- package/dist/rules/constants.d.mts +2 -0
- package/dist/rules/constants.d.mts.map +1 -1
- package/dist/rules/constants.mjs +9 -0
- package/dist/rules/constants.mjs.map +1 -1
- package/dist/rules/mutable-result.d.mts +10 -0
- package/dist/rules/mutable-result.d.mts.map +1 -0
- package/dist/rules/mutable-result.mjs +145 -0
- package/dist/rules/mutable-result.mjs.map +1 -0
- package/dist/rules/no-unnecessary-type-guard.d.mts.map +1 -1
- package/dist/rules/no-unnecessary-type-guard.mjs +6 -6
- package/dist/rules/no-unnecessary-type-guard.mjs.map +1 -1
- package/dist/rules/prefer-arr-generate.d.mts +57 -0
- package/dist/rules/prefer-arr-generate.d.mts.map +1 -0
- package/dist/rules/prefer-arr-generate.mjs +588 -0
- package/dist/rules/prefer-arr-generate.mjs.map +1 -0
- package/dist/rules/prefer-arr-is-array.d.mts.map +1 -1
- package/dist/rules/prefer-arr-is-array.mjs +7 -5
- package/dist/rules/prefer-arr-is-array.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 +9 -6
- 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 +9 -6
- 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 +9 -6
- 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 +9 -6
- 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 -6
- 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 +8 -11
- 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 +8 -6
- 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 +12 -166
- 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 +6 -8
- 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 +57 -34
- 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 +3 -3
- 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 +4 -4
- 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 +3 -2
- 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 +113 -43
- 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 +105 -48
- package/dist/rules/prefer-is-record-and-has-key.mjs.map +1 -1
- package/dist/rules/prefer-num-safe-parse-float.d.mts.map +1 -1
- package/dist/rules/prefer-num-safe-parse-float.mjs +6 -6
- 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 +6 -6
- 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 +9 -12
- 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 +104 -23
- package/dist/rules/prefer-range-for-loop.mjs.map +1 -1
- package/dist/rules/rules.d.mts +1 -0
- package/dist/rules/rules.d.mts.map +1 -1
- package/dist/rules/rules.mjs +2 -0
- package/dist/rules/rules.mjs.map +1 -1
- package/package.json +11 -10
- package/src/plugin.mts +1 -0
- package/src/rule-types.mts +17 -1
- package/src/rules/ast-utils.mts +73 -80
- package/src/rules/constants.mts +12 -0
- package/src/rules/mutable-result.mts +204 -0
- package/src/rules/no-unnecessary-type-guard.mts +14 -10
- package/src/rules/prefer-arr-generate.mts +918 -0
- package/src/rules/prefer-arr-generate.test.mts +967 -0
- package/src/rules/prefer-arr-is-array.mts +14 -7
- package/src/rules/prefer-arr-is-array.test.mts +14 -0
- package/src/rules/prefer-arr-is-bounded-length-array.mts +16 -10
- package/src/rules/prefer-arr-is-bounded-length-array.test.mts +19 -0
- package/src/rules/prefer-arr-is-fixed-length-array.mts +17 -11
- package/src/rules/prefer-arr-is-max-length-array.mts +17 -11
- package/src/rules/prefer-arr-is-max-length-array.test.mts +8 -0
- package/src/rules/prefer-arr-is-min-length-array.mts +17 -11
- package/src/rules/prefer-arr-is-min-length-array.test.mts +8 -0
- package/src/rules/prefer-arr-is-non-empty.mts +16 -13
- package/src/rules/prefer-arr-scan.mts +24 -19
- package/src/rules/prefer-arr-sum.mts +18 -14
- package/src/rules/prefer-arr-uniq.mts +25 -234
- package/src/rules/prefer-as-int.mts +19 -12
- package/src/rules/prefer-canonical-array-slicing.mts +79 -52
- package/src/rules/prefer-canonical-array-slicing.test.mts +55 -0
- package/src/rules/prefer-canonical-length-cast.mts +7 -3
- package/src/rules/prefer-canonical-length-guard.mts +9 -4
- package/src/rules/prefer-comparison-over-nullish-guard.mts +3 -2
- package/src/rules/prefer-comparison-over-nullish-guard.test.mts +16 -0
- package/src/rules/prefer-is-non-null-object.mts +171 -64
- package/src/rules/prefer-is-non-null-object.test.mts +138 -0
- package/src/rules/prefer-is-record-and-has-key.mts +159 -74
- package/src/rules/prefer-is-record-and-has-key.test.mts +118 -23
- package/src/rules/prefer-num-safe-parse-float.mts +15 -10
- package/src/rules/prefer-num-safe-parse-int.mts +15 -13
- package/src/rules/prefer-obj-over-entries-round-trip.mts +19 -22
- package/src/rules/prefer-range-for-loop.mts +157 -31
- package/src/rules/prefer-range-for-loop.test.mts +127 -5
- package/src/rules/rules.mts +2 -0
- package/dist/rules/import-utils.d.mts +0 -30
- package/dist/rules/import-utils.d.mts.map +0 -1
- package/dist/rules/import-utils.mjs +0 -98
- package/dist/rules/import-utils.mjs.map +0 -1
- 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.
|
|
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
|
-
"
|
|
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.
|
|
46
|
-
"eslint-plugin-ts-fortress": "1.3.
|
|
47
|
-
"eslint-plugin-ts-type-forge": "0.8.
|
|
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',
|
package/src/rule-types.mts
CHANGED
|
@@ -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
|
|
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;
|
package/src/rules/ast-utils.mts
CHANGED
|
@@ -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 ===
|
|
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
|
|
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
|
-
|
|
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
|
|
217
|
+
const operand = outermostTypeWrapper(node);
|
|
218
|
+
|
|
219
|
+
const { parent } = operand;
|
|
273
220
|
|
|
274
221
|
if (
|
|
275
|
-
parent
|
|
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 ===
|
|
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
|
-
|
|
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
|
-
|
|
275
|
+
node: TSESTree.Node,
|
|
316
276
|
name: string,
|
|
317
|
-
):
|
|
318
|
-
const
|
|
277
|
+
): boolean => {
|
|
278
|
+
const unwrapped = skipTypeWrappers(node);
|
|
319
279
|
|
|
320
280
|
return (
|
|
321
|
-
|
|
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
|
+
};
|
package/src/rules/constants.mts
CHANGED
|
@@ -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
|
-
|
|
9
|
+
buildNamedImportFix,
|
|
10
|
+
getImportDeclarations,
|
|
13
11
|
getImportedLocalName,
|
|
14
|
-
|
|
15
|
-
} from '
|
|
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
|
|
173
|
+
const tsDataForgeImports = getImportDeclarations(
|
|
174
|
+
program,
|
|
175
|
+
TS_DATA_FORGE_MODULE,
|
|
176
|
+
);
|
|
173
177
|
|
|
174
|
-
const resolveGuard = buildCalleeResolver(
|
|
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
|
-
|
|
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
|
-
...
|
|
207
|
+
...buildNamedImportFix(fixer, program, TS_DATA_FORGE_MODULE, [
|
|
204
208
|
replacement,
|
|
205
209
|
]),
|
|
206
210
|
fixer.replaceText(propertyNode, replacement),
|