eslint-plugin-ts-std-forge 0.1.7 → 0.1.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/rule-types.d.mts +2 -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 +8 -31
- package/dist/rules/ast-utils.d.mts.map +1 -1
- package/dist/rules/ast-utils.mjs +13 -75
- package/dist/rules/ast-utils.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 +7 -5
- 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 +9 -13
- package/dist/rules/prefer-is-record-and-has-key.mjs.map +1 -1
- package/dist/rules/prefer-safe-array-is-array.d.mts.map +1 -1
- package/dist/rules/prefer-safe-array-is-array.mjs +8 -5
- package/dist/rules/prefer-safe-array-is-array.mjs.map +1 -1
- package/dist/rules/prefer-safe-array-length-guard.d.mts.map +1 -1
- package/dist/rules/prefer-safe-array-length-guard.mjs +11 -14
- package/dist/rules/prefer-safe-array-length-guard.mjs.map +1 -1
- package/dist/rules/prefer-safe-number-parse-integer.d.mts.map +1 -1
- package/dist/rules/prefer-safe-number-parse-integer.mjs +9 -7
- package/dist/rules/prefer-safe-number-parse-integer.mjs.map +1 -1
- package/dist/rules/prefer-safe-number-parse.d.mts.map +1 -1
- package/dist/rules/prefer-safe-number-parse.mjs +9 -7
- package/dist/rules/prefer-safe-number-parse.mjs.map +1 -1
- package/package.json +5 -4
- package/src/rule-types.mts +2 -1
- package/src/rules/ast-utils.mts +20 -110
- package/src/rules/prefer-is-non-null-object.mts +13 -7
- package/src/rules/prefer-is-record-and-has-key.mts +28 -27
- package/src/rules/prefer-is-record-and-has-key.test.mts +30 -3
- package/src/rules/prefer-safe-array-is-array.mts +14 -10
- package/src/rules/prefer-safe-array-is-array.test.mts +14 -0
- package/src/rules/prefer-safe-array-length-guard.mts +25 -26
- package/src/rules/prefer-safe-array-length-guard.test.mts +19 -5
- package/src/rules/prefer-safe-number-parse-integer.mts +25 -19
- package/src/rules/prefer-safe-number-parse-integer.test.mts +19 -5
- package/src/rules/prefer-safe-number-parse.mts +25 -19
- package/src/rules/prefer-safe-number-parse.test.mts +19 -5
- package/dist/rules/import-utils.d.mts +0 -6
- package/dist/rules/import-utils.d.mts.map +0 -1
- package/dist/rules/import-utils.mjs +0 -28
- package/dist/rules/import-utils.mjs.map +0 -1
- package/src/rules/import-utils.mts +0 -58
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { AST_NODE_TYPES, } from '@typescript-eslint/utils';
|
|
2
|
+
import { buildNamedImportFix, getArgumentText, getImportDeclarations, getNamedImports, skipTypeWrappers, } from 'eslint-rule-forge';
|
|
2
3
|
import * as ts from 'typescript';
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
4
|
+
import { isIdentifierNamed, typeHoldsAtEveryLayer } from './ast-utils.mjs';
|
|
5
|
+
import { TS_STD_FORGE_MODULE } from './constants.mjs';
|
|
5
6
|
|
|
6
7
|
|
|
7
8
|
|
|
@@ -42,7 +43,7 @@ export const preferSafeNumberParse = {
|
|
|
42
43
|
create: (context) => {
|
|
43
44
|
const sourceCode = context.sourceCode;
|
|
44
45
|
const program = sourceCode.ast;
|
|
45
|
-
const
|
|
46
|
+
const tsStdForgeImports = getImportDeclarations(program, TS_STD_FORGE_MODULE);
|
|
46
47
|
const mut_nodesToFix = [];
|
|
47
48
|
return {
|
|
48
49
|
CallExpression: (node) => {
|
|
@@ -74,14 +75,15 @@ export const preferSafeNumberParse = {
|
|
|
74
75
|
|
|
75
76
|
|
|
76
77
|
|
|
77
|
-
|
|
78
|
-
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
if (!typeHoldsAtEveryLayer(sourceCode, firstArg, isStringType)) {
|
|
79
81
|
return;
|
|
80
82
|
}
|
|
81
83
|
mut_nodesToFix.push({ node, argExpression: firstArg });
|
|
82
84
|
},
|
|
83
85
|
'Program:exit': () => {
|
|
84
|
-
const namedImports = getNamedImports(
|
|
86
|
+
const namedImports = getNamedImports(tsStdForgeImports);
|
|
85
87
|
const missingImports = ['Result', 'SafeNumber'].filter((name) => !namedImports.includes(name));
|
|
86
88
|
for (const [index, { node, argExpression },] of mut_nodesToFix.entries()) {
|
|
87
89
|
const argText = getArgumentText(sourceCode, argExpression);
|
|
@@ -95,7 +97,7 @@ export const preferSafeNumberParse = {
|
|
|
95
97
|
fix: (fixer) => {
|
|
96
98
|
const core = `Result.unwrapOkOr(SafeNumber.parse(${argText}), Number.NaN)`;
|
|
97
99
|
const importFixes = index === 0 && missingImports.length > 0
|
|
98
|
-
?
|
|
100
|
+
? buildNamedImportFix(fixer, program, TS_STD_FORGE_MODULE, missingImports)
|
|
99
101
|
: [];
|
|
100
102
|
return [...importFixes, fixer.replaceText(node, core)];
|
|
101
103
|
},
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"prefer-safe-number-parse.mjs","sourceRoot":"","sources":["../../src/rules/prefer-safe-number-parse.mts"],"names":[],"mappings":"AAAA,OAAO,EACL,cAAc,GAGf,MAAM,0BAA0B,CAAC;AAClC,OAAO,
|
|
1
|
+
{"version":3,"file":"prefer-safe-number-parse.mjs","sourceRoot":"","sources":["../../src/rules/prefer-safe-number-parse.mts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "eslint-plugin-ts-std-forge",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.9",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "ESLint rules that steer code toward ts-std-forge idioms.",
|
|
6
6
|
"keywords": [
|
|
@@ -35,6 +35,7 @@
|
|
|
35
35
|
],
|
|
36
36
|
"dependencies": {
|
|
37
37
|
"@typescript-eslint/utils": "^8.71.1",
|
|
38
|
+
"eslint-rule-forge": "^0.1.0",
|
|
38
39
|
"ts-std-forge": "0.5.1",
|
|
39
40
|
"ts-type-forge": "~9.2.2"
|
|
40
41
|
},
|
|
@@ -42,9 +43,9 @@
|
|
|
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.18.0",
|
|
47
|
+
"eslint-plugin-ts-fortress": "1.3.3",
|
|
48
|
+
"eslint-plugin-ts-type-forge": "0.8.8",
|
|
48
49
|
"prettier": "3.9.9",
|
|
49
50
|
"prettier-plugin-organize-imports": "4.3.0",
|
|
50
51
|
"prettier-plugin-packagejson": "3.0.2",
|
package/src/rule-types.mts
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
import type { Linter } from 'eslint';
|
|
7
7
|
import { expectType } from 'ts-std-forge';
|
|
8
|
+
import type { ReadonlyRecord } from 'ts-type-forge';
|
|
8
9
|
import type { tsStdForgeRules } from './rules/index.mjs';
|
|
9
10
|
|
|
10
11
|
/**
|
|
@@ -106,7 +107,7 @@ export type EslintTsStdForgeRules = Readonly<{
|
|
|
106
107
|
'ts-std-forge/prefer-safe-number-parse-integer': PreferSafeNumberParseInteger.RuleEntry;
|
|
107
108
|
}>;
|
|
108
109
|
|
|
109
|
-
export type EslintTsStdForgeRulesOption =
|
|
110
|
+
export type EslintTsStdForgeRulesOption = ReadonlyRecord<never, never>;
|
|
110
111
|
|
|
111
112
|
// If this assertion fails to type-check, this generated file has drifted from
|
|
112
113
|
// the actual plugin rules — re-run `pnpm run gen:rule-types`.
|
package/src/rules/ast-utils.mts
CHANGED
|
@@ -1,33 +1,11 @@
|
|
|
1
1
|
import {
|
|
2
2
|
AST_NODE_TYPES,
|
|
3
|
-
ASTUtils,
|
|
4
3
|
type TSESLint,
|
|
5
4
|
type TSESTree,
|
|
6
5
|
} from '@typescript-eslint/utils';
|
|
6
|
+
import { skipTypeWrappers, typeWrapperLayers } from 'eslint-rule-forge';
|
|
7
7
|
import type * as ts from 'typescript';
|
|
8
8
|
|
|
9
|
-
/**
|
|
10
|
-
* `node` with every `as`, `satisfies`, `!` and `<T>` around it taken off: the
|
|
11
|
-
* expression whose value it is. Judge syntax and ask types on what this
|
|
12
|
-
* returns, so that a wrapper neither hides a pattern (`xs.length > (0 as
|
|
13
|
-
* const)`) nor vouches for a type (`Number(b as unknown as string)`, whose
|
|
14
|
-
* value is still a boolean), and keep the original node for fix text. See
|
|
15
|
-
* `docs/writing-lint-rules.md` at the repository root.
|
|
16
|
-
*
|
|
17
|
-
* A copy of the helper in `eslint-config-typed`, which this package, published
|
|
18
|
-
* on its own, cannot import.
|
|
19
|
-
*/
|
|
20
|
-
export const skipTypeWrappers = (
|
|
21
|
-
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
22
|
-
node: TSESTree.Node,
|
|
23
|
-
): TSESTree.Node =>
|
|
24
|
-
isTypeWrapper(node) ? skipTypeWrappers(node.expression) : node;
|
|
25
|
-
|
|
26
|
-
export const isTypeWrapper = (
|
|
27
|
-
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
28
|
-
node: TSESTree.Node,
|
|
29
|
-
): node is TypeWrapper => TYPE_WRAPPERS.has(node.type);
|
|
30
|
-
|
|
31
9
|
/**
|
|
32
10
|
* Whether `node`, read through its type wrappers, is the identifier `name`:
|
|
33
11
|
* `Number`, `(Number as NumberConstructor)` and `Number!` are the same object.
|
|
@@ -44,104 +22,36 @@ export const isIdentifierNamed = (
|
|
|
44
22
|
);
|
|
45
23
|
};
|
|
46
24
|
|
|
47
|
-
export type TypeWrapper =
|
|
48
|
-
| TSESTree.TSAsExpression
|
|
49
|
-
| TSESTree.TSNonNullExpression
|
|
50
|
-
| TSESTree.TSSatisfiesExpression
|
|
51
|
-
| TSESTree.TSTypeAssertion;
|
|
52
|
-
|
|
53
25
|
/**
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
* information. A
|
|
57
|
-
*
|
|
26
|
+
* Whether `holds` is true of the type of every layer of type wrappers around
|
|
27
|
+
* `node` (see {@link typeWrapperLayers}), outermost to innermost; `false`
|
|
28
|
+
* without type information. A widening cast (`s as string | number`) is the
|
|
29
|
+
* author saying the value may be more than it is, and is respected; a
|
|
30
|
+
* narrowing one (`s!`, `v as unknown as string`) cannot vouch for what the
|
|
31
|
+
* value's own type does not, and the fix, which keeps the wrapper, has to
|
|
32
|
+
* type-check at every layer too.
|
|
58
33
|
*/
|
|
59
|
-
export const
|
|
34
|
+
export const typeHoldsAtEveryLayer = (
|
|
60
35
|
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
61
36
|
sourceCode: TSESLint.SourceCode,
|
|
62
37
|
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
63
38
|
node: TSESTree.Node,
|
|
64
|
-
|
|
39
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
40
|
+
holds: (type: ts.Type, typeChecker: ts.TypeChecker) => boolean,
|
|
41
|
+
): boolean => {
|
|
65
42
|
const services = sourceCode.parserServices;
|
|
66
43
|
|
|
67
44
|
const checker = services?.program?.getTypeChecker();
|
|
68
45
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
if (checker === undefined || tsNode === undefined) {
|
|
72
|
-
return undefined;
|
|
46
|
+
if (checker === undefined) {
|
|
47
|
+
return false;
|
|
73
48
|
}
|
|
74
49
|
|
|
75
|
-
|
|
50
|
+
return typeWrapperLayers(node).every((layer) => {
|
|
51
|
+
const tsNode = services?.esTreeNodeToTSNodeMap?.get(layer);
|
|
76
52
|
|
|
77
|
-
|
|
53
|
+
return (
|
|
54
|
+
tsNode !== undefined && holds(checker.getTypeAtLocation(tsNode), checker)
|
|
55
|
+
);
|
|
56
|
+
});
|
|
78
57
|
};
|
|
79
|
-
|
|
80
|
-
/**
|
|
81
|
-
* The source text of `node` to put in a call's argument list. `getText`
|
|
82
|
-
* leaves out the parentheses the source puts around a node, and a sequence
|
|
83
|
-
* `(a, b)` without them would be read as two arguments.
|
|
84
|
-
*/
|
|
85
|
-
export const getArgumentText = (
|
|
86
|
-
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
87
|
-
sourceCode: TSESLint.SourceCode,
|
|
88
|
-
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
89
|
-
node: TSESTree.Node,
|
|
90
|
-
): string => {
|
|
91
|
-
const text = sourceCode.getText(node);
|
|
92
|
-
|
|
93
|
-
return node.type === AST_NODE_TYPES.SequenceExpression ? `(${text})` : text;
|
|
94
|
-
};
|
|
95
|
-
|
|
96
|
-
/**
|
|
97
|
-
* The range of `node` together with the parentheses the source puts around it,
|
|
98
|
-
* which `node.range` leaves out: all of `((Array.isArray))` for the member
|
|
99
|
-
* expression inside.
|
|
100
|
-
*/
|
|
101
|
-
export const getRangeWithParens = (
|
|
102
|
-
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
103
|
-
sourceCode: TSESLint.SourceCode,
|
|
104
|
-
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
105
|
-
node: TSESTree.Node,
|
|
106
|
-
): TSESTree.Range => {
|
|
107
|
-
const depth = parenthesesDepth(sourceCode, node);
|
|
108
|
-
|
|
109
|
-
if (depth === 0) {
|
|
110
|
-
return node.range;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
const opening = sourceCode.getTokenBefore(node, { skip: depth - 1 });
|
|
114
|
-
|
|
115
|
-
const closing = sourceCode.getTokenAfter(node, { skip: depth - 1 });
|
|
116
|
-
|
|
117
|
-
return opening === null || closing === null
|
|
118
|
-
? node.range
|
|
119
|
-
: [opening.range[0], closing.range[1]];
|
|
120
|
-
};
|
|
121
|
-
|
|
122
|
-
/** How many pairs of parentheses the source puts around `node`. */
|
|
123
|
-
const parenthesesDepth = (
|
|
124
|
-
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
125
|
-
sourceCode: TSESLint.SourceCode,
|
|
126
|
-
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
127
|
-
node: TSESTree.Node,
|
|
128
|
-
depth: number = 0,
|
|
129
|
-
): number =>
|
|
130
|
-
ASTUtils.isParenthesized(depth + 1, node, sourceCode)
|
|
131
|
-
? parenthesesDepth(sourceCode, node, depth + 1)
|
|
132
|
-
: depth;
|
|
133
|
-
|
|
134
|
-
const hasNonNullAssertion = (
|
|
135
|
-
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
136
|
-
node: TSESTree.Node,
|
|
137
|
-
): boolean =>
|
|
138
|
-
isTypeWrapper(node) &&
|
|
139
|
-
(node.type === AST_NODE_TYPES.TSNonNullExpression ||
|
|
140
|
-
hasNonNullAssertion(node.expression));
|
|
141
|
-
|
|
142
|
-
const TYPE_WRAPPERS: ReadonlySet<AST_NODE_TYPES> = new Set([
|
|
143
|
-
AST_NODE_TYPES.TSAsExpression,
|
|
144
|
-
AST_NODE_TYPES.TSNonNullExpression,
|
|
145
|
-
AST_NODE_TYPES.TSSatisfiesExpression,
|
|
146
|
-
AST_NODE_TYPES.TSTypeAssertion,
|
|
147
|
-
]);
|
|
@@ -3,12 +3,13 @@ import {
|
|
|
3
3
|
type TSESLint,
|
|
4
4
|
type TSESTree,
|
|
5
5
|
} from '@typescript-eslint/utils';
|
|
6
|
-
import { skipTypeWrappers } from './ast-utils.mjs';
|
|
7
6
|
import {
|
|
8
|
-
|
|
7
|
+
buildNamedImportFix,
|
|
8
|
+
getImportDeclarations,
|
|
9
9
|
getNamedImports,
|
|
10
|
-
|
|
11
|
-
} from '
|
|
10
|
+
skipTypeWrappers,
|
|
11
|
+
} from 'eslint-rule-forge';
|
|
12
|
+
import { TS_STD_FORGE_MODULE } from './constants.mjs';
|
|
12
13
|
|
|
13
14
|
type Options = readonly [];
|
|
14
15
|
|
|
@@ -34,7 +35,10 @@ export const preferIsNonNullObject: TSESLint.RuleModule<MessageIds, Options> = {
|
|
|
34
35
|
|
|
35
36
|
const program = sourceCode.ast;
|
|
36
37
|
|
|
37
|
-
const
|
|
38
|
+
const tsStdForgeImports = getImportDeclarations(
|
|
39
|
+
program,
|
|
40
|
+
TS_STD_FORGE_MODULE,
|
|
41
|
+
);
|
|
38
42
|
|
|
39
43
|
const mut_nodesToFix: {
|
|
40
44
|
node: TSESTree.LogicalExpression;
|
|
@@ -53,7 +57,7 @@ export const preferIsNonNullObject: TSESLint.RuleModule<MessageIds, Options> = {
|
|
|
53
57
|
},
|
|
54
58
|
'Program:exit': () => {
|
|
55
59
|
const hasIsNonNullObjectImport =
|
|
56
|
-
getNamedImports(
|
|
60
|
+
getNamedImports(tsStdForgeImports).includes('isNonNullObject');
|
|
57
61
|
|
|
58
62
|
// The import fix lives on the first report only:
|
|
59
63
|
// `insertTextBefore(program, …)` produces overlapping ranges
|
|
@@ -72,7 +76,9 @@ export const preferIsNonNullObject: TSESLint.RuleModule<MessageIds, Options> = {
|
|
|
72
76
|
|
|
73
77
|
const importFixes =
|
|
74
78
|
index === 0 && !hasIsNonNullObjectImport
|
|
75
|
-
?
|
|
79
|
+
? buildNamedImportFix(fixer, program, TS_STD_FORGE_MODULE, [
|
|
80
|
+
'isNonNullObject',
|
|
81
|
+
])
|
|
76
82
|
: [];
|
|
77
83
|
|
|
78
84
|
return [...importFixes, fixer.replaceText(node, replacement)];
|
|
@@ -3,19 +3,17 @@ import {
|
|
|
3
3
|
type TSESLint,
|
|
4
4
|
type TSESTree,
|
|
5
5
|
} from '@typescript-eslint/utils';
|
|
6
|
-
import { SafeArray } from 'ts-std-forge';
|
|
7
|
-
import * as ts from 'typescript';
|
|
8
6
|
import {
|
|
7
|
+
buildNamedImportFix,
|
|
9
8
|
getArgumentText,
|
|
10
|
-
|
|
11
|
-
isIdentifierNamed,
|
|
12
|
-
skipTypeWrappers,
|
|
13
|
-
} from './ast-utils.mjs';
|
|
14
|
-
import {
|
|
15
|
-
buildImportFixes,
|
|
9
|
+
getImportDeclarations,
|
|
16
10
|
getNamedImports,
|
|
17
|
-
|
|
18
|
-
} from '
|
|
11
|
+
skipTypeWrappers,
|
|
12
|
+
} from 'eslint-rule-forge';
|
|
13
|
+
import { SafeArray } from 'ts-std-forge';
|
|
14
|
+
import * as ts from 'typescript';
|
|
15
|
+
import { isIdentifierNamed, typeHoldsAtEveryLayer } from './ast-utils.mjs';
|
|
16
|
+
import { TS_STD_FORGE_MODULE } from './constants.mjs';
|
|
19
17
|
|
|
20
18
|
type Options = readonly [];
|
|
21
19
|
|
|
@@ -44,29 +42,27 @@ export const preferIsRecordAndHasKey: TSESLint.RuleModule<MessageIds, Options> =
|
|
|
44
42
|
|
|
45
43
|
const program = sourceCode.ast;
|
|
46
44
|
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
|
|
45
|
+
const tsStdForgeImports = getImportDeclarations(
|
|
46
|
+
program,
|
|
47
|
+
TS_STD_FORGE_MODULE,
|
|
48
|
+
);
|
|
50
49
|
|
|
51
50
|
/**
|
|
52
51
|
* Whether the `isRecord(...)` half of the rewrite would be dead code
|
|
53
52
|
* because the object already satisfies `hasKey`'s
|
|
54
|
-
* `R extends UnknownRecord` constraint
|
|
55
|
-
*
|
|
56
|
-
*
|
|
53
|
+
* `R extends UnknownRecord` constraint, at every layer of type
|
|
54
|
+
* wrappers: an array cast to a record is still rejected by `isRecord`,
|
|
55
|
+
* and a record widened to one that may be an array is the author saying
|
|
56
|
+
* it may need the guard. Without type information nothing is known, so
|
|
57
|
+
* the guard is kept.
|
|
57
58
|
*/
|
|
58
59
|
const isKnownRecord = (
|
|
59
60
|
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
60
61
|
expression: TSESTree.Expression,
|
|
61
|
-
): boolean =>
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
const type = getValueType(sourceCode, expression);
|
|
67
|
-
|
|
68
|
-
return type !== undefined && isAlreadyRecordType(checker, type);
|
|
69
|
-
};
|
|
62
|
+
): boolean =>
|
|
63
|
+
typeHoldsAtEveryLayer(sourceCode, expression, (type, checker) =>
|
|
64
|
+
isAlreadyRecordType(checker, type),
|
|
65
|
+
);
|
|
70
66
|
|
|
71
67
|
const mut_nodesToFix: {
|
|
72
68
|
node: TSESTree.CallExpression | TSESTree.BinaryExpression;
|
|
@@ -139,7 +135,7 @@ export const preferIsRecordAndHasKey: TSESLint.RuleModule<MessageIds, Options> =
|
|
|
139
135
|
},
|
|
140
136
|
|
|
141
137
|
'Program:exit': () => {
|
|
142
|
-
const namedImports = getNamedImports(
|
|
138
|
+
const namedImports = getNamedImports(tsStdForgeImports);
|
|
143
139
|
|
|
144
140
|
const rewrites = mut_nodesToFix.map((entry) => ({
|
|
145
141
|
...entry,
|
|
@@ -184,7 +180,12 @@ export const preferIsRecordAndHasKey: TSESLint.RuleModule<MessageIds, Options> =
|
|
|
184
180
|
// otherwise and only one of them would survive a pass.
|
|
185
181
|
const importFixes =
|
|
186
182
|
index === 0 && importsToAdd.length > 0
|
|
187
|
-
?
|
|
183
|
+
? buildNamedImportFix(
|
|
184
|
+
fixer,
|
|
185
|
+
program,
|
|
186
|
+
TS_STD_FORGE_MODULE,
|
|
187
|
+
importsToAdd,
|
|
188
|
+
)
|
|
188
189
|
: [];
|
|
189
190
|
|
|
190
191
|
return [...importFixes, fixer.replaceText(node, replacement)];
|
|
@@ -125,15 +125,42 @@ describe('prefer-is-record-and-has-key through type wrappers', () => {
|
|
|
125
125
|
errors: [{ messageId: 'useHasKey' }],
|
|
126
126
|
},
|
|
127
127
|
{
|
|
128
|
-
|
|
128
|
+
// `Object.hasOwn(undefined, 'a')` throws; `isRecord` answers false.
|
|
129
|
+
name: 'keeps isRecord for `!` on an optional record, which leaves undefined in place',
|
|
129
130
|
code: dedent`
|
|
130
131
|
declare const obj: Readonly<Record<string, unknown>> | undefined;
|
|
131
132
|
const ok = Object.hasOwn(obj!, 'a');
|
|
132
133
|
`,
|
|
133
134
|
output: dedent`
|
|
134
|
-
import { hasKey } from 'ts-std-forge';
|
|
135
|
+
import { isRecord, hasKey } from 'ts-std-forge';
|
|
135
136
|
declare const obj: Readonly<Record<string, unknown>> | undefined;
|
|
136
|
-
const ok = hasKey(obj!, 'a');
|
|
137
|
+
const ok = (isRecord(obj!) && hasKey(obj!, 'a'));
|
|
138
|
+
`,
|
|
139
|
+
errors: [{ messageId: 'useIsRecordAndHasKey' }],
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
name: 'keeps isRecord for a record widened by `as` to one that may be an array',
|
|
143
|
+
code: dedent`
|
|
144
|
+
declare const obj: Readonly<Record<string, unknown>>;
|
|
145
|
+
const ok = Object.hasOwn(obj as Readonly<Record<string, unknown>> | readonly string[], 'a');
|
|
146
|
+
`,
|
|
147
|
+
output: dedent`
|
|
148
|
+
import { isRecord, hasKey } from 'ts-std-forge';
|
|
149
|
+
declare const obj: Readonly<Record<string, unknown>>;
|
|
150
|
+
const ok = (isRecord(obj as Readonly<Record<string, unknown>> | readonly string[]) && hasKey(obj as Readonly<Record<string, unknown>> | readonly string[], 'a'));
|
|
151
|
+
`,
|
|
152
|
+
errors: [{ messageId: 'useIsRecordAndHasKey' }],
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
name: 'drops isRecord for a record that is a record at every layer',
|
|
156
|
+
code: dedent`
|
|
157
|
+
declare const obj: Readonly<Record<string, unknown>>;
|
|
158
|
+
const ok = Object.hasOwn(obj! as Readonly<Record<string, unknown>>, 'a');
|
|
159
|
+
`,
|
|
160
|
+
output: dedent`
|
|
161
|
+
import { hasKey } from 'ts-std-forge';
|
|
162
|
+
declare const obj: Readonly<Record<string, unknown>>;
|
|
163
|
+
const ok = hasKey(obj! as Readonly<Record<string, unknown>>, 'a');
|
|
137
164
|
`,
|
|
138
165
|
errors: [{ messageId: 'useHasKey' }],
|
|
139
166
|
},
|
|
@@ -4,15 +4,14 @@ import {
|
|
|
4
4
|
type TSESTree,
|
|
5
5
|
} from '@typescript-eslint/utils';
|
|
6
6
|
import {
|
|
7
|
+
buildNamedImportFix,
|
|
8
|
+
getImportDeclarations,
|
|
9
|
+
getNamedImports,
|
|
7
10
|
getRangeWithParens,
|
|
8
|
-
isIdentifierNamed,
|
|
9
11
|
skipTypeWrappers,
|
|
10
|
-
} from '
|
|
11
|
-
import {
|
|
12
|
-
|
|
13
|
-
getNamedImports,
|
|
14
|
-
getTsStdForgeImport,
|
|
15
|
-
} from './import-utils.mjs';
|
|
12
|
+
} from 'eslint-rule-forge';
|
|
13
|
+
import { isIdentifierNamed } from './ast-utils.mjs';
|
|
14
|
+
import { TS_STD_FORGE_MODULE } from './constants.mjs';
|
|
16
15
|
|
|
17
16
|
type Options = readonly [];
|
|
18
17
|
|
|
@@ -39,7 +38,10 @@ export const preferSafeArrayIsArray: TSESLint.RuleModule<MessageIds, Options> =
|
|
|
39
38
|
|
|
40
39
|
const program = sourceCode.ast;
|
|
41
40
|
|
|
42
|
-
const
|
|
41
|
+
const tsStdForgeImports = getImportDeclarations(
|
|
42
|
+
program,
|
|
43
|
+
TS_STD_FORGE_MODULE,
|
|
44
|
+
);
|
|
43
45
|
|
|
44
46
|
const mut_nodesToFix: TSESTree.CallExpression[] = [];
|
|
45
47
|
|
|
@@ -53,7 +55,7 @@ export const preferSafeArrayIsArray: TSESLint.RuleModule<MessageIds, Options> =
|
|
|
53
55
|
},
|
|
54
56
|
'Program:exit': () => {
|
|
55
57
|
const hasSafeArrayImport =
|
|
56
|
-
getNamedImports(
|
|
58
|
+
getNamedImports(tsStdForgeImports).includes('SafeArray');
|
|
57
59
|
|
|
58
60
|
// The import fix lives on the first report only:
|
|
59
61
|
// `insertTextBefore(program, …)` produces overlapping ranges
|
|
@@ -67,7 +69,9 @@ export const preferSafeArrayIsArray: TSESLint.RuleModule<MessageIds, Options> =
|
|
|
67
69
|
fix: (fixer) => {
|
|
68
70
|
const importFixes =
|
|
69
71
|
index === 0 && !hasSafeArrayImport
|
|
70
|
-
?
|
|
72
|
+
? buildNamedImportFix(fixer, program, TS_STD_FORGE_MODULE, [
|
|
73
|
+
'SafeArray',
|
|
74
|
+
])
|
|
71
75
|
: [];
|
|
72
76
|
|
|
73
77
|
// Only the callee is replaced, type wrappers included, along
|
|
@@ -45,6 +45,20 @@ ruleTester.run('prefer-safe-array-is-array', preferSafeArrayIsArray, {
|
|
|
45
45
|
`,
|
|
46
46
|
errors: [{ messageId: 'useSafeArrayIsArray' }],
|
|
47
47
|
},
|
|
48
|
+
{
|
|
49
|
+
name: 'Sees SafeArray imported by a second ts-std-forge declaration',
|
|
50
|
+
code: dedent`
|
|
51
|
+
import type { Result } from 'ts-std-forge';
|
|
52
|
+
import { SafeArray } from 'ts-std-forge';
|
|
53
|
+
const result = Array.isArray(value);
|
|
54
|
+
`,
|
|
55
|
+
output: dedent`
|
|
56
|
+
import type { Result } from 'ts-std-forge';
|
|
57
|
+
import { SafeArray } from 'ts-std-forge';
|
|
58
|
+
const result = SafeArray.isArray(value);
|
|
59
|
+
`,
|
|
60
|
+
errors: [{ messageId: 'useSafeArrayIsArray' }],
|
|
61
|
+
},
|
|
48
62
|
{
|
|
49
63
|
name: 'Replace multiple Array.isArray calls, importing once',
|
|
50
64
|
code: dedent`
|
|
@@ -3,17 +3,16 @@ import {
|
|
|
3
3
|
type TSESLint,
|
|
4
4
|
type TSESTree,
|
|
5
5
|
} from '@typescript-eslint/utils';
|
|
6
|
-
import type { ReadonlyRecord } from 'ts-type-forge';
|
|
7
6
|
import {
|
|
7
|
+
buildNamedImportFix,
|
|
8
8
|
getArgumentText,
|
|
9
|
-
|
|
10
|
-
skipTypeWrappers,
|
|
11
|
-
} from './ast-utils.mjs';
|
|
12
|
-
import {
|
|
13
|
-
buildImportFixes,
|
|
9
|
+
getImportDeclarations,
|
|
14
10
|
getNamedImports,
|
|
15
|
-
|
|
16
|
-
} from '
|
|
11
|
+
skipTypeWrappers,
|
|
12
|
+
} from 'eslint-rule-forge';
|
|
13
|
+
import type { ReadonlyRecord } from 'ts-type-forge';
|
|
14
|
+
import { typeHoldsAtEveryLayer } from './ast-utils.mjs';
|
|
15
|
+
import { TS_STD_FORGE_MODULE } from './constants.mjs';
|
|
17
16
|
|
|
18
17
|
type Options = readonly [];
|
|
19
18
|
|
|
@@ -82,31 +81,29 @@ export const preferSafeArrayLengthGuard: TSESLint.RuleModule<
|
|
|
82
81
|
|
|
83
82
|
const program = sourceCode.ast;
|
|
84
83
|
|
|
85
|
-
const
|
|
86
|
-
|
|
87
|
-
|
|
84
|
+
const tsStdForgeImports = getImportDeclarations(
|
|
85
|
+
program,
|
|
86
|
+
TS_STD_FORGE_MODULE,
|
|
87
|
+
);
|
|
88
88
|
|
|
89
89
|
/**
|
|
90
90
|
* Whether `expression` is an array or tuple. Without type information the
|
|
91
91
|
* answer is no: `.length` is also a string's, a function's and a
|
|
92
|
-
* `TypedArray`'s, and none of those can be handed to the guard.
|
|
93
|
-
*
|
|
92
|
+
* `TypedArray`'s, and none of those can be handed to the guard. It has to
|
|
93
|
+
* be one at every layer of type wrappers: a string cast to an array is
|
|
94
|
+
* still a string, and the fix, which keeps the wrapper, hands the guard
|
|
95
|
+
* the type the outermost layer states.
|
|
94
96
|
*/
|
|
95
97
|
const isArrayLike = (
|
|
96
98
|
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
97
99
|
expression: TSESTree.Expression,
|
|
98
|
-
): boolean =>
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
return (
|
|
106
|
-
type !== undefined &&
|
|
107
|
-
(checker.isArrayType(type) || checker.isTupleType(type))
|
|
100
|
+
): boolean =>
|
|
101
|
+
typeHoldsAtEveryLayer(
|
|
102
|
+
sourceCode,
|
|
103
|
+
expression,
|
|
104
|
+
(type, checker) =>
|
|
105
|
+
checker.isArrayType(type) || checker.isTupleType(type),
|
|
108
106
|
);
|
|
109
|
-
};
|
|
110
107
|
|
|
111
108
|
const mut_nodesToFix: {
|
|
112
109
|
node: TSESTree.BinaryExpression;
|
|
@@ -130,7 +127,7 @@ export const preferSafeArrayLengthGuard: TSESLint.RuleModule<
|
|
|
130
127
|
},
|
|
131
128
|
'Program:exit': () => {
|
|
132
129
|
const hasSafeArrayImport =
|
|
133
|
-
getNamedImports(
|
|
130
|
+
getNamedImports(tsStdForgeImports).includes('SafeArray');
|
|
134
131
|
|
|
135
132
|
// The import fix lives on the first report only:
|
|
136
133
|
// `insertTextBefore(program, …)` produces overlapping ranges
|
|
@@ -155,7 +152,9 @@ export const preferSafeArrayLengthGuard: TSESLint.RuleModule<
|
|
|
155
152
|
|
|
156
153
|
const importFixes =
|
|
157
154
|
index === 0 && !hasSafeArrayImport
|
|
158
|
-
?
|
|
155
|
+
? buildNamedImportFix(fixer, program, TS_STD_FORGE_MODULE, [
|
|
156
|
+
'SafeArray',
|
|
157
|
+
])
|
|
159
158
|
: [];
|
|
160
159
|
|
|
161
160
|
return [...importFixes, fixer.replaceText(node, replacement)];
|