@amritk/generate-validators 0.13.1 → 0.14.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.
@@ -1,6 +1,7 @@
1
1
  import { regexFlagsFor, regexLiteral } from "@amritk/helpers/escape-regex-pattern";
2
2
  import { getMjstInstanceOf, getMjstPrimitive, MJST_EXTENSION_KEY } from "@amritk/helpers/mjst-extension";
3
3
  import { multipleOfFailExpr, multipleOfPassExpr } from "@amritk/helpers/multiple-of-check";
4
+ import { declaresKey, readKey } from "@amritk/helpers/read-key";
4
5
  import { refToName } from "@amritk/helpers/ref-to-name";
5
6
  import { safeAccessor } from "@amritk/helpers/safe-accessor";
6
7
  import { hasAdditionalProperties, hasAllOf, hasAnyOf, hasConst, hasDependentRequired, hasEnum, hasExclusiveMaximum, hasExclusiveMinimum, hasItems, hasMaxItems, hasMaximum, hasMaxLength, hasMaxProperties, hasMinItems, hasMinimum, hasMinLength, hasMinProperties, hasMultipleOf, hasOneOf, hasPattern, hasProperties, hasPropertyNames, hasRef, hasRequired, hasStrictExclusiveMaximum, hasStrictExclusiveMinimum, hasType, hasUniqueItems, isObjectSchema, isSchemaObject } from "@amritk/helpers/schema-guards";
