@amritk/validation 0.4.1 → 0.6.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.
@@ -0,0 +1,335 @@
1
+ import { regexFlagsFor } from "@amritk/helpers/escape-regex-pattern";
2
+ import { readKey } from "@amritk/helpers/read-key";
3
+ import { refToName } from "@amritk/helpers/ref-to-name";
4
+ import { isSchemaObject } from "@amritk/helpers/schema-guards";
5
+ import { namedTest, schemaMap, subschema } from "./repair-context.js";
6
+ import { isReferenceSchema, leafKeywords } from "./repair-walkable.js";
7
+ const repairWalkNameFor = (typeName) => `walk${typeName}Repair`;
8
+ const SCALAR_TYPES = /* @__PURE__ */ new Set(["string", "number", "integer", "boolean", "null"]);
9
+ const typeCondition = (type) => {
10
+ switch (type) {
11
+ case "string":
12
+ return `typeof v === 'string'`;
13
+ case "number":
14
+ return `(typeof v === 'number' && (!run.finite || Number.isFinite(v)))`;
15
+ case "integer":
16
+ return `Number.isInteger(v)`;
17
+ case "boolean":
18
+ return `typeof v === 'boolean'`;
19
+ case "null":
20
+ return `v === null`;
21
+ case "array":
22
+ return `Array.isArray(v)`;
23
+ default:
24
+ return `(typeof v === 'object' && v !== null && !Array.isArray(v) && (!run.plain || isPlainObject(v)))`;
25
+ }
26
+ };
27
+ const equalsCondition = (value) => {
28
+ if (typeof value === "number" && Number.isNaN(value))
29
+ return `Number.isNaN(v)`;
30
+ if (value !== null && typeof value === "object")
31
+ return `valuesEqual(v, ${JSON.stringify(value)})`;
32
+ return `(v as unknown) === ${JSON.stringify(value)}`;
33
+ };
34
+ const ownChecks = (node, ctx) => {
35
+ const checks = [];
36
+ const type = readKey(node, "type");
37
+ if (typeof type === "string") {
38
+ checks.push({
39
+ condition: typeCondition(type),
40
+ report: `found.push(walkError(run, 'type', ${JSON.stringify(`must be ${type}`)}, ${JSON.stringify({ type })}))`
41
+ });
42
+ } else if (Array.isArray(type)) {
43
+ const types = type;
44
+ checks.push({
45
+ condition: `(${types.map(typeCondition).join(" || ")})`,
46
+ report: `found.push(walkError(run, 'type', ${JSON.stringify(`must be ${types.join(" or ")}`)}, ${JSON.stringify({ type })}))`
47
+ });
48
+ }
49
+ const values = readKey(node, "enum");
50
+ if (Array.isArray(values)) {
51
+ const label = values.map((value) => JSON.stringify(value)).join(", ");
52
+ checks.push({
53
+ condition: values.length === 0 ? "false" : `(${values.map(equalsCondition).join(" || ")})`,
54
+ report: `found.push(walkError(run, 'enum', ${JSON.stringify(`must be one of: ${label}`)}, ${JSON.stringify({ allowedValues: values })}))`
55
+ });
56
+ }
57
+ if (Object.hasOwn(node, "const")) {
58
+ const value = node["const"];
59
+ checks.push({
60
+ condition: equalsCondition(value),
61
+ report: `found.push(walkError(run, 'const', ${JSON.stringify(`must be ${JSON.stringify(value)}`)}, ${JSON.stringify({ allowedValue: value })}))`
62
+ });
63
+ }
64
+ const leaf = leafKeywords(node);
65
+ if (leaf !== null) {
66
+ const test = namedTest(leaf, ctx.repair);
67
+ if (test !== true) {
68
+ const explain = ctx.repair.matcher.explain(leaf);
69
+ checks.push({
70
+ condition: test === false ? "false" : `${test}(v)`,
71
+ report: explain === null ? `found.push(walkError(run, 'false schema', 'boolean schema is false', {}))` : `for (const error of ${explain}(v, walkPath(run))) found.push(error)`
72
+ });
73
+ }
74
+ }
75
+ return checks;
76
+ };
77
+ const coercibleTypes = (node) => {
78
+ const type = readKey(node, "type");
79
+ const types = typeof type === "string" ? [type] : Array.isArray(type) ? type : [];
80
+ return types.length > 0 && types.every((entry) => SCALAR_TYPES.has(entry)) ? JSON.stringify(types) : null;
81
+ };
82
+ const walkChild = (walker, key, write) => [
83
+ `run.path.push(${key})`,
84
+ `const n = ${walker}(c, run)`,
85
+ `run.path.pop()`,
86
+ `if (n !== c) ${write}`
87
+ ];
88
+ const inlineTypeId = (node, typeSuffix) => {
89
+ const branchName = (key) => {
90
+ const branch = readKey(node, key);
91
+ if (typeof branch !== "object" || branch === null)
92
+ return null;
93
+ const ref = readKey(branch, "$ref");
94
+ return typeof ref === "string" && Object.keys(branch).length === 1 ? refToName(ref, typeSuffix) : null;
95
+ };
96
+ const thenName = branchName("then");
97
+ const elseName = branchName("else");
98
+ const text = JSON.stringify(node);
99
+ let hash = 2166136261;
100
+ for (let i = 0; i < text.length; i++)
101
+ hash = Math.imul(hash ^ text.charCodeAt(i), 16777619);
102
+ const digest = (hash >>> 0).toString(16).padStart(8, "0");
103
+ return thenName !== null && elseName !== null ? `${thenName}|${elseName}#${digest}` : `inline#${digest}`;
104
+ };
105
+ const falseWalker = (ctx, walkers) => {
106
+ const existing = walkers.get(false);
107
+ if (existing)
108
+ return existing;
109
+ const drop = ctx.lookupFor(false);
110
+ const name = "_walkFalse";
111
+ walkers.set(false, name);
112
+ ctx.repair.helpers.push([
113
+ `const ${name}: RepairWalk = (v, run) =>`,
114
+ ` walkRepair(run, v, [walkError(run, 'false schema', 'boolean schema is false', {})], ${drop}, () => false, null)`
115
+ ].join("\n"));
116
+ return name;
117
+ };
118
+ const emitWalk = (schema, ctx, walkers, isRoot) => {
119
+ if (schema === false)
120
+ return falseWalker(ctx, walkers);
121
+ if (!isSchemaObject(schema))
122
+ return null;
123
+ if (walkers.has(schema))
124
+ return walkers.get(schema) ?? null;
125
+ const node = schema;
126
+ const helpers = ctx.repair.helpers;
127
+ const ref = readKey(node, "$ref");
128
+ const refWalker = typeof ref === "string" ? repairWalkNameFor(refToName(ref, ctx.repair.typeSuffix)) : null;
129
+ const checks = ownChecks(node, ctx);
130
+ const properties = schemaMap(node, "properties") ?? {};
131
+ const required = readKey(node, "required")?.filter((key) => typeof key === "string");
132
+ const hasObjectPart = Object.keys(properties).length > 0 || required !== void 0 && required.length > 0 || readKey(node, "additionalProperties") !== void 0 || readKey(node, "patternProperties") !== void 0;
133
+ const items = subschema(node, "items");
134
+ const hasConditional = readKey(node, "if") !== void 0 || Array.isArray(readKey(node, "allOf"));
135
+ const ownsObjectType = readKey(node, "type") === "object";
136
+ if (refWalker !== null && checks.length === 0 && !hasObjectPart && items === void 0 && !hasConditional && !isRoot) {
137
+ walkers.set(schema, refWalker);
138
+ return refWalker;
139
+ }
140
+ const id = ctx.repair.next();
141
+ const name = `_walk${id}`;
142
+ walkers.set(schema, name);
143
+ const typeId = JSON.stringify(isRoot ? ctx.typeName : inlineTypeId(node, ctx.repair.typeSuffix));
144
+ const lookup = ctx.lookupFor(schema);
145
+ const body = [];
146
+ if (refWalker !== null) {
147
+ body.push(`v = ${refWalker}(v, run)`, `if (v === REPAIR_DROP) return v`);
148
+ }
149
+ if (checks.length > 0) {
150
+ const ok = checks.map((check) => check.condition).join(" && ");
151
+ const okName = `_walkOk${id}`;
152
+ const fixName = `_walkFix${id}`;
153
+ helpers.push(`const ${okName} = (v: unknown, ${/\brun\b/.test(ok) ? "run" : "_run"}: RepairRun): boolean => ${ok}`);
154
+ helpers.push([
155
+ `const ${fixName} = (v: unknown, run: RepairRun): unknown => {`,
156
+ ` const found: ValidationError[] = []`,
157
+ ...checks.map((check) => ` if (!(${check.condition})) ${check.report}`),
158
+ ` return walkRepair(run, v, found, ${lookup ?? "null"}, ${okName}, ${coercibleTypes(node) ?? "null"})`,
159
+ `}`
160
+ ].join("\n"));
161
+ body.push(`if (!(${ok})) {`, ` const fixed = ${fixName}(v, run)`, ` if (fixed !== v) return fixed === REPAIR_DROP ? fixed : walkVerified(run, fixed, ${name})`, `}`);
162
+ }
163
+ const container = [];
164
+ if (hasObjectPart || ownsObjectType)
165
+ container.push(...objectPart(node, properties, required ?? [], lookup, id, ctx, walkers));
166
+ if (items !== void 0)
167
+ container.push(...arrayPart(items, ctx, walkers));
168
+ const conditional = conditionalPart(node, name, typeId, lookup, ctx, walkers);
169
+ if (container.length > 0) {
170
+ body.push(`if (++run.work > run.maxWork || run.path.length > run.maxDepth) return walkHalt(run, v)`);
171
+ body.push(...container);
172
+ }
173
+ body.push(...conditional);
174
+ if (body.length === 0) {
175
+ walkers.set(schema, null);
176
+ return null;
177
+ }
178
+ const bodyName = container.length > 0 ? `_walkBody${id}` : name;
179
+ helpers.push([`const ${bodyName}: RepairWalk = (v, run) => {`, ...body.map((line) => ` ${line}`), ` return v`, `}`].join("\n"));
180
+ if (container.length > 0) {
181
+ helpers.push(`const ${name}: RepairWalk = (v, run) => (run.memo === null ? ${bodyName}(v, run) : walkMemo(run, v, ${bodyName}))`);
182
+ }
183
+ return name;
184
+ };
185
+ const objectPart = (node, properties, required, lookup, id, ctx, walkers) => {
186
+ const inner = [];
187
+ const write = (key) => `o = walkSet(run, o, o !== src, ${key}, n)`;
188
+ const requiredSet = new Set(required);
189
+ for (const [key, child] of Object.entries(properties)) {
190
+ const walker = emitWalk(child, ctx, walkers, false);
191
+ const quoted = JSON.stringify(key);
192
+ if (requiredSet.has(key)) {
193
+ if (walker === null) {
194
+ inner.push(`if (!Object.hasOwn(o, ${quoted})) o = walkMissing(run, o, o !== src, ${quoted}, ${lookup ?? "null"}, null)`);
195
+ } else {
196
+ inner.push(`if (Object.hasOwn(o, ${quoted})) {`, ` const c = o[${quoted}]`, ...walkChild(walker, quoted, write(quoted)).map((line) => ` ${line}`), `} else o = walkMissing(run, o, o !== src, ${quoted}, ${lookup ?? "null"}, ${walker})`);
197
+ }
198
+ } else if (walker !== null) {
199
+ inner.push(`{`, ` const c = o[${quoted}]`, ` if (c !== undefined && Object.hasOwn(o, ${quoted})) {`, ...walkChild(walker, quoted, write(quoted)).map((line) => ` ${line}`), ` }`, `}`);
200
+ }
201
+ }
202
+ for (const key of required) {
203
+ if (Object.hasOwn(properties, key))
204
+ continue;
205
+ const quoted = JSON.stringify(key);
206
+ inner.push(`if (!Object.hasOwn(o, ${quoted})) o = walkMissing(run, o, o !== src, ${quoted}, ${lookup ?? "null"}, null)`);
207
+ }
208
+ inner.push(...mapPart(node, properties, id, ctx, walkers));
209
+ return [
210
+ `if (typeof v === 'object' && v !== null && !Array.isArray(v)) {`,
211
+ ` const src = v as Record<string, unknown>`,
212
+ ` let o = src`,
213
+ ` if (run.plain) o = walkProto(run, o, false)`,
214
+ ...inner.map((line) => ` ${line}`),
215
+ ` v = o`,
216
+ `}`
217
+ ];
218
+ };
219
+ const mapPart = (node, properties, id, ctx, walkers) => {
220
+ const patterns = Object.entries(schemaMap(node, "patternProperties") ?? {}).map(([source, child]) => ({
221
+ source,
222
+ walker: emitWalk(child, ctx, walkers, false)
223
+ }));
224
+ const additional = subschema(node, "additionalProperties");
225
+ const additionalWalker = additional === void 0 || additional === false ? null : emitWalk(additional, ctx, walkers, false);
226
+ const closed = additional === false;
227
+ const patternsMatter = patterns.some((pattern) => pattern.walker !== null) || closed || additionalWalker !== null;
228
+ if (!patternsMatter)
229
+ return [];
230
+ const helpers = ctx.repair.helpers;
231
+ const lines = [`for (const k in src) {`, ` if (!Object.hasOwn(src, k)) continue`];
232
+ const declared = Object.keys(properties);
233
+ if (declared.length > 0) {
234
+ const setName = `_walkDeclared${id}`;
235
+ helpers.push(`const ${setName} = new Set(${JSON.stringify(declared)})`);
236
+ lines.push(` if (${setName}.has(k)) continue`);
237
+ }
238
+ lines.push(` if (o !== src && !Object.hasOwn(o, k)) continue`);
239
+ const needsMatched = patterns.length > 0 && (closed || additionalWalker !== null);
240
+ if (needsMatched)
241
+ lines.push(` let matched = false`);
242
+ patterns.forEach((pattern, index) => {
243
+ const regexName = `_walkPattern${id}_${index}`;
244
+ helpers.push(`const ${regexName} = new RegExp(${JSON.stringify(pattern.source)}, ${JSON.stringify(regexFlagsFor(pattern.source))})`);
245
+ const walk = pattern.walker === null ? [] : [
246
+ `const c = o[k]`,
247
+ ...walkChild(pattern.walker, "k", `{`),
248
+ ` o = walkSet(run, o, o !== src, k, n)`,
249
+ ` if (n === REPAIR_DROP) continue`,
250
+ `}`
251
+ ];
252
+ if (!needsMatched && walk.length === 0)
253
+ return;
254
+ lines.push(` if (${regexName}.test(k)) {`);
255
+ if (needsMatched)
256
+ lines.push(` matched = true`);
257
+ lines.push(...walk.map((line) => ` ${line}`), ` }`);
258
+ });
259
+ const otherwise = needsMatched ? `if (!matched) ` : "";
260
+ if (closed) {
261
+ lines.push(` ${otherwise}o = walkExtra(run, o, o !== src, k)`);
262
+ } else if (additionalWalker !== null) {
263
+ lines.push(` ${otherwise}{`, ` const c = o[k]`, ...walkChild(additionalWalker, "k", `o = walkSet(run, o, o !== src, k, n)`).map((line) => ` ${line}`), ` }`);
264
+ }
265
+ lines.push(`}`);
266
+ return lines;
267
+ };
268
+ const arrayPart = (items, ctx, walkers) => {
269
+ const walker = emitWalk(items, ctx, walkers, false);
270
+ if (walker === null)
271
+ return [];
272
+ return [
273
+ `if (Array.isArray(v)) {`,
274
+ ` const src: unknown[] = v`,
275
+ ` let a = src`,
276
+ ` let dropped = false`,
277
+ ` for (let i = 0; i < src.length; i++) {`,
278
+ ` const c = src[i]`,
279
+ ...walkChild(walker, "i", `{`).map((line) => ` ${line}`),
280
+ ` if (n === REPAIR_DROP) dropped = true`,
281
+ ` a = walkSetItem(run, a, a !== src, i, n)`,
282
+ ` }`,
283
+ ` }`,
284
+ ` v = dropped ? walkCompact(a) : a`,
285
+ `}`
286
+ ];
287
+ };
288
+ const conditionalPart = (node, self, typeId, lookup, ctx, walkers) => {
289
+ const lines = [];
290
+ const leave = lookup === null ? "return v" : "return walkUnscope(run, v)";
291
+ const branch = (schema) => {
292
+ if (schema === void 0)
293
+ return [];
294
+ const walker = emitWalk(schema, ctx, walkers, false);
295
+ const out = walker === null ? [] : [`v = ${walker}(v, run)`, `if (v === REPAIR_DROP) ${leave}`];
296
+ if (isReferenceSchema(schema, ctx.rootSchema))
297
+ out.push(`walkRef(run, v, ${self}, ${typeId})`);
298
+ return out;
299
+ };
300
+ const ifSchema = subschema(node, "if");
301
+ const thenSchema = subschema(node, "then");
302
+ const elseSchema = subschema(node, "else");
303
+ if (ifSchema !== void 0 && (thenSchema !== void 0 || elseSchema !== void 0)) {
304
+ const test = namedTest(ifSchema, ctx.repair);
305
+ if (test === true)
306
+ lines.push(...branch(thenSchema));
307
+ else if (test === false)
308
+ lines.push(...branch(elseSchema));
309
+ else {
310
+ const thenLines = branch(thenSchema);
311
+ const elseLines = branch(elseSchema);
312
+ if (thenLines.length > 0) {
313
+ lines.push(`if (${test}(v)) {`, ...thenLines.map((line) => ` ${line}`));
314
+ if (elseLines.length > 0)
315
+ lines.push(`} else {`, ...elseLines.map((line) => ` ${line}`));
316
+ lines.push(`}`);
317
+ } else if (elseLines.length > 0) {
318
+ lines.push(`if (!${test}(v)) {`, ...elseLines.map((line) => ` ${line}`), `}`);
319
+ }
320
+ }
321
+ }
322
+ const allOf = readKey(node, "allOf");
323
+ if (Array.isArray(allOf)) {
324
+ for (const entry of allOf)
325
+ lines.push(...branch(entry));
326
+ }
327
+ if (lines.length === 0 || lookup === null)
328
+ return lines;
329
+ return [`walkScope(run, ${lookup})`, ...lines, `walkUnscope(run, v)`];
330
+ };
331
+ const emitRepairWalk = (schema, ctx) => emitWalk(schema, ctx, /* @__PURE__ */ new Map(), true);
332
+ export {
333
+ emitRepairWalk,
334
+ repairWalkNameFor
335
+ };
@@ -26,7 +26,12 @@ export declare const generateBooleanGuard: (schema: JSONSchema, typeName: string
26
26
  * fallback runs the whole validator and builds every error on the way to `false`,
27
27
  * so there the coercer walks first instead.
28
28
  */
29
- export declare const generateBooleanGuardSource: (schema: JSONSchema, typeName: string, unknownKeys?: UnknownKeysStrategy, formats?: ReadonlySet<string>) => {
29
+ export declare const generateBooleanGuardSource: (schema: JSONSchema, typeName: string, unknownKeys?: UnknownKeysStrategy, formats?: ReadonlySet<string>, refs?: {
30
+ /** The suffix `$ref`-derived names carry, so a ref's guard is called by its real name. */
31
+ readonly suffix?: string;
32
+ /** The whole document, for a ref that resolves inside it. */
33
+ readonly rootSchema?: Record<string, unknown>;
34
+ }) => {
30
35
  code: string;
31
36
  standalone: boolean;
32
37
  };
@@ -96,11 +101,24 @@ export declare const createSubschemaMatcher: (suffix: string, rootSchema: Record
96
101
  * tested, which on a union-heavy document was most of what coercing it cost.
97
102
  */
98
103
  test: (sub: JSONSchema) => boolean | string;
104
+ /**
105
+ * The early-return body of {@link test}, or `null` where `test` would have to
106
+ * fall back to its buffered expression — a guard then calls the validator.
107
+ */
108
+ guardBody: (sub: JSONSchema) => boolean | string | null;
99
109
  /**
100
110
  * The same test as a named function, hoisted with the rest and shared with
101
111
  * every other test of the same subschema in this file — or a constant, or
102
112
  * `null` for a body only the buffered form of {@link test} can express.
103
113
  */
104
114
  named: (sub: JSONSchema) => boolean | string | null;
115
+ /**
116
+ * The name of a hoisted function that says why a value fails `sub` — every
117
+ * error `validateX` would report for it, rooted at the path it is handed — or
118
+ * `null` when `sub` has no checks to explain anything with. The repair walk
119
+ * calls it once a position has already failed, so it costs nothing on valid
120
+ * input.
121
+ */
122
+ explain: (sub: JSONSchema) => string | null;
105
123
  declarations: (text: string) => string[];
106
124
  };
@@ -20,17 +20,13 @@ const checkerName = (typeName) => `check${typeName}`;
20
20
  const propertyRead = (objVar, key) => safeAccessor(objVar, key);
21
21
  const PROTOTYPE_MEMBERS = new Set(Object.getOwnPropertyNames(Object.prototype));
22
22
  const protoLocalName = (key, depth) => `_own${depth}_${key}`;
23
+ const propertyLocalName = (key, index, depth) => PROTOTYPE_MEMBERS.has(key) ? protoLocalName(key, depth) : `_v${depth}_${index}`;
23
24
  const hasOwnCheck = (objVar, key) => {
24
25
  if (PROTOTYPE_MEMBERS.has(key))
25
26
  return `Object.hasOwn(${objVar}, ${JSON.stringify(key)})`;
26
27
  return `${JSON.stringify(key)} in ${objVar}`;
27
28
  };
28
29
  const missingCheck = (objVar, key) => `!(${hasOwnCheck(objVar, key)})`;
29
- const typeofString = (type) => {
30
- if (type === "integer")
31
- return "number";
32
- return type;
33
- };
34
30
  const constMismatchCondition = (accessor, value) => {
35
31
  if (value === null || typeof value !== "object") {
36
32
  return `${accessor} !== ${JSON.stringify(value)}`;
@@ -219,7 +215,7 @@ const generateMissingRequiredChecks = (schema, ctx) => {
219
215
  }
220
216
  return lines;
221
217
  };
222
- const generatePropertyChecks = (key, propSchema, isRequired, suffix, ctx) => {
218
+ const generatePropertyChecks = (key, index, propSchema, isRequired, suffix, ctx) => {
223
219
  if (propSchema === false) {
224
220
  const path = `\`${ctx.pathPrefix}/${pointerSegment(key)}\``;
225
221
  const parentPath = ctx.depth === 0 ? "_path" : `\`${ctx.pathPrefix}\``;
@@ -239,18 +235,18 @@ const generatePropertyChecks = (key, propSchema, isRequired, suffix, ctx) => {
239
235
  ` }`
240
236
  ];
241
237
  }
242
- const raw = PROTOTYPE_MEMBERS.has(key) ? protoLocalName(key, ctx.depth) : propertyRead(ctx.objVar, key);
238
+ const raw = propertyLocalName(key, index, ctx.depth);
243
239
  const lines = [
244
- ...generatePropertyCheckLines(key, propSchema, isRequired, suffix, ctx),
240
+ ...generatePropertyCheckLines(key, raw, propSchema, isRequired, suffix, ctx),
245
241
  // `unevaluated*` is a sibling of everything else the property declares, and
246
242
  // several of the branches above return early, so it is appended here rather
247
243
  // than emitted inside them. The checks carry their own runtime type guard, so
248
244
  // an absent optional property is inert and needs no presence gate.
249
245
  ...generateUnevaluatedChecks(raw, `\`${ctx.pathPrefix}/${pointerSegment(key)}\``, propSchema, suffix, ctx)
250
246
  ];
251
- if (lines.length === 0 || !PROTOTYPE_MEMBERS.has(key))
247
+ if (!readsBinding(raw, lines.join("\n")))
252
248
  return lines;
253
- return [` const ${protoLocalName(key, ctx.depth)} = ${propertyRead(ctx.objVar, key)}`, ...lines];
249
+ return [` const ${raw} = ${propertyRead(ctx.objVar, key)}`, ...lines];
254
250
  };
255
251
  const singleIfBlock = (lines) => {
256
252
  if (lines.length < 3)
@@ -272,7 +268,11 @@ const generateKeywordChecks = (key, raw, path, schema, suffix, ctx, presence) =>
272
268
  const target = refToName(schema.$ref, suffix);
273
269
  const delegate = ctx.sink === FAIL_FAST_SINK ? [` const _r = ${checkerName(target)}(${raw}, ${path})`, ` if (_r !== true) return _r`] : ctx.sink === MATCH_BOOL_SINK ? [` if (!${ctx.refGuard(target)}(${raw})) return false`] : [
274
270
  ` const _r = ${validatorName(target)}(${raw}, ${path})`,
275
- ` if (_r !== true) ${ctx.sink}.push(..._r.errors)`
271
+ // A loop rather than a spread: spreading passes every error as an
272
+ // argument, and a document with tens of thousands of errors under
273
+ // one reference overflows the engine's argument limit with a
274
+ // RangeError instead of returning them.
275
+ ` if (_r !== true) for (const _e of _r.errors) ${ctx.sink}.push(_e)`
276
276
  ];
277
277
  if (presence === "")
278
278
  lines.push(` {`, ...delegate.map((line) => ` ${line}`), ` }`);
@@ -310,7 +310,7 @@ const generateKeywordChecks = (key, raw, path, schema, suffix, ctx, presence) =>
310
310
  const wrongType = wrongTypeCondition(typeTestAccessor(raw, ctx), t);
311
311
  if (wrongType) {
312
312
  lines.push(` if (${presence === "" ? wrongType : `${presence}(${wrongType})`}) {`);
313
- lines.push(` ${pushError(ctx.sink, `'must be ${typeofString(t)}'`, path, "type", JSON.stringify({ type: t }))}`);
313
+ lines.push(` ${pushError(ctx.sink, `'must be ${t}'`, path, "type", JSON.stringify({ type: t }))}`);
314
314
  lines.push(` }`);
315
315
  }
316
316
  }
@@ -318,7 +318,7 @@ const generateKeywordChecks = (key, raw, path, schema, suffix, ctx, presence) =>
318
318
  if (typeArray) {
319
319
  const allWrong = typeArray.map((t) => wrongTypeCondition(typeTestAccessor(raw, ctx), t)).filter((c) => c !== "").map((c) => `(${c})`).join(" && ");
320
320
  if (allWrong) {
321
- const label = typeArray.map((t) => typeofString(t)).join(" or ");
321
+ const label = typeArray.join(" or ");
322
322
  lines.push(` if (${presence === "" ? allWrong : `${presence}(${allWrong})`}) {`);
323
323
  lines.push(` ${pushError(ctx.sink, JSON.stringify(`must be ${label}`), path, "type", JSON.stringify({ type: schema.type }))}`);
324
324
  lines.push(` }`);
@@ -335,7 +335,7 @@ const generateKeywordChecks = (key, raw, path, schema, suffix, ctx, presence) =>
335
335
  }
336
336
  return lines;
337
337
  };
338
- const generatePropertyCheckLines = (key, propSchema, isRequired, suffix, ctx) => {
338
+ const generatePropertyCheckLines = (key, raw, propSchema, isRequired, suffix, ctx) => {
339
339
  const parentPath = ctx.depth === 0 ? "_path" : `\`${ctx.pathPrefix}\``;
340
340
  const missing = [
341
341
  ` if (${missingCheck(ctx.objVar, key)}) {`,
@@ -346,7 +346,6 @@ const generatePropertyCheckLines = (key, propSchema, isRequired, suffix, ctx) =>
346
346
  return [...missing, ` }`];
347
347
  return [];
348
348
  }
349
- const raw = PROTOTYPE_MEMBERS.has(key) ? protoLocalName(key, ctx.depth) : propertyRead(ctx.objVar, key);
350
349
  const path = `\`${ctx.pathPrefix}/${pointerSegment(key)}\``;
351
350
  if (isRequired) {
352
351
  const valueLines = generateKeywordChecks(key, raw, path, propSchema, suffix, ctx, "");
@@ -430,7 +429,7 @@ const generateConstraintChecks = (key, raw, path, propSchema, suffix, ctx) => {
430
429
  if (hasRef(itemSchema) && !declaresKeywordOutside(itemSchema, ["$ref"])) {
431
430
  const failFast = ctx.sink === FAIL_FAST_SINK;
432
431
  const target = refToName(itemSchema.$ref, suffix);
433
- const onFailure = failFast ? "return _ir" : `${ctx.sink}.push(..._ir.errors)`;
432
+ const onFailure = failFast ? "return _ir" : `for (const _e of _ir.errors) ${ctx.sink}.push(_e)`;
434
433
  lines.push(` if (Array.isArray(${raw})) {`);
435
434
  lines.push(` for (let ${iv} = ${firstTailIndex}; ${iv} < ${raw}.length; ${iv}++) {`);
436
435
  if (ctx.sink === MATCH_BOOL_SINK) {
@@ -573,7 +572,7 @@ const generateValueCheckLines = (_key, raw, path, propSchema, suffix, ctx, requi
573
572
  lines.push(...generateKeywordChecks("", raw, path, propSchema, suffix, valueCtx, presence));
574
573
  return lines;
575
574
  };
576
- const NARROWABLE_REFERENCE = /^[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*|\[[A-Za-z_$][\w$]*\]|\[\d+\])*$/;
575
+ const NARROWABLE_REFERENCE = /^[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*|\["[A-Za-z_$][\w$]*"\]|\[[A-Za-z_$][\w$]*\]|\[\d+\])*$/;
577
576
  const namedMatcher = (sub, suffix, ctx) => {
578
577
  const key = JSON.stringify(sub);
579
578
  const existing = ctx.namedMatchers.get(key);
@@ -710,14 +709,14 @@ const generateUnevaluatedChecks = (raw, path, schema, suffix, ctx) => {
710
709
  }
711
710
  return lines;
712
711
  };
713
- const branchErrorsReport = (guard, raw, explain, message, keyword, path, ctx) => [
712
+ const branchErrorsReport = (guard, raw, explain, total, message, keyword, path, ctx) => [
714
713
  ` if (${guard}) {`,
715
714
  ` ${pushError(ctx.sink, message, path, keyword)}`,
716
715
  ...explain.length === 0 ? [] : [
717
716
  ` const _bp = ${path}`,
718
717
  ` const _br: ValidationError[][] = []`,
719
718
  ` for (const _b of [${explain.map((name) => `${name}(${raw}, _bp)`).join(", ")}]) if (_b.length !== 0) _br.push(_b)`,
720
- ` for (const _e of selectBranchErrors(_br, _bp)) ${ctx.sink}.push(_e)`
719
+ ` for (const _e of selectBranchErrors(_br, _bp${explain.length === total ? `, ${total}` : ""})) ${ctx.sink}.push(_e)`
721
720
  ],
722
721
  ` }`
723
722
  ];
@@ -734,7 +733,7 @@ const generateCombinatorChecks = (key, raw, path, schema, suffix, ctx) => {
734
733
  const conds = schema.anyOf.map((b) => generateMatchesExpr(raw, b, suffix, ctx, true));
735
734
  if (!conds.includes("true")) {
736
735
  if (ctx.branchErrors) {
737
- lines.push(...branchErrorsReport(`!(${conds.join(" || ")})`, raw, branchExplainers(schema.anyOf, suffix, ctx), `'must match a schema in anyOf'`, "anyOf", path, ctx));
736
+ lines.push(...branchErrorsReport(`!(${conds.join(" || ")})`, raw, branchExplainers(schema.anyOf, suffix, ctx), schema.anyOf.length, `'must match a schema in anyOf'`, "anyOf", path, ctx));
738
737
  } else {
739
738
  const condition = `!(${conds.join(" || ")})`;
740
739
  if (LITERAL_CONDITION.test(condition))
@@ -756,7 +755,7 @@ const generateCombinatorChecks = (key, raw, path, schema, suffix, ctx) => {
756
755
  const explain = branchExplainers(schema.oneOf, suffix, ctx);
757
756
  lines.push(` {`);
758
757
  lines.push(` const _n = ${conds.join(" + ")}`);
759
- const report = branchErrorsReport("_n === 0", raw, explain, message, "oneOf", path, ctx).map((line) => ` ${line}`);
758
+ const report = branchErrorsReport("_n === 0", raw, explain, schema.oneOf.length, message, "oneOf", path, ctx).map((line) => ` ${line}`);
760
759
  lines.push(...report.slice(0, -1));
761
760
  lines.push(` } else if (_n !== 1) {`);
762
761
  lines.push(` ${pushError(ctx.sink, message, path, "oneOf")}`);
@@ -869,8 +868,8 @@ const generateInlineObjectChecks = (key, propSchema, raw, suffix, ctx) => {
869
868
  const required = new Set(hasRequired(propSchema) ? propSchema.required : []);
870
869
  const properties = hasProperties(propSchema) ? propSchema.properties : {};
871
870
  const innerLines = [];
872
- for (const [childKey, childSchema] of Object.entries(properties)) {
873
- innerLines.push(...generatePropertyChecks(childKey, childSchema, required.has(childKey), suffix, child));
871
+ for (const [index, [childKey, childSchema]] of Object.entries(properties).entries()) {
872
+ innerLines.push(...generatePropertyChecks(childKey, index, childSchema, required.has(childKey), suffix, child));
874
873
  }
875
874
  innerLines.push(...generateMissingRequiredChecks(propSchema, child));
876
875
  innerLines.push(...generatePatternAndAdditionalChecks(propSchema, suffix, child));
@@ -1198,8 +1197,8 @@ const generateObjectValidator = (schema, typeName, suffix, rootSchema, unknownKe
1198
1197
  const properties = hasProperties(schema) ? schema.properties : {};
1199
1198
  const ctx = createRootContext(rootSchema, formats, branchErrors, failFast);
1200
1199
  const propertyLines = [];
1201
- for (const [key, propSchema] of Object.entries(properties)) {
1202
- const checks = generatePropertyChecks(key, propSchema, required.has(key), suffix, ctx);
1200
+ for (const [index, [key, propSchema]] of Object.entries(properties).entries()) {
1201
+ const checks = generatePropertyChecks(key, index, propSchema, required.has(key), suffix, ctx);
1203
1202
  if (checks.length > 0) {
1204
1203
  propertyLines.push(...checks);
1205
1204
  }
@@ -1472,18 +1471,38 @@ const typeDescribesEveryAcceptedValue = (schema) => {
1472
1471
  return !IMPLICIT_OBJECT_KEYWORDS.some((keyword) => declaresKey(s, keyword));
1473
1472
  };
1474
1473
  const generateBooleanGuard = (schema, typeName, _suffix = "", unknownKeys = DEFAULT_UNKNOWN_KEYS, formats = NO_FORMATS) => generateBooleanGuardSource(schema, typeName, unknownKeys, formats).code;
1475
- const generateBooleanGuardSource = (schema, typeName, unknownKeys = DEFAULT_UNKNOWN_KEYS, formats = NO_FORMATS) => {
1474
+ const generateBooleanGuardSource = (schema, typeName, unknownKeys = DEFAULT_UNKNOWN_KEYS, formats = NO_FORMATS, refs = {}) => {
1476
1475
  const name = guardName(typeName);
1477
1476
  const returns = typeDescribesEveryAcceptedValue(rewriteNullable(schema)) ? `input is ${typeName}` : "boolean";
1478
- const fallback = `export const ${name} = (input: unknown): ${returns} => ${validatorName(typeName)}(input) === true`;
1477
+ const fallback = () => {
1478
+ const matcher = createSubschemaMatcher(refs.suffix ?? "", refs.rootSchema ?? (isSchemaObject(schema) ? schema : void 0), formats, "_guard");
1479
+ const body = matcher.guardBody(schema);
1480
+ if (typeof body !== "string" || !readsBinding("input", body)) {
1481
+ return {
1482
+ code: `export const ${name} = (input: unknown): ${returns} => ${validatorName(typeName)}(input) === true`,
1483
+ standalone: false
1484
+ };
1485
+ }
1486
+ const declarations = matcher.declarations(body);
1487
+ const code = [
1488
+ ...declarations.length > 0 ? [declarations.join("\n\n"), ""] : [],
1489
+ `export const ${name} = (input: unknown): ${returns} => {`,
1490
+ // Some checks build a path for a key or an index before they know whether
1491
+ // they fail. Nothing reports it here, so it is rooted at nothing.
1492
+ ...readsBinding("_path", body) ? [` const _path = ''`] : [],
1493
+ body,
1494
+ `}`
1495
+ ].join("\n");
1496
+ return { code, standalone: true };
1497
+ };
1479
1498
  const rewritten = rewriteNullable(schema);
1480
1499
  const ctx = createGuardContext(unknownKeys, formats);
1481
1500
  if (declaresObjectType(rewritten)) {
1482
1501
  if (!objectRootIsSelfContained(rewritten))
1483
- return { code: fallback, standalone: false };
1502
+ return fallback();
1484
1503
  const block = booleanObjectParts(rewritten, "input", "obj", ctx);
1485
1504
  if (block === null)
1486
- return { code: fallback, standalone: false };
1505
+ return fallback();
1487
1506
  const body = isFlatGuardBlock(block) ? [` return (`, block.conditions.map((part) => ` ${part}`).join(" &&\n"), ` )`] : [...renderGuardBlock(block, "return false", " ", "lines", unknownKeys), ` return true`];
1488
1507
  const code = [
1489
1508
  `export const ${name} = (input: unknown): ${returns} => {`,
@@ -1497,7 +1516,7 @@ const generateBooleanGuardSource = (schema, typeName, unknownKeys = DEFAULT_UNKN
1497
1516
  }
1498
1517
  const expr = booleanLeafExpr(rewritten, "input", ctx);
1499
1518
  if (expr === null)
1500
- return { code: fallback, standalone: false };
1519
+ return fallback();
1501
1520
  return { code: `export const ${name} = (input: unknown): ${returns} => ${expr}`, standalone: true };
1502
1521
  };
1503
1522
  const generateScalarValidator = (schema, typeName, suffix, rootSchema, formats, branchErrors, failFast) => {
@@ -1584,7 +1603,7 @@ const generateScalarValidator = (schema, typeName, suffix, rootSchema, formats,
1584
1603
  if (rootTypeArray2) {
1585
1604
  const allWrong = rootTypeArray2.map((t) => wrongTypeCondition(typeTestAccessor("input", ctx), t)).filter((c) => c !== "").map((c) => `(${c})`).join(" && ");
1586
1605
  if (allWrong) {
1587
- const label = rootTypeArray2.map((t) => typeofString(t)).join(" or ");
1606
+ const label = rootTypeArray2.join(" or ");
1588
1607
  checks.push(` if (${allWrong}) {`);
1589
1608
  checks.push(` ${pushError(ctx.sink, JSON.stringify(`must be ${label}`), rootPath, "type", JSON.stringify({ type: schema.type }))}`);
1590
1609
  checks.push(` }`);
@@ -1594,7 +1613,7 @@ const generateScalarValidator = (schema, typeName, suffix, rootSchema, formats,
1594
1613
  const wrongType = wrongTypeCondition(typeTestAccessor("input", ctx), t);
1595
1614
  if (wrongType) {
1596
1615
  checks.push(` if (${wrongType}) {`);
1597
- checks.push(` ${pushError(ctx.sink, `'must be ${typeofString(t)}'`, rootPath, "type", JSON.stringify({ type: t }))}`);
1616
+ checks.push(` ${pushError(ctx.sink, `'must be ${t}'`, rootPath, "type", JSON.stringify({ type: t }))}`);
1598
1617
  checks.push(` }`);
1599
1618
  }
1600
1619
  }
@@ -1616,7 +1635,7 @@ const generateScalarValidator = (schema, typeName, suffix, rootSchema, formats,
1616
1635
  const checks = [];
1617
1636
  const allWrong = rootTypeArray.map((t) => wrongTypeCondition(typeTestAccessor("input", ctx), t)).filter((c) => c !== "").map((c) => `(${c})`).join(" && ");
1618
1637
  if (allWrong) {
1619
- const label = rootTypeArray.map((t) => typeofString(t)).join(" or ");
1638
+ const label = rootTypeArray.join(" or ");
1620
1639
  checks.push(` if (${allWrong}) {`);
1621
1640
  checks.push(` ${pushError(ctx.sink, JSON.stringify(`must be ${label}`), rootPath, "type", JSON.stringify({ type: schema.type }))}`);
1622
1641
  checks.push(` }`);
@@ -1641,7 +1660,6 @@ const generateScalarValidator = (schema, typeName, suffix, rootSchema, formats,
1641
1660
  if (hasType(schema)) {
1642
1661
  const t = schema.type;
1643
1662
  const wrongType = wrongTypeCondition("input", t);
1644
- const typLabel = typeofString(t);
1645
1663
  const rootCtx = createRootContext(rootSchema, formats, branchErrors, failFast);
1646
1664
  const constraintLines = generateConstraintChecks("", "_root", "`${_path}`", schema, suffix, rootCtx);
1647
1665
  if (!wrongType) {
@@ -1655,7 +1673,7 @@ const generateScalarValidator = (schema, typeName, suffix, rootSchema, formats,
1655
1673
  return [
1656
1674
  `export const ${vName} = (input: unknown, _path = ''): ValidationResult => {`,
1657
1675
  ` if (${wrongType}) {`,
1658
- ` ${returnError(`'must be ${typLabel}'`, "_path", "type", JSON.stringify({ type: schema.type }))}`,
1676
+ ` ${returnError(`'must be ${t}'`, "_path", "type", JSON.stringify({ type: schema.type }))}`,
1659
1677
  ` }`,
1660
1678
  ` return true`,
1661
1679
  `}`
@@ -1664,7 +1682,7 @@ const generateScalarValidator = (schema, typeName, suffix, rootSchema, formats,
1664
1682
  return withHoisted(rootCtx.hoisted, [
1665
1683
  `export const ${vName} = (input: unknown, _path = ''): ValidationResult => {`,
1666
1684
  ` if (${wrongType}) {`,
1667
- ` ${returnError(`'must be ${typLabel}'`, "_path", "type", JSON.stringify({ type: schema.type }))}`,
1685
+ ` ${returnError(`'must be ${t}'`, "_path", "type", JSON.stringify({ type: schema.type }))}`,
1668
1686
  ` }`,
1669
1687
  ...readsBinding("_root", constraintLines.join("\n")) ? [` const _root: unknown = input`] : [],
1670
1688
  ...errorsBinding(failFast),
@@ -1796,12 +1814,23 @@ const createSubschemaMatcher = (suffix, rootSchema, formats, hoistNamespace, ref
1796
1814
  return true`;
1797
1815
  return ` return ${generateMatchesExpr("input", rewritten, suffix, ctx, true)}`;
1798
1816
  },
1817
+ guardBody: (sub) => {
1818
+ if (sub === false)
1819
+ return false;
1820
+ const state = { on: false, blocked: false };
1821
+ const checks = generateValueChecks("", "input", "`${_path}`", rewriteNullable(sub), suffix, { ...ctx, sink: MATCH_BOOL_SINK, failFast: state, branchErrors: false }, true);
1822
+ if (checks.length === 0)
1823
+ return true;
1824
+ return state.blocked ? null : `${checks.join("\n")}
1825
+ return true`;
1826
+ },
1799
1827
  named: (sub) => {
1800
1828
  if (sub === false)
1801
1829
  return false;
1802
1830
  const name = namedMatcher(rewriteNullable(sub), suffix, ctx);
1803
1831
  return name === "true" ? true : name;
1804
1832
  },
1833
+ explain: (sub) => namedBranchErrors(rewriteNullable(sub), suffix, ctx),
1805
1834
  declarations: (text) => keptHoisted(ctx.hoisted, text)
1806
1835
  };
1807
1836
  };