eslint-plugin-ts-data-forge 0.8.6 → 0.10.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.
Files changed (70) hide show
  1. package/README.md +21 -19
  2. package/dist/plugin.d.mts +4 -0
  3. package/dist/plugin.d.mts.map +1 -1
  4. package/dist/plugin.mjs +2 -0
  5. package/dist/plugin.mjs.map +1 -1
  6. package/dist/rule-types.d.mts +31 -1
  7. package/dist/rule-types.d.mts.map +1 -1
  8. package/dist/rule-types.mjs.map +1 -1
  9. package/dist/rules/ast-utils.d.mts +21 -2
  10. package/dist/rules/ast-utils.d.mts.map +1 -1
  11. package/dist/rules/ast-utils.mjs +51 -4
  12. package/dist/rules/ast-utils.mjs.map +1 -1
  13. package/dist/rules/constants.d.mts +2 -0
  14. package/dist/rules/constants.d.mts.map +1 -1
  15. package/dist/rules/constants.mjs +9 -0
  16. package/dist/rules/constants.mjs.map +1 -1
  17. package/dist/rules/mutable-result.d.mts +10 -0
  18. package/dist/rules/mutable-result.d.mts.map +1 -0
  19. package/dist/rules/mutable-result.mjs +145 -0
  20. package/dist/rules/mutable-result.mjs.map +1 -0
  21. package/dist/rules/prefer-arr-generate.d.mts +57 -0
  22. package/dist/rules/prefer-arr-generate.d.mts.map +1 -0
  23. package/dist/rules/prefer-arr-generate.mjs +588 -0
  24. package/dist/rules/prefer-arr-generate.mjs.map +1 -0
  25. package/dist/rules/prefer-arr-uniq.d.mts.map +1 -1
  26. package/dist/rules/prefer-arr-uniq.mjs +2 -149
  27. package/dist/rules/prefer-arr-uniq.mjs.map +1 -1
  28. package/dist/rules/prefer-canonical-array-slicing.d.mts.map +1 -1
  29. package/dist/rules/prefer-canonical-array-slicing.mjs +47 -28
  30. package/dist/rules/prefer-canonical-array-slicing.mjs.map +1 -1
  31. package/dist/rules/prefer-is-non-null-object.d.mts.map +1 -1
  32. package/dist/rules/prefer-is-non-null-object.mjs +110 -40
  33. package/dist/rules/prefer-is-non-null-object.mjs.map +1 -1
  34. package/dist/rules/prefer-is-record-and-has-key.d.mts.map +1 -1
  35. package/dist/rules/prefer-is-record-and-has-key.mjs +102 -45
  36. package/dist/rules/prefer-is-record-and-has-key.mjs.map +1 -1
  37. package/dist/rules/prefer-pipe-map-nullable.d.mts +24 -0
  38. package/dist/rules/prefer-pipe-map-nullable.d.mts.map +1 -0
  39. package/dist/rules/prefer-pipe-map-nullable.mjs +266 -0
  40. package/dist/rules/prefer-pipe-map-nullable.mjs.map +1 -0
  41. package/dist/rules/prefer-range-for-loop.d.mts.map +1 -1
  42. package/dist/rules/prefer-range-for-loop.mjs +97 -16
  43. package/dist/rules/prefer-range-for-loop.mjs.map +1 -1
  44. package/dist/rules/rules.d.mts +2 -0
  45. package/dist/rules/rules.d.mts.map +1 -1
  46. package/dist/rules/rules.mjs +4 -0
  47. package/dist/rules/rules.mjs.map +1 -1
  48. package/package.json +11 -11
  49. package/src/plugin.mts +2 -0
  50. package/src/rule-types.mts +33 -1
  51. package/src/rules/ast-utils.mts +72 -5
  52. package/src/rules/constants.mts +12 -0
  53. package/src/rules/mutable-result.mts +204 -0
  54. package/src/rules/prefer-arr-generate.mts +918 -0
  55. package/src/rules/prefer-arr-generate.test.mts +967 -0
  56. package/src/rules/prefer-arr-is-bounded-length-array.test.mts +19 -0
  57. package/src/rules/prefer-arr-is-max-length-array.test.mts +8 -0
  58. package/src/rules/prefer-arr-is-min-length-array.test.mts +8 -0
  59. package/src/rules/prefer-arr-uniq.mts +2 -207
  60. package/src/rules/prefer-canonical-array-slicing.mts +57 -40
  61. package/src/rules/prefer-canonical-array-slicing.test.mts +55 -0
  62. package/src/rules/prefer-is-non-null-object.mts +160 -57
  63. package/src/rules/prefer-is-non-null-object.test.mts +138 -0
  64. package/src/rules/prefer-is-record-and-has-key.mts +148 -67
  65. package/src/rules/prefer-is-record-and-has-key.test.mts +118 -23
  66. package/src/rules/prefer-pipe-map-nullable.mts +391 -0
  67. package/src/rules/prefer-pipe-map-nullable.test.mts +470 -0
  68. package/src/rules/prefer-range-for-loop.mts +142 -21
  69. package/src/rules/prefer-range-for-loop.test.mts +127 -5
  70. package/src/rules/rules.mts +4 -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
