eslint-plugin-ts-std-forge 0.1.9 → 0.1.10
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 +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 -38
- 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 +91 -25
- package/dist/rules/prefer-is-record-and-has-key.mjs.map +1 -1
- package/package.json +10 -10
- package/src/rule-types.mts +1 -1
- package/src/rules/prefer-is-non-null-object.mts +152 -49
- package/src/rules/prefer-is-non-null-object.test.mts +107 -0
- package/src/rules/prefer-is-record-and-has-key.mts +132 -39
- package/src/rules/prefer-is-record-and-has-key.test.mts +99 -39
package/dist/rule-types.d.mts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { Linter } from 'eslint';
|
|
2
2
|
import type { ReadonlyRecord } from 'ts-type-forge';
|
|
3
3
|
/**
|
|
4
|
-
* @description Replace `typeof u === "object" && u !== null
|
|
4
|
+
* @description Replace `typeof u === "object" && u !== null`, in either order and anywhere in an `&&` chain, with `isNonNullObject(u)` from ts-std-forge.
|
|
5
5
|
*
|
|
6
6
|
* ```md
|
|
7
7
|
* | key | value |
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"prefer-is-non-null-object.d.mts","sourceRoot":"","sources":["../../src/rules/prefer-is-non-null-object.mts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,QAAQ,EAEd,MAAM,0BAA0B,CAAC;
|
|
1
|
+
{"version":3,"file":"prefer-is-non-null-object.d.mts","sourceRoot":"","sources":["../../src/rules/prefer-is-non-null-object.mts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,QAAQ,EAEd,MAAM,0BAA0B,CAAC;AAWlC,KAAK,OAAO,GAAG,SAAS,EAAE,CAAC;AAE3B,KAAK,UAAU,GAAG,oBAAoB,CAAC;AAEvC,eAAO,MAAM,qBAAqB,EAAE,QAAQ,CAAC,UAAU,CAAC,UAAU,EAAE,OAAO,CA4EjE,CAAC"}
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { AST_NODE_TYPES, } from '@typescript-eslint/utils';
|
|
2
|
-
import { buildNamedImportFix, getImportDeclarations, getNamedImports, skipTypeWrappers, } from 'eslint-rule-forge';
|
|
2
|
+
import { buildNamedImportFix, getImportDeclarations, getNamedImports, getRangeWithParens, parenthesesDepth, skipTypeWrappers, } from 'eslint-rule-forge';
|
|
3
3
|
import { TS_STD_FORGE_MODULE } from './constants.mjs';
|
|
4
4
|
export const preferIsNonNullObject = {
|
|
5
5
|
meta: {
|
|
6
6
|
type: 'suggestion',
|
|
7
7
|
docs: {
|
|
8
|
-
description: 'Replace `typeof u === "object" && u !== null
|
|
8
|
+
description: 'Replace `typeof u === "object" && u !== null`, in either order and anywhere in an `&&` chain, with `isNonNullObject(u)` from ts-std-forge.',
|
|
9
9
|
},
|
|
10
10
|
fixable: 'code',
|
|
11
11
|
schema: [],
|
|
@@ -17,14 +17,14 @@ export const preferIsNonNullObject = {
|
|
|
17
17
|
const sourceCode = context.sourceCode;
|
|
18
18
|
const program = sourceCode.ast;
|
|
19
19
|
const tsStdForgeImports = getImportDeclarations(program, TS_STD_FORGE_MODULE);
|
|
20
|
-
const
|
|
20
|
+
const mut_pairsToFix = [];
|
|
21
21
|
return {
|
|
22
22
|
LogicalExpression: (node) => {
|
|
23
|
-
|
|
24
|
-
if (
|
|
23
|
+
|
|
24
|
+
if (node.operator !== '&&' || isChainOperand(sourceCode, node)) {
|
|
25
25
|
return;
|
|
26
26
|
}
|
|
27
|
-
|
|
27
|
+
mut_pairsToFix.push(...findNonNullObjectPairs(sourceCode, node));
|
|
28
28
|
},
|
|
29
29
|
'Program:exit': () => {
|
|
30
30
|
const hasIsNonNullObjectImport = getNamedImports(tsStdForgeImports).includes('isNonNullObject');
|
|
@@ -33,9 +33,12 @@ export const preferIsNonNullObject = {
|
|
|
33
33
|
|
|
34
34
|
|
|
35
35
|
|
|
36
|
-
for (const [index, {
|
|
36
|
+
for (const [index, { range, identifierName },] of mut_pairsToFix.entries()) {
|
|
37
37
|
context.report({
|
|
38
|
-
|
|
38
|
+
loc: {
|
|
39
|
+
start: sourceCode.getLocFromIndex(range[0]),
|
|
40
|
+
end: sourceCode.getLocFromIndex(range[1]),
|
|
41
|
+
},
|
|
39
42
|
messageId: 'useIsNonNullObject',
|
|
40
43
|
fix: (fixer) => {
|
|
41
44
|
const replacement = `isNonNullObject(${identifierName})`;
|
|
@@ -44,7 +47,10 @@ export const preferIsNonNullObject = {
|
|
|
44
47
|
'isNonNullObject',
|
|
45
48
|
])
|
|
46
49
|
: [];
|
|
47
|
-
return [
|
|
50
|
+
return [
|
|
51
|
+
...importFixes,
|
|
52
|
+
fixer.replaceTextRange(range, replacement),
|
|
53
|
+
];
|
|
48
54
|
},
|
|
49
55
|
});
|
|
50
56
|
}
|
|
@@ -55,57 +61,126 @@ export const preferIsNonNullObject = {
|
|
|
55
61
|
};
|
|
56
62
|
|
|
57
63
|
|
|
58
|
-
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
const isChainOperand = (
|
|
67
|
+
|
|
68
|
+
sourceCode,
|
|
69
|
+
|
|
70
|
+
node) => node.parent.type === AST_NODE_TYPES.LogicalExpression &&
|
|
71
|
+
node.parent.operator === '&&' &&
|
|
72
|
+
parenthesesDepth(sourceCode, node) === 0;
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
const findNonNullObjectPairs = (
|
|
80
|
+
|
|
81
|
+
sourceCode,
|
|
59
82
|
|
|
60
83
|
node) => {
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
84
|
+
const operands = chainOperands(sourceCode, node);
|
|
85
|
+
const pairsFrom = (index) => {
|
|
86
|
+
const first = operands[index];
|
|
87
|
+
const second = operands[index + 1];
|
|
88
|
+
if (first === undefined || second === undefined) {
|
|
89
|
+
return [];
|
|
90
|
+
}
|
|
91
|
+
const identifierName = pairIdentifierName(first, second) ?? pairIdentifierName(second, first);
|
|
92
|
+
return identifierName === undefined
|
|
93
|
+
? pairsFrom(index + 1)
|
|
94
|
+
: [
|
|
95
|
+
{
|
|
96
|
+
range: [
|
|
97
|
+
getRangeWithParens(sourceCode, first)[0],
|
|
98
|
+
getRangeWithParens(sourceCode, second)[1],
|
|
99
|
+
],
|
|
100
|
+
identifierName,
|
|
101
|
+
},
|
|
102
|
+
...pairsFrom(index + 2),
|
|
103
|
+
];
|
|
104
|
+
};
|
|
105
|
+
return pairsFrom(0);
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
const chainOperands = (
|
|
112
|
+
|
|
113
|
+
sourceCode,
|
|
114
|
+
|
|
115
|
+
node) => [node.left, node.right].flatMap((operand) => operand.type === AST_NODE_TYPES.LogicalExpression &&
|
|
116
|
+
operand.operator === '&&' &&
|
|
117
|
+
parenthesesDepth(sourceCode, operand) === 0
|
|
118
|
+
? chainOperands(sourceCode, operand)
|
|
119
|
+
: [operand]);
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
const pairIdentifierName = (
|
|
127
|
+
|
|
128
|
+
typeofCheck,
|
|
129
|
+
|
|
130
|
+
nullCheck) => {
|
|
131
|
+
const identifierName = getTypeofObjectIdentifierName(skipTypeWrappers(typeofCheck));
|
|
132
|
+
return identifierName !== undefined &&
|
|
133
|
+
identifierName ===
|
|
134
|
+
getNonNullCheckIdentifierName(skipTypeWrappers(nullCheck))
|
|
135
|
+
? identifierName
|
|
136
|
+
: undefined;
|
|
71
137
|
};
|
|
138
|
+
|
|
72
139
|
const getTypeofObjectIdentifierName = (
|
|
73
140
|
|
|
74
141
|
node) => {
|
|
75
|
-
if (node.type !== AST_NODE_TYPES.BinaryExpression
|
|
76
|
-
|
|
77
|
-
}
|
|
78
|
-
if (node.operator !== '===') {
|
|
142
|
+
if (node.type !== AST_NODE_TYPES.BinaryExpression ||
|
|
143
|
+
node.operator !== '===') {
|
|
79
144
|
return undefined;
|
|
80
145
|
}
|
|
81
146
|
const left = skipTypeWrappers(node.left);
|
|
82
147
|
const right = skipTypeWrappers(node.right);
|
|
83
|
-
|
|
84
|
-
left
|
|
85
|
-
|
|
86
|
-
|
|
148
|
+
const typeofSide = isLiteralOf(right, 'object')
|
|
149
|
+
? left
|
|
150
|
+
: isLiteralOf(left, 'object')
|
|
151
|
+
? right
|
|
152
|
+
: undefined;
|
|
153
|
+
if (typeofSide?.type !== AST_NODE_TYPES.UnaryExpression ||
|
|
154
|
+
typeofSide.operator !== 'typeof') {
|
|
87
155
|
return undefined;
|
|
88
156
|
}
|
|
89
|
-
const argument = skipTypeWrappers(
|
|
157
|
+
const argument = skipTypeWrappers(typeofSide.argument);
|
|
90
158
|
return argument.type === AST_NODE_TYPES.Identifier
|
|
91
159
|
? argument.name
|
|
92
160
|
: undefined;
|
|
93
161
|
};
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
|
|
94
167
|
const getNonNullCheckIdentifierName = (
|
|
95
168
|
|
|
96
169
|
node) => {
|
|
97
|
-
if (node.type !== AST_NODE_TYPES.BinaryExpression
|
|
98
|
-
|
|
99
|
-
}
|
|
100
|
-
if (node.operator !== '!==') {
|
|
170
|
+
if (node.type !== AST_NODE_TYPES.BinaryExpression ||
|
|
171
|
+
(node.operator !== '!==' && node.operator !== '!=')) {
|
|
101
172
|
return undefined;
|
|
102
173
|
}
|
|
103
174
|
const left = skipTypeWrappers(node.left);
|
|
104
175
|
const right = skipTypeWrappers(node.right);
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
176
|
+
const checked = isLiteralOf(right, null)
|
|
177
|
+
? left
|
|
178
|
+
: isLiteralOf(left, null)
|
|
179
|
+
? right
|
|
180
|
+
: undefined;
|
|
181
|
+
return checked?.type === AST_NODE_TYPES.Identifier ? checked.name : undefined;
|
|
110
182
|
};
|
|
183
|
+
const isLiteralOf = (
|
|
184
|
+
|
|
185
|
+
node, value) => node.type === AST_NODE_TYPES.Literal && node.value === value;
|
|
111
186
|
//# sourceMappingURL=prefer-is-non-null-object.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"prefer-is-non-null-object.mjs","sourceRoot":"","sources":["../../src/rules/prefer-is-non-null-object.mts"],"names":[],"mappings":"AAAA,OAAO,EACL,cAAc,GAGf,MAAM,0BAA0B,CAAC;AAClC,OAAO,EACL,mBAAmB,EACnB,qBAAqB,EACrB,eAAe,EACf,gBAAgB,GACjB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EAAE,mBAAmB,EAAE,MAAM,iBAAiB,CAAC;AAMtD,MAAM,CAAC,MAAM,qBAAqB,GAA6C;IAC7E,IAAI,EAAE;QACJ,IAAI,EAAE,YAAY;QAClB,IAAI,EAAE;YACJ,WAAW,EACT,
|
|
1
|
+
{"version":3,"file":"prefer-is-non-null-object.mjs","sourceRoot":"","sources":["../../src/rules/prefer-is-non-null-object.mts"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"prefer-is-record-and-has-key.d.mts","sourceRoot":"","sources":["../../src/rules/prefer-is-record-and-has-key.mts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,QAAQ,EAEd,MAAM,0BAA0B,CAAC;AAalC,KAAK,OAAO,GAAG,SAAS,EAAE,CAAC;AAE3B,KAAK,UAAU,GAAG,sBAAsB,GAAG,WAAW,CAAC;AAEvD,eAAO,MAAM,uBAAuB,EAAE,QAAQ,CAAC,UAAU,CAAC,UAAU,EAAE,OAAO,
|
|
1
|
+
{"version":3,"file":"prefer-is-record-and-has-key.d.mts","sourceRoot":"","sources":["../../src/rules/prefer-is-record-and-has-key.mts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,QAAQ,EAEd,MAAM,0BAA0B,CAAC;AAalC,KAAK,OAAO,GAAG,SAAS,EAAE,CAAC;AAE3B,KAAK,UAAU,GAAG,sBAAsB,GAAG,WAAW,CAAC;AAEvD,eAAO,MAAM,uBAAuB,EAAE,QAAQ,CAAC,UAAU,CAAC,UAAU,EAAE,OAAO,CA4NjE,CAAC"}
|
|
@@ -33,6 +33,17 @@ export const preferIsRecordAndHasKey = {
|
|
|
33
33
|
const isKnownRecord = (
|
|
34
34
|
|
|
35
35
|
expression) => typeHoldsAtEveryLayer(sourceCode, expression, (type, checker) => isAlreadyRecordType(checker, type));
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
const mayBeArray = (
|
|
44
|
+
|
|
45
|
+
expression) => sourceCode.parserServices?.program != null &&
|
|
46
|
+
!typeHoldsAtEveryLayer(sourceCode, expression, (type, checker) => !mayBeArrayType(checker, type));
|
|
36
47
|
const mut_nodesToFix = [];
|
|
37
48
|
return {
|
|
38
49
|
|
|
@@ -55,7 +66,8 @@ export const preferIsRecordAndHasKey = {
|
|
|
55
66
|
}
|
|
56
67
|
|
|
57
68
|
if (objArg.type === AST_NODE_TYPES.SpreadElement ||
|
|
58
|
-
keyArg.type === AST_NODE_TYPES.SpreadElement
|
|
69
|
+
keyArg.type === AST_NODE_TYPES.SpreadElement ||
|
|
70
|
+
mayBeArray(objArg)) {
|
|
59
71
|
return;
|
|
60
72
|
}
|
|
61
73
|
mut_nodesToFix.push({
|
|
@@ -72,7 +84,8 @@ export const preferIsRecordAndHasKey = {
|
|
|
72
84
|
const keyExpression = node.left;
|
|
73
85
|
const objExpression = node.right;
|
|
74
86
|
|
|
75
|
-
if (keyExpression.type === AST_NODE_TYPES.PrivateIdentifier
|
|
87
|
+
if (keyExpression.type === AST_NODE_TYPES.PrivateIdentifier ||
|
|
88
|
+
mayBeArray(objExpression)) {
|
|
76
89
|
return;
|
|
77
90
|
}
|
|
78
91
|
mut_nodesToFix.push({
|
|
@@ -83,20 +96,29 @@ export const preferIsRecordAndHasKey = {
|
|
|
83
96
|
},
|
|
84
97
|
'Program:exit': () => {
|
|
85
98
|
const namedImports = getNamedImports(tsStdForgeImports);
|
|
86
|
-
const rewrites = mut_nodesToFix.map((entry) =>
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
!
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
99
|
+
const rewrites = mut_nodesToFix.map((entry) => {
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
const needsIsRecord = !isKnownRecord(entry.objExpression);
|
|
103
|
+
return {
|
|
104
|
+
...entry,
|
|
105
|
+
needsIsRecord,
|
|
106
|
+
|
|
107
|
+
fixable: !needsIsRecord || isSideEffectFree(entry.objExpression),
|
|
108
|
+
};
|
|
109
|
+
});
|
|
110
|
+
const fixableRewrites = rewrites.filter(({ fixable }) => fixable);
|
|
111
|
+
const importsToAdd = SafeArray.isEmpty(fixableRewrites)
|
|
112
|
+
? []
|
|
113
|
+
: [
|
|
114
|
+
...(fixableRewrites.some(({ needsIsRecord }) => needsIsRecord) && !namedImports.includes('isRecord')
|
|
115
|
+
? ['isRecord']
|
|
116
|
+
: []),
|
|
117
|
+
...(namedImports.includes('hasKey') ? [] : ['hasKey']),
|
|
118
|
+
];
|
|
119
|
+
const importCarrier = fixableRewrites[0];
|
|
120
|
+
for (const rewrite of rewrites) {
|
|
121
|
+
const { node, objExpression, keyExpression, needsIsRecord } = rewrite;
|
|
100
122
|
const objText = getArgumentText(sourceCode, objExpression);
|
|
101
123
|
const keyText = getArgumentText(sourceCode, keyExpression);
|
|
102
124
|
const originalText = sourceCode.getText(node);
|
|
@@ -108,18 +130,24 @@ export const preferIsRecordAndHasKey = {
|
|
|
108
130
|
objName: objText,
|
|
109
131
|
keyName: keyText,
|
|
110
132
|
},
|
|
111
|
-
fix:
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
133
|
+
fix: !rewrite.fixable
|
|
134
|
+
? undefined
|
|
135
|
+
: (fixer) => {
|
|
136
|
+
const replacement = needsIsRecord
|
|
137
|
+
? `(isRecord(${objText}) && hasKey(${objText}, ${keyText}))`
|
|
138
|
+
: `hasKey(${objText}, ${keyText})`;
|
|
115
139
|
|
|
116
140
|
|
|
117
141
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
142
|
+
|
|
143
|
+
const importFixes = rewrite === importCarrier && importsToAdd.length > 0
|
|
144
|
+
? buildNamedImportFix(fixer, program, TS_STD_FORGE_MODULE, importsToAdd)
|
|
145
|
+
: [];
|
|
146
|
+
return [
|
|
147
|
+
...importFixes,
|
|
148
|
+
fixer.replaceText(node, replacement),
|
|
149
|
+
];
|
|
150
|
+
},
|
|
123
151
|
});
|
|
124
152
|
}
|
|
125
153
|
},
|
|
@@ -131,6 +159,44 @@ export const preferIsRecordAndHasKey = {
|
|
|
131
159
|
|
|
132
160
|
|
|
133
161
|
|
|
162
|
+
const isSideEffectFree = (
|
|
163
|
+
|
|
164
|
+
wrapped) => {
|
|
165
|
+
const node = skipTypeWrappers(wrapped);
|
|
166
|
+
return (node.type === AST_NODE_TYPES.Identifier ||
|
|
167
|
+
node.type === AST_NODE_TYPES.ThisExpression ||
|
|
168
|
+
(node.type === AST_NODE_TYPES.MemberExpression &&
|
|
169
|
+
!node.computed &&
|
|
170
|
+
isSideEffectFree(node.object)));
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
const mayBeArrayType = (
|
|
178
|
+
|
|
179
|
+
checker,
|
|
180
|
+
|
|
181
|
+
type) => {
|
|
182
|
+
if (type.isUnionOrIntersection()) {
|
|
183
|
+
return type.types.some((member) => mayBeArrayType(checker, member));
|
|
184
|
+
}
|
|
185
|
+
if ((type.flags &
|
|
186
|
+
(ts.TypeFlags.TypeParameter |
|
|
187
|
+
ts.TypeFlags.IndexedAccess |
|
|
188
|
+
ts.TypeFlags.Conditional |
|
|
189
|
+
ts.TypeFlags.Substitution)) !==
|
|
190
|
+
0) {
|
|
191
|
+
const constraint = checker.getBaseConstraintOfType(type);
|
|
192
|
+
return constraint !== undefined && mayBeArrayType(checker, constraint);
|
|
193
|
+
}
|
|
194
|
+
return checker.isArrayType(type) || checker.isTupleType(type);
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
|
|
134
200
|
|
|
135
201
|
|
|
136
202
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"prefer-is-record-and-has-key.mjs","sourceRoot":"","sources":["../../src/rules/prefer-is-record-and-has-key.mts"],"names":[],"mappings":"AAAA,OAAO,EACL,cAAc,GAGf,MAAM,0BAA0B,CAAC;AAClC,OAAO,EACL,mBAAmB,EACnB,eAAe,EACf,qBAAqB,EACrB,eAAe,EACf,gBAAgB,GACjB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AACzC,OAAO,KAAK,EAAE,MAAM,YAAY,CAAC;AACjC,OAAO,EAAE,iBAAiB,EAAE,qBAAqB,EAAE,MAAM,iBAAiB,CAAC;AAC3E,OAAO,EAAE,mBAAmB,EAAE,MAAM,iBAAiB,CAAC;AAMtD,MAAM,CAAC,MAAM,uBAAuB,GAClC;IACE,IAAI,EAAE;QACJ,IAAI,EAAE,YAAY;QAClB,IAAI,EAAE;YACJ,WAAW,EACT,8GAA8G;SACjH;QACD,OAAO,EAAE,MAAM;QACf,MAAM,EAAE,EAAE;QACV,QAAQ,EAAE;YACR,oBAAoB,EAClB,4GAA4G;YAC9G,SAAS,EACP,mFAAmF;SACtF;KACF;IAED,MAAM,EAAE,CAAC,OAAO,EAAE,EAAE;QAClB,MAAM,UAAU,GAAG,OAAO,CAAC,UAAU,CAAC;QAEtC,MAAM,OAAO,GAAG,UAAU,CAAC,GAAG,CAAC;QAE/B,MAAM,iBAAiB,GAAG,qBAAqB,CAC7C,OAAO,EACP,mBAAmB,CACpB,CAAC;QAEF;;;;;;;;WAQG;QACH,MAAM,aAAa,GAAG;QACpB,8EAA8E;QAC9E,UAA+B,EACtB,EAAE,CACX,qBAAqB,CAAC,UAAU,EAAE,UAAU,EAAE,CAAC,IAAI,EAAE,OAAO,EAAE,EAAE,CAC9D,mBAAmB,CAAC,OAAO,EAAE,IAAI,CAAC,CACnC,CAAC;QAEJ,MAAM,cAAc,GAId,EAAE,CAAC;QAET,OAAO;YACL,iCAAiC;YACjC,cAAc,EAAE,CAAC,IAAI,EAAE,EAAE;gBACvB,MAAM,MAAM,GAAG,gBAAgB,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;gBAE7C,IACE,MAAM,CAAC,IAAI,KAAK,cAAc,CAAC,gBAAgB;oBAC/C,CAAC,iBAAiB,CAAC,MAAM,CAAC,MAAM,EAAE,QAAQ,CAAC;oBAC3C,MAAM,CAAC,QAAQ,CAAC,IAAI,KAAK,cAAc,CAAC,UAAU;oBAClD,MAAM,CAAC,QAAQ,CAAC,IAAI,KAAK,QAAQ,EACjC,CAAC;oBACD,OAAO;gBACT,CAAC;gBAED,2CAA2C;gBAC3C,IAAI,IAAI,CAAC,SAAS,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;oBAChC,OAAO;gBACT,CAAC;gBAED,MAAM,MAAM,GAAG,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC;gBAEjC,MAAM,MAAM,GAAG,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC;gBAEjC,IAAI,MAAM,KAAK,SAAS,IAAI,MAAM,KAAK,SAAS,EAAE,CAAC;oBACjD,OAAO;gBACT,CAAC;gBAED,mEAAmE;gBACnE,IACE,MAAM,CAAC,IAAI,KAAK,cAAc,CAAC,aAAa;oBAC5C,MAAM,CAAC,IAAI,KAAK,cAAc,CAAC,aAAa,
|
|
1
|
+
{"version":3,"file":"prefer-is-record-and-has-key.mjs","sourceRoot":"","sources":["../../src/rules/prefer-is-record-and-has-key.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.10",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "ESLint rules that steer code toward ts-std-forge idioms.",
|
|
6
6
|
"keywords": [
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
],
|
|
36
36
|
"dependencies": {
|
|
37
37
|
"@typescript-eslint/utils": "^8.71.1",
|
|
38
|
-
"eslint-rule-forge": "^0.
|
|
38
|
+
"eslint-rule-forge": "^0.2.0",
|
|
39
39
|
"ts-std-forge": "0.5.1",
|
|
40
40
|
"ts-type-forge": "~9.2.2"
|
|
41
41
|
},
|
|
@@ -43,9 +43,9 @@
|
|
|
43
43
|
"@typescript-eslint/parser": "8.71.1",
|
|
44
44
|
"@typescript-eslint/rule-tester": "8.71.1",
|
|
45
45
|
"dedent": "1.7.2",
|
|
46
|
-
"eslint-config-typed": "5.
|
|
47
|
-
"eslint-plugin-ts-fortress": "1.3.
|
|
48
|
-
"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",
|
|
49
49
|
"prettier": "3.9.9",
|
|
50
50
|
"prettier-plugin-organize-imports": "4.3.0",
|
|
51
51
|
"prettier-plugin-packagejson": "3.0.2",
|
|
@@ -66,15 +66,15 @@
|
|
|
66
66
|
"scripts": {
|
|
67
67
|
"build": "tsx --tsconfig ../../tools/configs/tsconfig.tsx.json ./scripts/cmd/build.mts",
|
|
68
68
|
"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",
|
|
69
|
+
"check:lint": "node ../../tools/scripts/cmd/assert-deps-built.mts && oxlint . && eslint .",
|
|
70
|
+
"check:test": "node ../../tools/scripts/cmd/assert-deps-built.mts && pnpm run z:vitest run",
|
|
71
|
+
"check:test:cov": "node ../../tools/scripts/cmd/assert-deps-built.mts && pnpm run z:vitest run --coverage",
|
|
72
|
+
"check:types": "node ../../tools/scripts/cmd/assert-deps-built.mts && node ../../node_modules/typescript-native/bin/tsc --noEmit",
|
|
73
73
|
"doc": "pnpm run gen:readme",
|
|
74
74
|
"fix:fmt": "tsx --tsconfig ../../tools/configs/tsconfig.tsx.json ../../libs/ts-repo-utils/src/cmd/format-uncommitted.mts --cwd .",
|
|
75
75
|
"fix:fmt:diff": "tsx --tsconfig ../../tools/configs/tsconfig.tsx.json ../../libs/ts-repo-utils/src/cmd/format-diff-from.mts origin/main --cwd .",
|
|
76
76
|
"fix:fmt:full": "pnpm run z:prettier .",
|
|
77
|
-
"fix:lint": "oxlint . --fix && eslint . --fix",
|
|
77
|
+
"fix:lint": "node ../../tools/scripts/cmd/assert-deps-built.mts && oxlint . --fix && eslint . --fix",
|
|
78
78
|
"fmt": "pnpm run fix:fmt",
|
|
79
79
|
"gen": "run-s gen:rule-types gen:readme",
|
|
80
80
|
"gen:readme": "tsx --tsconfig ../../tools/configs/tsconfig.tsx.json ./scripts/cmd/embed-examples.mts",
|
package/src/rule-types.mts
CHANGED
|
@@ -9,7 +9,7 @@ import type { ReadonlyRecord } from 'ts-type-forge';
|
|
|
9
9
|
import type { tsStdForgeRules } from './rules/index.mjs';
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
|
-
* @description Replace `typeof u === "object" && u !== null
|
|
12
|
+
* @description Replace `typeof u === "object" && u !== null`, in either order and anywhere in an `&&` chain, with `isNonNullObject(u)` from ts-std-forge.
|
|
13
13
|
*
|
|
14
14
|
* ```md
|
|
15
15
|
* | key | value |
|
|
@@ -7,6 +7,8 @@ import {
|
|
|
7
7
|
buildNamedImportFix,
|
|
8
8
|
getImportDeclarations,
|
|
9
9
|
getNamedImports,
|
|
10
|
+
getRangeWithParens,
|
|
11
|
+
parenthesesDepth,
|
|
10
12
|
skipTypeWrappers,
|
|
11
13
|
} from 'eslint-rule-forge';
|
|
12
14
|
import { TS_STD_FORGE_MODULE } from './constants.mjs';
|
|
@@ -20,7 +22,7 @@ export const preferIsNonNullObject: TSESLint.RuleModule<MessageIds, Options> = {
|
|
|
20
22
|
type: 'suggestion',
|
|
21
23
|
docs: {
|
|
22
24
|
description:
|
|
23
|
-
'Replace `typeof u === "object" && u !== null
|
|
25
|
+
'Replace `typeof u === "object" && u !== null`, in either order and anywhere in an `&&` chain, with `isNonNullObject(u)` from ts-std-forge.',
|
|
24
26
|
},
|
|
25
27
|
fixable: 'code',
|
|
26
28
|
schema: [],
|
|
@@ -40,20 +42,16 @@ export const preferIsNonNullObject: TSESLint.RuleModule<MessageIds, Options> = {
|
|
|
40
42
|
TS_STD_FORGE_MODULE,
|
|
41
43
|
);
|
|
42
44
|
|
|
43
|
-
const
|
|
44
|
-
node: TSESTree.LogicalExpression;
|
|
45
|
-
identifierName: string;
|
|
46
|
-
}[] = [];
|
|
45
|
+
const mut_pairsToFix: NonNullObjectPair[] = [];
|
|
47
46
|
|
|
48
47
|
return {
|
|
49
48
|
LogicalExpression: (node) => {
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
if (identifierName === undefined) {
|
|
49
|
+
// A chain is read once, from its outermost `&&`.
|
|
50
|
+
if (node.operator !== '&&' || isChainOperand(sourceCode, node)) {
|
|
53
51
|
return;
|
|
54
52
|
}
|
|
55
53
|
|
|
56
|
-
|
|
54
|
+
mut_pairsToFix.push(...findNonNullObjectPairs(sourceCode, node));
|
|
57
55
|
},
|
|
58
56
|
'Program:exit': () => {
|
|
59
57
|
const hasIsNonNullObjectImport =
|
|
@@ -66,10 +64,13 @@ export const preferIsNonNullObject: TSESLint.RuleModule<MessageIds, Options> = {
|
|
|
66
64
|
// with it.
|
|
67
65
|
for (const [
|
|
68
66
|
index,
|
|
69
|
-
{
|
|
70
|
-
] of
|
|
67
|
+
{ range, identifierName },
|
|
68
|
+
] of mut_pairsToFix.entries()) {
|
|
71
69
|
context.report({
|
|
72
|
-
|
|
70
|
+
loc: {
|
|
71
|
+
start: sourceCode.getLocFromIndex(range[0]),
|
|
72
|
+
end: sourceCode.getLocFromIndex(range[1]),
|
|
73
|
+
},
|
|
73
74
|
messageId: 'useIsNonNullObject',
|
|
74
75
|
fix: (fixer) => {
|
|
75
76
|
const replacement = `isNonNullObject(${identifierName})`;
|
|
@@ -81,7 +82,10 @@ export const preferIsNonNullObject: TSESLint.RuleModule<MessageIds, Options> = {
|
|
|
81
82
|
])
|
|
82
83
|
: [];
|
|
83
84
|
|
|
84
|
-
return [
|
|
85
|
+
return [
|
|
86
|
+
...importFixes,
|
|
87
|
+
fixer.replaceTextRange(range, replacement),
|
|
88
|
+
];
|
|
85
89
|
},
|
|
86
90
|
});
|
|
87
91
|
}
|
|
@@ -91,41 +95,124 @@ export const preferIsNonNullObject: TSESLint.RuleModule<MessageIds, Options> = {
|
|
|
91
95
|
defaultOptions: [],
|
|
92
96
|
} as const;
|
|
93
97
|
|
|
94
|
-
|
|
95
|
-
|
|
98
|
+
/**
|
|
99
|
+
* A `typeof u === 'object'` and a `u !== null` next to each other in an `&&`
|
|
100
|
+
* chain, as the source range from the first to the second (parentheses
|
|
101
|
+
* included) and the name of `u`.
|
|
102
|
+
*/
|
|
103
|
+
type NonNullObjectPair = Readonly<{
|
|
104
|
+
range: TSESTree.Range;
|
|
105
|
+
identifierName: string;
|
|
106
|
+
}>;
|
|
96
107
|
|
|
97
|
-
|
|
108
|
+
/**
|
|
109
|
+
* Whether `node` is an operand of an enclosing `&&` chain: a `&&` written
|
|
110
|
+
* directly under another one, without parentheses.
|
|
111
|
+
*/
|
|
112
|
+
const isChainOperand = (
|
|
113
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
114
|
+
sourceCode: TSESLint.SourceCode,
|
|
98
115
|
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
99
116
|
node: TSESTree.LogicalExpression,
|
|
100
|
-
):
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
117
|
+
): boolean =>
|
|
118
|
+
node.parent.type === AST_NODE_TYPES.LogicalExpression &&
|
|
119
|
+
node.parent.operator === '&&' &&
|
|
120
|
+
parenthesesDepth(sourceCode, node) === 0;
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Every non-overlapping pair in the `&&` chain `node` heads, left to right. The
|
|
124
|
+
* two halves may come in either order, and the pair may have operands before
|
|
125
|
+
* and after it: `a && u !== null && typeof u === 'object' && b`. They have to
|
|
126
|
+
* be next to each other, so that the pair is one range of the source.
|
|
127
|
+
*/
|
|
128
|
+
const findNonNullObjectPairs = (
|
|
129
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
130
|
+
sourceCode: TSESLint.SourceCode,
|
|
131
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
132
|
+
node: TSESTree.LogicalExpression,
|
|
133
|
+
): readonly NonNullObjectPair[] => {
|
|
134
|
+
const operands = chainOperands(sourceCode, node);
|
|
135
|
+
|
|
136
|
+
const pairsFrom = (index: number): readonly NonNullObjectPair[] => {
|
|
137
|
+
const first = operands[index];
|
|
138
|
+
|
|
139
|
+
const second = operands[index + 1];
|
|
140
|
+
|
|
141
|
+
if (first === undefined || second === undefined) {
|
|
142
|
+
return [];
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const identifierName =
|
|
146
|
+
pairIdentifierName(first, second) ?? pairIdentifierName(second, first);
|
|
147
|
+
|
|
148
|
+
return identifierName === undefined
|
|
149
|
+
? pairsFrom(index + 1)
|
|
150
|
+
: [
|
|
151
|
+
{
|
|
152
|
+
range: [
|
|
153
|
+
getRangeWithParens(sourceCode, first)[0],
|
|
154
|
+
getRangeWithParens(sourceCode, second)[1],
|
|
155
|
+
],
|
|
156
|
+
identifierName,
|
|
157
|
+
},
|
|
158
|
+
...pairsFrom(index + 2),
|
|
159
|
+
];
|
|
160
|
+
};
|
|
104
161
|
|
|
105
|
-
|
|
106
|
-
|
|
162
|
+
return pairsFrom(0);
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* The operands of the `&&` chain `node`, left to right. A parenthesized `&&`
|
|
167
|
+
* is one operand: its halves cannot be replaced without its parentheses.
|
|
168
|
+
*/
|
|
169
|
+
const chainOperands = (
|
|
170
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
171
|
+
sourceCode: TSESLint.SourceCode,
|
|
172
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
173
|
+
node: TSESTree.LogicalExpression,
|
|
174
|
+
): readonly TSESTree.Expression[] =>
|
|
175
|
+
[node.left, node.right].flatMap((operand) =>
|
|
176
|
+
operand.type === AST_NODE_TYPES.LogicalExpression &&
|
|
177
|
+
operand.operator === '&&' &&
|
|
178
|
+
parenthesesDepth(sourceCode, operand) === 0
|
|
179
|
+
? chainOperands(sourceCode, operand)
|
|
180
|
+
: [operand],
|
|
107
181
|
);
|
|
108
182
|
|
|
109
|
-
|
|
110
|
-
|
|
183
|
+
// Every operand below is read through its type wrappers: `typeof (u satisfies
|
|
184
|
+
// unknown) === 'object' && u! !== null` is the same check.
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* The name of `u` when `typeofCheck` is `typeof u === 'object'` and
|
|
188
|
+
* `nullCheck` is `u !== null` for the same `u`.
|
|
189
|
+
*/
|
|
190
|
+
const pairIdentifierName = (
|
|
191
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
192
|
+
typeofCheck: TSESTree.Expression,
|
|
193
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
194
|
+
nullCheck: TSESTree.Expression,
|
|
195
|
+
): string | undefined => {
|
|
196
|
+
const identifierName = getTypeofObjectIdentifierName(
|
|
197
|
+
skipTypeWrappers(typeofCheck),
|
|
111
198
|
);
|
|
112
199
|
|
|
113
|
-
return
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
?
|
|
117
|
-
:
|
|
200
|
+
return identifierName !== undefined &&
|
|
201
|
+
identifierName ===
|
|
202
|
+
getNonNullCheckIdentifierName(skipTypeWrappers(nullCheck))
|
|
203
|
+
? identifierName
|
|
204
|
+
: undefined;
|
|
118
205
|
};
|
|
119
206
|
|
|
207
|
+
/** `u` of `typeof u === 'object'`, in either operand order. */
|
|
120
208
|
const getTypeofObjectIdentifierName = (
|
|
121
209
|
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
122
210
|
node: TSESTree.Node,
|
|
123
211
|
): string | undefined => {
|
|
124
|
-
if (
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
if (node.operator !== '===') {
|
|
212
|
+
if (
|
|
213
|
+
node.type !== AST_NODE_TYPES.BinaryExpression ||
|
|
214
|
+
node.operator !== '==='
|
|
215
|
+
) {
|
|
129
216
|
return undefined;
|
|
130
217
|
}
|
|
131
218
|
|
|
@@ -133,31 +220,39 @@ const getTypeofObjectIdentifierName = (
|
|
|
133
220
|
|
|
134
221
|
const right = skipTypeWrappers(node.right);
|
|
135
222
|
|
|
223
|
+
const typeofSide = isLiteralOf(right, 'object')
|
|
224
|
+
? left
|
|
225
|
+
: isLiteralOf(left, 'object')
|
|
226
|
+
? right
|
|
227
|
+
: undefined;
|
|
228
|
+
|
|
136
229
|
if (
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
right.type !== AST_NODE_TYPES.Literal ||
|
|
140
|
-
right.value !== 'object'
|
|
230
|
+
typeofSide?.type !== AST_NODE_TYPES.UnaryExpression ||
|
|
231
|
+
typeofSide.operator !== 'typeof'
|
|
141
232
|
) {
|
|
142
233
|
return undefined;
|
|
143
234
|
}
|
|
144
235
|
|
|
145
|
-
const argument = skipTypeWrappers(
|
|
236
|
+
const argument = skipTypeWrappers(typeofSide.argument);
|
|
146
237
|
|
|
147
238
|
return argument.type === AST_NODE_TYPES.Identifier
|
|
148
239
|
? argument.name
|
|
149
240
|
: undefined;
|
|
150
241
|
};
|
|
151
242
|
|
|
243
|
+
/**
|
|
244
|
+
* `u` of `u !== null`, in either operand order. `u != null` also excludes
|
|
245
|
+
* `undefined`, which `typeof u === 'object'` has already excluded, so it is
|
|
246
|
+
* the same check here.
|
|
247
|
+
*/
|
|
152
248
|
const getNonNullCheckIdentifierName = (
|
|
153
249
|
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
154
250
|
node: TSESTree.Node,
|
|
155
251
|
): string | undefined => {
|
|
156
|
-
if (
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
if (node.operator !== '!==') {
|
|
252
|
+
if (
|
|
253
|
+
node.type !== AST_NODE_TYPES.BinaryExpression ||
|
|
254
|
+
(node.operator !== '!==' && node.operator !== '!=')
|
|
255
|
+
) {
|
|
161
256
|
return undefined;
|
|
162
257
|
}
|
|
163
258
|
|
|
@@ -165,9 +260,17 @@ const getNonNullCheckIdentifierName = (
|
|
|
165
260
|
|
|
166
261
|
const right = skipTypeWrappers(node.right);
|
|
167
262
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
263
|
+
const checked = isLiteralOf(right, null)
|
|
264
|
+
? left
|
|
265
|
+
: isLiteralOf(left, null)
|
|
266
|
+
? right
|
|
267
|
+
: undefined;
|
|
268
|
+
|
|
269
|
+
return checked?.type === AST_NODE_TYPES.Identifier ? checked.name : undefined;
|
|
173
270
|
};
|
|
271
|
+
|
|
272
|
+
const isLiteralOf = (
|
|
273
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
274
|
+
node: TSESTree.Node,
|
|
275
|
+
value: string | null,
|
|
276
|
+
): boolean => node.type === AST_NODE_TYPES.Literal && node.value === value;
|
|
@@ -106,3 +106,110 @@ describe('prefer-is-non-null-object through type wrappers', () => {
|
|
|
106
106
|
],
|
|
107
107
|
});
|
|
108
108
|
});
|
|
109
|
+
|
|
110
|
+
describe('prefer-is-non-null-object in other forms', () => {
|
|
111
|
+
ruleTester.run('prefer-is-non-null-object', preferIsNonNullObject, {
|
|
112
|
+
valid: [
|
|
113
|
+
{
|
|
114
|
+
name: 'the two halves joined by `||`',
|
|
115
|
+
code: dedent`
|
|
116
|
+
const ok = typeof u === 'object' || u !== null;
|
|
117
|
+
`,
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
name: 'the two halves not next to each other in the chain',
|
|
121
|
+
code: dedent`
|
|
122
|
+
declare const a: boolean;
|
|
123
|
+
const ok = typeof u === 'object' && a && u !== null;
|
|
124
|
+
`,
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
name: 'an undefined check is not a null check',
|
|
128
|
+
code: dedent`
|
|
129
|
+
const ok = u !== undefined && typeof u === 'object';
|
|
130
|
+
`,
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
name: 'the halves split by parentheses',
|
|
134
|
+
code: dedent`
|
|
135
|
+
declare const b: boolean;
|
|
136
|
+
const ok = typeof u === 'object' && (u !== null && b);
|
|
137
|
+
`,
|
|
138
|
+
},
|
|
139
|
+
],
|
|
140
|
+
invalid: [
|
|
141
|
+
{
|
|
142
|
+
name: 'the halves in reverse order',
|
|
143
|
+
code: dedent`
|
|
144
|
+
const ok = u !== null && typeof u === 'object';
|
|
145
|
+
`,
|
|
146
|
+
output: dedent`
|
|
147
|
+
import { isNonNullObject } from 'ts-std-forge';
|
|
148
|
+
const ok = isNonNullObject(u);
|
|
149
|
+
`,
|
|
150
|
+
errors: [{ messageId: 'useIsNonNullObject' }],
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
name: 'each comparison with its operands swapped',
|
|
154
|
+
code: dedent`
|
|
155
|
+
const ok = 'object' === typeof u && null !== u;
|
|
156
|
+
`,
|
|
157
|
+
output: dedent`
|
|
158
|
+
import { isNonNullObject } from 'ts-std-forge';
|
|
159
|
+
const ok = isNonNullObject(u);
|
|
160
|
+
`,
|
|
161
|
+
errors: [{ messageId: 'useIsNonNullObject' }],
|
|
162
|
+
},
|
|
163
|
+
{
|
|
164
|
+
name: '`!= null`, which typeof has already made the same check',
|
|
165
|
+
code: dedent`
|
|
166
|
+
const ok = typeof u === 'object' && u != null;
|
|
167
|
+
`,
|
|
168
|
+
output: dedent`
|
|
169
|
+
import { isNonNullObject } from 'ts-std-forge';
|
|
170
|
+
const ok = isNonNullObject(u);
|
|
171
|
+
`,
|
|
172
|
+
errors: [{ messageId: 'useIsNonNullObject' }],
|
|
173
|
+
},
|
|
174
|
+
{
|
|
175
|
+
name: 'the pair at the start, the end and the middle of a longer chain',
|
|
176
|
+
code: dedent`
|
|
177
|
+
declare const a: boolean;
|
|
178
|
+
declare const b: boolean;
|
|
179
|
+
const x = typeof u === 'object' && u !== null && 'k' in u;
|
|
180
|
+
const y = a && typeof u === 'object' && u !== null;
|
|
181
|
+
const z = a && u !== null && (typeof u === 'object') && b;
|
|
182
|
+
`,
|
|
183
|
+
output: dedent`
|
|
184
|
+
import { isNonNullObject } from 'ts-std-forge';
|
|
185
|
+
declare const a: boolean;
|
|
186
|
+
declare const b: boolean;
|
|
187
|
+
const x = isNonNullObject(u) && 'k' in u;
|
|
188
|
+
const y = a && isNonNullObject(u);
|
|
189
|
+
const z = a && isNonNullObject(u) && b;
|
|
190
|
+
`,
|
|
191
|
+
errors: [
|
|
192
|
+
{ messageId: 'useIsNonNullObject' },
|
|
193
|
+
{ messageId: 'useIsNonNullObject' },
|
|
194
|
+
{ messageId: 'useIsNonNullObject' },
|
|
195
|
+
],
|
|
196
|
+
},
|
|
197
|
+
{
|
|
198
|
+
name: 'two pairs in one chain',
|
|
199
|
+
code: dedent`
|
|
200
|
+
declare const v: unknown;
|
|
201
|
+
const ok = typeof u === 'object' && u !== null && v !== null && typeof v === 'object';
|
|
202
|
+
`,
|
|
203
|
+
output: dedent`
|
|
204
|
+
import { isNonNullObject } from 'ts-std-forge';
|
|
205
|
+
declare const v: unknown;
|
|
206
|
+
const ok = isNonNullObject(u) && isNonNullObject(v);
|
|
207
|
+
`,
|
|
208
|
+
errors: [
|
|
209
|
+
{ messageId: 'useIsNonNullObject' },
|
|
210
|
+
{ messageId: 'useIsNonNullObject' },
|
|
211
|
+
],
|
|
212
|
+
},
|
|
213
|
+
],
|
|
214
|
+
});
|
|
215
|
+
});
|
|
@@ -64,6 +64,24 @@ export const preferIsRecordAndHasKey: TSESLint.RuleModule<MessageIds, Options> =
|
|
|
64
64
|
isAlreadyRecordType(checker, type),
|
|
65
65
|
);
|
|
66
66
|
|
|
67
|
+
/**
|
|
68
|
+
* Whether the object may be an array at some layer of type wrappers.
|
|
69
|
+
* `isRecord` rejects an array, so `'0' in xs` would turn from `true` to
|
|
70
|
+
* `false`; a widening cast to a type that may be an array is the author
|
|
71
|
+
* saying it may be one. Without type information nothing is known, and
|
|
72
|
+
* the rule reports as it always has.
|
|
73
|
+
*/
|
|
74
|
+
const mayBeArray = (
|
|
75
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
76
|
+
expression: TSESTree.Expression,
|
|
77
|
+
): boolean =>
|
|
78
|
+
sourceCode.parserServices?.program != null &&
|
|
79
|
+
!typeHoldsAtEveryLayer(
|
|
80
|
+
sourceCode,
|
|
81
|
+
expression,
|
|
82
|
+
(type, checker) => !mayBeArrayType(checker, type),
|
|
83
|
+
);
|
|
84
|
+
|
|
67
85
|
const mut_nodesToFix: {
|
|
68
86
|
node: TSESTree.CallExpression | TSESTree.BinaryExpression;
|
|
69
87
|
objExpression: TSESTree.Expression;
|
|
@@ -100,7 +118,8 @@ export const preferIsRecordAndHasKey: TSESLint.RuleModule<MessageIds, Options> =
|
|
|
100
118
|
// Type guard: ensure arguments are Expressions (not SpreadElement)
|
|
101
119
|
if (
|
|
102
120
|
objArg.type === AST_NODE_TYPES.SpreadElement ||
|
|
103
|
-
keyArg.type === AST_NODE_TYPES.SpreadElement
|
|
121
|
+
keyArg.type === AST_NODE_TYPES.SpreadElement ||
|
|
122
|
+
mayBeArray(objArg)
|
|
104
123
|
) {
|
|
105
124
|
return;
|
|
106
125
|
}
|
|
@@ -123,7 +142,10 @@ export const preferIsRecordAndHasKey: TSESLint.RuleModule<MessageIds, Options> =
|
|
|
123
142
|
const objExpression = node.right;
|
|
124
143
|
|
|
125
144
|
// Type guard: ensure keyExpression is Expression (not PrivateIdentifier)
|
|
126
|
-
if (
|
|
145
|
+
if (
|
|
146
|
+
keyExpression.type === AST_NODE_TYPES.PrivateIdentifier ||
|
|
147
|
+
mayBeArray(objExpression)
|
|
148
|
+
) {
|
|
127
149
|
return;
|
|
128
150
|
}
|
|
129
151
|
|
|
@@ -137,25 +159,40 @@ export const preferIsRecordAndHasKey: TSESLint.RuleModule<MessageIds, Options> =
|
|
|
137
159
|
'Program:exit': () => {
|
|
138
160
|
const namedImports = getNamedImports(tsStdForgeImports);
|
|
139
161
|
|
|
140
|
-
const rewrites = mut_nodesToFix.map((entry) =>
|
|
141
|
-
...entry,
|
|
162
|
+
const rewrites = mut_nodesToFix.map((entry) => {
|
|
142
163
|
// A record-typed object makes the `isRecord(...)` conjunct always
|
|
143
164
|
// true, so it is left out of the replacement entirely.
|
|
144
|
-
needsIsRecord
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
165
|
+
const needsIsRecord = !isKnownRecord(entry.objExpression);
|
|
166
|
+
|
|
167
|
+
return {
|
|
168
|
+
...entry,
|
|
169
|
+
needsIsRecord,
|
|
170
|
+
// `isRecord(obj) && hasKey(obj, key)` evaluates `obj` twice.
|
|
171
|
+
fixable: !needsIsRecord || isSideEffectFree(entry.objExpression),
|
|
172
|
+
};
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
const fixableRewrites = rewrites.filter(({ fixable }) => fixable);
|
|
176
|
+
|
|
177
|
+
const importsToAdd: readonly string[] = SafeArray.isEmpty(
|
|
178
|
+
fixableRewrites,
|
|
179
|
+
)
|
|
180
|
+
? []
|
|
181
|
+
: [
|
|
182
|
+
...(fixableRewrites.some(
|
|
183
|
+
({ needsIsRecord }) => needsIsRecord,
|
|
184
|
+
) && !namedImports.includes('isRecord')
|
|
185
|
+
? ['isRecord']
|
|
186
|
+
: []),
|
|
187
|
+
...(namedImports.includes('hasKey') ? [] : ['hasKey']),
|
|
188
|
+
];
|
|
189
|
+
|
|
190
|
+
const importCarrier = fixableRewrites[0];
|
|
191
|
+
|
|
192
|
+
for (const rewrite of rewrites) {
|
|
193
|
+
const { node, objExpression, keyExpression, needsIsRecord } =
|
|
194
|
+
rewrite;
|
|
195
|
+
|
|
159
196
|
const objText = getArgumentText(sourceCode, objExpression);
|
|
160
197
|
|
|
161
198
|
const keyText = getArgumentText(sourceCode, keyExpression);
|
|
@@ -170,26 +207,32 @@ export const preferIsRecordAndHasKey: TSESLint.RuleModule<MessageIds, Options> =
|
|
|
170
207
|
objName: objText,
|
|
171
208
|
keyName: keyText,
|
|
172
209
|
},
|
|
173
|
-
fix:
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
210
|
+
fix: !rewrite.fixable
|
|
211
|
+
? undefined
|
|
212
|
+
: (fixer) => {
|
|
213
|
+
const replacement = needsIsRecord
|
|
214
|
+
? `(isRecord(${objText}) && hasKey(${objText}, ${keyText}))`
|
|
215
|
+
: `hasKey(${objText}, ${keyText})`;
|
|
216
|
+
|
|
217
|
+
// All import fixes have to live on the first fixable
|
|
218
|
+
// report: `insertTextBefore(program, …)` produces
|
|
219
|
+
// overlapping ranges otherwise and only one of them would
|
|
220
|
+
// survive a pass.
|
|
221
|
+
const importFixes =
|
|
222
|
+
rewrite === importCarrier && importsToAdd.length > 0
|
|
223
|
+
? buildNamedImportFix(
|
|
224
|
+
fixer,
|
|
225
|
+
program,
|
|
226
|
+
TS_STD_FORGE_MODULE,
|
|
227
|
+
importsToAdd,
|
|
228
|
+
)
|
|
229
|
+
: [];
|
|
230
|
+
|
|
231
|
+
return [
|
|
232
|
+
...importFixes,
|
|
233
|
+
fixer.replaceText(node, replacement),
|
|
234
|
+
];
|
|
235
|
+
},
|
|
193
236
|
});
|
|
194
237
|
}
|
|
195
238
|
},
|
|
@@ -198,6 +241,56 @@ export const preferIsRecordAndHasKey: TSESLint.RuleModule<MessageIds, Options> =
|
|
|
198
241
|
defaultOptions: [],
|
|
199
242
|
} as const;
|
|
200
243
|
|
|
244
|
+
/**
|
|
245
|
+
* Whether reading `node` again has no side effects: a variable, `this`, or a
|
|
246
|
+
* non-computed property chain on one, type wrappers aside.
|
|
247
|
+
*/
|
|
248
|
+
const isSideEffectFree = (
|
|
249
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
250
|
+
wrapped: TSESTree.Node,
|
|
251
|
+
): boolean => {
|
|
252
|
+
const node = skipTypeWrappers(wrapped);
|
|
253
|
+
|
|
254
|
+
return (
|
|
255
|
+
node.type === AST_NODE_TYPES.Identifier ||
|
|
256
|
+
node.type === AST_NODE_TYPES.ThisExpression ||
|
|
257
|
+
(node.type === AST_NODE_TYPES.MemberExpression &&
|
|
258
|
+
!node.computed &&
|
|
259
|
+
isSideEffectFree(node.object))
|
|
260
|
+
);
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Whether a value of `type` may be an array or a tuple: some member of a union
|
|
265
|
+
* or an intersection is one, or the constraint of a deferred type is.
|
|
266
|
+
* `object` and `unknown` say nothing either way and are not counted.
|
|
267
|
+
*/
|
|
268
|
+
const mayBeArrayType = (
|
|
269
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
270
|
+
checker: ts.TypeChecker,
|
|
271
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
272
|
+
type: ts.Type,
|
|
273
|
+
): boolean => {
|
|
274
|
+
if (type.isUnionOrIntersection()) {
|
|
275
|
+
return type.types.some((member) => mayBeArrayType(checker, member));
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
if (
|
|
279
|
+
(type.flags &
|
|
280
|
+
(ts.TypeFlags.TypeParameter |
|
|
281
|
+
ts.TypeFlags.IndexedAccess |
|
|
282
|
+
ts.TypeFlags.Conditional |
|
|
283
|
+
ts.TypeFlags.Substitution)) !==
|
|
284
|
+
0
|
|
285
|
+
) {
|
|
286
|
+
const constraint = checker.getBaseConstraintOfType(type);
|
|
287
|
+
|
|
288
|
+
return constraint !== undefined && mayBeArrayType(checker, constraint);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
return checker.isArrayType(type) || checker.isTupleType(type);
|
|
292
|
+
};
|
|
293
|
+
|
|
201
294
|
/**
|
|
202
295
|
* Whether `type` already satisfies `hasKey`'s `R extends UnknownRecord`
|
|
203
296
|
* constraint, so that guarding the call with `isRecord` adds nothing.
|
|
@@ -83,21 +83,6 @@ describe('prefer-is-record-and-has-key through type wrappers', () => {
|
|
|
83
83
|
tester.run('prefer-is-record-and-has-key', preferIsRecordAndHasKey, {
|
|
84
84
|
valid: [],
|
|
85
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
86
|
{
|
|
102
87
|
name: 'keeps isRecord for an unknown value cast to a record in `in`',
|
|
103
88
|
code: dedent`
|
|
@@ -138,19 +123,6 @@ describe('prefer-is-record-and-has-key through type wrappers', () => {
|
|
|
138
123
|
`,
|
|
139
124
|
errors: [{ messageId: 'useIsRecordAndHasKey' }],
|
|
140
125
|
},
|
|
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
126
|
{
|
|
155
127
|
name: 'drops isRecord for a record that is a record at every layer',
|
|
156
128
|
code: dedent`
|
|
@@ -186,34 +158,39 @@ describe('prefer-is-record-and-has-key with parenthesized operands', () => {
|
|
|
186
158
|
valid: [],
|
|
187
159
|
invalid: [
|
|
188
160
|
{
|
|
189
|
-
name: '
|
|
161
|
+
name: 'a sequence object in `in`, read twice by the fix, gets no fix',
|
|
190
162
|
code: dedent`
|
|
191
163
|
declare const a: number;
|
|
192
164
|
declare const o: unknown;
|
|
193
165
|
const ok = 'k' in (a, o);
|
|
194
166
|
`,
|
|
195
|
-
output:
|
|
196
|
-
|
|
167
|
+
output: null,
|
|
168
|
+
errors: [{ messageId: 'useIsRecordAndHasKey' }],
|
|
169
|
+
},
|
|
170
|
+
{
|
|
171
|
+
name: 'a sequence object in Object.hasOwn, read twice by the fix, gets no fix',
|
|
172
|
+
code: dedent`
|
|
197
173
|
declare const a: number;
|
|
198
174
|
declare const o: unknown;
|
|
199
|
-
const ok = (
|
|
175
|
+
const ok = Object.hasOwn((a, o), 'k');
|
|
200
176
|
`,
|
|
177
|
+
output: null,
|
|
201
178
|
errors: [{ messageId: 'useIsRecordAndHasKey' }],
|
|
202
179
|
},
|
|
203
180
|
{
|
|
204
|
-
name: 'keeps the parentheses of a sequence object
|
|
181
|
+
name: 'keeps the parentheses of a sequence object read once',
|
|
205
182
|
code: dedent`
|
|
206
183
|
declare const a: number;
|
|
207
|
-
declare const o: unknown
|
|
208
|
-
const ok =
|
|
184
|
+
declare const o: Readonly<Record<string, unknown>>;
|
|
185
|
+
const ok = 'k' in (a, o);
|
|
209
186
|
`,
|
|
210
187
|
output: dedent`
|
|
211
|
-
import {
|
|
188
|
+
import { hasKey } from 'ts-std-forge';
|
|
212
189
|
declare const a: number;
|
|
213
|
-
declare const o: unknown
|
|
214
|
-
const ok =
|
|
190
|
+
declare const o: Readonly<Record<string, unknown>>;
|
|
191
|
+
const ok = hasKey((a, o), 'k');
|
|
215
192
|
`,
|
|
216
|
-
errors: [{ messageId: '
|
|
193
|
+
errors: [{ messageId: 'useHasKey' }],
|
|
217
194
|
},
|
|
218
195
|
{
|
|
219
196
|
name: 'keeps the parentheses of a sequence key',
|
|
@@ -233,3 +210,86 @@ describe('prefer-is-record-and-has-key with parenthesized operands', () => {
|
|
|
233
210
|
],
|
|
234
211
|
});
|
|
235
212
|
});
|
|
213
|
+
|
|
214
|
+
describe('prefer-is-record-and-has-key where the rewrite would change behavior', () => {
|
|
215
|
+
tester.run('prefer-is-record-and-has-key', preferIsRecordAndHasKey, {
|
|
216
|
+
valid: [
|
|
217
|
+
{
|
|
218
|
+
// `isRecord` rejects an array, so `'0' in xs` would turn false.
|
|
219
|
+
name: 'an array',
|
|
220
|
+
code: dedent`
|
|
221
|
+
declare const xs: readonly number[];
|
|
222
|
+
const a = '0' in xs;
|
|
223
|
+
const b = Object.hasOwn(xs, 'length');
|
|
224
|
+
`,
|
|
225
|
+
},
|
|
226
|
+
{
|
|
227
|
+
name: 'a union with an array',
|
|
228
|
+
code: dedent`
|
|
229
|
+
declare const v: Readonly<Record<string, unknown>> | readonly number[];
|
|
230
|
+
const ok = 'k' in v;
|
|
231
|
+
`,
|
|
232
|
+
},
|
|
233
|
+
{
|
|
234
|
+
name: 'an array cast to a record',
|
|
235
|
+
code: dedent`
|
|
236
|
+
declare const arr: readonly string[];
|
|
237
|
+
const ok = Object.hasOwn(arr as unknown as Record<string, unknown>, '0');
|
|
238
|
+
`,
|
|
239
|
+
},
|
|
240
|
+
{
|
|
241
|
+
name: 'a record widened by `as` to one that may be an array: the author says it may be one',
|
|
242
|
+
code: dedent`
|
|
243
|
+
declare const obj: Readonly<Record<string, unknown>>;
|
|
244
|
+
const ok = Object.hasOwn(obj as Readonly<Record<string, unknown>> | readonly string[], 'a');
|
|
245
|
+
`,
|
|
246
|
+
},
|
|
247
|
+
],
|
|
248
|
+
invalid: [
|
|
249
|
+
{
|
|
250
|
+
// `isRecord(…) && hasKey(…)` would evaluate the object twice.
|
|
251
|
+
name: 'an object with side effects is reported without a fix',
|
|
252
|
+
code: dedent`
|
|
253
|
+
declare const getObj: () => object;
|
|
254
|
+
declare const log: () => void;
|
|
255
|
+
declare const o: object;
|
|
256
|
+
const a = 'k' in getObj();
|
|
257
|
+
const b = Object.hasOwn(getObj(), 'k');
|
|
258
|
+
const c = 'k' in (log(), o);
|
|
259
|
+
`,
|
|
260
|
+
output: null,
|
|
261
|
+
errors: [
|
|
262
|
+
{ messageId: 'useIsRecordAndHasKey' },
|
|
263
|
+
{ messageId: 'useIsRecordAndHasKey' },
|
|
264
|
+
{ messageId: 'useIsRecordAndHasKey' },
|
|
265
|
+
],
|
|
266
|
+
},
|
|
267
|
+
{
|
|
268
|
+
name: 'an object with side effects is fixed when isRecord is dropped: it is read once',
|
|
269
|
+
code: dedent`
|
|
270
|
+
declare const getRec: () => Readonly<Record<string, unknown>>;
|
|
271
|
+
const ok = 'k' in getRec();
|
|
272
|
+
`,
|
|
273
|
+
output: dedent`
|
|
274
|
+
import { hasKey } from 'ts-std-forge';
|
|
275
|
+
declare const getRec: () => Readonly<Record<string, unknown>>;
|
|
276
|
+
const ok = hasKey(getRec(), 'k');
|
|
277
|
+
`,
|
|
278
|
+
errors: [{ messageId: 'useHasKey' }],
|
|
279
|
+
},
|
|
280
|
+
{
|
|
281
|
+
name: 'a reference read through type wrappers is still read without side effects',
|
|
282
|
+
code: dedent`
|
|
283
|
+
declare const o: { readonly p: object };
|
|
284
|
+
const ok = 'k' in (o.p satisfies object);
|
|
285
|
+
`,
|
|
286
|
+
output: dedent`
|
|
287
|
+
import { isRecord, hasKey } from 'ts-std-forge';
|
|
288
|
+
declare const o: { readonly p: object };
|
|
289
|
+
const ok = (isRecord(o.p satisfies object) && hasKey(o.p satisfies object, 'k'));
|
|
290
|
+
`,
|
|
291
|
+
errors: [{ messageId: 'useIsRecordAndHasKey' }],
|
|
292
|
+
},
|
|
293
|
+
],
|
|
294
|
+
});
|
|
295
|
+
});
|