eslint-plugin-ts-std-forge 0.1.3 → 0.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/plugin.d.mts +1 -1
- package/dist/plugin.d.mts.map +1 -1
- package/dist/plugin.mjs +1 -0
- package/dist/plugin.mjs.map +1 -1
- package/dist/rule-types.d.mts +1 -1
- package/dist/rule-types.d.mts.map +1 -1
- package/dist/rules/ast-utils.d.mts +41 -0
- package/dist/rules/ast-utils.d.mts.map +1 -0
- package/dist/rules/ast-utils.mjs +101 -0
- package/dist/rules/ast-utils.mjs.map +1 -0
- package/dist/rules/import-utils.d.mts +1 -1
- package/dist/rules/import-utils.d.mts.map +1 -1
- package/dist/rules/import-utils.mjs +2 -1
- package/dist/rules/import-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 +55 -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 +22 -17
- 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 +23 -14
- 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 +19 -15
- 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 +16 -17
- 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 +14 -16
- package/dist/rules/prefer-safe-number-parse.mjs.map +1 -1
- package/dist/types.d.mts +2 -2
- package/dist/types.d.mts.map +1 -1
- package/package.json +24 -22
- package/src/plugin.mts +5 -4
- package/src/plugin.test.mts +0 -13
- package/src/rule-types.mts +2 -2
- package/src/rules/ast-utils.mts +147 -0
- package/src/rules/import-utils.mts +4 -2
- package/src/rules/prefer-is-non-null-object.mts +63 -38
- package/src/rules/prefer-is-non-null-object.test.mts +54 -0
- package/src/rules/prefer-is-record-and-has-key.mts +34 -21
- package/src/rules/prefer-is-record-and-has-key.test.mts +128 -0
- package/src/rules/prefer-safe-array-is-array.mts +34 -16
- package/src/rules/prefer-safe-array-is-array.test.mts +93 -0
- package/src/rules/prefer-safe-array-length-guard.mts +32 -19
- package/src/rules/prefer-safe-array-length-guard.test.mts +98 -0
- package/src/rules/prefer-safe-number-parse-integer.mts +29 -23
- package/src/rules/prefer-safe-number-parse-integer.test.mts +104 -0
- package/src/rules/prefer-safe-number-parse.mts +25 -21
- package/src/rules/prefer-safe-number-parse.test.mts +97 -0
- package/src/rules/rules.mts +1 -1
- package/src/types.mts +2 -2
|
@@ -3,7 +3,7 @@ import {
|
|
|
3
3
|
type TSESLint,
|
|
4
4
|
type TSESTree,
|
|
5
5
|
} from '@typescript-eslint/utils';
|
|
6
|
-
import {
|
|
6
|
+
import { skipTypeWrappers } from './ast-utils.mjs';
|
|
7
7
|
import {
|
|
8
8
|
buildImportFixes,
|
|
9
9
|
getNamedImports,
|
|
@@ -45,7 +45,9 @@ export const preferIsNonNullObject: TSESLint.RuleModule<MessageIds, Options> = {
|
|
|
45
45
|
LogicalExpression: (node) => {
|
|
46
46
|
const identifierName = getNonNullObjectIdentifierName(node);
|
|
47
47
|
|
|
48
|
-
if (identifierName === undefined)
|
|
48
|
+
if (identifierName === undefined) {
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
49
51
|
|
|
50
52
|
mut_nodesToFix.push({ node, identifierName });
|
|
51
53
|
},
|
|
@@ -83,60 +85,83 @@ export const preferIsNonNullObject: TSESLint.RuleModule<MessageIds, Options> = {
|
|
|
83
85
|
defaultOptions: [],
|
|
84
86
|
} as const;
|
|
85
87
|
|
|
88
|
+
// Every operand below is read through its type wrappers: `typeof (u satisfies
|
|
89
|
+
// unknown) === 'object' && u! !== null` is the same check.
|
|
90
|
+
|
|
86
91
|
const getNonNullObjectIdentifierName = (
|
|
87
|
-
|
|
92
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
93
|
+
node: TSESTree.LogicalExpression,
|
|
88
94
|
): string | undefined => {
|
|
89
|
-
if (node.operator !== '&&')
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
const
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
return leftIdentifierName
|
|
95
|
+
if (node.operator !== '&&') {
|
|
96
|
+
return undefined;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const leftIdentifierName = getTypeofObjectIdentifierName(
|
|
100
|
+
skipTypeWrappers(node.left),
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
const rightIdentifierName = getNonNullCheckIdentifierName(
|
|
104
|
+
skipTypeWrappers(node.right),
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
return leftIdentifierName === undefined ||
|
|
108
|
+
rightIdentifierName === undefined ||
|
|
109
|
+
leftIdentifierName !== rightIdentifierName
|
|
110
|
+
? undefined
|
|
111
|
+
: leftIdentifierName;
|
|
102
112
|
};
|
|
103
113
|
|
|
104
114
|
const getTypeofObjectIdentifierName = (
|
|
105
|
-
|
|
115
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
116
|
+
node: TSESTree.Node,
|
|
106
117
|
): string | undefined => {
|
|
107
|
-
if (node.type !== AST_NODE_TYPES.BinaryExpression)
|
|
108
|
-
|
|
109
|
-
|
|
118
|
+
if (node.type !== AST_NODE_TYPES.BinaryExpression) {
|
|
119
|
+
return undefined;
|
|
120
|
+
}
|
|
110
121
|
|
|
111
|
-
|
|
122
|
+
if (node.operator !== '===') {
|
|
123
|
+
return undefined;
|
|
124
|
+
}
|
|
112
125
|
|
|
113
|
-
|
|
126
|
+
const left = skipTypeWrappers(node.left);
|
|
114
127
|
|
|
115
|
-
|
|
128
|
+
const right = skipTypeWrappers(node.right);
|
|
116
129
|
|
|
117
|
-
if (
|
|
130
|
+
if (
|
|
131
|
+
left.type !== AST_NODE_TYPES.UnaryExpression ||
|
|
132
|
+
left.operator !== 'typeof' ||
|
|
133
|
+
right.type !== AST_NODE_TYPES.Literal ||
|
|
134
|
+
right.value !== 'object'
|
|
135
|
+
) {
|
|
136
|
+
return undefined;
|
|
137
|
+
}
|
|
118
138
|
|
|
119
|
-
|
|
139
|
+
const argument = skipTypeWrappers(left.argument);
|
|
120
140
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
141
|
+
return argument.type === AST_NODE_TYPES.Identifier
|
|
142
|
+
? argument.name
|
|
143
|
+
: undefined;
|
|
124
144
|
};
|
|
125
145
|
|
|
126
146
|
const getNonNullCheckIdentifierName = (
|
|
127
|
-
|
|
147
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
148
|
+
node: TSESTree.Node,
|
|
128
149
|
): string | undefined => {
|
|
129
|
-
if (node.type !== AST_NODE_TYPES.BinaryExpression)
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
const { left, right } = node;
|
|
150
|
+
if (node.type !== AST_NODE_TYPES.BinaryExpression) {
|
|
151
|
+
return undefined;
|
|
152
|
+
}
|
|
134
153
|
|
|
135
|
-
if (
|
|
154
|
+
if (node.operator !== '!==') {
|
|
155
|
+
return undefined;
|
|
156
|
+
}
|
|
136
157
|
|
|
137
|
-
|
|
158
|
+
const left = skipTypeWrappers(node.left);
|
|
138
159
|
|
|
139
|
-
|
|
160
|
+
const right = skipTypeWrappers(node.right);
|
|
140
161
|
|
|
141
|
-
return left.
|
|
162
|
+
return left.type !== AST_NODE_TYPES.Identifier ||
|
|
163
|
+
right.type !== AST_NODE_TYPES.Literal ||
|
|
164
|
+
right.value !== null
|
|
165
|
+
? undefined
|
|
166
|
+
: left.name;
|
|
142
167
|
};
|
|
@@ -52,3 +52,57 @@ ruleTester.run('prefer-is-non-null-object', preferIsNonNullObject, {
|
|
|
52
52
|
},
|
|
53
53
|
],
|
|
54
54
|
});
|
|
55
|
+
|
|
56
|
+
// The replacement names the bare identifier: that is the reference both
|
|
57
|
+
// halves narrow, and a wrapper around the argument would stop the guard from
|
|
58
|
+
// narrowing it.
|
|
59
|
+
describe('prefer-is-non-null-object through type wrappers', () => {
|
|
60
|
+
ruleTester.run('prefer-is-non-null-object', preferIsNonNullObject, {
|
|
61
|
+
valid: [
|
|
62
|
+
{
|
|
63
|
+
name: 'The two halves are about different identifiers behind wrappers',
|
|
64
|
+
code: dedent`
|
|
65
|
+
const ok = typeof (u satisfies unknown) === 'object' && v! !== null;
|
|
66
|
+
`,
|
|
67
|
+
},
|
|
68
|
+
],
|
|
69
|
+
invalid: [
|
|
70
|
+
{
|
|
71
|
+
name: 'reads the typeof operand through `satisfies`',
|
|
72
|
+
code: dedent`
|
|
73
|
+
const ok = typeof (u satisfies unknown) === 'object' && u !== null;
|
|
74
|
+
`,
|
|
75
|
+
output: dedent`
|
|
76
|
+
import { isNonNullObject } from 'ts-std-forge';
|
|
77
|
+
const ok = isNonNullObject(u);
|
|
78
|
+
`,
|
|
79
|
+
errors: [{ messageId: 'useIsNonNullObject' }],
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
name: 'reads the null check through `!` and `as`',
|
|
83
|
+
code: dedent`
|
|
84
|
+
const ok = typeof u === 'object' && u! !== (null as null);
|
|
85
|
+
`,
|
|
86
|
+
output: dedent`
|
|
87
|
+
import { isNonNullObject } from 'ts-std-forge';
|
|
88
|
+
const ok = isNonNullObject(u);
|
|
89
|
+
`,
|
|
90
|
+
errors: [{ messageId: 'useIsNonNullObject' }],
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
name: 'reads the literal and both halves through wrappers',
|
|
94
|
+
code: dedent`
|
|
95
|
+
const ok =
|
|
96
|
+
((typeof (<unknown>u) === ('object' as const)) satisfies boolean) &&
|
|
97
|
+
((u !== null) as boolean);
|
|
98
|
+
`,
|
|
99
|
+
output: dedent`
|
|
100
|
+
import { isNonNullObject } from 'ts-std-forge';
|
|
101
|
+
const ok =
|
|
102
|
+
isNonNullObject(u);
|
|
103
|
+
`,
|
|
104
|
+
errors: [{ messageId: 'useIsNonNullObject' }],
|
|
105
|
+
},
|
|
106
|
+
],
|
|
107
|
+
});
|
|
108
|
+
});
|
|
@@ -5,6 +5,12 @@ import {
|
|
|
5
5
|
} from '@typescript-eslint/utils';
|
|
6
6
|
import { SafeArray } from 'ts-std-forge';
|
|
7
7
|
import * as ts from 'typescript';
|
|
8
|
+
import {
|
|
9
|
+
getArgumentText,
|
|
10
|
+
getValueType,
|
|
11
|
+
isIdentifierNamed,
|
|
12
|
+
skipTypeWrappers,
|
|
13
|
+
} from './ast-utils.mjs';
|
|
8
14
|
import {
|
|
9
15
|
buildImportFixes,
|
|
10
16
|
getNamedImports,
|
|
@@ -40,28 +46,26 @@ export const preferIsRecordAndHasKey: TSESLint.RuleModule<MessageIds, Options> =
|
|
|
40
46
|
|
|
41
47
|
const tsStdForgeImport = getTsStdForgeImport(program);
|
|
42
48
|
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
const checker = services?.program?.getTypeChecker();
|
|
49
|
+
const checker = sourceCode.parserServices?.program?.getTypeChecker();
|
|
46
50
|
|
|
47
51
|
/**
|
|
48
52
|
* Whether the `isRecord(...)` half of the rewrite would be dead code
|
|
49
53
|
* because the object already satisfies `hasKey`'s
|
|
50
54
|
* `R extends UnknownRecord` constraint. Without type information nothing
|
|
51
|
-
* is known, so the guard is kept.
|
|
55
|
+
* is known, so the guard is kept. The type is the value's, not a cast's:
|
|
56
|
+
* an array cast to a record is still rejected by `isRecord`.
|
|
52
57
|
*/
|
|
53
58
|
const isKnownRecord = (
|
|
54
59
|
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
55
60
|
expression: TSESTree.Expression,
|
|
56
61
|
): boolean => {
|
|
57
|
-
if (checker === undefined)
|
|
62
|
+
if (checker === undefined) {
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
58
65
|
|
|
59
|
-
const
|
|
66
|
+
const type = getValueType(sourceCode, expression);
|
|
60
67
|
|
|
61
|
-
return (
|
|
62
|
-
tsNode !== undefined &&
|
|
63
|
-
isAlreadyRecordType(checker, checker.getTypeAtLocation(tsNode))
|
|
64
|
-
);
|
|
68
|
+
return type !== undefined && isAlreadyRecordType(checker, type);
|
|
65
69
|
};
|
|
66
70
|
|
|
67
71
|
const mut_nodesToFix: {
|
|
@@ -73,24 +77,29 @@ export const preferIsRecordAndHasKey: TSESLint.RuleModule<MessageIds, Options> =
|
|
|
73
77
|
return {
|
|
74
78
|
// Handle Object.hasOwn(obj, key)
|
|
75
79
|
CallExpression: (node) => {
|
|
80
|
+
const callee = skipTypeWrappers(node.callee);
|
|
81
|
+
|
|
76
82
|
if (
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
node.callee.property.name !== 'hasOwn'
|
|
83
|
+
callee.type !== AST_NODE_TYPES.MemberExpression ||
|
|
84
|
+
!isIdentifierNamed(callee.object, 'Object') ||
|
|
85
|
+
callee.property.type !== AST_NODE_TYPES.Identifier ||
|
|
86
|
+
callee.property.name !== 'hasOwn'
|
|
82
87
|
) {
|
|
83
88
|
return;
|
|
84
89
|
}
|
|
85
90
|
|
|
86
91
|
// Check arguments: Object.hasOwn(obj, key)
|
|
87
|
-
if (node.arguments.length !== 2)
|
|
92
|
+
if (node.arguments.length !== 2) {
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
88
95
|
|
|
89
96
|
const objArg = node.arguments[0];
|
|
90
97
|
|
|
91
98
|
const keyArg = node.arguments[1];
|
|
92
99
|
|
|
93
|
-
if (objArg === undefined || keyArg === undefined)
|
|
100
|
+
if (objArg === undefined || keyArg === undefined) {
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
94
103
|
|
|
95
104
|
// Type guard: ensure arguments are Expressions (not SpreadElement)
|
|
96
105
|
if (
|
|
@@ -109,14 +118,18 @@ export const preferIsRecordAndHasKey: TSESLint.RuleModule<MessageIds, Options> =
|
|
|
109
118
|
|
|
110
119
|
// Handle key in obj
|
|
111
120
|
BinaryExpression: (node) => {
|
|
112
|
-
if (node.operator !== 'in')
|
|
121
|
+
if (node.operator !== 'in') {
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
113
124
|
|
|
114
125
|
const keyExpression = node.left;
|
|
115
126
|
|
|
116
127
|
const objExpression = node.right;
|
|
117
128
|
|
|
118
129
|
// Type guard: ensure keyExpression is Expression (not PrivateIdentifier)
|
|
119
|
-
if (keyExpression.type === AST_NODE_TYPES.PrivateIdentifier)
|
|
130
|
+
if (keyExpression.type === AST_NODE_TYPES.PrivateIdentifier) {
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
120
133
|
|
|
121
134
|
mut_nodesToFix.push({
|
|
122
135
|
node,
|
|
@@ -147,9 +160,9 @@ export const preferIsRecordAndHasKey: TSESLint.RuleModule<MessageIds, Options> =
|
|
|
147
160
|
index,
|
|
148
161
|
{ node, objExpression, keyExpression, needsIsRecord },
|
|
149
162
|
] of rewrites.entries()) {
|
|
150
|
-
const objText = sourceCode
|
|
163
|
+
const objText = getArgumentText(sourceCode, objExpression);
|
|
151
164
|
|
|
152
|
-
const keyText = sourceCode
|
|
165
|
+
const keyText = getArgumentText(sourceCode, keyExpression);
|
|
153
166
|
|
|
154
167
|
const originalText = sourceCode.getText(node);
|
|
155
168
|
|
|
@@ -78,3 +78,131 @@ describe('prefer-is-record-and-has-key', () => {
|
|
|
78
78
|
],
|
|
79
79
|
});
|
|
80
80
|
});
|
|
81
|
+
|
|
82
|
+
describe('prefer-is-record-and-has-key through type wrappers', () => {
|
|
83
|
+
tester.run('prefer-is-record-and-has-key', preferIsRecordAndHasKey, {
|
|
84
|
+
valid: [],
|
|
85
|
+
invalid: [
|
|
86
|
+
{
|
|
87
|
+
// `isRecord` rejects an array, so dropping it on the cast's word
|
|
88
|
+
// would turn `false` into `true`.
|
|
89
|
+
name: 'keeps isRecord for an array cast to a record',
|
|
90
|
+
code: dedent`
|
|
91
|
+
declare const arr: string[];
|
|
92
|
+
const ok = Object.hasOwn(arr as unknown as Record<string, unknown>, '0');
|
|
93
|
+
`,
|
|
94
|
+
output: dedent`
|
|
95
|
+
import { isRecord, hasKey } from 'ts-std-forge';
|
|
96
|
+
declare const arr: string[];
|
|
97
|
+
const ok = (isRecord(arr as unknown as Record<string, unknown>) && hasKey(arr as unknown as Record<string, unknown>, '0'));
|
|
98
|
+
`,
|
|
99
|
+
errors: [{ messageId: 'useIsRecordAndHasKey' }],
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
name: 'keeps isRecord for an unknown value cast to a record in `in`',
|
|
103
|
+
code: dedent`
|
|
104
|
+
declare const u: unknown;
|
|
105
|
+
const ok = 'a' in (<Record<string, unknown>>u);
|
|
106
|
+
`,
|
|
107
|
+
output: dedent`
|
|
108
|
+
import { isRecord, hasKey } from 'ts-std-forge';
|
|
109
|
+
declare const u: unknown;
|
|
110
|
+
const ok = (isRecord(<Record<string, unknown>>u) && hasKey(<Record<string, unknown>>u, 'a'));
|
|
111
|
+
`,
|
|
112
|
+
errors: [{ messageId: 'useIsRecordAndHasKey' }],
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
name: 'drops isRecord for a record behind `satisfies` and keeps the wrapper',
|
|
116
|
+
code: dedent`
|
|
117
|
+
declare const obj: Readonly<Record<string, unknown>>;
|
|
118
|
+
const ok = Object.hasOwn(obj satisfies object, 'a');
|
|
119
|
+
`,
|
|
120
|
+
output: dedent`
|
|
121
|
+
import { hasKey } from 'ts-std-forge';
|
|
122
|
+
declare const obj: Readonly<Record<string, unknown>>;
|
|
123
|
+
const ok = hasKey(obj satisfies object, 'a');
|
|
124
|
+
`,
|
|
125
|
+
errors: [{ messageId: 'useHasKey' }],
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
name: 'reads `!` on an optional record as the record it asserts',
|
|
129
|
+
code: dedent`
|
|
130
|
+
declare const obj: Readonly<Record<string, unknown>> | undefined;
|
|
131
|
+
const ok = Object.hasOwn(obj!, 'a');
|
|
132
|
+
`,
|
|
133
|
+
output: dedent`
|
|
134
|
+
import { hasKey } from 'ts-std-forge';
|
|
135
|
+
declare const obj: Readonly<Record<string, unknown>> | undefined;
|
|
136
|
+
const ok = hasKey(obj!, 'a');
|
|
137
|
+
`,
|
|
138
|
+
errors: [{ messageId: 'useHasKey' }],
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
name: 'reads `Object` through `as`',
|
|
142
|
+
code: dedent`
|
|
143
|
+
declare const obj: Readonly<Record<string, unknown>>;
|
|
144
|
+
const ok = (Object as ObjectConstructor).hasOwn(obj, 'a');
|
|
145
|
+
`,
|
|
146
|
+
output: dedent`
|
|
147
|
+
import { hasKey } from 'ts-std-forge';
|
|
148
|
+
declare const obj: Readonly<Record<string, unknown>>;
|
|
149
|
+
const ok = hasKey(obj, 'a');
|
|
150
|
+
`,
|
|
151
|
+
errors: [{ messageId: 'useHasKey' }],
|
|
152
|
+
},
|
|
153
|
+
],
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
describe('prefer-is-record-and-has-key with parenthesized operands', () => {
|
|
158
|
+
tester.run('prefer-is-record-and-has-key', preferIsRecordAndHasKey, {
|
|
159
|
+
valid: [],
|
|
160
|
+
invalid: [
|
|
161
|
+
{
|
|
162
|
+
name: 'keeps the parentheses of a sequence object in `in`',
|
|
163
|
+
code: dedent`
|
|
164
|
+
declare const a: number;
|
|
165
|
+
declare const o: unknown;
|
|
166
|
+
const ok = 'k' in (a, o);
|
|
167
|
+
`,
|
|
168
|
+
output: dedent`
|
|
169
|
+
import { isRecord, hasKey } from 'ts-std-forge';
|
|
170
|
+
declare const a: number;
|
|
171
|
+
declare const o: unknown;
|
|
172
|
+
const ok = (isRecord((a, o)) && hasKey((a, o), 'k'));
|
|
173
|
+
`,
|
|
174
|
+
errors: [{ messageId: 'useIsRecordAndHasKey' }],
|
|
175
|
+
},
|
|
176
|
+
{
|
|
177
|
+
name: 'keeps the parentheses of a sequence object in Object.hasOwn',
|
|
178
|
+
code: dedent`
|
|
179
|
+
declare const a: number;
|
|
180
|
+
declare const o: unknown;
|
|
181
|
+
const ok = Object.hasOwn((a, o), 'k');
|
|
182
|
+
`,
|
|
183
|
+
output: dedent`
|
|
184
|
+
import { isRecord, hasKey } from 'ts-std-forge';
|
|
185
|
+
declare const a: number;
|
|
186
|
+
declare const o: unknown;
|
|
187
|
+
const ok = (isRecord((a, o)) && hasKey((a, o), 'k'));
|
|
188
|
+
`,
|
|
189
|
+
errors: [{ messageId: 'useIsRecordAndHasKey' }],
|
|
190
|
+
},
|
|
191
|
+
{
|
|
192
|
+
name: 'keeps the parentheses of a sequence key',
|
|
193
|
+
code: dedent`
|
|
194
|
+
declare const a: number;
|
|
195
|
+
declare const o: Readonly<Record<string, unknown>>;
|
|
196
|
+
const ok = (a, 'k') in o;
|
|
197
|
+
`,
|
|
198
|
+
output: dedent`
|
|
199
|
+
import { hasKey } from 'ts-std-forge';
|
|
200
|
+
declare const a: number;
|
|
201
|
+
declare const o: Readonly<Record<string, unknown>>;
|
|
202
|
+
const ok = hasKey(o, (a, 'k'));
|
|
203
|
+
`,
|
|
204
|
+
errors: [{ messageId: 'useHasKey' }],
|
|
205
|
+
},
|
|
206
|
+
],
|
|
207
|
+
});
|
|
208
|
+
});
|
|
@@ -3,7 +3,11 @@ import {
|
|
|
3
3
|
type TSESLint,
|
|
4
4
|
type TSESTree,
|
|
5
5
|
} from '@typescript-eslint/utils';
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
getRangeWithParens,
|
|
8
|
+
isIdentifierNamed,
|
|
9
|
+
skipTypeWrappers,
|
|
10
|
+
} from './ast-utils.mjs';
|
|
7
11
|
import {
|
|
8
12
|
buildImportFixes,
|
|
9
13
|
getNamedImports,
|
|
@@ -41,7 +45,9 @@ export const preferSafeArrayIsArray: TSESLint.RuleModule<MessageIds, Options> =
|
|
|
41
45
|
|
|
42
46
|
return {
|
|
43
47
|
CallExpression: (node) => {
|
|
44
|
-
if (!isArrayIsArrayCall(node))
|
|
48
|
+
if (!isArrayIsArrayCall(node)) {
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
45
51
|
|
|
46
52
|
mut_nodesToFix.push(node);
|
|
47
53
|
},
|
|
@@ -59,18 +65,21 @@ export const preferSafeArrayIsArray: TSESLint.RuleModule<MessageIds, Options> =
|
|
|
59
65
|
node,
|
|
60
66
|
messageId: 'useSafeArrayIsArray',
|
|
61
67
|
fix: (fixer) => {
|
|
62
|
-
const callee = node.callee;
|
|
63
|
-
|
|
64
|
-
if (callee.type !== AST_NODE_TYPES.MemberExpression) return [];
|
|
65
|
-
|
|
66
|
-
const replacement = `SafeArray.isArray${sourceCode.getText(node).slice(sourceCode.getText(callee).length)}`;
|
|
67
|
-
|
|
68
68
|
const importFixes =
|
|
69
69
|
index === 0 && !hasSafeArrayImport
|
|
70
70
|
? buildImportFixes(fixer, program, ['SafeArray'])
|
|
71
71
|
: [];
|
|
72
72
|
|
|
73
|
-
|
|
73
|
+
// Only the callee is replaced, type wrappers included, along
|
|
74
|
+
// with the parentheses around it, which its range leaves out;
|
|
75
|
+
// the arguments stay as written.
|
|
76
|
+
return [
|
|
77
|
+
...importFixes,
|
|
78
|
+
fixer.replaceTextRange(
|
|
79
|
+
getRangeWithParens(sourceCode, node.callee),
|
|
80
|
+
'SafeArray.isArray',
|
|
81
|
+
),
|
|
82
|
+
];
|
|
74
83
|
},
|
|
75
84
|
});
|
|
76
85
|
}
|
|
@@ -81,19 +90,28 @@ export const preferSafeArrayIsArray: TSESLint.RuleModule<MessageIds, Options> =
|
|
|
81
90
|
} as const;
|
|
82
91
|
|
|
83
92
|
const isArrayIsArrayCall = (
|
|
84
|
-
|
|
93
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
94
|
+
node: TSESTree.CallExpression,
|
|
85
95
|
): boolean => {
|
|
86
|
-
|
|
96
|
+
const callee = skipTypeWrappers(node.callee);
|
|
87
97
|
|
|
88
|
-
|
|
98
|
+
if (callee.type !== AST_NODE_TYPES.MemberExpression) {
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
89
101
|
|
|
90
|
-
|
|
102
|
+
const { object, property } = callee;
|
|
91
103
|
|
|
92
|
-
if (object
|
|
104
|
+
if (!isIdentifierNamed(object, 'Array')) {
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
93
107
|
|
|
94
|
-
if (property.type !== AST_NODE_TYPES.Identifier)
|
|
108
|
+
if (property.type !== AST_NODE_TYPES.Identifier) {
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
95
111
|
|
|
96
|
-
if (property.name !== 'isArray')
|
|
112
|
+
if (property.name !== 'isArray') {
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
97
115
|
|
|
98
116
|
return true;
|
|
99
117
|
};
|
|
@@ -63,3 +63,96 @@ ruleTester.run('prefer-safe-array-is-array', preferSafeArrayIsArray, {
|
|
|
63
63
|
},
|
|
64
64
|
],
|
|
65
65
|
});
|
|
66
|
+
|
|
67
|
+
describe('prefer-safe-array-is-array through type wrappers', () => {
|
|
68
|
+
ruleTester.run('prefer-safe-array-is-array', preferSafeArrayIsArray, {
|
|
69
|
+
valid: [],
|
|
70
|
+
invalid: [
|
|
71
|
+
{
|
|
72
|
+
name: 'reads `Array` through `as`',
|
|
73
|
+
code: dedent`
|
|
74
|
+
const result = (Array as ArrayConstructor).isArray(value);
|
|
75
|
+
`,
|
|
76
|
+
output: dedent`
|
|
77
|
+
import { SafeArray } from 'ts-std-forge';
|
|
78
|
+
const result = SafeArray.isArray(value);
|
|
79
|
+
`,
|
|
80
|
+
errors: [{ messageId: 'useSafeArrayIsArray' }],
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
name: 'reads `Array` through `!`',
|
|
84
|
+
code: dedent`
|
|
85
|
+
const result = Array!.isArray(value);
|
|
86
|
+
`,
|
|
87
|
+
output: dedent`
|
|
88
|
+
import { SafeArray } from 'ts-std-forge';
|
|
89
|
+
const result = SafeArray.isArray(value);
|
|
90
|
+
`,
|
|
91
|
+
errors: [{ messageId: 'useSafeArrayIsArray' }],
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
name: 'reads the callee through `satisfies`',
|
|
95
|
+
code: dedent`
|
|
96
|
+
const result = (Array.isArray satisfies unknown)(value);
|
|
97
|
+
`,
|
|
98
|
+
output: dedent`
|
|
99
|
+
import { SafeArray } from 'ts-std-forge';
|
|
100
|
+
const result = SafeArray.isArray(value);
|
|
101
|
+
`,
|
|
102
|
+
errors: [{ messageId: 'useSafeArrayIsArray' }],
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
name: 'keeps a wrapper on the argument',
|
|
106
|
+
code: dedent`
|
|
107
|
+
const result = Array.isArray(value satisfies unknown);
|
|
108
|
+
`,
|
|
109
|
+
output: dedent`
|
|
110
|
+
import { SafeArray } from 'ts-std-forge';
|
|
111
|
+
const result = SafeArray.isArray(value satisfies unknown);
|
|
112
|
+
`,
|
|
113
|
+
errors: [{ messageId: 'useSafeArrayIsArray' }],
|
|
114
|
+
},
|
|
115
|
+
],
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
describe('prefer-safe-array-is-array with parenthesized operands', () => {
|
|
120
|
+
ruleTester.run('prefer-safe-array-is-array', preferSafeArrayIsArray, {
|
|
121
|
+
valid: [],
|
|
122
|
+
invalid: [
|
|
123
|
+
{
|
|
124
|
+
name: 'replaces a parenthesized callee with its parentheses',
|
|
125
|
+
code: dedent`
|
|
126
|
+
const result = (Array.isArray)(x);
|
|
127
|
+
`,
|
|
128
|
+
output: dedent`
|
|
129
|
+
import { SafeArray } from 'ts-std-forge';
|
|
130
|
+
const result = SafeArray.isArray(x);
|
|
131
|
+
`,
|
|
132
|
+
errors: [{ messageId: 'useSafeArrayIsArray' }],
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
name: 'replaces a doubly parenthesized callee of an optional call',
|
|
136
|
+
code: dedent`
|
|
137
|
+
const result = ((Array.isArray))?.(x);
|
|
138
|
+
`,
|
|
139
|
+
output: dedent`
|
|
140
|
+
import { SafeArray } from 'ts-std-forge';
|
|
141
|
+
const result = SafeArray.isArray?.(x);
|
|
142
|
+
`,
|
|
143
|
+
errors: [{ messageId: 'useSafeArrayIsArray' }],
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
name: 'keeps a parenthesized argument',
|
|
147
|
+
code: dedent`
|
|
148
|
+
const result = Array.isArray((a, x));
|
|
149
|
+
`,
|
|
150
|
+
output: dedent`
|
|
151
|
+
import { SafeArray } from 'ts-std-forge';
|
|
152
|
+
const result = SafeArray.isArray((a, x));
|
|
153
|
+
`,
|
|
154
|
+
errors: [{ messageId: 'useSafeArrayIsArray' }],
|
|
155
|
+
},
|
|
156
|
+
],
|
|
157
|
+
});
|
|
158
|
+
});
|