- typeWrapperLayers,
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. Without type information nothing
56
- * is known, so the guard is kept.
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
- if (checker === undefined) {
63
- return false;
64
- }
65
-
66
- // At every layer of type wrappers (see typeWrapperLayers): an array
67
- // cast to `Record<string, unknown>` still needs its `isRecord`.
68
- return typeWrapperLayers(expression).every((layer) => {
69
- const tsNode = services?.esTreeNodeToTSNodeMap?.get(layer);
70
-
71
- return (
72
- tsNode !== undefined &&
73
- isAlreadyRecordType(checker, checker.getTypeAtLocation(tsNode))
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
- node.callee.type !== AST_NODE_TYPES.MemberExpression ||
89
- node.callee.object.type !== AST_NODE_TYPES.Identifier ||
90
- node.callee.object.name !== 'Object' ||
91
- node.callee.property.type !== AST_NODE_TYPES.Identifier ||
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 (keyExpression.type === AST_NODE_TYPES.PrivateIdentifier) {
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: !isKnownRecord(entry.objExpression),
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 importsToAdd: readonly string[] = [
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
- index,
167
- { node, objExpression, keyExpression, needsIsRecord },
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 keyText = sourceCode.getText(keyExpression);
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: (fixer) => {
184
- const replacement = needsIsRecord
185
- ? `(isRecord(${objText}) && hasKey(${objText}, ${keyText}))`
186
- : `hasKey(${objText}, ${keyText})`;
187
-
188
- // All import fixes have to live on the first report:
189
- // `insertTextBefore(program, …)` produces overlapping ranges
190
- // otherwise and only one of them would survive a pass.
191
- const importFixes =
192
- index === 0 && importsToAdd.length > 0
193
- ? buildNamedImportFix(
194
- fixer,
195
- program,
196
- TS_DATA_FORGE_MODULE,
197
- importsToAdd,
198
- )
199
- : [];
200
-
201
- return [...importFixes, fixer.replaceText(node, replacement)];
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: 'keeps isRecord for an array cast to a record: the value is still an array',
285
+ name: 'reads `Object` through `as`',
299
286
  code: dedent`
300
- declare const arr: readonly string[];
301
- const ok = Object.hasOwn(arr as unknown as Record<string, unknown>, '0');
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 { isRecord, hasKey } from 'ts-data-forge';
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 = (isRecord(arr as unknown as Record<string, unknown>) && hasKey(arr as unknown as Record<string, unknown>, '0'));
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: 'keeps isRecord for a record widened to include an array: the author says it may be one',
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 unknown[], 'a');
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 obj: Readonly<Record<string, unknown>>;
319
- const ok = (isRecord(obj as Readonly<Record<string, unknown>> | readonly unknown[]) && hasKey(obj as Readonly<Record<string, unknown>> | readonly unknown[], 'a'));
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
  },