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