ata-validator 1.46.0 → 1.47.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,4 +1,5 @@
1
1
  'use strict'
2
+ const { compileFunction } = require('./compile-fn')
2
3
 
3
4
  // uniqueItems helpers for the boolean generator, hoisted once per compiled
4
5
  // function (and carried into standalone output through the preamble).
@@ -111,11 +112,6 @@ function emitDeq(ctx) {
111
112
  // regex passes for the keys that need nothing, which is almost all of them.
112
113
  const PTR_ESC_HELPER = "function _pe(s){for(let i=0;i<s.length;i++){const c=s.charCodeAt(i);if(c===126||c===47)return s.replace(/~/g,'~0').replace(/\\//g,'~1')}return s}"
113
114
 
114
- function emitPtrEsc(ctx) {
115
- hoistOnce(ctx, '_peHoisted', PTR_ESC_HELPER)
116
- return '_pe'
117
- }
118
-
119
115
  // `_uq` compares items with `_deq`, so it can never be hoisted alone.
120
116
  function emitUq(ctx) {
121
117
  emitDeq(ctx)
@@ -123,34 +119,6 @@ function emitUq(ctx) {
123
119
  return '_uq'
124
120
  }
125
121
 
126
- // The error and combined generators need the duplicate pair, not only the
127
- // verdict. `_uqp` returns the index of the duplicate (-1 when every item is
128
- // distinct) and leaves the item it repeats in `_uqpI`: the first item that has
129
- // a later equal, paired with its first later equal. That is the pair the
130
- // interpreted engine reports, and the generators used to report a different
131
- // one (the earliest repeat, paired with what it repeats), so the same document
132
- // named a different pair depending on the engine that answered. See
133
- // lib/unique-items.js for how it finds the pair.
134
- function emitUqp(ctx) {
135
- emitUq(ctx)
136
- hoistOnce(ctx, '_uqpHoisted', uqSources().uqp)
137
- return '_uqp'
138
- }
139
-
140
- // An object or array a const error reports as `allowedValue`, parsed once per
141
- // compiled function rather than on every error: parsing it per error made
142
- // reading a const error two to five times what the check costs. A copy of its
143
- // own, not the compared constant, so an error a caller edits cannot change
144
- // what the validator compares against. Declared in the function's helper
145
- // code, so standalone output carries it the same way.
146
- function emitParamConstant(ctx, value) {
147
- const name = `_pc${ctx.varCounter++}`
148
- const decl = `const ${name}=JSON.parse(${JSON.stringify(JSON.stringify(value))})`
149
- if (ctx.preamble) ctx.preamble.push(decl)
150
- else ctx.helperCode.push(decl)
151
- return name
152
- }
153
-
154
122
  // The compared value, parsed once at compile time. `JSON.parse` rather than a
155
123
  // JS object literal so a `__proto__` member stays an ordinary property.
156
124
  function emitConstant(ctx, value) {
@@ -180,11 +148,14 @@ function enumCondition(ctx, vals, v) {
180
148
  }
181
149
  return parts.filter(Boolean).join('||') || 'false'
182
150
  }
183
-
184
-
185
- const { codeFor } = require('./error-codes')
186
- const { ordinalFor, rankFor } = require('./schema-order')
187
- const { compileSafe, patternIsSafe } = require('./safe-regex')
151
+ // The linear regex engine (safe-regex.js) is read on the first pattern a
152
+ // schema brings, not with this module: most schemas have none, and loading
153
+ // it cost every process part of its cold start. The two names keep their
154
+ // identity (compileSafe is handed to generated code as a closure value).
155
+ let _safeRe = null
156
+ const safeRe = () => _safeRe || (_safeRe = require('./safe-regex'))
157
+ function compileSafe (src) { return safeRe().compileSafe(src) }
158
+ function patternIsSafe (src) { return safeRe().patternIsSafe(src) }
188
159
  // Whether a pattern goes to the linear-time engine. A pattern the platform
189
160
  // RegExp also runs in linear time (nativeIsLinear) takes the RegExp: it answers
190
161
  // the same, runs faster, and a compiled module that needs no other pattern
@@ -225,88 +196,6 @@ function safeReClosure (ctx, src) {
225
196
  return new RegExp(src, reFlags(src))
226
197
  }
227
198
 
228
- // Compile a JSON Schema into a pure JS validator function.
229
- // Closure-based validator — no new Function() or eval().
230
- // Returns null if the schema is too complex for JS compilation.
231
-
232
- const DOC_BASE = 'https://ata-validator.com/e/'
233
-
234
- // Build the literal fragments injected into an emitted _e.push({...}) call.
235
- // Returns { codeStr, docUrl, frame } where `frame` is a JS source fragment
236
- // like ",schemaSource:Object.freeze({...})" or "" if no source map hit.
237
- // The fragment always starts with a leading comma when non-empty so callers
238
- // can splice it inside an existing object literal.
239
- // The error's position in schema declaration order, as a literal field the
240
- // reader sorts by without deriving it from the path. Empty when the path is
241
- // not into the root document.
242
- function ordinalField (ctx, schemaPath) {
243
- if (ctx && ctx.noOrdinal) return ''
244
- const o = ctx && ctx.rootSchema ? ordinalFor(ctx.rootSchema, unescapeSp(schemaPath)) : null
245
- return o === null ? '' : `,_o:${o}`
246
- }
247
-
248
- // A schema path as the generators hold it is text for a single-quoted source
249
- // literal: ptrSeg put a backslash before every backslash and quote in a key.
250
- // Anything that uses the path as a value, the ordinal lookup and the frozen
251
- // error objects below, has to take that backslash off again, or a key such as
252
- // `it's` reads as `it\'s` and the error names a path that does not exist.
253
- // The value of a string literal the generators built for source. One that does
254
- // not parse on its own (a key with a raw line break) would not parse inside the
255
- // generated function either, so the generator declines, as it would have when
256
- // that function failed to build.
257
- function literalValue (lit) {
258
- try {
259
- return Function('return ' + lit)()
260
- } catch {
261
- throw DECLINE
262
- }
263
- }
264
-
265
- function unescapeSp (sp) {
266
- return sp.indexOf('\\') === -1 ? sp : sp.replace(/\\u([0-9a-fA-F]{4})|\\(.)/g, (m, h, c) => (h !== undefined ? String.fromCharCode(parseInt(h, 16)) : c))
267
- }
268
-
269
- const FRAME_INLINE_MAX = 120
270
-
271
- function buildErrorLiteral (opts) {
272
- const { keyword, format, schemaPath, sourceMap } = opts
273
- let code = (keyword === 'format' && format) ? codeFor('format', format) : codeFor(keyword)
274
- if (!code) code = 'ATA9001'
275
- const docUrl = DOC_BASE + code
276
- let frame = ''
277
- if (sourceMap && sourceMap.file && sourceMap.map) {
278
- const ptr = schemaPath && schemaPath.charAt(0) === '#' ? schemaPath.slice(1) : (schemaPath || '')
279
- let hit = sourceMap.map[ptr + '#key']
280
- if (!hit) hit = sourceMap.map[ptr]
281
- if (hit) {
282
- // Support both {line,col,text} (raw output of buildPositionMap) and
283
- // [line,col,text] (compact form passed by callers that pre-flattened).
284
- const line = Array.isArray(hit) ? hit[0] : hit.line
285
- const col = Array.isArray(hit) ? hit[1] : hit.col
286
- const text = Array.isArray(hit) ? hit[2] : hit.text
287
- if (sourceMap.frames && text.length > FRAME_INLINE_MAX) {
288
- // A long source line is written once at module scope (see
289
- // sourceFrameDecls in aot-impl.js) and the literal names it. Inlined,
290
- // every error site carried the whole line, and a schema kept on one
291
- // line is one line of all of it: 181 KB minified for a 2 KB schema.
292
- // Short lines stay inline: gzip folds the repeats, and measured on
293
- // the 50-field fixture the table cost 1206 gzipped bytes where the
294
- // inline frames cost 696.
295
- const key = line + ':' + col
296
- let f = sourceMap.frames.get(key)
297
- if (!f) { f = { line, col, text, index: sourceMap.frames.size }; sourceMap.frames.set(key, f) }
298
- frame = ',schemaSource:__ataSS[' + f.index + ']'
299
- } else {
300
- frame = ',schemaSource:Object.freeze({file:' + JSON.stringify(sourceMap.file) +
301
- ',line:' + line +
302
- ',col:' + col +
303
- ',text:' + JSON.stringify(text) + '})'
304
- }
305
- }
306
- }
307
- return { codeStr: code, docUrl, frame }
308
- }
309
-
310
199
  // Count Unicode code points, not UTF-16 code units (surrogate pairs).
311
200
  // JSON Schema: minLength/maxLength count characters per RFC 8259.
312
201
  // Fast path: if no surrogate pairs exist, .length is correct (covers >99% of real data).
@@ -929,30 +818,35 @@ function _canonical(x) {
929
818
  return '{' + Object.keys(x).sort().map((k) => JSON.stringify(k) + ':' + _canonical(x[k])).join(',') + '}'
930
819
  }
931
820
 
932
- const _formats = { ...require('./formats'), ...require('./formats-source') }
821
+ // The format checkers' source (formats-source.js, 15 KB) is read only when a
822
+ // schema with a `format` is compiled; loading it with this module cost every
823
+ // process 0.8 ms of its cold start.
824
+ let _formatsCache = null
825
+ function getFormats () { return _formatsCache || (_formatsCache = { ...require('./formats'), ...require('./formats-source') }) }
933
826
  // The closure path's formats. It carried its own idea of an email and knew
934
827
  // five formats in all, so a schema that reached it was told "valid" for
935
828
  // anything the other engines turned away; the first call on a fresh validator
936
829
  // is the one that lands here. Every entry now names the same function the
937
830
  // other two paths use, and the list is the list they implement.
831
+ const _fmtFns = require('./formats')
938
832
  const FORMAT_CHECKS = {
939
- email: _formats.email,
940
- 'idn-email': _formats.idnEmail,
941
- date: _formats.date,
942
- 'date-time': _formats.dateTime,
943
- time: _formats.time,
944
- duration: _formats.duration,
945
- uuid: _formats.uuid,
946
- uri: _formats.uri,
947
- 'uri-reference': _formats.uriReference,
948
- 'uri-template': _formats.uriTemplate,
949
- iri: _formats.iri,
950
- 'iri-reference': _formats.iriReference,
951
- ipv4: _formats.ipv4,
952
- ipv6: _formats.ipv6,
953
- hostname: _formats.hostname,
954
- 'json-pointer': _formats.jsonPointer,
955
- 'relative-json-pointer': _formats.relativeJsonPointer,
833
+ email: _fmtFns.email,
834
+ 'idn-email': _fmtFns.idnEmail,
835
+ date: _fmtFns.date,
836
+ 'date-time': _fmtFns.dateTime,
837
+ time: _fmtFns.time,
838
+ duration: _fmtFns.duration,
839
+ uuid: _fmtFns.uuid,
840
+ uri: _fmtFns.uri,
841
+ 'uri-reference': _fmtFns.uriReference,
842
+ 'uri-template': _fmtFns.uriTemplate,
843
+ iri: _fmtFns.iri,
844
+ 'iri-reference': _fmtFns.iriReference,
845
+ ipv4: _fmtFns.ipv4,
846
+ ipv6: _fmtFns.ipv6,
847
+ hostname: _fmtFns.hostname,
848
+ 'json-pointer': _fmtFns.jsonPointer,
849
+ 'relative-json-pointer': _fmtFns.relativeJsonPointer,
956
850
  regex: (s) => { try { new RegExp(s, 'u'); return true } catch { return false } },
957
851
  }
958
852
 
@@ -1269,11 +1163,51 @@ function _setCodeBudget (n) { codeBudget = n === undefined ? CODE_BUDGET : n }
1269
1163
  // points instead, and the caller's generator declines the schema.
1270
1164
  function nestedGenCode(schema, v, lines, ctx) {
1271
1165
  ctx.nestedBoolean = (ctx.nestedBoolean || 0) + 1
1166
+ const before = lines.length
1272
1167
  try {
1273
1168
  genCode(schema, v, lines, ctx)
1274
1169
  } finally {
1275
1170
  ctx.nestedBoolean--
1276
1171
  }
1172
+ assertEmitted(schema, lines.length - before, false)
1173
+ }
1174
+
1175
+ // Silent acceptance is the failure this generator must never produce, and
1176
+ // its usual shape is a subschema that constrains something for which no line
1177
+ // was written: the caller then reads "no lines" as "holds". That has happened
1178
+ // through a dropped deferred check, a reference expanded to nothing, and a
1179
+ // generator that returned early. So every place that turns a subschema into
1180
+ // lines asserts that a schema with a validating keyword produced at least one,
1181
+ // and declines otherwise, which routes the schema to the interpreted engine.
1182
+ // `format` is left out (not asserted under every option), and so is `type`
1183
+ // when the caller already knows the type (`ignoreType`). Unknown keywords do
1184
+ // not count: validators ignore them.
1185
+ const HC_SUBSCHEMA_KEYS = ['additionalProperties', 'items', 'unevaluatedProperties', 'unevaluatedItems', 'additionalItems', 'propertyNames', 'contains', 'not']
1186
+ const HC_SUBSCHEMA_MAPS = ['properties', 'patternProperties', 'dependentSchemas']
1187
+ const HC_SUBSCHEMA_LISTS = ['allOf', 'anyOf', 'oneOf', 'prefixItems']
1188
+ const HC_ALWAYS = new Set(['enum', 'const', 'multipleOf', 'maximum', 'exclusiveMaximum', 'minimum', 'exclusiveMinimum', 'maxLength', 'pattern', 'maxItems', 'maxProperties', '$ref', '$dynamicRef', '$recursiveRef'])
1189
+ function hasConstraints (sub, ignoreType) {
1190
+ if (sub === false) return true
1191
+ if (sub === true || sub === null || typeof sub !== 'object' || Array.isArray(sub)) return false
1192
+ for (const k of Object.keys(sub)) {
1193
+ const v = sub[k]
1194
+ if (HC_ALWAYS.has(k)) return true
1195
+ if (k === 'type') { if (!ignoreType) return true; continue }
1196
+ if (k === 'required' || k === 'dependentRequired') { if (Array.isArray(v) ? v.length > 0 : (v && typeof v === 'object' && Object.values(v).some((a) => Array.isArray(a) && a.length))) return true; continue }
1197
+ if (k === 'dependencies') { if (v && typeof v === 'object' && Object.values(v).some((a) => Array.isArray(a) ? a.length > 0 : hasConstraints(a, false))) return true; continue }
1198
+ if (k === 'uniqueItems') { if (v === true) return true; continue }
1199
+ if (k === 'minItems' || k === 'minProperties' || k === 'minLength' || k === 'minContains') { if (v > 0 && (k !== 'minContains' || sub.contains !== undefined)) return true; continue }
1200
+ if (k === 'maxContains') { if (sub.contains !== undefined) return true; continue }
1201
+ if (k === 'if') { if ((sub.then !== undefined && hasConstraints(sub.then, false)) || (sub.else !== undefined && hasConstraints(sub.else, false))) return true; continue }
1202
+ if (HC_SUBSCHEMA_KEYS.includes(k)) { if (hasConstraints(v, false)) return true; continue }
1203
+ if (HC_SUBSCHEMA_MAPS.includes(k)) { if (v && typeof v === 'object' && Object.values(v).some((x) => hasConstraints(x, false))) return true; continue }
1204
+ if (HC_SUBSCHEMA_LISTS.includes(k)) { if (Array.isArray(v) && v.some((x) => hasConstraints(x, false))) return true; continue }
1205
+ if (k === 'items' && Array.isArray(v)) { if (v.some((x) => hasConstraints(x, false))) return true; continue }
1206
+ }
1207
+ return false
1208
+ }
1209
+ function assertEmitted (sub, emitted, ignoreType) {
1210
+ if (emitted === 0 && hasConstraints(sub, ignoreType)) throw DECLINE
1277
1211
  }
1278
1212
 
1279
1213
  const CYCLE_DEPTH = 512
@@ -1379,11 +1313,6 @@ const REF_NEUTRAL_SIBLINGS = new Set([
1379
1313
  'markdownEnumDescriptions', 'defaultSnippets', 'doNotSuggest', 'suggestSortText',
1380
1314
  ])
1381
1315
 
1382
- // Stands for the schema path of a call to a recursive definition's error
1383
- // helper while its body is generated; see genCodeENode. Not a pointer, so no
1384
- // ordinal or source frame is looked up for it at compile time.
1385
- const SP_PLACEHOLDER = '@@ata-sp@@'
1386
-
1387
1316
  // Follows a definition that is a local $ref to another definition until it
1388
1317
  // reaches one that is not. False for a chain that loops, and for one whose
1389
1318
  // next hop is not a definition in this document; codegenSafe decides whether
@@ -2684,7 +2613,7 @@ function compileToJSCodegen(schema, schemaMap, userFormats, opts) {
2684
2613
  for (const dc of ctx.deferredChecks) lines.push(dc)
2685
2614
  }
2686
2615
 
2687
- if (lines.length === 0) return () => true
2616
+ if (lines.length === 0) { if (hasConstraints(schema, false)) return null; return () => true }
2688
2617
 
2689
2618
  const checkStr = lines.join('\n ')
2690
2619
 
@@ -2753,122 +2682,158 @@ function compileToJSCodegen(schema, schemaMap, userFormats, opts) {
2753
2682
  try {
2754
2683
  let boolFn
2755
2684
  if (closureNames.length > 0) {
2756
- const factory = new Function(...closureNames, `${preambleStr}return function(d){${body}}`)
2685
+ const factory = compileFunction(...closureNames, `${preambleStr}return function(d){${body}}`)
2757
2686
  boolFn = factory(...closureValues)
2758
2687
  } else if (preambleStr) {
2759
- const factory = new Function(`${preambleStr}return function(d){${body}}`)
2688
+ const factory = compileFunction(`${preambleStr}return function(d){${body}}`)
2760
2689
  boolFn = factory()
2761
2690
  } else {
2762
- boolFn = new Function('d', body)
2763
- }
2691
+ boolFn = compileFunction('d', body)
2692
+ }
2693
+
2694
+ // Light by default: the pieces the tiering and standalone paths need
2695
+ // (the hybrid, result and tail factories, the source and its parts) are
2696
+ // not attached here. They are closures over the generated text, and a
2697
+ // validator kept them for as long as it lived: 2.2 of the 3.4 KB a
2698
+ // compiled verdict retained, measured over two thousand validators used
2699
+ // once. A validator that tiers, or a build that emits it, asks for them
2700
+ // through the lazy properties below, which compile the same schema once
2701
+ // more in full (`opts.full`), a few hundred microseconds once, and only
2702
+ // for the validators that get there. The generator is deterministic, so
2703
+ // the full compile writes the same program.
2704
+ // A schema with custom keywords compiles in full at once: a keyword's
2705
+ // compile form runs per compile, and a second compile at the tier would
2706
+ // run it again.
2707
+ if (!(opts && opts.full) && !(opts && opts.keywords)) {
2708
+ attachLazyHeavy(boolFn, inputSchema, schemaMap, userFormats, opts, !!ctx.usesSafeRe)
2709
+ return boolFn
2710
+ }
2711
+ attachHeavy(boolFn, { ctx, closureNames, closureValues, preambleStr, guardStr, hybridBody, resultThunk, tailThunk, body })
2712
+ return boolFn
2713
+ } catch {
2714
+ return null
2715
+ }
2716
+ }
2764
2717
 
2718
+ const HEAVY_DATA = ['_source', '_preambleSource', '_preambleGuard', '_preambleParts', '_sharedHelpers', '_hybridSource', '_formatClosures', '_closures']
2719
+ function attachLazyHeavy (boolFn, schema, schemaMap, userFormats, opts, usesSafeRe) {
2720
+ boolFn._usesSafeRe = usesSafeRe
2721
+ let fullFn
2722
+ const full = () => fullFn || (fullFn = compileToJSCodegen(schema, schemaMap, userFormats, Object.assign({}, opts || {}, { full: true })))
2723
+ // The factories are functions that compile in full when called: the
2724
+ // callers test for their presence at construction and call them at the
2725
+ // tier, and a getter would have compiled at the test.
2726
+ boolFn._hybridFactory = (R, E) => { const f = full(); return f && f._hybridFactory ? f._hybridFactory(R, E) : null }
2727
+ boolFn._resultFactory = (LR, B, W, EE) => { const f = full(); return f && f._resultFactory ? f._resultFactory(LR, B, W, EE) : null }
2728
+ boolFn._withTail = (tail) => { const f = full(); return f && f._withTail ? f._withTail(tail) : null }
2729
+ for (const k of HEAVY_DATA) Object.defineProperty(boolFn, k, { get () { const f = full(); return f ? f[k] : undefined }, configurable: true, enumerable: false })
2730
+ }
2731
+ // The factories and the source, as the full compile attaches them.
2732
+ function attachHeavy (boolFn, P) {
2733
+ const { ctx, closureNames, closureValues, preambleStr, guardStr, hybridBody, resultThunk, tailThunk, body } = P
2765
2734
  // The hybrid: same body, return R instead of true, return E(d) instead of
2766
- // false. Compiled on first request; null when that compile is refused, and
2767
- // the caller then assembles from the error functions instead.
2768
- let hybridFactory
2769
- boolFn._hybridFactory = (R, E) => {
2770
- if (hybridFactory === undefined) {
2771
- try {
2772
- hybridFactory = new Function(...closureNames, 'R', 'E', `${preambleStr}return function(d){${hybridBody()}}`)
2773
- } catch {
2774
- hybridFactory = null
2775
- }
2735
+ // false. Compiled on first request; null when that compile is refused, and
2736
+ // the caller then assembles from the error functions instead.
2737
+ let hybridFactory
2738
+ boolFn._hybridFactory = (R, E) => {
2739
+ if (hybridFactory === undefined) {
2740
+ try {
2741
+ hybridFactory = compileFunction(...closureNames, 'R', 'E', `${preambleStr}return function(d){${hybridBody()}}`)
2742
+ } catch {
2743
+ hybridFactory = null
2776
2744
  }
2777
- return hybridFactory === null ? null : hybridFactory(...closureValues, R, E)
2778
- }
2779
-
2780
- // validate() itself: the same checks returning the result objects, for a
2781
- // validator past its first calls (see validator-core). One function per
2782
- // schema means the result is built at an allocation site of its own and
2783
- // nothing between the caller and the checks is shared with other schemas.
2784
- // Shared wrappers are what made reading results slow across many schemas:
2785
- // every validator's calls went through the same few call sites. The
2786
- // rejection is the lazy one, built from the arguments.
2787
- let resultFactory
2788
- boolFn._resultFactory = (LR, B, W, EE) => {
2789
- if (resultFactory === undefined) {
2790
- try {
2791
- resultFactory = new Function(...closureNames, '__ataLR', '__ataB', '__ataW', '__ataEE', `${preambleStr}return function(d){${resultThunk()}}`)
2792
- } catch {
2793
- resultFactory = null
2794
- }
2745
+ }
2746
+ return hybridFactory === null ? null : hybridFactory(...closureValues, R, E)
2747
+ }
2748
+
2749
+ // validate() itself: the same checks returning the result objects, for a
2750
+ // validator past its first calls (see validator-core). One function per
2751
+ // schema means the result is built at an allocation site of its own and
2752
+ // nothing between the caller and the checks is shared with other schemas.
2753
+ // Shared wrappers are what made reading results slow across many schemas:
2754
+ // every validator's calls went through the same few call sites. The
2755
+ // rejection is the lazy one, built from the arguments.
2756
+ let resultFactory
2757
+ boolFn._resultFactory = (LR, B, W, EE) => {
2758
+ if (resultFactory === undefined) {
2759
+ try {
2760
+ resultFactory = compileFunction(...closureNames, '__ataLR', '__ataB', '__ataW', '__ataEE', `${preambleStr}return function(d){${resultThunk()}}`)
2761
+ } catch {
2762
+ resultFactory = null
2795
2763
  }
2796
- return resultFactory === null ? null : resultFactory(...closureValues, LR, B, W, EE)
2797
- }
2798
-
2799
- // The same function with one more check where it would return true, for a
2800
- // wrapper that enforces something the schema does not carry (see
2801
- // Validator#_extendVerdict). Calling the check from here rather than from a
2802
- // function around this one is the point: this function is too large for
2803
- // the engine to inline, so a wrapper is a second call on every document,
2804
- // and a rejection never reaches the check at all. Built on first request,
2805
- // since almost no validator is extended. Null when code generation is
2806
- // refused, and the caller composes instead.
2807
- let tailFactory
2808
- boolFn._withTail = (tail) => {
2809
- if (tailFactory === undefined) {
2810
- try {
2811
- tailFactory = new Function(...closureNames, '__ataTail', `${preambleStr}return function(d){${tailThunk()}}`)
2812
- } catch {
2813
- tailFactory = null
2814
- }
2764
+ }
2765
+ return resultFactory === null ? null : resultFactory(...closureValues, LR, B, W, EE)
2766
+ }
2767
+
2768
+ // The same function with one more check where it would return true, for a
2769
+ // wrapper that enforces something the schema does not carry (see
2770
+ // Validator#_extendVerdict). Calling the check from here rather than from a
2771
+ // function around this one is the point: this function is too large for
2772
+ // the engine to inline, so a wrapper is a second call on every document,
2773
+ // and a rejection never reaches the check at all. Built on first request,
2774
+ // since almost no validator is extended. Null when code generation is
2775
+ // refused, and the caller composes instead.
2776
+ let tailFactory
2777
+ boolFn._withTail = (tail) => {
2778
+ if (tailFactory === undefined) {
2779
+ try {
2780
+ tailFactory = compileFunction(...closureNames, '__ataTail', `${preambleStr}return function(d){${tailThunk()}}`)
2781
+ } catch {
2782
+ tailFactory = null
2815
2783
  }
2816
- return tailFactory === null ? null : tailFactory(...closureValues, tail)
2817
- }
2818
-
2819
- // Store source for standalone compilation. Regex declarations are NOT
2820
- // inlined into the body: they are carried by _closures and the emitters
2821
- // declare them once at load scope. Inlining them here put a
2822
- // `const _reN=__ataSafeRe(...)` inside the emitted function, which
2823
- // shadowed the module-scope binding and recompiled the pattern on every
2824
- // call, 25x on a schema with one pattern.
2825
- const emitHelpers = ctx.helperCode.filter((c) => !/^const _re\d+=(?:__ataSafeRe|new RegExp)\(/.test(c))
2826
- const helperStr = emitHelpers.length ? emitHelpers.join('\n ') + '\n ' : ''
2827
- boolFn._source = helperStr + body
2828
- boolFn._preambleSource = preambleStr
2829
- boolFn._preambleGuard = guardStr
2830
- boolFn._preambleParts = ctx.preamble ? ctx.preamble.slice() : []
2831
- boolFn._sharedHelpers = ctx.shared ? ctx.shared.slice() : []
2832
- Object.defineProperty(boolFn, '_hybridSource', {
2833
- get: () => helperStr + hybridBody(),
2834
- configurable: true,
2835
- enumerable: false,
2836
- })
2837
- boolFn._usesSafeRe = !!ctx.usesSafeRe
2838
- // Custom-format closure entries that the bundle output needs to recreate.
2839
- // Stored as { name, fn } so consumers can serialize via Function#toString.
2840
- if (ctx.userFormats) {
2841
- const fmtEntries = []
2842
- for (let i = 0; i < closureNames.length; i++) {
2843
- if (closureNames[i].startsWith('_uf_')) {
2844
- // The format's own name, so an emitter can look it up at runtime
2845
- // instead of embedding the function.
2846
- let format = null
2847
- for (const key of Object.keys(ctx.userFormats)) {
2848
- if (ctx.userFormats[key] === closureValues[i]) { format = key; break }
2849
- }
2850
- fmtEntries.push({ name: closureNames[i], fn: closureValues[i], format })
2784
+ }
2785
+ return tailFactory === null ? null : tailFactory(...closureValues, tail)
2786
+ }
2787
+
2788
+ // Store source for standalone compilation. Regex declarations are NOT
2789
+ // inlined into the body: they are carried by _closures and the emitters
2790
+ // declare them once at load scope. Inlining them here put a
2791
+ // `const _reN=__ataSafeRe(...)` inside the emitted function, which
2792
+ // shadowed the module-scope binding and recompiled the pattern on every
2793
+ // call, 25x on a schema with one pattern.
2794
+ const emitHelpers = ctx.helperCode.filter((c) => !/^const _re\d+=(?:__ataSafeRe|new RegExp)\(/.test(c))
2795
+ const helperStr = emitHelpers.length ? emitHelpers.join('\n ') + '\n ' : ''
2796
+ boolFn._source = helperStr + body
2797
+ boolFn._preambleSource = preambleStr
2798
+ boolFn._preambleGuard = guardStr
2799
+ boolFn._preambleParts = ctx.preamble ? ctx.preamble.slice() : []
2800
+ boolFn._sharedHelpers = ctx.shared ? ctx.shared.slice() : []
2801
+ Object.defineProperty(boolFn, '_hybridSource', {
2802
+ get: () => helperStr + hybridBody(),
2803
+ configurable: true,
2804
+ enumerable: false,
2805
+ })
2806
+ boolFn._usesSafeRe = !!ctx.usesSafeRe
2807
+ // Custom-format closure entries that the bundle output needs to recreate.
2808
+ // Stored as { name, fn } so consumers can serialize via Function#toString.
2809
+ if (ctx.userFormats) {
2810
+ const fmtEntries = []
2811
+ for (let i = 0; i < closureNames.length; i++) {
2812
+ if (closureNames[i].startsWith('_uf_')) {
2813
+ // The format's own name, so an emitter can look it up at runtime
2814
+ // instead of embedding the function.
2815
+ let format = null
2816
+ for (const key of Object.keys(ctx.userFormats)) {
2817
+ if (ctx.userFormats[key] === closureValues[i]) { format = key; break }
2851
2818
  }
2819
+ fmtEntries.push({ name: closureNames[i], fn: closureValues[i], format })
2852
2820
  }
2853
- if (fmtEntries.length) boolFn._formatClosures = fmtEntries
2854
- }
2855
- // Closure variables (regex, sub-validators, sets) referenced in _source that
2856
- // standalone module output must declare. Excludes _cpLen (emitted by _CP_LEN_SOURCE)
2857
- // and _uf_* (emitted via _formatClosures).
2858
- {
2859
- const entries = []
2860
- for (let i = 0; i < closureNames.length; i++) {
2861
- const name = closureNames[i]
2862
- if (name === '_cpLen' || name.startsWith('_uf_')) continue
2863
- entries.push({ name, val: closureValues[i] })
2864
- }
2865
- if (entries.length) boolFn._closures = entries
2866
2821
  }
2867
-
2868
- return boolFn
2869
- } catch {
2870
- return null
2822
+ if (fmtEntries.length) boolFn._formatClosures = fmtEntries
2823
+ }
2824
+ // Closure variables (regex, sub-validators, sets) referenced in _source that
2825
+ // standalone module output must declare. Excludes _cpLen (emitted by _CP_LEN_SOURCE)
2826
+ // and _uf_* (emitted via _formatClosures).
2827
+ {
2828
+ const entries = []
2829
+ for (let i = 0; i < closureNames.length; i++) {
2830
+ const name = closureNames[i]
2831
+ if (name === '_cpLen' || name.startsWith('_uf_')) continue
2832
+ entries.push({ name, val: closureValues[i] })
2833
+ }
2834
+ if (entries.length) boolFn._closures = entries
2871
2835
  }
2836
+
2872
2837
  }
2873
2838
 
2874
2839
  // Replace top-level `return false` → `return E(d)` and `return true` → `return R`.
@@ -3071,7 +3036,13 @@ function tryGenCombined(schema, access, ctx) {
3071
3036
  function _deferOrInline(ctx, lines, v, check) {
3072
3037
  // Deferring is only sound at the top level of the root function. Inside a
3073
3038
  // conditional applicator (dependentSchemas) the check must stay in its block.
3074
- if (v === 'd' && !ctx.condDepth) {
3039
+ // Nor inside a verdict generated for a subschema (nestedGenCode, the inline
3040
+ // annotation verdicts of run-time unevaluatedProperties): those lines go
3041
+ // into a block or a function of their own and nothing flushes the deferred
3042
+ // list for them, so a deferred check was dropped. A branch of an anyOf with
3043
+ // `additionalProperties: { const: true }` at the root then read as holding
3044
+ // for every object, and unevaluatedProperties accepted what it evaluated.
3045
+ if (v === 'd' && !ctx.condDepth && !ctx.nestedBoolean) {
3075
3046
  if (!ctx.deferredChecks) ctx.deferredChecks = []
3076
3047
  ctx.deferredChecks.push(check)
3077
3048
  } else {
@@ -4712,86 +4683,59 @@ function genUnevaluatedItemsNode(schema, v, lines, ctx, knownType, isArr) {
4712
4683
  // costs more than the call. They are hoisted once per compiled function and
4713
4684
  // invoked by name: one real schema with four email fields grew by 20 KB when
4714
4685
  // the check was inlined at each of them.
4715
- const EMAIL_HELPER = `function _em(_s){${_formats.emailSource('_s', true)}return true}`
4686
+ const emailHelper = () => `function _em(_s){${getFormats().emailSource('_s', true)}return true}`
4716
4687
 
4717
4688
  // `uri` is the most expensive format an ordinary document carries: a schema
4718
4689
  // with a handful of URL fields spent more time here than on every structural
4719
4690
  // check put together. The walk reads its character classes out of hoisted
4720
4691
  // tables, so it is declared once per compiled function rather than inlined at
4721
4692
  // each call site, where the tables would have to be rebuilt.
4722
- const URI_HELPER = _formats.uriHelperSource('_uri')
4693
+ const uriHelper = () => getFormats().uriHelperSource('_uri')
4723
4694
 
4724
4695
  const FORMAT_CODEGEN = {
4725
4696
  email: (v, isStr, ctx) => {
4726
- if (!ctx) return _formats.emailSource(v, isStr)
4727
- hoistOnce(ctx, '_emHoisted', EMAIL_HELPER)
4697
+ if (!ctx) return getFormats().emailSource(v, isStr)
4698
+ hoistOnce(ctx, '_emHoisted', emailHelper())
4728
4699
  return isStr ? `if(!_em(${v}))return false` : `if(typeof ${v}==='string'&&!_em(${v}))return false`
4729
4700
  },
4730
- 'json-pointer': _formats.jsonPointerSource,
4731
- 'relative-json-pointer': _formats.relativeJsonPointerSource,
4732
- 'uri-template': _formats.uriTemplateSource,
4733
- iri: _formats.iriSource,
4734
- 'iri-reference': _formats.iriReferenceSource,
4735
- 'idn-email': _formats.idnEmailSource,
4701
+ 'json-pointer': (...a) => getFormats().jsonPointerSource(...a),
4702
+ 'relative-json-pointer': (...a) => getFormats().relativeJsonPointerSource(...a),
4703
+ 'uri-template': (...a) => getFormats().uriTemplateSource(...a),
4704
+ iri: (...a) => getFormats().iriSource(...a),
4705
+ 'iri-reference': (...a) => getFormats().iriReferenceSource(...a),
4706
+ 'idn-email': (...a) => getFormats().idnEmailSource(...a),
4736
4707
  regex: (v, isStr) => {
4737
4708
  const inner = `try{new RegExp(${v},'u')}catch(_er){return false}`
4738
4709
  return isStr ? `{${inner}}` : `if(typeof ${v}==='string'){${inner}}`
4739
4710
  },
4740
- date: _formats.dateSource,
4741
- uuid: _formats.uuidSource,
4742
- 'date-time': _formats.dateTimeSource,
4743
- time: _formats.timeSource,
4744
- duration: _formats.durationSource,
4711
+ date: (...a) => getFormats().dateSource(...a),
4712
+ uuid: (...a) => getFormats().uuidSource(...a),
4713
+ 'date-time': (...a) => getFormats().dateTimeSource(...a),
4714
+ time: (...a) => getFormats().timeSource(...a),
4715
+ duration: (...a) => getFormats().durationSource(...a),
4745
4716
  uri: (v, isStr, ctx) => {
4746
- if (!ctx) return _formats.uriSource(v, isStr)
4747
- hoistOnce(ctx, '_uriHoisted', URI_HELPER)
4717
+ if (!ctx) return getFormats().uriSource(v, isStr)
4718
+ hoistOnce(ctx, '_uriHoisted', uriHelper())
4748
4719
  return isStr ? `if(!_uri(${v}))return false` : `if(typeof ${v}==='string'&&!_uri(${v}))return false`
4749
4720
  },
4750
4721
  'uri-reference': (v, isStr) => isStr
4751
- ? `{${_formats.uriCharsSource(v, '0')}}`
4752
- : `if(typeof ${v}==='string'){${_formats.uriCharsSource(v, '0')}}`,
4722
+ ? `{${getFormats().uriCharsSource(v, '0')}}`
4723
+ : `if(typeof ${v}==='string'){${getFormats().uriCharsSource(v, '0')}}`,
4753
4724
  ipv4: (v, isStr, ctx) => {
4754
- if (!ctx) return _formats.ipv4Source(v, isStr)
4725
+ if (!ctx) return getFormats().ipv4Source(v, isStr)
4755
4726
  // Hoisted: a regex literal in the body would build a new RegExp per call.
4756
- hoistOnce(ctx, '_ip4Hoisted', 'const _ip4=/' + _formats.IPV4.source + '/')
4727
+ hoistOnce(ctx, '_ip4Hoisted', 'const _ip4=/' + getFormats().IPV4.source + '/')
4757
4728
  return isStr ? `if(!_ip4.test(${v}))return false` : `if(typeof ${v}==='string'&&!_ip4.test(${v}))return false`
4758
4729
  },
4759
4730
  ipv6: (v, isStr, ctx) => {
4760
- if (!ctx) return _formats.ipv6Source(v, isStr)
4761
- hoistOnce(ctx, '_ip6Hoisted', 'const _ip6f=/' + _formats.IPV6_FULL.source + '/')
4731
+ if (!ctx) return getFormats().ipv6Source(v, isStr)
4732
+ hoistOnce(ctx, '_ip6Hoisted', 'const _ip6f=/' + getFormats().IPV6_FULL.source + '/')
4762
4733
  // The fast accept jumps past the walk; the walk's `return false`s are what
4763
4734
  // the error generators rewrite, so they stay as they are.
4764
- const body = `_ip6:{if(${v}.length>=15&&_ip6f.test(${v}))break _ip6;${_formats.ipv6Source(v, true).replace(/^\{|\}$/g, '')}}`
4735
+ const body = `_ip6:{if(${v}.length>=15&&_ip6f.test(${v}))break _ip6;${getFormats().ipv6Source(v, true).replace(/^\{|\}$/g, '')}}`
4765
4736
  return isStr ? body : `if(typeof ${v}==='string'){${body}}`
4766
4737
  },
4767
- hostname: _formats.hostnameSource,
4768
- }
4769
-
4770
- // Safe key escaping: use JSON.stringify to handle all special chars (newlines, null bytes, etc.)
4771
- // A schema string as the body of a JavaScript string literal, single- or
4772
- // double-quoted. JSON.stringify escapes the double quote and the backslash;
4773
- // the single quote needs its own escape, because the error emitters put these
4774
- // in '...' literals. Without it a property name holding a quote closed the
4775
- // literal, and the rest of the name ran as code in the generated validator.
4776
- function esc(s) { return JSON.stringify(s).slice(1, -1).replace(/'/g, "\\'") }
4777
- // A schema pointer segment for a pattern: JSON Pointer escaping, then made
4778
- // safe for the single-quoted literal the error emitters use.
4779
- // A pointer segment as text for a single-quoted literal. Control characters
4780
- // and the two line separators go in as \u escapes: written raw, a property
4781
- // named "foo\nbar" broke the literal, the combined function failed to compile
4782
- // and every schema with such a name lost it.
4783
- function ptrSeg(s) { return s.replace(/~/g, '~0').replace(/\//g, '~1').replace(/\\/g, '\\\\').replace(/'/g, "\\'").replace(/[\u0000-\u001f\u2028\u2029]/g, (c) => '\\u' + c.charCodeAt(0).toString(16).padStart(4, '0')) }
4784
-
4785
- // Resolve child path at codegen time when parent is a static string literal.
4786
- // This enables frozen pre-allocation for ALL nested error objects.
4787
- function childPathExpr(parentExpr, suffix) {
4788
- if (!parentExpr) return `'/${suffix}'`
4789
- if (parentExpr.startsWith("'") && !parentExpr.includes('+')) {
4790
- // Static parent: resolve at codegen time → '/parent/child' (single literal)
4791
- return `'${parentExpr.slice(1, -1)}/${suffix}'`
4792
- }
4793
- // Dynamic parent: keep as concat expression
4794
- return `${parentExpr}+'/${suffix}'`
4738
+ hostname: (...a) => getFormats().hostnameSource(...a),
4795
4739
  }
4796
4740
 
4797
4741
  // Compile simple regex patterns to inline charCode checks — avoids RegExp engine overhead.
@@ -4931,12 +4875,6 @@ function charClassToCheck(charClass, codeExpr) {
4931
4875
  return `(${checks.join('||')})`
4932
4876
  }
4933
4877
 
4934
- // Same but for dynamic segments (array indices)
4935
- function childPathDynExpr(parentExpr, indexExpr) {
4936
- if (!parentExpr) return `'/'+${indexExpr}`
4937
- return `${parentExpr}+'/'+${indexExpr}`
4938
- }
4939
-
4940
4878
  // unevaluated* differs from additionalProperties/items only through
4941
4879
  // annotations contributed by in-place applicators at the same schema object.
4942
4880
  // When every applicator subschema is provably free of the annotating keywords
@@ -4983,64 +4921,6 @@ function unevalContributions (sub, kind, names, state, depth) {
4983
4921
  return true
4984
4922
  }
4985
4923
 
4986
- // A contribution that names only what the node's own adjacent keywords
4987
- // already evaluate unconditionally changes nothing: a key in the node's own
4988
- // `properties` is evaluated whether or not the branch that also names it
4989
- // passes, and a branch prefix no longer than the node's own is covered
4990
- // position for position. Anything beyond those bounds is conditional
4991
- // evaluation the plain counterpart cannot express, and the node stays with
4992
- // the interpreted engine.
4993
- function unevalLocalOk (node, key) {
4994
- if (node[key] !== false) return false
4995
- if (node.$ref !== undefined || node.$dynamicRef !== undefined || node.$recursiveRef !== undefined) return false
4996
- if (node.dependentSchemas !== undefined || node.dependencies !== undefined) return false
4997
- const kind = key === 'unevaluatedProperties' ? 'props' : 'items'
4998
- if (kind === 'props' && node.patternProperties !== undefined) return false
4999
- if (kind === 'items' && node.contains !== undefined) return false
5000
- const names = new Set()
5001
- const state = { prefix: 0 }
5002
- for (const k of ['allOf', 'anyOf', 'oneOf']) {
5003
- if (node[k] !== undefined) {
5004
- if (!Array.isArray(node[k])) return false
5005
- for (const b of node[k]) if (!unevalContributions(b, kind, names, state, 0)) return false
5006
- }
5007
- }
5008
- for (const k of ['if', 'then', 'else']) {
5009
- if (node[k] !== undefined && !unevalContributions(node[k], kind, names, state, 0)) return false
5010
- }
5011
- if (kind === 'props') {
5012
- const own = node.properties && typeof node.properties === 'object' ? node.properties : {}
5013
- for (const n of names) if (!Object.prototype.hasOwnProperty.call(own, n)) return false
5014
- } else {
5015
- const ownPrefix = Array.isArray(node.prefixItems) ? node.prefixItems.length
5016
- : (Array.isArray(node.items) ? node.items.length : 0)
5017
- if (node.items !== undefined && !Array.isArray(node.items)) { /* all evaluated; any prefix is covered */ }
5018
- else if (state.prefix > ownPrefix) return false
5019
- }
5020
- return true
5021
- }
5022
-
5023
- // Every occurrence in the document must qualify or the generator declines the
5024
- // whole schema, exactly as the string scan it replaces did. The walk is
5025
- // generic and may visit data positions (an enum value, say); judging those is
5026
- // harmless, since a data node that fails the test only makes the answer more
5027
- // conservative and the generators never read data positions as schemas.
5028
- function unevalAllProvablyLocal (root, only) {
5029
- const seen = new Set()
5030
- const stack = [root]
5031
- while (stack.length) {
5032
- const n = stack.pop()
5033
- if (n === null || typeof n !== 'object' || seen.has(n)) continue
5034
- seen.add(n)
5035
- if (!Array.isArray(n)) {
5036
- if (only !== 'items' && n.unevaluatedProperties !== undefined && !unevalLocalOk(n, 'unevaluatedProperties')) return false
5037
- if (n.unevaluatedItems !== undefined && !unevalLocalOk(n, 'unevaluatedItems')) return false
5038
- }
5039
- for (const k of Array.isArray(n) ? n : Object.values(n)) stack.push(k)
5040
- }
5041
- return true
5042
- }
5043
-
5044
4924
  // Detect simple prefix patterns like "^x-", "^_", "^prefix" and generate fast charCodeAt checks
5045
4925
  // Returns a JS expression string or null if pattern is too complex
5046
4926
  function fastPrefixCheck(pattern, keyVar) {
@@ -5085,2794 +4965,135 @@ function genCharCodeSwitch(keys, v) {
5085
4965
  return `for(var _k in ${v}){switch(_k.charCodeAt(0)){${cases}default:break}return false}`
5086
4966
  }
5087
4967
 
5088
- // --- Error-collecting codegen: same checks, but pushes errors instead of returning false ---
5089
- // Returns a function: (data, allErrors) => { valid, errors }
5090
- // Valid path is still fast — only error path does extra work.
5091
- function compileToJSCodegenWithErrors(schema, schemaMap, userFormats, sourceOpts) {
5092
- const inputSchema = schema
5093
- schema = prepareForCodegen(schema, schemaMap)
5094
- // unevaluated* is generated only where it is provably its plain counterpart
5095
- // (see unevalLocalOk); any occurrence that is not keeps the whole schema on
5096
- // the interpreted engine's error path, exactly as before.
5097
- if (typeof schema === 'object' && schema !== null) {
5098
- const s = JSON.stringify(schema)
5099
- if ((s.includes('unevaluatedProperties') || s.includes('unevaluatedItems')) && !unevalAllProvablyLocal(schema)) return null
5100
- }
5101
- if (typeof schema === 'boolean') {
5102
- return schema
5103
- ? () => ({ valid: true, errors: [] })
5104
- : () => ({ valid: false, errors: [{ keyword: 'not', instancePath: '', schemaPath: '#', params: {}, message: 'boolean schema is false' }] })
4968
+ // The verdict form of genUnevaluatedDynamicC: the same set, built the same
4969
+ // way, and the answer false at the first key outside it.
4970
+ function genUnevalDynamicVerdict(schema, v, lines, ctx, isObj, nonObjApplicators) {
4971
+ if (schema.additionalProperties !== undefined) {
4972
+ if (nonObjApplicators) throw DECLINE
4973
+ return
5105
4974
  }
5106
- if (typeof schema !== 'object' || schema === null) return null
5107
- if (!sharedCodegenGate(schema, schemaMap)) return null
5108
- if (schema.patternProperties) {
5109
- for (const [pat, sub] of Object.entries(schema.patternProperties)) {
5110
- if (typeof sub === 'object' && sub !== null && !codegenSafe(sub, schemaMap)) return null
5111
- }
4975
+ const ui = ctx.varCounter++
4976
+ const S = `_ev${ui}`
4977
+ const body = [`const ${S}=[]`]
4978
+ addLocalAnnotations(schema, v, S, body, ctx)
4979
+ // With the anyOf and oneOf blocks moved here (see genCodeNode), their
4980
+ // passing branches are counted by the annotation pass and judged from the
4981
+ // count: a oneOf holds with exactly one, an anyOf with at least one.
4982
+ const counts = nonObjApplicators ? { anyOf: `_ac${ui}`, oneOf: `_oc${ui}`, oneOfMask: null, of: null, verdict: true } : null
4983
+ if (counts) body.push(`let ${counts.anyOf}=0,${counts.oneOf}=0`)
4984
+ addTopApplicatorAnnotations(schema, v, S, body, ctx, counts)
4985
+ if (counts) {
4986
+ if (Array.isArray(schema.anyOf)) body.push(`if(${counts.anyOf}<1)return false`)
4987
+ if (Array.isArray(schema.oneOf)) body.push(`if(${counts.oneOf}!==1)return false`)
5112
4988
  }
5113
- if (schema.dependentSchemas) {
5114
- for (const sub of Object.values(schema.dependentSchemas)) {
5115
- if (typeof sub === 'object' && sub !== null && !codegenSafe(sub, schemaMap)) return null
5116
- }
4989
+ const kVar = `_uk${ui}`
4990
+ const up = schema.unevaluatedProperties
4991
+ if (up === false) {
4992
+ body.push(`for(const ${kVar} of Object.keys(${v}))if(!${S}.includes(${kVar}))return false`)
4993
+ } else if (typeof up === 'object' && up !== null) {
4994
+ const sub = []
4995
+ genCode(up, `${v}[${kVar}]`, sub, ctx)
4996
+ if (sub.length) body.push(`for(const ${kVar} of Object.keys(${v}))if(!${S}.includes(${kVar})){${sub.join('\n ')}\n }`)
4997
+ } else {
4998
+ throw DECLINE
5117
4999
  }
5118
- if (schema.propertyNames && typeof schema.propertyNames === 'object' && !isSimplePN(schema.propertyNames) && !codegenSafe(schema.propertyNames, schemaMap)) return null
5000
+ // A value that is not an object skips the pass, so the blocks moved here
5001
+ // run for it in the `else`; a value known to be an object has no `else`.
5002
+ const other = nonObjApplicators && nonObjApplicators.length && !isObj ? `else{${nonObjApplicators.join('\n ')}}` : ''
5003
+ lines.push(isObj ? `{${body.join(';')}}` : `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){${body.join(';')}}${other}`)
5004
+ }
5119
5005
 
5120
- // Build anchors map for $ref/#anchor and $dynamicRef resolution
5121
- const eRootDefs = schema.$defs || schema.definitions || null
5122
- const eAnchors = {}
5123
- if (schema.$dynamicAnchor) eAnchors['#' + schema.$dynamicAnchor] = schema
5124
- if (schema.$anchor) eAnchors['#' + schema.$anchor] = schema
5125
- if (typeof schema.$id === 'string' && schema.$id.startsWith('#')) eAnchors[schema.$id] = schema
5126
- if (eRootDefs) {
5127
- for (const def of Object.values(eRootDefs)) {
5128
- if (def && typeof def === 'object') {
5129
- if (def.$dynamicAnchor) eAnchors['#' + def.$dynamicAnchor] = def
5130
- if (def.$anchor) eAnchors['#' + def.$anchor] = def
5131
- if (typeof def.$id === 'string' && def.$id.startsWith('#')) eAnchors[def.$id] = def
5132
- }
5133
- }
5006
+ // The names a node's own keywords evaluate, pushed onto the array `S`:
5007
+ // present declared properties, keys matching a pattern, and every key when
5008
+ // additionalProperties is there.
5009
+ function addLocalAnnotations(node, v, S, out, ctx) {
5010
+ if (node.additionalProperties !== undefined) {
5011
+ out.push(`for(const _ka in ${v})${S}.push(_ka)`)
5012
+ return
5134
5013
  }
5135
- if (schemaMap) {
5136
- for (const ext of schemaMap.values()) {
5137
- if (ext && typeof ext === 'object') {
5138
- if (ext.$dynamicAnchor && !eAnchors['#' + ext.$dynamicAnchor]) eAnchors['#' + ext.$dynamicAnchor] = ext
5139
- if (ext.$anchor && !eAnchors['#' + ext.$anchor]) eAnchors['#' + ext.$anchor] = ext
5140
- if (typeof ext.$id === 'string' && ext.$id.startsWith('#') && !eAnchors[ext.$id]) eAnchors[ext.$id] = ext
5141
- }
5014
+ for (const k of Object.keys(node.properties || {})) out.push(`if(Object.prototype.hasOwnProperty.call(${v},${JSON.stringify(k)}))${S}.push(${JSON.stringify(k)})`)
5015
+ const pats = Object.keys(node.patternProperties || {})
5016
+ if (pats.length) {
5017
+ const tests = []
5018
+ const kv = `_pk${ctx.varCounter++}`
5019
+ for (const pat of pats) {
5020
+ const fast = fastPrefixCheck(pat, kv)
5021
+ if (fast) { tests.push(fast); continue }
5022
+ const ri = ctx.varCounter++
5023
+ ctx.closureVars.push(`_re${ri}`)
5024
+ ctx.closureVals.push(safeReClosure(ctx, pat))
5025
+ tests.push(`_re${ri}.test(${kv})`)
5142
5026
  }
5027
+ out.push(`for(const ${kv} in ${v})if(${tests.join('||')})${S}.push(${kv})`)
5143
5028
  }
5029
+ }
5144
5030
 
5145
- // As in the boolean generator: a reference this path cannot resolve or follow
5146
- // emits no lines, and an empty program is returned below as always-valid.
5147
- if (hasUnresolvableRef(schema, eRootDefs, eAnchors, schemaMap, new Set())) return null
5148
- if (needsBaseTracking(schema, schemaMap, new Set())) return null
5149
-
5150
- const ctx = { varCounter: 0, helperCode: [], rootDefs: eRootDefs, shared: [], refStack: new Set(), schemaMap: schemaMap || null, anchors: eAnchors, rootE: schema, rootSchema: inputSchema, userFormats: userFormats || null,
5151
- // Custom format checkers referenced by the body, bound as
5152
- // closure parameters below the way the other entry points do.
5153
- closureVars: [], closureVals: [],
5154
- sourceMap: (sourceOpts && sourceOpts.sourceMap && sourceOpts.schemaFile)
5155
- ? { file: sourceOpts.schemaFile, map: sourceOpts.sourceMap, frames: sourceOpts.frames || null }
5156
- : null }
5157
- ctx.helperCode.push('const _cpLen=s=>{let n=0;for(const _ of s)n++;return n}')
5158
- const lines = []
5159
- try {
5160
- genCodeE(schema, 'd', '', lines, ctx, '#')
5161
- } catch (e) {
5162
- if (e === DECLINE) return null
5163
- throw e
5031
+ // What the applicators of the node that owns the unevaluatedProperties add.
5032
+ // Their validity is checked by the code around this; here a branch only
5033
+ // contributes, through its annotation function, when it passes, and a oneOf
5034
+ // only with exactly one passing branch. An annotation function that fails
5035
+ // leaves `S` as it found it.
5036
+ function addTopApplicatorAnnotations(node, v, S, out, ctx, counts) {
5037
+ if (node.$dynamicRef !== undefined || node.$recursiveRef !== undefined || node.dependencies !== undefined) throw DECLINE
5038
+ for (const b of node.allOf || []) out.push(`${emitAnnot(b, ctx)}(${v},${S})`)
5039
+ // With `counts`, the passing branches of anyOf and oneOf are counted into
5040
+ // variables the caller declared, so the keyword's own error code further
5041
+ // down can skip its branches when the count already says it holds (see
5042
+ // emitBranchCollapse).
5043
+ if (Array.isArray(node.anyOf)) {
5044
+ if (counts) out.push(`${counts.anyOf}=0;${node.anyOf.map((b) => `if(${emitAnnot(b, ctx)}(${v},${S}))${counts.anyOf}++;`).join('')}`)
5045
+ else for (const b of node.anyOf) out.push(`${emitAnnot(b, ctx)}(${v},${S})`)
5164
5046
  }
5165
- // Inline branch-collapse helper only when the schema actually used a
5166
- // oneOf/anyOf branch path (tracked via ctx.usesBranchCollapse). Keeps the
5167
- // AOT output small for schemas that don't compose. Standalone modules can't
5168
- // require('./branch-collapse'), so the helper body is embedded verbatim.
5169
- if (ctx.usesBranchCollapse) {
5170
- ctx.helperCode.push(require('./branch-collapse').embedSource())
5047
+ if (Array.isArray(node.oneOf)) {
5048
+ const t = counts ? counts.oneOf : `_oc${ctx.varCounter++}`
5049
+ // Which branches passed, as bits, for a collapse that has to name them.
5050
+ const mask = counts && node.oneOf.length <= 30 ? counts.oneOfMask : null
5051
+ // In a verdict (counts.verdict), a second passing branch is the answer.
5052
+ const over = counts && counts.verdict ? `if(${t}>1)return false;` : ''
5053
+ out.push(`{const ${t}m=${S}.length;${counts ? '' : 'let '}${t}=0;${node.oneOf.map((b, i) => `if(${emitAnnot(b, ctx)}(${v},${S})){${t}++;${over}${mask ? `${mask}|=${1 << i};` : ''}}`).join('')}if(${t}!==1)_rb(${S},${t}m)}`)
5171
5054
  }
5172
- if (lines.length === 0) return (d) => ({ valid: true, errors: [] })
5173
-
5174
- const checkStr = lines.join('\n ')
5175
- const defSetsE = ctx.defFns ? Array.from(ctx.defFns.values()) : []
5176
- const needsGuardE = ctx.usesRecursion || defSetsE.length > 0
5177
- const guardStrE = needsGuardE ? emitGuardState() : ''
5178
- const helpersE = ctx.helperCode.length ? ctx.helperCode.join('\n ') + '\n ' : ''
5179
-
5180
- let body
5181
- if (ctx.usesRecursion) {
5182
- // The abandoned first pass may have pushed errors, so the retry starts
5183
- // from an empty list and the reported errors come from the guarded run
5184
- // alone.
5185
- body = `const _e=[];\n ` + guardStrE + helpersE +
5186
- emitRootGuard('_validateE', 'd,_all,_e', '') +
5187
- `function _validateE_b(d,_all,_e){\n ${checkStr}\n }\n ` +
5188
- emitGuardedRun('_validateE(d,_all,_e)', '_e.length=0;', defSetsE) +
5189
- `return{valid:_e.length===0,errors:_e}`
5190
- } else if (defSetsE.length > 0) {
5191
- body = `const _e=[];\n ` + guardStrE + helpersE +
5192
- `function _bodyE(d,_all,_e){\n ${checkStr}\n }\n ` +
5193
- emitGuardedRun('_bodyE(d,_all,_e)', '_e.length=0;', defSetsE) +
5194
- `return{valid:_e.length===0,errors:_e}`
5195
- } else {
5196
- body = `const _e=[];\n ` + helpersE + checkStr +
5197
- `\n return{valid:_e.length===0,errors:_e}`
5055
+ if (node.if !== undefined) {
5056
+ const thenCall = node.then !== undefined ? `${emitAnnot(node.then, ctx)}(${v},${S})` : ''
5057
+ const elseCall = node.else !== undefined ? `${emitAnnot(node.else, ctx)}(${v},${S})` : ''
5058
+ out.push(`if(${emitAnnot(node.if, ctx)}(${v},${S})){${thenCall}}else{${elseCall}}`)
5198
5059
  }
5199
- try {
5200
- let fn
5201
- // Custom format checkers come in as leading parameters, bound once here,
5202
- // so the body's `_uf_<name>(...)` calls resolve. Without a binding the
5203
- // first invalid document threw a ReferenceError instead of reporting.
5204
- const cvars = ctx.closureVars
5205
- const cvals = ctx.closureVals
5206
- // The helpers are constants: the uri tables, the compiled patterns, the
5207
- // branch-collapse functions, the name sets. In `body` they sit inside the
5208
- // function and were rebuilt on every call, which on a product schema with
5209
- // six URLs and one anyOf cost about 450 ns before a single check ran, more
5210
- // than every check and every error together. Here they run once, in a
5211
- // factory, as the verdict function's preamble does; with $defs or
5212
- // recursion too, below. `_errSource` stays `body` either way, for the
5213
- // emitters.
5214
- // The factory builds the helpers once and returns the error function.
5215
- // With $defs or recursion the helpers are the definitions' functions and
5216
- // the cycle guard's state lives beside them; the guarded run resets that
5217
- // state at its start, and a call made while one is running (a custom
5218
- // keyword validating with the same validator) gets a function from a fresh
5219
- // factory call instead of sharing it. `_errFactory` carries the same text
5220
- // to the standalone emitter.
5221
- let factoryBody
5222
- if (needsGuardE) {
5223
- const attempt = ctx.usesRecursion ? '_validateE(d,_all,_e)' : '_bodyE(d,_all,_e)'
5224
- const runner = ctx.usesRecursion
5225
- ? emitRootGuard('_validateE', 'd,_all,_e', '') + `function _validateE_b(d,_all,_e){\n ${checkStr}\n }\n `
5226
- : `function _bodyE(d,_all,_e){\n ${checkStr}\n }\n `
5227
- factoryBody = guardStrE + helpersE + runner + `return function(d,_all){const _e=[];\n ` +
5228
- emitGuardedRun(attempt, '_e.length=0;', defSetsE) + `return{valid:_e.length===0,errors:_e}}`
5229
- } else {
5230
- factoryBody = helpersE + 'return function(d,_all){const _e=[];\n ' + checkStr +
5231
- '\n return{valid:_e.length===0,errors:_e}}'
5232
- }
5233
- // For the runtime, the literals take the legacy shape directly. `code` and
5234
- // `docUrl` are there for standalone modules, which return errors as they
5235
- // are; the runtime derives both from the keyword when it enriches, and
5236
- // orders errors by schemaPath, so the literals carried three fields that
5237
- // every read then copied the error to remove. The patterns are this
5238
- // generator's own spelling; the collapse helper writes its fields with a
5239
- // space and keeps them.
5240
- if (sourceOpts && sourceOpts.runtimeShape) {
5241
- const shape = (src) => src.replace(/code:'ATA\d{4}',/g, '').replace(/,docUrl:'https:\/\/ata-validator\.com\/e\/ATA\d{4}'/g, '').replace(/,_o:\d+/g, '')
5242
- body = shape(body)
5243
- factoryBody = shape(factoryBody)
5244
- }
5245
- const hoistable = !ctx.usesRecursion && defSetsE.length === 0 && helpersE !== ''
5246
- if (hoistable || needsGuardE) {
5247
- const params = [...cvars, '__ataSafeRe']
5248
- const args = [...cvals, compileSafe]
5249
- const make = new Function(...params, factoryBody)
5250
- const main = make(...args)
5251
- if (needsGuardE) {
5252
- let busy = false
5253
- fn = (d, _all) => {
5254
- if (busy) return make(...args)(d, _all)
5255
- busy = true
5256
- try { return main(d, _all) } finally { busy = false }
5257
- }
5258
- } else {
5259
- fn = main
5260
- }
5261
- } else if (ctx.usesSafeRe) {
5262
- // Inlined helperCode references __ataSafeRe; bind the safe-regex factory.
5263
- // Standalone keeps the inline _errSource — the embed defines __ataSafeRe.
5264
- const built = new Function(...cvars, '__ataSafeRe', 'd', '_all', body)
5265
- fn = cvars.length
5266
- ? (d, _all) => built(...cvals, compileSafe, d, _all)
5267
- : (d, _all) => built(compileSafe, d, _all)
5268
- } else if (cvars.length) {
5269
- const built = new Function(...cvars, 'd', '_all', body)
5270
- fn = (d, _all) => built(...cvals, d, _all)
5271
- } else {
5272
- fn = new Function('d', '_all', body)
5273
- }
5274
- fn._errSource = body
5275
- fn._errFactory = factoryBody
5276
- fn._errGuarded = needsGuardE
5277
- fn._sharedHelpers = ctx.shared ? ctx.shared.slice() : []
5278
- fn._usesSafeRe = !!ctx.usesSafeRe
5279
- if (cvars.length) {
5280
- fn._formatClosures = cvars.map((name, i) => {
5281
- let format = null
5282
- for (const key of Object.keys(ctx.userFormats)) {
5283
- if (ctx.userFormats[key] === cvals[i]) { format = key; break }
5284
- }
5285
- return { name, fn: cvals[i], format }
5286
- })
5287
- }
5288
- return fn
5289
- } catch (e) {
5290
- // Declining here is safe (the interpreter takes over) but silent, which
5291
- // is how every past regression in this file hid. The env hook makes the
5292
- // decline visible without changing behavior.
5293
- if (process.env.ATA_DEBUG_ERRGEN) console.error('error codegen declined:', e.message)
5294
- return null
5060
+ if (node.dependentSchemas && typeof node.dependentSchemas === 'object') {
5061
+ for (const [k, ds] of Object.entries(node.dependentSchemas)) out.push(`if(Object.prototype.hasOwnProperty.call(${v},${JSON.stringify(k)}))${emitAnnot(ds, ctx)}(${v},${S})`)
5295
5062
  }
5063
+ if (typeof node.$ref === 'string') out.push(`${emitAnnot(refTargetFor(node.$ref, ctx), ctx)}(${v},${S})`)
5296
5064
  }
5297
5065
 
5298
- // Emit codegen for oneOf/anyOf that runs each branch with error collection,
5299
- // aggregates results, and uses __ataCollapse to surface a single best-branch
5300
- // error. Wraps the existing per-branch evaluation; does not replace it.
5301
- const BRANCH_VERDICT_MAX = 4096
5302
-
5303
- // What undoing a trial emission needs: during one, the context's arrays only
5304
- // grow and its maps and sets only gain keys (a reference pushed onto refStack
5305
- // by a declined branch is such a key), so their lengths and sizes are enough,
5306
- // and what was added is cut off the end, in insertion order. Copying them
5307
- // instead made the trial quadratic: 297 oneOf and anyOf on apollo-router,
5308
- // each copying a helper list thousands long.
5309
- function snapshotCtx (ctx) {
5310
- const out = new Map()
5311
- for (const k of Object.keys(ctx)) {
5312
- const x = ctx[k]
5313
- if (Array.isArray(x)) out.set(k, { kind: 'a', ref: x, n: x.length })
5314
- else if (x instanceof Map || x instanceof Set) out.set(k, { kind: 'c', ref: x, n: x.size })
5315
- else out.set(k, { kind: 'v', ref: x })
5316
- }
5317
- return out
5318
- }
5319
- function restoreCtx (ctx, saved) {
5320
- for (const k of Object.keys(ctx)) if (!saved.has(k)) delete ctx[k]
5321
- for (const [k, e] of saved) {
5322
- if (e.kind === 'a') { e.ref.length = Math.min(e.ref.length, e.n); ctx[k] = e.ref }
5323
- else if (e.kind === 'c') {
5324
- const extra = e.ref.size - e.n
5325
- if (extra > 0) {
5326
- const added = []
5327
- let i = 0
5328
- for (const key of e.ref.keys()) if (i++ >= e.n) added.push(key)
5329
- for (const key of added) e.ref.delete(key)
5330
- }
5331
- ctx[k] = e.ref
5332
- } else ctx[k] = e.ref
5333
- }
5066
+ function refTargetFor(ref, ctx) {
5067
+ const m = ref.match(/^#\/(?:\$defs|definitions)\/(.+)$/)
5068
+ const t = m && ctx.rootDefs ? ctx.rootDefs[m[1]] : undefined
5069
+ if (t === undefined) throw DECLINE
5070
+ return t
5334
5071
  }
5335
5072
 
5336
- // Whether a node holds a reference anywhere below it.
5337
- function holdsRef (node, seen = new Set()) {
5338
- if (node === null || typeof node !== 'object' || seen.has(node)) return false
5339
- seen.add(node)
5340
- if (typeof node.$ref === 'string' || typeof node.$dynamicRef === 'string') return true
5341
- for (const k of Object.keys(node)) if (holdsRef(node[k], seen)) return true
5342
- return false
5343
- }
5073
+ // The keywords an annotation function evaluates itself, through its children;
5074
+ // the rest of a subschema is checked by a plain verdict.
5075
+ const ANNOT_APPLICATORS = new Set(['allOf', 'anyOf', 'oneOf', 'if', 'then', 'else', 'dependentSchemas', '$ref', 'not', 'unevaluatedProperties'])
5344
5076
 
5345
- // Every `$ref` under `node`, outside the values of enum, const, default and
5346
- // examples. Null when one is not a plain pointer into the root's definitions
5347
- // (after prepareForCodegen those are what remain, besides `#` itself) or
5348
- // when the node carries a scope keyword of its own, which would not mean the
5349
- // same thing under a wrapper.
5350
- const BRANCH_SCOPE_KEYS = new Set(['$id', '$anchor', '$dynamicAnchor', '$dynamicRef', '$defs', 'definitions', '$schema'])
5351
- function branchRefsLocal (node, defsKey, seen) {
5352
- if (node === null || typeof node !== 'object') return true
5353
- if (seen.has(node)) return true
5354
- seen.add(node)
5355
- if (Array.isArray(node)) { for (const x of node) if (!branchRefsLocal(x, defsKey, seen)) return false; return true }
5356
- for (const k of Object.keys(node)) {
5357
- if (SKIP_VALUE_KEYS.has(k)) continue
5358
- if (BRANCH_SCOPE_KEYS.has(k)) return false
5359
- const v = node[k]
5360
- if (k === '$ref') { if (typeof v !== 'string' || !v.startsWith('#/' + defsKey + '/')) return false; continue }
5361
- if (!branchRefsLocal(v, defsKey, seen)) return false
5362
- }
5363
- return true
5364
- }
5365
- // One verdict function per branch, compiled on its own with the root's
5366
- // definitions under it, or null when a branch cannot be wrapped that way
5367
- // (then the site keeps its error functions for the decision). The
5368
- // definitions themselves are checked once per root.
5369
- const _defsLocal = new WeakMap()
5370
- function compileBranchVerdicts (branches, ctx) {
5371
- const root = ctx.rootC
5372
- if (root === null || typeof root !== 'object') return null
5373
- const defsKey = root.$defs && typeof root.$defs === 'object' ? '$defs' : (root.definitions && typeof root.definitions === 'object' ? 'definitions' : null)
5374
- if (defsKey !== null) {
5375
- let ok = _defsLocal.get(root)
5376
- if (ok === undefined) { ok = branchRefsLocal(root[defsKey], defsKey, new Set()); _defsLocal.set(root, ok) }
5377
- if (!ok) return null
5077
+ // A function `(value, S) => valid` for one subschema: whether it passes, and
5078
+ // when it does, what it evaluated pushed onto S; when it does not, S is left
5079
+ // as it was. Validity is worked out bottom up: the subschema's own keywords
5080
+ // by a verdict of the subschema without its applicators, each applicator from
5081
+ // its branches' annotation functions. Asking each branch for a full verdict
5082
+ // and then again for its annotations repeated every nested branch once per
5083
+ // level, which made nested references the slowest shape there was.
5084
+ function emitAnnot(sub, ctx) {
5085
+ if (sub === true) return '_anT'
5086
+ if (sub === false) return '_anF'
5087
+ if (sub === null || typeof sub !== 'object') throw DECLINE
5088
+ if (!ctx.annFns) {
5089
+ ctx.annFns = new Map()
5090
+ if (ctx.sharedRuntime) bindRt(ctx, ['_anT', '_anF', '_rb'])
5091
+ else (ctx.preamble || ctx.helperCode).push('function _anT(){return true}function _anF(){return false}function _rb(S,m){if(S.length!==m)S.length=m}')
5378
5092
  }
5379
- const out = []
5380
- for (const sub of branches) {
5381
- if (sub === true) { out.push(() => true); continue }
5382
- if (sub === false) { out.push(() => false); continue }
5383
- if (sub === null || typeof sub !== 'object' || Array.isArray(sub)) return null
5384
- if (!branchRefsLocal(sub, defsKey === null ? '$defs' : defsKey, new Set())) return null
5385
- const wrapped = Object.assign({}, sub)
5386
- if (defsKey !== null) wrapped[defsKey] = root[defsKey]
5387
- if (typeof ctx.rootSchema === 'object' && ctx.rootSchema !== null && typeof ctx.rootSchema.$schema === 'string') wrapped.$schema = ctx.rootSchema.$schema
5388
- let fn = null
5389
- try { fn = compileToJSCodegen(wrapped, ctx.schemaMap, ctx.userFormats) } catch (e) { if (e !== DECLINE) throw e }
5390
- if (typeof fn !== 'function') return null
5391
- out.push(fn)
5392
- }
5393
- return out
5394
- }
5395
-
5396
- // A verdict for one subtree of the combined function, as a function in the
5397
- // preamble, so the collecting code of that subtree runs only for a value the
5398
- // verdict rejects. The collecting code is slower than the verdict on an
5399
- // accepted value (it keeps paths, builds nothing it can avoid but tests more
5400
- // than the verdict's cheapest-first order), so without this the whole
5401
- // function was put behind one verdict of the whole document (`_VF`), and a
5402
- // rejected document was validated twice in full. With a guard at every
5403
- // array element and every object-valued property, an accepted value costs
5404
- // its verdict and a rejected one costs its verdict plus the collection of
5405
- // the subtree that failed. Only in the combined function, the runtime path;
5406
- // null for a subtree whose verdict declines, is a leaf, or is too large.
5407
- const GUARD_KEYS = ['oneOf', 'anyOf', 'allOf', '$ref', 'items', 'prefixItems', 'properties', 'patternProperties', 'additionalProperties', 'not', 'if', 'contains', 'dependentSchemas', 'dependencies', 'propertyNames', 'unevaluatedProperties', 'unevaluatedItems']
5408
- const GUARD_MAX = 16 * 1024
5409
- const GUARD_BUDGET = 512 * 1024
5410
- // Whether a schema node is worth a guard of its own: it applies something
5411
- // beyond leaf checks. A node whose children are themselves worth guarding is
5412
- // not guarded; the guards go on the children. Guarding both ran the parent's
5413
- // verdict in full and then, when it failed, every child's verdict again: on a
5414
- // list of 1000 users with one bad one that was two verdicts of the list
5415
- // before the one user was collected.
5416
- const weighty = (n) => n !== null && typeof n === 'object' && !Array.isArray(n) && GUARD_KEYS.some((k) => n[k] !== undefined)
5417
- function childrenWeighty (sub) {
5418
- if (weighty(sub.items)) return true
5419
- if (Array.isArray(sub.prefixItems) && sub.prefixItems.some(weighty)) return true
5420
- if (weighty(sub.additionalProperties)) return true
5421
- if (sub.properties && typeof sub.properties === 'object') for (const k of Object.keys(sub.properties)) if (weighty(sub.properties[k])) return true
5422
- if (sub.patternProperties && typeof sub.patternProperties === 'object') for (const k of Object.keys(sub.patternProperties)) if (weighty(sub.patternProperties[k])) return true
5423
- return false
5424
- }
5425
- // Below this much verdict source the guard is written into the function
5426
- // itself as a labelled block (`gN:{...break gN...}`), not as a function it
5427
- // calls: in the first thousands of calls of a process, before the optimizing
5428
- // compiler has inlined anything, each call is paid in full, and a schema
5429
- // with six small guards paid six. Above it the guard stays a function, so
5430
- // one large subtree does not swell the function around it.
5431
- const GUARD_INLINE_MAX = 2048
5432
- function subtreeGuard (sub, ctx, gv) {
5433
- if (!ctx.inCombined || !weighty(sub) || childrenWeighty(sub)) return null
5434
- if ((ctx.guardChars || 0) >= GUARD_BUDGET) return null
5435
- // The verdict generator registers the definition functions it writes under
5436
- // `defFns`, and so does the error generator for the branch functions of
5437
- // this same program: shared, the verdict found the error generator's
5438
- // `_defE` function under the definition's name and called it with one
5439
- // argument. The guards keep a registry of their own.
5440
- if (!ctx.guardDefFns) ctx.guardDefFns = new Map()
5441
- const saved = snapshotCtx(ctx)
5442
- const outerDefFns = ctx.defFns
5443
- const outerShare = ctx.shareDefs
5444
- ctx.defFns = ctx.guardDefFns
5445
- // Definitions referenced from several places are written once, as
5446
- // functions in the preamble (the verdict generator's shared mode), not
5447
- // expanded into every guard that reaches them: expanded, the guards of a
5448
- // 25-definition API schema came to 55 KB of source in a 133 KB function,
5449
- // and a function that size reaches the optimizing compiler late.
5450
- ctx.shareDefs = true
5451
- try {
5452
- const bl = []
5453
- try { nestedGenCode(sub, gv, bl, ctx) } finally { ctx.defFns = outerDefFns; ctx.shareDefs = outerShare }
5454
- if (bl.length === 0) { restoreCtx(ctx, saved); return null }
5455
- const body = bl.join(';')
5456
- // The verdict generator's recursion names (`_validate`, the cycle guard
5457
- // state) are not this function's; a subtree that reaches them stays
5458
- // unguarded.
5459
- const added = ctx.preamble.slice(saved.get('preamble').n).join(';')
5460
- if (body.length > GUARD_MAX || body.includes(SP_PLACEHOLDER) || /\b(?:_validate|_stk|_sd|_sg|_CYC)\b/.test(body + added)) { restoreCtx(ctx, saved); return null }
5461
- ctx.guardChars = (ctx.guardChars || 0) + body.length
5462
- if (body.length <= GUARD_INLINE_MAX) return { body }
5463
- const name = `_vg${ctx.varCounter++}`
5464
- ctx.helperCode.push(`const ${name}=function(${gv}){${body};return true}`)
5465
- return { name }
5466
- } catch (e) {
5467
- if (e !== DECLINE) throw e
5468
- restoreCtx(ctx, saved)
5469
- return null
5470
- }
5471
- }
5472
- // The collecting code for `sub` at `v`, behind its guard when there is one.
5473
- function genGuardedC (sub, v, pathExpr, lines, ctx, schemaPrefix) {
5474
- const gi = ctx.varCounter++
5475
- const gv = `_gv${gi}`
5476
- const g = subtreeGuard(sub, ctx, gv)
5477
- if (g === null) { genCodeC(sub, v, pathExpr, lines, ctx, schemaPrefix); return }
5478
- if (g.name !== undefined) lines.push(`if(!${g.name}(${v})){`)
5479
- else {
5480
- // The verdict's exits become jumps out of the block: a failure leaves
5481
- // the flag set, success clears it. replaceTopLevel keeps nested
5482
- // closures' returns as they are (see the hybrid rewrite).
5483
- lines.push(`{const ${gv}=${v};let _g${gi}=1;g${gi}:{${replaceTopLevel(g.body, `{_g${gi}=0;break g${gi}}`, `break g${gi}`)};_g${gi}=0}if(_g${gi}){`)
5484
- }
5485
- ctx.guardDepth = (ctx.guardDepth || 0) + 1
5486
- try { genCodeC(sub, v, pathExpr, lines, ctx, schemaPrefix) } finally { ctx.guardDepth-- }
5487
- lines.push(g.name !== undefined ? '}' : '}}')
5488
- }
5489
-
5490
- // A branch's error-collecting body rewritten to count what it would have
5491
- // collected: each `_e.push({keyword:'k',...})` becomes a count and a severity
5492
- // (lib/branch-collapse.js, __ataScore), and the body returns the score the
5493
- // collapse would compute from the errors. When no branch of a oneOf or anyOf
5494
- // holds, the collapse reports the closest branch's errors; it used to run
5495
- // every branch's error function to score them, building every branch's
5496
- // errors to keep one branch's. Now the counting bodies score, and only the
5497
- // closest branch's error function runs. Null for a body the rewrite cannot
5498
- // follow (a shared definition call, a nested collapse, a push that is not a
5499
- // literal); that site keeps the old path.
5500
- function countingBody (body) {
5501
- // A nested oneOf or anyOf that does not hold is one collapsed error in the
5502
- // branch's list (keyword oneOf or anyOf, severity 4): its marked section,
5503
- // which would run the inner branches to build it, becomes that count.
5504
- // Two kinds of section: CB is one error whatever the branches would say;
5505
- // CU is a section the copy cannot score (the value is not the one the
5506
- // counts were taken for), so the copy answers -1 and the picker runs the
5507
- // branches as before.
5508
- while (true) {
5509
- const b1 = body.indexOf('/*CB*/'), b2 = body.indexOf('/*CU*/')
5510
- const b = b1 < 0 ? b2 : b2 < 0 ? b1 : Math.min(b1, b2)
5511
- if (b < 0) break
5512
- const unknown = b === b2
5513
- let depth = 1
5514
- let j = b + 6
5515
- while (j < body.length && depth > 0) {
5516
- if (body.startsWith('/*CB*/', j) || body.startsWith('/*CU*/', j)) { depth++; j += 6; continue }
5517
- if (body.startsWith('/*CE*/', j)) { depth--; j += 6; continue }
5518
- j++
5519
- }
5520
- if (depth !== 0) return null
5521
- body = body.slice(0, b) + (unknown ? 'return -1' : '(_n++,_s+=4)') + body.slice(j)
5522
- }
5523
- if (/\b(?:_defE\d|__ataCollapse|__ataMulti|_brf\d|_bsf\d)/.test(body)) return null
5524
- const SEV = require('./branch-collapse').SEVERITY
5525
- let out = ''
5526
- let i = 0
5527
- const head = 'const _e=[];'
5528
- if (!body.startsWith(head)) return null
5529
- out += 'let _n=0,_s=0;'
5530
- i = head.length
5531
- while (i < body.length) {
5532
- const ch = body[i]
5533
- if (ch === '"' || ch === "'" || ch === '`') { const j = skipString(body, i); out += body.slice(i, j); i = j; continue }
5534
- if (body.startsWith('_e.push(', i)) {
5535
- let j = i + 8
5536
- let depth = 1
5537
- while (j < body.length && depth > 0) {
5538
- const c = body[j]
5539
- if (c === '"' || c === "'" || c === '`') { j = skipString(body, j); continue }
5540
- if (c === '(' || c === '{' || c === '[') depth++
5541
- else if (c === ')' || c === '}' || c === ']') depth--
5542
- j++
5543
- }
5544
- if (depth !== 0) return null
5545
- const arg = body.slice(i + 8, j - 1)
5546
- // The literal may still carry its `code` (the shape pass strips it).
5547
- const m = /^\{(?:code:'ATA\d{4}',)?keyword:'([A-Za-z$][\w$]*)',/.exec(arg)
5548
- if (m === null) return null
5549
- out += `(_n++,_s+=${SEV[m[1]] || 4})`
5550
- i = j
5551
- continue
5552
- }
5553
- if (body.startsWith('return{valid:false,errors:_e}', i)) { out += 'return _n*100+_s'; i += 29; continue }
5554
- if (body.startsWith('return{valid:_e.length===0,errors:_e}', i)) { out += 'return _n*100+_s'; i += 37; continue }
5555
- if (body.startsWith('_e.length', i)) { out += '_n'; i += 9; continue }
5556
- out += ch
5557
- i++
5558
- }
5559
- if (/\b_e\b/.test(out.replace(/'[^']*'|"[^"]*"/g, ''))) return null
5560
- return out
5561
- }
5562
-
5563
- function emitBranchCollapse (branches, keyword, v, pathExpr, lines, ctx, schemaPrefix) {
5564
- ctx.usesBranchCollapse = true
5565
- const fi = ctx.varCounter++
5566
- const branchKw = keyword
5567
- const branchSp = schemaPrefix + '/' + branchKw
5568
- // Per-branch error-collecting functions, each returning {valid,errors}.
5569
- // They take the value, its instance path, `_all` and, inside a definition
5570
- // written once for all its references, that reference's schema path, so
5571
- // they can be built once per call of the error function rather than each
5572
- // time the keyword is reached: on a list of 1723 tokens that is thousands
5573
- // of closure arrays per validation.
5574
- const fns = []
5575
- const titles = []
5576
- let usesSp = false
5577
- for (let i = 0; i < branches.length; i++) {
5578
- const sub = branches[i]
5579
- const subLines = []
5580
- const subSp = branchSp + '/' + i
5581
- if (typeof sub === 'object' && sub !== null) {
5582
- // A branch is a function of its own with a local `_e`; a collapse
5583
- // inside it reports there, not into the combined function's lists.
5584
- ctx.inBranchFn = (ctx.inBranchFn || 0) + 1
5585
- try {
5586
- genCodeE(sub, '_bv', '_bpth', subLines, ctx, subSp)
5587
- } finally {
5588
- ctx.inBranchFn--
5589
- }
5590
- } else if (sub === false) {
5591
- // A false branch never matches; it reports the same error the
5592
- // interpreter attaches to a false schema.
5593
- subLines.push(`_e.push({keyword:'not',instancePath:_bpth,schemaPath:'${subSp}'${ordinalField(ctx, `${subSp}`)},params:{},message:'boolean schema is false'})`)
5594
- }
5595
- const title = (sub && typeof sub === 'object' && typeof sub.title === 'string') ? sub.title : ''
5596
- titles.push(title)
5597
- let body = subLines.length === 0 ? '' : `const _e=[];${subLines.join(';')};return{valid:_e.length===0,errors:_e}`
5598
- if (body.includes(SP_PLACEHOLDER)) {
5599
- body = body.split(`'${SP_PLACEHOLDER}`).join(`_sp+'`).split(`"${SP_PLACEHOLDER}`).join(`_sp+"`)
5600
- if (body.includes(SP_PLACEHOLDER)) throw DECLINE
5601
- usesSp = true
5602
- }
5603
- fns.push(body)
5604
- }
5605
- const params = usesSp ? '_bv,_bpth,_all,_sp' : '_bv,_bpth,_all'
5606
- const fnArr = `_brf${fi}`
5607
- const resArr = `_brr${fi}`
5608
- const titleArr = JSON.stringify(titles)
5609
- const collapsed = `_brc${fi}`
5610
- const pp = pathExpr || '""'
5611
- ctx.helperCode.push(`const ${fnArr}=[${fns.map((b) => b === '' ? `function(){return{valid:true,errors:[]}}` : `function(${params}){${b}}`).join(',')}];const _bt${fi}=${titleArr}`)
5612
- const call = `${fnArr}[_bi](${v},_pp${fi},_all${usesSp ? ',_sp' : ''})`
5613
- // branchSp is source text for a quoted literal, with backslashes and quotes
5614
- // escaped; the ordinal is looked up by the path itself. Looked up by the
5615
- // text, a path through a pattern such as `^\\d+$` found no node, or the
5616
- // wrong one, and the collapsed error sorted out of place against the other
5617
- // engines.
5618
- // In the runtime shape there is no ordering key (the reader looks the path
5619
- // up), so the collapsed error is in its final shape as it is built.
5620
- const ord = ctx.noOrdinal ? 'null' : (() => { const o = ordinalFor(ctx.rootSchema, unescapeSp(branchSp)); return o === null ? 'null' : o })()
5621
- // In the combined function `_e` is created on the first error and every
5622
- // error is collected (`_all` is true there), so the report only pushes.
5623
- const report = ctx.inCombined && !ctx.inBranchFn
5624
- ? `const ${collapsed}=__ataCollapse('${branchKw}',${resArr},_pp${fi},'${branchSp}',${ord},_bt${fi});if(${collapsed})(_e=_ap(_A,_e,${collapsed},${apOrd(ctx, branchSp)}))`
5625
- : `const ${collapsed}=__ataCollapse('${branchKw}',${resArr},_pp${fi},'${branchSp}',${ord},_bt${fi});if(${collapsed}){_e.push(${collapsed});if(!_all)return{valid:false,errors:_e}}`
5626
- // Score first, collect once (countingBody), in the combined function where
5627
- // the collapsed error is built in its final shape (no ordering key).
5628
- let noneReport = null
5629
- if (ctx.inCombined && (ctx.noOrdinal ? 'null' : '') === 'null') {
5630
- const cbs = fns.map((b) => b === '' ? 'return 0' : countingBody(b))
5631
- if (!cbs.some((c) => c === null)) {
5632
- const sfx = usesSp ? ',_sp' : ''
5633
- const code = branchKw === 'oneOf' ? 'ATA4001' : 'ATA4003'
5634
- // `code: ` with the space: the runtime shape strips this generator's own
5635
- // `code:'ATA....',` from the literals and keeps the collapse's, which
5636
- // the reader expects to carry it (see the shape pass below).
5637
- const lit = `{code: '${code}',keyword:'${branchKw}',instancePath:_pp${fi}||'',path:_pp${fi}||'',schemaPath:'${branchSp}',message:'value matched 0 of ${branches.length} ${branchKw} variants',params:{variants:${branches.length},closest:_bi${fi},closestName:_bt${fi}[_bi${fi}]||('variant '+(_bi${fi}+1))},branchErrors:_br${fi}.errors}`
5638
- // In a branch function the error joins that function's list (the
5639
- // combined function's `_ap` and ordering state are not its).
5640
- const push = ctx.inBranchFn ? `_e.push(${lit});if(!_all)return{valid:false,errors:_e}` : `(_e=_ap(_A,_e,${lit},${apOrd(ctx, branchSp)}))`
5641
- if (cbs.length === 1) {
5642
- // One branch is the closest one: nothing to score, no counting copy.
5643
- // Scoring it ran the branch a third time (verdict, count, errors).
5644
- noneReport = `{let _bi${fi}=0;const _br${fi}=${fnArr}[0](${v},_pp${fi},_all${sfx});${push}}`
5645
- } else {
5646
- ctx.helperCode.push(cbs.map((b, i) => `const _bsf${fi}_${i}=function(${params}){${b}}`).join(';'))
5647
- const picks = cbs.map((_, i) => `{const _s=_bsf${fi}_${i}(${v},_pp${fi},_all${sfx});if(_s<0)_ng${fi}=1;else if(_s<_bs${fi}){_bs${fi}=_s;_bi${fi}=${i}}}`).join('')
5648
- // A copy that could not score (-1): every branch runs, as before.
5649
- const noneOld = branchKw === 'anyOf'
5650
- ? `const ${resArr}=[];for(let _bi=0;_bi<${branches.length};_bi++){const _br=${call};${resArr}.push(_br.valid?{valid:false,errors:_br.errors}:_br)}${report}`
5651
- : `const ${resArr}=[];for(let _bi=0;_bi<${branches.length};_bi++)${resArr}.push(${call});${report}`
5652
- noneReport = `{let _bi${fi}=0,_bs${fi}=Infinity,_ng${fi}=0;${picks}if(_ng${fi}){${noneOld}}else{const _br${fi}=${fnArr}[_bi${fi}](${v},_pp${fi},_all${sfx});${push}}}`
5653
- }
5654
- }
5655
- }
5656
- // The node's runtime unevaluatedProperties already counted how many of
5657
- // these branches pass, through the annotation functions, which answer the
5658
- // same validity. A oneOf with exactly one, an anyOf with any, holds and
5659
- // reports nothing, so its branches are not run a second time to say so.
5660
- // Only in the combined function proper, where that count is in scope.
5661
- // Inside a branch function only a factory-scope count (factoryCount) is
5662
- // in reach; the node's own counts are locals of the combined function.
5663
- const countedRaw = ctx.inCombined && ctx.branchCounts ? ctx.branchCounts.get(branches) : undefined
5664
- const counted = countedRaw !== undefined && (!ctx.inBranchFn || countedRaw.factory) ? countedRaw : undefined
5665
- const same = counted !== undefined ? `${v}!==null&&typeof ${v}==='object'&&${counted.of}===${v}` : ''
5666
- let guard = counted !== undefined
5667
- ? (keyword === 'anyOf' ? `if(!(${same}&&${counted.count}>=1))` : `if(!(${same}&&${counted.count}===1))`)
5668
- : ''
5669
- // More than one oneOf branch passed: the error names them and carries no
5670
- // branch errors, so it is built from the bits the count left.
5671
- if (guard !== '' && keyword === 'oneOf' && counted.mask && ctx.inCombined) {
5672
- const ord0 = ctx.noOrdinal ? 'null' : (() => { const o = ordinalFor(ctx.rootSchema, unescapeSp(branchSp)); return o === null ? 'null' : o })()
5673
- const multiC = `__ataMulti(_pi${fi},${branches.length},${pp},'${branchSp}',${ord0})`
5674
- const pushC = ctx.inBranchFn ? `_e.push(${multiC});if(!_all)return{valid:false,errors:_e}` : `(_e=_ap(_A,_e,${multiC},${apOrd(ctx, branchSp)}))`
5675
- // The marked section is one error in a counting copy (countingBody).
5676
- guard = `if(${same}&&${counted.count}>1){/*CB*/const _pi${fi}=[];for(let _k=0;_k<${branches.length};_k++)if((${counted.mask}>>_k)&1)_pi${fi}.push(_k);${pushC}/*CE*/}else if(!(${same}&&${counted.count}===1))`
5677
- }
5678
- // Without such a count, each branch also gets a verdict function, and the
5679
- // passing branches are counted with those: a oneOf with exactly one and an
5680
- // anyOf with any hold, and a oneOf with more than one is reported from the
5681
- // bits, so the error-collecting branches run only when no branch passes
5682
- // and the collapse needs their errors to pick the closest. Running them to
5683
- // learn the count built a result object and an error list per branch,
5684
- // which made a oneOf matched twice cost 69 ns where counting costs about
5685
- // 12. A branch the verdict generator declines keeps the old path.
5686
- let vfArr = null
5687
- // Branches that hold a reference are left to the error branches: their
5688
- // verdicts would expand every definition again, as a second copy, where the
5689
- // error branches call the shared one.
5690
- if (guard === '' && !branches.some((b) => holdsRef(b))) {
5691
- // A trial: the generators are not exception-safe on the context (a
5692
- // reference left on refStack by a declined branch made the next error
5693
- // function write that branch as nothing, which read as valid), so the
5694
- // context is put back as it was when the verdicts decline.
5695
- const saved = snapshotCtx(ctx)
5696
- try {
5697
- const vfs = branches.map((sub) => {
5698
- if (sub === true) return 'function(){return true}'
5699
- if (sub === false) return 'function(){return false}'
5700
- const bl = []
5701
- nestedGenCode(sub, '_bv', bl, ctx)
5702
- return bl.length === 0 ? 'function(){return true}' : `function(_bv){${bl.join(';')};return true}`
5703
- })
5704
- // Only while the verdicts are small: they are a second copy of the
5705
- // branches, with every reference expanded, and on large branches that
5706
- // copy cost more to compile than counting saves (apollo-router's error
5707
- // function grew from 1.5 to 2.5 MB of source).
5708
- if (!vfs.some((b) => b.includes(SP_PLACEHOLDER)) && vfs.reduce((n, b) => n + b.length, 0) <= BRANCH_VERDICT_MAX) {
5709
- // One constant per branch, called directly: through an array the one
5710
- // call site sees every branch and none of them is inlined.
5711
- vfArr = `_bvf${fi}`
5712
- ctx.helperCode.push(vfs.map((b, i) => `const ${vfArr}_${i}=${b}`).join(';'))
5713
- } else {
5714
- restoreCtx(ctx, saved)
5715
- }
5716
- } catch (e) {
5717
- if (e !== DECLINE) throw e
5718
- restoreCtx(ctx, saved)
5719
- }
5720
- }
5721
- // In the combined function (the runtime path; a standalone module cannot
5722
- // carry a compiled function) a branch that holds a reference, or a site
5723
- // whose inline verdicts were too large, gets a verdict compiled on its own
5724
- // and handed in as a closure value: the branch with the root's definitions
5725
- // under it, through compileToJSCodegen, with its own shared definitions and
5726
- // its own regex engines. Without one, every branch's error function ran on
5727
- // every value the keyword saw, accepted or not: json-patch's six-way oneOf
5728
- // cost 1363 ns on a valid list of patches where its verdict costs 261.
5729
- if (vfArr === null && guard === '' && ctx.inCombined) {
5730
- const compiled = compileBranchVerdicts(branches, ctx)
5731
- if (compiled !== null) {
5732
- vfArr = `_bvf${fi}`
5733
- for (let i = 0; i < compiled.length; i++) { ctx.closureVars.push(`${vfArr}_${i}`); ctx.closureVals.push(compiled[i]) }
5734
- }
5735
- }
5736
- if (vfArr !== null) {
5737
- const L = branches.length
5738
- const top = ctx.inCombined && !ctx.inBranchFn
5739
- // An accepted document is still answered by the verdict first (see the
5740
- // result shape in compileToJSCombined): that is the fastest path to it,
5741
- // and dropping it measured 20 ns slower per accepted oneOf.
5742
- if (top && !ctx.guardDepth) ctx.unguardedCollapse = true
5743
- const tests = branches.map((_, i) => `${vfArr}_${i}(${v})`)
5744
- if (keyword === 'anyOf') {
5745
- const none = noneReport !== null ? noneReport : `const ${resArr}=[];for(let _bi=0;_bi<${L};_bi++){const _br=${call};${resArr}.push(_br.valid?{valid:false,errors:_br.errors}:_br)}${report}`
5746
- // In a branch function the section a counting copy replaces is marked
5747
- // (countingBody): when the keyword does not hold, the error function
5748
- // reports one collapsed error, whatever the branches would say.
5749
- lines.push(`{const _pp${fi}=${pp};if(!(${tests.join('||')})){${top ? '' : '/*CB*/'}if(_all===0){_e.push(null);return{valid:false,errors:_e}}${none}${top ? '' : '/*CE*/'}}}`)
5750
- } else {
5751
- const mk = L <= 30
5752
- const collect = mk
5753
- ? `const _pi${fi}=[];for(let _k=0;_k<${L};_k++)if((_m${fi}>>_k)&1)_pi${fi}.push(_k);`
5754
- : ''
5755
- const multi = `__ataMulti(_pi${fi},${L},_pp${fi},'${branchSp}',${ord})`
5756
- const pushMulti = top ? `(_e=_ap(_A,_e,${multi},${apOrd(ctx, branchSp)}))` : `_e.push(${multi});if(!_all)return{valid:false,errors:_e}`
5757
- const count = tests.map((t, i) => `if(${t}){_n${fi}++;${mk ? `_m${fi}|=${1 << i}` : `_pi${fi}.push(${i})`}}`).join('')
5758
- const none = noneReport !== null ? noneReport : `const ${resArr}=[];for(let _bi=0;_bi<${L};_bi++)${resArr}.push(${call});${report}`
5759
- lines.push(`{const _pp${fi}=${pp};let _n${fi}=0${mk ? `,_m${fi}=0` : `,_pi${fi}=[]`};${count}if(_n${fi}!==1){${top ? '' : '/*CB*/'}if(_all===0){_e.push(null);return{valid:false,errors:_e}}if(_n${fi}>1){${collect}${pushMulti}}else{${none}}${top ? '' : '/*CE*/'}}}`)
5760
- }
5761
- return
5762
- }
5763
- if (guard === '' && ctx.inCombined && !ctx.inBranchFn && !ctx.guardDepth) ctx.unguardedCollapse = true
5764
- if (keyword === 'anyOf') {
5765
- // anyOf reports nothing once a branch passes, so the branches are first
5766
- // only asked whether they pass. They are called with `_all` set to 0, a
5767
- // third value next to true (collect everything) and false (stop at the
5768
- // first error, for abortEarly): every check reads it as `!_all` and stops
5769
- // at its first error, and an anyOf reached that way that fails returns
5770
- // at once instead of collecting its branches and collapsing them, since
5771
- // the caller only wants to know. An object tried against a branch that is
5772
- // an anyOf of primitive types used to collapse that anyOf in full and then
5773
- // pass the next branch, which on a list of 1723 tokens was 2353 collapses
5774
- // no one read. Only when no branch passes do they run again with the
5775
- // caller's `_all`, and the collapse sees them all, as before.
5776
- const probe = `${fnArr}[_bi](${v},_pp${fi},0${usesSp ? ',_sp' : ''})`
5777
- const none = noneReport !== null ? noneReport : `const ${resArr}=[];for(let _bi=0;_bi<${fnArr}.length;_bi++){const _br=${call};${resArr}.push(_br.valid?{valid:false,errors:_br.errors}:_br)}${report}`
5778
- const probeOld = `let _bp${fi}=false;for(let _bi=0;_bi<${fnArr}.length;_bi++){if(${probe}.valid){_bp${fi}=true;break}}if(!_bp${fi}){if(_all===0){_e.push(null);return{valid:false,errors:_e}}${none}}`
5779
- // With a count: on the counted object the guard already said no branch
5780
- // held (CB, one error in a counting copy); on another object the branches
5781
- // are probed as before (CU, a counting copy cannot know).
5782
- if (counted !== undefined) lines.push(`${guard}{const _pp${fi}=${pp};if(${same}){/*CB*/if(_all===0){_e.push(null);return{valid:false,errors:_e}}${none}/*CE*/}else{/*CU*/${probeOld}/*CE*/}}`)
5783
- else lines.push(`${guard}{const _pp${fi}=${pp};${probeOld}}`)
5784
- } else {
5785
- // Asked only whether it passes (`_all` 0, see anyOf above), oneOf counts
5786
- // the passing branches and answers; the branch errors of a probe are not
5787
- // errors anyone reads and are not collapsed.
5788
- // With a count from the node's unevaluatedProperties (`counted`), a count
5789
- // of zero on the counted object means no branch held, which is the case
5790
- // the closest-branch path answers without running every branch.
5791
- const oldO = `const ${resArr}=[];for(let _bi=0;_bi<${fnArr}.length;_bi++)${resArr}.push(${call});if(_all===0){let _n=0;for(const _r of ${resArr})if(_r.valid)_n++;if(_n!==1){_e.push(null);return{valid:false,errors:_e}}}else{${report}}`
5792
- const noneO = noneReport !== null ? noneReport : `const ${resArr}=[];for(let _bi=0;_bi<${fnArr}.length;_bi++)${resArr}.push(${call});${report}`
5793
- if (counted !== undefined) lines.push(`${guard}{const _pp${fi}=${pp};if(${same}){/*CB*/if(_all===0){_e.push(null);return{valid:false,errors:_e}}${noneO}/*CE*/}else{/*CU*/${oldO}/*CE*/}}`)
5794
- else lines.push(`${guard}{const _pp${fi}=${pp};${oldO}}`)
5795
- }
5796
- }
5797
-
5798
- // Error-collecting code generator.
5799
- // Instead of `return false`, pushes to `_e` array and optionally early-returns.
5800
- // `_all` parameter: if falsy, return after first error.
5801
- function genCodeE(schema, v, pathExpr, lines, ctx, schemaPrefix) {
5802
- return withPlain(schema, v, lines, ctx, () => genCodeENode(schema, v, pathExpr, lines, ctx, schemaPrefix))
5803
- }
5804
- function genCodeENode(schema, v, pathExpr, lines, ctx, schemaPrefix) {
5805
- if (!schemaPrefix) schemaPrefix = '#'
5806
- // A false schema reports one error at the value that reached it, with the
5807
- // keyword the interpreter has always used for it, `not` (lib/plan-compiler.js
5808
- // line 58; its `err()` discards the 'false schema' label). Other validators
5809
- // name this keyword `false schema`; changing it here would change output
5810
- // the interpreter path already produces, so both engines say `not`.
5811
- if (schema === false) {
5812
- lines.push(`;_e.push({keyword:'not',instancePath:${pathExpr||'""'},schemaPath:'${schemaPrefix}'${ordinalField(ctx, `${schemaPrefix}`)},params:{},message:'boolean schema is false'});if(!_all)return{valid:false,errors:_e}`)
5813
- return
5814
- }
5815
- if (schema === true) return
5816
- if (typeof schema !== 'object' || schema === null) return
5817
- if (!ctx.regExpMap) {
5818
- ctx.regExpMap = new Map();
5819
- }
5820
- // $ref — resolve local and cross-schema refs
5821
- if (schema.$ref && !schema[REF_DONE]) {
5822
- // A keyword beside $ref that validates applies too (2020-12): the
5823
- // reference is written on its own, then the node again with the
5824
- // reference marked done, on a fresh copy that keeps `$ref` for the
5825
- // annotation readers, as genCodeCNode does. This used to decline, which
5826
- // kept the 2020-12 meta-schema (every vocabulary schema is a reference
5827
- // with siblings once unrolled by scope) off both error generators and on
5828
- // the interpreter for its errors, about 2 microseconds a rejection.
5829
- if (Object.keys(schema).some((k) => !REF_NEUTRAL_SIBLINGS.has(k) && !k.startsWith('x-'))) {
5830
- genCodeENode({ $ref: schema.$ref }, v, pathExpr, lines, ctx, schemaPrefix)
5831
- const rest = { ...schema }
5832
- Object.defineProperty(rest, REF_DONE, { value: true })
5833
- genCodeENode(rest, v, pathExpr, lines, ctx, schemaPrefix)
5834
- return
5835
- }
5836
- // A self-reference is a call to a helper over the root schema that takes
5837
- // the path and the schema path as arguments, as a definition on a cycle
5838
- // does below: the root helper takes no path, so errors raised through it
5839
- // carried the root's path, and this declined, which left a standalone
5840
- // module reporting one stub error for a schema with `$ref: "#"` anywhere
5841
- // in it. This used to fall through as a permissive no-op, which accepted
5842
- // whatever the reference guarded.
5843
- if (schema.$ref === '#') {
5844
- // The body comes from the prepared root (`rootE`), whose references
5845
- // are the resolvable ones; from the schema as given, a reference by
5846
- // `$id` resolved to nothing and the helper for the suite's recursive
5847
- // tree checked no node.
5848
- if (!ctx.rootE || typeof ctx.rootE !== 'object') throw DECLINE
5849
- if (!ctx.defFns) ctx.defFns = new Map()
5850
- let fnName = ctx.defFns.get('#')
5851
- if (!fnName) {
5852
- fnName = '_defE' + ctx.defFns.size + '_root'
5853
- ctx.defFns.set('#', fnName)
5854
- // The helper is its own function, so the references expanded in it
5855
- // start from nothing: inside the expansion of a definition that led
5856
- // here, the definition's own reference would otherwise read as
5857
- // already being expanded.
5858
- const bodyLines = []
5859
- const outerStack = ctx.refStack
5860
- ctx.refStack = new Set()
5861
- try { genCodeE(ctx.rootE, 'd', '_p', bodyLines, ctx, SP_PLACEHOLDER) } finally { ctx.refStack = outerStack }
5862
- let body = bodyLines.join('\n ')
5863
- body = body.split(`'${SP_PLACEHOLDER}`).join(`_sp+'`).split(`"${SP_PLACEHOLDER}`).join(`_sp+"`)
5864
- if (body.includes(SP_PLACEHOLDER)) throw DECLINE
5865
- ctx.helperCode.push(
5866
- `const ${fnName}_s=new Set()\n function ${fnName}(d,_p,_all,_e,_sp){\n ` +
5867
- `if(_sg){if(typeof d!=='object'||d===null)return ${fnName}_b(d,_p,_all,_e,_sp);if(${fnName}_s.has(d))return;${fnName}_s.add(d);try{return ${fnName}_b(d,_p,_all,_e,_sp)}finally{${fnName}_s.delete(d)}}\n ` +
5868
- `if(++_sd>${CYCLE_DEPTH})throw _CYC\n const _r=${fnName}_b(d,_p,_all,_e,_sp)\n _sd--\n return _r\n }\n ` +
5869
- `function ${fnName}_b(d,_p,_all,_e,_sp){${body}}`,
5870
- )
5871
- }
5872
- lines.push(`${fnName}(${v},${pathExpr || '""'},_all,_e,'${schemaPrefix}');if(!_all&&_e.length)return{valid:false,errors:_e}`)
5873
- return
5874
- }
5875
- const m = schema.$ref.match(/^#\/(?:\$defs|definitions)\/(.+)$/)
5876
- if (m && ctx.rootDefs && ctx.rootDefs[m[1]]) {
5877
- const defName = m[1]
5878
- if (!ctx.cyclicDefs) ctx.cyclicDefs = cyclicDefNames(ctx.rootDefs)
5879
- if (!ctx.sharedDefs) ctx.sharedDefs = sharedDefNames(ctx.rootSchema, ctx.rootDefs)
5880
- if (ctx.cyclicDefs.has(defName) || ctx.sharedDefs.has(defName)) {
5881
- // Named helper for a def on a cycle, and for one referenced from
5882
- // several places, which inlined would repeat its whole error code at
5883
- // each: Uniswap's token-list schema has 16 references to 10
5884
- // definitions. The path comes in as an argument because every call
5885
- // site has its own. Only the error path runs these; the verdict
5886
- // function, which answers valid documents, is unchanged.
5887
- if (!ctx.defFns) ctx.defFns = new Map()
5888
- let fnName = ctx.defFns.get(defName)
5889
- if (!fnName) {
5890
- fnName = '_defE' + ctx.defFns.size + '_' + defName.replace(/[^A-Za-z0-9_]/g, '_')
5891
- ctx.defFns.set(defName, fnName)
5892
- // The schema path comes in as an argument too, `_sp`, so an error
5893
- // names the path it was reached by, as it does when the definition
5894
- // is inlined and as the interpreted engine reports it; a fixed
5895
- // `#/$defs/<name>` named the definition instead. The body is
5896
- // generated against a placeholder prefix that becomes `_sp` in every
5897
- // string it starts. One left anywhere else declines, rather than
5898
- // emitting a path the placeholder stands in for.
5899
- const bodyLines = []
5900
- genCodeE(ctx.rootDefs[defName], 'd', '_p', bodyLines, ctx, SP_PLACEHOLDER)
5901
- let body = bodyLines.join('\n ')
5902
- body = body.split(`'${SP_PLACEHOLDER}`).join(`_sp+'`).split(`"${SP_PLACEHOLDER}`).join(`_sp+"`)
5903
- if (body.includes(SP_PLACEHOLDER)) throw DECLINE
5904
- ctx.helperCode.push(
5905
- `const ${fnName}_s=new Set()\n function ${fnName}(d,_p,_all,_e,_sp){\n ` +
5906
- `if(_sg){if(typeof d!=='object'||d===null)return ${fnName}_b(d,_p,_all,_e,_sp);if(${fnName}_s.has(d))return;${fnName}_s.add(d);try{return ${fnName}_b(d,_p,_all,_e,_sp)}finally{${fnName}_s.delete(d)}}\n ` +
5907
- `if(++_sd>${CYCLE_DEPTH})throw _CYC\n const _r=${fnName}_b(d,_p,_all,_e,_sp)\n _sd--\n return _r\n }\n ` +
5908
- `function ${fnName}_b(d,_p,_all,_e,_sp){${body}}`,
5909
- )
5910
- }
5911
- lines.push(`${fnName}(${v},${pathExpr || '""'},_all,_e,'${schemaPrefix}');if(!_all&&_e.length)return{valid:false,errors:_e}`)
5912
- return
5913
- }
5914
- // A reference back into what is being expanded, on a cycle the
5915
- // definitions alone do not show (through the root, or another
5916
- // document): declined. Returning emitted nothing, which read as valid.
5917
- if (ctx.refStack.has(schema.$ref)) throw DECLINE
5918
- ctx.refStack.add(schema.$ref)
5919
- genCodeE(ctx.rootDefs[defName], v, pathExpr, lines, ctx, schemaPrefix)
5920
- ctx.refStack.delete(schema.$ref)
5921
- return
5922
- }
5923
- // Anchor ref: "#foo" — resolve via rootDefs or anchors map
5924
- if (!m && schema.$ref.startsWith('#') && !schema.$ref.startsWith('#/')) {
5925
- const entry = ctx.rootDefs && ctx.rootDefs[schema.$ref]
5926
- const anchorTarget = entry && entry.raw ? entry.raw : (ctx.anchors && ctx.anchors[schema.$ref])
5927
- if (anchorTarget) {
5928
- if (ctx.refStack.has(schema.$ref)) throw DECLINE
5929
- ctx.refStack.add(schema.$ref)
5930
- genCodeE(anchorTarget, v, pathExpr, lines, ctx, schemaPrefix)
5931
- ctx.refStack.delete(schema.$ref)
5932
- return
5933
- }
5934
- }
5935
- if (ctx.schemaMap && ctx.schemaMap.has(schema.$ref)) {
5936
- if (ctx.refStack.has(schema.$ref)) throw DECLINE
5937
- ctx.refStack.add(schema.$ref)
5938
- genCodeE(ctx.schemaMap.get(schema.$ref), v, pathExpr, lines, ctx, schemaPrefix)
5939
- ctx.refStack.delete(schema.$ref)
5940
- return
5941
- }
5942
- // Cross-schema ref with JSON pointer fragment ("<id>#/<path>")
5943
- if (ctx.schemaMap && schema.$ref.includes('#') && !schema.$ref.startsWith('#')) {
5944
- const r = resolveCrossSchemaRef(schema.$ref, ctx.schemaMap)
5945
- if (r) {
5946
- if (ctx.refStack.has(schema.$ref)) throw DECLINE
5947
- ctx.refStack.add(schema.$ref)
5948
- genCodeE(r.schema, v, pathExpr, lines, ctx, schemaPrefix)
5949
- ctx.refStack.delete(schema.$ref)
5950
- return
5951
- }
5952
- }
5953
- // A reference none of the above resolved: declined. Falling through
5954
- // emitted nothing for it, which read as valid.
5955
- throw DECLINE
5956
- }
5957
-
5958
- // $dynamicRef — resolve via anchors map
5959
- if (schema.$dynamicRef) genDynamicRefE(schema, v, pathExpr, lines, ctx, schemaPrefix)
5960
-
5961
- const types = schema.type ? (Array.isArray(schema.type) ? schema.type : [schema.type]) : null
5962
- if (types) {
5963
- const conds = types.map(t => {
5964
- switch (t) {
5965
- case 'object': return `(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v}))`
5966
- case 'array': return `Array.isArray(${v})`
5967
- case 'string': return `typeof ${v}==='string'`
5968
- case 'number': return `Number.isFinite(${v})`
5969
- case 'integer': return `Number.isInteger(${v})`
5970
- case 'boolean': return `typeof ${v}==='boolean'`
5971
- case 'null': return `${v}===null`
5972
- default: return 'true'
5973
- }
5974
- })
5975
- const expected = types.join(', ')
5976
- // As written: a one-element array stays an array, as the interpreted engine
5977
- // reports it.
5978
- const expectedParam = !Array.isArray(schema.type) ? `'${types[0]}'` : JSON.stringify(types)
5979
- {
5980
- const typeSp = `${schemaPrefix}/type`
5981
- const lit = buildErrorLiteral({ keyword: 'type', schemaPath: typeSp, sourceMap: ctx.sourceMap })
5982
- lines.push(`if(!(${conds.join('||')})){_e.push({code:'${lit.codeStr}',keyword:'type',instancePath:${pathExpr||'""'},schemaPath:'${typeSp}'${ordinalField(ctx, `${typeSp}`)},params:{type:${expectedParam}},message:'must be ${expected}',docUrl:'${lit.docUrl}'${lit.frame}});if(!_all)return{valid:false,errors:_e}}`)
5983
- }
5984
- }
5985
-
5986
- // In error mode, never assume type — always guard (data may have failed type check but allErrors continues)
5987
- const isObj = false
5988
- const isArr = false
5989
- const isStr = false
5990
- const isNum = false
5991
-
5992
- const fail = (keyword, schemaSuffix, paramsCode, msgCode, fmt) => {
5993
- const sp = schemaPrefix + '/' + schemaSuffix
5994
- const lit = buildErrorLiteral({ keyword, format: fmt, schemaPath: sp, sourceMap: ctx.sourceMap })
5995
- return `_e.push({code:'${lit.codeStr}',keyword:'${keyword}',instancePath:${pathExpr||'""'},schemaPath:'${sp}',params:${paramsCode},message:${msgCode},docUrl:'${lit.docUrl}'${lit.frame}${ordinalField(ctx, sp)}});if(!_all)return{valid:false,errors:_e}`
5996
- }
5997
-
5998
- // enum
5999
- if (schema.enum) {
6000
- lines.push(`if(!(${enumCondition(ctx, schema.enum, v)})){${fail('enum', 'enum', `{allowedValues:${JSON.stringify(schema.enum)}}`, "'must be equal to one of the allowed values'")}}`)
6001
- }
6002
-
6003
- // const — use canonical (sorted-key) comparison for objects
6004
- if (schema.const !== undefined) {
6005
- const cv = schema.const
6006
- if (cv === null || typeof cv !== 'object') {
6007
- lines.push(`if(${v}!==${JSON.stringify(cv)}){${fail('const', 'const', `{allowedValue:${JSON.stringify(schema.const)}}`, "'must be equal to constant'")}}`)
6008
- } else {
6009
- lines.push(`if(!${emitDeq(ctx)}(${v},${emitConstant(ctx, cv)})){${fail('const', 'const', `{allowedValue:${emitParamConstant(ctx, schema.const)}}`, "'must be equal to constant'")}}`)
6010
- }
6011
- }
6012
-
6013
- // required — no destructuring in error mode (data might not be an object)
6014
- const requiredSet = new Set(schema.required || [])
6015
- const hoisted = {}
6016
- if (schema.required) {
6017
- const reqSp = `${schemaPrefix}/required`
6018
- const reqLit = buildErrorLiteral({ keyword: 'required', schemaPath: reqSp, sourceMap: ctx.sourceMap })
6019
- for (const key of schema.required) {
6020
- lines.push(`if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})&&!${ownKeyExpr(ctx, v, key)}){_e.push({code:'${reqLit.codeStr}',keyword:'required',instancePath:${pathExpr||'""'},schemaPath:'${reqSp}'${ordinalField(ctx, `${reqSp}`)},params:{missingProperty:'${esc(key)}'},message:"must have required property '${esc(key)}'",docUrl:'${reqLit.docUrl}'${reqLit.frame}});if(!_all)return{valid:false,errors:_e}}`)
6021
- }
6022
- }
6023
-
6024
- // numeric
6025
- if (schema.minimum !== undefined) {
6026
- const c = isNum ? `${v}<${schema.minimum}` : `typeof ${v}==='number'&&${v}<${schema.minimum}`
6027
- lines.push(`if(${c}){${fail('minimum', 'minimum', `{comparison:'>=',limit:${schema.minimum}}`, `'must be >= ${schema.minimum}'`)}}`)
6028
- }
6029
- if (schema.maximum !== undefined) {
6030
- const c = isNum ? `${v}>${schema.maximum}` : `typeof ${v}==='number'&&${v}>${schema.maximum}`
6031
- lines.push(`if(${c}){${fail('maximum', 'maximum', `{comparison:'<=',limit:${schema.maximum}}`, `'must be <= ${schema.maximum}'`)}}`)
6032
- }
6033
- if (schema.exclusiveMinimum !== undefined) {
6034
- const c = isNum ? `${v}<=${schema.exclusiveMinimum}` : `typeof ${v}==='number'&&${v}<=${schema.exclusiveMinimum}`
6035
- lines.push(`if(${c}){${fail('exclusiveMinimum', 'exclusiveMinimum', `{comparison:'>',limit:${schema.exclusiveMinimum}}`, `'must be > ${schema.exclusiveMinimum}'`)}}`)
6036
- }
6037
- if (schema.exclusiveMaximum !== undefined) {
6038
- const c = isNum ? `${v}>=${schema.exclusiveMaximum}` : `typeof ${v}==='number'&&${v}>=${schema.exclusiveMaximum}`
6039
- lines.push(`if(${c}){${fail('exclusiveMaximum', 'exclusiveMaximum', `{comparison:'<',limit:${schema.exclusiveMaximum}}`, `'must be < ${schema.exclusiveMaximum}'`)}}`)
6040
- }
6041
- if (schema.multipleOf !== undefined) {
6042
- const m = schema.multipleOf
6043
- const ci = ctx.varCounter++
6044
- // Use tolerance-based check for floating point (matches C++ behavior)
6045
- lines.push(`{if(typeof ${v}==='number'&&${multipleOfBad(v, m)}){${fail('multipleOf', 'multipleOf', `{multipleOf:${m}}`, `'must be multiple of ${m}'`)}}}`)
6046
- }
6047
-
6048
- // string length — same s.length fast paths as the boolean codegen above.
6049
- // length < M → certain fail; length > 2*X → certain fail; sweet spot
6050
- // 2*M <= length <= X passes without scanning. Only call _cpLen in the
6051
- // uncertain band (caches it so it isn't called twice when both bounds are set).
6052
- if (schema.minLength !== undefined) {
6053
- const M = schema.minLength
6054
- const inner = `${v}.length<${M}||(${v}.length<${M * 2}&&_cpLen(${v})<${M})`
6055
- const c = isStr ? inner : `typeof ${v}==='string'&&(${inner})`
6056
- lines.push(`if(${c}){${fail('minLength', 'minLength', `{limit:${M}}`, `'must NOT have fewer than ${M} characters'`)}}`)
6057
- }
6058
- if (schema.maxLength !== undefined) {
6059
- const X = schema.maxLength
6060
- const inner = `${v}.length>${X * 2}||(${v}.length>${X}&&_cpLen(${v})>${X})`
6061
- const c = isStr ? inner : `typeof ${v}==='string'&&(${inner})`
6062
- lines.push(`if(${c}){${fail('maxLength', 'maxLength', `{limit:${X}}`, `'must NOT have more than ${X} characters'`)}}`)
6063
- }
6064
- if (schema.pattern) {
6065
- const inlineCheck = compilePatternInline(schema.pattern, v)
6066
- if (inlineCheck) {
6067
- const c = isStr ? `!(${inlineCheck})` : `typeof ${v}==='string'&&!(${inlineCheck})`
6068
- lines.push(`if(${c}){${fail('pattern', 'pattern', `{pattern:${JSON.stringify(schema.pattern)}}`, JSON.stringify(`must match pattern "${schema.pattern}"`))}}`)
6069
- } else {
6070
- const pattern = JSON.stringify(schema.pattern);
6071
- if (!ctx.regExpMap.has(pattern)) {
6072
- const ri = ctx.varCounter++
6073
- ctx.regExpMap.set(pattern, ri)
6074
- if (useSafeEngine(schema.pattern)) {
6075
- ctx.helperCode.push(`const _re${ri}=__ataSafeRe(${pattern})`)
6076
- ctx.usesSafeRe = true
6077
- } else {
6078
- ctx.helperCode.push(`const _re${ri}=new RegExp(${pattern}${reFlagArg(pattern)})`)
6079
- }
6080
- }
6081
- const ri = ctx.regExpMap.get(pattern);
6082
- const c = isStr ? `!_re${ri}.test(${v})` : `typeof ${v}==='string'&&!_re${ri}.test(${v})`
6083
- lines.push(`if(${c}){${fail('pattern', 'pattern', `{pattern:${JSON.stringify(schema.pattern)}}`, JSON.stringify(`must match pattern "${schema.pattern}"`))}}`)
6084
- }
6085
- }
6086
- if (schema.format) {
6087
- const fc = FORMAT_CODEGEN[schema.format]
6088
- const fmtSp = `${schemaPrefix}/format`
6089
- const fmtLit = buildErrorLiteral({ keyword: 'format', format: schema.format, schemaPath: fmtSp, sourceMap: ctx.sourceMap })
6090
- const failPush = `_e.push({code:'${fmtLit.codeStr}',keyword:'format',instancePath:${pathExpr||'""'},schemaPath:'${fmtSp}'${ordinalField(ctx, `${fmtSp}`)},params:{format:'${esc(schema.format)}'},message:'must match format "${esc(schema.format)}"',docUrl:'${fmtLit.docUrl}'${fmtLit.frame}});if(!_all)return{valid:false,errors:_e}`
6091
- // Format errors use the boolean codegen, each `return false` becoming an
6092
- // error push. One violation reports once: the first failing statement
6093
- // pushes and leaves the labeled block, where it used to fall through and
6094
- // let every later statement of the same check push the same error again.
6095
- if (fc) {
6096
- const ri = ctx.varCounter++
6097
- const fmtCode = fc(v, isStr, ctx).replace(/return false/g, `{${failPush};break _fmt${ri}}`)
6098
- lines.push(`_fmt${ri}:{${fmtCode}}`)
6099
- } else if (ctx.userFormats && typeof ctx.userFormats[schema.format] === 'function') {
6100
- // User-supplied format checker on the error path: the same closure
6101
- // plumbing as the boolean codegen. The function is bound as a parameter
6102
- // of the compiled error function; bundle output serializes it via
6103
- // Function#toString separately.
6104
- const safeName = schema.format.replace(/[^a-zA-Z0-9_]/g, '_')
6105
- const closureName = `_uf_${safeName}`
6106
- if (ctx.closureVars && !ctx.closureVars.includes(closureName)) {
6107
- ctx.closureVars.push(closureName)
6108
- ctx.closureVals.push(ctx.userFormats[schema.format])
6109
- }
6110
- const guard = isStr ? '' : `typeof ${v}==='string'&&`
6111
- lines.push(`if(${guard}!${closureName}(${v})){${failPush}}`)
6112
- }
6113
- }
6114
-
6115
- // array size
6116
- if (schema.minItems !== undefined) {
6117
- const c = isArr ? `${v}.length<${schema.minItems}` : `Array.isArray(${v})&&${v}.length<${schema.minItems}`
6118
- lines.push(`if(${c}){${fail('minItems', 'minItems', `{limit:${schema.minItems}}`, `'must NOT have fewer than ${schema.minItems} items'`)}}`)
6119
- }
6120
- if (schema.maxItems !== undefined) {
6121
- const c = isArr ? `${v}.length>${schema.maxItems}` : `Array.isArray(${v})&&${v}.length>${schema.maxItems}`
6122
- lines.push(`if(${c}){${fail('maxItems', 'maxItems', `{limit:${schema.maxItems}}`, `'must NOT have more than ${schema.maxItems} items'`)}}`)
6123
- }
6124
-
6125
- // uniqueItems — tiered: small primitive arrays use nested loop (no allocation)
6126
- if (schema.uniqueItems) genUniqueItemsE(schema, v, pathExpr, lines, ctx, schemaPrefix, fail, isArr)
6127
-
6128
- // object size
6129
- if (schema.minProperties !== undefined) {
6130
- lines.push(`if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})&&Object.keys(${v}).length<${schema.minProperties}){${fail('minProperties', 'minProperties', `{limit:${schema.minProperties}}`, `'must NOT have fewer than ${schema.minProperties} properties'`)}}`)
6131
- }
6132
- if (schema.maxProperties !== undefined) {
6133
- lines.push(`if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})&&Object.keys(${v}).length>${schema.maxProperties}){${fail('maxProperties', 'maxProperties', `{limit:${schema.maxProperties}}`, `'must NOT have more than ${schema.maxProperties} properties'`)}}`)
6134
- }
6135
-
6136
- // additionalProperties: false. A key matched by any patternProperties
6137
- // entry is not additional, so those patterns are consulted here as well.
6138
- if (schema.additionalProperties === false) {
6139
- const allowed = Object.keys(schema.properties || {}).map(k => `${JSON.stringify(k)}`).join(',')
6140
- const ci = ctx.varCounter++
6141
- const apSp = `${schemaPrefix}/additionalProperties`
6142
- const apLit = buildErrorLiteral({ keyword: 'additionalProperties', schemaPath: apSp, sourceMap: ctx.sourceMap })
6143
- const patChecks = []
6144
- for (const pat of Object.keys(schema.patternProperties || {})) {
6145
- const pattern = JSON.stringify(pat)
6146
- if (!ctx.regExpMap.has(pattern)) {
6147
- const ri = ctx.varCounter++
6148
- ctx.regExpMap.set(pattern, ri)
6149
- if (useSafeEngine(pat)) {
6150
- ctx.helperCode.push(`const _re${ri}=__ataSafeRe(${pattern})`)
6151
- ctx.usesSafeRe = true
6152
- } else {
6153
- ctx.helperCode.push(`const _re${ri}=new RegExp(${pattern}${reFlagArg(pattern)})`)
6154
- }
6155
- }
6156
- patChecks.push(`_re${ctx.regExpMap.get(pattern)}.test(_k${ci}[_i])`)
6157
- }
6158
- const isAdditional = patChecks.length ? `!_a${ci}.has(_k${ci}[_i])&&!(${patChecks.join('||')})` : `!_a${ci}.has(_k${ci}[_i])`
6159
- // Same shape as the combined generator: a for-in says whether anything is
6160
- // extra without allocating, and the keys are materialised only to name one.
6161
- // This function walks every node of a document that failed anywhere, so on a
6162
- // large array the clean items were each paying for a key array.
6163
- const gateChecks = patChecks.map((c) => c.split(`_k${ci}[_i]`).join(`_f${ci}`))
6164
- const gateTest = gateChecks.length ? `!_a${ci}.has(_f${ci})&&!(${gateChecks.join('||')})` : `!_a${ci}.has(_f${ci})`
6165
- const detail = `const _k${ci}=Object.keys(${v});for(let _i=0;_i<_k${ci}.length;_i++){if(${isAdditional}){_e.push({code:'${apLit.codeStr}',keyword:'additionalProperties',instancePath:${pathExpr||'""'},schemaPath:'${apSp}'${ordinalField(ctx, `${apSp}`)},params:{additionalProperty:_k${ci}[_i]},message:'must NOT have additional properties',docUrl:'${apLit.docUrl}'${apLit.frame}});if(!_all)return{valid:false,errors:_e}}}`
6166
- // The name set is built once per call of the error function, not at every
6167
- // object it checks: on a list of 1723 tokens it was 1723 Sets.
6168
- ctx.helperCode.push(`const _a${ci}=new Set([${allowed}])`)
6169
- const inner = `let _x${ci}=0;for(const _f${ci} in ${v}){if(${gateTest}){_x${ci}=1;break}}if(_x${ci}){${detail}}`
6170
- lines.push(isObj ? `{${inner}}` : `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){${inner}}`)
6171
- }
6172
-
6173
- // unevaluatedProperties: false, on a node where it is provably its plain
6174
- // counterpart (the bail above guaranteed that for every occurrence). With
6175
- // additionalProperties present everything is evaluated and there is nothing
6176
- // to emit. The error keeps the keyword's own identity: the interpreted
6177
- // engine reports these as unevaluatedProperties with the key named, and the
6178
- // two engines are pinned to identical output.
6179
- // Inside the combined function (a oneOf or anyOf branch, see
6180
- // emitBranchCollapse) this generator only has the static form. A branch
6181
- // whose evaluated set depends on its own branches would report the wrong
6182
- // errors, and the collapse would pick the wrong closest branch; declined.
6183
- if (ctx.inCombined && schema.unevaluatedProperties !== undefined && schema.unevaluatedProperties !== true && !unevalLocalOk(schema, 'unevaluatedProperties')) throw DECLINE
6184
- if (ctx.inCombined && schema.unevaluatedItems !== undefined && schema.unevaluatedItems !== true && !unevalLocalOk(schema, 'unevaluatedItems')) throw DECLINE
6185
- if (schema.unevaluatedProperties === false && schema.additionalProperties === undefined) genUnevaluatedFalseE(schema, v, pathExpr, lines, ctx, schemaPrefix, isObj)
6186
-
6187
- // unevaluatedItems: false under the same rule. The interpreted engine
6188
- // reports one error for the array with the first unevaluated position as
6189
- // the limit; with adjacent `items` everything is evaluated. The draft-07
6190
- // array form of items acts as the prefix.
6191
- if (schema.unevaluatedItems === false) genUnevaluatedItemsFalseE(schema, v, pathExpr, lines, ctx, schemaPrefix)
6192
-
6193
- // dependentRequired
6194
- if (schema.dependentRequired) {
6195
- const drSp = `${schemaPrefix}/dependentRequired`
6196
- const drLit = buildErrorLiteral({ keyword: 'dependentRequired', schemaPath: drSp, sourceMap: ctx.sourceMap })
6197
- for (const [key, deps] of Object.entries(schema.dependentRequired)) {
6198
- for (const dep of deps) {
6199
- lines.push(`if(typeof ${v}==='object'&&${v}!==null&&${ownKeyExpr(ctx, v, key)}&&!${ownKeyExpr(ctx, v, dep)}){_e.push({code:'${drLit.codeStr}',keyword:'required',instancePath:${pathExpr||'""'},schemaPath:'${drSp}'${ordinalField(ctx, `${drSp}`)},params:{missingProperty:'${esc(dep)}'},message:"must have required property '${esc(dep)}'",docUrl:'${drLit.docUrl}'${drLit.frame}});if(!_all)return{valid:false,errors:_e}}`)
6200
- }
6201
- }
6202
- }
6203
-
6204
- // properties — always guard (error mode, data may not be an object or may be array)
6205
- if (schema.properties) {
6206
- for (const [key, prop] of Object.entries(schema.properties)) {
6207
- const childPath = childPathExpr(pathExpr, ptrSeg(key))
6208
- lines.push(`if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})&&${ownKeyExpr(ctx, v, key)}){`)
6209
- genCodeE(prop, `${v}[${JSON.stringify(key)}]`, childPath, lines, ctx, schemaPrefix+'/properties/'+ptrSeg(key))
6210
- lines.push(`}`)
6211
- }
6212
- }
6213
-
6214
- // patternProperties
6215
- if (schema.patternProperties) genPatternPropertiesE(schema, v, pathExpr, lines, ctx, schemaPrefix)
6216
-
6217
- // additionalProperties as a schema — validate each property not declared in
6218
- // `properties` against the subschema, with a dynamic /<key> path. codegenSafe
6219
- // guarantees no patternProperties or composition here, so "additional" is
6220
- // simply "not declared".
6221
- if (typeof schema.additionalProperties === 'object' && schema.additionalProperties !== null) genAdditionalSchemaE(schema, v, pathExpr, lines, ctx, schemaPrefix)
6222
-
6223
- // dependentSchemas
6224
- if (schema.dependentSchemas) genDependentSchemasE(schema, v, pathExpr, lines, ctx, schemaPrefix)
6225
-
6226
- // propertyNames: false reports every key, at the key's own path, the way the
6227
- // interpreter does (plan-compiler: instancePath + '/' + key).
6228
- if (schema.propertyNames === false) genPropertyNamesFalseE(schema, v, pathExpr, lines, ctx, schemaPrefix)
6229
-
6230
- // propertyNames
6231
- if (schema.propertyNames && typeof schema.propertyNames === 'object') genPropertyNamesE(schema, v, pathExpr, lines, ctx, schemaPrefix, fail)
6232
-
6233
- // items — starts after prefixItems (Draft 2020-12 semantics)
6234
- if (schema.items !== undefined && schema.items !== true) {
6235
- const startIdx = schema.prefixItems ? schema.prefixItems.length : 0
6236
- const idx = `_j${ctx.varCounter}`
6237
- const elem = `_ei${ctx.varCounter}`
6238
- ctx.varCounter++
6239
- const childPath = childPathDynExpr(pathExpr, idx)
6240
- lines.push(`if(Array.isArray(${v})){for(let ${idx}=${startIdx};${idx}<${v}.length;${idx}++){const ${elem}=${v}[${idx}]`)
6241
- genCodeE(schema.items, elem, childPath, lines, ctx, schemaPrefix+'/items')
6242
- lines.push(`}}`)
6243
- }
6244
-
6245
- // prefixItems
6246
- if (schema.prefixItems) genPrefixItemsE(schema, v, pathExpr, lines, ctx, schemaPrefix)
6247
-
6248
- // contains
6249
- if (schema.contains !== undefined) genContainsE(schema, v, pathExpr, lines, ctx, schemaPrefix, fail)
6250
-
6251
- // allOf
6252
- if (schema.allOf) {
6253
- for (let _ai = 0; _ai < schema.allOf.length; _ai++) {
6254
- genCodeE(schema.allOf[_ai], v, pathExpr, lines, ctx, schemaPrefix+'/allOf/'+_ai)
6255
- }
6256
- }
6257
-
6258
- // anyOf - collapse to single best-branch error via __ataCollapse
6259
- if (schema.anyOf) genAnyOfE(schema, v, pathExpr, lines, ctx, schemaPrefix)
6260
-
6261
- // oneOf - collapse to single best-branch error via __ataCollapse
6262
- if (schema.oneOf) genOneOfE(schema, v, pathExpr, lines, ctx, schemaPrefix)
6263
-
6264
- // not
6265
- if (schema.not !== undefined) genNotE(schema, v, pathExpr, lines, ctx, schemaPrefix, fail)
6266
-
6267
- // if/then/else
6268
- if (schema.if !== undefined) genIfE(schema, v, pathExpr, lines, ctx, schemaPrefix)
6269
- }
6270
-
6271
- // Rarely used keyword families of genCodeENode, kept out of it so their compile is
6272
- // paid only by schemas that use them.
6273
-
6274
- function genDynamicRefE(schema, v, pathExpr, lines, ctx, schemaPrefix) {
6275
- const anchorKey = schema.$dynamicRef.startsWith('#') ? schema.$dynamicRef : '#' + schema.$dynamicRef
6276
- if (ctx.anchors && ctx.anchors[anchorKey]) {
6277
- const target = ctx.anchors[anchorKey]
6278
- if (target === ctx.rootSchema) {
6279
- // Self-recursive: generate _validateE call
6280
- ctx.usesRecursion = true
6281
- lines.push(`_validateE(${v},_all,_e)`)
6282
- } else {
6283
- const refKey = '$dynamicRef:' + anchorKey
6284
- if (!ctx.refStack.has(refKey)) {
6285
- ctx.refStack.add(refKey)
6286
- genCodeE(target, v, pathExpr, lines, ctx, schemaPrefix)
6287
- ctx.refStack.delete(refKey)
6288
- }
6289
- }
6290
- }
6291
- }
6292
-
6293
- function genUniqueItemsE(schema, v, pathExpr, lines, ctx, schemaPrefix, fail, isArr) {
6294
- const si = ctx.varCounter++
6295
- const failExpr = (iVar, jVar) => fail('uniqueItems', 'uniqueItems', `{i:${iVar},j:${jVar}}`, `'must NOT have duplicate items (items ## '+${jVar}+' and '+${iVar}+' are identical)'`)
6296
- // Only reached once the verdict has rejected, or when errors are wanted, so
6297
- // one strategy for every item type: the hoisted pair finder. See UQP_HELPER.
6298
- const inner = `const _j${si}=${emitUqp(ctx)}(${v});if(_j${si}>=0){const _i${si}=_uqpI;${failExpr('_i' + si, '_j' + si)}}`
6299
- lines.push(isArr ? `{${inner}}` : `if(Array.isArray(${v})){${inner}}`)
6300
- }
6301
-
6302
- function genUnevaluatedFalseE(schema, v, pathExpr, lines, ctx, schemaPrefix, isObj) {
6303
- const allowedU = Object.keys(schema.properties || {}).map(k => `${JSON.stringify(k)}`).join(',')
6304
- const ui = ctx.varCounter++
6305
- const upSp = `${schemaPrefix}/unevaluatedProperties`
6306
- const upLit = buildErrorLiteral({ keyword: 'unevaluatedProperties', schemaPath: upSp, sourceMap: ctx.sourceMap })
6307
- const patChecksU = []
6308
- for (const pat of Object.keys(schema.patternProperties || {})) {
6309
- const pattern = JSON.stringify(pat)
6310
- if (!ctx.regExpMap.has(pattern)) {
6311
- const ri = ctx.varCounter++
6312
- ctx.regExpMap.set(pattern, ri)
6313
- if (useSafeEngine(pat)) {
6314
- ctx.helperCode.push(`const _re${ri}=__ataSafeRe(${pattern})`)
6315
- ctx.usesSafeRe = true
6316
- } else {
6317
- ctx.helperCode.push(`const _re${ri}=new RegExp(${pattern}${reFlagArg(pattern)})`)
6318
- }
6319
- }
6320
- patChecksU.push(`_re${ctx.regExpMap.get(pattern)}.test(_k${ui}[_i])`)
6321
- }
6322
- const isUneval = patChecksU.length ? `!_a${ui}.has(_k${ui}[_i])&&!(${patChecksU.join('||')})` : `!_a${ui}.has(_k${ui}[_i])`
6323
- ctx.helperCode.push(`const _a${ui}=new Set([${allowedU}])`)
6324
- const innerU = `const _k${ui}=Object.keys(${v});for(let _i=0;_i<_k${ui}.length;_i++){if(${isUneval}){_e.push({code:'${upLit.codeStr}',keyword:'unevaluatedProperties',instancePath:${pathExpr||'""'},schemaPath:'${upSp}'${ordinalField(ctx, `${upSp}`)},params:{unevaluatedProperty:_k${ui}[_i]},message:'must NOT have unevaluated properties',docUrl:'${upLit.docUrl}'${upLit.frame}});if(!_all)return{valid:false,errors:_e}}}`
6325
- lines.push(isObj ? `{${innerU}}` : `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){${innerU}}`)
6326
- }
6327
-
6328
- function genUnevaluatedItemsFalseE(schema, v, pathExpr, lines, ctx, schemaPrefix) {
6329
- const itemsIsPrefix = Array.isArray(schema.items)
6330
- const allEval = schema.items !== undefined && !itemsIsPrefix
6331
- if (!allEval) {
6332
- const plen = itemsIsPrefix ? schema.items.length : (Array.isArray(schema.prefixItems) ? schema.prefixItems.length : 0)
6333
- const uiSp = `${schemaPrefix}/unevaluatedItems`
6334
- const uiLit = buildErrorLiteral({ keyword: 'unevaluatedItems', schemaPath: uiSp, sourceMap: ctx.sourceMap })
6335
- lines.push(`if(Array.isArray(${v})&&${v}.length>${plen}){_e.push({code:'${uiLit.codeStr}',keyword:'unevaluatedItems',instancePath:${pathExpr||'""'},schemaPath:'${uiSp}'${ordinalField(ctx, `${uiSp}`)},params:{limit:${plen}},message:'must NOT have more than ${plen} items',docUrl:'${uiLit.docUrl}'${uiLit.frame}});if(!_all)return{valid:false,errors:_e}}`)
6336
- }
6337
- }
6338
-
6339
- function genPatternPropertiesE(schema, v, pathExpr, lines, ctx, schemaPrefix) {
6340
- for (const [pat, sub] of Object.entries(schema.patternProperties)) {
6341
- const pattern = JSON.stringify(pat);
6342
- if (!ctx.regExpMap.has(pattern)) {
6343
- const ri = ctx.varCounter++
6344
- ctx.regExpMap.set(pattern, ri)
6345
- if (useSafeEngine(pat)) {
6346
- ctx.helperCode.push(`const _re${ri}=__ataSafeRe(${pattern})`);
6347
- ctx.usesSafeRe = true
6348
- } else {
6349
- ctx.helperCode.push(`const _re${ri}=new RegExp(${pattern}${reFlagArg(pattern)})`);
6350
- }
6351
- }
6352
- const ri = ctx.regExpMap.get(pattern);
6353
- const ki = ctx.varCounter++
6354
- lines.push(`if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){for(const _k${ki} in ${v}){if(_re${ri}.test(_k${ki})){`)
6355
- const _peFn = emitPtrEsc(ctx)
6356
- const p = pathExpr ? `${pathExpr}+'/'+${_peFn}(_k${ki})` : `'/'+${_peFn}(_k${ki})`
6357
- // The real pointer, so source maps can locate the failing keyword.
6358
- genCodeE(sub, `${v}[_k${ki}]`, p, lines, ctx, schemaPrefix + '/patternProperties/' + ptrSeg(pat))
6359
- lines.push(`}}}`)
6360
- }
6361
- }
6362
-
6363
- function genAdditionalSchemaE(schema, v, pathExpr, lines, ctx, schemaPrefix) {
6364
- const ki = ctx.varCounter++
6365
- const known = Object.keys(schema.properties || {})
6366
- if (known.length) ctx.helperCode.push(`const _ak${ki}=new Set([${known.map((k) => JSON.stringify(k)).join(',')}])`)
6367
- const guard = ''
6368
- // A key matched by any patternProperties entry is not additional.
6369
- const patTests = []
6370
- for (const pat of Object.keys(schema.patternProperties || {})) {
6371
- const pattern = JSON.stringify(pat)
6372
- if (!ctx.regExpMap.has(pattern)) {
6373
- const ri = ctx.varCounter++
6374
- ctx.regExpMap.set(pattern, ri)
6375
- if (useSafeEngine(pat)) {
6376
- ctx.helperCode.push(`const _re${ri}=__ataSafeRe(${pattern})`)
6377
- ctx.usesSafeRe = true
6378
- } else {
6379
- ctx.helperCode.push(`const _re${ri}=new RegExp(${pattern}${reFlagArg(pattern)})`)
6380
- }
6381
- }
6382
- patTests.push(`_re${ctx.regExpMap.get(pattern)}.test(_k${ki})`)
6383
- }
6384
- const conds = []
6385
- if (known.length) conds.push(`!_ak${ki}.has(_k${ki})`)
6386
- if (patTests.length) conds.push(`!(${patTests.join('||')})`)
6387
- const keep = conds.length ? `if(${conds.join('&&')}){` : `{`
6388
- lines.push(`if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){${guard}for(const _k${ki} in ${v}){${keep}`)
6389
- const _peFn = emitPtrEsc(ctx)
6390
- const p = pathExpr ? `${pathExpr}+'/'+${_peFn}(_k${ki})` : `'/'+${_peFn}(_k${ki})`
6391
- genCodeE(schema.additionalProperties, `${v}[_k${ki}]`, p, lines, ctx, schemaPrefix + '/additionalProperties')
6392
- lines.push(`}}}`)
6393
- }
6394
-
6395
- function genDependentSchemasE(schema, v, pathExpr, lines, ctx, schemaPrefix) {
6396
- for (const [key, depSchema] of Object.entries(schema.dependentSchemas)) {
6397
- lines.push(`if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})&&${ownKeyExpr(ctx, v, key)}){`)
6398
- genCodeE(depSchema, v, pathExpr, lines, ctx, schemaPrefix+'/dependentSchemas/'+ptrSeg(key))
6399
- lines.push(`}`)
6400
- }
6401
- }
6402
-
6403
- function genPropertyNamesFalseE(schema, v, pathExpr, lines, ctx, schemaPrefix) {
6404
- const ki = ctx.varCounter++
6405
- // The subschema is applied to the property NAME, so the failure belongs to
6406
- // the object that carries the name, not to `/<key>`, which is where the
6407
- // value lives. Same placement as the object-valued propertyNames branch.
6408
- const p = pathExpr || '""'
6409
- lines.push(`if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){for(const _k${ki} in ${v}){_e.push({keyword:'not',instancePath:${p},schemaPath:'${schemaPrefix}/propertyNames'${ordinalField(ctx, `${schemaPrefix}/propertyNames`)},params:{},message:'boolean schema is false'});if(!_all)return{valid:false,errors:_e}}}`)
6410
- }
6411
-
6412
- function genPropertyNamesE(schema, v, pathExpr, lines, ctx, schemaPrefix, fail) {
6413
- const pn = schema.propertyNames
6414
- const ki = ctx.varCounter++
6415
- lines.push(`if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){for(const _k${ki} in ${v}){`)
6416
- // The subschema checks the key as a value, and its errors are reported at
6417
- // the object, as the interpreter reports them.
6418
- if (!isSimplePN(pn)) {
6419
- genCodeE(pn, `_k${ki}`, pathExpr, lines, ctx, schemaPrefix + '/propertyNames')
6420
- lines.push(`}}`)
6421
- return
6422
- }
6423
- if (pn.minLength !== undefined) {
6424
- lines.push(`if(_k${ki}.length<${pn.minLength}){${fail('minLength', 'propertyNames/minLength', `{limit:${pn.minLength}}`, `'must NOT have fewer than ${pn.minLength} characters'`)}}`)
6425
- }
6426
- if (pn.maxLength !== undefined) {
6427
- lines.push(`if(_k${ki}.length>${pn.maxLength}){${fail('maxLength', 'propertyNames/maxLength', `{limit:${pn.maxLength}}`, `'must NOT have more than ${pn.maxLength} characters'`)}}`)
6428
- }
6429
- if (pn.pattern) {
6430
- const pattern = JSON.stringify(pn.pattern);
6431
- if (!ctx.regExpMap.has(pattern)) {
6432
- const ri = ctx.varCounter++
6433
- ctx.regExpMap.set(pattern, ri)
6434
- if (useSafeEngine(pn.pattern)) {
6435
- ctx.helperCode.push(`const _re${ri}=__ataSafeRe(${pattern})`);
6436
- ctx.usesSafeRe = true
6437
- } else {
6438
- ctx.helperCode.push(`const _re${ri}=new RegExp(${pattern}${reFlagArg(pattern)})`);
6439
- }
6440
- }
6441
- const ri = ctx.regExpMap.get(pattern);
6442
- lines.push(`if(!_re${ri}.test(_k${ki})){${fail('pattern', 'propertyNames/pattern', `{pattern:${JSON.stringify(pn.pattern)}}`, JSON.stringify(`must match pattern "${pn.pattern}"`))}}`)
6443
- }
6444
- if (pn.const !== undefined) {
6445
- lines.push(`if(_k${ki}!==${JSON.stringify(pn.const)}){${fail('const', 'propertyNames/const', `{allowedValue:${JSON.stringify(pn.const)}}`, "'must be equal to constant'")}}`)
6446
- }
6447
- if (pn.enum) {
6448
- const ei = ctx.varCounter++
6449
- ctx.helperCode.push(`const _es${ei}=new Set(${JSON.stringify(pn.enum)})`)
6450
- lines.push(`if(!_es${ei}.has(_k${ki})){${fail('enum', 'propertyNames/enum', `{allowedValues:${JSON.stringify(pn.enum)}}`, "'must be equal to one of the allowed values'")}}`)
6451
- }
6452
- lines.push(`}}`)
6453
- }
6454
-
6455
- function genPrefixItemsE(schema, v, pathExpr, lines, ctx, schemaPrefix) {
6456
- for (let i = 0; i < schema.prefixItems.length; i++) {
6457
- const childPath = childPathExpr(pathExpr, String(i))
6458
- lines.push(`if(Array.isArray(${v})&&${v}.length>${i}){`)
6459
- genCodeE(schema.prefixItems[i], `${v}[${i}]`, childPath, lines, ctx, schemaPrefix+'/prefixItems/'+i)
6460
- lines.push(`}`)
6461
- }
6462
- }
6463
-
6464
- function genContainsE(schema, v, pathExpr, lines, ctx, schemaPrefix, fail) {
6465
- const ci = ctx.varCounter++
6466
- const subLines = []
6467
- nestedGenCode(schema.contains, `_cv`, subLines, ctx)
6468
- const fnBody = subLines.length === 0 ? `return true` : `${subLines.join(';')};return true`
6469
- const minC = schema.minContains !== undefined ? schema.minContains : 1
6470
- const maxC = schema.maxContains
6471
- lines.push(`if(Array.isArray(${v})){const _cf${ci}=function(_cv){${fnBody}};let _cc${ci}=0;for(let _ci${ci}=0;_ci${ci}<${v}.length;_ci${ci}++){if(_cf${ci}(${v}[_ci${ci}]))_cc${ci}++}`)
6472
- lines.push(`if(_cc${ci}<${minC}){${fail('contains', 'contains', `{minContains:${minC}}`, `'must contain at least ${minC} valid item(s)'`)}}`)
6473
- if (maxC !== undefined) {
6474
- lines.push(`if(_cc${ci}>${maxC}){${fail('contains', 'contains', `{minContains:${minC},maxContains:${maxC}}`, `'must NOT contain more than ${maxC} valid item(s)'`)}}`)
6475
- }
6476
- lines.push(`}`)
6477
- }
6478
-
6479
- function genAnyOfE(schema, v, pathExpr, lines, ctx, schemaPrefix) {
6480
- emitBranchCollapse(schema.anyOf, 'anyOf', v, pathExpr, lines, ctx, schemaPrefix)
6481
- }
6482
-
6483
- function genOneOfE(schema, v, pathExpr, lines, ctx, schemaPrefix) {
6484
- emitBranchCollapse(schema.oneOf, 'oneOf', v, pathExpr, lines, ctx, schemaPrefix)
6485
- }
6486
-
6487
- function genNotE(schema, v, pathExpr, lines, ctx, schemaPrefix, fail) {
6488
- const subLines = []
6489
- nestedGenCode(schema.not, '_nv', subLines, ctx)
6490
- const nfn = subLines.length === 0 ? `function(_nv){return true}` : `function(_nv){${subLines.join(';')};return true}`
6491
- const fi = ctx.varCounter++
6492
- lines.push(`{const _nf${fi}=${nfn};if(_nf${fi}(${v})){${fail('not', 'not', '{}', "'must NOT be valid'")}}}`)
6493
- }
6494
-
6495
- function genIfE(schema, v, pathExpr, lines, ctx, schemaPrefix) {
6496
- const ifLines = []
6497
- nestedGenCode(schema.if, '_iv', ifLines, ctx)
6498
- const fi = ctx.varCounter++
6499
- const ifFn = ifLines.length === 0
6500
- ? `function(_iv){return true}`
6501
- : `function(_iv){${ifLines.join(';')};return true}`
6502
- lines.push(`{const _if${fi}=${ifFn}`)
6503
- if (schema.then !== undefined) {
6504
- lines.push(`if(_if${fi}(${v})){`)
6505
- genCodeE(schema.then, v, pathExpr, lines, ctx, schemaPrefix+'/then')
6506
- lines.push(`}`)
6507
- }
6508
- if (schema.else !== undefined) {
6509
- lines.push(`${schema.then !== undefined ? 'else' : `if(!_if${fi}(${v}))`}{`)
6510
- genCodeE(schema.else, v, pathExpr, lines, ctx, schemaPrefix+'/else')
6511
- lines.push(`}`)
6512
- }
6513
- lines.push(`}`)
6514
- }
6515
-
6516
- // --- Combined validator: single pass, validates + collects errors ---
6517
- // Returns VALID_RESULT for valid data, {valid:false, errors} for invalid.
6518
- // Avoids double-pass (jsFn → false → errFn runs same checks again).
6519
- // Uses type-aware optimizations: after type check passes, skip guards.
6520
- // An error literal the combined function shares between calls, frozen. Two
6521
- // non-enumerable fields ride along, invisible to Object.keys, spread and
6522
- // JSON: `_s` says the object is shared, so the reader copies it before handing
6523
- // it out (Object.isFrozen, the check it replaces, cost more than the copy),
6524
- // and `_o` is its schema ordinal, known here and otherwise looked up by path
6525
- // on every read. An `_o` the literal already carries, for standalone output,
6526
- // is kept. `_t` is an empty slot lib/enrich-error.js fills once.
6527
- function sharedErr (lit, rootSchema) {
6528
- Object.defineProperty(lit, '_s', { value: true })
6529
- // A slot the enrichment fills on the first read with the fields that depend
6530
- // only on this literal (code, expected, rank), so later reads skip them.
6531
- Object.defineProperty(lit, '_t', { value: {} })
6532
- if (lit._o === undefined) {
6533
- const o = ordinalFor(rootSchema, lit.schemaPath)
6534
- if (typeof o === 'number') Object.defineProperty(lit, '_o', { value: o })
6535
- }
6536
- return Object.freeze(lit)
6537
- }
6538
-
6539
- // The push for one error in the combined generator. With `ctx.rich` (the
6540
- // validator's errors are being read and enriched, see validator-core) the
6541
- // error is built in its final enriched shape where it happens, by makeRich in
6542
- // lib/enrich-site.js with a site record made here at compile time; otherwise
6543
- // it is the plain literal `lean` the generator always emitted. `valueExpr` is
6544
- // the value at `pathExpr`, which enrichment otherwise finds again by walking
6545
- // the document. `paramsShared` names a frozen params object hoisted for a
6546
- // static site, so a static error's params stay one shared object as before.
6547
- // The ordinal `_ap` is handed with each error of the combined function: the
6548
- // error's place in schema order when its path is fixed at compile time, -1
6549
- // when it is not (a path through a shared definition, a collapsed branch
6550
- // error, an enriched error). `_ap` keeps whether the list is still in order
6551
- // as it grows; a list that came out in order, which is most of them, is
6552
- // handed out without the sort that looked every error's path up in a map.
6553
- function apOrd (ctx, sp, coarse) {
6554
- // Inside a shared definition's function every error takes the ordinal of
6555
- // the reference that reached it, handed in as `_so`: errors behind one
6556
- // `$ref` sort where the reference is written, in the order the definition
6557
- // reports them.
6558
- if (typeof sp === 'string' && sp.includes(SP_PLACEHOLDER) && ctx && ctx.inDefC) return '_so'
6559
- if (typeof sp !== 'string' || sp.includes(SP_PLACEHOLDER) || sp.includes('+')) return -1
6560
- if (!ctx || !ctx.rootSchema) return -1
6561
- const path = unescapeSp(sp)
6562
- const o = ordinalFor(ctx.rootSchema, path)
6563
- if (typeof o !== 'number') return -1
6564
- // The reader orders by ordinal and, between equal ordinals, by the rank of
6565
- // the path (tieOrder in lib/rejections.js). Both are known here, so a site
6566
- // gets a token that assignOrdKeys turns into a dense integer in exactly that
6567
- // order when the program is assembled: two errors with one key are ones the
6568
- // reader keeps as they came. Keyed by the ordinal alone, two errors under
6569
- // one synthetic definition shared a key and sent every such list to the
6570
- // reader, which ranked the paths again: 45 percent of reading a list
6571
- // schema's two errors.
6572
- if (!ctx.ordSites) ctx.ordSites = new Map()
6573
- let site = ctx.ordSites.get(path)
6574
- if (site === undefined) { site = { i: ctx.ordSites.size, o, r: rankFor(ctx.rootSchema, path), key: -1 }; ctx.ordSites.set(path, site) }
6575
- // A coarse key, for the errors behind a reference into a shared definition
6576
- // (see `ap` in lib/combined-runtime.js): -(k+2), written when k is known.
6577
- return coarse ? `@@C${site.i}@@` : `@@O${site.i}@@`
6578
- }
6579
-
6580
- // The dense keys of a program's error sites, in the reader's order: ordinal,
6581
- // then rank as tieOrder compares it, element by element and the shorter
6582
- // first; equal pairs share a key, and a site without a rank keeps -1, which
6583
- // hands its list to the reader. Replaces the tokens apOrd wrote.
6584
- function assignOrdKeys (ctx, src) {
6585
- if (!ctx.ordSites || ctx.ordSites.size === 0) return src
6586
- const sites = [...ctx.ordSites.values()]
6587
- const cmp = (a, b) => {
6588
- if (a.o !== b.o) return a.o - b.o
6589
- const m = Math.min(a.r.length, b.r.length)
6590
- for (let k = 0; k < m; k++) if (a.r[k] !== b.r[k]) return a.r[k] - b.r[k]
6591
- return a.r.length - b.r.length
6592
- }
6593
- const ranked = sites.filter((x) => x.r !== null).sort(cmp)
6594
- let key = 0
6595
- for (let i = 0; i < ranked.length; i++) { if (i > 0 && cmp(ranked[i - 1], ranked[i]) !== 0) key++; ranked[i].key = key }
6596
- return src.replace(/@@([OC])(\d+)@@/g, (_, t, i) => { const k = sites[+i].key; return String(t === 'O' || k < 0 ? k : -(k + 2)) })
6597
- }
6598
-
6599
- // The error list and its order live in lib/combined-runtime.js (`ap`, `sr`):
6600
- // `_A` is this program's state, reset at the top of each call.
6601
- const AP_RESET = '_A.mo=true;_A.lo=-1;_A.on=0;'
6602
- function errPushC(ctx, keyword, sp, pathExpr, paramsCode, msgCode, valueExpr, lean, paramsShared) {
6603
- if (!ctx.rich) return `(_e=_ap(_A,_e,${lean},${apOrd(ctx, sp)}))`
6604
- if (!ctx.richMk) {
6605
- ctx.richMk = true
6606
- const { makeRich } = require('./enrich-site')
6607
- ctx.closureVars.push('_mk'); ctx.closureVals.push(makeRich)
6608
- }
6609
- let paramsVal
6610
- try { paramsVal = Function('return ' + paramsCode)() } catch { paramsVal = undefined }
6611
- const fmt = keyword === 'format' && paramsVal ? paramsVal.format : undefined
6612
- const { errorSite } = require('./enrich-site')
6613
- const siteVar = `_S${ctx.varCounter++}`
6614
- ctx.closureVars.push(siteVar)
6615
- // Inside a shared definition (emitSharedDefC) the site holds the path past
6616
- // the definition and the reference's own comes in as `_sp`.
6617
- const rel = sp.startsWith(SP_PLACEHOLDER)
6618
- ctx.closureVals.push(errorSite(keyword, unescapeSp(rel ? sp.slice(SP_PLACEHOLDER.length) : sp), paramsVal, fmt))
6619
- return `(_e=_ap(_A,_e,_mk(${siteVar},${pathExpr || '""'},${paramsShared || paramsCode},${msgCode},${valueExpr},d${rel ? ',_sp' : ''}),-1))`
6620
- }
6621
-
6622
- function compileToJSCombined(schema, VALID_RESULT, schemaMap, userFormats, opts) {
6623
- const inputSchema = schema
6624
- schema = prepareForCodegen(schema, schemaMap)
6625
- // Same provably-local rule as the error generator above.
6626
- if (typeof schema === 'object' && schema !== null) {
6627
- const s = JSON.stringify(schema)
6628
- // unevaluatedProperties and unevaluatedItems that are not provably local
6629
- // are worked out at run time (genUnevaluatedDynamicC,
6630
- // genUnevaluatedItemsDynamicC).
6631
- // A schema that references itself, through `$ref: "#"` or a definition on
6632
- // a cycle, has those references written as calls under the cycle guard
6633
- // (emitDefFnC).
6634
- // Bail on oneOf/anyOf — branch-collapse aggregation lives in genCodeE only.
6635
- // Letting genCodeC emit the legacy "must match exactly one" placeholder
6636
- // would short-circuit the ATA4001/ATA4002/ATA4003 contract. The boolean
6637
- // fast path plus errFn fallback handles these schemas with collapse.
6638
- // Branches are collapsed into one error by the error generator's emitter
6639
- // (emitBranchCollapse). Its errors are plain, so the rich form, which
6640
- // builds every error enriched where it fails, declines them.
6641
- // In the rich form the collapsed error and its branch errors come out
6642
- // plain (the branches are the error generator's) and are enriched when
6643
- // presented; every other error is built enriched. See richBuilder.
6644
- }
6645
- if (typeof schema === 'boolean') {
6646
- // A `false` root has one fixed error; the rich form leaves it to enrich.
6647
- if (!schema && opts && (opts.rich || opts.resultShape)) return null
6648
- if (opts && opts.resultShape) return null
6649
- return schema
6650
- ? () => VALID_RESULT
6651
- : () => ({ valid: false, errors: [{ keyword: 'not', instancePath: '', schemaPath: '#', params: {}, message: 'boolean schema is false' }] })
6652
- }
6653
- if (typeof schema !== 'object' || schema === null) return null
6654
- if (!sharedCodegenGate(schema, schemaMap)) return null
6655
- if (schema.patternProperties) {
6656
- for (const [pat, sub] of Object.entries(schema.patternProperties)) {
6657
- if (typeof sub === 'object' && sub !== null && !codegenSafe(sub, schemaMap)) return null
6658
- }
6659
- }
6660
- if (schema.dependentSchemas) {
6661
- for (const sub of Object.values(schema.dependentSchemas)) {
6662
- if (typeof sub === 'object' && sub !== null && !codegenSafe(sub, schemaMap)) return null
6663
- }
6664
- }
6665
- if (schema.propertyNames && typeof schema.propertyNames === 'object' && !isSimplePN(schema.propertyNames) && !codegenSafe(schema.propertyNames, schemaMap)) return null
6666
-
6667
- // Build anchors map for $ref/#anchor and $dynamicRef resolution
6668
- const cRootDefs = schema.$defs || schema.definitions || null
6669
- const cAnchors = {}
6670
- if (schema.$dynamicAnchor) cAnchors['#' + schema.$dynamicAnchor] = schema
6671
- if (schema.$anchor) cAnchors['#' + schema.$anchor] = schema
6672
- if (typeof schema.$id === 'string' && schema.$id.startsWith('#')) cAnchors[schema.$id] = schema
6673
- if (cRootDefs) {
6674
- for (const def of Object.values(cRootDefs)) {
6675
- if (def && typeof def === 'object') {
6676
- if (def.$dynamicAnchor) cAnchors['#' + def.$dynamicAnchor] = def
6677
- if (def.$anchor) cAnchors['#' + def.$anchor] = def
6678
- if (typeof def.$id === 'string' && def.$id.startsWith('#')) cAnchors[def.$id] = def
6679
- }
6680
- }
6681
- }
6682
- if (schemaMap) {
6683
- for (const ext of schemaMap.values()) {
6684
- if (ext && typeof ext === 'object') {
6685
- if (ext.$dynamicAnchor && !cAnchors['#' + ext.$dynamicAnchor]) cAnchors['#' + ext.$dynamicAnchor] = ext
6686
- if (ext.$anchor && !cAnchors['#' + ext.$anchor]) cAnchors['#' + ext.$anchor] = ext
6687
- if (typeof ext.$id === 'string' && ext.$id.startsWith('#') && !cAnchors[ext.$id]) cAnchors[ext.$id] = ext
6688
- }
6689
- }
6690
- }
6691
-
6692
- // The same two bails the boolean generator makes. Without them this path
6693
- // emits nothing for a reference it cannot resolve or follow, and an empty
6694
- // program is returned below as `() => VALID_RESULT`, so every constraint
6695
- // behind the reference is dropped instead of the schema routing to the
6696
- // interpreted engine.
6697
- if (hasUnresolvableRef(schema, cRootDefs, cAnchors, schemaMap, new Set())) return null
6698
- if (needsBaseTracking(schema, schemaMap, new Set())) return null
6699
-
6700
- // runtimeShape: errors without the ordinal, see compileToJSCodegenWithErrors.
6701
- // `_ap` adds an error to a list that may not exist yet: the first one
6702
- // creates it as a one-element literal. Pushing onto an empty `[]` grew its
6703
- // backing store on every rejection, about 7% of reading errors across the
6704
- // official suite.
6705
- const ctx = { noOrdinal: !!(opts && opts.runtimeShape), rich: !!(opts && opts.rich), inCombined: true, hoisted: [], rootC: schema, varCounter: 0, preamble: [], helperCode: [], shared: [], closureVars: ['_cpLen', '_A', '_ap', '_sr', '_srX'], closureVals: [_cpLen, sharedRt().apState(), sharedRt().ap, sharedRt().sr, sharedRt().srX], sharedRuntime: true,
6706
- rootDefs: cRootDefs, refStack: new Set(), schemaMap: schemaMap || null, anchors: cAnchors, rootSchema: inputSchema, userFormats: userFormats || null }
6707
- const lines = []
6708
- try {
6709
- genCodeC(schema, 'd', '', lines, ctx, '#')
6710
- } catch (e) {
6711
- if (e === DECLINE) return null
6712
- throw e
6713
- }
6714
- if (lines.length === 0) return () => VALID_RESULT
6715
-
6716
- // Use factory pattern: closure vars (regexes, etc.) created once, not per call.
6717
- // The helpers are constants too and are declared in the factory: inside the
6718
- // function they were rebuilt on every call. `__ataSafeRe` is bound when a
6719
- // helper compiles a pattern through it, which the boolean generator does for
6720
- // subschemas under not/if/contains; without the binding those threw a
6721
- // ReferenceError on the first document that reached them.
6722
- if (opts && opts.resultShape) {
6723
- ctx.closureVars.push('_ER', '_EE', '_SRT', '_FB', '_VF')
6724
- ctx.closureVals.push(opts.resultShape.Rejection, opts.resultShape.empty, opts.resultShape.sort, opts.resultShape.fallback, opts.resultShape.verdict || null)
6725
- }
6726
- if (ctx.usesBranchCollapse) bindRt(ctx, ['__ataCollapse', '__ataMulti'])
6727
- const closureNames = ctx.usesSafeRe ? [...ctx.closureVars, '__ataSafeRe'] : ctx.closureVars
6728
- const closureArgs = ctx.usesSafeRe ? [...ctx.closureVals, compileSafe] : ctx.closureVals
6729
- const closureParams = closureNames.join(',')
6730
- // A definition the error generator wrote as a named function inside a
6731
- // branch (one on a cycle, or referenced from several places) reads the
6732
- // cycle-guard state the error function declares. Here that state is
6733
- // declared too, and the checks run as the error function runs them: in a
6734
- // body of their own, again in the guarded mode when data turns out to
6735
- // point back at itself. This used to decline, which left every schema
6736
- // whose oneOf or anyOf branches reach a shared definition without one
6737
- // pass.
6738
- const guardList = [...(ctx.defFns ? ctx.defFns.values() : []), ...(ctx.cycFnsC || [])]
6739
- const guardDefs = guardList.length ? guardList : null
6740
- if (guardDefs) ctx.helperCode.unshift(emitGuardState())
6741
- // Collapsed oneOf/anyOf: the helper the error generator embeds, `_all` for
6742
- // the branch functions (this function collects everything), and the
6743
- // runtime shape the error generator gives its literals, without code and
6744
- // docUrl. The collapsed error itself keeps its code, as everywhere.
6745
- if (ctx.usesBranchCollapse && opts && opts.runtimeShape) {
6746
- const shape = (src) => src.replace(/code:'ATA\d{4}',/g, '').replace(/,docUrl:'https:\/\/ata-validator\.com\/e\/ATA\d{4}'/g, '')
6747
- for (let i = 0; i < ctx.helperCode.length; i++) ctx.helperCode[i] = shape(ctx.helperCode[i])
6748
- for (let i = 0; i < lines.length; i++) lines[i] = shape(lines[i])
6749
- }
6750
- // Lazy error array: no allocation for valid data (the common case)
6751
- // resultShape: validate() itself for a validator whose errors are being read
6752
- // (see validator-core): the result objects built here, so one pass decides,
6753
- // collects and answers, with nothing between the caller and the checks.
6754
- const rs = opts && opts.resultShape
6755
- // Counts shared between a node's unevaluatedProperties and its anyOf and
6756
- // oneOf (genUnevaluatedDynamicC), visible to every line of the function.
6757
- let hoistDecl = ctx.hoisted.length ? `let ${ctx.hoisted.map((n) => n + '=null').join(',')};` : ''
6758
- let checks = lines.join('\n ')
6759
- let allDecl = ctx.usesBranchCollapse ? 'const _all=true;' : ''
6760
- if (guardDefs) {
6761
- const clears = ['_stk.clear()'].concat(guardDefs.map((n) => `${n}_s.clear()`)).join(';')
6762
- ctx.helperCode.push(`function _bodyC(d){${hoistDecl}${allDecl}let _e;\n ${checks}\n return _e}`)
6763
- hoistDecl = ''
6764
- allDecl = ''
6765
- checks = `_sd=0;try{_e=_bodyC(d)}catch(_err){if(_err!==_CYC)throw _err;_sg=true;${clears};${AP_RESET}try{_e=_bodyC(d)}finally{_sg=false}}`
6766
- }
6767
- // In the result shape this function is validate() itself, so a document
6768
- // that makes it throw is answered by the caller's previous path instead
6769
- // (`_FB`). A thrown error reaching the caller is the one thing this form
6770
- // must not add; a try that does not throw costs nothing measurable.
6771
- const inner = rs
6772
- // With collapsed branches, collecting runs every branch of every oneOf
6773
- // and anyOf in full, which an accepted document does not need: the
6774
- // verdict answers it first.
6775
- // Not when every collapse is guarded by a count (emitBranchCollapse): an
6776
- // accepted document then runs no branch twice, and a rejected one would
6777
- // pay for the verdict for nothing.
6778
- ? `try{${hoistDecl}${ctx.usesBranchCollapse && ctx.unguardedCollapse ? 'if(_VF!==null&&_VF(d))return{valid:true,data:d,errors:_EE};' : ''}${allDecl}let _e;${AP_RESET}\n ` + checks +
6779
- // A collapsed branch error is presented even alone (its ordinal and
6780
- // raw branch errors stay inside); otherwise only a list of two or more
6781
- // needs ordering.
6782
- `\n return _e?new _ER(_A.mo?_e:${process.env.ATA_CHECK_SORT ? '_srX(_A,_e,_SRT)' : '_sr(_A,_e,_SRT)'}):{valid:true,data:d,errors:_EE}}catch(_x){return _FB(d)}`
6783
- : hoistDecl + allDecl + `let _e;${AP_RESET}\n ` + checks + `\n return _e?{valid:false,errors:_e}:R`
6784
- // The verdict generator's hoisted functions (subtreeGuard writes guards
6785
- // through it) come first: a oneOf branch it could not hoist was a closure
6786
- // built at every call of the guard, six per element of a json-patch list.
6787
- const helpers = (ctx.preamble.length ? ctx.preamble.join('\n ') + '\n ' : '') + (ctx.helperCode.length ? ctx.helperCode.join('\n ') + '\n ' : '')
6788
-
6789
- try {
6790
- if (typeof process !== 'undefined' && process.env && process.env.ATA_DUMP_CODEGEN) console.log('=== COMBINED CODEGEN ===\n' + helpers + inner + '\n=== CLOSURE VARS: ' + ctx.closureVars.length + ' ===')
6791
- const factory = new Function('R' + (closureParams ? ',' + closureParams : ''),
6792
- assignOrdKeys(ctx, `${helpers}return function _vC(d){${inner}}`))
6793
- return factory(VALID_RESULT, ...closureArgs)
6794
- } catch (e) {
6795
- if (typeof process !== 'undefined' && process.env && process.env.ATA_DEBUG) console.error('compileToJSCombined error:', e.message, '\n', inner.slice(0, 500))
6796
- return null
6797
- }
6798
- }
6799
-
6800
- // Combined code generator: type-aware like genCode, error-collecting like genCodeE.
6801
- // After type check passes → use optimizations (destructuring, no guards).
6802
- // If type check fails → push error, skip property checks (they'd crash).
6803
- // What of a node still applies to a value that failed its `type`, for the
6804
- // combined generator: the node without `type`, without what it emits outside
6805
- // the type block anyway (enum, const, anyOf, oneOf), and without the keywords
6806
- // of the declared types, which no value of another type meets. Null when
6807
- // nothing that validates is left.
6808
- const TYPE_KEYWORDS = {
6809
- object: ['properties', 'patternProperties', 'additionalProperties', 'required', 'propertyNames', 'minProperties', 'maxProperties', 'dependentRequired', 'dependentSchemas', 'dependencies', 'unevaluatedProperties'],
6810
- array: ['items', 'prefixItems', 'additionalItems', 'contains', 'minContains', 'maxContains', 'minItems', 'maxItems', 'uniqueItems', 'unevaluatedItems'],
6811
- string: ['minLength', 'maxLength', 'pattern'],
6812
- number: ['minimum', 'maximum', 'exclusiveMinimum', 'exclusiveMaximum', 'multipleOf'],
6813
- }
6814
- TYPE_KEYWORDS.integer = TYPE_KEYWORDS.number
6815
- const NOT_VALIDATING = new Set(['$schema', '$id', '$anchor', '$comment', '$defs', 'definitions', 'title', 'description', 'default', 'examples', 'deprecated', 'readOnly', 'writeOnly', '$vocabulary'])
6816
- function typeMismatchRest(schema, types) {
6817
- const drop = new Set(['type', 'enum', 'const', 'anyOf', 'oneOf'])
6818
- for (const t of types) for (const k of (TYPE_KEYWORDS[t] || [])) drop.add(k)
6819
- // A non-integer number reaches the type block already (numberToo).
6820
- let rest = null
6821
- for (const k of Object.keys(schema)) {
6822
- if (drop.has(k)) continue
6823
- if (rest === null) rest = {}
6824
- setOwn(rest, k, schema[k])
6825
- }
6826
- if (rest === null) return null
6827
- // A node whose reference was already written (genCodeCNode) keeps that
6828
- // mark, or the reference is written again here and its errors twice.
6829
- if (schema[REF_DONE]) Object.defineProperty(rest, REF_DONE, { value: true })
6830
- for (const k of Object.keys(rest)) if (!NOT_VALIDATING.has(k) && !k.startsWith('x-')) return rest
6831
- return null
6832
- }
6833
-
6834
- function genCodeC(schema, v, pathExpr, lines, ctx, schemaPrefix) {
6835
- return withPlain(schema, v, lines, ctx, () => genCodeCNode(schema, v, pathExpr, lines, ctx, schemaPrefix))
6836
- }
6837
- // A definition referenced from several places, written once as a function
6838
- // and called at each reference, instead of its whole code at every one: the
6839
- // combined function of SchemaStore's cloud-run (48 KB of schema) came to
6840
- // 1.1 MB of source and sarif's (79 KB) to 2.8 MB, and compiling those was most
6841
- // of the 50 to 300 ms a first rejection with its errors read took. The body is
6842
- // generated against a placeholder schema path that becomes the `_sp` argument,
6843
- // as the error generator does (genCodeENode), so an error names the path it
6844
- // was reached by. It collects into its own list, returned to the caller, and
6845
- // declares its own branch counts (genUnevaluatedDynamicC). The document root
6846
- // comes in as `d` for the rich errors that read it. Not for a definition on a
6847
- // reference cycle, which keeps its existing path. False when not shared.
6848
- const COMBINED_DEF_FN_MIN = 160
6849
- const _defOwnChars = new WeakMap()
6850
- // The definition's own size, its annotations left out, measured once.
6851
- function defOwnChars (def) {
6852
- if (def === null || typeof def !== 'object') return 0
6853
- let n = _defOwnChars.get(def)
6854
- if (n === undefined) {
6855
- n = 0
6856
- const walk = (x) => { if (x === null || typeof x !== 'object') { n += 6; return } if (Array.isArray(x)) { for (const y of x) walk(y); return } for (const k of Object.keys(x)) { if (SIZE_SKIPS.has(k)) continue; n += k.length + 4; walk(x[k]) } }
6857
- walk(def)
6858
- _defOwnChars.set(def, n)
6859
- }
6860
- return n
6861
- }
6862
-
6863
- function emitSharedDefC(defName, v, pathExpr, lines, ctx, schemaPrefix) {
6864
- if (!ctx.cyclicDefs) ctx.cyclicDefs = cyclicDefNames(ctx.rootDefs)
6865
- if (!ctx.sharedDefs) ctx.sharedDefs = sharedDefNames(ctx.rootSchema, ctx.rootDefs)
6866
- const cyclic = ctx.cyclicDefs.has(defName)
6867
- // In the combined function every definition of some size is a function of
6868
- // its own, referenced once or not: expanded, a 25-definition API schema
6869
- // was one 122 KB function, which the optimizing compiler took tens of
6870
- // thousands of calls to reach, so its first twenty thousand answers cost
6871
- // 89 ns each where the hundred-thousandth costs 10. Functions the size of
6872
- // one definition tier up early, and the hot ones first.
6873
- if (!cyclic && !ctx.sharedDefs.has(defName) && !(ctx.inCombined && defOwnChars(ctx.rootDefs[defName]) >= COMBINED_DEF_FN_MIN)) return false
6874
- emitDefFnC(defName, ctx.rootDefs[defName], cyclic, v, pathExpr, lines, ctx, schemaPrefix)
6875
- return true
6876
- }
6877
-
6878
- // One schema as a function of its own in the combined function (see
6879
- // emitSharedDefC). The name is registered before the body is written, so a
6880
- // reference back to it inside becomes a call. One that can re-enter itself,
6881
- // a definition on a cycle or the root through `$ref: "#"`, goes through the
6882
- // cycle guard the error generator's definitions use: depth counted, and in
6883
- // the guarded pass a value already being checked is not checked again.
6884
- function emitDefFnC(key, target, cyclic, v, pathExpr, lines, ctx, schemaPrefix) {
6885
- if (!ctx.defFnsC) ctx.defFnsC = new Map()
6886
- let fnName = ctx.defFnsC.get(key)
6887
- if (!fnName) {
6888
- fnName = '_defC' + ctx.defFnsC.size + '_' + (key === '#' ? 'root' : key.replace(/[^A-Za-z0-9_]/g, '_'))
6889
- ctx.defFnsC.set(key, fnName)
6890
- const outerHoisted = ctx.hoisted
6891
- ctx.hoisted = []
6892
- // The body is a function of its own: a reference the caller was in the
6893
- // middle of expanding is not on the stack inside it, where reaching it
6894
- // again is a call (this function, or another one) rather than a loop.
6895
- // With the caller's stack, a definition expanded once on the way into
6896
- // the root's function was taken for a cycle inside it, and the schema
6897
- // (a tree whose nodes hold trees) lost its one-pass function.
6898
- const outerRefs = ctx.refStack
6899
- ctx.refStack = new Set()
6900
- const bodyLines = []
6901
- let hoisted
6902
- try {
6903
- ctx.inDefC = (ctx.inDefC || 0) + 1
6904
- genCodeC(target, '_dv', '_p', bodyLines, ctx, SP_PLACEHOLDER)
6905
- } finally {
6906
- ctx.inDefC--
6907
- hoisted = ctx.hoisted
6908
- ctx.hoisted = outerHoisted
6909
- ctx.refStack = outerRefs
6910
- }
6911
- let body = bodyLines.join('\n ')
6912
- body = body.split(`'${SP_PLACEHOLDER}`).join(`_sp+'`).split(`"${SP_PLACEHOLDER}`).join(`_sp+"`)
6913
- if (body.includes(SP_PLACEHOLDER)) throw DECLINE
6914
- const decl = hoisted.length ? `let ${hoisted.map((n) => n + '=null').join(',')};` : ''
6915
- const fn = `function ${cyclic ? fnName + '_b' : fnName}(_dv,_p,_sp,d,_so){const _all=true;${decl}let _e;\n ${body}\n return _e}`
6916
- if (cyclic) {
6917
- if (!ctx.cycFnsC) ctx.cycFnsC = []
6918
- ctx.cycFnsC.push(fnName)
6919
- ctx.helperCode.push(
6920
- `const ${fnName}_s=new Set()\n function ${fnName}(_dv,_p,_sp,d,_so){\n ` +
6921
- `if(_sg){if(typeof _dv!=='object'||_dv===null)return ${fnName}_b(_dv,_p,_sp,d,_so);if(${fnName}_s.has(_dv))return;${fnName}_s.add(_dv);try{return ${fnName}_b(_dv,_p,_sp,d,_so)}finally{${fnName}_s.delete(_dv)}}\n ` +
6922
- `if(++_sd>${CYCLE_DEPTH})throw _CYC\n const _r=${fnName}_b(_dv,_p,_sp,d,_so)\n _sd--\n return _r\n }\n ` + fn)
6923
- } else {
6924
- ctx.helperCode.push(fn)
6925
- }
6926
- }
6927
- lines.push(`{const _r=${fnName}(${v},${pathExpr || '""'},'${schemaPrefix}',d,${apOrd(ctx, `${schemaPrefix}/$ref`, true)});if(_r!==undefined){if(_e===undefined)_e=_r;else for(let _i=0;_i<_r.length;_i++)_e.push(_r[_i])}}`)
6928
- }
6929
-
6930
- const REF_DONE = Symbol('ata.refDone')
6931
- function genCodeCNode(schema, v, pathExpr, lines, ctx, schemaPrefix) {
6932
- if (!schemaPrefix) schemaPrefix = '#'
6933
- // Per-node, for the reason spelled out in genCode.
6934
- let ppHandledPropertyNames = false
6935
- if (schema === false) {
6936
- // Leading semicolon: the previous generated line may end in an
6937
- // expression, and a line starting with `(` would be parsed as a call.
6938
- lines.push(';' + errPushC(ctx, 'not', schemaPrefix, pathExpr, '{}', "'boolean schema is false'", v, `{keyword:'not',instancePath:${pathExpr||'""'},schemaPath:'${schemaPrefix}'${ordinalField(ctx, `${schemaPrefix}`)},params:{},message:'boolean schema is false'}`))
6939
- return
6940
- }
6941
- if (schema === true) return
6942
- if (typeof schema !== 'object' || schema === null) return
6943
-
6944
- // $ref — resolve local, anchor, and cross-schema refs
6945
- if (schema.$ref && !schema[REF_DONE]) {
6946
- // A keyword beside $ref that validates applies too (2020-12; draft 7
6947
- // drops such siblings when the schema is read). The reference is written
6948
- // on its own, then the node again with the reference marked done, on a
6949
- // fresh copy so the mark cannot reach the same schema object met through
6950
- // another path; the copy keeps `$ref` for unevaluatedProperties and
6951
- // unevaluatedItems, which read its annotations. Dropping the siblings
6952
- // once accepted what they reject, which is why this declined. Not in a
6953
- // branch function, which the error generator writes.
6954
- if (Object.keys(schema).some((k) => !REF_NEUTRAL_SIBLINGS.has(k) && !k.startsWith('x-'))) {
6955
- if (!ctx.inCombined || ctx.inBranchFn) throw DECLINE
6956
- genCodeCNode({ $ref: schema.$ref }, v, pathExpr, lines, ctx, schemaPrefix)
6957
- const rest = { ...schema }
6958
- Object.defineProperty(rest, REF_DONE, { value: true })
6959
- genCodeCNode(rest, v, pathExpr, lines, ctx, schemaPrefix)
6960
- return
6961
- }
6962
- // A reference back into what is being expanded is a call to that
6963
- // schema's function (emitDefFnC), never nothing: an expansion that stops
6964
- // and emits no lines reads as valid.
6965
- if (schema.$ref === '#') {
6966
- if (ctx.inCombined && !ctx.inBranchFn && ctx.rootC) { emitDefFnC('#', ctx.rootC, true, v, pathExpr, lines, ctx, schemaPrefix); return }
6967
- throw DECLINE
6968
- }
6969
- const m = schema.$ref.match(/^#\/(?:\$defs|definitions)\/(.+)$/)
6970
- if (m && ctx.rootDefs && ctx.rootDefs[m[1]]) {
6971
- if (ctx.inCombined && !ctx.inBranchFn && emitSharedDefC(m[1], v, pathExpr, lines, ctx, schemaPrefix)) return
6972
- if (ctx.refStack.has(schema.$ref)) throw DECLINE
6973
- ctx.refStack.add(schema.$ref)
6974
- genCodeC(ctx.rootDefs[m[1]], v, pathExpr, lines, ctx, schemaPrefix)
6975
- ctx.refStack.delete(schema.$ref)
6976
- return
6977
- }
6978
- // Anchor ref: "#foo" — resolve via rootDefs or anchors map
6979
- if (!m && schema.$ref.startsWith('#') && !schema.$ref.startsWith('#/')) {
6980
- const entry = ctx.rootDefs && ctx.rootDefs[schema.$ref]
6981
- const anchorTarget = entry && entry.raw ? entry.raw : (ctx.anchors && ctx.anchors[schema.$ref])
6982
- if (anchorTarget) {
6983
- if (ctx.refStack.has(schema.$ref)) throw DECLINE
6984
- ctx.refStack.add(schema.$ref)
6985
- genCodeC(anchorTarget, v, pathExpr, lines, ctx, schemaPrefix)
6986
- ctx.refStack.delete(schema.$ref)
6987
- return
6988
- }
6989
- }
6990
- if (ctx.schemaMap && ctx.schemaMap.has(schema.$ref)) {
6991
- if (ctx.refStack.has(schema.$ref)) throw DECLINE
6992
- ctx.refStack.add(schema.$ref)
6993
- genCodeC(ctx.schemaMap.get(schema.$ref), v, pathExpr, lines, ctx, schemaPrefix)
6994
- ctx.refStack.delete(schema.$ref)
6995
- return
6996
- }
6997
- // Cross-schema ref with JSON pointer fragment ("<id>#/<path>")
6998
- if (ctx.schemaMap && schema.$ref.includes('#') && !schema.$ref.startsWith('#')) {
6999
- const r = resolveCrossSchemaRef(schema.$ref, ctx.schemaMap)
7000
- if (r) {
7001
- if (ctx.refStack.has(schema.$ref)) throw DECLINE
7002
- ctx.refStack.add(schema.$ref)
7003
- genCodeC(r.schema, v, pathExpr, lines, ctx, schemaPrefix)
7004
- ctx.refStack.delete(schema.$ref)
7005
- return
7006
- }
7007
- }
7008
- }
7009
-
7010
- // $dynamicRef — resolve via anchors map
7011
- if (schema.$dynamicRef) genDynamicRefC(schema, v, pathExpr, lines, ctx, schemaPrefix)
7012
-
7013
- const types = schema.type ? (Array.isArray(schema.type) ? schema.type : [schema.type]) : null
7014
- let isObj = false, isArr = false, isStr = false, isNum = false
7015
-
7016
- // Pre-allocate error objects as closure variables for static paths.
7017
- // This shrinks the generated function body → better V8 JIT on valid path.
7018
- const isStaticPath = !pathExpr || (pathExpr.startsWith("'") && !pathExpr.includes('+'))
7019
- const fail = (keyword, schemaSuffix, paramsCode, msgCode) => {
7020
- const sp = schemaPrefix + '/' + schemaSuffix
7021
- if (ctx.rich) {
7022
- // A static site shares one frozen params object between calls, as its
7023
- // hoisted literal does below; a dynamic one builds its params per call.
7024
- let shared = null
7025
- if (isStaticPath && msgCode.startsWith("'") && !msgCode.includes('+')) {
7026
- let pv
7027
- try { pv = Function('return ' + paramsCode)() } catch { pv = undefined }
7028
- if (pv !== undefined) {
7029
- shared = `_P${ctx.varCounter++}`
7030
- ctx.closureVars.push(shared); ctx.closureVals.push(Object.freeze(pv))
7031
- }
7032
- }
7033
- return errPushC(ctx, keyword, sp, pathExpr, paramsCode, msgCode, v, null, shared)
7034
- }
7035
- // Hoisting a static error as one shared literal keeps a function that
7036
- // also answers accepted documents small. In the runtime shape this
7037
- // function only runs for documents being rejected (the verdict is its own
7038
- // function), and a shared literal has to be copied for the caller on
7039
- // every read, so there it is written fresh.
7040
- if (isStaticPath && !ctx.noOrdinal && msgCode.startsWith("'") && !msgCode.includes('+')) {
7041
- // Try to evaluate paramsCode as a static constant
7042
- let paramsVal
7043
- try { paramsVal = Function('return ' + paramsCode)() } catch { /* dynamic params — fall through */ }
7044
- if (paramsVal !== undefined) {
7045
- // Static error: pre-allocate as frozen closure variable
7046
- const ei = ctx.varCounter++
7047
- const errVar = `_E${ei}`
7048
- // The path and message are source literals; evaluate them rather than
7049
- // strip the quotes, so an escaped quote or backslash in a key comes
7050
- // out as the character and not as the escape.
7051
- let pathVal, msgVal
7052
- try {
7053
- pathVal = pathExpr ? Function('return ' + pathExpr)() : ''
7054
- msgVal = Function('return ' + msgCode)()
7055
- } catch {
7056
- // Not a literal that parses on its own (a key with a raw line break):
7057
- // leave it to the inline form below, which declines the same way the
7058
- // whole function would.
7059
- pathVal = undefined
7060
- }
7061
- if (pathVal !== undefined) {
7062
- const spVal = unescapeSp(sp)
7063
- ctx.closureVars.push(errVar)
7064
- const o = ctx.noOrdinal ? null : ordinalFor(ctx.rootSchema, spVal)
7065
- const lit = {keyword, instancePath: pathVal, schemaPath: spVal, params: Object.freeze(paramsVal), message: msgVal}
7066
- if (o !== null) lit._o = o
7067
- ctx.closureVals.push(sharedErr(lit, ctx.rootSchema))
7068
- return `(_e=_ap(_A,_e,${errVar},${apOrd(ctx, sp)}))`
7069
- }
7070
- }
7071
- }
7072
- // Dynamic path (e.g., array index): inline as before
7073
- return `(_e=_ap(_A,_e,{keyword:'${keyword}',instancePath:${pathExpr||'""'},schemaPath:'${sp}',params:${paramsCode},message:${msgCode}${ordinalField(ctx, sp)}},${apOrd(ctx, sp)}))`
7074
- }
7075
-
7076
- // enum and const apply to a value of any type, so a value that fails
7077
- // `type` is still checked against them, as the other engines do. They sat
7078
- // inside the type-success block, and `{type: 'number', enum: [1]}` reported
7079
- // only `type` for 'x' where the interpreter reports both.
7080
- const emitEnumConst = () => {
7081
- // enum
7082
- if (schema.enum) {
7083
- lines.push(`if(!(${enumCondition(ctx, schema.enum, v)})){${fail('enum', 'enum', `{allowedValues:${JSON.stringify(schema.enum)}}`, "'must be equal to one of the allowed values'")}}`)
7084
- }
7085
-
7086
- // const
7087
- if (schema.const !== undefined) {
7088
- const cv = schema.const
7089
- if (cv === null || typeof cv !== 'object') {
7090
- lines.push(`if(${v}!==${JSON.stringify(cv)}){${fail('const', 'const', `{allowedValue:${JSON.stringify(schema.const)}}`, "'must be equal to constant'")}}`)
7091
- } else {
7092
- lines.push(`if(!${emitDeq(ctx)}(${v},${emitConstant(ctx, cv)})){${fail('const', 'const', `{allowedValue:${emitParamConstant(ctx, schema.const)}}`, "'must be equal to constant'")}}`)
7093
- }
7094
- }
7095
- }
7096
-
7097
- if (types) {
7098
- const conds = types.map(t => {
7099
- switch (t) {
7100
- case 'object': return `(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v}))`
7101
- case 'array': return `Array.isArray(${v})`
7102
- case 'string': return `typeof ${v}==='string'`
7103
- case 'number': return `Number.isFinite(${v})`
7104
- case 'integer': return `Number.isInteger(${v})`
7105
- case 'boolean': return `typeof ${v}==='boolean'`
7106
- case 'null': return `${v}===null`
7107
- default: return 'true'
7108
- }
7109
- })
7110
- const expected = types.join(', ')
7111
- // As written: a one-element array stays an array, as the interpreted engine
7112
- // reports it.
7113
- const expectedParam = !Array.isArray(schema.type) ? `'${types[0]}'` : JSON.stringify(types)
7114
- // Type check: push error but continue — wrap remaining in type-success block
7115
- const typeOk = `_tok${ctx.varCounter++}`
7116
- lines.push(`const ${typeOk}=${conds.join('||')}`)
7117
- lines.push(`if(!${typeOk}){${fail('type', 'type', `{type:${expectedParam}}`, `'must be ${expected}'`)}}`)
7118
- // Subsequent optimized code runs inside if(typeOk){...}
7119
- if (types.length === 1) {
7120
- isObj = types[0] === 'object'
7121
- isArr = types[0] === 'array'
7122
- isStr = types[0] === 'string'
7123
- isNum = types[0] === 'number' || types[0] === 'integer'
7124
- }
7125
- emitEnumConst()
7126
- // A number that is not an integer fails `integer` and is still a number:
7127
- // minimum, multipleOf and the rest apply to it, and the other engines
7128
- // report them next to the type error. The block opens for any number.
7129
- const numberToo = types.includes('integer') && !types.includes('number')
7130
- lines.push(numberToo ? `if(${typeOk}||typeof ${v}==='number'){` : `if(${typeOk}){`)
7131
- } else {
7132
- emitEnumConst()
7133
- }
7134
-
7135
- // required — use destructuring when type is object (SAFE because type check already passed)
7136
- const requiredSet = new Set(schema.required || [])
7137
- const hoisted = {}
7138
- if (schema.required && schema.properties && isObj) {
7139
- const destructKeys = []
7140
- for (const key of schema.required) {
7141
- if (schema.properties[key]) {
7142
- const lv = `_h${ctx.varCounter++}`
7143
- hoisted[key] = lv
7144
- destructKeys.push(`${JSON.stringify(key)}:${lv}`)
7145
- }
7146
- }
7147
- if (destructKeys.length > 0) {
7148
- lines.push(`let{${destructKeys.join(',')}}=${v}`)
7149
- // A value the object only inherits reads as absent, here and in the
7150
- // property checks that use the same local below.
7151
- for (const key of schema.required) {
7152
- if (hoisted[key]) lines.push(`if(${hoisted[key]}!==undefined&&!${ownGuard(ctx, v, key)})${hoisted[key]}=undefined`)
7153
- }
7154
- }
7155
- for (const key of schema.required) {
7156
- const check = hoisted[key] ? `${hoisted[key]}===undefined` : `!${ownKeyExpr(ctx, v, key)}`
7157
- if (isStaticPath && !ctx.rich && !ctx.noOrdinal) {
7158
- const ei = ctx.varCounter++
7159
- const errVar = `_E${ei}`
7160
- const pathVal = pathExpr ? literalValue(pathExpr) : ''
7161
- ctx.closureVars.push(errVar)
7162
- ctx.closureVals.push(sharedErr({keyword: 'required', instancePath: pathVal, schemaPath: unescapeSp(`${schemaPrefix}/required`), params: Object.freeze({missingProperty: key}), message: `must have required property '${key}'`}, ctx.rootSchema))
7163
- lines.push(`if(${check}){(_e=_ap(_A,_e,${errVar},${apOrd(ctx, `${schemaPrefix}/required`)}))}`)
7164
- } else {
7165
- lines.push(`if(${check}){${errPushC(ctx, 'required', `${schemaPrefix}/required`, pathExpr, `{missingProperty:'${esc(key)}'}`, `"must have required property '${esc(key)}'"`, v, `{keyword:'required',instancePath:${pathExpr||'""'},schemaPath:'${schemaPrefix}/required'${ordinalField(ctx, `${schemaPrefix}/required`)},params:{missingProperty:'${esc(key)}'},message:"must have required property '${esc(key)}'"}`)}}`)
7166
- }
7167
- }
7168
- } else if (schema.required) {
7169
- for (const key of schema.required) {
7170
- const isStatic = !pathExpr || (pathExpr.startsWith("'") && !pathExpr.includes('+'))
7171
- if (isStatic && !ctx.rich && !ctx.noOrdinal) {
7172
- const ei = ctx.varCounter++
7173
- const errVar = `_E${ei}`
7174
- const pathVal = pathExpr ? literalValue(pathExpr) : ''
7175
- ctx.closureVars.push(errVar)
7176
- ctx.closureVals.push(sharedErr({keyword: 'required', instancePath: pathVal, schemaPath: unescapeSp(`${schemaPrefix}/required`), params: Object.freeze({missingProperty: key}), message: `must have required property '${key}'`}, ctx.rootSchema))
7177
- lines.push(`if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})&&!${ownKeyExpr(ctx, v, key)}){(_e=_ap(_A,_e,${errVar},${apOrd(ctx, `${schemaPrefix}/required`)}))}`)
7178
- } else {
7179
- lines.push(`if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})&&!${ownKeyExpr(ctx, v, key)}){${errPushC(ctx, 'required', `${schemaPrefix}/required`, pathExpr, `{missingProperty:'${esc(key)}'}`, `"must have required property '${esc(key)}'"`, v, `{keyword:'required',instancePath:${pathExpr||'""'},schemaPath:'${schemaPrefix}/required'${ordinalField(ctx, `${schemaPrefix}/required`)},params:{missingProperty:'${esc(key)}'},message:"must have required property '${esc(key)}'"}`)}}`)
7180
- }
7181
- }
7182
- }
7183
-
7184
- // numeric — skip type guard if known
7185
- if (schema.minimum !== undefined) { const c = isNum ? `${v}<${schema.minimum}` : `typeof ${v}==='number'&&${v}<${schema.minimum}`; lines.push(`if(${c}){${fail('minimum', 'minimum', `{comparison:'>=',limit:${schema.minimum}}`, `'must be >= ${schema.minimum}'`)}}`) }
7186
- if (schema.maximum !== undefined) { const c = isNum ? `${v}>${schema.maximum}` : `typeof ${v}==='number'&&${v}>${schema.maximum}`; lines.push(`if(${c}){${fail('maximum', 'maximum', `{comparison:'<=',limit:${schema.maximum}}`, `'must be <= ${schema.maximum}'`)}}`) }
7187
- if (schema.exclusiveMinimum !== undefined) { const c = isNum ? `${v}<=${schema.exclusiveMinimum}` : `typeof ${v}==='number'&&${v}<=${schema.exclusiveMinimum}`; lines.push(`if(${c}){${fail('exclusiveMinimum', 'exclusiveMinimum', `{comparison:'>',limit:${schema.exclusiveMinimum}}`, `'must be > ${schema.exclusiveMinimum}'`)}}`) }
7188
- if (schema.exclusiveMaximum !== undefined) { const c = isNum ? `${v}>=${schema.exclusiveMaximum}` : `typeof ${v}==='number'&&${v}>=${schema.exclusiveMaximum}`; lines.push(`if(${c}){${fail('exclusiveMaximum', 'exclusiveMaximum', `{comparison:'<',limit:${schema.exclusiveMaximum}}`, `'must be < ${schema.exclusiveMaximum}'`)}}`) }
7189
- if (schema.multipleOf !== undefined) {
7190
- const m = schema.multipleOf
7191
- const ci = ctx.varCounter++
7192
- lines.push(`{if(typeof ${v}==='number'&&${multipleOfBad(v, m)}){${fail('multipleOf', 'multipleOf', `{multipleOf:${m}}`, `'must be multiple of ${m}'`)}}}`)
7193
- }
7194
-
7195
- // string length — s.length fast paths, _cpLen only in the uncertain band.
7196
- if (schema.minLength !== undefined) {
7197
- const M = schema.minLength
7198
- const inner = `${v}.length<${M}||(${v}.length<${M * 2}&&_cpLen(${v})<${M})`
7199
- const c = isStr ? inner : `typeof ${v}==='string'&&(${inner})`
7200
- lines.push(`if(${c}){${fail('minLength', 'minLength', `{limit:${M}}`, `'must NOT have fewer than ${M} characters'`)}}`)
7201
- }
7202
- if (schema.maxLength !== undefined) {
7203
- const X = schema.maxLength
7204
- const inner = `${v}.length>${X * 2}||(${v}.length>${X}&&_cpLen(${v})>${X})`
7205
- const c = isStr ? inner : `typeof ${v}==='string'&&(${inner})`
7206
- lines.push(`if(${c}){${fail('maxLength', 'maxLength', `{limit:${X}}`, `'must NOT have more than ${X} characters'`)}}`)
7207
- }
7208
- if (schema.pattern) {
7209
- const inlineCheck = compilePatternInline(schema.pattern, v)
7210
- if (inlineCheck) {
7211
- const c = isStr ? `!(${inlineCheck})` : `typeof ${v}==='string'&&!(${inlineCheck})`
7212
- lines.push(`if(${c}){${fail('pattern', 'pattern', `{pattern:${JSON.stringify(schema.pattern)}}`, JSON.stringify(`must match pattern "${schema.pattern}"`))}}`)
7213
- } else {
7214
- const ri = ctx.varCounter++
7215
- const reVar = `_re${ri}`
7216
- ctx.closureVars.push(reVar)
7217
- ctx.closureVals.push(useSafeEngine(schema.pattern) ? compileSafe(schema.pattern) : new RegExp(schema.pattern, reFlags(schema.pattern)))
7218
- const c = isStr ? `!${reVar}.test(${v})` : `typeof ${v}==='string'&&!${reVar}.test(${v})`
7219
- lines.push(`if(${c}){${fail('pattern', 'pattern', `{pattern:${JSON.stringify(schema.pattern)}}`, JSON.stringify(`must match pattern "${schema.pattern}"`))}}`)
7220
- }
7221
- }
7222
- if (schema.format) {
7223
- const fc = FORMAT_CODEGEN[schema.format]
7224
- if (fc) {
7225
- // Same rule as the error codegen: one violation, one error. The first
7226
- // failing statement pushes and leaves the labeled block.
7227
- const ri = ctx.varCounter++
7228
- const code = fc(v, isStr, ctx).replace(/return false/g, `{${fail('format', 'format', `{format:'${esc(schema.format)}'}`, `'must match format "${esc(schema.format)}"'`)};break _fmt${ri}}`)
7229
- lines.push(`_fmt${ri}:{${code}}`)
7230
- } else if (ctx.userFormats && typeof ctx.userFormats[schema.format] === 'function') {
7231
- const safeName = schema.format.replace(/[^a-zA-Z0-9_]/g, '_')
7232
- const closureName = `_uf_${safeName}`
7233
- if (!ctx.closureVars.includes(closureName)) {
7234
- ctx.closureVars.push(closureName)
7235
- ctx.closureVals.push(ctx.userFormats[schema.format])
7236
- }
7237
- const guard = isStr ? '' : `typeof ${v}==='string'&&`
7238
- lines.push(`if(${guard}!${closureName}(${v})){${fail('format', 'format', `{format:'${esc(schema.format)}'}`, `'must match format "${esc(schema.format)}"'`)}}`)
7239
- }
7240
- }
7241
-
7242
- // array size
7243
- if (schema.minItems !== undefined) { const c = isArr ? `${v}.length<${schema.minItems}` : `Array.isArray(${v})&&${v}.length<${schema.minItems}`; lines.push(`if(${c}){${fail('minItems', 'minItems', `{limit:${schema.minItems}}`, `'must NOT have fewer than ${schema.minItems} items'`)}}`) }
7244
- if (schema.maxItems !== undefined) { const c = isArr ? `${v}.length>${schema.maxItems}` : `Array.isArray(${v})&&${v}.length>${schema.maxItems}`; lines.push(`if(${c}){${fail('maxItems', 'maxItems', `{limit:${schema.maxItems}}`, `'must NOT have more than ${schema.maxItems} items'`)}}`) }
7245
-
7246
- // uniqueItems — tiered: small primitive arrays use nested loop (no allocation)
7247
- if (schema.uniqueItems) genUniqueItemsC(schema, v, pathExpr, lines, ctx, schemaPrefix, fail, isArr)
7248
-
7249
- // object size
7250
- if (schema.minProperties !== undefined) lines.push(`if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})&&Object.keys(${v}).length<${schema.minProperties}){${fail('minProperties', 'minProperties', `{limit:${schema.minProperties}}`, `'must NOT have fewer than ${schema.minProperties} properties'`)}}`)
7251
- if (schema.maxProperties !== undefined) lines.push(`if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})&&Object.keys(${v}).length>${schema.maxProperties}){${fail('maxProperties', 'maxProperties', `{limit:${schema.maxProperties}}`, `'must NOT have more than ${schema.maxProperties} properties'`)}}`)
7252
-
7253
- // additionalProperties — skip if patternProperties present (handled in unified loop below)
7254
- // Small property sets: direct === chain (no Set allocation)
7255
- if (schema.additionalProperties === false && !schema.patternProperties) {
7256
- const propKeys = Object.keys(schema.properties || {})
7257
- const ci = ctx.varCounter++
7258
- // The error has to name the offending property, so the keys have to be
7259
- // materialised to report one. They do not have to be materialised to find
7260
- // out whether there is one: a for-in answers that with no allocation, and
7261
- // an accepted document is the common case. The verdict generator pays a
7262
- // bare key count here, and this kept the combined function 2.8x that on a
7263
- // valid document, which is what stopped it from replacing the
7264
- // verdict-then-revalidate pair on the error path.
7265
- const detail = (test) =>
7266
- `const _k${ci}=Object.keys(${v});for(let _i=0;_i<_k${ci}.length;_i++)if(${test}){${fail('additionalProperties', 'additionalProperties', `{additionalProperty:_k${ci}[_i]}`, "'must NOT have additional properties'")}}`
7267
- let body
7268
- if (propKeys.length <= 8) {
7269
- // Direct chain: no Set allocation for small schemas
7270
- // With nothing declared every key is additional: an empty chain would
7271
- // leave `if()`, so the test is `true`.
7272
- const gate = propKeys.map(k => `_f${ci}!==${JSON.stringify(k)}`).join('&&') || 'true'
7273
- const checks = propKeys.map(k => `_k${ci}[_i]!==${JSON.stringify(k)}`).join('&&') || 'true'
7274
- body = `let _x${ci}=0;for(const _f${ci} in ${v}){if(${gate}){_x${ci}=1;break}}if(_x${ci}){${detail(checks)}}`
7275
- } else {
7276
- // The name set lives in the closure, built once with the function: built
7277
- // in the body it was a 33-name Set on every call of this function, which
7278
- // is where a rejected npm-badges document spent most of its 701 ns.
7279
- ctx.closureVars.push(`_a${ci}`)
7280
- ctx.closureVals.push(new Set(propKeys))
7281
- body = `let _x${ci}=0;for(const _f${ci} in ${v}){if(!_a${ci}.has(_f${ci})){_x${ci}=1;break}}if(_x${ci}){${detail(`!_a${ci}.has(_k${ci}[_i])`)}}`
7282
- }
7283
- // When every declared property is required, a clean object has exactly the
7284
- // declared keys: if the enumerated count equals the declared count and
7285
- // every declared name is there, the enumerated set is the declared set and
7286
- // nothing is extra. That is the verdict generator's count, and it skips
7287
- // comparing each key against every name, which kept this function over
7288
- // twice the verdict's cost on an accepted document. Anything else, a
7289
- // different count or a missing name, falls through to the scan, which
7290
- // names the extra keys. A name Object.prototype carries answers `in`
7291
- // through the prototype without being enumerated, so those stay on the
7292
- // scan.
7293
- const req = new Set(Array.isArray(schema.required) ? schema.required : [])
7294
- if (propKeys.length > 0 && propKeys.every((k) => req.has(k) && !PROTO_NAMES.has(k))) {
7295
- const present = propKeys.map((k) => `${JSON.stringify(k)} in ${v}`).join('&&')
7296
- body = `let _n${ci}=0;for(const _f${ci} in ${v})_n${ci}++;if(_n${ci}!==${propKeys.length}||!(${present})){${body}}`
7297
- }
7298
- lines.push(isObj
7299
- ? `{${body}}`
7300
- : `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){${body}}`)
7301
- }
7302
-
7303
- // unevaluatedProperties: false where it is provably additionalProperties
7304
- // (the entry bail guaranteed that), keeping the keyword's own identity so
7305
- // the two engines report the same error.
7306
- if (schema.unevaluatedProperties !== undefined && schema.unevaluatedProperties !== true) {
7307
- if (unevalLocalOk(schema, 'unevaluatedProperties')) {
7308
- if (schema.additionalProperties === undefined && !schema.patternProperties) genUnevaluatedFalseC(schema, v, pathExpr, lines, ctx, schemaPrefix, fail, isObj)
7309
- } else {
7310
- genUnevaluatedDynamicC(schema, v, pathExpr, lines, ctx, schemaPrefix, fail, isObj)
7311
- }
7312
- }
7313
-
7314
- // unevaluatedItems: false under the same rule: one error for the array,
7315
- // the first uncovered position as the limit. Adjacent schema-form `items`
7316
- // evaluates everything; the draft-07 array form acts as the prefix.
7317
- if (schema.unevaluatedItems !== undefined && schema.unevaluatedItems !== true) {
7318
- if (unevalLocalOk(schema, 'unevaluatedItems')) genUnevaluatedItemsFalseC(schema, v, pathExpr, lines, ctx, schemaPrefix, fail)
7319
- else genUnevaluatedItemsDynamicC(schema, v, pathExpr, lines, ctx, schemaPrefix, fail)
7320
- }
7321
-
7322
- // dependentRequired
7323
- if (schema.dependentRequired) {
7324
- for (const [key, deps] of Object.entries(schema.dependentRequired)) {
7325
- for (const dep of deps) {
7326
- const isStatic = !pathExpr || (pathExpr.startsWith("'") && !pathExpr.includes('+'))
7327
- if (isStatic && !ctx.rich && !ctx.noOrdinal) {
7328
- const ei = ctx.varCounter++
7329
- const errVar = `_E${ei}`
7330
- const pathVal = pathExpr ? literalValue(pathExpr) : ''
7331
- ctx.closureVars.push(errVar)
7332
- ctx.closureVals.push(sharedErr({keyword: 'required', instancePath: pathVal, schemaPath: unescapeSp(`${schemaPrefix}/dependentRequired`), params: Object.freeze({missingProperty: dep}), message: `must have required property '${dep}'`}, ctx.rootSchema))
7333
- lines.push(`if(typeof ${v}==='object'&&${v}!==null&&${ownKeyExpr(ctx, v, key)}&&!${ownKeyExpr(ctx, v, dep)}){(_e=_ap(_A,_e,${errVar},-1))}`)
7334
- } else {
7335
- lines.push(`if(typeof ${v}==='object'&&${v}!==null&&${ownKeyExpr(ctx, v, key)}&&!${ownKeyExpr(ctx, v, dep)}){${errPushC(ctx, 'required', `${schemaPrefix}/dependentRequired`, pathExpr, `{missingProperty:'${esc(dep)}'}`, `"must have required property '${esc(dep)}'"`, v, `{keyword:'required',instancePath:${pathExpr||'""'},schemaPath:'${schemaPrefix}/dependentRequired'${ordinalField(ctx, `${schemaPrefix}/dependentRequired`)},params:{missingProperty:'${esc(dep)}'},message:"must have required property '${esc(dep)}'"}`)}}`)
7336
- }
7337
- }
7338
- }
7339
- }
7340
-
7341
- // properties — use hoisted vars for required+known-object, full guard otherwise
7342
- if (schema.properties) {
7343
- for (const [key, prop] of Object.entries(schema.properties)) {
7344
- const pv = hoisted[key] || `${v}[${JSON.stringify(key)}]`
7345
- const childPath = childPathExpr(pathExpr, ptrSeg(key))
7346
- if (requiredSet.has(key) && isObj) {
7347
- // A hoisted local was already cleared if the value is only inherited.
7348
- lines.push(`if(${pv}!==undefined${hoisted[key] ? '' : '&&' + ownGuard(ctx, v, key)}){`)
7349
- genGuardedC(prop, pv, childPath, lines, ctx, schemaPrefix+'/properties/'+ptrSeg(key))
7350
- lines.push(`}`)
7351
- } else if (isObj) {
7352
- const oi = ctx.varCounter++
7353
- lines.push(`{const _o${oi}=${v}[${JSON.stringify(key)}];if(_o${oi}!==undefined&&${ownGuard(ctx, v, key)}){`)
7354
- genGuardedC(prop, `_o${oi}`, childPath, lines, ctx, schemaPrefix+'/properties/'+ptrSeg(key))
7355
- lines.push(`}}`)
7356
- } else {
7357
- lines.push(`if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})&&${ownKeyExpr(ctx, v, key)}){`)
7358
- genGuardedC(prop, `${v}[${JSON.stringify(key)}]`, childPath, lines, ctx, schemaPrefix+'/properties/'+ptrSeg(key))
7359
- lines.push(`}`)
7360
- }
7361
- }
7362
- }
7363
-
7364
- // patternProperties — same optimizations as genCode: charCodeAt + inline key comparison + merged propertyNames
7365
- if (schema.patternProperties) {
7366
- if (genPatternPropertiesC(schema, v, pathExpr, lines, ctx, schemaPrefix, fail, isObj)) ppHandledPropertyNames = true
7367
- }
7368
- // additionalProperties as a schema: each key that is neither declared nor
7369
- // matched by a pattern, validated where it is (see genAdditionalSchemaC).
7370
- if (typeof schema.additionalProperties === 'object' && schema.additionalProperties !== null) genAdditionalSchemaC(schema, v, pathExpr, lines, ctx, schemaPrefix, isObj)
7371
-
7372
-
7373
- // dependentSchemas
7374
- if (schema.dependentSchemas) genDependentSchemasC(schema, v, pathExpr, lines, ctx, schemaPrefix)
7375
-
7376
- if (schema.propertyNames === false) genPropertyNamesFalseC(schema, v, pathExpr, lines, ctx, schemaPrefix)
7377
-
7378
- // propertyNames — skip if already merged into patternProperties loop
7379
- if (schema.propertyNames && typeof schema.propertyNames === 'object' && !ppHandledPropertyNames) genPropertyNamesC(schema, v, pathExpr, lines, ctx, schemaPrefix, fail)
7380
-
7381
- // items
7382
- if (schema.items !== undefined && schema.items !== true) {
7383
- const startIdx = schema.prefixItems ? schema.prefixItems.length : 0
7384
- const idx = `_j${ctx.varCounter}`, elem = `_ei${ctx.varCounter}`
7385
- ctx.varCounter++
7386
- const childPath = childPathDynExpr(pathExpr, idx)
7387
- lines.push(`if(Array.isArray(${v})){for(let ${idx}=${startIdx};${idx}<${v}.length;${idx}++){const ${elem}=${v}[${idx}]`)
7388
- genGuardedC(schema.items, elem, childPath, lines, ctx, schemaPrefix+'/items')
7389
- lines.push(`}}`)
7390
- }
7391
-
7392
- // prefixItems
7393
- if (schema.prefixItems) genPrefixItemsC(schema, v, pathExpr, lines, ctx, schemaPrefix)
7394
-
7395
- // contains
7396
- if (schema.contains !== undefined) genContainsC(schema, v, pathExpr, lines, ctx, schemaPrefix, fail)
7397
-
7398
- // allOf
7399
- if (schema.allOf) { for (let _ai = 0; _ai < schema.allOf.length; _ai++) genCodeC(schema.allOf[_ai], v, pathExpr, lines, ctx, schemaPrefix+'/allOf/'+_ai) }
7400
-
7401
- // anyOf, oneOf. A collapsed branch applies to a value of any type, as enum
7402
- // and const do: the other engines report it next to a failed `type`, so
7403
- // in the combined function it goes after the type-success block below.
7404
- if (schema.anyOf && !ctx.inCombined) genAnyOfC(schema, v, pathExpr, lines, ctx, schemaPrefix, fail)
7405
- if (schema.oneOf && !ctx.inCombined) genOneOfC(schema, v, pathExpr, lines, ctx, schemaPrefix, fail)
7406
-
7407
- // not
7408
- if (schema.not !== undefined) genNotC(schema, v, pathExpr, lines, ctx, schemaPrefix, fail)
7409
-
7410
- // if/then/else
7411
- if (schema.if !== undefined) genIfC(schema, v, pathExpr, lines, ctx, schemaPrefix)
7412
-
7413
- // Close type-success block if opened
7414
- if (types) {
7415
- lines.push(`}`)
7416
- // A value of another type still meets every keyword that applies to its
7417
- // own type, and the applicators (allOf, not, if/then/else, $ref): the
7418
- // other engines report those next to the type error, and the block above
7419
- // skipped them all. The keywords of the declared types cannot apply to
7420
- // it and are left out, so the else is small and does not repeat the
7421
- // declared type's subtree.
7422
- const rest = typeMismatchRest(schema, types)
7423
- if (rest !== null) {
7424
- lines.push(`else{`)
7425
- genCodeC(rest, v, pathExpr, lines, ctx, schemaPrefix)
7426
- lines.push(`}`)
7427
- }
7428
- }
7429
- if (schema.anyOf && ctx.inCombined) genAnyOfC(schema, v, pathExpr, lines, ctx, schemaPrefix, fail)
7430
- if (schema.oneOf && ctx.inCombined) genOneOfC(schema, v, pathExpr, lines, ctx, schemaPrefix, fail)
7431
-
7432
- }
7433
-
7434
- // Rarely used keyword families of genCodeCNode, kept out of it so their compile is
7435
- // paid only by schemas that use them.
7436
-
7437
- function genDynamicRefC(schema, v, pathExpr, lines, ctx, schemaPrefix) {
7438
- const anchorKey = schema.$dynamicRef.startsWith('#') ? schema.$dynamicRef : '#' + schema.$dynamicRef
7439
- if (ctx.anchors && ctx.anchors[anchorKey]) {
7440
- const target = ctx.anchors[anchorKey]
7441
- // This generator has no named functions, so it cannot follow a reference
7442
- // back to the root or around a cycle. It used to emit nothing in both
7443
- // cases, which the function then answered as valid: a document the other
7444
- // generators reject passed through validateJSON. It declines instead,
7445
- // and the schema takes the error generator.
7446
- if (target === ctx.rootSchema) throw DECLINE
7447
- const refKey = '$dynamicRef:' + anchorKey
7448
- if (ctx.refStack.has(refKey)) throw DECLINE
7449
- ctx.refStack.add(refKey)
7450
- genCodeC(target, v, pathExpr, lines, ctx, schemaPrefix)
7451
- ctx.refStack.delete(refKey)
7452
- }
7453
- }
7454
-
7455
- function genUniqueItemsC(schema, v, pathExpr, lines, ctx, schemaPrefix, fail, isArr) {
7456
- const si = ctx.varCounter++
7457
- const failExpr = (iVar, jVar) => fail('uniqueItems', 'uniqueItems', `{i:${iVar},j:${jVar}}`, `'must NOT have duplicate items (items ## '+${jVar}+' and '+${iVar}+' are identical)'`)
7458
- // Only reached once the verdict has rejected, or when errors are wanted, so
7459
- // one strategy for every item type: the hoisted pair finder. See UQP_HELPER.
7460
- const inner = `const _j${si}=${emitUqp(ctx)}(${v});if(_j${si}>=0){const _i${si}=_uqpI;${failExpr('_i' + si, '_j' + si)}}`
7461
- lines.push(isArr ? `{${inner}}` : `if(Array.isArray(${v})){${inner}}`)
7462
- }
7463
-
7464
- // The verdict form of genUnevaluatedDynamicC: the same set, built the same
7465
- // way, and the answer false at the first key outside it.
7466
- function genUnevalDynamicVerdict(schema, v, lines, ctx, isObj, nonObjApplicators) {
7467
- if (schema.additionalProperties !== undefined) {
7468
- if (nonObjApplicators) throw DECLINE
7469
- return
7470
- }
7471
- const ui = ctx.varCounter++
7472
- const S = `_ev${ui}`
7473
- const body = [`const ${S}=[]`]
7474
- addLocalAnnotations(schema, v, S, body, ctx)
7475
- // With the anyOf and oneOf blocks moved here (see genCodeNode), their
7476
- // passing branches are counted by the annotation pass and judged from the
7477
- // count: a oneOf holds with exactly one, an anyOf with at least one.
7478
- const counts = nonObjApplicators ? { anyOf: `_ac${ui}`, oneOf: `_oc${ui}`, oneOfMask: null, of: null, verdict: true } : null
7479
- if (counts) body.push(`let ${counts.anyOf}=0,${counts.oneOf}=0`)
7480
- addTopApplicatorAnnotations(schema, v, S, body, ctx, counts)
7481
- if (counts) {
7482
- if (Array.isArray(schema.anyOf)) body.push(`if(${counts.anyOf}<1)return false`)
7483
- if (Array.isArray(schema.oneOf)) body.push(`if(${counts.oneOf}!==1)return false`)
7484
- }
7485
- const kVar = `_uk${ui}`
7486
- const up = schema.unevaluatedProperties
7487
- if (up === false) {
7488
- body.push(`for(const ${kVar} of Object.keys(${v}))if(!${S}.includes(${kVar}))return false`)
7489
- } else if (typeof up === 'object' && up !== null) {
7490
- const sub = []
7491
- genCode(up, `${v}[${kVar}]`, sub, ctx)
7492
- if (sub.length) body.push(`for(const ${kVar} of Object.keys(${v}))if(!${S}.includes(${kVar})){${sub.join('\n ')}\n }`)
7493
- } else {
7494
- throw DECLINE
7495
- }
7496
- // A value that is not an object skips the pass, so the blocks moved here
7497
- // run for it in the `else`; a value known to be an object has no `else`.
7498
- const other = nonObjApplicators && nonObjApplicators.length && !isObj ? `else{${nonObjApplicators.join('\n ')}}` : ''
7499
- lines.push(isObj ? `{${body.join(';')}}` : `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){${body.join(';')}}${other}`)
7500
- }
7501
-
7502
- // unevaluatedProperties where what is evaluated depends on which branches
7503
- // pass: the set is built at run time with the interpreted engine's rules.
7504
- // The node's own properties, patternProperties and additionalProperties
7505
- // count whether or not their values pass; an allOf, anyOf or dependentSchemas
7506
- // branch, an if, then or else, or a $ref target counts only when it passes,
7507
- // a oneOf only when exactly one branch passes, and `not` never. Each branch is
7508
- // asked through an annotation function (emitAnnot).
7509
- // Run-time unevaluatedProperties written as straight-line code. The names a
7510
- // node and its in-place applicators can evaluate are known when the code is
7511
- // written (their `properties`), so the evaluated set is a bit per name in an
7512
- // integer, each branch's verdict a labelled block, and a failed oneOf's
7513
- // rollback the integer it started from. The annotation functions this
7514
- // replaces (emitAnnot) pushed names onto an array, asked it with `includes`
7515
- // for every key, and were a call per branch per level: ten functions and
7516
- // twenty calls for the suite's nested-reference case, which in the first
7517
- // thousands of calls of a process, before anything is inlined, cost 548 ns
7518
- // against 131. Null when the shape is not modelled: names that are only known
7519
- // at run time (patternProperties, additionalProperties), a nested
7520
- // unevaluatedProperties, a reference cycle, more than 30 names, or too much
7521
- // code; the annotation functions then stand in as before.
7522
- const ANNOT_INLINE_MAX = 8192
7523
- const ANNOT_NAME_MAX = 30
7524
- const ANNOT_BLOCKERS = ['unevaluatedItems', '$dynamicRef', '$recursiveRef', 'dependencies']
7525
- // Besides a bit per declared name, the mask has a bit per pattern of a
7526
- // patternProperties (set when the subschema holding it holds: the keys it
7527
- // matches are evaluated) and one bit, `all`, for a subschema that evaluates
7528
- // every key when it holds (additionalProperties of any value, or a nested
7529
- // `unevaluatedProperties: true`). A nested `unevaluatedProperties` that
7530
- // checks something is not modelled.
7531
- const ALL_BIT_KEY = '\u0000all'
7532
- function annotSubs (node, ctx) {
7533
- const subs = []
7534
- for (const b of node.allOf || []) subs.push(b)
7535
- for (const b of node.anyOf || []) subs.push(b)
7536
- for (const b of node.oneOf || []) subs.push(b)
7537
- if (node.if !== undefined) subs.push(node.if)
7538
- if (node.then !== undefined) subs.push(node.then)
7539
- if (node.else !== undefined) subs.push(node.else)
7540
- if (node.dependentSchemas && typeof node.dependentSchemas === 'object') for (const ds of Object.values(node.dependentSchemas)) subs.push(ds)
7541
- if (typeof node.$ref === 'string') subs.push(refTargetFor(node.$ref, ctx))
7542
- return subs
7543
- }
7544
- function annotNames (node, ctx, names, seen, top) {
7545
- if (node === true || node === false) return true
7546
- if (node === null || typeof node !== 'object' || Array.isArray(node)) return false
7547
- if (seen.has(node)) return false
7548
- seen.add(node)
7549
- try {
7550
- for (const k of ANNOT_BLOCKERS) if (node[k] !== undefined) return false
7551
- if (!top && node.unevaluatedProperties !== undefined && node.unevaluatedProperties !== true) return false
7552
- const add = (key) => { if (!names.has(key)) { if (names.size >= ANNOT_NAME_MAX) return false; names.set(key, names.size) } return true }
7553
- if (node.properties && typeof node.properties === 'object') for (const k of Object.keys(node.properties)) if (!add(k)) return false
7554
- if (node.patternProperties && typeof node.patternProperties === 'object') for (const pat of Object.keys(node.patternProperties)) if (!add('/' + pat)) return false
7555
- if ((!top && node.additionalProperties !== undefined) || (!top && node.unevaluatedProperties === true)) if (!add(ALL_BIT_KEY)) return false
7556
- let subs
7557
- try { subs = annotSubs(node, ctx) } catch { return false }
7558
- for (const b of subs) if (!annotNames(b, ctx, names, seen, false)) return false
7559
- return true
7560
- } finally { seen.delete(node) }
7561
- }
7562
- // The bits a subschema's own keywords evaluate when it holds: its declared
7563
- // names that are present, every pattern it declares, and `all` for a
7564
- // subschema that evaluates every key.
7565
- function ownBits (sub, v, names, ctx, t, top) {
7566
- const out = []
7567
- for (const k of Object.keys(sub.properties || {})) out.push(`if(${ownKeyExpr(ctx, v, k)})${t}|=${1 << names.get(k)}`)
7568
- let fixed = 0
7569
- for (const pat of Object.keys(sub.patternProperties || {})) fixed |= 1 << names.get('/' + pat)
7570
- if (!top && (sub.additionalProperties !== undefined || sub.unevaluatedProperties === true)) fixed |= 1 << names.get(ALL_BIT_KEY)
7571
- if (fixed !== 0) out.push(`${t}|=${fixed}`)
7572
- return out
7573
- }
7574
- // The test for one pattern bit against a key variable, a closure regex or a
7575
- // fixed prefix test (fastPrefixCheck) as addLocalAnnotations writes them.
7576
- function patternKeyTest (ctx, pat, kv) {
7577
- const fast = fastPrefixCheck(pat, kv)
7578
- if (fast) return fast
7579
- const ri = ctx.varCounter++
7580
- ctx.closureVars.push(`_re${ri}`)
7581
- ctx.closureVals.push(safeReClosure(ctx, pat))
7582
- return `_re${ri}.test(${kv})`
7583
- }
7584
- // Factory-scope count variables for one anyOf or oneOf array (by identity),
7585
- // registered in ctx.branchCounts for emitBranchCollapse: `of` the object
7586
- // they were counted for, `count` how many branches held, `mask` which (oneOf
7587
- // up to 30 branches). Declared once per program; a stale value is never
7588
- // taken, since every reader checks `of` against the value at hand.
7589
- function factoryCount (ctx, arr, withMask) {
7590
- if (!ctx.inCombined) return null
7591
- if (!ctx.branchCounts) ctx.branchCounts = new Map()
7592
- const hit = ctx.branchCounts.get(arr)
7593
- if (hit !== undefined) return hit.factory ? hit : null
7594
- const id = ctx.varCounter++
7595
- const fc = { of: `_fa${id}`, count: `_fn${id}`, mask: withMask ? `_fm${id}` : null, factory: true }
7596
- ctx.helperCode.push(`let ${fc.of}=null,${fc.count}=0${withMask ? `,${fc.mask}=0` : ''}`)
7597
- ctx.branchCounts.set(arr, fc)
7598
- return fc
7599
- }
7600
- // Lines that set `ok` (declared by the caller, 1 or 0) for `sub` at `v`, an
7601
- // object, and OR the names it evaluated into `tv` when it holds.
7602
- function emitAnnotInline (sub, v, names, ctx, out, ok, tv) {
7603
- if (sub === true) { out.push(`${ok}=1`); return }
7604
- if (sub === false) { out.push(`${ok}=0`); return }
7605
- const id = ctx.varCounter++
7606
- const t = `_t${id}`
7607
- const local = {}
7608
- for (const k of Object.keys(sub)) if (!ANNOT_APPLICATORS.has(k)) setOwn(local, k, sub[k])
7609
- const vl = []
7610
- // The value is known to be an object here (the block runs for objects
7611
- // only), which the verdict is told when the branch does not say otherwise:
7612
- // told a type, the generator takes it for the value's and skips the
7613
- // branch's own `type`, so a `type: "string"` branch has to keep its check.
7614
- ctx.nestedBoolean = (ctx.nestedBoolean || 0) + 1
7615
- try { genCode(local, v, vl, ctx, local.type === undefined || local.type === 'object' ? 'object' : undefined) } finally { ctx.nestedBoolean-- }
7616
- out.push(`let ${t}=0;${ok}=1`)
7617
- if (vl.length) out.push(`a${id}:{${replaceTopLevel(vl.join(';'), `break a${id}`, `{${ok}=0;break a${id}}`)}}`)
7618
- if (sub.not !== undefined) {
7619
- const nl = []
7620
- nestedGenCode(sub.not, v, nl, ctx)
7621
- if (nl.length) out.push(`if(${ok}){let _nk${id}=1;n${id}:{${replaceTopLevel(nl.join(';'), `break n${id}`, `{_nk${id}=0;break n${id}}`)}};if(_nk${id})${ok}=0}`)
7622
- else out.push(`${ok}=0`)
7623
- }
7624
- const own = ownBits(sub, v, names, ctx, t, false)
7625
- if (own.length) out.push(`if(${ok}){${own.join(';')}}`)
7626
- let bi = 0
7627
- const child = (b) => { const o = `_ok${id}_${bi}`, tb = `_tb${id}_${bi}`; bi++; out.push(`let ${o}=1,${tb}=0`); emitAnnotInline(b, v, names, ctx, out, o, tb); return [o, tb] }
7628
- for (const b of sub.allOf || []) { const [o, tb] = child(b); out.push(`if(!${o})${ok}=0;else ${t}|=${tb}`) }
7629
- // The counts of a nested anyOf or oneOf go to factory-scope variables as
7630
- // well (factoryCount), where the keyword's own error code in a branch
7631
- // function, and the counting copies, read them instead of deciding the
7632
- // branches again: the suite's nested-reference case decided each branch
7633
- // four times over (this pass, the verdict closures, the counting copies,
7634
- // the chosen branch's error function).
7635
- if (Array.isArray(sub.anyOf)) {
7636
- const na = `_na${id}`
7637
- const fc = factoryCount(ctx, sub.anyOf, false)
7638
- out.push(`let ${na}=0${fc ? `;${fc.of}=${v};${fc.count}=0` : ''}`)
7639
- for (const b of sub.anyOf) { const [o, tb] = child(b); out.push(`if(${o}){${na}++;${t}|=${tb}${fc ? `;${fc.count}++` : ''}}`) }
7640
- out.push(`if(${na}===0)${ok}=0`)
7641
- }
7642
- if (Array.isArray(sub.oneOf)) {
7643
- const nc = `_nc${id}`, tc = `_tc${id}`
7644
- const fc = factoryCount(ctx, sub.oneOf, sub.oneOf.length <= 30)
7645
- out.push(`let ${nc}=0,${tc}=0${fc ? `;${fc.of}=${v};${fc.count}=0${fc.mask ? `;${fc.mask}=0` : ''}` : ''}`)
7646
- sub.oneOf.forEach((b, i) => { const [o, tb] = child(b); out.push(`if(${o}){${nc}++;${tc}=${tb}${fc ? `;${fc.count}++${fc.mask ? `;${fc.mask}|=${1 << i}` : ''}` : ''}}`) })
7647
- out.push(`if(${nc}===1)${t}|=${tc};else ${ok}=0`)
7648
- }
7649
- if (sub.if !== undefined) {
7650
- const [oi, ti] = child(sub.if)
7651
- const thenPart = []
7652
- if (sub.then !== undefined) { const saved = out.length; const [ot, tt] = child(sub.then); thenPart.push(...out.splice(saved)); thenPart.push(`if(!${ot})${ok}=0;else ${t}|=${tt}`) }
7653
- const elsePart = []
7654
- if (sub.else !== undefined) { const saved = out.length; const [oe, te] = child(sub.else); elsePart.push(...out.splice(saved)); elsePart.push(`if(!${oe})${ok}=0;else ${t}|=${te}`) }
7655
- out.push(`if(${oi}){${t}|=${ti};${thenPart.join(';')}}else{${elsePart.join(';')}}`)
7656
- }
7657
- if (sub.dependentSchemas && typeof sub.dependentSchemas === 'object') {
7658
- for (const [k, ds] of Object.entries(sub.dependentSchemas)) { const saved = out.length; const [o, tb] = child(ds); const body = out.splice(saved); out.push(`if(${ownKeyExpr(ctx, v, k)}){${body.join(';')};if(!${o})${ok}=0;else ${t}|=${tb}}`) }
7659
- }
7660
- if (typeof sub.$ref === 'string') { const [o, tb] = child(refTargetFor(sub.$ref, ctx)); out.push(`if(!${o})${ok}=0;else ${t}|=${tb}`) }
7661
- out.push(`if(${ok})${tv}|=${t}`)
7662
- }
7663
- // The whole run-time unevaluatedProperties block for `schema` at `v`, or
7664
- // null. `counts` as in genUnevaluatedDynamicC: how many anyOf and oneOf
7665
- // branches held, for the keywords' own code to read.
7666
- function inlineUnevalBlock (schema, v, pathExpr, ctx, schemaPrefix, fail, counts) {
7667
- const names = new Map()
7668
- if (!annotNames(schema, ctx, names, new Set(), true)) return null
7669
- const saved = snapshotCtx(ctx)
7670
- try {
7671
- const id = ctx.varCounter++
7672
- const m = `_m${id}`
7673
- const out = [`let ${m}=0`]
7674
- // One prototype flag for the object, read by every presence test below.
7675
- if (!ctx.plainOf) ctx.plainOf = new Map()
7676
- const prevPk = ctx.plainOf.get(v)
7677
- const pk = prevPk !== undefined ? prevPk : `_pk${id}`
7678
- const prevReuse = ctx.plainReuse
7679
- if (prevPk === undefined) { out.push(`const ${pk}=${protoIs(ctx, v)}`); ctx.plainOf.set(v, pk); (ctx.plainUsed || (ctx.plainUsed = new Set())).add(pk) }
7680
- ctx.plainReuse = v
7681
- out.push(...ownBits(schema, v, names, ctx, m, true))
7682
- let bi = 0
7683
- const child = (b) => { const o = `_ok${id}_${bi}`, tb = `_tb${id}_${bi}`; bi++; out.push(`let ${o}=1,${tb}=0`); emitAnnotInline(b, v, names, ctx, out, o, tb); return [o, tb] }
7684
- for (const b of schema.allOf || []) { const [, tb] = child(b); out.push(`${m}|=${tb}`) }
7685
- if (Array.isArray(schema.anyOf)) {
7686
- const na = counts ? counts.anyOf : `_na${id}`
7687
- out.push(counts ? `${na}=0` : `let ${na}=0`)
7688
- for (const b of schema.anyOf) { const [o, tb] = child(b); out.push(`if(${o}){${na}++;${m}|=${tb}}`) }
7689
- }
7690
- if (Array.isArray(schema.oneOf)) {
7691
- const nc = counts ? counts.oneOf : `_nc${id}`, tc = `_tc${id}`
7692
- out.push(counts ? `${nc}=0;let ${tc}=0` : `let ${nc}=0,${tc}=0`)
7693
- schema.oneOf.forEach((b, i) => { const [o, tb] = child(b); out.push(`if(${o}){${nc}++;${tc}=${tb};${counts && counts.oneOfMask && schema.oneOf.length <= 30 ? `${counts.oneOfMask}|=${1 << i};` : ''}}`) })
7694
- out.push(`if(${nc}===1)${m}|=${tc}`)
7695
- }
7696
- if (schema.if !== undefined) {
7697
- const [oi, ti] = child(schema.if)
7698
- const thenPart = []
7699
- if (schema.then !== undefined) { const sv = out.length; const [ot, tt] = child(schema.then); thenPart.push(...out.splice(sv)); thenPart.push(`if(${ot})${m}|=${tt}`) }
7700
- const elsePart = []
7701
- if (schema.else !== undefined) { const sv = out.length; const [oe, te] = child(schema.else); elsePart.push(...out.splice(sv)); elsePart.push(`if(${oe})${m}|=${te}`) }
7702
- out.push(`if(${oi}){${m}|=${ti};${thenPart.join(';')}}else{${elsePart.join(';')}}`)
7703
- }
7704
- if (schema.dependentSchemas && typeof schema.dependentSchemas === 'object') {
7705
- for (const [k, ds] of Object.entries(schema.dependentSchemas)) { const sv = out.length; const [o, tb] = child(ds); const body = out.splice(sv); out.push(`if(${ownKeyExpr(ctx, v, k)}){${body.join(';')};if(${o})${m}|=${tb}}`) }
7706
- }
7707
- if (typeof schema.$ref === 'string') { const [o, tb] = child(refTargetFor(schema.$ref, ctx)); out.push(`if(${o})${m}|=${tb}`) }
7708
- // What is left: every own key whose bit is not set.
7709
- const kVar = `_uk${id}`
7710
- const cases = [...names].filter(([k]) => k !== ALL_BIT_KEY && k[0] !== '/').map(([k, i]) => `case ${JSON.stringify(k)}:_ub${id}=${1 << i};break`).join(';')
7711
- const allBit = names.has(ALL_BIT_KEY) ? `if((${m}&${1 << names.get(ALL_BIT_KEY)})!==0)continue;` : ''
7712
- const patBits = [...names].filter(([k]) => k[0] === '/').map(([k, i]) => `if((${m}&${1 << i})!==0&&${patternKeyTest(ctx, k.slice(1), `_uk${id}`)})continue;`).join('')
7713
- const up = schema.unevaluatedProperties
7714
- let body
7715
- if (up === false) {
7716
- body = fail('unevaluatedProperties', 'unevaluatedProperties', `{unevaluatedProperty:${kVar}}`, "'must NOT have unevaluated properties'")
7717
- } else if (typeof up === 'object' && up !== null) {
7718
- const pe = emitPtrEsc(ctx)
7719
- const p = pathExpr ? `${pathExpr}+'/'+${pe}(${kVar})` : `'/'+${pe}(${kVar})`
7720
- const sub = []
7721
- genCodeC(up, `${v}[${kVar}]`, p, sub, ctx, schemaPrefix + '/unevaluatedProperties')
7722
- body = sub.join('\n ')
7723
- } else {
7724
- throw DECLINE
7725
- }
7726
- if (body) out.push(`for(const ${kVar} in ${v}){if(!${ownKeyExpr(ctx, v, kVar).split(JSON.stringify(kVar)).join(kVar)})continue;let _ub${id}=0;switch(${kVar}){${cases}}if(_ub${id}!==0&&(${m}&_ub${id})!==0)continue;${allBit}${patBits}${body}}`)
7727
- const text = out.join(';')
7728
- ctx.plainReuse = prevReuse
7729
- if (prevPk === undefined) ctx.plainOf.delete(v)
7730
- if (text.length > ANNOT_INLINE_MAX || /\b(?:_validate|_stk|_sd|_sg|_CYC)\b/.test(text)) { restoreCtx(ctx, saved); return null }
7731
- return text
7732
- } catch (e) {
7733
- if (e !== DECLINE) throw e
7734
- restoreCtx(ctx, saved)
7735
- return null
7736
- }
7737
- }
7738
-
7739
- function genUnevaluatedDynamicC(schema, v, pathExpr, lines, ctx, schemaPrefix, fail, isObj) {
7740
- // additionalProperties evaluates every key the others do not, pass or fail.
7741
- if (schema.additionalProperties !== undefined) return
7742
- const ui = ctx.varCounter++
7743
- const S = `_ev${ui}`
7744
- const body = [`const ${S}=[]`]
7745
- addLocalAnnotations(schema, v, S, body, ctx)
7746
- // The anyOf and oneOf counts are handed to the keywords' own error code
7747
- // (emitBranchCollapse), with the object they were counted for: they are
7748
- // declared once at the top of the combined function, where every later
7749
- // line sees them, and read only when that object is the value at hand, so
7750
- // a count left from another value, another loop iteration or another place
7751
- // is never taken for this one. Only in the combined function proper.
7752
- const share = ctx.inCombined && !ctx.inBranchFn && ctx.hoisted !== undefined &&
7753
- (Array.isArray(schema.anyOf) || Array.isArray(schema.oneOf))
7754
- const counts = share ? { anyOf: `_ac${ui}`, oneOf: `_oc${ui}`, oneOfMask: `_om${ui}`, of: `_ao${ui}` } : null
7755
- if (share) {
7756
- ctx.hoisted.push(counts.anyOf, counts.oneOf, counts.oneOfMask, counts.of)
7757
- body.push(`${counts.of}=${v};${counts.oneOfMask}=0`)
7758
- if (!ctx.branchCounts) ctx.branchCounts = new Map()
7759
- if (Array.isArray(schema.anyOf)) ctx.branchCounts.set(schema.anyOf, { of: counts.of, count: counts.anyOf, mask: null })
7760
- if (Array.isArray(schema.oneOf)) ctx.branchCounts.set(schema.oneOf, { of: counts.of, count: counts.oneOf, mask: schema.oneOf.length <= 30 ? counts.oneOfMask : null })
7761
- }
7762
- const inline = inlineUnevalBlock(schema, v, pathExpr, ctx, schemaPrefix, fail, counts)
7763
- if (inline !== null) {
7764
- const pre = share ? `${counts.of}=${v};${counts.oneOfMask}=0;` : ''
7765
- lines.push(isObj ? `{${pre}${inline}}` : `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){${pre}${inline}}`)
7766
- return
7767
- }
7768
- addTopApplicatorAnnotations(schema, v, S, body, ctx, counts)
7769
- const kVar = `_uk${ui}`
7770
- const up = schema.unevaluatedProperties
7771
- if (up === false) {
7772
- body.push(`for(const ${kVar} of Object.keys(${v}))if(!${S}.includes(${kVar})){${fail('unevaluatedProperties', 'unevaluatedProperties', `{unevaluatedProperty:${kVar}}`, "'must NOT have unevaluated properties'")}}`)
7773
- } else if (typeof up === 'object' && up !== null) {
7774
- const pe = emitPtrEsc(ctx)
7775
- const p = pathExpr ? `${pathExpr}+'/'+${pe}(${kVar})` : `'/'+${pe}(${kVar})`
7776
- const sub = []
7777
- genCodeC(up, `${v}[${kVar}]`, p, sub, ctx, schemaPrefix + '/unevaluatedProperties')
7778
- if (sub.length) body.push(`for(const ${kVar} of Object.keys(${v}))if(!${S}.includes(${kVar})){${sub.join('\n ')}\n }`)
7779
- } else {
7780
- throw DECLINE
7781
- }
7782
- lines.push(isObj ? `{${body.join(';')}}` : `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){${body.join(';')}}`)
7783
- }
7784
-
7785
- // The names a node's own keywords evaluate, pushed onto the array `S`:
7786
- // present declared properties, keys matching a pattern, and every key when
7787
- // additionalProperties is there.
7788
- function addLocalAnnotations(node, v, S, out, ctx) {
7789
- if (node.additionalProperties !== undefined) {
7790
- out.push(`for(const _ka in ${v})${S}.push(_ka)`)
7791
- return
7792
- }
7793
- for (const k of Object.keys(node.properties || {})) out.push(`if(Object.prototype.hasOwnProperty.call(${v},${JSON.stringify(k)}))${S}.push(${JSON.stringify(k)})`)
7794
- const pats = Object.keys(node.patternProperties || {})
7795
- if (pats.length) {
7796
- const tests = []
7797
- const kv = `_pk${ctx.varCounter++}`
7798
- for (const pat of pats) {
7799
- const fast = fastPrefixCheck(pat, kv)
7800
- if (fast) { tests.push(fast); continue }
7801
- const ri = ctx.varCounter++
7802
- ctx.closureVars.push(`_re${ri}`)
7803
- ctx.closureVals.push(safeReClosure(ctx, pat))
7804
- tests.push(`_re${ri}.test(${kv})`)
7805
- }
7806
- out.push(`for(const ${kv} in ${v})if(${tests.join('||')})${S}.push(${kv})`)
7807
- }
7808
- }
7809
-
7810
- // What the applicators of the node that owns the unevaluatedProperties add.
7811
- // Their validity is checked by the code around this; here a branch only
7812
- // contributes, through its annotation function, when it passes, and a oneOf
7813
- // only with exactly one passing branch. An annotation function that fails
7814
- // leaves `S` as it found it.
7815
- function addTopApplicatorAnnotations(node, v, S, out, ctx, counts) {
7816
- if (node.$dynamicRef !== undefined || node.$recursiveRef !== undefined || node.dependencies !== undefined) throw DECLINE
7817
- for (const b of node.allOf || []) out.push(`${emitAnnot(b, ctx)}(${v},${S})`)
7818
- // With `counts`, the passing branches of anyOf and oneOf are counted into
7819
- // variables the caller declared, so the keyword's own error code further
7820
- // down can skip its branches when the count already says it holds (see
7821
- // emitBranchCollapse).
7822
- if (Array.isArray(node.anyOf)) {
7823
- if (counts) out.push(`${counts.anyOf}=0;${node.anyOf.map((b) => `if(${emitAnnot(b, ctx)}(${v},${S}))${counts.anyOf}++;`).join('')}`)
7824
- else for (const b of node.anyOf) out.push(`${emitAnnot(b, ctx)}(${v},${S})`)
7825
- }
7826
- if (Array.isArray(node.oneOf)) {
7827
- const t = counts ? counts.oneOf : `_oc${ctx.varCounter++}`
7828
- // Which branches passed, as bits, for a collapse that has to name them.
7829
- const mask = counts && node.oneOf.length <= 30 ? counts.oneOfMask : null
7830
- // In a verdict (counts.verdict), a second passing branch is the answer.
7831
- const over = counts && counts.verdict ? `if(${t}>1)return false;` : ''
7832
- out.push(`{const ${t}m=${S}.length;${counts ? '' : 'let '}${t}=0;${node.oneOf.map((b, i) => `if(${emitAnnot(b, ctx)}(${v},${S})){${t}++;${over}${mask ? `${mask}|=${1 << i};` : ''}}`).join('')}if(${t}!==1)_rb(${S},${t}m)}`)
7833
- }
7834
- if (node.if !== undefined) {
7835
- const thenCall = node.then !== undefined ? `${emitAnnot(node.then, ctx)}(${v},${S})` : ''
7836
- const elseCall = node.else !== undefined ? `${emitAnnot(node.else, ctx)}(${v},${S})` : ''
7837
- out.push(`if(${emitAnnot(node.if, ctx)}(${v},${S})){${thenCall}}else{${elseCall}}`)
7838
- }
7839
- if (node.dependentSchemas && typeof node.dependentSchemas === 'object') {
7840
- for (const [k, ds] of Object.entries(node.dependentSchemas)) out.push(`if(Object.prototype.hasOwnProperty.call(${v},${JSON.stringify(k)}))${emitAnnot(ds, ctx)}(${v},${S})`)
7841
- }
7842
- if (typeof node.$ref === 'string') out.push(`${emitAnnot(refTargetFor(node.$ref, ctx), ctx)}(${v},${S})`)
7843
- }
7844
-
7845
- function refTargetFor(ref, ctx) {
7846
- const m = ref.match(/^#\/(?:\$defs|definitions)\/(.+)$/)
7847
- const t = m && ctx.rootDefs ? ctx.rootDefs[m[1]] : undefined
7848
- if (t === undefined) throw DECLINE
7849
- return t
7850
- }
7851
-
7852
- // The keywords an annotation function evaluates itself, through its children;
7853
- // the rest of a subschema is checked by a plain verdict.
7854
- const ANNOT_APPLICATORS = new Set(['allOf', 'anyOf', 'oneOf', 'if', 'then', 'else', 'dependentSchemas', '$ref', 'not', 'unevaluatedProperties'])
7855
-
7856
- // A function `(value, S) => valid` for one subschema: whether it passes, and
7857
- // when it does, what it evaluated pushed onto S; when it does not, S is left
7858
- // as it was. Validity is worked out bottom up: the subschema's own keywords
7859
- // by a verdict of the subschema without its applicators, each applicator from
7860
- // its branches' annotation functions. Asking each branch for a full verdict
7861
- // and then again for its annotations repeated every nested branch once per
7862
- // level, which made nested references the slowest shape there was.
7863
- function emitAnnot(sub, ctx) {
7864
- if (sub === true) return '_anT'
7865
- if (sub === false) return '_anF'
7866
- if (sub === null || typeof sub !== 'object') throw DECLINE
7867
- if (!ctx.annFns) {
7868
- ctx.annFns = new Map()
7869
- if (ctx.sharedRuntime) bindRt(ctx, ['_anT', '_anF', '_rb'])
7870
- else (ctx.preamble || ctx.helperCode).push('function _anT(){return true}function _anF(){return false}function _rb(S,m){if(S.length!==m)S.length=m}')
7871
- }
7872
- const hit = ctx.annFns.get(sub)
7873
- if (hit !== undefined) {
7874
- if (hit === null) throw DECLINE // a reference cycle: not modelled here
7875
- return hit
5093
+ const hit = ctx.annFns.get(sub)
5094
+ if (hit !== undefined) {
5095
+ if (hit === null) throw DECLINE // a reference cycle: not modelled here
5096
+ return hit
7876
5097
  }
7877
5098
  if (sub.$dynamicRef !== undefined || sub.$recursiveRef !== undefined || sub.dependencies !== undefined) throw DECLINE
7878
5099
  // unevaluatedItems reads what the applicators evaluate too; the local
@@ -7933,576 +5154,6 @@ function emitAnnot(sub, ctx) {
7933
5154
  return name
7934
5155
  }
7935
5156
 
7936
- // unevaluatedItems worked out at run time, for the combined function. The
7937
- // evaluated positions of an array are kept as the interpreter keeps them
7938
- // (lib/plan-compiler.js): `n`, every index below it evaluated, and `x`, the
7939
- // indexes past it that `contains` matched. This node's own prefixItems,
7940
- // items and contains mark them whatever their own verdict, and each in-place
7941
- // subschema marks them only when it passes (emitAnnotI); what is left must
7942
- // pass unevaluatedItems. `false` reports once, at the first position left,
7943
- // as the interpreter does.
7944
- // Run-time unevaluatedItems written as straight-line code, the way
7945
- // inlineUnevalBlock writes unevaluatedProperties. Without `contains` the
7946
- // positions a node and its in-place applicators evaluate are a prefix: items
7947
- // evaluates every position, prefixItems the first p, a nested
7948
- // `unevaluatedItems: true` every position once its subschema holds; the
7949
- // record is one integer, merged with max where a branch holds, and what is
7950
- // left starts at that index. `contains` (positions that are not a prefix), a
7951
- // tuple `items`, a nested unevaluatedItems that checks something, dynamic
7952
- // references and reference cycles keep the annotation functions.
7953
- const ANNOT_I_BLOCKERS = ['contains', 'additionalItems', '$dynamicRef', '$recursiveRef', 'dependencies']
7954
- function annotItemsFit (node, ctx, seen, top) {
7955
- if (node === true || node === false) return true
7956
- if (node === null || typeof node !== 'object' || Array.isArray(node)) return false
7957
- if (seen.has(node)) return false
7958
- seen.add(node)
7959
- try {
7960
- for (const k of ANNOT_I_BLOCKERS) if (node[k] !== undefined) return false
7961
- if (Array.isArray(node.items)) return false
7962
- if (!top && node.unevaluatedItems !== undefined && node.unevaluatedItems !== true) return false
7963
- if (node.unevaluatedProperties !== undefined && node.unevaluatedProperties !== true) return false
7964
- let subs
7965
- try { subs = annotSubs(node, ctx) } catch { return false }
7966
- for (const b of subs) if (!annotItemsFit(b, ctx, seen, false)) return false
7967
- return true
7968
- } finally { seen.delete(node) }
7969
- }
7970
- // The positions a node's own keywords evaluate, as an expression of the
7971
- // array length `len`: null when none.
7972
- function ownPrefix (node, len, top) {
7973
- if (node.items !== undefined) return len
7974
- if (!top && node.unevaluatedItems === true) return len
7975
- if (Array.isArray(node.prefixItems) && node.prefixItems.length) return `(${len}<${node.prefixItems.length}?${len}:${node.prefixItems.length})`
7976
- return null
7977
- }
7978
- // Lines that set `ok` for `sub` at `v`, an array of length `len`, and raise
7979
- // `tn` to the positions it evaluated when it holds.
7980
- function emitAnnotInlineI (sub, v, len, ctx, out, ok, tn) {
7981
- if (sub === true) { out.push(`${ok}=1`); return }
7982
- if (sub === false) { out.push(`${ok}=0`); return }
7983
- const id = ctx.varCounter++
7984
- const t = `_t${id}`
7985
- const local = {}
7986
- for (const k of Object.keys(sub)) if (!ANNOT_ITEM_APPLICATORS.has(k)) setOwn(local, k, sub[k])
7987
- const vl = []
7988
- ctx.nestedBoolean = (ctx.nestedBoolean || 0) + 1
7989
- try { genCode(local, v, vl, ctx, local.type === undefined || local.type === 'array' ? 'array' : undefined) } finally { ctx.nestedBoolean-- }
7990
- out.push(`let ${t}=0;${ok}=1`)
7991
- if (vl.length) out.push(`a${id}:{${replaceTopLevel(vl.join(';'), `break a${id}`, `{${ok}=0;break a${id}}`)}}`)
7992
- if (sub.not !== undefined) {
7993
- const nl = []
7994
- nestedGenCode(sub.not, v, nl, ctx)
7995
- if (nl.length) out.push(`if(${ok}){let _nk${id}=1;n${id}:{${replaceTopLevel(nl.join(';'), `break n${id}`, `{_nk${id}=0;break n${id}}`)}};if(_nk${id})${ok}=0}`)
7996
- else out.push(`${ok}=0`)
7997
- }
7998
- const own = ownPrefix(sub, len, false)
7999
- if (own !== null) out.push(`if(${ok})${t}=${own}`)
8000
- let bi = 0
8001
- const child = (b) => { const o = `_ok${id}_${bi}`, tb = `_tb${id}_${bi}`; bi++; out.push(`let ${o}=1,${tb}=0`); emitAnnotInlineI(b, v, len, ctx, out, o, tb); return [o, tb] }
8002
- const raise = (tb) => `if(${tb}>${t})${t}=${tb}`
8003
- for (const b of sub.allOf || []) { const [o, tb] = child(b); out.push(`if(!${o})${ok}=0;else ${raise(tb)}`) }
8004
- if (Array.isArray(sub.anyOf)) {
8005
- const na = `_na${id}`
8006
- const fc = factoryCount(ctx, sub.anyOf, false)
8007
- out.push(`let ${na}=0${fc ? `;${fc.of}=${v};${fc.count}=0` : ''}`)
8008
- for (const b of sub.anyOf) { const [o, tb] = child(b); out.push(`if(${o}){${na}++;${raise(tb)}${fc ? `;${fc.count}++` : ''}}`) }
8009
- out.push(`if(${na}===0)${ok}=0`)
8010
- }
8011
- if (Array.isArray(sub.oneOf)) {
8012
- const nc = `_nc${id}`, tc = `_tc${id}`
8013
- const fc = factoryCount(ctx, sub.oneOf, sub.oneOf.length <= 30)
8014
- out.push(`let ${nc}=0,${tc}=0${fc ? `;${fc.of}=${v};${fc.count}=0${fc.mask ? `;${fc.mask}=0` : ''}` : ''}`)
8015
- sub.oneOf.forEach((b, i) => { const [o, tb] = child(b); out.push(`if(${o}){${nc}++;${tc}=${tb}${fc ? `;${fc.count}++${fc.mask ? `;${fc.mask}|=${1 << i}` : ''}` : ''}}`) })
8016
- out.push(`if(${nc}===1){${raise(tc)}}else ${ok}=0`)
8017
- }
8018
- if (sub.if !== undefined) {
8019
- const [oi, ti] = child(sub.if)
8020
- const thenPart = []
8021
- if (sub.then !== undefined) { const sv = out.length; const [ot, tt] = child(sub.then); thenPart.push(...out.splice(sv)); thenPart.push(`if(!${ot})${ok}=0;else ${raise(tt)}`) }
8022
- const elsePart = []
8023
- if (sub.else !== undefined) { const sv = out.length; const [oe, te] = child(sub.else); elsePart.push(...out.splice(sv)); elsePart.push(`if(!${oe})${ok}=0;else ${raise(te)}`) }
8024
- out.push(`if(${oi}){${raise(ti)};${thenPart.join(';')}}else{${elsePart.join(';')}}`)
8025
- }
8026
- if (typeof sub.$ref === 'string') { const [o, tb] = child(refTargetFor(sub.$ref, ctx)); out.push(`if(!${o})${ok}=0;else ${raise(tb)}`) }
8027
- out.push(`if(${ok}&&${t}>${tn})${tn}=${t}`)
8028
- }
8029
- function inlineUnevalItemsBlock (schema, v, pathExpr, ctx, schemaPrefix, fail) {
8030
- if (!ctx.inCombined || !annotItemsFit(schema, ctx, new Set(), true)) return null
8031
- const saved = snapshotCtx(ctx)
8032
- try {
8033
- const id = ctx.varCounter++
8034
- const n = `_un${id}`, len = `_ul${id}`
8035
- const out = [`const ${len}=${v}.length;let ${n}=0`]
8036
- const own = ownPrefix(schema, len, true)
8037
- if (own !== null) out.push(`${n}=${own}`)
8038
- let bi = 0
8039
- const child = (b) => { const o = `_ok${id}_${bi}`, tb = `_tb${id}_${bi}`; bi++; out.push(`let ${o}=1,${tb}=0`); emitAnnotInlineI(b, v, len, ctx, out, o, tb); return [o, tb] }
8040
- const raise = (tb) => `if(${tb}>${n})${n}=${tb}`
8041
- for (const b of schema.allOf || []) { const [, tb] = child(b); out.push(raise(tb)) }
8042
- if (Array.isArray(schema.anyOf)) {
8043
- const fc = factoryCount(ctx, schema.anyOf, false)
8044
- if (fc) out.push(`${fc.of}=${v};${fc.count}=0`)
8045
- for (const b of schema.anyOf) { const [o, tb] = child(b); out.push(`if(${o}){${raise(tb)}${fc ? `;${fc.count}++` : ''}}`) }
8046
- }
8047
- if (Array.isArray(schema.oneOf)) {
8048
- const fc = factoryCount(ctx, schema.oneOf, schema.oneOf.length <= 30)
8049
- const nc = `_nc${id}`, tc = `_tc${id}`
8050
- out.push(`let ${nc}=0,${tc}=0${fc ? `;${fc.of}=${v};${fc.count}=0${fc.mask ? `;${fc.mask}=0` : ''}` : ''}`)
8051
- schema.oneOf.forEach((b, i) => { const [o, tb] = child(b); out.push(`if(${o}){${nc}++;${tc}=${tb}${fc ? `;${fc.count}++${fc.mask ? `;${fc.mask}|=${1 << i}` : ''}` : ''}}`) })
8052
- out.push(`if(${nc}===1)${raise(tc)}`)
8053
- }
8054
- if (schema.if !== undefined) {
8055
- const [oi, ti] = child(schema.if)
8056
- const thenPart = []
8057
- if (schema.then !== undefined) { const sv = out.length; const [ot, tt] = child(schema.then); thenPart.push(...out.splice(sv)); thenPart.push(`if(${ot})${raise(tt)}`) }
8058
- const elsePart = []
8059
- if (schema.else !== undefined) { const sv = out.length; const [oe, te] = child(schema.else); elsePart.push(...out.splice(sv)); elsePart.push(`if(${oe})${raise(te)}`) }
8060
- out.push(`if(${oi}){${raise(ti)};${thenPart.join(';')}}else{${elsePart.join(';')}}`)
8061
- }
8062
- if (typeof schema.$ref === 'string') { const [o, tb] = child(refTargetFor(schema.$ref, ctx)); out.push(`if(${o})${raise(tb)}`) }
8063
- const iv = `_ix${id}`
8064
- const ui2 = schema.unevaluatedItems
8065
- if (ui2 === false) {
8066
- // Reported once, at the first position left, as the interpreter does.
8067
- out.push(`if(${n}<${len}){const ${iv}=${n};${fail('unevaluatedItems', 'unevaluatedItems', `{limit:${iv}}`, `'must NOT have more than '+${iv}+' items'`)}}`)
8068
- } else if (typeof ui2 === 'object' && ui2 !== null) {
8069
- const pe = pathExpr ? `${pathExpr}+'/'+${iv}` : `'/'+${iv}`
8070
- const sub = []
8071
- genCodeC(ui2, `${v}[${iv}]`, pe, sub, ctx, schemaPrefix + '/unevaluatedItems')
8072
- if (sub.length) out.push(`for(let ${iv}=${n};${iv}<${len};${iv}++){${sub.join('\n ')}\n }`)
8073
- } else {
8074
- throw DECLINE
8075
- }
8076
- const text = out.join(';')
8077
- if (text.length > ANNOT_INLINE_MAX || /\b(?:_validate|_stk|_sd|_sg|_CYC)\b/.test(text)) { restoreCtx(ctx, saved); return null }
8078
- return text
8079
- } catch (e) {
8080
- if (e !== DECLINE) throw e
8081
- restoreCtx(ctx, saved)
8082
- return null
8083
- }
8084
- }
8085
-
8086
- function genUnevaluatedItemsDynamicC(schema, v, pathExpr, lines, ctx, schemaPrefix, fail) {
8087
- if (schema.$dynamicRef !== undefined || schema.$recursiveRef !== undefined) throw DECLINE
8088
- if (Array.isArray(schema.items) || schema.additionalItems !== undefined) throw DECLINE
8089
- const inline = inlineUnevalItemsBlock(schema, v, pathExpr, ctx, schemaPrefix, fail)
8090
- if (inline !== null) { lines.push(`if(Array.isArray(${v})){${inline}}`); return }
8091
- annItemHelpers(ctx)
8092
- const ui = ctx.varCounter++
8093
- const S = `_es${ui}`
8094
- const body = [`const ${S}={n:0,x:null}`]
8095
- addLocalItemAnnotations(schema, v, S, body, ctx)
8096
- for (const b of schema.allOf || []) body.push(`${emitAnnotI(b, ctx)}(${v},${S})`)
8097
- if (Array.isArray(schema.anyOf)) for (const b of schema.anyOf) body.push(`${emitAnnotI(b, ctx)}(${v},${S})`)
8098
- if (Array.isArray(schema.oneOf)) {
8099
- body.push(`{const _n0=${S}.n,_x0=${S}.x===null?0:${S}.x.length;let _c=0;${schema.oneOf.map((b) => `if(${emitAnnotI(b, ctx)}(${v},${S}))_c++;`).join('')}if(_c!==1)_rbI(${S},_n0,_x0)}`)
8100
- }
8101
- if (schema.if !== undefined) {
8102
- const thenCall = schema.then !== undefined ? `${emitAnnotI(schema.then, ctx)}(${v},${S})` : ''
8103
- const elseCall = schema.else !== undefined ? `${emitAnnotI(schema.else, ctx)}(${v},${S})` : ''
8104
- body.push(`if(${emitAnnotI(schema.if, ctx)}(${v},${S})){${thenCall}}else{${elseCall}}`)
8105
- }
8106
- if (typeof schema.$ref === 'string') body.push(`${emitAnnotI(refTargetFor(schema.$ref, ctx), ctx)}(${v},${S})`)
8107
- const iv = `_ix${ui}`
8108
- const ui2 = schema.unevaluatedItems
8109
- if (ui2 === false) {
8110
- const failI = fail('unevaluatedItems', 'unevaluatedItems', `{limit:${iv}}`, `'must NOT have more than '+${iv}+' items'`)
8111
- body.push(`for(let ${iv}=0;${iv}<${v}.length;${iv}++){if(_hI(${S},${iv}))continue;${failI};break}`)
8112
- } else if (typeof ui2 === 'object' && ui2 !== null) {
8113
- const pe = pathExpr ? `${pathExpr}+'/'+${iv}` : `'/'+${iv}`
8114
- const sub = []
8115
- genCodeC(ui2, `${v}[${iv}]`, pe, sub, ctx, schemaPrefix + '/unevaluatedItems')
8116
- if (sub.length) body.push(`for(let ${iv}=0;${iv}<${v}.length;${iv}++){if(_hI(${S},${iv}))continue;${sub.join('\n ')}\n }`)
8117
- } else {
8118
- throw DECLINE
8119
- }
8120
- lines.push(`if(Array.isArray(${v})){${body.join(';')}}`)
8121
- }
8122
-
8123
- function annItemHelpers(ctx) {
8124
- if (ctx.annItemHelpers) return
8125
- ctx.annItemHelpers = true
8126
- ;(ctx.preamble || ctx.helperCode).push(
8127
- 'function _aI(S,i){if(i<S.n)return;if(i===S.n){S.n=i+1;return}(S.x||(S.x=[])).push(i)}' +
8128
- 'function _mrI(S,f,t){if(f>=t)return;if(f<=S.n){if(t>S.n)S.n=t;return}const x=S.x||(S.x=[]);for(let i=f;i<t;i++)x.push(i)}' +
8129
- 'function _hI(S,i){if(i<S.n)return true;const x=S.x;if(x===null)return false;for(let j=0;j<x.length;j++)if(x[j]===i)return true;return false}' +
8130
- 'function _rbI(S,n,l){S.n=n;if(S.x!==null)S.x.length=l}' +
8131
- 'function _anIT(){return true}function _anIF(){return false}')
8132
- }
8133
-
8134
- // The positions a node's own keywords evaluate, marked on `S`, for an array.
8135
- function addLocalItemAnnotations(node, v, S, out, ctx) {
8136
- if (Array.isArray(node.items) || node.additionalItems !== undefined) throw DECLINE
8137
- const prefix = Array.isArray(node.prefixItems) ? node.prefixItems.length : 0
8138
- if (prefix) out.push(`_mrI(${S},0,Math.min(${v}.length,${prefix}))`)
8139
- if (node.items !== undefined) out.push(`_mrI(${S},${prefix},${v}.length)`)
8140
- if (node.contains !== undefined) {
8141
- const cl = []
8142
- nestedGenCode(node.contains, '_cv', cl, ctx)
8143
- const ci = ctx.varCounter++
8144
- ;(ctx.preamble || ctx.helperCode).push(`function _cI${ci}(_cv){${cl.join('\n ')}\n return true}`)
8145
- out.push(`for(let _i=0;_i<${v}.length;_i++)if(_cI${ci}(${v}[_i]))_aI(${S},_i)`)
8146
- }
8147
- }
8148
-
8149
- // The keywords an item annotation function evaluates through its children;
8150
- // the rest of a subschema is checked by a plain verdict.
8151
- const ANNOT_ITEM_APPLICATORS = new Set(['allOf', 'anyOf', 'oneOf', 'if', 'then', 'else', '$ref', 'not', 'unevaluatedItems'])
8152
-
8153
- // A function `(value, S) => valid` for one subschema, marking on S the array
8154
- // positions it evaluated when it passes, and leaving S as it was when it does
8155
- // not. A subschema with an unevaluatedItems of its own evaluates every
8156
- // position once that passes, so it works on a record of its own and then
8157
- // marks them all.
8158
- function emitAnnotI(sub, ctx) {
8159
- if (sub === true) return '_anIT'
8160
- if (sub === false) return '_anIF'
8161
- if (sub === null || typeof sub !== 'object') throw DECLINE
8162
- if (!ctx.annFnsI) ctx.annFnsI = new Map()
8163
- const hit = ctx.annFnsI.get(sub)
8164
- if (hit !== undefined) {
8165
- if (hit === null) throw DECLINE
8166
- return hit
8167
- }
8168
- if (sub.$dynamicRef !== undefined || sub.$recursiveRef !== undefined) throw DECLINE
8169
- if (Array.isArray(sub.items) || sub.additionalItems !== undefined) throw DECLINE
8170
- // unevaluatedProperties reads the applicators too, which the local verdict
8171
- // below leaves out.
8172
- if (sub.unevaluatedProperties !== undefined && sub.unevaluatedProperties !== true) throw DECLINE
8173
- ctx.annFnsI.set(sub, null)
8174
- annItemHelpers(ctx)
8175
- const name = `_ai${ctx.varCounter++}`
8176
- const local = {}
8177
- for (const k of Object.keys(sub)) if (!ANNOT_ITEM_APPLICATORS.has(k)) setOwn(local, k, sub[k])
8178
- const vl = []
8179
- nestedGenCode(local, '_av', vl, ctx)
8180
- const decls = [`function ${name}_v(_av){${vl.join('\n ')}\n return true}`]
8181
- const body = [`if(!${name}_v(d))return false`]
8182
- if (sub.not !== undefined) {
8183
- const nl = []
8184
- nestedGenCode(sub.not, '_nv', nl, ctx)
8185
- decls.push(`function ${name}_n(_nv){${nl.join('\n ')}\n return true}`)
8186
- body.push(`if(${name}_n(d))return false`)
8187
- }
8188
- const own = sub.unevaluatedItems !== undefined && sub.unevaluatedItems !== true
8189
- const R = own ? '_R' : 'S'
8190
- body.push(own ? `const _R={n:0,x:null}` : `const _n0=S.n,_x0=S.x===null?0:S.x.length`)
8191
- const failOut = own ? 'return false' : '{_rbI(S,_n0,_x0);return false}'
8192
- const locals = []
8193
- addLocalItemAnnotations(sub, 'd', R, locals, ctx)
8194
- if (locals.length) body.push(`if(Array.isArray(d)){${locals.join(';')}}`)
8195
- for (const b of sub.allOf || []) body.push(`if(!${emitAnnotI(b, ctx)}(d,${R}))${failOut}`)
8196
- if (Array.isArray(sub.anyOf)) body.push(`{let _a=false;${sub.anyOf.map((b) => `if(${emitAnnotI(b, ctx)}(d,${R}))_a=true;`).join('')}if(!_a)${failOut}}`)
8197
- if (Array.isArray(sub.oneOf)) body.push(`{let _c=0;${sub.oneOf.map((b) => `if(${emitAnnotI(b, ctx)}(d,${R})&&++_c>1)${failOut}`).join('')}if(_c!==1)${failOut}}`)
8198
- if (sub.if !== undefined) {
8199
- const thenCheck = sub.then !== undefined ? `if(!${emitAnnotI(sub.then, ctx)}(d,${R}))${failOut}` : ''
8200
- const elseCheck = sub.else !== undefined ? `if(!${emitAnnotI(sub.else, ctx)}(d,${R}))${failOut}` : ''
8201
- body.push(`if(${emitAnnotI(sub.if, ctx)}(d,${R})){${thenCheck}}else{${elseCheck}}`)
8202
- }
8203
- if (typeof sub.$ref === 'string') body.push(`if(!${emitAnnotI(refTargetFor(sub.$ref, ctx), ctx)}(d,${R}))${failOut}`)
8204
- if (own) {
8205
- const u = sub.unevaluatedItems
8206
- if (u === false) {
8207
- body.push(`if(Array.isArray(d)){for(let _i=0;_i<d.length;_i++)if(!_hI(_R,_i))return false}`)
8208
- } else if (typeof u === 'object' && u !== null) {
8209
- const ul = []
8210
- nestedGenCode(u, '_uv', ul, ctx)
8211
- decls.push(`function ${name}_u(_uv){${ul.join('\n ')}\n return true}`)
8212
- body.push(`if(Array.isArray(d)){for(let _i=0;_i<d.length;_i++)if(!_hI(_R,_i)&&!${name}_u(d[_i]))return false}`)
8213
- } else {
8214
- throw DECLINE
8215
- }
8216
- body.push(`if(Array.isArray(d))_mrI(S,0,d.length)`)
8217
- }
8218
- // `unevaluatedItems: true` evaluates every position once the subschema
8219
- // has passed, with nothing to check; marked last, so a failure above has
8220
- // nothing of it to undo.
8221
- if (sub.unevaluatedItems === true) body.push(`if(Array.isArray(d))_mrI(S,0,d.length)`)
8222
- decls.push(`function ${name}(d,S){${body.join(';')};return true}`)
8223
- ;(ctx.preamble || ctx.helperCode).push(decls.join(''))
8224
- ctx.annFnsI.set(sub, name)
8225
- return name
8226
- }
8227
-
8228
- function genUnevaluatedFalseC(schema, v, pathExpr, lines, ctx, schemaPrefix, fail, isObj) {
8229
- const propKeysU = Object.keys(schema.properties || {})
8230
- const ui = ctx.varCounter++
8231
- const failU = fail('unevaluatedProperties', 'unevaluatedProperties', `{unevaluatedProperty:_k${ui}[_i]}`, "'must NOT have unevaluated properties'")
8232
- let innerU
8233
- if (propKeysU.length === 0) {
8234
- innerU = `const _k${ui}=Object.keys(${v});for(let _i=0;_i<_k${ui}.length;_i++){${failU}}`
8235
- } else if (propKeysU.length <= 8) {
8236
- const checksU = propKeysU.map(k => `_k${ui}[_i]!==${JSON.stringify(k)}`).join('&&')
8237
- innerU = `const _k${ui}=Object.keys(${v});for(let _i=0;_i<_k${ui}.length;_i++)if(${checksU}){${failU}}`
8238
- } else {
8239
- ctx.closureVars.push(`_a${ui}`)
8240
- ctx.closureVals.push(new Set(propKeysU))
8241
- innerU = `const _k${ui}=Object.keys(${v});for(let _i=0;_i<_k${ui}.length;_i++)if(!_a${ui}.has(_k${ui}[_i])){${failU}}`
8242
- }
8243
- lines.push(isObj ? `{${innerU}}` : `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){${innerU}}`)
8244
- }
8245
-
8246
- function genUnevaluatedItemsFalseC(schema, v, pathExpr, lines, ctx, schemaPrefix, fail) {
8247
- const itemsIsPrefixC = Array.isArray(schema.items)
8248
- const allEvalC = schema.items !== undefined && !itemsIsPrefixC
8249
- if (!allEvalC) {
8250
- const plenC = itemsIsPrefixC ? schema.items.length : (Array.isArray(schema.prefixItems) ? schema.prefixItems.length : 0)
8251
- const failI = fail('unevaluatedItems', 'unevaluatedItems', `{limit:${plenC}}`, `'must NOT have more than ${plenC} items'`)
8252
- lines.push(`if(Array.isArray(${v})&&${v}.length>${plenC}){${failI}}`)
8253
- }
8254
- }
8255
-
8256
- // The error generator's per-key loop (genAdditionalSchemaE), in the combined
8257
- // generator: the subschema compiled by genCodeC at each additional key, with
8258
- // that key's path. Patterns go through the same matchers patternProperties
8259
- // uses here.
8260
- function genAdditionalSchemaC(schema, v, pathExpr, lines, ctx, schemaPrefix, isObj) {
8261
- const ki = ctx.varCounter++
8262
- const kVar = `_ak${ki}`
8263
- const known = Object.keys(schema.properties || {})
8264
- const conds = []
8265
- if (known.length) {
8266
- const set = `_akn${ki}`
8267
- ctx.closureVars.push(set)
8268
- ctx.closureVals.push(new Set(known))
8269
- conds.push(`!${set}.has(${kVar})`)
8270
- }
8271
- const pats = []
8272
- for (const pat of Object.keys(schema.patternProperties || {})) {
8273
- const fast = fastPrefixCheck(pat, kVar)
8274
- if (fast) { pats.push(fast); continue }
8275
- const ri = ctx.varCounter++
8276
- ctx.closureVars.push(`_re${ri}`)
8277
- ctx.closureVals.push(safeReClosure(ctx, pat))
8278
- pats.push(`_re${ri}.test(${kVar})`)
8279
- }
8280
- if (pats.length) conds.push(`!(${pats.join('||')})`)
8281
- const guard = isObj ? '' : `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v}))`
8282
- const keep = conds.length ? `if(${conds.join('&&')})` : ''
8283
- const pe = emitPtrEsc(ctx)
8284
- const p = pathExpr ? `${pathExpr}+'/'+${pe}(${kVar})` : `'/'+${pe}(${kVar})`
8285
- const sub = []
8286
- genCodeC(schema.additionalProperties, `${v}[${kVar}]`, p, sub, ctx, schemaPrefix + '/additionalProperties')
8287
- if (sub.length === 0) return
8288
- lines.push(`${guard}{for(const ${kVar} in ${v}){${keep}{${sub.join('\n ')}\n }}}`)
8289
- }
8290
-
8291
- function genPatternPropertiesC(schema, v, pathExpr, lines, ctx, schemaPrefix, fail, isObj) {
8292
- let handledPropertyNames = false
8293
- const ppEntries = Object.entries(schema.patternProperties)
8294
- const pn = isSimplePN(schema.propertyNames) ? schema.propertyNames : null
8295
- const pi = ctx.varCounter++
8296
-
8297
- // Build pattern matchers: prefer charCodeAt for simple prefixes
8298
- const matchers = []
8299
- for (const [pat] of ppEntries) {
8300
- const kVar = `_k${pi}`
8301
- const fast = fastPrefixCheck(pat, kVar)
8302
- if (fast) {
8303
- matchers.push({ check: fast })
8304
- } else {
8305
- const ri = ctx.varCounter++
8306
- ctx.closureVars.push(`_re${ri}`)
8307
- ctx.closureVals.push(safeReClosure(ctx, pat))
8308
- matchers.push({ check: `_re${ri}.test(_k${pi})` })
8309
- }
8310
- }
8311
-
8312
- const guard = isObj ? '' : `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v}))`
8313
- const kVar = `_k${pi}`
8314
-
8315
- if (schema.additionalProperties === false) {
8316
- handledPropertyNames = !!pn
8317
- const propKeys = Object.keys(schema.properties || {})
8318
- // Inline key comparison for small property sets
8319
- const keyCheck = propKeys.length <= 8
8320
- ? propKeys.map(k => `${kVar}===${JSON.stringify(k)}`).join('||')
8321
- : null
8322
- if (!keyCheck) {
8323
- const allowedSet = `_as${pi}`
8324
- ctx.closureVars.push(allowedSet)
8325
- ctx.closureVals.push(new Set(propKeys))
8326
- }
8327
-
8328
- lines.push(`${guard}{for(const ${kVar} in ${v}){`)
8329
- // propertyNames checks (merged)
8330
- if (pn) {
8331
- if (pn.minLength !== undefined) lines.push(`if(${kVar}.length<${pn.minLength}){${fail('minLength', 'propertyNames/minLength', `{limit:${pn.minLength}}`, `'must NOT have fewer than ${pn.minLength} characters'`)}}`)
8332
- if (pn.maxLength !== undefined) lines.push(`if(${kVar}.length>${pn.maxLength}){${fail('maxLength', 'propertyNames/maxLength', `{limit:${pn.maxLength}}`, `'must NOT have more than ${pn.maxLength} characters'`)}}`)
8333
- if (pn.pattern) {
8334
- const fast = fastPrefixCheck(pn.pattern, kVar)
8335
- if (fast) {
8336
- lines.push(`if(!(${fast})){${fail('pattern', 'propertyNames/pattern', `{pattern:${JSON.stringify(pn.pattern)}}`, JSON.stringify(`must match pattern "${pn.pattern}"`))}}`)
8337
- } else {
8338
- const ri = ctx.varCounter++
8339
- ctx.closureVars.push(`_re${ri}`)
8340
- ctx.closureVals.push(safeReClosure(ctx, pn.pattern))
8341
- lines.push(`if(!_re${ri}.test(${kVar})){${fail('pattern', 'propertyNames/pattern', `{pattern:${JSON.stringify(pn.pattern)}}`, JSON.stringify(`must match pattern "${pn.pattern}"`))}}`)
8342
- }
8343
- }
8344
- if (pn.const !== undefined) lines.push(`if(${kVar}!==${JSON.stringify(pn.const)}){${fail('const', 'propertyNames/const', `{allowedValue:${JSON.stringify(pn.const)}}`, "'must be equal to constant'")}}`)
8345
- if (pn.enum) {
8346
- const ei = ctx.varCounter++
8347
- ctx.closureVars.push(`_es${ei}`)
8348
- ctx.closureVals.push(new Set(pn.enum))
8349
- lines.push(`if(!_es${ei}.has(${kVar})){${fail('enum', 'propertyNames/enum', `{allowedValues:${JSON.stringify(pn.enum)}}`, "'must be equal to one of the allowed values'")}}`)
8350
- }
8351
- }
8352
- const matchExpr = keyCheck || `_as${pi}.has(${kVar})`
8353
- lines.push(`let _m${pi}=${matchExpr}`)
8354
- for (let i = 0; i < ppEntries.length; i++) {
8355
- // The subschema is generated in place so its own keywords report
8356
- // the real instance path and schema pointer.
8357
- lines.push(`if(${matchers[i].check}){_m${pi}=true;{const _ppv${pi}_${i}=${v}[${kVar}]`)
8358
- genCodeC(ppEntries[i][1], `_ppv${pi}_${i}`, childPathDynExpr(pathExpr, `${emitPtrEsc(ctx)}(${kVar})`), lines, ctx, schemaPrefix + '/patternProperties/' + ptrSeg(ppEntries[i][0]))
8359
- lines.push(`}}`)
8360
- }
8361
- lines.push(`if(!_m${pi}){${fail('additionalProperties', 'additionalProperties', `{additionalProperty:${kVar}}`, "'must NOT have additional properties'")}}`)
8362
- lines.push(`}}`)
8363
- } else {
8364
- handledPropertyNames = !!pn
8365
- lines.push(`${guard}{for(const ${kVar} in ${v}){`)
8366
- if (pn) {
8367
- if (pn.minLength !== undefined) lines.push(`if(${kVar}.length<${pn.minLength}){${fail('minLength', 'propertyNames/minLength', `{limit:${pn.minLength}}`, `'must NOT have fewer than ${pn.minLength} characters'`)}}`)
8368
- if (pn.maxLength !== undefined) lines.push(`if(${kVar}.length>${pn.maxLength}){${fail('maxLength', 'propertyNames/maxLength', `{limit:${pn.maxLength}}`, `'must NOT have more than ${pn.maxLength} characters'`)}}`)
8369
- if (pn.pattern) {
8370
- const fast = fastPrefixCheck(pn.pattern, kVar)
8371
- if (fast) {
8372
- lines.push(`if(!(${fast})){${fail('pattern', 'propertyNames/pattern', `{pattern:${JSON.stringify(pn.pattern)}}`, JSON.stringify(`must match pattern "${pn.pattern}"`))}}`)
8373
- } else {
8374
- const ri = ctx.varCounter++
8375
- ctx.closureVars.push(`_re${ri}`)
8376
- ctx.closureVals.push(safeReClosure(ctx, pn.pattern))
8377
- lines.push(`if(!_re${ri}.test(${kVar})){${fail('pattern', 'propertyNames/pattern', `{pattern:${JSON.stringify(pn.pattern)}}`, JSON.stringify(`must match pattern "${pn.pattern}"`))}}`)
8378
- }
8379
- }
8380
- if (pn.const !== undefined) lines.push(`if(${kVar}!==${JSON.stringify(pn.const)}){${fail('const', 'propertyNames/const', `{allowedValue:${JSON.stringify(pn.const)}}`, "'must be equal to constant'")}}`)
8381
- if (pn.enum) {
8382
- const ei = ctx.varCounter++
8383
- ctx.closureVars.push(`_es${ei}`)
8384
- ctx.closureVals.push(new Set(pn.enum))
8385
- lines.push(`if(!_es${ei}.has(${kVar})){${fail('enum', 'propertyNames/enum', `{allowedValues:${JSON.stringify(pn.enum)}}`, "'must be equal to one of the allowed values'")}}`)
8386
- }
8387
- }
8388
- for (let i = 0; i < ppEntries.length; i++) {
8389
- lines.push(`if(${matchers[i].check}){const _ppv${pi}_${i}=${v}[${kVar}]`)
8390
- genCodeC(ppEntries[i][1], `_ppv${pi}_${i}`, childPathDynExpr(pathExpr, `${emitPtrEsc(ctx)}(${kVar})`), lines, ctx, schemaPrefix + '/patternProperties/' + ptrSeg(ppEntries[i][0]))
8391
- lines.push(`}`)
8392
- }
8393
- lines.push(`}}`)
8394
- }
8395
- return handledPropertyNames
8396
- }
8397
-
8398
- function genDependentSchemasC(schema, v, pathExpr, lines, ctx, schemaPrefix) {
8399
- for (const [key, depSchema] of Object.entries(schema.dependentSchemas)) {
8400
- lines.push(`if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})&&${ownKeyExpr(ctx, v, key)}){`)
8401
- genCodeC(depSchema, v, pathExpr, lines, ctx, schemaPrefix+'/dependentSchemas/'+ptrSeg(key))
8402
- lines.push(`}`)
8403
- }
8404
- }
8405
-
8406
- function genPropertyNamesFalseC(schema, v, pathExpr, lines, ctx, schemaPrefix) {
8407
- const ki = ctx.varCounter++
8408
- const p = pathExpr || '""'
8409
- lines.push(`;if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){for(const _k${ki} in ${v}){${errPushC(ctx, 'not', `${schemaPrefix}/propertyNames`, pathExpr, '{}', "'boolean schema is false'", v, `{keyword:'not',instancePath:${p},schemaPath:'${schemaPrefix}/propertyNames'${ordinalField(ctx, `${schemaPrefix}/propertyNames`)},params:{},message:'boolean schema is false'}`)}}}`)
8410
- }
8411
-
8412
- function genPropertyNamesC(schema, v, pathExpr, lines, ctx, schemaPrefix, fail) {
8413
- const pn = schema.propertyNames
8414
- const ki = ctx.varCounter++
8415
- // In the rich function an error carries the value it was raised on, and
8416
- // here that would be the key where the interpreter's error points at the
8417
- // object, so that one is left to the plain function and enrichment.
8418
- if (!isSimplePN(pn) && ctx.rich) throw DECLINE
8419
- lines.push(`if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){for(const _k${ki} in ${v}){`)
8420
- if (!isSimplePN(pn)) {
8421
- genCodeC(pn, `_k${ki}`, pathExpr, lines, ctx, schemaPrefix + '/propertyNames')
8422
- lines.push(`}}`)
8423
- return
8424
- }
8425
- if (pn.minLength !== undefined) {
8426
- lines.push(`if(_k${ki}.length<${pn.minLength}){${fail('minLength', 'propertyNames/minLength', `{limit:${pn.minLength}}`, `'must NOT have fewer than ${pn.minLength} characters'`)}}`)
8427
- }
8428
- if (pn.maxLength !== undefined) {
8429
- lines.push(`if(_k${ki}.length>${pn.maxLength}){${fail('maxLength', 'propertyNames/maxLength', `{limit:${pn.maxLength}}`, `'must NOT have more than ${pn.maxLength} characters'`)}}`)
8430
- }
8431
- if (pn.pattern) {
8432
- const ri = ctx.varCounter++
8433
- ctx.closureVars.push(`_re${ri}`)
8434
- ctx.closureVals.push(safeReClosure(ctx, pn.pattern))
8435
- lines.push(`if(!_re${ri}.test(_k${ki})){${fail('pattern', 'propertyNames/pattern', `{pattern:${JSON.stringify(pn.pattern)}}`, JSON.stringify(`must match pattern "${pn.pattern}"`))}}`)
8436
- }
8437
- if (pn.const !== undefined) {
8438
- lines.push(`if(_k${ki}!==${JSON.stringify(pn.const)}){${fail('const', 'propertyNames/const', `{allowedValue:${JSON.stringify(pn.const)}}`, "'must be equal to constant'")}}`)
8439
- }
8440
- if (pn.enum) {
8441
- const ei = ctx.varCounter++
8442
- ctx.closureVars.push(`_es${ei}`)
8443
- ctx.closureVals.push(new Set(pn.enum))
8444
- lines.push(`if(!_es${ei}.has(_k${ki})){${fail('enum', 'propertyNames/enum', `{allowedValues:${JSON.stringify(pn.enum)}}`, "'must be equal to one of the allowed values'")}}`)
8445
- }
8446
- lines.push(`}}`)
8447
- }
8448
-
8449
- function genPrefixItemsC(schema, v, pathExpr, lines, ctx, schemaPrefix) {
8450
- for (let i = 0; i < schema.prefixItems.length; i++) {
8451
- const childPath = childPathExpr(pathExpr, String(i))
8452
- lines.push(`if(Array.isArray(${v})&&${v}.length>${i}){`)
8453
- genCodeC(schema.prefixItems[i], `${v}[${i}]`, childPath, lines, ctx, schemaPrefix+'/prefixItems/'+i)
8454
- lines.push(`}`)
8455
- }
8456
- }
8457
-
8458
- function genContainsC(schema, v, pathExpr, lines, ctx, schemaPrefix, fail) {
8459
- const ci = ctx.varCounter++
8460
- const subLines = []
8461
- nestedGenCode(schema.contains, `_cv`, subLines, ctx)
8462
- const fnBody = subLines.length === 0 ? `return true` : `${subLines.join(';')};return true`
8463
- const minC = schema.minContains !== undefined ? schema.minContains : 1
8464
- const maxC = schema.maxContains
8465
- lines.push(`if(Array.isArray(${v})){const _cf${ci}=function(_cv){${fnBody}};let _cc${ci}=0;for(let _ci${ci}=0;_ci${ci}<${v}.length;_ci${ci}++){if(_cf${ci}(${v}[_ci${ci}]))_cc${ci}++}`)
8466
- lines.push(`if(_cc${ci}<${minC}){${fail('contains', 'contains', `{minContains:${minC}}`, `'must contain at least ${minC} valid item(s)'`)}}`)
8467
- if (maxC !== undefined) lines.push(`if(_cc${ci}>${maxC}){${fail('contains', 'contains', `{minContains:${minC},maxContains:${maxC}}`, `'must NOT contain more than ${maxC} valid item(s)'`)}}`)
8468
- lines.push(`}`)
8469
- }
8470
-
8471
- function genAnyOfC(schema, v, pathExpr, lines, ctx, schemaPrefix, fail) {
8472
- // The collapsed branch error the other engines report (ATA4001-4003),
8473
- // built by the error generator's emitter; see compileToJSCombined.
8474
- if (ctx.inCombined) return emitBranchCollapse(schema.anyOf, 'anyOf', v, pathExpr, lines, ctx, schemaPrefix)
8475
- const fi = ctx.varCounter++
8476
- const fns = schema.anyOf.map(sub => { const sl = []; nestedGenCode(sub, '_av', sl, ctx); return sl.length === 0 ? `function(_av){return true}` : `function(_av){${sl.join(';')};return true}` })
8477
- lines.push(`{const _af${fi}=[${fns.join(',')}];let _am=false;for(let _ai=0;_ai<_af${fi}.length;_ai++){if(_af${fi}[_ai](${v})){_am=true;break}}if(!_am){${fail('anyOf', 'anyOf', '{}', "'must match a schema in anyOf'")}}}`)
8478
- }
8479
-
8480
- function genOneOfC(schema, v, pathExpr, lines, ctx, schemaPrefix, fail) {
8481
- // The collapsed branch error the other engines report (ATA4001-4003),
8482
- // built by the error generator's emitter; see compileToJSCombined.
8483
- if (ctx.inCombined) return emitBranchCollapse(schema.oneOf, 'oneOf', v, pathExpr, lines, ctx, schemaPrefix)
8484
- const fi = ctx.varCounter++
8485
- const fns = schema.oneOf.map(sub => { const sl = []; nestedGenCode(sub, '_ov', sl, ctx); return sl.length === 0 ? `function(_ov){return true}` : `function(_ov){${sl.join(';')};return true}` })
8486
- lines.push(`{const _of${fi}=[${fns.join(',')}];let _oc=0;for(let _oi=0;_oi<_of${fi}.length;_oi++){if(_of${fi}[_oi](${v}))_oc++;if(_oc>1)break}if(_oc!==1){${fail('oneOf', 'oneOf', '{}', "'must match exactly one schema in oneOf'")}}}`)
8487
- }
8488
-
8489
- function genNotC(schema, v, pathExpr, lines, ctx, schemaPrefix, fail) {
8490
- const sl = []; nestedGenCode(schema.not, '_nv', sl, ctx)
8491
- const nfn = sl.length === 0 ? `function(_nv){return true}` : `function(_nv){${sl.join(';')};return true}`
8492
- const fi = ctx.varCounter++
8493
- lines.push(`{const _nf${fi}=${nfn};if(_nf${fi}(${v})){${fail('not', 'not', '{}', "'must NOT be valid'")}}}`)
8494
- }
8495
-
8496
- function genIfC(schema, v, pathExpr, lines, ctx, schemaPrefix) {
8497
- const sl = []; nestedGenCode(schema.if, '_iv', sl, ctx)
8498
- const fi = ctx.varCounter++
8499
- const ifFn = sl.length === 0 ? `function(_iv){return true}` : `function(_iv){${sl.join(';')};return true}`
8500
- lines.push(`{const _if${fi}=${ifFn}`)
8501
- if (schema.then !== undefined) { lines.push(`if(_if${fi}(${v})){`); genCodeC(schema.then, v, pathExpr, lines, ctx, schemaPrefix+'/then'); lines.push(`}`) }
8502
- if (schema.else !== undefined) { lines.push(`${schema.then !== undefined ? 'else' : `if(!_if${fi}(${v}))`}{`); genCodeC(schema.else, v, pathExpr, lines, ctx, schemaPrefix+'/else'); lines.push(`}`) }
8503
- lines.push(`}`)
8504
- }
8505
-
8506
5157
  // Collect statically-known evaluated properties/items from a schema.
8507
5158
  // Returns { props: string[], items: number|null, allProps: bool, allItems: bool, dynamic: bool }
8508
5159
  function collectEvaluated(schema, schemaMap, rootDefs) {
@@ -8638,9 +5289,22 @@ function _collectEval(schema, result, defs, schemaMap, refStack, isRoot) {
8638
5289
 
8639
5290
  // not → contributes nothing (spec: annotations from not are discarded)
8640
5291
  }
5292
+ // The error generator and the one-pass (combined) generator live in
5293
+ // lib/js-compiler-errors.js, read on the first call that needs them: a
5294
+ // validator's errors are compiled when they are first read, and most
5295
+ // processes load this module to answer verdicts. The two files were one; the
5296
+ // generators share this module's helpers through _internal below, which is
5297
+ // an implementation detail and not part of the package's interface.
5298
+ let _errorsModule = null
5299
+ function errorsModule () { return _errorsModule || (_errorsModule = require('./js-compiler-errors')) }
5300
+ function compileToJSCodegenWithErrors (schema, schemaMap, userFormats, sourceOpts) { return errorsModule().compileToJSCodegenWithErrors(schema, schemaMap, userFormats, sourceOpts) }
5301
+ function compileToJSCombined (schema, VALID_RESULT, schemaMap, userFormats, opts) { return errorsModule().compileToJSCombined(schema, VALID_RESULT, schemaMap, userFormats, opts) }
8641
5302
 
8642
- module.exports = { compileToJS, compileToJSCodegen, compileToJSCodegenWithErrors, compileToJSCombined, collectEvaluated, unevalContributions, expandedChars, AJV_MESSAGES, _setCodeBudget }
5303
+
5304
+ module.exports = { compileToJS, compileToJSCodegen, compileToJSCodegenWithErrors, compileToJSCombined, collectEvaluated, unevalContributions, expandedChars, AJV_MESSAGES, _setCodeBudget, _helperSources: { DEQ_HELPER, PTR_ESC_HELPER, OWN_HELPER_CODE } }
8643
5305
  // For tests: the dynamic-scope unrolling, to hold its output to the interpreter.
8644
5306
  Object.defineProperty(module.exports, '_expandDynamicScopes', { value: expandDynamicScopes, enumerable: false })
8645
5307
  // For tests/test_custom_keyword_codegen.js, which holds these copies to the interpreter's.
8646
5308
  Object.defineProperty(module.exports, '_kwTypeBits', { value: { kwTypeBit, kwDataBits, KW_T_ANY }, enumerable: false })
5309
+
5310
+ module.exports._internal = { hasConstraints, assertEmitted, prepareForCodegen, sharedCodegenGate, codegenSafe, isSimplePN, hasUnresolvableRef, needsBaseTracking, _cpLen, sharedRt, DECLINE, bindRt, compileSafe, emitGuardState, withPlain, REF_NEUTRAL_SIBLINGS, resolveCrossSchemaRef, enumCondition, emitDeq, emitConstant, ownGuard, ownKeyExpr, multipleOfBad, compilePatternInline, useSafeEngine, reFlags, FORMAT_CODEGEN, PROTO_NAMES, setOwn, nestedGenCode, compileToJSCodegen, SKIP_VALUE_KEYS, skipString, CYCLE_DEPTH, cyclicDefNames, sharedDefNames, reFlagArg, hoistOnce, PTR_ESC_HELPER, unevalContributions, emitUq, uqSources, safeReClosure, fastPrefixCheck, replaceTopLevel, refTargetFor, genCode, addLocalAnnotations, addTopApplicatorAnnotations, protoIs, ANNOT_APPLICATORS, SIZE_SKIPS, emitRootGuard, emitGuardedRun }