eslint-plugin-ts-data-forge 0.8.6 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +20 -19
- package/dist/plugin.d.mts +2 -0
- package/dist/plugin.d.mts.map +1 -1
- package/dist/plugin.mjs +1 -0
- package/dist/plugin.mjs.map +1 -1
- package/dist/rule-types.d.mts +16 -1
- package/dist/rule-types.d.mts.map +1 -1
- package/dist/rule-types.mjs.map +1 -1
- package/dist/rules/ast-utils.d.mts +21 -2
- package/dist/rules/ast-utils.d.mts.map +1 -1
- package/dist/rules/ast-utils.mjs +51 -4
- package/dist/rules/ast-utils.mjs.map +1 -1
- package/dist/rules/constants.d.mts +2 -0
- package/dist/rules/constants.d.mts.map +1 -1
- package/dist/rules/constants.mjs +9 -0
- package/dist/rules/constants.mjs.map +1 -1
- package/dist/rules/mutable-result.d.mts +10 -0
- package/dist/rules/mutable-result.d.mts.map +1 -0
- package/dist/rules/mutable-result.mjs +145 -0
- package/dist/rules/mutable-result.mjs.map +1 -0
- package/dist/rules/prefer-arr-generate.d.mts +57 -0
- package/dist/rules/prefer-arr-generate.d.mts.map +1 -0
- package/dist/rules/prefer-arr-generate.mjs +588 -0
- package/dist/rules/prefer-arr-generate.mjs.map +1 -0
- package/dist/rules/prefer-arr-uniq.d.mts.map +1 -1
- package/dist/rules/prefer-arr-uniq.mjs +2 -149
- package/dist/rules/prefer-arr-uniq.mjs.map +1 -1
- package/dist/rules/prefer-canonical-array-slicing.d.mts.map +1 -1
- package/dist/rules/prefer-canonical-array-slicing.mjs +47 -28
- package/dist/rules/prefer-canonical-array-slicing.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 +110 -40
- 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 +102 -45
- package/dist/rules/prefer-is-record-and-has-key.mjs.map +1 -1
- package/dist/rules/prefer-range-for-loop.d.mts.map +1 -1
- package/dist/rules/prefer-range-for-loop.mjs +97 -16
- package/dist/rules/prefer-range-for-loop.mjs.map +1 -1
- package/dist/rules/rules.d.mts +1 -0
- package/dist/rules/rules.d.mts.map +1 -1
- package/dist/rules/rules.mjs +2 -0
- package/dist/rules/rules.mjs.map +1 -1
- package/package.json +11 -11
- package/src/plugin.mts +1 -0
- package/src/rule-types.mts +17 -1
- package/src/rules/ast-utils.mts +72 -5
- package/src/rules/constants.mts +12 -0
- package/src/rules/mutable-result.mts +204 -0
- package/src/rules/prefer-arr-generate.mts +918 -0
- package/src/rules/prefer-arr-generate.test.mts +967 -0
- package/src/rules/prefer-arr-is-bounded-length-array.test.mts +19 -0
- package/src/rules/prefer-arr-is-max-length-array.test.mts +8 -0
- package/src/rules/prefer-arr-is-min-length-array.test.mts +8 -0
- package/src/rules/prefer-arr-uniq.mts +2 -207
- package/src/rules/prefer-canonical-array-slicing.mts +57 -40
- package/src/rules/prefer-canonical-array-slicing.test.mts +55 -0
- package/src/rules/prefer-is-non-null-object.mts +160 -57
- package/src/rules/prefer-is-non-null-object.test.mts +138 -0
- package/src/rules/prefer-is-record-and-has-key.mts +148 -67
- package/src/rules/prefer-is-record-and-has-key.test.mts +118 -23
- package/src/rules/prefer-range-for-loop.mts +142 -21
- package/src/rules/prefer-range-for-loop.test.mts +127 -5
- package/src/rules/rules.mts +2 -0
|
@@ -5,12 +5,14 @@ import {
|
|
|
5
5
|
} from '@typescript-eslint/utils';
|
|
6
6
|
import {
|
|
7
7
|
buildNamedImportFix,
|
|
8
|
+
getArgumentText,
|
|
8
9
|
getImportDeclarations,
|
|
9
10
|
getNamedImports,
|
|
10
|
-
|
|
11
|
+
skipTypeWrappers,
|
|
11
12
|
} from 'eslint-rule-forge';
|
|
12
13
|
import { Arr } from 'ts-data-forge';
|
|
13
14
|
import * as ts from 'typescript';
|
|
15
|
+
import { isIdentifierNamed, typeHoldsAtEveryLayer } from './ast-utils.mjs';
|
|
14
16
|
import { TS_DATA_FORGE_MODULE } from './constants.mjs';
|
|
15
17
|
|
|
16
18
|
type Options = readonly [];
|
|
@@ -45,35 +47,40 @@ export const preferIsRecordAndHasKey: TSESLint.RuleModule<MessageIds, Options> =
|
|
|
45
47
|
TS_DATA_FORGE_MODULE,
|
|
46
48
|
);
|
|
47
49
|
|
|
48
|
-
const services = sourceCode.parserServices;
|
|
49
|
-
|
|
50
|
-
const checker = services?.program?.getTypeChecker();
|
|
51
|
-
|
|
52
50
|
/**
|
|
53
51
|
* Whether the `isRecord(...)` half of the rewrite would be dead code
|
|
54
52
|
* because the object already satisfies `hasKey`'s
|
|
55
|
-
* `R extends UnknownRecord` constraint
|
|
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
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
62
|
+
): boolean =>
|
|
63
|
+
typeHoldsAtEveryLayer(sourceCode, expression, (type, checker) =>
|
|
64
|
+
isAlreadyRecordType(checker, type),
|
|
65
|
+
);
|
|
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
|
+
);
|
|
77
84
|
|
|
78
85
|
const mut_nodesToFix: {
|
|
79
86
|
node: TSESTree.CallExpression | TSESTree.BinaryExpression;
|
|
@@ -84,12 +91,13 @@ export const preferIsRecordAndHasKey: TSESLint.RuleModule<MessageIds, Options> =
|
|
|
84
91
|
return {
|
|
85
92
|
// Handle Object.hasOwn(obj, key)
|
|
86
93
|
CallExpression: (node) => {
|
|
94
|
+
const callee = skipTypeWrappers(node.callee);
|
|
95
|
+
|
|
87
96
|
if (
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
node.callee.property.name !== 'hasOwn'
|
|
97
|
+
callee.type !== AST_NODE_TYPES.MemberExpression ||
|
|
98
|
+
!isIdentifierNamed(callee.object, 'Object') ||
|
|
99
|
+
callee.property.type !== AST_NODE_TYPES.Identifier ||
|
|
100
|
+
callee.property.name !== 'hasOwn'
|
|
93
101
|
) {
|
|
94
102
|
return;
|
|
95
103
|
}
|
|
@@ -110,7 +118,8 @@ export const preferIsRecordAndHasKey: TSESLint.RuleModule<MessageIds, Options> =
|
|
|
110
118
|
// Type guard: ensure arguments are Expressions (not SpreadElement)
|
|
111
119
|
if (
|
|
112
120
|
objArg.type === AST_NODE_TYPES.SpreadElement ||
|
|
113
|
-
keyArg.type === AST_NODE_TYPES.SpreadElement
|
|
121
|
+
keyArg.type === AST_NODE_TYPES.SpreadElement ||
|
|
122
|
+
mayBeArray(objArg)
|
|
114
123
|
) {
|
|
115
124
|
return;
|
|
116
125
|
}
|
|
@@ -133,7 +142,10 @@ export const preferIsRecordAndHasKey: TSESLint.RuleModule<MessageIds, Options> =
|
|
|
133
142
|
const objExpression = node.right;
|
|
134
143
|
|
|
135
144
|
// Type guard: ensure keyExpression is Expression (not PrivateIdentifier)
|
|
136
|
-
if (
|
|
145
|
+
if (
|
|
146
|
+
keyExpression.type === AST_NODE_TYPES.PrivateIdentifier ||
|
|
147
|
+
mayBeArray(objExpression)
|
|
148
|
+
) {
|
|
137
149
|
return;
|
|
138
150
|
}
|
|
139
151
|
|
|
@@ -147,28 +159,41 @@ export const preferIsRecordAndHasKey: TSESLint.RuleModule<MessageIds, Options> =
|
|
|
147
159
|
'Program:exit': () => {
|
|
148
160
|
const namedImports = getNamedImports(tsDataForgeImports);
|
|
149
161
|
|
|
150
|
-
const rewrites = mut_nodesToFix.map((entry) =>
|
|
151
|
-
...entry,
|
|
162
|
+
const rewrites = mut_nodesToFix.map((entry) => {
|
|
152
163
|
// A record-typed object makes the `isRecord(...)` conjunct always
|
|
153
164
|
// true, so it is left out of the replacement entirely.
|
|
154
|
-
needsIsRecord
|
|
155
|
-
|
|
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[] = Arr.isEmpty(fixableRewrites)
|
|
178
|
+
? []
|
|
179
|
+
: [
|
|
180
|
+
...(fixableRewrites.some(
|
|
181
|
+
({ needsIsRecord }) => needsIsRecord,
|
|
182
|
+
) && !namedImports.includes('isRecord')
|
|
183
|
+
? ['isRecord']
|
|
184
|
+
: []),
|
|
185
|
+
...(namedImports.includes('hasKey') ? [] : ['hasKey']),
|
|
186
|
+
];
|
|
156
187
|
|
|
157
|
-
const
|
|
158
|
-
...(rewrites.some(({ needsIsRecord }) => needsIsRecord) &&
|
|
159
|
-
!namedImports.includes('isRecord')
|
|
160
|
-
? ['isRecord']
|
|
161
|
-
: []),
|
|
162
|
-
...(namedImports.includes('hasKey') ? [] : ['hasKey']),
|
|
163
|
-
];
|
|
188
|
+
const importCarrier = fixableRewrites[0];
|
|
164
189
|
|
|
165
|
-
for (const
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
] of rewrites.entries()) {
|
|
169
|
-
const objText = sourceCode.getText(objExpression);
|
|
190
|
+
for (const rewrite of rewrites) {
|
|
191
|
+
const { node, objExpression, keyExpression, needsIsRecord } =
|
|
192
|
+
rewrite;
|
|
170
193
|
|
|
171
|
-
const
|
|
194
|
+
const objText = getArgumentText(sourceCode, objExpression);
|
|
195
|
+
|
|
196
|
+
const keyText = getArgumentText(sourceCode, keyExpression);
|
|
172
197
|
|
|
173
198
|
const originalText = sourceCode.getText(node);
|
|
174
199
|
|
|
@@ -180,26 +205,32 @@ export const preferIsRecordAndHasKey: TSESLint.RuleModule<MessageIds, Options> =
|
|
|
180
205
|
objName: objText,
|
|
181
206
|
keyName: keyText,
|
|
182
207
|
},
|
|
183
|
-
fix:
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
208
|
+
fix: !rewrite.fixable
|
|
209
|
+
? undefined
|
|
210
|
+
: (fixer) => {
|
|
211
|
+
const replacement = needsIsRecord
|
|
212
|
+
? `(isRecord(${objText}) && hasKey(${objText}, ${keyText}))`
|
|
213
|
+
: `hasKey(${objText}, ${keyText})`;
|
|
214
|
+
|
|
215
|
+
// All import fixes have to live on the first fixable
|
|
216
|
+
// report: `insertTextBefore(program, …)` produces
|
|
217
|
+
// overlapping ranges otherwise and only one of them would
|
|
218
|
+
// survive a pass.
|
|
219
|
+
const importFixes =
|
|
220
|
+
rewrite === importCarrier && importsToAdd.length > 0
|
|
221
|
+
? buildNamedImportFix(
|
|
222
|
+
fixer,
|
|
223
|
+
program,
|
|
224
|
+
TS_DATA_FORGE_MODULE,
|
|
225
|
+
importsToAdd,
|
|
226
|
+
)
|
|
227
|
+
: [];
|
|
228
|
+
|
|
229
|
+
return [
|
|
230
|
+
...importFixes,
|
|
231
|
+
fixer.replaceText(node, replacement),
|
|
232
|
+
];
|
|
233
|
+
},
|
|
203
234
|
});
|
|
204
235
|
}
|
|
205
236
|
},
|
|
@@ -208,6 +239,56 @@ export const preferIsRecordAndHasKey: TSESLint.RuleModule<MessageIds, Options> =
|
|
|
208
239
|
defaultOptions: [],
|
|
209
240
|
} as const;
|
|
210
241
|
|
|
242
|
+
/**
|
|
243
|
+
* Whether reading `node` again has no side effects: a variable, `this`, or a
|
|
244
|
+
* non-computed property chain on one, type wrappers aside.
|
|
245
|
+
*/
|
|
246
|
+
const isSideEffectFree = (
|
|
247
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
248
|
+
wrapped: TSESTree.Node,
|
|
249
|
+
): boolean => {
|
|
250
|
+
const node = skipTypeWrappers(wrapped);
|
|
251
|
+
|
|
252
|
+
return (
|
|
253
|
+
node.type === AST_NODE_TYPES.Identifier ||
|
|
254
|
+
node.type === AST_NODE_TYPES.ThisExpression ||
|
|
255
|
+
(node.type === AST_NODE_TYPES.MemberExpression &&
|
|
256
|
+
!node.computed &&
|
|
257
|
+
isSideEffectFree(node.object))
|
|
258
|
+
);
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Whether a value of `type` may be an array or a tuple: some member of a union
|
|
263
|
+
* or an intersection is one, or the constraint of a deferred type is.
|
|
264
|
+
* `object` and `unknown` say nothing either way and are not counted.
|
|
265
|
+
*/
|
|
266
|
+
const mayBeArrayType = (
|
|
267
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
268
|
+
checker: ts.TypeChecker,
|
|
269
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly-parameter-types
|
|
270
|
+
type: ts.Type,
|
|
271
|
+
): boolean => {
|
|
272
|
+
if (type.isUnionOrIntersection()) {
|
|
273
|
+
return type.types.some((member) => mayBeArrayType(checker, member));
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
if (
|
|
277
|
+
(type.flags &
|
|
278
|
+
(ts.TypeFlags.TypeParameter |
|
|
279
|
+
ts.TypeFlags.IndexedAccess |
|
|
280
|
+
ts.TypeFlags.Conditional |
|
|
281
|
+
ts.TypeFlags.Substitution)) !==
|
|
282
|
+
0
|
|
283
|
+
) {
|
|
284
|
+
const constraint = checker.getBaseConstraintOfType(type);
|
|
285
|
+
|
|
286
|
+
return constraint !== undefined && mayBeArrayType(checker, constraint);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
return checker.isArrayType(type) || checker.isTupleType(type);
|
|
290
|
+
};
|
|
291
|
+
|
|
211
292
|
/**
|
|
212
293
|
* Whether `type` already satisfies `hasKey`'s `R extends UnknownRecord`
|
|
213
294
|
* constraint, so that guarding the call with `isRecord` adds nothing.
|
|
@@ -62,19 +62,6 @@ describe('prefer-is-record-and-has-key', () => {
|
|
|
62
62
|
`,
|
|
63
63
|
errors: [{ messageId: 'useHasKey' }],
|
|
64
64
|
},
|
|
65
|
-
{
|
|
66
|
-
name: 'keeps isRecord when only part of a union is a record',
|
|
67
|
-
code: dedent`
|
|
68
|
-
declare const obj: Record<string, unknown> | readonly unknown[];
|
|
69
|
-
const ok = Object.hasOwn(obj, 'a');
|
|
70
|
-
`,
|
|
71
|
-
output: dedent`
|
|
72
|
-
import { isRecord, hasKey } from 'ts-data-forge';
|
|
73
|
-
declare const obj: Record<string, unknown> | readonly unknown[];
|
|
74
|
-
const ok = (isRecord(obj) && hasKey(obj, 'a'));
|
|
75
|
-
`,
|
|
76
|
-
errors: [{ messageId: 'useIsRecordAndHasKey' }],
|
|
77
|
-
},
|
|
78
65
|
{
|
|
79
66
|
name: 'keeps isRecord for an index-signature-carrying callable',
|
|
80
67
|
code: dedent`
|
|
@@ -295,28 +282,136 @@ describe('prefer-is-record-and-has-key through type wrappers', () => {
|
|
|
295
282
|
valid: [],
|
|
296
283
|
invalid: [
|
|
297
284
|
{
|
|
298
|
-
name: '
|
|
285
|
+
name: 'reads `Object` through `as`',
|
|
299
286
|
code: dedent`
|
|
300
|
-
declare const
|
|
301
|
-
const ok = Object.hasOwn(
|
|
287
|
+
declare const obj: Readonly<Record<string, unknown>>;
|
|
288
|
+
const ok = (Object as ObjectConstructor).hasOwn(obj, 'a');
|
|
302
289
|
`,
|
|
303
290
|
output: dedent`
|
|
304
|
-
import {
|
|
291
|
+
import { hasKey } from 'ts-data-forge';
|
|
292
|
+
declare const obj: Readonly<Record<string, unknown>>;
|
|
293
|
+
const ok = hasKey(obj, 'a');
|
|
294
|
+
`,
|
|
295
|
+
errors: [{ messageId: 'useHasKey' }],
|
|
296
|
+
},
|
|
297
|
+
],
|
|
298
|
+
});
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
describe('prefer-is-record-and-has-key with parenthesized operands', () => {
|
|
302
|
+
tester.run('prefer-is-record-and-has-key', preferIsRecordAndHasKey, {
|
|
303
|
+
valid: [],
|
|
304
|
+
invalid: [
|
|
305
|
+
{
|
|
306
|
+
name: 'keeps the parentheses of a sequence object',
|
|
307
|
+
code: dedent`
|
|
308
|
+
declare const a: number;
|
|
309
|
+
declare const o: Readonly<Record<string, unknown>>;
|
|
310
|
+
const ok = 'k' in (a, o);
|
|
311
|
+
`,
|
|
312
|
+
output: dedent`
|
|
313
|
+
import { hasKey } from 'ts-data-forge';
|
|
314
|
+
declare const a: number;
|
|
315
|
+
declare const o: Readonly<Record<string, unknown>>;
|
|
316
|
+
const ok = hasKey((a, o), 'k');
|
|
317
|
+
`,
|
|
318
|
+
errors: [{ messageId: 'useHasKey' }],
|
|
319
|
+
},
|
|
320
|
+
{
|
|
321
|
+
name: 'keeps the parentheses of a sequence key',
|
|
322
|
+
code: dedent`
|
|
323
|
+
declare const a: number;
|
|
324
|
+
declare const o: Readonly<Record<string, unknown>>;
|
|
325
|
+
const ok = (a, 'k') in o;
|
|
326
|
+
`,
|
|
327
|
+
output: dedent`
|
|
328
|
+
import { hasKey } from 'ts-data-forge';
|
|
329
|
+
declare const a: number;
|
|
330
|
+
declare const o: Readonly<Record<string, unknown>>;
|
|
331
|
+
const ok = hasKey(o, (a, 'k'));
|
|
332
|
+
`,
|
|
333
|
+
errors: [{ messageId: 'useHasKey' }],
|
|
334
|
+
},
|
|
335
|
+
],
|
|
336
|
+
});
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
describe('prefer-is-record-and-has-key where the rewrite would change behavior', () => {
|
|
340
|
+
tester.run('prefer-is-record-and-has-key', preferIsRecordAndHasKey, {
|
|
341
|
+
valid: [
|
|
342
|
+
{
|
|
343
|
+
// `isRecord` rejects an array, so `'0' in xs` would turn false.
|
|
344
|
+
name: 'an array',
|
|
345
|
+
code: dedent`
|
|
346
|
+
declare const xs: readonly number[];
|
|
347
|
+
const a = '0' in xs;
|
|
348
|
+
const b = Object.hasOwn(xs, 'length');
|
|
349
|
+
`,
|
|
350
|
+
},
|
|
351
|
+
{
|
|
352
|
+
name: 'a union with an array',
|
|
353
|
+
code: dedent`
|
|
354
|
+
declare const v: Readonly<Record<string, unknown>> | readonly number[];
|
|
355
|
+
const ok = 'k' in v;
|
|
356
|
+
`,
|
|
357
|
+
},
|
|
358
|
+
{
|
|
359
|
+
name: 'an array cast to a record',
|
|
360
|
+
code: dedent`
|
|
305
361
|
declare const arr: readonly string[];
|
|
306
|
-
const ok = (
|
|
362
|
+
const ok = Object.hasOwn(arr as unknown as Record<string, unknown>, '0');
|
|
307
363
|
`,
|
|
308
|
-
errors: [{ messageId: 'useIsRecordAndHasKey' }],
|
|
309
364
|
},
|
|
310
365
|
{
|
|
311
|
-
name: '
|
|
366
|
+
name: 'a record widened by `as` to one that may be an array: the author says it may be one',
|
|
312
367
|
code: dedent`
|
|
313
368
|
declare const obj: Readonly<Record<string, unknown>>;
|
|
314
|
-
const ok = Object.hasOwn(obj as Readonly<Record<string, unknown>> | readonly
|
|
369
|
+
const ok = Object.hasOwn(obj as Readonly<Record<string, unknown>> | readonly string[], 'a');
|
|
370
|
+
`,
|
|
371
|
+
},
|
|
372
|
+
],
|
|
373
|
+
invalid: [
|
|
374
|
+
{
|
|
375
|
+
// `isRecord(…) && hasKey(…)` would evaluate the object twice.
|
|
376
|
+
name: 'an object with side effects is reported without a fix',
|
|
377
|
+
code: dedent`
|
|
378
|
+
declare const getObj: () => object;
|
|
379
|
+
declare const log: () => void;
|
|
380
|
+
declare const o: object;
|
|
381
|
+
const a = 'k' in getObj();
|
|
382
|
+
const b = Object.hasOwn(getObj(), 'k');
|
|
383
|
+
const c = 'k' in (log(), o);
|
|
384
|
+
`,
|
|
385
|
+
output: null,
|
|
386
|
+
errors: [
|
|
387
|
+
{ messageId: 'useIsRecordAndHasKey' },
|
|
388
|
+
{ messageId: 'useIsRecordAndHasKey' },
|
|
389
|
+
{ messageId: 'useIsRecordAndHasKey' },
|
|
390
|
+
],
|
|
391
|
+
},
|
|
392
|
+
{
|
|
393
|
+
name: 'an object with side effects is fixed when isRecord is dropped: it is read once',
|
|
394
|
+
code: dedent`
|
|
395
|
+
declare const getRec: () => Readonly<Record<string, unknown>>;
|
|
396
|
+
const ok = 'k' in getRec();
|
|
397
|
+
`,
|
|
398
|
+
output: dedent`
|
|
399
|
+
import { hasKey } from 'ts-data-forge';
|
|
400
|
+
declare const getRec: () => Readonly<Record<string, unknown>>;
|
|
401
|
+
const ok = hasKey(getRec(), 'k');
|
|
402
|
+
`,
|
|
403
|
+
errors: [{ messageId: 'useHasKey' }],
|
|
404
|
+
},
|
|
405
|
+
{
|
|
406
|
+
name: 'a reference read through type wrappers is still read without side effects',
|
|
407
|
+
code: dedent`
|
|
408
|
+
declare const o: { readonly p: object };
|
|
409
|
+
const ok = 'k' in (o.p satisfies object);
|
|
315
410
|
`,
|
|
316
411
|
output: dedent`
|
|
317
412
|
import { isRecord, hasKey } from 'ts-data-forge';
|
|
318
|
-
declare const
|
|
319
|
-
const ok = (isRecord(
|
|
413
|
+
declare const o: { readonly p: object };
|
|
414
|
+
const ok = (isRecord(o.p satisfies object) && hasKey(o.p satisfies object, 'k'));
|
|
320
415
|
`,
|
|
321
416
|
errors: [{ messageId: 'useIsRecordAndHasKey' }],
|
|
322
417
|
},
|