@amritk/generate-validators 0.12.1 → 0.13.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,5 +1,5 @@
1
1
  import { regexFlagsFor, regexLiteral } from "@amritk/helpers/escape-regex-pattern";
2
- import { getMjstInstanceOf, getMjstPrimitive } from "@amritk/helpers/mjst-extension";
2
+ import { getMjstInstanceOf, getMjstPrimitive, MJST_EXTENSION_KEY } from "@amritk/helpers/mjst-extension";
3
3
  import { multipleOfFailExpr, multipleOfPassExpr } from "@amritk/helpers/multiple-of-check";
4
4
  import { refToName } from "@amritk/helpers/ref-to-name";
5
5
  import { safeAccessor } from "@amritk/helpers/safe-accessor";
@@ -8,6 +8,8 @@ import { maxLengthFailExpr, maxLengthPassExpr, minLengthFailExpr, minLengthPassE
8
8
  import { unknownKeyCheck } from "@amritk/helpers/unknown-key-check";
9
9
  import { assertGeneratableRefs } from "./assert-generatable-refs.js";
10
10
  import { assertUnevaluatedGeneratable, UNPROVABLE_COVERAGE_MESSAGE } from "./assert-unevaluated-generatable.js";
11
+ import { declaresKeywordOutside } from "./enforced-keywords.js";
12
+ import { tupleShapeOf } from "./tuple-shape.js";
11
13
  import { unevaluatedItemsExpr, unevaluatedPropertiesExpr } from "./unevaluated-match.js";
12
14
  const validatorName = (typeName) => `validate${typeName}`;
13
15
  const propertyRead = (objVar, key) => safeAccessor(objVar, key);
@@ -54,16 +56,15 @@ const schemaIsScalarOnly = (schema) => {
54
56
  return types.length > 0 && types.every((x) => typeof x === "string" && SCALAR_ITEM_TYPES.has(x));
55
57
  };
56
58
  const arrayItemsAreScalarOnly = (schema) => {
57
- const prefix = schema["prefixItems"];
58
- if (Array.isArray(prefix)) {
59
- if (!prefix.every((p) => schemaIsScalarOnly(p)))
59
+ const { tuple, tail, tailIsClosed } = tupleShapeOf(schema);
60
+ if (tuple !== void 0) {
61
+ if (!tuple.every((p) => schemaIsScalarOnly(p)))
60
62
  return false;
61
- const tail = "items" in schema ? schema["items"] : schema["additionalItems"];
62
- if (tail === false)
63
+ if (tailIsClosed)
63
64
  return true;
64
65
  return schemaIsScalarOnly(tail);
65
66
  }
66
- return schemaIsScalarOnly(schema["items"]);
67
+ return schemaIsScalarOnly(tail);
67
68
  };
68
69
  const wrongTypeCondition = (accessor, type) => {
69
70
  switch (type) {
@@ -86,6 +87,7 @@ const wrongTypeCondition = (accessor, type) => {
86
87
  }
87
88
  };
88
89
  const declaresObjectType = (schema) => hasType(schema) && schema.type === "object";
90
+ const objectRootIsSelfContained = (schema) => !hasRef(schema) && !hasConst(schema) && !hasEnum(schema) && getMjstInstanceOf(schema) === void 0 && getMjstPrimitive(schema) === void 0;
89
91
  const getTypeArray = (schema) => {
90
92
  if (!isSchemaObject(schema) || !("type" in schema) || !Array.isArray(schema.type))
91
93
  return null;
@@ -107,6 +109,15 @@ const patternPropertySources = (schema) => {
107
109
  return [];
108
110
  return Object.keys(patterns);
109
111
  };
112
+ const readsObjBinding = (text) => /\bobj\b/.test(text);
113
+ const withHoisted = (hoisted, text) => {
114
+ 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");
117
+ return kept.length > 0 ? `${kept.join("\n")}
118
+
119
+ ${body}` : body;
120
+ };
110
121
  const generateStrictKeyChecks = (schema, ctx) => {
111
122
  if (!isSchemaObject(schema))
112
123
  return [];
@@ -114,20 +125,27 @@ const generateStrictKeyChecks = (schema, ctx) => {
114
125
  return [];
115
126
  const known = Object.keys(hasProperties(schema) ? schema.properties : {});
116
127
  const d = ctx.depth;
117
- const check = unknownKeyCheck(known, `_knownKeys${ctx.hoisted.length}`);
118
- ctx.hoisted.push(...check.declarations);
128
+ const knownKeysName = `_knownKeys${ctx.hoisted.length}`;
129
+ const check = unknownKeyCheck(known, knownKeysName);
130
+ const unknownTest = check.isUnknown(`_key${d}`);
131
+ for (const declaration of check.declarations) {
132
+ ctx.hoisted.push({ declaration, reference: unknownTest });
133
+ }
119
134
  const patterns = patternPropertySources(schema);
120
135
  let patternGuard = "";
121
136
  if (patterns.length > 0) {
122
137
  const patternsName = `_patterns${ctx.hoisted.length}`;
123
138
  const compiled = patterns.map((p) => `new RegExp(${JSON.stringify(p)}, ${JSON.stringify(regexFlagsFor(p))})`);
124
- ctx.hoisted.push(`const ${patternsName} = [${compiled.join(", ")}]`);
125
139
  patternGuard = ` && !${patternsName}.some((re) => re.test(_key${d}))`;
140
+ ctx.hoisted.push({
141
+ declaration: `const ${patternsName} = [${compiled.join(", ")}]`,
142
+ reference: patternGuard
143
+ });
126
144
  }
127
145
  return [
128
146
  ` for (const _key${d} in ${ctx.objVar}) {`,
129
- ` if (${check.isUnknown(`_key${d}`)}${patternGuard}) {`,
130
- ` errors.push({ message: 'must NOT have additional properties', path: \`${ctx.pathPrefix}/\${_key${d}}\` })`,
147
+ ` if (${unknownTest}${patternGuard}) {`,
148
+ ` errors.push({ message: 'must NOT have additional properties', path: \`${ctx.pathPrefix}/\${escapePointer(_key${d})}\` })`,
131
149
  ` }`,
132
150
  ` }`
133
151
  ];
@@ -179,172 +197,109 @@ const generatePropertyChecks = (key, propSchema, isRequired, suffix, ctx) => {
179
197
  return lines;
180
198
  return [` const ${protoLocalName(key, ctx.depth)} = ${propertyRead(ctx.objVar, key)}`, ...lines];
181
199
  };
182
- const generatePropertyCheckLines = (key, propSchema, isRequired, suffix, ctx) => {
183
- if (!isSchemaObject(propSchema)) {
184
- if (isRequired && propSchema === true) {
185
- const parentPath2 = ctx.depth === 0 ? "_path" : `\`${ctx.pathPrefix}\``;
186
- return [
187
- ` if (${missingCheck(ctx.objVar, key)}) {`,
188
- ` errors.push({ message: ${JSON.stringify(`must have required property '${key}'`)}, path: ${parentPath2} })`,
189
- ` }`
190
- ];
191
- }
192
- return [];
200
+ const singleIfBlock = (lines) => {
201
+ if (lines.length < 3)
202
+ return null;
203
+ const opener = /^ {2}if \((.*)\) \{$/.exec(lines[0]);
204
+ if (opener === null || lines[lines.length - 1] !== " }")
205
+ return null;
206
+ for (let i = 1; i < lines.length - 1; i++) {
207
+ if (!lines[i].startsWith(" "))
208
+ return null;
193
209
  }
194
- const raw = PROTOTYPE_MEMBERS.has(key) ? protoLocalName(key, ctx.depth) : propertyRead(ctx.objVar, key);
195
- const path = `\`${ctx.pathPrefix}/${pointerSegment(key)}\``;
196
- const parentPath = ctx.depth === 0 ? "_path" : `\`${ctx.pathPrefix}\``;
210
+ return { condition: opener[1], body: lines.slice(1, -1) };
211
+ };
212
+ const generateKeywordChecks = (key, raw, path, schema, suffix, ctx, presence) => {
213
+ if (!isSchemaObject(schema))
214
+ return [];
197
215
  const lines = [];
198
- if (hasRef(propSchema)) {
199
- const ref = propSchema.$ref;
200
- const vName = validatorName(refToName(ref, suffix));
201
- const siblings = [
202
- ...generateConstraintChecks(key, raw, path, propSchema, suffix, ctx),
203
- ...generateCombinatorChecks(key, raw, path, propSchema, suffix, ctx)
204
- ];
205
- const delegate = [
206
- ` const _r = ${vName}(${raw}, ${path})`,
207
- ` if (_r !== true) errors.push(..._r.errors)`,
208
- ...siblings
209
- ];
210
- if (isRequired) {
211
- lines.push(` if (${missingCheck(ctx.objVar, key)}) {`);
212
- lines.push(` errors.push({ message: ${JSON.stringify(`must have required property '${key}'`)}, path: ${parentPath} })`);
213
- lines.push(` } else {`);
214
- lines.push(...delegate);
215
- lines.push(` }`);
216
- } else {
217
- lines.push(` if (${raw} !== undefined) {`);
216
+ if (hasRef(schema)) {
217
+ const vName = validatorName(refToName(schema.$ref, suffix));
218
+ const delegate = [` const _r = ${vName}(${raw}, ${path})`, ` if (_r !== true) errors.push(..._r.errors)`];
219
+ if (presence === "")
218
220
  lines.push(...delegate);
219
- lines.push(` }`);
220
- }
221
- return lines;
221
+ else
222
+ lines.push(` if (${raw} !== undefined) {`, ...delegate.map((line) => ` ${line}`), ` }`);
222
223
  }
223
- const instanceOf = getMjstInstanceOf(propSchema);
224
+ const instanceOf = getMjstInstanceOf(schema);
224
225
  if (instanceOf) {
225
- if (isRequired) {
226
- lines.push(` if (${missingCheck(ctx.objVar, key)}) {`);
227
- lines.push(` errors.push({ message: ${JSON.stringify(`must have required property '${key}'`)}, path: ${parentPath} })`);
228
- lines.push(` } else if (!(${raw} instanceof ${instanceOf})) {`);
229
- lines.push(` errors.push({ message: 'must be ${instanceOf}', path: ${path} })`);
230
- lines.push(` }`);
231
- } else {
232
- lines.push(` if (${raw} !== undefined && !(${raw} instanceof ${instanceOf})) {`);
233
- lines.push(` errors.push({ message: 'must be ${instanceOf}', path: ${path} })`);
234
- lines.push(` }`);
235
- }
236
- return lines;
226
+ lines.push(` if (${presence}!(${raw} instanceof ${instanceOf})) {`);
227
+ lines.push(` errors.push({ message: 'must be ${instanceOf}', path: ${path} })`);
228
+ lines.push(` }`);
237
229
  }
238
- const primitive = getMjstPrimitive(propSchema);
230
+ const primitive = getMjstPrimitive(schema);
239
231
  if (primitive) {
240
- if (isRequired) {
241
- lines.push(` if (${missingCheck(ctx.objVar, key)}) {`);
242
- lines.push(` errors.push({ message: ${JSON.stringify(`must have required property '${key}'`)}, path: ${parentPath} })`);
243
- lines.push(` } else if (typeof ${raw} !== "${primitive}") {`);
244
- lines.push(` errors.push({ message: 'must be ${primitive}', path: ${path} })`);
245
- lines.push(` }`);
246
- } else {
247
- lines.push(` if (${raw} !== undefined && typeof ${raw} !== "${primitive}") {`);
248
- lines.push(` errors.push({ message: 'must be ${primitive}', path: ${path} })`);
249
- lines.push(` }`);
250
- }
251
- return lines;
232
+ lines.push(` if (${presence}typeof ${raw} !== "${primitive}") {`);
233
+ lines.push(` errors.push({ message: 'must be ${primitive}', path: ${path} })`);
234
+ lines.push(` }`);
252
235
  }
253
- if (hasConst(propSchema)) {
254
- const mismatch = constMismatchCondition(raw, propSchema.const);
255
- const msg = JSON.stringify(`must be ${JSON.stringify(propSchema.const)}`);
256
- if (isRequired) {
257
- lines.push(` if (${missingCheck(ctx.objVar, key)}) {`);
258
- lines.push(` errors.push({ message: ${JSON.stringify(`must have required property '${key}'`)}, path: ${parentPath} })`);
259
- lines.push(` } else if (${mismatch}) {`);
260
- lines.push(` errors.push({ message: ${msg}, path: ${path} })`);
261
- lines.push(` }`);
262
- } else {
263
- lines.push(` if (${raw} !== undefined && ${mismatch}) {`);
264
- lines.push(` errors.push({ message: ${msg}, path: ${path} })`);
265
- lines.push(` }`);
266
- }
267
- return lines;
236
+ if (hasConst(schema)) {
237
+ const mismatch = constMismatchCondition(raw, schema.const);
238
+ const msg = JSON.stringify(`must be ${JSON.stringify(schema.const)}`);
239
+ lines.push(` if (${presence}${mismatch}) {`);
240
+ lines.push(` errors.push({ message: ${msg}, path: ${path} })`);
241
+ lines.push(` }`);
268
242
  }
269
- if (hasEnum(propSchema)) {
270
- const member = enumMembershipExpr(propSchema.enum, raw);
271
- const label = propSchema.enum.map((v) => JSON.stringify(v)).join(", ");
272
- if (isRequired) {
273
- lines.push(` if (${missingCheck(ctx.objVar, key)}) {`);
274
- lines.push(` errors.push({ message: ${JSON.stringify(`must have required property '${key}'`)}, path: ${parentPath} })`);
275
- lines.push(` } else if (!${member}) {`);
276
- lines.push(` errors.push({ message: ${JSON.stringify(`must be one of: ${label}`)}, path: ${path} })`);
277
- lines.push(` }`);
278
- } else {
279
- lines.push(` if (${raw} !== undefined && !${member}) {`);
280
- lines.push(` errors.push({ message: ${JSON.stringify(`must be one of: ${label}`)}, path: ${path} })`);
281
- lines.push(` }`);
282
- }
283
- return lines;
243
+ if (hasEnum(schema)) {
244
+ const label = schema.enum.map((v) => JSON.stringify(v)).join(", ");
245
+ lines.push(` if (${presence}!${enumMembershipExpr(schema.enum, raw)}) {`);
246
+ lines.push(` errors.push({ message: ${JSON.stringify(`must be one of: ${label}`)}, path: ${path} })`);
247
+ lines.push(` }`);
284
248
  }
285
- const typeArray = getTypeArray(propSchema);
286
- if (typeArray) {
287
- const allWrong = typeArray.map((t) => wrongTypeCondition(raw, t)).filter((c) => c !== "").map((c) => `(${c})`).join(" && ");
288
- const label = typeArray.map((t) => typeofString(t)).join(" or ");
289
- if (isRequired) {
290
- lines.push(` if (${missingCheck(ctx.objVar, key)}) {`);
291
- lines.push(` errors.push({ message: ${JSON.stringify(`must have required property '${key}'`)}, path: ${parentPath} })`);
249
+ if (instanceOf === void 0 && primitive === void 0) {
250
+ if (hasType(schema)) {
251
+ const t = schema.type;
252
+ const wrongType = wrongTypeCondition(raw, t);
253
+ if (wrongType) {
254
+ lines.push(` if (${presence === "" ? wrongType : `${presence}(${wrongType})`}) {`);
255
+ lines.push(` errors.push({ message: 'must be ${typeofString(t)}', path: ${path} })`);
256
+ lines.push(` }`);
257
+ }
258
+ }
259
+ const typeArray = getTypeArray(schema);
260
+ if (typeArray) {
261
+ const allWrong = typeArray.map((t) => wrongTypeCondition(raw, t)).filter((c) => c !== "").map((c) => `(${c})`).join(" && ");
292
262
  if (allWrong) {
293
- lines.push(` } else if (${allWrong}) {`);
263
+ const label = typeArray.map((t) => typeofString(t)).join(" or ");
264
+ lines.push(` if (${presence === "" ? allWrong : `${presence}(${allWrong})`}) {`);
294
265
  lines.push(` errors.push({ message: ${JSON.stringify(`must be ${label}`)}, path: ${path} })`);
266
+ lines.push(` }`);
295
267
  }
296
- lines.push(` }`);
297
- } else if (allWrong) {
298
- lines.push(` if (${raw} !== undefined && (${allWrong})) {`);
299
- lines.push(` errors.push({ message: ${JSON.stringify(`must be ${label}`)}, path: ${path} })`);
300
- lines.push(` }`);
301
268
  }
302
- lines.push(...generateConstraintChecks(key, raw, path, propSchema, suffix, ctx));
303
- return lines;
304
269
  }
305
- if (hasType(propSchema)) {
306
- const t = propSchema.type;
307
- const wrongType = wrongTypeCondition(raw, t);
308
- const typLabel = typeofString(t);
309
- if (isRequired) {
310
- lines.push(` if (${missingCheck(ctx.objVar, key)}) {`);
311
- lines.push(` errors.push({ message: ${JSON.stringify(`must have required property '${key}'`)}, path: ${parentPath} })`);
312
- if (wrongType) {
313
- lines.push(` } else if (${wrongType}) {`);
314
- lines.push(` errors.push({ message: 'must be ${typLabel}', path: ${path} })`);
315
- }
316
- lines.push(` }`);
317
- } else if (wrongType) {
318
- lines.push(` if (${raw} !== undefined && (${wrongType})) {`);
319
- lines.push(` errors.push({ message: 'must be ${typLabel}', path: ${path} })`);
320
- lines.push(` }`);
321
- }
322
- lines.push(...generateConstraintChecks(key, raw, path, propSchema, suffix, ctx));
270
+ lines.push(...generateConstraintChecks(key, raw, path, schema, suffix, ctx));
271
+ const combinators = generateCombinatorChecks(key, raw, path, schema, suffix, ctx);
272
+ if (combinators.length > 0) {
273
+ if (presence === "")
274
+ lines.push(...combinators);
275
+ else
276
+ lines.push(` if (${raw} !== undefined) {`, ...combinators.map((line) => ` ${line}`), ` }`);
323
277
  }
324
- const extraLines = hasType(propSchema) ? generateCombinatorChecks(key, raw, path, propSchema, suffix, ctx) : [
325
- ...generateConstraintChecks(key, raw, path, propSchema, suffix, ctx),
326
- ...generateCombinatorChecks(key, raw, path, propSchema, suffix, ctx)
278
+ return lines;
279
+ };
280
+ const generatePropertyCheckLines = (key, propSchema, isRequired, suffix, ctx) => {
281
+ const parentPath = ctx.depth === 0 ? "_path" : `\`${ctx.pathPrefix}\``;
282
+ const missing = [
283
+ ` if (${missingCheck(ctx.objVar, key)}) {`,
284
+ ` errors.push({ message: ${JSON.stringify(`must have required property '${key}'`)}, path: ${parentPath} })`
327
285
  ];
328
- if (hasType(propSchema)) {
329
- if (extraLines.length > 0) {
330
- lines.push(` if (${raw} !== undefined) {`);
331
- lines.push(...extraLines);
332
- lines.push(` }`);
333
- }
334
- } else if (isRequired) {
335
- lines.push(` if (${missingCheck(ctx.objVar, key)}) {`);
336
- lines.push(` errors.push({ message: ${JSON.stringify(`must have required property '${key}'`)}, path: ${parentPath} })`);
337
- if (extraLines.length > 0) {
338
- lines.push(` } else {`);
339
- lines.push(...extraLines);
340
- }
341
- lines.push(` }`);
342
- } else if (extraLines.length > 0) {
343
- lines.push(` if (${raw} !== undefined) {`);
344
- lines.push(...extraLines);
345
- lines.push(` }`);
286
+ if (!isSchemaObject(propSchema)) {
287
+ if (isRequired && propSchema === true)
288
+ return [...missing, ` }`];
289
+ return [];
346
290
  }
347
- return lines;
291
+ const raw = PROTOTYPE_MEMBERS.has(key) ? protoLocalName(key, ctx.depth) : propertyRead(ctx.objVar, key);
292
+ const path = `\`${ctx.pathPrefix}/${pointerSegment(key)}\``;
293
+ if (isRequired) {
294
+ const valueLines = generateKeywordChecks(key, raw, path, propSchema, suffix, ctx, "");
295
+ if (valueLines.length === 0)
296
+ return [...missing, ` }`];
297
+ const single = singleIfBlock(valueLines);
298
+ if (single !== null)
299
+ return [...missing, ` } else if (${single.condition}) {`, ...single.body, ` }`];
300
+ return [...missing, ` } else {`, ...valueLines.map((line) => ` ${line}`), ` }`];
301
+ }
302
+ return generateKeywordChecks(key, raw, path, propSchema, suffix, ctx, `${raw} !== undefined && `);
348
303
  };
349
304
  const generateConstraintChecks = (key, raw, path, propSchema, suffix, ctx) => {
350
305
  if (!isSchemaObject(propSchema))
@@ -359,7 +314,7 @@ const generateConstraintChecks = (key, raw, path, propSchema, suffix, ctx) => {
359
314
  lines.push(` errors.push({ message: ${msg}, path: ${path} })`);
360
315
  lines.push(` }`);
361
316
  }
362
- if (hasMinLength(propSchema)) {
317
+ if (hasMinLength(propSchema) && propSchema.minLength > 0) {
363
318
  lines.push(` if (typeof ${raw} === 'string' && ${minLengthFailExpr(raw, propSchema.minLength)}) {`);
364
319
  lines.push(` errors.push({ message: 'must have at least ${propSchema.minLength} characters', path: ${path} })`);
365
320
  lines.push(` }`);
@@ -401,13 +356,13 @@ const generateConstraintChecks = (key, raw, path, propSchema, suffix, ctx) => {
401
356
  lines.push(` }`);
402
357
  }
403
358
  }
404
- if (hasItems(propSchema)) {
405
- const itemSchema = propSchema.items;
406
- const prefix = sp["prefixItems"];
407
- const firstTailIndex = Array.isArray(prefix) ? prefix.length : 0;
359
+ const { tuple, tail, tailIsClosed } = tupleShapeOf(sp);
360
+ if (tail !== void 0) {
361
+ const itemSchema = tail;
362
+ const firstTailIndex = tuple !== void 0 ? tuple.length : 0;
408
363
  const iv = `_i${ctx.depth}`;
409
364
  const itemPath = `\`${path.slice(1, -1)}/\${${iv}}\``;
410
- if (hasRef(itemSchema)) {
365
+ if (hasRef(itemSchema) && !declaresKeywordOutside(itemSchema, ["$ref"])) {
411
366
  const vName = validatorName(refToName(itemSchema.$ref, suffix));
412
367
  lines.push(` if (Array.isArray(${raw})) {`);
413
368
  lines.push(` for (let ${iv} = ${firstTailIndex}; ${iv} < ${raw}.length; ${iv}++) {`);
@@ -428,8 +383,8 @@ const generateConstraintChecks = (key, raw, path, propSchema, suffix, ctx) => {
428
383
  }
429
384
  }
430
385
  }
431
- if (hasMinItems(propSchema) || hasMaxItems(propSchema) || hasUniqueItems(propSchema) && propSchema.uniqueItems === true || "contains" in sp || Array.isArray(sp["prefixItems"]) || sp["items"] === false && !Array.isArray(sp["prefixItems"])) {
432
- if (sp["items"] === false && !Array.isArray(sp["prefixItems"])) {
386
+ if (hasMinItems(propSchema) || hasMaxItems(propSchema) || hasUniqueItems(propSchema) && propSchema.uniqueItems === true || "contains" in sp || tuple !== void 0 || tail === false && tuple === void 0) {
387
+ if (tail === false && tuple === void 0) {
433
388
  lines.push(` if (Array.isArray(${raw}) && ${raw}.length > 0) {`);
434
389
  lines.push(` errors.push({ message: 'must NOT have more than 0 items', path: ${path} })`);
435
390
  lines.push(` }`);
@@ -462,20 +417,19 @@ const generateConstraintChecks = (key, raw, path, propSchema, suffix, ctx) => {
462
417
  lines.push(` }`);
463
418
  lines.push(` }`);
464
419
  }
465
- const prefix = sp["prefixItems"];
466
- if (Array.isArray(prefix)) {
420
+ if (tuple !== void 0) {
467
421
  lines.push(` if (Array.isArray(${raw})) {`);
468
- for (let i = 0; i < prefix.length; i++) {
469
- const itemChecks = generateValueChecks("", `${raw}[${i}]`, `\`${path.slice(1, -1)}/${i}\``, prefix[i], suffix, ctx);
422
+ for (let i = 0; i < tuple.length; i++) {
423
+ const itemChecks = generateValueChecks("", `${raw}[${i}]`, `\`${path.slice(1, -1)}/${i}\``, tuple[i], suffix, ctx, true);
470
424
  if (itemChecks.length > 0) {
471
425
  lines.push(` if (${raw}.length > ${i}) {`);
472
426
  lines.push(...itemChecks.map((l) => ` ${l}`));
473
427
  lines.push(` }`);
474
428
  }
475
429
  }
476
- if (sp["items"] === false || sp["additionalItems"] === false) {
477
- lines.push(` if (${raw}.length > ${prefix.length}) {`);
478
- lines.push(` errors.push({ message: 'must NOT have more than ${prefix.length} items', path: ${path} })`);
430
+ if (tailIsClosed) {
431
+ lines.push(` if (${raw}.length > ${tuple.length}) {`);
432
+ lines.push(` errors.push({ message: 'must NOT have more than ${tuple.length} items', path: ${path} })`);
479
433
  lines.push(` }`);
480
434
  }
481
435
  lines.push(` }`);
@@ -503,68 +457,6 @@ const generateValueCheckLines = (_key, raw, path, propSchema, suffix, ctx, requi
503
457
  if (!isSchemaObject(propSchema))
504
458
  return [];
505
459
  const presence = required ? "" : `${raw} !== undefined && `;
506
- if (hasRef(propSchema)) {
507
- const vName = validatorName(refToName(propSchema.$ref, suffix));
508
- if (required) {
509
- lines.push(` const _r = ${vName}(${raw}, ${path})`);
510
- lines.push(` if (_r !== true) errors.push(..._r.errors)`);
511
- } else {
512
- lines.push(` if (${raw} !== undefined) {`);
513
- lines.push(` const _r = ${vName}(${raw}, ${path})`);
514
- lines.push(` if (_r !== true) errors.push(..._r.errors)`);
515
- lines.push(` }`);
516
- }
517
- return lines;
518
- }
519
- const instanceOf = getMjstInstanceOf(propSchema);
520
- if (instanceOf) {
521
- lines.push(` if (${presence}!(${raw} instanceof ${instanceOf})) {`);
522
- lines.push(` errors.push({ message: 'must be ${instanceOf}', path: ${path} })`);
523
- lines.push(` }`);
524
- return lines;
525
- }
526
- const primitive = getMjstPrimitive(propSchema);
527
- if (primitive) {
528
- lines.push(` if (${presence}typeof ${raw} !== "${primitive}") {`);
529
- lines.push(` errors.push({ message: 'must be ${primitive}', path: ${path} })`);
530
- lines.push(` }`);
531
- return lines;
532
- }
533
- if (hasConst(propSchema)) {
534
- const mismatch = constMismatchCondition(raw, propSchema.const);
535
- const msg = JSON.stringify(`must be ${JSON.stringify(propSchema.const)}`);
536
- lines.push(` if (${presence}${mismatch}) {`);
537
- lines.push(` errors.push({ message: ${msg}, path: ${path} })`);
538
- lines.push(` }`);
539
- return lines;
540
- }
541
- if (hasEnum(propSchema)) {
542
- const label = propSchema.enum.map((v) => JSON.stringify(v)).join(", ");
543
- lines.push(` if (${presence}!${enumMembershipExpr(propSchema.enum, raw)}) {`);
544
- lines.push(` errors.push({ message: ${JSON.stringify(`must be one of: ${label}`)}, path: ${path} })`);
545
- lines.push(` }`);
546
- return lines;
547
- }
548
- if (hasType(propSchema)) {
549
- const t = propSchema.type;
550
- const wrongType = wrongTypeCondition(raw, t);
551
- const typLabel = typeofString(t);
552
- if (wrongType) {
553
- lines.push(` if (${presence}(${wrongType})) {`);
554
- lines.push(` errors.push({ message: 'must be ${typLabel}', path: ${path} })`);
555
- lines.push(` }`);
556
- }
557
- }
558
- const typeArray = getTypeArray(propSchema);
559
- if (typeArray) {
560
- const allWrong = typeArray.map((t) => wrongTypeCondition(raw, t)).filter((c) => c !== "").map((c) => `(${c})`).join(" && ");
561
- if (allWrong) {
562
- const label = typeArray.map((t) => typeofString(t)).join(" or ");
563
- lines.push(` if (${presence}(${allWrong})) {`);
564
- lines.push(` errors.push({ message: ${JSON.stringify(`must be ${label}`)}, path: ${path} })`);
565
- lines.push(` }`);
566
- }
567
- }
568
460
  const valueCtx = {
569
461
  objVar: ctx.objVar,
570
462
  pathPrefix: path.slice(1, -1),
@@ -572,10 +464,10 @@ const generateValueCheckLines = (_key, raw, path, propSchema, suffix, ctx, requi
572
464
  hoisted: ctx.hoisted,
573
465
  rootSchema: ctx.rootSchema
574
466
  };
575
- lines.push(...generateConstraintChecks("", raw, path, propSchema, suffix, valueCtx));
576
- lines.push(...generateCombinatorChecks("", raw, path, propSchema, suffix, valueCtx));
467
+ lines.push(...generateKeywordChecks("", raw, path, propSchema, suffix, valueCtx, presence));
577
468
  return lines;
578
469
  };
470
+ const NARROWABLE_REFERENCE = /^[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*|\[[A-Za-z_$][\w$]*\]|\[\d+\])*$/;
579
471
  const generateMatchesExpr = (raw, sub, suffix, ctx) => {
580
472
  if (sub === true)
581
473
  return "true";
@@ -583,12 +475,15 @@ const generateMatchesExpr = (raw, sub, suffix, ctx) => {
583
475
  return "false";
584
476
  if (!isSchemaObject(sub))
585
477
  return "true";
586
- const checks = generateValueChecks("", raw, "`${_path}`", sub, suffix, ctx);
478
+ const value = NARROWABLE_REFERENCE.test(raw) ? raw : `_mv${ctx.depth}`;
479
+ const checks = generateValueChecks("", value, "`${_path}`", sub, suffix, ctx);
587
480
  if (checks.length === 0)
588
481
  return "true";
589
482
  const body = checks.join("\n").replaceAll("errors.push(", "_m.push(");
483
+ const binding = value === raw ? "" : `const ${value}: unknown = ${raw}
484
+ `;
590
485
  return `((): boolean => { const _m: ValidationError[] = []
591
- ${body}
486
+ ${binding}${body}
592
487
  return _m.length === 0 })()`;
593
488
  };
594
489
  const unevaluatedMatcher = (suffix, ctx) => (accessor, schema, depth) => generateMatchesExpr(accessor, schema, suffix, { ...ctx, depth: ctx.depth + depth + 1 });
@@ -636,9 +531,11 @@ const generateCombinatorChecks = (key, raw, path, schema, suffix, ctx) => {
636
531
  }
637
532
  if (hasAnyOf(schema) && schema.anyOf.length > 0) {
638
533
  const conds = schema.anyOf.map((b) => generateMatchesExpr(raw, b, suffix, ctx));
639
- lines.push(` if (!(${conds.join(" || ")})) {`);
640
- lines.push(` errors.push({ message: 'must match a schema in anyOf', path: ${path} })`);
641
- lines.push(` }`);
534
+ if (!conds.includes("true")) {
535
+ lines.push(` if (!(${conds.join(" || ")})) {`);
536
+ lines.push(` errors.push({ message: 'must match a schema in anyOf', path: ${path} })`);
537
+ lines.push(` }`);
538
+ }
642
539
  }
643
540
  if (hasOneOf(schema) && schema.oneOf.length > 0) {
644
541
  const conds = schema.oneOf.map((b) => `(${generateMatchesExpr(raw, b, suffix, ctx)} ? 1 : 0)`);
@@ -649,9 +546,11 @@ const generateCombinatorChecks = (key, raw, path, schema, suffix, ctx) => {
649
546
  const not = schema["not"];
650
547
  if (not !== void 0 && (isSchemaObject(not) || typeof not === "boolean")) {
651
548
  const cond = generateMatchesExpr(raw, not, suffix, ctx);
652
- lines.push(` if (${cond}) {`);
653
- lines.push(` errors.push({ message: 'must NOT match the schema in not', path: ${path} })`);
654
- lines.push(` }`);
549
+ if (cond !== "false") {
550
+ lines.push(` if (${cond}) {`);
551
+ lines.push(` errors.push({ message: 'must NOT match the schema in not', path: ${path} })`);
552
+ lines.push(` }`);
553
+ }
655
554
  }
656
555
  const ifSchema = schema["if"];
657
556
  if (ifSchema !== void 0 && (isSchemaObject(ifSchema) || typeof ifSchema === "boolean")) {
@@ -660,11 +559,18 @@ const generateCombinatorChecks = (key, raw, path, schema, suffix, ctx) => {
660
559
  const thenLines = thenSchema !== void 0 ? generateValueChecks(key, raw, path, thenSchema, suffix, ctx) : [];
661
560
  const elseLines = elseSchema !== void 0 ? generateValueChecks(key, raw, path, elseSchema, suffix, ctx) : [];
662
561
  if (thenLines.length > 0 || elseLines.length > 0) {
663
- lines.push(` if (${generateMatchesExpr(raw, ifSchema, suffix, ctx)}) {`);
664
- lines.push(...thenLines);
665
- lines.push(` } else {`);
666
- lines.push(...elseLines);
667
- lines.push(` }`);
562
+ const cond = generateMatchesExpr(raw, ifSchema, suffix, ctx);
563
+ if (cond === "true") {
564
+ lines.push(...thenLines);
565
+ } else if (cond === "false") {
566
+ lines.push(...elseLines);
567
+ } else {
568
+ lines.push(` if (${cond}) {`);
569
+ lines.push(...thenLines);
570
+ lines.push(` } else {`);
571
+ lines.push(...elseLines);
572
+ lines.push(` }`);
573
+ }
668
574
  }
669
575
  }
670
576
  return lines;
@@ -680,7 +586,7 @@ const generatePatternAndAdditionalChecks = (schema, suffix, ctx) => {
680
586
  for (const [pattern, sub] of patternEntries) {
681
587
  const re = regexLiteral(pattern);
682
588
  const kv = `_pk${d}`;
683
- const valueChecks = generateValueChecks(`\${${kv}}`, `${obj}[${kv}]`, `\`${ctx.pathPrefix}/\${${kv}}\``, sub, suffix, ctx);
589
+ const valueChecks = generateValueChecks(`\${${kv}}`, `${obj}[${kv}]`, `\`${ctx.pathPrefix}/\${escapePointer(${kv})}\``, sub, suffix, ctx);
684
590
  if (valueChecks.length === 0)
685
591
  continue;
686
592
  lines.push(` for (const ${kv} in ${obj}) {`);
@@ -692,7 +598,7 @@ const generatePatternAndAdditionalChecks = (schema, suffix, ctx) => {
692
598
  if (hasAdditionalProperties(schema) && isSchemaObject(schema.additionalProperties)) {
693
599
  const additional = schema.additionalProperties;
694
600
  const kv = `_ak${d}`;
695
- const valueChecks = generateValueChecks(`\${${kv}}`, `${obj}[${kv}]`, `\`${ctx.pathPrefix}/\${${kv}}\``, additional, suffix, ctx);
601
+ const valueChecks = generateValueChecks(`\${${kv}}`, `${obj}[${kv}]`, `\`${ctx.pathPrefix}/\${escapePointer(${kv})}\``, additional, suffix, ctx);
696
602
  if (valueChecks.length > 0) {
697
603
  const known = Object.keys(hasProperties(schema) ? schema.properties : {});
698
604
  lines.push(` for (const ${kv} in ${obj}) {`);
@@ -746,10 +652,10 @@ const generateInlineObjectChecks = (key, propSchema, raw, suffix, ctx) => {
746
652
  ];
747
653
  };
748
654
  const generatePropertyNameChecks = (nameSchema, suffix, ctx) => {
749
- const at = `\`${ctx.pathPrefix}/\${_name}\``;
655
+ const at = `\`${ctx.pathPrefix}/\${escapePointer(_name)}\``;
750
656
  const nameCtx = {
751
657
  objVar: ctx.objVar,
752
- pathPrefix: `${ctx.pathPrefix}/\${_name}`,
658
+ pathPrefix: `${ctx.pathPrefix}/\${escapePointer(_name)}`,
753
659
  depth: ctx.depth + 1,
754
660
  hoisted: ctx.hoisted,
755
661
  rootSchema: ctx.rootSchema
@@ -777,6 +683,10 @@ const generateDependentRequiredChecks = (schema, ctx) => {
777
683
  }
778
684
  return lines;
779
685
  };
686
+ const dependentSelfBinding = (objVar, depth) => {
687
+ const name = `_dep${depth}`;
688
+ return { name, declaration: ` const ${name}: unknown = ${objVar}` };
689
+ };
780
690
  const generateDependentSchemasChecks = (schema, suffix, ctx) => {
781
691
  if (!isSchemaObject(schema))
782
692
  return [];
@@ -799,10 +709,12 @@ const generateDependentSchemasChecks = (schema, suffix, ctx) => {
799
709
  }
800
710
  if (!isSchemaObject(sub))
801
711
  continue;
802
- const checks = generateValueChecks("", obj, objPath, sub, suffix, ctx);
712
+ const self = dependentSelfBinding(obj, ctx.depth);
713
+ const checks = generateValueChecks("", self.name, objPath, sub, suffix, ctx);
803
714
  if (checks.length === 0)
804
715
  continue;
805
716
  lines.push(` if (${hasOwnCheck(obj, trigger)}) {`);
717
+ lines.push(` ${self.declaration}`);
806
718
  lines.push(...checks.map((line) => ` ${line}`));
807
719
  lines.push(` }`);
808
720
  }
@@ -841,10 +753,12 @@ const generateDependenciesChecks = (schema, suffix, ctx) => {
841
753
  }
842
754
  if (!isSchemaObject(value))
843
755
  continue;
844
- const checks = generateValueChecks("", obj, objPath, value, suffix, ctx);
756
+ const self = dependentSelfBinding(obj, ctx.depth);
757
+ const checks = generateValueChecks("", self.name, objPath, value, suffix, ctx);
845
758
  if (checks.length === 0)
846
759
  continue;
847
760
  lines.push(` if (${hasOwnCheck(obj, trigger)}) {`);
761
+ lines.push(` ${self.declaration}`);
848
762
  lines.push(...checks.map((line) => ` ${line}`));
849
763
  lines.push(` }`);
850
764
  }
@@ -993,15 +907,17 @@ const generateObjectValidator = (schema, typeName, suffix, rootSchema) => {
993
907
  if (hasPropertyNames(schema)) {
994
908
  propertyLines.push(...generatePropertyNameChecks(schema.propertyNames, suffix, ctx));
995
909
  }
996
- propertyLines.push(...generateCombinatorChecks("", "obj", "`${_path}`", schema, suffix, ctx));
910
+ const objectCombinators = generateCombinatorChecks("", "_root", "`${_path}`", schema, suffix, ctx);
911
+ if (objectCombinators.length > 0) {
912
+ propertyLines.push(` const _root: unknown = input`);
913
+ propertyLines.push(...objectCombinators);
914
+ }
997
915
  const body = (propertyLines.length > 0 ? "\n" + propertyLines.join("\n") + "\n" : "").replaceAll("errors.push(", "(errors ??= []).push(");
998
- const hoistedBlock = ctx.hoisted.length > 0 ? `${ctx.hoisted.join("\n")}
999
-
1000
- ` : "";
1001
916
  const guard = guardObjectConditions(schema, "input", "obj");
917
+ const objBinding = readsObjBinding(body) ? [` const obj = input as Record<string, unknown>`] : [];
1002
918
  const collectBody = (name, exported) => [
1003
919
  `${exported ? "export " : ""}const ${name} = (input: unknown, _path = ''): ValidationResult => {`,
1004
- ` const obj = input as Record<string, unknown>`,
920
+ ...objBinding,
1005
921
  ` if (typeof input !== 'object' || input === null || Array.isArray(input)) {`,
1006
922
  ` return { valid: false, errors: [{ message: 'must be object', path: _path }] }`,
1007
923
  ` }`,
@@ -1012,14 +928,15 @@ const generateObjectValidator = (schema, typeName, suffix, rootSchema) => {
1012
928
  `}`
1013
929
  ].join("\n");
1014
930
  if (!guard) {
1015
- return `${hoistedBlock}${collectBody(vName, true)}`;
931
+ return withHoisted(ctx.hoisted, collectBody(vName, true));
1016
932
  }
933
+ const guardText = guard.join("\n");
1017
934
  const collectName = `${vName}Errors`;
1018
- return [
1019
- `${hoistedBlock}${collectBody(collectName, false)}`,
935
+ return withHoisted(ctx.hoisted, [
936
+ collectBody(collectName, false),
1020
937
  ``,
1021
938
  `export const ${vName} = (input: unknown, _path = ''): ValidationResult => {`,
1022
- ` const obj = input as Record<string, unknown>`,
939
+ ...readsObjBinding(guardText) ? [` const obj = input as Record<string, unknown>`] : [],
1023
940
  ` if (`,
1024
941
  guard.map((condition) => ` ${condition}`).join(" &&\n"),
1025
942
  ` ) {`,
@@ -1027,21 +944,49 @@ const generateObjectValidator = (schema, typeName, suffix, rootSchema) => {
1027
944
  ` }`,
1028
945
  ` return ${collectName}(input, _path)`,
1029
946
  `}`
1030
- ].join("\n");
947
+ ].join("\n"));
1031
948
  };
1032
949
  const guardName = (typeName) => `is${typeName}`;
1033
- const booleanLeafExpr = (schema, acc) => {
950
+ const valueCanHaveType = (value, type) => {
951
+ switch (type) {
952
+ case "string":
953
+ return typeof value === "string";
954
+ case "number":
955
+ return typeof value === "number";
956
+ case "integer":
957
+ return typeof value === "number" && Number.isInteger(value);
958
+ case "boolean":
959
+ return typeof value === "boolean";
960
+ case "null":
961
+ return value === null;
962
+ case "array":
963
+ return Array.isArray(value);
964
+ case "object":
965
+ return typeof value === "object" && value !== null && !Array.isArray(value);
966
+ default:
967
+ return true;
968
+ }
969
+ };
970
+ const booleanLeafExpr = (schema, acc, narrowable = true) => {
1034
971
  if (!isSchemaObject(schema))
1035
972
  return null;
1036
- 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 || carriesUnevaluated(schema) || getMjstInstanceOf(schema) !== void 0 || getMjstPrimitive(schema) !== void 0) {
973
+ 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
975
+ // tail in `additionalItems`. Neither is expressible flat, and reading past
976
+ // 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) {
1037
978
  return null;
1038
979
  }
1039
- if (hasEnum(schema)) {
980
+ if (!hasType(schema)) {
981
+ if (!hasEnum(schema))
982
+ return null;
983
+ if (declaresKeywordOutside(schema, ["enum"]))
984
+ return null;
1040
985
  return enumMembershipExpr(schema.enum, acc);
1041
986
  }
1042
- if (!hasType(schema))
1043
- return null;
1044
987
  const t = schema.type;
988
+ const membership = hasEnum(schema) ? enumMembershipExpr(schema.enum.filter((member) => valueCanHaveType(member, t)), acc) : null;
989
+ const withMembership = (expr) => expr === null || membership === null ? expr : `${expr} && ${membership}`;
1045
990
  switch (t) {
1046
991
  // Each constraint is the exact negation of the validator's error condition,
1047
992
  // so edge values — `NaN` above all, which compares `false` against every
@@ -1050,56 +995,61 @@ const booleanLeafExpr = (schema, acc) => {
1050
995
  // condition is its negation; the length checks come from the same
1051
996
  // `string-length-check` emitter the validator uses, for the same reason.
1052
997
  case "string": {
998
+ const str = typed("string");
1053
999
  const parts = [`typeof ${acc} === 'string'`];
1054
1000
  if (hasPattern(schema))
1055
- parts.push(`${regexLiteral(schema.pattern)}.test(${acc})`);
1001
+ parts.push(`${regexLiteral(schema.pattern)}.test(${str})`);
1056
1002
  if (hasMinLength(schema))
1057
- parts.push(minLengthPassExpr(acc, schema.minLength));
1003
+ parts.push(minLengthPassExpr(str, schema.minLength));
1058
1004
  if (hasMaxLength(schema))
1059
- parts.push(maxLengthPassExpr(acc, schema.maxLength));
1060
- return parts.join(" && ");
1005
+ parts.push(maxLengthPassExpr(str, schema.maxLength));
1006
+ return withMembership(parts.join(" && "));
1061
1007
  }
1062
1008
  case "number":
1063
1009
  case "integer": {
1010
+ const num = typed("number");
1064
1011
  const parts = [`typeof ${acc} === 'number'`];
1065
1012
  if (t === "integer")
1066
1013
  parts.push(`Number.isInteger(${acc})`);
1067
1014
  if (hasMinimum(schema))
1068
- parts.push(`${acc} ${hasStrictExclusiveMinimum(schema) ? ">" : ">="} ${schema.minimum}`);
1015
+ parts.push(`${num} ${hasStrictExclusiveMinimum(schema) ? ">" : ">="} ${schema.minimum}`);
1069
1016
  if (hasMaximum(schema))
1070
- parts.push(`${acc} ${hasStrictExclusiveMaximum(schema) ? "<" : "<="} ${schema.maximum}`);
1017
+ parts.push(`${num} ${hasStrictExclusiveMaximum(schema) ? "<" : "<="} ${schema.maximum}`);
1071
1018
  if (hasExclusiveMinimum(schema))
1072
- parts.push(`${acc} > ${schema.exclusiveMinimum}`);
1019
+ parts.push(`${num} > ${schema.exclusiveMinimum}`);
1073
1020
  if (hasExclusiveMaximum(schema))
1074
- parts.push(`${acc} < ${schema.exclusiveMaximum}`);
1021
+ parts.push(`${num} < ${schema.exclusiveMaximum}`);
1075
1022
  if (hasMultipleOf(schema))
1076
- parts.push(multipleOfPassExpr(acc, schema.multipleOf));
1077
- return parts.join(" && ");
1023
+ parts.push(multipleOfPassExpr(num, schema.multipleOf));
1024
+ return withMembership(parts.join(" && "));
1078
1025
  }
1079
1026
  case "boolean":
1080
- return `typeof ${acc} === 'boolean'`;
1027
+ return withMembership(`typeof ${acc} === 'boolean'`);
1081
1028
  case "null":
1082
- return `${acc} === null`;
1029
+ return withMembership(`${acc} === null`);
1083
1030
  case "object": {
1084
- const parts = booleanObjectParts(schema, acc, `(${acc} as Record<string, unknown>)`);
1085
- return parts === null ? null : parts.join(" && ");
1031
+ const parts = booleanObjectParts(schema, acc, `(${acc} as Record<string, unknown>)`, false);
1032
+ return parts === null ? null : withMembership(parts.join(" && "));
1086
1033
  }
1087
1034
  case "array":
1088
- return booleanArrayExpr(schema, acc);
1035
+ return withMembership(booleanArrayExpr(schema, acc, narrowable));
1089
1036
  default:
1090
1037
  return null;
1091
1038
  }
1092
1039
  };
1093
- const booleanArrayExpr = (schema, acc) => {
1040
+ const booleanArrayExpr = (schema, acc, narrowable = true) => {
1041
+ const arr = narrowable ? acc : `(${acc} as unknown[])`;
1094
1042
  const parts = [`Array.isArray(${acc})`];
1095
1043
  if (hasMinItems(schema))
1096
- parts.push(`${acc}.length >= ${schema.minItems}`);
1044
+ parts.push(`${arr}.length >= ${schema.minItems}`);
1097
1045
  if (hasMaxItems(schema))
1098
- parts.push(`${acc}.length <= ${schema.maxItems}`);
1046
+ parts.push(`${arr}.length <= ${schema.maxItems}`);
1099
1047
  if (hasUniqueItems(schema) && schema.uniqueItems === true) {
1100
- parts.push(arrayItemsAreScalarOnly(schema) ? `new Set((${acc} as unknown[]).map((_u) => JSON.stringify(_u))).size === ${acc}.length` : `allUnique(${acc} as unknown[])`);
1048
+ parts.push(arrayItemsAreScalarOnly(schema) ? `new Set((${acc} as unknown[]).map((_u) => JSON.stringify(_u))).size === ${arr}.length` : `allUnique(${acc} as unknown[])`);
1101
1049
  }
1102
1050
  const base = parts.join(" && ");
1051
+ if (schema["items"] === false)
1052
+ return `${base} && ${arr}.length === 0`;
1103
1053
  if (!hasItems(schema))
1104
1054
  return base;
1105
1055
  const items = schema.items;
@@ -1112,9 +1062,13 @@ const booleanArrayExpr = (schema, acc) => {
1112
1062
  return null;
1113
1063
  return `${base} && Array.from(${acc} as unknown[]).every((_it) => (${itemExpr}))`;
1114
1064
  };
1115
- const booleanObjectParts = (schema, raw, objAcc) => {
1065
+ const booleanObjectParts = (schema, raw, objAcc, narrowable = true) => {
1116
1066
  if (!isObjectSchema(schema))
1117
1067
  return null;
1068
+ if (hasRef(schema) || hasConst(schema) || hasEnum(schema))
1069
+ return null;
1070
+ if (getMjstInstanceOf(schema) !== void 0 || getMjstPrimitive(schema) !== void 0)
1071
+ return null;
1118
1072
  if (hasDependentRequired(schema) || hasPropertyNames(schema) || "dependentSchemas" in schema)
1119
1073
  return null;
1120
1074
  if (hasMinProperties(schema) || hasMaxProperties(schema) || "dependencies" in schema)
@@ -1144,11 +1098,16 @@ const booleanObjectParts = (schema, raw, objAcc) => {
1144
1098
  if (PROTOTYPE_MEMBERS.has(key))
1145
1099
  return null;
1146
1100
  const member = safeAccessor(objAcc, key);
1147
- const expr = booleanLeafExpr(propSchema, member);
1101
+ const expr = booleanLeafExpr(propSchema, member, narrowable);
1148
1102
  if (expr === null)
1149
1103
  return null;
1150
1104
  parts.push(required.has(key) ? expr : `(${member} === undefined || (${expr}))`);
1151
1105
  }
1106
+ for (const key of required) {
1107
+ if (Object.hasOwn(properties, key))
1108
+ continue;
1109
+ parts.push(hasOwnCheck(objAcc, key));
1110
+ }
1152
1111
  if (strict) {
1153
1112
  if (keys.length === 0) {
1154
1113
  parts.push(`Object.keys(${objAcc}).length === 0`);
@@ -1182,13 +1141,15 @@ const generateBooleanGuard = (schema, typeName, _suffix = "") => {
1182
1141
  const returns = typeDescribesEveryAcceptedValue(rewriteNullable(schema)) ? `input is ${typeName}` : "boolean";
1183
1142
  const fallback = `export const ${name} = (input: unknown): ${returns} => ${validatorName(typeName)}(input) === true`;
1184
1143
  const rewritten = rewriteNullable(schema);
1185
- if (declaresObjectType(rewritten)) {
1144
+ if (declaresObjectType(rewritten) && objectRootIsSelfContained(rewritten)) {
1186
1145
  const parts = booleanObjectParts(rewritten, "input", "obj");
1187
1146
  if (parts === null)
1188
1147
  return fallback;
1189
1148
  return [
1190
1149
  `export const ${name} = (input: unknown): ${returns} => {`,
1191
- ` const obj = input as Record<string, unknown>`,
1150
+ // Same unused-local as the validator's hot guard: a node with no property
1151
+ // to read guards on the shape alone and never touches the narrowing.
1152
+ ...readsObjBinding(parts.join("\n")) ? [` const obj = input as Record<string, unknown>`] : [],
1192
1153
  ` return (`,
1193
1154
  parts.map((part) => ` ${part}`).join(" &&\n"),
1194
1155
  ` )`,
@@ -1212,7 +1173,10 @@ const generateScalarValidator = (schema, typeName, suffix, rootSchema) => {
1212
1173
  }
1213
1174
  return [`export const ${vName} = (_input: unknown, _path = ''): ValidationResult => {`, ` return true`, `}`].join("\n");
1214
1175
  }
1176
+ const generalRoot = () => generateGeneralRootValidator(schema, typeName, suffix, rootSchema);
1215
1177
  if (hasRef(schema)) {
1178
+ if (declaresKeywordOutside(schema, ["$ref"]))
1179
+ return generalRoot();
1216
1180
  const delegateName = validatorName(refToName(schema.$ref, suffix));
1217
1181
  return [
1218
1182
  `export const ${vName} = (input: unknown, _path = ''): ValidationResult => {`,
@@ -1222,6 +1186,8 @@ const generateScalarValidator = (schema, typeName, suffix, rootSchema) => {
1222
1186
  }
1223
1187
  const instanceOf = getMjstInstanceOf(schema);
1224
1188
  if (instanceOf) {
1189
+ if (declaresKeywordOutside(schema, [MJST_EXTENSION_KEY, "type"]))
1190
+ return generalRoot();
1225
1191
  return [
1226
1192
  `export const ${vName} = (input: unknown, _path = ''): ValidationResult => {`,
1227
1193
  ` if (!(input instanceof ${instanceOf})) {`,
@@ -1233,6 +1199,8 @@ const generateScalarValidator = (schema, typeName, suffix, rootSchema) => {
1233
1199
  }
1234
1200
  const primitive = getMjstPrimitive(schema);
1235
1201
  if (primitive) {
1202
+ if (declaresKeywordOutside(schema, [MJST_EXTENSION_KEY, "type"]))
1203
+ return generalRoot();
1236
1204
  return [
1237
1205
  `export const ${vName} = (input: unknown, _path = ''): ValidationResult => {`,
1238
1206
  ` if (typeof input !== "${primitive}") {`,
@@ -1243,6 +1211,8 @@ const generateScalarValidator = (schema, typeName, suffix, rootSchema) => {
1243
1211
  ].join("\n");
1244
1212
  }
1245
1213
  if (hasConst(schema)) {
1214
+ if (declaresKeywordOutside(schema, ["const"]))
1215
+ return generalRoot();
1246
1216
  const mismatch = constMismatchCondition("input", schema.const);
1247
1217
  const msg = JSON.stringify(`must be ${JSON.stringify(schema.const)}`);
1248
1218
  return [
@@ -1255,6 +1225,8 @@ const generateScalarValidator = (schema, typeName, suffix, rootSchema) => {
1255
1225
  ].join("\n");
1256
1226
  }
1257
1227
  if (hasEnum(schema)) {
1228
+ if (declaresKeywordOutside(schema, ["enum"]))
1229
+ return generalRoot();
1258
1230
  const label = schema.enum.map((v) => JSON.stringify(v)).join(", ");
1259
1231
  return [
1260
1232
  `export const ${vName} = (input: unknown, _path = ''): ValidationResult => {`,
@@ -1290,16 +1262,13 @@ const generateScalarValidator = (schema, typeName, suffix, rootSchema) => {
1290
1262
  checks.push(...generateConstraintChecks("", "input", rootPath, schema, suffix, ctx));
1291
1263
  checks.push(...generateCombinatorChecks("", "input", rootPath, schema, suffix, ctx));
1292
1264
  const body = checks.join("\n").replaceAll("errors.push(", "(errors ??= []).push(");
1293
- const hoistedBlock = ctx.hoisted.length > 0 ? `${ctx.hoisted.join("\n")}
1294
-
1295
- ` : "";
1296
- return [
1297
- `${hoistedBlock}export const ${vName} = (input: unknown, _path = ''): ValidationResult => {`,
1265
+ return withHoisted(ctx.hoisted, [
1266
+ `export const ${vName} = (input: unknown, _path = ''): ValidationResult => {`,
1298
1267
  ` let errors: ValidationError[] | undefined`,
1299
1268
  body,
1300
1269
  ` return errors !== undefined ? { valid: false, errors } : true`,
1301
1270
  `}`
1302
- ].join("\n");
1271
+ ].join("\n"));
1303
1272
  }
1304
1273
  const rootTypeArray = getTypeArray(schema);
1305
1274
  if (rootTypeArray) {
@@ -1322,23 +1291,20 @@ const generateScalarValidator = (schema, typeName, suffix, rootSchema) => {
1322
1291
  ].join("\n");
1323
1292
  }
1324
1293
  const body = checks.join("\n").replaceAll("errors.push(", "(errors ??= []).push(");
1325
- const hoistedBlock = ctx.hoisted.length > 0 ? `${ctx.hoisted.join("\n")}
1326
-
1327
- ` : "";
1328
- return [
1329
- `${hoistedBlock}export const ${vName} = (input: unknown, _path = ''): ValidationResult => {`,
1294
+ return withHoisted(ctx.hoisted, [
1295
+ `export const ${vName} = (input: unknown, _path = ''): ValidationResult => {`,
1330
1296
  ` let errors: ValidationError[] | undefined`,
1331
1297
  body,
1332
1298
  ` return errors !== undefined ? { valid: false, errors } : true`,
1333
1299
  `}`
1334
- ].join("\n");
1300
+ ].join("\n"));
1335
1301
  }
1336
1302
  if (hasType(schema)) {
1337
1303
  const t = schema.type;
1338
1304
  const wrongType = wrongTypeCondition("input", t);
1339
1305
  const typLabel = typeofString(t);
1340
1306
  const rootCtx = createRootContext(rootSchema);
1341
- const constraintLines = generateConstraintChecks("", "input", "`${_path}`", schema, suffix, rootCtx);
1307
+ const constraintLines = generateConstraintChecks("", "_root", "`${_path}`", schema, suffix, rootCtx);
1342
1308
  if (!wrongType) {
1343
1309
  return [
1344
1310
  `export const ${vName} = (_input: unknown, _path = ''): ValidationResult => {`,
@@ -1356,50 +1322,42 @@ const generateScalarValidator = (schema, typeName, suffix, rootSchema) => {
1356
1322
  `}`
1357
1323
  ].join("\n");
1358
1324
  }
1359
- const hoistedBlock = rootCtx.hoisted.length > 0 ? `${rootCtx.hoisted.join("\n")}
1360
-
1361
- ` : "";
1362
- return [
1363
- `${hoistedBlock}export const ${vName} = (input: unknown, _path = ''): ValidationResult => {`,
1325
+ return withHoisted(rootCtx.hoisted, [
1326
+ `export const ${vName} = (input: unknown, _path = ''): ValidationResult => {`,
1364
1327
  ` if (${wrongType}) {`,
1365
1328
  ` return { valid: false, errors: [{ message: 'must be ${typLabel}', path: _path }] }`,
1366
1329
  ` }`,
1330
+ ` const _root: unknown = input`,
1367
1331
  ` let errors: ValidationError[] | undefined`,
1368
1332
  constraintLines.join("\n").replaceAll("errors.push(", "(errors ??= []).push("),
1369
1333
  ` return errors !== undefined ? { valid: false, errors } : true`,
1370
1334
  `}`
1371
- ].join("\n");
1335
+ ].join("\n"));
1372
1336
  }
1373
1337
  const typelessCtx = createRootContext(rootSchema);
1374
1338
  const typelessChecks = generateConstraintChecks("", "input", "`${_path}`", schema, suffix, typelessCtx);
1375
1339
  if (typelessChecks.length === 0) {
1376
1340
  return [`export const ${vName} = (_input: unknown, _path = ''): ValidationResult => {`, ` return true`, `}`].join("\n");
1377
1341
  }
1378
- const typelessHoisted = typelessCtx.hoisted.length > 0 ? `${typelessCtx.hoisted.join("\n")}
1379
-
1380
- ` : "";
1381
- return [
1382
- `${typelessHoisted}export const ${vName} = (input: unknown, _path = ''): ValidationResult => {`,
1342
+ return withHoisted(typelessCtx.hoisted, [
1343
+ `export const ${vName} = (input: unknown, _path = ''): ValidationResult => {`,
1383
1344
  ` let errors: ValidationError[] | undefined`,
1384
1345
  typelessChecks.join("\n").replaceAll("errors.push(", "(errors ??= []).push("),
1385
1346
  ` return errors !== undefined ? { valid: false, errors } : true`,
1386
1347
  `}`
1387
- ].join("\n");
1348
+ ].join("\n"));
1388
1349
  };
1389
- const generateUnevaluatedRootValidator = (schema, typeName, suffix, rootSchema) => {
1350
+ const generateGeneralRootValidator = (schema, typeName, suffix, rootSchema) => {
1390
1351
  const ctx = createRootContext(rootSchema);
1391
1352
  const checks = generateValueChecks("", "input", "`${_path}`", schema, suffix, ctx, true);
1392
1353
  const body = checks.join("\n").replaceAll("errors.push(", "(errors ??= []).push(");
1393
- const hoistedBlock = ctx.hoisted.length > 0 ? `${ctx.hoisted.join("\n")}
1394
-
1395
- ` : "";
1396
- return [
1397
- `${hoistedBlock}export const ${validatorName(typeName)} = (input: unknown, _path = ''): ValidationResult => {`,
1354
+ return withHoisted(ctx.hoisted, [
1355
+ `export const ${validatorName(typeName)} = (input: unknown, _path = ''): ValidationResult => {`,
1398
1356
  ` let errors: ValidationError[] | undefined`,
1399
1357
  body,
1400
1358
  ` return errors !== undefined ? { valid: false, errors } : true`,
1401
1359
  `}`
1402
- ].join("\n");
1360
+ ].join("\n"));
1403
1361
  };
1404
1362
  const SINGLE_SUBSCHEMA_KEYS = /* @__PURE__ */ new Set([
1405
1363
  "additionalProperties",
@@ -1462,9 +1420,9 @@ const generateValidatorFunction = (schema, typeName, suffix = "", rootSchema) =>
1462
1420
  const document = rootSchema ?? schema;
1463
1421
  assertUnevaluatedGeneratable(rewritten, typeName, document, unevaluatedMatcher(suffix, createRootContext(document)));
1464
1422
  if (carriesUnevaluated(rewritten)) {
1465
- return generateUnevaluatedRootValidator(rewritten, typeName, suffix, document);
1423
+ return generateGeneralRootValidator(rewritten, typeName, suffix, document);
1466
1424
  }
1467
- if (declaresObjectType(rewritten)) {
1425
+ if (declaresObjectType(rewritten) && objectRootIsSelfContained(rewritten)) {
1468
1426
  return generateObjectValidator(rewritten, typeName, suffix, document);
1469
1427
  }
1470
1428
  return generateScalarValidator(rewritten, typeName, suffix, document);