@@ -49,7 +50,7 @@ const SCALAR_ITEM_TYPES = /* @__PURE__ */ new Set(["string", "number", "integer"
49
50
  const schemaIsScalarOnly = (schema) => {
50
51
  if (!isSchemaObject(schema))
51
52
  return false;
52
- const t = schema["type"];
53
+ const t = readKey(schema, "type");
53
54
  if (t === void 0)
54
55
  return false;
55
56
  const types = Array.isArray(t) ? t : [t];
@@ -89,31 +90,40 @@ const wrongTypeCondition = (accessor, type) => {
89
90
  const declaresObjectType = (schema) => hasType(schema) && schema.type === "object";
90
91
  const objectRootIsSelfContained = (schema) => !hasRef(schema) && !hasConst(schema) && !hasEnum(schema) && getMjstInstanceOf(schema) === void 0 && getMjstPrimitive(schema) === void 0;
91
92
  const getTypeArray = (schema) => {
92
- if (!isSchemaObject(schema) || !("type" in schema) || !Array.isArray(schema.type))
93
+ if (!isSchemaObject(schema) || !declaresKey(schema, "type") || !Array.isArray(schema.type))
93
94
  return null;
94
95
  return schema.type;
95
96
  };
97
+ const ROOT_ERROR_SINK = "(errors ??= [])";
98
+ const MATCH_ERROR_SINK = "_m";
96
99
  const createRootContext = (rootSchema) => ({
97
100
  objVar: "obj",
98
101
  pathPrefix: "${_path}",
99
102
  depth: 0,
100
103
  hoisted: [],
101
- rootSchema
104
+ rootSchema,
105
+ sink: ROOT_ERROR_SINK
102
106
  });
103
- const pointerSegment = (key) => key.replace(/~/g, "~0").replace(/\//g, "~1").replace(/[\\`$]/g, "\\$&");
107
+ const pointerSegment = (key) => {
108
+ const pointer = key.replace(/~/g, "~0").replace(/\//g, "~1");
109
+ return JSON.stringify(pointer).slice(1, -1).replace(/[`$]/g, "\\$&");
110
+ };
104
111
  const patternPropertySources = (schema) => {
105
- if (!isSchemaObject(schema) || !("patternProperties" in schema))
112
+ if (!isSchemaObject(schema) || !declaresKey(schema, "patternProperties"))
106
113
  return [];
107
114
  const patterns = schema.patternProperties;
108
115
  if (typeof patterns !== "object" || patterns === null)
109
116
  return [];
110
117
  return Object.keys(patterns);
111
118
  };
112
- const readsObjBinding = (text) => /\bobj\b/.test(text);
119
+ const readsBinding = (name, text) => new RegExp(`\\b${name}\\b`).test(text);
120
+ const readsObjBinding = (text) => readsBinding("obj", text);
113
121
  const withHoisted = (hoisted, text) => {
114
122
  const kept = hoisted.filter((entry) => text.includes(entry.reference)).map((entry) => entry.declaration);
115
- const reads = text.replaceAll("(input: unknown", "(");
116
- const body = /\binput\b/.test(reads) ? text : text.replaceAll("(input: unknown", "(_input: unknown");
123
+ const firstBreak = text.indexOf("\n");
124
+ const signature = firstBreak === -1 ? text : text.slice(0, firstBreak);
125
+ const rest = firstBreak === -1 ? "" : text.slice(firstBreak);
126
+ const body = /\binput\b/.test(rest) ? text : signature.replace("(input: unknown", "(_input: unknown") + rest;
117
127
  return kept.length > 0 ? `${kept.join("\n")}
118
128
 
119
129
  ${body}` : body;
@@ -145,7 +155,7 @@ const generateStrictKeyChecks = (schema, ctx) => {
145
155
  return [
146
156
  ` for (const _key${d} in ${ctx.objVar}) {`,
147
157
  ` if (${unknownTest}${patternGuard}) {`,
148
- ` errors.push({ message: 'must NOT have additional properties', path: \`${ctx.pathPrefix}/\${escapePointer(_key${d})}\` })`,
158
+ ` ${ctx.sink}.push({ message: 'must NOT have additional properties', path: \`${ctx.pathPrefix}/\${escapePointer(_key${d})}\` })`,
149
159
  ` }`,
150
160
  ` }`
151
161
  ];
@@ -160,7 +170,7 @@ const generateMissingRequiredChecks = (schema, ctx) => {
160
170
  if (Object.hasOwn(props, key))
161
171
  continue;
162
172
  lines.push(` if (${missingCheck(ctx.objVar, key)}) {`);
163
- lines.push(` errors.push({ message: ${JSON.stringify(`must have required property '${key}'`)}, path: ${parentPath} })`);
173
+ lines.push(` ${ctx.sink}.push({ message: ${JSON.stringify(`must have required property '${key}'`)}, path: ${parentPath} })`);
164
174
  lines.push(` }`);
165
175
  }
166
176
  return lines;
@@ -172,15 +182,15 @@ const generatePropertyChecks = (key, propSchema, isRequired, suffix, ctx) => {
172
182
  if (isRequired) {
173
183
  return [
174
184
  ` if (${missingCheck(ctx.objVar, key)}) {`,
175
- ` errors.push({ message: ${JSON.stringify(`must have required property '${key}'`)}, path: ${parentPath} })`,
185
+ ` ${ctx.sink}.push({ message: ${JSON.stringify(`must have required property '${key}'`)}, path: ${parentPath} })`,
176
186
  ` } else {`,
177
- ` errors.push({ message: ${JSON.stringify(FALSE_SCHEMA_MESSAGE)}, path: ${path} })`,
187
+ ` ${ctx.sink}.push({ message: ${JSON.stringify(FALSE_SCHEMA_MESSAGE)}, path: ${path} })`,
178
188
  ` }`
179
189
  ];
180
190
  }
181
191
  return [
182
192
  ` if (${hasOwnCheck(ctx.objVar, key)}) {`,
183
- ` errors.push({ message: ${JSON.stringify(FALSE_SCHEMA_MESSAGE)}, path: ${path} })`,
193
+ ` ${ctx.sink}.push({ message: ${JSON.stringify(FALSE_SCHEMA_MESSAGE)}, path: ${path} })`,
184
194
  ` }`
185
195
  ];
186
196
  }
@@ -215,35 +225,35 @@ const generateKeywordChecks = (key, raw, path, schema, suffix, ctx, presence) =>
215
225
  const lines = [];
216
226
  if (hasRef(schema)) {
217
227
  const vName = validatorName(refToName(schema.$ref, suffix));
218
- const delegate = [` const _r = ${vName}(${raw}, ${path})`, ` if (_r !== true) errors.push(..._r.errors)`];
228
+ const delegate = [` const _r = ${vName}(${raw}, ${path})`, ` if (_r !== true) ${ctx.sink}.push(..._r.errors)`];
219
229
  if (presence === "")
220
- lines.push(...delegate);
230
+ lines.push(` {`, ...delegate.map((line) => ` ${line}`), ` }`);
221
231
  else
222
232
  lines.push(` if (${raw} !== undefined) {`, ...delegate.map((line) => ` ${line}`), ` }`);
223
233
  }
224
234
  const instanceOf = getMjstInstanceOf(schema);
225
235
  if (instanceOf) {
226
236
  lines.push(` if (${presence}!(${raw} instanceof ${instanceOf})) {`);
227
- lines.push(` errors.push({ message: 'must be ${instanceOf}', path: ${path} })`);
237
+ lines.push(` ${ctx.sink}.push({ message: 'must be ${instanceOf}', path: ${path} })`);
228
238
  lines.push(` }`);
229
239
  }
230
240
  const primitive = getMjstPrimitive(schema);
231
241
  if (primitive) {
232
242
  lines.push(` if (${presence}typeof ${raw} !== "${primitive}") {`);
233
- lines.push(` errors.push({ message: 'must be ${primitive}', path: ${path} })`);
243
+ lines.push(` ${ctx.sink}.push({ message: 'must be ${primitive}', path: ${path} })`);
234
244
  lines.push(` }`);
235
245
  }
236
246
  if (hasConst(schema)) {
237
247
  const mismatch = constMismatchCondition(raw, schema.const);
238
248
  const msg = JSON.stringify(`must be ${JSON.stringify(schema.const)}`);
239
249
  lines.push(` if (${presence}${mismatch}) {`);
240
- lines.push(` errors.push({ message: ${msg}, path: ${path} })`);
250
+ lines.push(` ${ctx.sink}.push({ message: ${msg}, path: ${path} })`);
241
251
  lines.push(` }`);
242
252
  }
243
253
  if (hasEnum(schema)) {
244
254
  const label = schema.enum.map((v) => JSON.stringify(v)).join(", ");
245
255
  lines.push(` if (${presence}!${enumMembershipExpr(schema.enum, raw)}) {`);
246
- lines.push(` errors.push({ message: ${JSON.stringify(`must be one of: ${label}`)}, path: ${path} })`);
256
+ lines.push(` ${ctx.sink}.push({ message: ${JSON.stringify(`must be one of: ${label}`)}, path: ${path} })`);
247
257
  lines.push(` }`);
248
258
  }
249
259
  if (instanceOf === void 0 && primitive === void 0) {
@@ -252,7 +262,7 @@ const generateKeywordChecks = (key, raw, path, schema, suffix, ctx, presence) =>
252
262
  const wrongType = wrongTypeCondition(raw, t);
253
263
  if (wrongType) {
254
264
  lines.push(` if (${presence === "" ? wrongType : `${presence}(${wrongType})`}) {`);
255
- lines.push(` errors.push({ message: 'must be ${typeofString(t)}', path: ${path} })`);
265
+ lines.push(` ${ctx.sink}.push({ message: 'must be ${typeofString(t)}', path: ${path} })`);
256
266
  lines.push(` }`);
257
267
  }
258
268
  }
@@ -262,7 +272,7 @@ const generateKeywordChecks = (key, raw, path, schema, suffix, ctx, presence) =>
262
272
  if (allWrong) {
263
273
  const label = typeArray.map((t) => typeofString(t)).join(" or ");
264
274
  lines.push(` if (${presence === "" ? allWrong : `${presence}(${allWrong})`}) {`);
265
- lines.push(` errors.push({ message: ${JSON.stringify(`must be ${label}`)}, path: ${path} })`);
275
+ lines.push(` ${ctx.sink}.push({ message: ${JSON.stringify(`must be ${label}`)}, path: ${path} })`);
266
276
  lines.push(` }`);
267
277
  }
268
278
  }
@@ -281,7 +291,7 @@ const generatePropertyCheckLines = (key, propSchema, isRequired, suffix, ctx) =>
281
291
  const parentPath = ctx.depth === 0 ? "_path" : `\`${ctx.pathPrefix}\``;
282
292
  const missing = [
283
293
  ` if (${missingCheck(ctx.objVar, key)}) {`,
284
- ` errors.push({ message: ${JSON.stringify(`must have required property '${key}'`)}, path: ${parentPath} })`
294
+ ` ${ctx.sink}.push({ message: ${JSON.stringify(`must have required property '${key}'`)}, path: ${parentPath} })`
285
295
  ];
286
296
  if (!isSchemaObject(propSchema)) {
287
297
  if (isRequired && propSchema === true)
@@ -311,17 +321,17 @@ const generateConstraintChecks = (key, raw, path, propSchema, suffix, ctx) => {
311
321
  const re = regexLiteral(propSchema.pattern);
312
322
  const msg = JSON.stringify(`must match pattern ${propSchema.pattern}`);
313
323
  lines.push(` if (typeof ${raw} === 'string' && !${re}.test(${raw})) {`);
314
- lines.push(` errors.push({ message: ${msg}, path: ${path} })`);
324
+ lines.push(` ${ctx.sink}.push({ message: ${msg}, path: ${path} })`);
315
325
  lines.push(` }`);
316
326
  }
317
327
  if (hasMinLength(propSchema) && propSchema.minLength > 0) {
318
328
  lines.push(` if (typeof ${raw} === 'string' && ${minLengthFailExpr(raw, propSchema.minLength)}) {`);
319
- lines.push(` errors.push({ message: 'must have at least ${propSchema.minLength} characters', path: ${path} })`);
329
+ lines.push(` ${ctx.sink}.push({ message: 'must have at least ${propSchema.minLength} characters', path: ${path} })`);
320
330
  lines.push(` }`);
321
331
  }
322
332
  if (hasMaxLength(propSchema)) {
323
333
  lines.push(` if (typeof ${raw} === 'string' && ${maxLengthFailExpr(raw, propSchema.maxLength)}) {`);
324
- lines.push(` errors.push({ message: 'must have at most ${propSchema.maxLength} characters', path: ${path} })`);
334
+ lines.push(` ${ctx.sink}.push({ message: 'must have at most ${propSchema.maxLength} characters', path: ${path} })`);
325
335
  lines.push(` }`);
326
336
  }
327
337
  }
@@ -330,29 +340,29 @@ const generateConstraintChecks = (key, raw, path, propSchema, suffix, ctx) => {
330
340
  const strict = hasStrictExclusiveMinimum(propSchema);
331
341
  const rel = strict ? ">" : ">=";
332
342
  lines.push(` if (typeof ${raw} === 'number' && !(${raw} ${rel} ${propSchema.minimum})) {`);
333
- lines.push(` errors.push({ message: 'must be ${rel} ${propSchema.minimum}', path: ${path} })`);
343
+ lines.push(` ${ctx.sink}.push({ message: 'must be ${rel} ${propSchema.minimum}', path: ${path} })`);
334
344
  lines.push(` }`);
335
345
  }
336
346
  if (hasMaximum(propSchema)) {
337
347
  const strict = hasStrictExclusiveMaximum(propSchema);
338
348
  const rel = strict ? "<" : "<=";
339
349
  lines.push(` if (typeof ${raw} === 'number' && !(${raw} ${rel} ${propSchema.maximum})) {`);
340
- lines.push(` errors.push({ message: 'must be ${rel} ${propSchema.maximum}', path: ${path} })`);
350
+ lines.push(` ${ctx.sink}.push({ message: 'must be ${rel} ${propSchema.maximum}', path: ${path} })`);
341
351
  lines.push(` }`);
342
352
  }
343
353
  if (hasExclusiveMinimum(propSchema)) {
344
354
  lines.push(` if (typeof ${raw} === 'number' && !(${raw} > ${propSchema.exclusiveMinimum})) {`);
345
- lines.push(` errors.push({ message: 'must be > ${propSchema.exclusiveMinimum}', path: ${path} })`);
355
+ lines.push(` ${ctx.sink}.push({ message: 'must be > ${propSchema.exclusiveMinimum}', path: ${path} })`);
346
356
  lines.push(` }`);
347
357
  }
348
358
  if (hasExclusiveMaximum(propSchema)) {
349
359
  lines.push(` if (typeof ${raw} === 'number' && !(${raw} < ${propSchema.exclusiveMaximum})) {`);
350
- lines.push(` errors.push({ message: 'must be < ${propSchema.exclusiveMaximum}', path: ${path} })`);
360
+ lines.push(` ${ctx.sink}.push({ message: 'must be < ${propSchema.exclusiveMaximum}', path: ${path} })`);
351
361
  lines.push(` }`);
352
362
  }
353
363
  if (hasMultipleOf(propSchema)) {
354
364
  lines.push(` if (typeof ${raw} === 'number' && ${multipleOfFailExpr(raw, propSchema.multipleOf)}) {`);
355
- lines.push(` errors.push({ message: 'must be a multiple of ${propSchema.multipleOf}', path: ${path} })`);
365
+ lines.push(` ${ctx.sink}.push({ message: 'must be a multiple of ${propSchema.multipleOf}', path: ${path} })`);
356
366
  lines.push(` }`);
357
367
  }
358
368
  }
@@ -367,7 +377,7 @@ const generateConstraintChecks = (key, raw, path, propSchema, suffix, ctx) => {
367
377
  lines.push(` if (Array.isArray(${raw})) {`);
368
378
  lines.push(` for (let ${iv} = ${firstTailIndex}; ${iv} < ${raw}.length; ${iv}++) {`);
369
379
  lines.push(` const _ir = ${vName}(${raw}[${iv}], ${itemPath})`);
370
- lines.push(` if (_ir !== true) errors.push(..._ir.errors)`);
380
+ lines.push(` if (_ir !== true) ${ctx.sink}.push(..._ir.errors)`);
371
381
  lines.push(` }`);
372
382
  lines.push(` }`);
373
383
  } else if (isSchemaObject(itemSchema)) {
@@ -376,46 +386,72 @@ const generateConstraintChecks = (key, raw, path, propSchema, suffix, ctx) => {
376
386
  if (detail.length > 0) {
377
387
  lines.push(` if (Array.isArray(${raw})) {`);
378
388
  lines.push(` for (let ${iv} = ${firstTailIndex}; ${iv} < ${raw}.length; ${iv}++) {`);
379
- lines.push(` const ${itemVar} = ${raw}[${iv}]`);
389
+ if (readsBinding(itemVar, detail.join("\n")))
390
+ lines.push(` const ${itemVar} = ${raw}[${iv}]`);
380
391
  lines.push(...detail.map((l) => ` ${l}`));
381
392
  lines.push(` }`);
382
393
  lines.push(` }`);
383
394
  }
384
395
  }
385
396
  }
386
- if (hasMinItems(propSchema) || hasMaxItems(propSchema) || hasUniqueItems(propSchema) && propSchema.uniqueItems === true || "contains" in sp || tuple !== void 0 || tail === false && tuple === void 0) {
397
+ if (hasMinItems(propSchema) || hasMaxItems(propSchema) || hasUniqueItems(propSchema) && propSchema.uniqueItems === true || declaresKey(sp, "contains") || tuple !== void 0 || tail === false && tuple === void 0) {
387
398
  if (tail === false && tuple === void 0) {
388
399
  lines.push(` if (Array.isArray(${raw}) && ${raw}.length > 0) {`);
389
- lines.push(` errors.push({ message: 'must NOT have more than 0 items', path: ${path} })`);
400
+ lines.push(` ${ctx.sink}.push({ message: 'must NOT have more than 0 items', path: ${path} })`);
390
401
  lines.push(` }`);
391
402
  }
392
403
  if (hasMinItems(propSchema)) {
393
404
  lines.push(` if (Array.isArray(${raw}) && ${raw}.length < ${propSchema.minItems}) {`);
394
- lines.push(` errors.push({ message: 'must have at least ${propSchema.minItems} items', path: ${path} })`);
405
+ lines.push(` ${ctx.sink}.push({ message: 'must have at least ${propSchema.minItems} items', path: ${path} })`);
395
406
  lines.push(` }`);
396
407
  }
397
408
  if (hasMaxItems(propSchema)) {
398
409
  lines.push(` if (Array.isArray(${raw}) && ${raw}.length > ${propSchema.maxItems}) {`);
399
- lines.push(` errors.push({ message: 'must have at most ${propSchema.maxItems} items', path: ${path} })`);
410
+ lines.push(` ${ctx.sink}.push({ message: 'must have at most ${propSchema.maxItems} items', path: ${path} })`);
400
411
  lines.push(` }`);
401
412
  }
402
413
  if (hasUniqueItems(propSchema) && propSchema.uniqueItems === true) {
403
- const dupCond = arrayItemsAreScalarOnly(sp) ? `new Set((${raw} as unknown[]).map((_u) => JSON.stringify(_u))).size !== ${raw}.length` : `!allUnique(${raw} as unknown[])`;
414
+ const dupCond = arrayItemsAreScalarOnly(sp) ? `new Set(${raw} as unknown[]).size !== ${raw}.length` : `!allUnique(${raw} as unknown[])`;
404
415
  lines.push(` if (Array.isArray(${raw}) && ${dupCond}) {`);
405
- lines.push(` errors.push({ message: 'must NOT have duplicate items', path: ${path} })`);
416
+ lines.push(` ${ctx.sink}.push({ message: 'must NOT have duplicate items', path: ${path} })`);
406
417
  lines.push(` }`);
407
418
  }
408
- if ("contains" in sp) {
409
- const min = typeof sp["minContains"] === "number" ? sp["minContains"] : 1;
410
- const max = typeof sp["maxContains"] === "number" ? sp["maxContains"] : void 0;
411
- const matchExpr = generateMatchesExpr("_c", sp["contains"], suffix, ctx);
412
- const bound = max !== void 0 ? `_cn < ${min} || _cn > ${max}` : `_cn < ${min}`;
413
- lines.push(` if (Array.isArray(${raw})) {`);
414
- lines.push(` const _cn = (${raw} as unknown[]).filter((_c) => ${matchExpr}).length`);
415
- lines.push(` if (${bound}) {`);
416
- lines.push(` errors.push({ message: 'array does not contain the required matching items', path: ${path} })`);
417
- lines.push(` }`);
418
- lines.push(` }`);
419
+ if (declaresKey(sp, "contains")) {
420
+ const declaredMin = readKey(sp, "minContains");
421
+ const declaredMax = readKey(sp, "maxContains");
422
+ const min = typeof declaredMin === "number" ? declaredMin : 1;
423
+ const max = typeof declaredMax === "number" ? declaredMax : void 0;
424
+ if (min > 0 || max !== void 0) {
425
+ const matchExpr = generateMatchesExpr("_c", readKey(sp, "contains"), suffix, ctx, true);
426
+ const report = `${ctx.sink}.push({ message: 'array does not contain the required matching items', path: ${path} })`;
427
+ if (matchExpr === "true") {
428
+ const bound = max !== void 0 ? `${raw}.length < ${min} || ${raw}.length > ${max}` : `${raw}.length < ${min}`;
429
+ lines.push(` if (Array.isArray(${raw}) && (${bound})) {`);
430
+ lines.push(` ${report}`);
431
+ lines.push(` }`);
432
+ } else if (matchExpr === "false") {
433
+ if (0 < min || max !== void 0 && 0 > max) {
434
+ lines.push(` if (Array.isArray(${raw})) {`);
435
+ lines.push(` ${report}`);
436
+ lines.push(` }`);
437
+ }
438
+ } else {
439
+ const bound = max !== void 0 ? `_cn < ${min} || _cn > ${max}` : `_cn < ${min}`;
440
+ lines.push(` if (Array.isArray(${raw})) {`);
441
+ lines.push(` const _ca = ${raw} as unknown[]`);
442
+ lines.push(` let _cn = 0`);
443
+ lines.push(` for (let _ci = 0; _ci < _ca.length; _ci++) {`);
444
+ lines.push(` const _c = _ca[_ci]`);
445
+ lines.push(` if (${matchExpr}) _cn++`);
446
+ if (max === void 0)
447
+ lines.push(` if (_cn >= ${min}) break`);
448
+ lines.push(` }`);
449
+ lines.push(` if (${bound}) {`);
450
+ lines.push(` ${report}`);
451
+ lines.push(` }`);
452
+ lines.push(` }`);
453
+ }
454
+ }
419
455
  }
420
456
  if (tuple !== void 0) {
421
457
  lines.push(` if (Array.isArray(${raw})) {`);
@@ -429,7 +465,7 @@ const generateConstraintChecks = (key, raw, path, propSchema, suffix, ctx) => {
429
465
  }
430
466
  if (tailIsClosed) {
431
467
  lines.push(` if (${raw}.length > ${tuple.length}) {`);
432
- lines.push(` errors.push({ message: 'must NOT have more than ${tuple.length} items', path: ${path} })`);
468
+ lines.push(` ${ctx.sink}.push({ message: 'must NOT have more than ${tuple.length} items', path: ${path} })`);
433
469
  lines.push(` }`);
434
470
  }
435
471
  lines.push(` }`);
@@ -449,9 +485,9 @@ const generateValueChecks = (key, raw, path, propSchema, suffix, ctx, required =
449
485
  const generateValueCheckLines = (_key, raw, path, propSchema, suffix, ctx, required = false) => {
450
486
  const lines = [];
451
487
  if (propSchema === false) {
452
- const report = `errors.push({ message: ${JSON.stringify(FALSE_SCHEMA_MESSAGE)}, path: ${path} })`;
488
+ const report = `${ctx.sink}.push({ message: ${JSON.stringify(FALSE_SCHEMA_MESSAGE)}, path: ${path} })`;
453
489
  if (required)
454
- return [` ${report}`];
490
+ return [` {`, ` ${report}`, ` }`];
455
491
  return [` if (${raw} !== undefined) {`, ` ${report}`, ` }`];
456
492
  }
457
493
  if (!isSchemaObject(propSchema))
@@ -462,13 +498,14 @@ const generateValueCheckLines = (_key, raw, path, propSchema, suffix, ctx, requi
462
498
  pathPrefix: path.slice(1, -1),
463
499
  depth: ctx.depth + 1,
464
500
  hoisted: ctx.hoisted,
465
- rootSchema: ctx.rootSchema
501
+ rootSchema: ctx.rootSchema,
502
+ sink: ctx.sink
466
503
  };
467
504
  lines.push(...generateKeywordChecks("", raw, path, propSchema, suffix, valueCtx, presence));
468
505
  return lines;
469
506
  };
470
507
  const NARROWABLE_REFERENCE = /^[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*|\[[A-Za-z_$][\w$]*\]|\[\d+\])*$/;
471
- const generateMatchesExpr = (raw, sub, suffix, ctx) => {
508
+ const generateMatchesExpr = (raw, sub, suffix, ctx, required = false) => {
472
509
  if (sub === true)
473
510
  return "true";
474
511
  if (sub === false)
@@ -476,22 +513,30 @@ const generateMatchesExpr = (raw, sub, suffix, ctx) => {
476
513
  if (!isSchemaObject(sub))
477
514
  return "true";
478
515
  const value = NARROWABLE_REFERENCE.test(raw) ? raw : `_mv${ctx.depth}`;
479
- const checks = generateValueChecks("", value, "`${_path}`", sub, suffix, ctx);
516
+ const checks = generateValueChecks("", value, "`${_path}`", sub, suffix, { ...ctx, sink: MATCH_ERROR_SINK }, required);
480
517
  if (checks.length === 0)
481
518
  return "true";
482
- const body = checks.join("\n").replaceAll("errors.push(", "_m.push(");
519
+ const body = checks.join("\n");
483
520
  const binding = value === raw ? "" : `const ${value}: unknown = ${raw}
484
521
  `;
485
522
  return `((): boolean => { const _m: ValidationError[] = []
486
523
  ${binding}${body}
487
524
  return _m.length === 0 })()`;
488
525
  };
489
- const unevaluatedMatcher = (suffix, ctx) => (accessor, schema, depth) => generateMatchesExpr(accessor, schema, suffix, { ...ctx, depth: ctx.depth + depth + 1 });
526
+ const unevaluatedMatcher = (suffix, ctx) => (accessor, schema, depth) => (
527
+ // Always `required`: every accessor handed to this matcher is a value the
528
+ // surrounding test has already established is there — the whole instance
529
+ // inside its type guard, or a key/index the sweep is walking. In optional
530
+ // mode a property whose value is `undefined` matched every branch, so
531
+ // `{ a: undefined }` satisfied an `unevaluatedProperties: { type: 'string' }`
532
+ // that Ajv and the interpreter both reject.
533
+ generateMatchesExpr(accessor, schema, suffix, { ...ctx, depth: ctx.depth + depth + 1 }, true)
534
+ );
490
535
  const generateUnevaluatedChecks = (raw, path, schema, suffix, ctx) => {
491
536
  if (!isSchemaObject(schema))
492
537
  return [];
493
538
  const s = schema;
494
- if (!("unevaluatedProperties" in s) && !("unevaluatedItems" in s))
539
+ if (!declaresKey(s, "unevaluatedProperties") && !declaresKey(s, "unevaluatedItems"))
495
540
  return [];
496
541
  const match = unevaluatedMatcher(suffix, ctx);
497
542
  const lines = [];
@@ -503,7 +548,7 @@ const generateUnevaluatedChecks = (raw, path, schema, suffix, ctx) => {
503
548
  for (const statement of properties.setup)
504
549
  lines.push(` ${statement}`);
505
550
  lines.push(` if (!(${properties.expr})) {`);
506
- lines.push(` errors.push({ message: 'must NOT have unevaluated properties', path: ${path} })`);
551
+ lines.push(` ${ctx.sink}.push({ message: 'must NOT have unevaluated properties', path: ${path} })`);
507
552
  lines.push(` }`);
508
553
  lines.push(` }`);
509
554
  }
@@ -515,7 +560,7 @@ const generateUnevaluatedChecks = (raw, path, schema, suffix, ctx) => {
515
560
  for (const statement of items.setup)
516
561
  lines.push(` ${statement}`);
517
562
  lines.push(` if (!(${items.expr})) {`);
518
- lines.push(` errors.push({ message: 'must NOT have unevaluated items', path: ${path} })`);
563
+ lines.push(` ${ctx.sink}.push({ message: 'must NOT have unevaluated items', path: ${path} })`);
519
564
  lines.push(` }`);
520
565
  lines.push(` }`);
521
566
  }
@@ -527,39 +572,39 @@ const generateCombinatorChecks = (key, raw, path, schema, suffix, ctx) => {
527
572
  const lines = [];
528
573
  if (hasAllOf(schema)) {
529
574
  for (const branch of schema.allOf)
530
- lines.push(...generateValueChecks(key, raw, path, branch, suffix, ctx));
575
+ lines.push(...generateValueChecks(key, raw, path, branch, suffix, ctx, true));
531
576
  }
532
577
  if (hasAnyOf(schema) && schema.anyOf.length > 0) {
533
- const conds = schema.anyOf.map((b) => generateMatchesExpr(raw, b, suffix, ctx));
578
+ const conds = schema.anyOf.map((b) => generateMatchesExpr(raw, b, suffix, ctx, true));
534
579
  if (!conds.includes("true")) {
535
580
  lines.push(` if (!(${conds.join(" || ")})) {`);
536
- lines.push(` errors.push({ message: 'must match a schema in anyOf', path: ${path} })`);
581
+ lines.push(` ${ctx.sink}.push({ message: 'must match a schema in anyOf', path: ${path} })`);
537
582
  lines.push(` }`);
538
583
  }
539
584
  }
540
585
  if (hasOneOf(schema) && schema.oneOf.length > 0) {
541
- const conds = schema.oneOf.map((b) => `(${generateMatchesExpr(raw, b, suffix, ctx)} ? 1 : 0)`);
586
+ const conds = schema.oneOf.map((b) => `(${generateMatchesExpr(raw, b, suffix, ctx, true)} ? 1 : 0)`);
542
587
  lines.push(` if ((${conds.join(" + ")}) !== 1) {`);
543
- lines.push(` errors.push({ message: 'must match exactly one schema in oneOf', path: ${path} })`);
588
+ lines.push(` ${ctx.sink}.push({ message: 'must match exactly one schema in oneOf', path: ${path} })`);
544
589
  lines.push(` }`);
545
590
  }
546
- const not = schema["not"];
591
+ const not = readKey(schema, "not");
547
592
  if (not !== void 0 && (isSchemaObject(not) || typeof not === "boolean")) {
548
- const cond = generateMatchesExpr(raw, not, suffix, ctx);
593
+ const cond = generateMatchesExpr(raw, not, suffix, ctx, true);
549
594
  if (cond !== "false") {
550
595
  lines.push(` if (${cond}) {`);
551
- lines.push(` errors.push({ message: 'must NOT match the schema in not', path: ${path} })`);
596
+ lines.push(` ${ctx.sink}.push({ message: 'must NOT match the schema in not', path: ${path} })`);
552
597
  lines.push(` }`);
553
598
  }
554
599
  }
555
- const ifSchema = schema["if"];
600
+ const ifSchema = readKey(schema, "if");
556
601
  if (ifSchema !== void 0 && (isSchemaObject(ifSchema) || typeof ifSchema === "boolean")) {
557
- const thenSchema = schema["then"];
558
- const elseSchema = schema["else"];
559
- const thenLines = thenSchema !== void 0 ? generateValueChecks(key, raw, path, thenSchema, suffix, ctx) : [];
560
- const elseLines = elseSchema !== void 0 ? generateValueChecks(key, raw, path, elseSchema, suffix, ctx) : [];
602
+ const thenSchema = readKey(schema, "then");
603
+ const elseSchema = readKey(schema, "else");
604
+ const thenLines = thenSchema !== void 0 ? generateValueChecks(key, raw, path, thenSchema, suffix, ctx, true) : [];
605
+ const elseLines = elseSchema !== void 0 ? generateValueChecks(key, raw, path, elseSchema, suffix, ctx, true) : [];
561
606
  if (thenLines.length > 0 || elseLines.length > 0) {
562
- const cond = generateMatchesExpr(raw, ifSchema, suffix, ctx);
607
+ const cond = generateMatchesExpr(raw, ifSchema, suffix, ctx, true);
563
608
  if (cond === "true") {
564
609
  lines.push(...thenLines);
565
610
  } else if (cond === "false") {
@@ -580,38 +625,48 @@ const generatePatternAndAdditionalChecks = (schema, suffix, ctx) => {
580
625
  return [];
581
626
  const obj = ctx.objVar;
582
627
  const d = ctx.depth;
583
- const lines = [];
584
- const patternsRecord = "patternProperties" in schema && typeof schema.patternProperties === "object" && schema.patternProperties !== null ? schema.patternProperties : {};
585
- const patternEntries = Object.entries(patternsRecord);
586
- for (const [pattern, sub] of patternEntries) {
587
- const re = regexLiteral(pattern);
588
- const kv = `_pk${d}`;
589
- const valueChecks = generateValueChecks(`\${${kv}}`, `${obj}[${kv}]`, `\`${ctx.pathPrefix}/\${escapePointer(${kv})}\``, sub, suffix, ctx);
628
+ const patternsRecord = declaresKey(schema, "patternProperties") && typeof schema.patternProperties === "object" && schema.patternProperties !== null ? schema.patternProperties : {};
629
+ const patterns = Object.keys(patternsRecord);
630
+ const kv = `_pk${d}`;
631
+ const valueChecksFor = (sub) => (
632
+ // `required` mode: the key came out of the loop over the object, so its value
633
+ // is there to be judged. Optional mode wrote `obj[_pk0] !== undefined &&` in
634
+ // front of every check, and a property whose value *is* `undefined` then
635
+ // satisfied all of them — `{ a: undefined }` passed a
636
+ // `patternProperties: { "^a": { type: "string" } }` that Ajv and the
637
+ // interpreter both reject.
638
+ generateValueChecks(`\${${kv}}`, `${obj}[${kv}]`, `\`${ctx.pathPrefix}/\${escapePointer(${kv})}\``, sub, suffix, ctx, true)
639
+ );
640
+ const body = [];
641
+ for (const [pattern, sub] of Object.entries(patternsRecord)) {
642
+ const valueChecks = valueChecksFor(sub);
590
643
  if (valueChecks.length === 0)
591
644
  continue;
592
- lines.push(` for (const ${kv} in ${obj}) {`);
593
- lines.push(` if (${re}.test(${kv})) {`);
594
- lines.push(...valueChecks.map((line) => ` ${line}`));
595
- lines.push(` }`);
596
- lines.push(` }`);
645
+ body.push(` if (${regexLiteral(pattern)}.test(${kv})) {`);
646
+ body.push(...valueChecks.map((line) => ` ${line}`));
647
+ body.push(` }`);
597
648
  }
598
649
  if (hasAdditionalProperties(schema) && isSchemaObject(schema.additionalProperties)) {
599
- const additional = schema.additionalProperties;
600
- const kv = `_ak${d}`;
601
- const valueChecks = generateValueChecks(`\${${kv}}`, `${obj}[${kv}]`, `\`${ctx.pathPrefix}/\${escapePointer(${kv})}\``, additional, suffix, ctx);
650
+ const valueChecks = valueChecksFor(schema.additionalProperties);
602
651
  if (valueChecks.length > 0) {
603
652
  const known = Object.keys(hasProperties(schema) ? schema.properties : {});
604
- lines.push(` for (const ${kv} in ${obj}) {`);
605
- if (known.length > 0)
606
- lines.push(` if (${JSON.stringify(known)}.includes(${kv})) continue`);
607
- for (const pattern of Object.keys(patternsRecord)) {
608
- lines.push(` if (${regexLiteral(pattern)}.test(${kv})) continue`);
653
+ if (known.length > 0) {
654
+ const check = unknownKeyCheck(known, `_knownKeys${ctx.hoisted.length}`);
655
+ const knownTest = check.isKnown(kv);
656
+ for (const declaration of check.declarations) {
657
+ ctx.hoisted.push({ declaration, reference: knownTest });
658
+ }
659
+ body.push(` if (${knownTest}) continue`);
609
660
  }
610
- lines.push(...valueChecks.map((line) => ` ${line}`));
611
- lines.push(` }`);
661
+ for (const pattern of patterns) {
662
+ body.push(` if (${regexLiteral(pattern)}.test(${kv})) continue`);
663
+ }
664
+ body.push(...valueChecks.map((line) => ` ${line}`));
612
665
  }
613
666
  }
614
- return lines;
667
+ if (body.length === 0)
668
+ return [];
669
+ return [` for (const ${kv} in ${obj}) {`, ...body, ` }`];
615
670
  };
616
671
  const generateInlineObjectChecks = (key, propSchema, raw, suffix, ctx) => {
617
672
  if (!isSchemaObject(propSchema))
@@ -624,7 +679,8 @@ const generateInlineObjectChecks = (key, propSchema, raw, suffix, ctx) => {
624
679
  pathPrefix: key === "" ? ctx.pathPrefix : `${ctx.pathPrefix}/${pointerSegment(key)}`,
625
680
  depth: ctx.depth + 1,
626
681
  hoisted: ctx.hoisted,
627
- rootSchema: ctx.rootSchema
682
+ rootSchema: ctx.rootSchema,
683
+ sink: ctx.sink
628
684
  };
629
685
  const required = new Set(hasRequired(propSchema) ? propSchema.required : []);
630
686
  const properties = hasProperties(propSchema) ? propSchema.properties : {};
@@ -658,7 +714,8 @@ const generatePropertyNameChecks = (nameSchema, suffix, ctx) => {
658
714
  pathPrefix: `${ctx.pathPrefix}/\${escapePointer(_name)}`,
659
715
  depth: ctx.depth + 1,
660
716
  hoisted: ctx.hoisted,
661
- rootSchema: ctx.rootSchema
717
+ rootSchema: ctx.rootSchema,
718
+ sink: ctx.sink
662
719
  };
663
720
  const checks = generateValueChecks("", "_name", at, nameSchema, suffix, nameCtx, true);
664
721
  if (checks.length === 0)
@@ -677,7 +734,7 @@ const generateDependentRequiredChecks = (schema, ctx) => {
677
734
  for (const dep of deps) {
678
735
  const msg = JSON.stringify(`must have property '${dep}' when '${trigger}' is present`);
679
736
  lines.push(` if (${hasOwnCheck(obj, trigger)} && ${missingCheck(obj, dep)}) {`);
680
- lines.push(` errors.push({ message: ${msg}, path: ${at} })`);
737
+ lines.push(` ${ctx.sink}.push({ message: ${msg}, path: ${at} })`);
681
738
  lines.push(` }`);
682
739
  }
683
740
  }
@@ -690,7 +747,7 @@ const dependentSelfBinding = (objVar, depth) => {
690
747
  const generateDependentSchemasChecks = (schema, suffix, ctx) => {
691
748
  if (!isSchemaObject(schema))
692
749
  return [];
693
- const dep = schema["dependentSchemas"];
750
+ const dep = readKey(schema, "dependentSchemas");
694
751
  if (typeof dep !== "object" || dep === null || Array.isArray(dep))
695
752
  return [];
696
753
  const obj = ctx.objVar;
@@ -703,7 +760,7 @@ const generateDependentSchemasChecks = (schema, suffix, ctx) => {
703
760
  if (sub === false) {
704
761
  const msg = JSON.stringify(`must NOT have property '${trigger}'`);
705
762
  lines.push(` if (${hasOwnCheck(obj, trigger)}) {`);
706
- lines.push(` errors.push({ message: ${msg}, path: ${at} })`);
763
+ lines.push(` ${ctx.sink}.push({ message: ${msg}, path: ${at} })`);
707
764
  lines.push(` }`);
708
765
  continue;
709
766
  }
@@ -723,7 +780,7 @@ const generateDependentSchemasChecks = (schema, suffix, ctx) => {
723
780
  const generateDependenciesChecks = (schema, suffix, ctx) => {
724
781
  if (!isSchemaObject(schema))
725
782
  return [];
726
- const dep = schema["dependencies"];
783
+ const dep = readKey(schema, "dependencies");
727
784
  if (typeof dep !== "object" || dep === null || Array.isArray(dep))
728
785
  return [];
729
786
  const obj = ctx.objVar;
@@ -737,7 +794,7 @@ const generateDependenciesChecks = (schema, suffix, ctx) => {
737
794
  continue;
738
795
  const msg = JSON.stringify(`must have property '${key}' when '${trigger}' is present`);
739
796
  lines.push(` if (${hasOwnCheck(obj, trigger)} && ${missingCheck(obj, key)}) {`);
740
- lines.push(` errors.push({ message: ${msg}, path: ${at} })`);
797
+ lines.push(` ${ctx.sink}.push({ message: ${msg}, path: ${at} })`);
741
798
  lines.push(` }`);
742
799
  }
743
800
  continue;
@@ -747,7 +804,7 @@ const generateDependenciesChecks = (schema, suffix, ctx) => {
747
804
  if (value === false) {
748
805
  const msg = JSON.stringify(`must NOT have property '${trigger}'`);
749
806
  lines.push(` if (${hasOwnCheck(obj, trigger)}) {`);
750
- lines.push(` errors.push({ message: ${msg}, path: ${at} })`);
807
+ lines.push(` ${ctx.sink}.push({ message: ${msg}, path: ${at} })`);
751
808
  lines.push(` }`);
752
809
  continue;
753
810
  }
@@ -778,13 +835,13 @@ const generateMinMaxPropertiesChecks = (schema, ctx) => {
778
835
  if (hasMin) {
779
836
  const msg = JSON.stringify(`must have at least ${schema.minProperties} properties`);
780
837
  lines.push(` if (${count} < ${schema.minProperties}) {`);
781
- lines.push(` errors.push({ message: ${msg}, path: ${at} })`);
838
+ lines.push(` ${ctx.sink}.push({ message: ${msg}, path: ${at} })`);
782
839
  lines.push(` }`);
783
840
  }
784
841
  if (hasMax) {
785
842
  const msg = JSON.stringify(`must have at most ${schema.maxProperties} properties`);
786
843
  lines.push(` if (${count} > ${schema.maxProperties}) {`);
787
- lines.push(` errors.push({ message: ${msg}, path: ${at} })`);
844
+ lines.push(` ${ctx.sink}.push({ message: ${msg}, path: ${at} })`);
788
845
  lines.push(` }`);
789
846
  }
790
847
  return lines;
@@ -792,19 +849,19 @@ const generateMinMaxPropertiesChecks = (schema, ctx) => {
792
849
  const hasObjectLevelCombinator = (schema) => {
793
850
  if (!isSchemaObject(schema))
794
851
  return false;
795
- return hasAllOf(schema) || hasAnyOf(schema) || hasOneOf(schema) || "not" in schema || "if" in schema;
852
+ return hasAllOf(schema) || hasAnyOf(schema) || hasOneOf(schema) || declaresKey(schema, "not") || declaresKey(schema, "if");
796
853
  };
797
854
  const carriesUnevaluated = (schema) => {
798
855
  if (!isSchemaObject(schema))
799
856
  return false;
800
857
  const s = schema;
801
- return "unevaluatedProperties" in s && s["unevaluatedProperties"] !== true || "unevaluatedItems" in s && s["unevaluatedItems"] !== true;
858
+ return declaresKey(s, "unevaluatedProperties") && s["unevaluatedProperties"] !== true || declaresKey(s, "unevaluatedItems") && s["unevaluatedItems"] !== true;
802
859
  };
803
860
  const guardPropConditions = (key, propSchema, objAcc) => {
804
861
  if (!isSchemaObject(propSchema))
805
862
  return null;
806
863
  const raw = safeAccessor(objAcc, key);
807
- if (hasRef(propSchema) || hasEnum(propSchema) || hasConst(propSchema) || hasOneOf(propSchema) || hasAnyOf(propSchema) || hasAllOf(propSchema) || "not" in propSchema || "if" in propSchema || getMjstInstanceOf(propSchema) !== void 0 || getMjstPrimitive(propSchema) !== void 0 || hasPattern(propSchema) || hasMinLength(propSchema) || hasMaxLength(propSchema) || hasMinimum(propSchema) || hasMaximum(propSchema) || hasExclusiveMinimum(propSchema) || hasExclusiveMaximum(propSchema) || hasMultipleOf(propSchema) || hasItems(propSchema) || carriesUnevaluated(propSchema)) {
864
+ if (hasRef(propSchema) || hasEnum(propSchema) || hasConst(propSchema) || hasOneOf(propSchema) || hasAnyOf(propSchema) || hasAllOf(propSchema) || declaresKey(propSchema, "not") || declaresKey(propSchema, "if") || getMjstInstanceOf(propSchema) !== void 0 || getMjstPrimitive(propSchema) !== void 0 || hasPattern(propSchema) || hasMinLength(propSchema) || hasMaxLength(propSchema) || hasMinimum(propSchema) || hasMaximum(propSchema) || hasExclusiveMinimum(propSchema) || hasExclusiveMaximum(propSchema) || hasMultipleOf(propSchema) || hasItems(propSchema) || carriesUnevaluated(propSchema)) {
808
865
  return null;
809
866
  }
810
867
  if (!hasType(propSchema))
@@ -844,11 +901,11 @@ const arrayRejectedByRequiredProp = (keys, required, properties) => {
844
901
  const guardObjectConditions = (schema, raw, objAcc) => {
845
902
  if (!isObjectSchema(schema))
846
903
  return null;
847
- if (hasDependentRequired(schema) || hasPropertyNames(schema) || "dependentSchemas" in schema)
904
+ if (hasDependentRequired(schema) || hasPropertyNames(schema) || declaresKey(schema, "dependentSchemas"))
848
905
  return null;
849
- if (hasMinProperties(schema) || hasMaxProperties(schema) || "dependencies" in schema)
906
+ if (hasMinProperties(schema) || hasMaxProperties(schema) || declaresKey(schema, "dependencies"))
850
907
  return null;
851
- if (isSchemaObject(schema) && "patternProperties" in schema)
908
+ if (isSchemaObject(schema) && declaresKey(schema, "patternProperties"))
852
909
  return null;
853
910
  if (hasObjectLevelCombinator(schema))
854
911
  return null;
@@ -909,10 +966,11 @@ const generateObjectValidator = (schema, typeName, suffix, rootSchema) => {
909
966
  }
910
967
  const objectCombinators = generateCombinatorChecks("", "_root", "`${_path}`", schema, suffix, ctx);
911
968
  if (objectCombinators.length > 0) {
912
- propertyLines.push(` const _root: unknown = input`);
969
+ if (readsBinding("_root", objectCombinators.join("\n")))
970
+ propertyLines.push(` const _root: unknown = input`);
913
971
  propertyLines.push(...objectCombinators);
914
972
  }
915
- const body = (propertyLines.length > 0 ? "\n" + propertyLines.join("\n") + "\n" : "").replaceAll("errors.push(", "(errors ??= []).push(");
973
+ const body = propertyLines.length > 0 ? "\n" + propertyLines.join("\n") + "\n" : "";
916
974
  const guard = guardObjectConditions(schema, "input", "obj");
917
975
  const objBinding = readsObjBinding(body) ? [` const obj = input as Record<string, unknown>`] : [];
918
976
  const collectBody = (name, exported) => [
@@ -971,10 +1029,10 @@ const booleanLeafExpr = (schema, acc, narrowable = true) => {
971
1029
  if (!isSchemaObject(schema))
972
1030
  return null;
973
1031
  const typed = (type) => narrowable ? acc : `(${acc} as ${type})`;
974
- if (hasRef(schema) || hasConst(schema) || hasOneOf(schema) || "anyOf" in schema || "allOf" in schema || "not" in schema || "if" in schema || "contains" in schema || "prefixItems" in schema || // A draft-07 tuple: the fixed positions live in an *array* `items` and the
1032
+ if (hasRef(schema) || hasConst(schema) || hasOneOf(schema) || declaresKey(schema, "anyOf") || declaresKey(schema, "allOf") || declaresKey(schema, "not") || declaresKey(schema, "if") || declaresKey(schema, "contains") || declaresKey(schema, "prefixItems") || // A draft-07 tuple: the fixed positions live in an *array* `items` and the
975
1033
  // tail in `additionalItems`. Neither is expressible flat, and reading past
976
1034
  // them let the guard accept tuples `validateX` rejects.
977
- Array.isArray(schema["items"]) || "additionalItems" in schema || carriesUnevaluated(schema) || getMjstInstanceOf(schema) !== void 0 || getMjstPrimitive(schema) !== void 0) {
1035
+ Array.isArray(readKey(schema, "items")) || declaresKey(schema, "additionalItems") || carriesUnevaluated(schema) || getMjstInstanceOf(schema) !== void 0 || getMjstPrimitive(schema) !== void 0) {
978
1036
  return null;
979
1037
  }
980
1038
  if (!hasType(schema)) {
@@ -1045,10 +1103,10 @@ const booleanArrayExpr = (schema, acc, narrowable = true) => {
1045
1103
  if (hasMaxItems(schema))
1046
1104
  parts.push(`${arr}.length <= ${schema.maxItems}`);
1047
1105
  if (hasUniqueItems(schema) && schema.uniqueItems === true) {
1048
- parts.push(arrayItemsAreScalarOnly(schema) ? `new Set((${acc} as unknown[]).map((_u) => JSON.stringify(_u))).size === ${arr}.length` : `allUnique(${acc} as unknown[])`);
1106
+ parts.push(arrayItemsAreScalarOnly(schema) ? `new Set(${acc} as unknown[]).size === ${arr}.length` : `allUnique(${acc} as unknown[])`);
1049
1107
  }
1050
1108
  const base = parts.join(" && ");
1051
- if (schema["items"] === false)
1109
+ if (readKey(schema, "items") === false)
1052
1110
  return `${base} && ${arr}.length === 0`;
1053
1111
  if (!hasItems(schema))
1054
1112
  return base;
@@ -1069,11 +1127,11 @@ const booleanObjectParts = (schema, raw, objAcc, narrowable = true) => {
1069
1127
  return null;
1070
1128
  if (getMjstInstanceOf(schema) !== void 0 || getMjstPrimitive(schema) !== void 0)
1071
1129
  return null;
1072
- if (hasDependentRequired(schema) || hasPropertyNames(schema) || "dependentSchemas" in schema)
1130
+ if (hasDependentRequired(schema) || hasPropertyNames(schema) || declaresKey(schema, "dependentSchemas"))
1073
1131
  return null;
1074
- if (hasMinProperties(schema) || hasMaxProperties(schema) || "dependencies" in schema)
1132
+ if (hasMinProperties(schema) || hasMaxProperties(schema) || declaresKey(schema, "dependencies"))
1075
1133
  return null;
1076
- if (isSchemaObject(schema) && "patternProperties" in schema)
1134
+ if (isSchemaObject(schema) && declaresKey(schema, "patternProperties"))
1077
1135
  return null;
1078
1136
  if (hasObjectLevelCombinator(schema))
1079
1137
  return null;
@@ -1125,16 +1183,16 @@ const typeDescribesEveryAcceptedValue = (schema) => {
1125
1183
  if (!isSchemaObject(schema))
1126
1184
  return true;
1127
1185
  const s = schema;
1128
- if ("type" in s || "enum" in s || "const" in s || "$ref" in s)
1186
+ if (declaresKey(s, "type") || declaresKey(s, "enum") || declaresKey(s, "const") || declaresKey(s, "$ref"))
1129
1187
  return true;
1130
1188
  for (const keyword of ["allOf", "anyOf", "oneOf"]) {
1131
- const branches = s[keyword];
1189
+ const branches = readKey(s, keyword);
1132
1190
  if (!Array.isArray(branches))
1133
1191
  continue;
1134
1192
  if (!branches.every((branch) => typeDescribesEveryAcceptedValue(branch)))
1135
1193
  return false;
1136
1194
  }
1137
- return !IMPLICIT_OBJECT_KEYWORDS.some((keyword) => keyword in s);
1195
+ return !IMPLICIT_OBJECT_KEYWORDS.some((keyword) => declaresKey(s, keyword));
1138
1196
  };
1139
1197
  const generateBooleanGuard = (schema, typeName, _suffix = "") => {
1140
1198
  const name = guardName(typeName);
@@ -1237,7 +1295,7 @@ const generateScalarValidator = (schema, typeName, suffix, rootSchema) => {
1237
1295
  `}`
1238
1296
  ].join("\n");
1239
1297
  }
1240
- if (hasAllOf(schema) || hasAnyOf(schema) || hasOneOf(schema) || "not" in schema || "if" in schema) {
1298
+ if (hasAllOf(schema) || hasAnyOf(schema) || hasOneOf(schema) || declaresKey(schema, "not") || declaresKey(schema, "if")) {
1241
1299
  const ctx = createRootContext(rootSchema);
1242
1300
  const checks = [];
1243
1301
  const rootPath = "`${_path}`";
@@ -1247,7 +1305,7 @@ const generateScalarValidator = (schema, typeName, suffix, rootSchema) => {
1247
1305
  if (allWrong) {
1248
1306
  const label = rootTypeArray2.map((t) => typeofString(t)).join(" or ");
1249
1307
  checks.push(` if (${allWrong}) {`);
1250
- checks.push(` errors.push({ message: ${JSON.stringify(`must be ${label}`)}, path: ${rootPath} })`);
1308
+ checks.push(` ${ctx.sink}.push({ message: ${JSON.stringify(`must be ${label}`)}, path: ${rootPath} })`);
1251
1309
  checks.push(` }`);
1252
1310
  }
1253
1311
  } else if (hasType(schema)) {
@@ -1255,13 +1313,13 @@ const generateScalarValidator = (schema, typeName, suffix, rootSchema) => {
1255
1313
  const wrongType = wrongTypeCondition("input", t);
1256
1314
  if (wrongType) {
1257
1315
  checks.push(` if (${wrongType}) {`);
1258
- checks.push(` errors.push({ message: 'must be ${typeofString(t)}', path: ${rootPath} })`);
1316
+ checks.push(` ${ctx.sink}.push({ message: 'must be ${typeofString(t)}', path: ${rootPath} })`);
1259
1317
  checks.push(` }`);
1260
1318
  }
1261
1319
  }
1262
1320
  checks.push(...generateConstraintChecks("", "input", rootPath, schema, suffix, ctx));
1263
1321
  checks.push(...generateCombinatorChecks("", "input", rootPath, schema, suffix, ctx));
1264
- const body = checks.join("\n").replaceAll("errors.push(", "(errors ??= []).push(");
1322
+ const body = checks.join("\n");
1265
1323
  return withHoisted(ctx.hoisted, [
1266
1324
  `export const ${vName} = (input: unknown, _path = ''): ValidationResult => {`,
1267
1325
  ` let errors: ValidationError[] | undefined`,
@@ -1279,7 +1337,7 @@ const generateScalarValidator = (schema, typeName, suffix, rootSchema) => {
1279
1337
  if (allWrong) {
1280
1338
  const label = rootTypeArray.map((t) => typeofString(t)).join(" or ");
1281
1339
  checks.push(` if (${allWrong}) {`);
1282
- checks.push(` errors.push({ message: ${JSON.stringify(`must be ${label}`)}, path: ${rootPath} })`);
1340
+ checks.push(` ${ctx.sink}.push({ message: ${JSON.stringify(`must be ${label}`)}, path: ${rootPath} })`);
1283
1341
  checks.push(` }`);
1284
1342
  }
1285
1343
  checks.push(...generateConstraintChecks("", "input", rootPath, schema, suffix, ctx));
@@ -1290,7 +1348,7 @@ const generateScalarValidator = (schema, typeName, suffix, rootSchema) => {
1290
1348
  `}`
1291
1349
  ].join("\n");
1292
1350
  }
1293
- const body = checks.join("\n").replaceAll("errors.push(", "(errors ??= []).push(");
1351
+ const body = checks.join("\n");
1294
1352
  return withHoisted(ctx.hoisted, [
1295
1353
  `export const ${vName} = (input: unknown, _path = ''): ValidationResult => {`,
1296
1354
  ` let errors: ValidationError[] | undefined`,
@@ -1327,9 +1385,9 @@ const generateScalarValidator = (schema, typeName, suffix, rootSchema) => {
1327
1385
  ` if (${wrongType}) {`,
1328
1386
  ` return { valid: false, errors: [{ message: 'must be ${typLabel}', path: _path }] }`,
1329
1387
  ` }`,
1330
- ` const _root: unknown = input`,
1388
+ ...readsBinding("_root", constraintLines.join("\n")) ? [` const _root: unknown = input`] : [],
1331
1389
  ` let errors: ValidationError[] | undefined`,
1332
- constraintLines.join("\n").replaceAll("errors.push(", "(errors ??= []).push("),
1390
+ constraintLines.join("\n"),
1333
1391
  ` return errors !== undefined ? { valid: false, errors } : true`,
1334
1392
  `}`
1335
1393
  ].join("\n"));
@@ -1342,7 +1400,7 @@ const generateScalarValidator = (schema, typeName, suffix, rootSchema) => {
1342
1400
  return withHoisted(typelessCtx.hoisted, [
1343
1401
  `export const ${vName} = (input: unknown, _path = ''): ValidationResult => {`,
1344
1402
  ` let errors: ValidationError[] | undefined`,
1345
- typelessChecks.join("\n").replaceAll("errors.push(", "(errors ??= []).push("),
1403
+ typelessChecks.join("\n"),
1346
1404
  ` return errors !== undefined ? { valid: false, errors } : true`,
1347
1405
  `}`
1348
1406
  ].join("\n"));
@@ -1350,7 +1408,7 @@ const generateScalarValidator = (schema, typeName, suffix, rootSchema) => {
1350
1408
  const generateGeneralRootValidator = (schema, typeName, suffix, rootSchema) => {
1351
1409
  const ctx = createRootContext(rootSchema);
1352
1410
  const checks = generateValueChecks("", "input", "`${_path}`", schema, suffix, ctx, true);
1353
- const body = checks.join("\n").replaceAll("errors.push(", "(errors ??= []).push(");
1411
+ const body = checks.join("\n");
1354
1412
  return withHoisted(ctx.hoisted, [
1355
1413
  `export const ${validatorName(typeName)} = (input: unknown, _path = ''): ValidationResult => {`,
1356
1414
  ` let errors: ValidationError[] | undefined`,
@@ -1410,7 +1468,7 @@ const rewriteNullable = (node) => {
1410
1468
  defineEntry(out, key, value);
1411
1469
  }
1412
1470
  }
1413
- if (src["nullable"] === true)
1471
+ if (readKey(src, "nullable") === true)
1414
1472
  return { anyOf: [{ type: "null" }, out] };
1415
1473
  return out;
1416
1474
  };