ata-validator 1.41.0 → 1.43.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/index.js +124 -2
- package/lib/branch-collapse.js +32 -17
- package/lib/enrich-error.js +46 -30
- package/lib/enrich-site.js +58 -0
- package/lib/interpreter.js +6 -0
- package/lib/js-compiler.js +1391 -171
- package/lib/plan-compiler.js +7 -2
- package/lib/rejections.js +20 -3
- package/lib/scan-compiler.js +210 -25
- package/lib/scan-runtime.js +13 -1
- package/lib/validator-core.js +122 -14
- package/lib/version.js +1 -1
- package/package.json +9 -9
package/lib/js-compiler.js
CHANGED
|
@@ -101,6 +101,38 @@ function emitUq(ctx) {
|
|
|
101
101
|
return '_uq'
|
|
102
102
|
}
|
|
103
103
|
|
|
104
|
+
// The error and combined generators need the duplicate pair, not only the
|
|
105
|
+
// verdict. `_uqp` returns the index of the duplicate (-1 when every item is
|
|
106
|
+
// distinct) and leaves the item it repeats in `_uqpI`: the first item that has
|
|
107
|
+
// a later equal, paired with its first later equal. That is the pair the
|
|
108
|
+
// interpreted engine reports, and the generators used to report a different
|
|
109
|
+
// one (the earliest repeat, paired with what it repeats), so the same document
|
|
110
|
+
// named a different pair depending on the engine that answered. Short arrays
|
|
111
|
+
// compare pairwise and allocate nothing; longer ones key a Map by value, or by
|
|
112
|
+
// canonical string once an object appears. Building a canonical string for
|
|
113
|
+
// every item on every call, as the generators did, made reading a uniqueItems
|
|
114
|
+
// error up to ten times the cost of the verdict.
|
|
115
|
+
const UQP_HELPER = "var _uqpI=0;function _uqp(a){var n=a.length,i,k;if(n<2)return -1;if(n<=12){for(i=0;i<n-1;i++)for(k=i+1;k<n;k++)if(_deq(a[i],a[k])){_uqpI=i;return k}return -1}var prim=true;for(i=0;i<n;i++){var x=a[i];if(x!==null&&typeof x==='object'){prim=false;break}}var m=new Map(),bi=-1,bj=-1;for(i=0;i<n;i++){var c=prim?a[i]:_cn(a[i]);var p=m.get(c);if(p===undefined)m.set(c,i);else if(p>=0){if(bi<0||p<bi){bi=p;bj=i}m.set(c,-1)}}if(bi<0)return -1;_uqpI=bi;return bj}"
|
|
116
|
+
function emitUqp(ctx) {
|
|
117
|
+
emitUq(ctx)
|
|
118
|
+
hoistOnce(ctx, '_uqpHoisted', UQP_HELPER)
|
|
119
|
+
return '_uqp'
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// An object or array a const error reports as `allowedValue`, parsed once per
|
|
123
|
+
// compiled function rather than on every error: parsing it per error made
|
|
124
|
+
// reading a const error two to five times what the check costs. A copy of its
|
|
125
|
+
// own, not the compared constant, so an error a caller edits cannot change
|
|
126
|
+
// what the validator compares against. Declared in the function's helper
|
|
127
|
+
// code, so standalone output carries it the same way.
|
|
128
|
+
function emitParamConstant(ctx, value) {
|
|
129
|
+
const name = `_pc${ctx.varCounter++}`
|
|
130
|
+
const decl = `const ${name}=JSON.parse(${JSON.stringify(JSON.stringify(value))})`
|
|
131
|
+
if (ctx.preamble) ctx.preamble.push(decl)
|
|
132
|
+
else ctx.helperCode.push(decl)
|
|
133
|
+
return name
|
|
134
|
+
}
|
|
135
|
+
|
|
104
136
|
// The compared value, parsed once at compile time. `JSON.parse` rather than a
|
|
105
137
|
// JS object literal so a `__proto__` member stays an ordinary property.
|
|
106
138
|
function emitConstant(ctx, value) {
|
|
@@ -144,7 +176,24 @@ const { compileSafe, patternIsSafe } = require('./safe-regex')
|
|
|
144
176
|
let _nativeIsLinear = null
|
|
145
177
|
// The linearity check only parses; it runs first so a pattern bound for the
|
|
146
178
|
// RegExp is never compiled for the engine just to be told it could have been.
|
|
179
|
+
// Unicode property escapes need a real RegExp in unicode mode, as the
|
|
180
|
+
// interpreter compiles them (Interpreter#pattern): the linear engine does not
|
|
181
|
+
// know them, and without the flag `\p{L}` reads as the letters p, {, L, }.
|
|
182
|
+
// The flag only where the pattern is valid with it, again as the interpreter
|
|
183
|
+
// does. These patterns used to send the whole schema to the interpreter.
|
|
184
|
+
const PROP_ESCAPE = /\\[pP]\{/
|
|
185
|
+
function reFlags (src) {
|
|
186
|
+
if (!PROP_ESCAPE.test(src)) return ''
|
|
187
|
+
try { new RegExp(src, 'u'); return 'u' } catch { return '' }
|
|
188
|
+
}
|
|
189
|
+
// The flag argument for generated `new RegExp(<json literal>)` source.
|
|
190
|
+
function reFlagArg (jsonLit) {
|
|
191
|
+
let raw
|
|
192
|
+
try { raw = JSON.parse(jsonLit) } catch { return '' }
|
|
193
|
+
return typeof raw === 'string' && reFlags(raw) ? ",'u'" : ''
|
|
194
|
+
}
|
|
147
195
|
const useSafeEngine = (src) => {
|
|
196
|
+
if (PROP_ESCAPE.test(src)) return false
|
|
148
197
|
if (_nativeIsLinear === null) _nativeIsLinear = require('./regex-linear').nativeIsLinear
|
|
149
198
|
if (_nativeIsLinear(src)) return false
|
|
150
199
|
return patternIsSafe(src)
|
|
@@ -155,7 +204,7 @@ const useSafeEngine = (src) => {
|
|
|
155
204
|
// standalone embeds the engine), else a plain RegExp. Both expose `.test`.
|
|
156
205
|
function safeReClosure (ctx, src) {
|
|
157
206
|
if (useSafeEngine(src)) { ctx.usesSafeRe = true; return compileSafe(src) }
|
|
158
|
-
return new RegExp(src)
|
|
207
|
+
return new RegExp(src, reFlags(src))
|
|
159
208
|
}
|
|
160
209
|
|
|
161
210
|
// Compile a JSON Schema into a pure JS validator function.
|
|
@@ -196,7 +245,7 @@ function literalValue (lit) {
|
|
|
196
245
|
}
|
|
197
246
|
|
|
198
247
|
function unescapeSp (sp) {
|
|
199
|
-
return sp.indexOf('\\') === -1 ? sp : sp.replace(/\\(.)/g,
|
|
248
|
+
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))
|
|
200
249
|
}
|
|
201
250
|
|
|
202
251
|
const FRAME_INLINE_MAX = 120
|
|
@@ -306,6 +355,7 @@ function compileToJS(schema, defs, schemaMap) {
|
|
|
306
355
|
// A $ref cycle on one value recurses here as it does in generated code; see
|
|
307
356
|
// hasSameInstanceRefCycle.
|
|
308
357
|
if (!defs && hasSameInstanceRefCycle(schema)) return null
|
|
358
|
+
if (!defs && bothDefsContainers(schema)) return null
|
|
309
359
|
|
|
310
360
|
// unevaluatedProperties / unevaluatedItems need annotation tracking this
|
|
311
361
|
// path does not do. Decline so the schema reaches an engine that does.
|
|
@@ -557,7 +607,7 @@ function compileToJS(schema, defs, schemaMap) {
|
|
|
557
607
|
}
|
|
558
608
|
if (schema.pattern) {
|
|
559
609
|
try {
|
|
560
|
-
const re = useSafeEngine(schema.pattern) ? compileSafe(schema.pattern) : new RegExp(schema.pattern)
|
|
610
|
+
const re = useSafeEngine(schema.pattern) ? compileSafe(schema.pattern) : new RegExp(schema.pattern, reFlags(schema.pattern))
|
|
561
611
|
checks.push((d) => typeof d !== 'string' || re.test(d))
|
|
562
612
|
} catch {
|
|
563
613
|
return null
|
|
@@ -1390,6 +1440,25 @@ function unevalPropsModeled(schema, schemaMap) {
|
|
|
1390
1440
|
return true
|
|
1391
1441
|
}
|
|
1392
1442
|
|
|
1443
|
+
// Every `$ref` string under a node, once per node.
|
|
1444
|
+
const _refStrings = new WeakMap()
|
|
1445
|
+
function refStringsOf(node) {
|
|
1446
|
+
let set = _refStrings.get(node)
|
|
1447
|
+
if (set !== undefined) return set
|
|
1448
|
+
set = new Set()
|
|
1449
|
+
const seen = new Set()
|
|
1450
|
+
const walk = (n) => {
|
|
1451
|
+
if (n === null || typeof n !== 'object' || seen.has(n)) return
|
|
1452
|
+
seen.add(n)
|
|
1453
|
+
if (Array.isArray(n)) { for (const x of n) walk(x); return }
|
|
1454
|
+
if (typeof n.$ref === 'string') set.add(n.$ref)
|
|
1455
|
+
for (const k of Object.keys(n)) if (k !== 'enum' && k !== 'const' && k !== 'default' && k !== 'examples') walk(n[k])
|
|
1456
|
+
}
|
|
1457
|
+
walk(node)
|
|
1458
|
+
_refStrings.set(node, set)
|
|
1459
|
+
return set
|
|
1460
|
+
}
|
|
1461
|
+
|
|
1393
1462
|
function codegenSafe(schema, schemaMap) {
|
|
1394
1463
|
if (typeof schema === 'boolean') return true
|
|
1395
1464
|
if (typeof schema !== 'object' || schema === null) return true
|
|
@@ -1430,7 +1499,6 @@ function codegenSafe(schema, schemaMap) {
|
|
|
1430
1499
|
}
|
|
1431
1500
|
|
|
1432
1501
|
// Unicode property escapes in pattern need 'u' flag — codegen uses RegExp without it
|
|
1433
|
-
if (schema.pattern && /\\[pP]\{/.test(schema.pattern)) return false
|
|
1434
1502
|
|
|
1435
1503
|
// $ref — allow local refs (#/$defs/Name) and non-local refs if in schemaMap
|
|
1436
1504
|
if (schema.$ref) {
|
|
@@ -1465,17 +1533,11 @@ function codegenSafe(schema, schemaMap) {
|
|
|
1465
1533
|
!resolvedTarget.propertyNames
|
|
1466
1534
|
if (!targetSimple && schema.unevaluatedProperties === undefined && schema.unevaluatedItems === undefined) return false
|
|
1467
1535
|
}
|
|
1468
|
-
// In Draft 2020-12, $ref with siblings
|
|
1469
|
-
//
|
|
1470
|
-
//
|
|
1471
|
-
//
|
|
1472
|
-
//
|
|
1473
|
-
// inferred from what is not a validation keyword, so a keyword this list
|
|
1474
|
-
// does not know still declines instead of being skipped. Measured on the
|
|
1475
|
-
// 977 SchemaStore schemas: a $ref with a description or title beside it sent
|
|
1476
|
-
// 223 of them to the interpreted engine.
|
|
1477
|
-
const siblings = Object.keys(schema).filter(k => !REF_NEUTRAL_SIBLINGS.has(k) && !k.startsWith('x-'))
|
|
1478
|
-
if (siblings.length > 0 && schema.unevaluatedProperties === undefined && schema.unevaluatedItems === undefined) return false
|
|
1536
|
+
// In Draft 2020-12, $ref with siblings applies both. The verdict
|
|
1537
|
+
// generator compiles the siblings after the reference (genCodeNode), the
|
|
1538
|
+
// combined one too (genCodeCNode), and the error generator declines such
|
|
1539
|
+
// a node, so a sibling is never dropped. This used to decline the whole
|
|
1540
|
+
// schema unless an unevaluated keyword was beside the reference.
|
|
1479
1541
|
}
|
|
1480
1542
|
|
|
1481
1543
|
// additionalProperties as schema — supported when no composition (allOf/oneOf/anyOf)
|
|
@@ -1505,8 +1567,9 @@ function codegenSafe(schema, schemaMap) {
|
|
|
1505
1567
|
// that only the interpreted engine provides.
|
|
1506
1568
|
if (schema.unevaluatedProperties === false || (typeof schema.unevaluatedProperties === 'object' && schema.unevaluatedProperties !== null)) {
|
|
1507
1569
|
const evalResult = collectEvaluated(schema, schemaMap)
|
|
1508
|
-
|
|
1509
|
-
|
|
1570
|
+
// What the static model cannot represent is built at run time
|
|
1571
|
+
// (genUnevalDynamicVerdict); shapes that cannot be (dynamic references,
|
|
1572
|
+
// reference cycles) decline there.
|
|
1510
1573
|
}
|
|
1511
1574
|
}
|
|
1512
1575
|
// unevaluatedItems: allow boolean and schema values
|
|
@@ -1527,7 +1590,12 @@ function codegenSafe(schema, schemaMap) {
|
|
|
1527
1590
|
// Defs on a $ref cycle compile as named functions (see genCode); nothing
|
|
1528
1591
|
// to decline here. cyclicDefNames() is the walk that used to bail.
|
|
1529
1592
|
for (const [name, def] of Object.entries(defs)) {
|
|
1530
|
-
|
|
1593
|
+
// A name the generators would have to spell inside a path or an
|
|
1594
|
+
// identifier. Only a problem when a reference names it: tsconfig.json
|
|
1595
|
+
// keeps a comment object under the definition `//`, which nothing
|
|
1596
|
+
// references, and declined whole for it. A definition reached through
|
|
1597
|
+
// a deeper pointer was renamed by normalizeDeepRefs.
|
|
1598
|
+
if (/[~/"']/.test(name) && refStringsOf(schema).has('#/' + (schema.$defs === defs ? '$defs' : 'definitions') + '/' + name.replace(/~/g, '~0').replace(/\//g, '~1'))) return false
|
|
1531
1599
|
if (typeof def === 'boolean') return false
|
|
1532
1600
|
if (typeof def === 'object' && def !== null) {
|
|
1533
1601
|
// A non-fragment $id (e.g. 'sub.json', 'http://...') opens a new base URI
|
|
@@ -1551,7 +1619,6 @@ function codegenSafe(schema, schemaMap) {
|
|
|
1551
1619
|
// compiled to code that ignored it.
|
|
1552
1620
|
if (schema.patternProperties) {
|
|
1553
1621
|
for (const [pat, sub] of Object.entries(schema.patternProperties)) {
|
|
1554
|
-
if (/\\[pP]\{/.test(pat)) return false
|
|
1555
1622
|
if (typeof sub === 'object' && sub !== null && !codegenSafe(sub, schemaMap)) return false
|
|
1556
1623
|
}
|
|
1557
1624
|
}
|
|
@@ -1587,37 +1654,6 @@ function codegenSafe(schema, schemaMap) {
|
|
|
1587
1654
|
return true
|
|
1588
1655
|
}
|
|
1589
1656
|
|
|
1590
|
-
// Returns true if the schema (or any nested sub-schema reachable through the
|
|
1591
|
-
// usual keywords) has additionalProperties as a schema value. Used by the
|
|
1592
|
-
// combined-codegen bail since that path does not emit the per-key loop yet.
|
|
1593
|
-
function hasAdditionalPropertiesSchema(schema) {
|
|
1594
|
-
if (typeof schema !== 'object' || schema === null) return false
|
|
1595
|
-
if (typeof schema.additionalProperties === 'object' && schema.additionalProperties !== null) return true
|
|
1596
|
-
const objSubs = ['properties', 'patternProperties', '$defs', 'definitions', 'dependentSchemas']
|
|
1597
|
-
for (const key of objSubs) {
|
|
1598
|
-
if (schema[key] && typeof schema[key] === 'object') {
|
|
1599
|
-
for (const v of Object.values(schema[key])) {
|
|
1600
|
-
if (hasAdditionalPropertiesSchema(v)) return true
|
|
1601
|
-
}
|
|
1602
|
-
}
|
|
1603
|
-
}
|
|
1604
|
-
const arrSubs = ['allOf', 'anyOf', 'oneOf', 'prefixItems']
|
|
1605
|
-
for (const key of arrSubs) {
|
|
1606
|
-
if (Array.isArray(schema[key])) {
|
|
1607
|
-
for (const s of schema[key]) {
|
|
1608
|
-
if (hasAdditionalPropertiesSchema(s)) return true
|
|
1609
|
-
}
|
|
1610
|
-
}
|
|
1611
|
-
}
|
|
1612
|
-
const singleSubs = ['items', 'contains', 'not', 'if', 'then', 'else', 'propertyNames']
|
|
1613
|
-
for (const key of singleSubs) {
|
|
1614
|
-
if (typeof schema[key] === 'object' && schema[key] !== null) {
|
|
1615
|
-
if (hasAdditionalPropertiesSchema(schema[key])) return true
|
|
1616
|
-
}
|
|
1617
|
-
}
|
|
1618
|
-
return false
|
|
1619
|
-
}
|
|
1620
|
-
|
|
1621
1657
|
// Schema-bearing keywords, so the walk below never mistakes a property named
|
|
1622
1658
|
// "$ref" for an actual reference.
|
|
1623
1659
|
const SUBSCHEMA_MAPS = ['properties', 'patternProperties', '$defs', 'definitions', 'dependentSchemas']
|
|
@@ -1921,8 +1957,20 @@ function hasSameInstanceRefCycle(root) {
|
|
|
1921
1957
|
return found
|
|
1922
1958
|
}
|
|
1923
1959
|
|
|
1960
|
+
// The generators look a definition up by name in one table, `$defs` or else
|
|
1961
|
+
// `definitions`, whichever container the pointer named. With both at the root,
|
|
1962
|
+
// `#/definitions/x` read `$defs/x`: a draft 7 schema carrying both, with
|
|
1963
|
+
// different schemas under one name, rejected what it allowed and accepted
|
|
1964
|
+
// what it rejected. The interpreter resolves the pointer as written, so a
|
|
1965
|
+
// schema with both goes there. None of SchemaStore's 980 has both.
|
|
1966
|
+
function bothDefsContainers(schema) {
|
|
1967
|
+
return schema.$defs !== null && typeof schema.$defs === 'object' &&
|
|
1968
|
+
schema.definitions !== null && typeof schema.definitions === 'object'
|
|
1969
|
+
}
|
|
1970
|
+
|
|
1924
1971
|
function sharedCodegenGate(schema, schemaMap) {
|
|
1925
1972
|
if (typeof schema !== 'object' || schema === null) return true
|
|
1973
|
+
if (bothDefsContainers(schema)) return false
|
|
1926
1974
|
if (hasSameInstanceRefCycle(schema)) return false
|
|
1927
1975
|
if (!codegenSafe(schema, schemaMap)) return false
|
|
1928
1976
|
if (needsBaseTracking(schema, schemaMap, new Set())) return false
|
|
@@ -2020,9 +2068,248 @@ function normalizePropertyNamesWalk(root) {
|
|
|
2020
2068
|
return walk(root)
|
|
2021
2069
|
}
|
|
2022
2070
|
|
|
2071
|
+
// A local `$ref` that is a JSON pointer to anything other than a top-level
|
|
2072
|
+
// definition (`#/definitions/item/properties/id`, `#/properties/foo`,
|
|
2073
|
+
// `#/$defs/a/items`) became the generators' own definition reference: the
|
|
2074
|
+
// pointer is resolved here, at compile time, and its target added as a
|
|
2075
|
+
// definition under a name of its own, so every path that already handles
|
|
2076
|
+
// `#/$defs/name` handles it, cycles included. The generators knew only the
|
|
2077
|
+
// top-level form and declined the rest, which kept 40 of 980 SchemaStore
|
|
2078
|
+
// schemas, tsconfig.json among them, on the interpreted engine for every
|
|
2079
|
+
// document. Left alone, as before: a pointer that does not resolve to a
|
|
2080
|
+
// schema, one that passes through a subschema with its own `$id` (another
|
|
2081
|
+
// base, whose pointers mean something else), and every `$ref` inside such a
|
|
2082
|
+
// subschema. Copy on write; errors keep their paths, which name the place of
|
|
2083
|
+
// the reference, not of its target, and are ordered against the schema as
|
|
2084
|
+
// the caller wrote it.
|
|
2085
|
+
const DEF_REF = /^#\/(?:\$defs|definitions)\/[^/]+$/
|
|
2086
|
+
const _deepRefsNormalized = new WeakMap()
|
|
2087
|
+
function normalizeDeepRefs(root) {
|
|
2088
|
+
if (!root || typeof root !== 'object' || Array.isArray(root)) return root
|
|
2089
|
+
const hit = _deepRefsNormalized.get(root)
|
|
2090
|
+
if (hit !== undefined) return hit
|
|
2091
|
+
const out = normalizeDeepRefsWalk(root)
|
|
2092
|
+
_deepRefsNormalized.set(root, out)
|
|
2093
|
+
return out
|
|
2094
|
+
}
|
|
2095
|
+
function resolveLocalPointer(root, ref) {
|
|
2096
|
+
let node = root
|
|
2097
|
+
const segs = ref.slice(2).split('/')
|
|
2098
|
+
for (let i = 0; i < segs.length; i++) {
|
|
2099
|
+
let seg = segs[i].replace(/~1/g, '/').replace(/~0/g, '~')
|
|
2100
|
+
// Draft 7 schemas reach the generators with `definitions` renamed to
|
|
2101
|
+
// `$defs` (lib/draft7.js) and their references as written, which the
|
|
2102
|
+
// top-level form already reads either way; so does this.
|
|
2103
|
+
if (i === 0 && (seg === 'definitions' || seg === '$defs') && !Object.prototype.hasOwnProperty.call(node, seg)) seg = seg === 'definitions' ? '$defs' : 'definitions'
|
|
2104
|
+
if (node === null || typeof node !== 'object') return undefined
|
|
2105
|
+
if (Array.isArray(node)) {
|
|
2106
|
+
if (!/^(?:0|[1-9]\d*)$/.test(seg) || +seg >= node.length) return undefined
|
|
2107
|
+
node = node[+seg]
|
|
2108
|
+
} else {
|
|
2109
|
+
if (!Object.prototype.hasOwnProperty.call(node, seg)) return undefined
|
|
2110
|
+
node = node[seg]
|
|
2111
|
+
}
|
|
2112
|
+
if (node !== null && typeof node === 'object' && !Array.isArray(node) && typeof node.$id === 'string') return undefined
|
|
2113
|
+
}
|
|
2114
|
+
if (typeof node === 'boolean') return node
|
|
2115
|
+
if (node === null || typeof node !== 'object' || Array.isArray(node)) return undefined
|
|
2116
|
+
return node
|
|
2117
|
+
}
|
|
2118
|
+
function normalizeDeepRefsWalk(root) {
|
|
2119
|
+
// First pass: the deep pointers used outside any nested `$id`, and their
|
|
2120
|
+
// targets.
|
|
2121
|
+
const targets = new Map()
|
|
2122
|
+
const scan = (node, nested, seen) => {
|
|
2123
|
+
if (node === null || typeof node !== 'object') return
|
|
2124
|
+
if (seen.has(node)) return
|
|
2125
|
+
seen.add(node)
|
|
2126
|
+
if (Array.isArray(node)) { for (const n of node) scan(n, nested, seen); return }
|
|
2127
|
+
const inner = nested || (node !== root && typeof node.$id === 'string')
|
|
2128
|
+
if (!inner && typeof node.$ref === 'string' && node.$ref.startsWith('#/') && !DEF_REF.test(node.$ref) && !targets.has(node.$ref)) {
|
|
2129
|
+
const t = resolveLocalPointer(root, node.$ref)
|
|
2130
|
+
if (t !== undefined) targets.set(node.$ref, t)
|
|
2131
|
+
}
|
|
2132
|
+
for (const k of Object.keys(node)) if (k !== 'enum' && k !== 'const' && k !== 'default' && k !== 'examples') scan(node[k], inner, seen)
|
|
2133
|
+
}
|
|
2134
|
+
scan(root, false, new Set())
|
|
2135
|
+
if (targets.size === 0) return root
|
|
2136
|
+
const defsKey = root.$defs && typeof root.$defs === 'object' ? '$defs' : (root.definitions && typeof root.definitions === 'object' ? 'definitions' : '$defs')
|
|
2137
|
+
const existing = root[defsKey] && typeof root[defsKey] === 'object' ? root[defsKey] : {}
|
|
2138
|
+
const names = new Map()
|
|
2139
|
+
let n = 0
|
|
2140
|
+
for (const ref of targets.keys()) {
|
|
2141
|
+
let name
|
|
2142
|
+
do { name = '__ata_ptr_' + n++ } while (Object.prototype.hasOwnProperty.call(existing, name))
|
|
2143
|
+
names.set(ref, name)
|
|
2144
|
+
}
|
|
2145
|
+
// Second pass: the references rewritten, copy on write, sharing one copy
|
|
2146
|
+
// per node so a target reached twice stays one object.
|
|
2147
|
+
const memo = new Map()
|
|
2148
|
+
const walk = (node, nested) => {
|
|
2149
|
+
if (node === null || typeof node !== 'object') return node
|
|
2150
|
+
const key = nested ? null : node
|
|
2151
|
+
if (key !== null && memo.has(key)) return memo.get(key)
|
|
2152
|
+
if (Array.isArray(node)) {
|
|
2153
|
+
let out = node
|
|
2154
|
+
if (key !== null) memo.set(key, out)
|
|
2155
|
+
for (let i = 0; i < node.length; i++) {
|
|
2156
|
+
const w = walk(node[i], nested)
|
|
2157
|
+
if (w !== node[i]) { if (out === node) { out = node.slice(); if (key !== null) memo.set(key, out) } out[i] = w }
|
|
2158
|
+
}
|
|
2159
|
+
return out
|
|
2160
|
+
}
|
|
2161
|
+
const inner = nested || (node !== root && typeof node.$id === 'string')
|
|
2162
|
+
if (inner) return node
|
|
2163
|
+
let out = node
|
|
2164
|
+
memo.set(node, out)
|
|
2165
|
+
const own = (k, v) => { if (out === node) { out = { ...node }; memo.set(node, out) } out[k] = v }
|
|
2166
|
+
if (typeof node.$ref === 'string' && names.has(node.$ref)) own('$ref', '#/' + defsKey + '/' + names.get(node.$ref))
|
|
2167
|
+
for (const k of Object.keys(node)) {
|
|
2168
|
+
if (k === '$ref' || k === 'enum' || k === 'const' || k === 'default' || k === 'examples') continue
|
|
2169
|
+
const v = node[k]
|
|
2170
|
+
if (v === null || typeof v !== 'object') continue
|
|
2171
|
+
const w = walk(v, false)
|
|
2172
|
+
if (w !== v) own(k, w)
|
|
2173
|
+
}
|
|
2174
|
+
return out
|
|
2175
|
+
}
|
|
2176
|
+
const rewritten = walk(root, false)
|
|
2177
|
+
const out = rewritten === root ? { ...root } : rewritten
|
|
2178
|
+
const defs = { ...(out[defsKey] && typeof out[defsKey] === 'object' ? out[defsKey] : {}) }
|
|
2179
|
+
for (const [ref, name] of names) {
|
|
2180
|
+
const t = targets.get(ref)
|
|
2181
|
+
defs[name] = typeof t === 'boolean' ? t : walk(t, false)
|
|
2182
|
+
}
|
|
2183
|
+
out[defsKey] = defs
|
|
2184
|
+
return out
|
|
2185
|
+
}
|
|
2186
|
+
|
|
2187
|
+
// Every reference the interpreted engine can resolve becomes a definition
|
|
2188
|
+
// reference the generators handle: URNs, `$id`-relative and absolute URIs,
|
|
2189
|
+
// anchors, pointers into a resource with its own `$id`, names with characters
|
|
2190
|
+
// the generators cannot spell, documents registered through `schemas`. The
|
|
2191
|
+
// target of each is found by the interpreter's own resolver (indexSchemas and
|
|
2192
|
+
// resolveRef in lib/interpreter.js), so the two engines cannot resolve a
|
|
2193
|
+
// reference differently, and is added under a name of its own as with
|
|
2194
|
+
// normalizeDeepRefs. Once every reference names a definition, nested `$id`s
|
|
2195
|
+
// no longer change what anything means and are dropped from the copy, which
|
|
2196
|
+
// is what kept those schemas from compiling. Left to normalizeDeepRefs, as
|
|
2197
|
+
// before: a schema with dynamic references (their meaning depends on the
|
|
2198
|
+
// resources at run time), one with an embedded resource in another dialect,
|
|
2199
|
+
// and one with any reference the resolver cannot resolve.
|
|
2200
|
+
const _refsNormalized = new WeakMap()
|
|
2201
|
+
function normalizeRefs(root, schemaMap) {
|
|
2202
|
+
if (!root || typeof root !== 'object' || Array.isArray(root)) return root
|
|
2203
|
+
let byMap = _refsNormalized.get(root)
|
|
2204
|
+
const mapKey = schemaMap || _refsNormalized
|
|
2205
|
+
if (byMap !== undefined && byMap.has(mapKey)) return byMap.get(mapKey)
|
|
2206
|
+
let out
|
|
2207
|
+
try { out = normalizeRefsWalk(root, schemaMap) } catch { out = null }
|
|
2208
|
+
if (out === null) out = normalizeDeepRefs(root)
|
|
2209
|
+
if (byMap === undefined) { byMap = new Map(); _refsNormalized.set(root, byMap) }
|
|
2210
|
+
byMap.set(mapKey, out)
|
|
2211
|
+
return out
|
|
2212
|
+
}
|
|
2213
|
+
const SKIP_VALUE_KEYS = new Set(['enum', 'const', 'default', 'examples'])
|
|
2214
|
+
function normalizeRefsWalk(root, schemaMap) {
|
|
2215
|
+
const text = JSON.stringify(root)
|
|
2216
|
+
if (!text.includes('"$ref"')) return root
|
|
2217
|
+
if (/"\$(?:dynamicRef|recursiveRef|dynamicAnchor|recursiveAnchor)"/.test(text)) return null
|
|
2218
|
+
const { indexSchemas, resolveRef } = require('./interpreter')._refInternals
|
|
2219
|
+
const state = indexSchemas(root, schemaMap && schemaMap.size ? schemaMap : null)
|
|
2220
|
+
const rootDialect = typeof root.$schema === 'string' ? root.$schema.replace(/#$/, '') : null
|
|
2221
|
+
// Resolve every reference reachable from the root, following into targets
|
|
2222
|
+
// in other documents.
|
|
2223
|
+
const names = new Map() // target node -> synthetic name
|
|
2224
|
+
const refTarget = new Map() // reference site node -> target node
|
|
2225
|
+
const visited = new Set()
|
|
2226
|
+
let fail = false
|
|
2227
|
+
const visit = (node) => {
|
|
2228
|
+
if (fail || node === null || typeof node !== 'object' || visited.has(node)) return
|
|
2229
|
+
visited.add(node)
|
|
2230
|
+
if (Array.isArray(node)) { for (const n of node) visit(n); return }
|
|
2231
|
+
if (node !== root && typeof node.$schema === 'string' && rootDialect !== null && node.$schema.replace(/#$/, '') !== rootDialect) { fail = true; return }
|
|
2232
|
+
// A dynamic reference reached through another document means the same as
|
|
2233
|
+
// one in the root: its target depends on the resources at run time.
|
|
2234
|
+
if (node.$dynamicRef !== undefined || node.$dynamicAnchor !== undefined || node.$recursiveRef !== undefined || node.$recursiveAnchor !== undefined) { fail = true; return }
|
|
2235
|
+
if (typeof node.$ref === 'string' && state.nodeBase.has(node)) {
|
|
2236
|
+
const base = state.nodeBase.get(node)
|
|
2237
|
+
const ref = node.$ref
|
|
2238
|
+
const local = base === state.rootBase && (ref === '#' || (DEF_REF.test(ref) && !/[~%]/.test(ref)))
|
|
2239
|
+
if (!local) {
|
|
2240
|
+
const { node: t } = resolveRef(ref, base, state)
|
|
2241
|
+
if (t === undefined || (typeof t !== 'boolean' && (t === null || typeof t !== 'object' || Array.isArray(t)))) { fail = true; return }
|
|
2242
|
+
refTarget.set(node, t)
|
|
2243
|
+
// The root, however it is spelled, is `#`: a copy of it under its own
|
|
2244
|
+
// definitions would contain itself.
|
|
2245
|
+
if (t === root) { /* named '#' below */ }
|
|
2246
|
+
else if (typeof t === 'object' && !names.has(t)) { names.set(t, null); visit(t) }
|
|
2247
|
+
if (typeof t === 'boolean' && !names.has(t)) names.set(t, null)
|
|
2248
|
+
}
|
|
2249
|
+
}
|
|
2250
|
+
for (const k of Object.keys(node)) if (!SKIP_VALUE_KEYS.has(k)) visit(node[k])
|
|
2251
|
+
}
|
|
2252
|
+
visit(root)
|
|
2253
|
+
if (fail) return null
|
|
2254
|
+
if (refTarget.size === 0) return root
|
|
2255
|
+
const defsKey = root.$defs && typeof root.$defs === 'object' ? '$defs' : (root.definitions && typeof root.definitions === 'object' ? 'definitions' : '$defs')
|
|
2256
|
+
const existing = root[defsKey] && typeof root[defsKey] === 'object' ? root[defsKey] : {}
|
|
2257
|
+
let n = 0
|
|
2258
|
+
for (const t of names.keys()) {
|
|
2259
|
+
let name
|
|
2260
|
+
do { name = '__ata_ref_' + n++ } while (Object.prototype.hasOwnProperty.call(existing, name))
|
|
2261
|
+
names.set(t, name)
|
|
2262
|
+
}
|
|
2263
|
+
// The copy: references renamed, nested `$id`s dropped.
|
|
2264
|
+
const memo = new Map()
|
|
2265
|
+
const walk = (node) => {
|
|
2266
|
+
if (node === null || typeof node !== 'object') return node
|
|
2267
|
+
if (memo.has(node)) return memo.get(node)
|
|
2268
|
+
if (Array.isArray(node)) {
|
|
2269
|
+
const out = []
|
|
2270
|
+
memo.set(node, out)
|
|
2271
|
+
for (const x of node) out.push(walk(x))
|
|
2272
|
+
return out
|
|
2273
|
+
}
|
|
2274
|
+
const out = {}
|
|
2275
|
+
memo.set(node, out)
|
|
2276
|
+
for (const k of Object.keys(node)) {
|
|
2277
|
+
if (k === '$id' && node !== root) continue
|
|
2278
|
+
if (k === '$ref' && refTarget.has(node)) {
|
|
2279
|
+
const t = refTarget.get(node)
|
|
2280
|
+
out.$ref = t === root ? '#' : '#/' + defsKey + '/' + names.get(t)
|
|
2281
|
+
continue
|
|
2282
|
+
}
|
|
2283
|
+
out[k] = SKIP_VALUE_KEYS.has(k) ? node[k] : walk(node[k])
|
|
2284
|
+
}
|
|
2285
|
+
return out
|
|
2286
|
+
}
|
|
2287
|
+
const out = walk(root)
|
|
2288
|
+
const defs = { ...(out[defsKey] && typeof out[defsKey] === 'object' ? out[defsKey] : {}) }
|
|
2289
|
+
for (const [t, name] of names) defs[name] = typeof t === 'boolean' ? t : walk(t)
|
|
2290
|
+
out[defsKey] = defs
|
|
2291
|
+
return out
|
|
2292
|
+
}
|
|
2293
|
+
|
|
2294
|
+
// What the three generators compile from: the schema with deep local pointers
|
|
2295
|
+
// turned into definition references, then propertyNames normalized. The same
|
|
2296
|
+
// for all three, so they decline and agree on the same schemas.
|
|
2297
|
+
function prepareForCodegen(schema, schemaMap) {
|
|
2298
|
+
return normalizePropertyNames(normalizeRefs(schema, schemaMap))
|
|
2299
|
+
}
|
|
2300
|
+
|
|
2301
|
+
const VERDICT_SPLIT = 64 * 1024
|
|
2302
|
+
function sourceSize (lines, ctx) {
|
|
2303
|
+
let n = 0
|
|
2304
|
+
for (const l of lines) n += l.length
|
|
2305
|
+
for (const l of ctx.preamble) n += l.length
|
|
2306
|
+
if (ctx.deferredChecks) for (const l of ctx.deferredChecks) n += l.length
|
|
2307
|
+
return n
|
|
2308
|
+
}
|
|
2309
|
+
|
|
2023
2310
|
function compileToJSCodegen(schema, schemaMap, userFormats, opts) {
|
|
2024
2311
|
const inputSchema = schema
|
|
2025
|
-
schema =
|
|
2312
|
+
schema = prepareForCodegen(schema, schemaMap)
|
|
2026
2313
|
if (typeof schema === 'boolean') return schema ? () => true : () => false
|
|
2027
2314
|
if (typeof schema !== 'object' || schema === null) return null
|
|
2028
2315
|
// removeAdditional: a verdict function that deletes the keys the remover
|
|
@@ -2044,7 +2331,6 @@ function compileToJSCodegen(schema, schemaMap, userFormats, opts) {
|
|
|
2044
2331
|
// patternProperties: bail only on boolean sub-schemas or unicode property escapes
|
|
2045
2332
|
if (schema.patternProperties) {
|
|
2046
2333
|
for (const [pat, sub] of Object.entries(schema.patternProperties)) {
|
|
2047
|
-
if (/\\[pP]\{/.test(pat)) return null
|
|
2048
2334
|
if (typeof sub === 'object' && sub !== null && !codegenSafe(sub, schemaMap)) return null
|
|
2049
2335
|
}
|
|
2050
2336
|
}
|
|
@@ -2093,20 +2379,28 @@ function compileToJSCodegen(schema, schemaMap, userFormats, opts) {
|
|
|
2093
2379
|
if (hasUnresolvableRef(schema, rootDefs, anchors, schemaMap, new Set())) return null
|
|
2094
2380
|
if (needsBaseTracking(schema, schemaMap, new Set())) return null
|
|
2095
2381
|
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
if (
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2382
|
+
// Macro keywords expand into a schema of their own, which this generator
|
|
2383
|
+
// does not apply; such a schema stays on the interpreted engine.
|
|
2384
|
+
if (opts && opts.keywords && usesMacroKeyword(schema, schemaMap, opts.keywords)) return null
|
|
2385
|
+
// Every reference is first expanded where it is used, which is the fastest
|
|
2386
|
+
// form for the checks. Past VERDICT_SPLIT of source the function is
|
|
2387
|
+
// generated again with each definition referenced from several places
|
|
2388
|
+
// written once, as a function in the preamble: expanded, SchemaStore's sarif
|
|
2389
|
+
// (79 KB of schema) came to 2.8 MB of verdict source, and compiling it was
|
|
2390
|
+
// half of the first validate() call.
|
|
2391
|
+
let ctx, lines
|
|
2392
|
+
for (const share of [false, true]) {
|
|
2393
|
+
ctx = { varCounter: 0, helpers: [], helperCode: [], preamble: [], shared: [], closureVars: ['_cpLen'], closureVals: [_cpLen], rootDefs, refStack: new Set(), schemaMap: schemaMap || null, anchors, rootSchema: inputSchema, userFormats: userFormats || null, removeNodes,
|
|
2394
|
+
keywords: (opts && opts.keywords) || null, kwEmitted: null, shareDefs: share }
|
|
2395
|
+
if (ctx.keywords) ctx.kwEmitted = new Set()
|
|
2396
|
+
lines = []
|
|
2397
|
+
try {
|
|
2398
|
+
genCode(schema, 'd', lines, ctx)
|
|
2399
|
+
} catch (e) {
|
|
2400
|
+
if (e === DECLINE) return null
|
|
2401
|
+
throw e
|
|
2402
|
+
}
|
|
2403
|
+
if (share || sourceSize(lines, ctx) <= VERDICT_SPLIT) break
|
|
2110
2404
|
}
|
|
2111
2405
|
// Every schema node that carries a custom keyword must have had its check
|
|
2112
2406
|
// written. A path that inlined a node without going through genCodeNode
|
|
@@ -2140,10 +2434,10 @@ function compileToJSCodegen(schema, schemaMap, userFormats, opts) {
|
|
|
2140
2434
|
closureValues.push(compileSafe(JSON.parse(safeMatch[2])))
|
|
2141
2435
|
continue
|
|
2142
2436
|
}
|
|
2143
|
-
const match = code.match(/^const (_re\d+)=new RegExp\((
|
|
2437
|
+
const match = code.match(/^const (_re\d+)=new RegExp\((.+?)(?:,'(u)')?\)$/)
|
|
2144
2438
|
if (match) {
|
|
2145
2439
|
closureNames.push(match[1])
|
|
2146
|
-
closureValues.push(new RegExp(JSON.parse(match[2])))
|
|
2440
|
+
closureValues.push(new RegExp(JSON.parse(match[2]), match[3] || ''))
|
|
2147
2441
|
}
|
|
2148
2442
|
}
|
|
2149
2443
|
|
|
@@ -2154,7 +2448,7 @@ function compileToJSCodegen(schema, schemaMap, userFormats, opts) {
|
|
|
2154
2448
|
// are built when first asked for, not here: most validators never call
|
|
2155
2449
|
// validateJSON or get extended, and rewriting and compiling the source twice
|
|
2156
2450
|
// more on every schema was about a third of what a first call cost.
|
|
2157
|
-
let body, hybridThunk, tailThunk
|
|
2451
|
+
let body, hybridThunk, tailThunk, resultThunk
|
|
2158
2452
|
if (ctx.usesRecursion) {
|
|
2159
2453
|
// Self-recursive: a named body reached through the cycle guard, so data
|
|
2160
2454
|
// that points back at itself settles instead of exhausting the stack.
|
|
@@ -2164,6 +2458,7 @@ function compileToJSCodegen(schema, schemaMap, userFormats, opts) {
|
|
|
2164
2458
|
body = `${decl}function _run(d){\n ${run}\n }\n return _run(d)`
|
|
2165
2459
|
// Hybrid: keep the boolean answer, wrap only the outer call
|
|
2166
2460
|
hybridThunk = () => `${decl}function _run(d){\n ${run}\n }\n return _run(d)?R:E(d)`
|
|
2461
|
+
resultThunk = () => `${decl}function _run(d){\n ${run}\n }\n return _run(d)?${RESULT_TRUE}:${RESULT_FALSE}`
|
|
2167
2462
|
tailThunk = () => `${decl}function _run(d){\n ${run}\n }\n return _run(d)&&__ataTail(d)`
|
|
2168
2463
|
} else if (defSets.length > 0) {
|
|
2169
2464
|
// No self-reference at the root, but a def on a cycle can still be
|
|
@@ -2171,10 +2466,12 @@ function compileToJSCodegen(schema, schemaMap, userFormats, opts) {
|
|
|
2171
2466
|
const run = emitGuardedRun(`return _body(d)`, '', defSets)
|
|
2172
2467
|
body = `function _body(d){\n ${checkStr}\n return true\n }\n function _run(d){\n ${run}\n }\n return _run(d)`
|
|
2173
2468
|
hybridThunk = () => `function _body(d){\n ${checkStr}\n return true\n }\n function _run(d){\n ${run}\n }\n return _run(d)?R:E(d)`
|
|
2469
|
+
resultThunk = () => `function _body(d){\n ${checkStr}\n return true\n }\n function _run(d){\n ${run}\n }\n return _run(d)?${RESULT_TRUE}:${RESULT_FALSE}`
|
|
2174
2470
|
tailThunk = () => `function _body(d){\n ${checkStr}\n return true\n }\n function _run(d){\n ${run}\n }\n return _run(d)&&__ataTail(d)`
|
|
2175
2471
|
} else {
|
|
2176
2472
|
body = checkStr + '\n return true'
|
|
2177
2473
|
hybridThunk = () => replaceTopLevel(checkStr + '\n return R')
|
|
2474
|
+
resultThunk = () => replaceTopLevel(checkStr + '\n return true', `return ${RESULT_TRUE}`, `return ${RESULT_FALSE}`)
|
|
2178
2475
|
// An early accept inside the checks has to reach the tail too, so every
|
|
2179
2476
|
// top-level `return true` becomes the tail call, by the same rewrite the
|
|
2180
2477
|
// hybrid above relies on and test_hybrid_agreement holds to the suite.
|
|
@@ -2214,6 +2511,25 @@ function compileToJSCodegen(schema, schemaMap, userFormats, opts) {
|
|
|
2214
2511
|
return hybridFactory === null ? null : hybridFactory(...closureValues, R, E)
|
|
2215
2512
|
}
|
|
2216
2513
|
|
|
2514
|
+
// validate() itself: the same checks returning the result objects, for a
|
|
2515
|
+
// validator past its first calls (see validator-core). One function per
|
|
2516
|
+
// schema means the result is built at an allocation site of its own and
|
|
2517
|
+
// nothing between the caller and the checks is shared with other schemas.
|
|
2518
|
+
// Shared wrappers are what made reading results slow across many schemas:
|
|
2519
|
+
// every validator's calls went through the same few call sites. The
|
|
2520
|
+
// rejection is the lazy one, built from the arguments.
|
|
2521
|
+
let resultFactory
|
|
2522
|
+
boolFn._resultFactory = (LR, B, W, EE) => {
|
|
2523
|
+
if (resultFactory === undefined) {
|
|
2524
|
+
try {
|
|
2525
|
+
resultFactory = new Function(...closureNames, '__ataLR', '__ataB', '__ataW', '__ataEE', `${preambleStr}return function(d){${resultThunk()}}`)
|
|
2526
|
+
} catch {
|
|
2527
|
+
resultFactory = null
|
|
2528
|
+
}
|
|
2529
|
+
}
|
|
2530
|
+
return resultFactory === null ? null : resultFactory(...closureValues, LR, B, W, EE)
|
|
2531
|
+
}
|
|
2532
|
+
|
|
2217
2533
|
// The same function with one more check where it would return true, for a
|
|
2218
2534
|
// wrapper that enforces something the schema does not carry (see
|
|
2219
2535
|
// Validator#_extendVerdict). Calling the check from here rather than from a
|
|
@@ -2291,40 +2607,75 @@ function compileToJSCodegen(schema, schemaMap, userFormats, opts) {
|
|
|
2291
2607
|
|
|
2292
2608
|
// Replace top-level `return false` → `return E(d)` and `return true` → `return R`.
|
|
2293
2609
|
// Tracks function nesting depth to preserve nested function internals.
|
|
2610
|
+
const RESULT_TRUE = '{valid:true,data:d,errors:__ataEE}'
|
|
2611
|
+
const RESULT_FALSE = 'new __ataLR(__ataB,d,__ataW)'
|
|
2612
|
+
// The end of the string literal that starts at `i` (a quote character), or
|
|
2613
|
+
// the end of the code when it does not close.
|
|
2614
|
+
function skipString(code, i) {
|
|
2615
|
+
const q = code[i]
|
|
2616
|
+
let j = i + 1
|
|
2617
|
+
while (j < code.length) {
|
|
2618
|
+
const c = code[j]
|
|
2619
|
+
if (c === '\\') { j += 2; continue }
|
|
2620
|
+
if (c === q) return j + 1
|
|
2621
|
+
j++
|
|
2622
|
+
}
|
|
2623
|
+
return code.length
|
|
2624
|
+
}
|
|
2625
|
+
|
|
2294
2626
|
function replaceTopLevel(code, onTrue = 'return R', onFalse = 'return E(d)') {
|
|
2295
2627
|
let fnDepth = 0, result = '', i = 0
|
|
2296
2628
|
while (i < code.length) {
|
|
2629
|
+
// String literals are copied as they are. A property called `function`
|
|
2630
|
+
// reads `d["function"]`, and the scan took the word for a nested function
|
|
2631
|
+
// and left every return after it alone, so the hybrid returned a bare
|
|
2632
|
+
// `false` where a result was due: validateJSON answered `false` instead
|
|
2633
|
+
// of a rejection for such schemas. Text such as `return false` or a brace
|
|
2634
|
+
// inside a string is not code either.
|
|
2635
|
+
const ch = code[i]
|
|
2636
|
+
if (ch === '"' || ch === "'" || ch === '`') {
|
|
2637
|
+
const j = skipString(code, i)
|
|
2638
|
+
result += code.slice(i, j)
|
|
2639
|
+
i = j
|
|
2640
|
+
continue
|
|
2641
|
+
}
|
|
2297
2642
|
// A nested closure keeps its own returns. Both spellings count: a
|
|
2298
2643
|
// `function` keyword and an arrow with a block body. The inline pattern
|
|
2299
2644
|
// compiler emits `(()=>{...return false...return true})()` and rewriting
|
|
2300
2645
|
// those returns turned the arrow's boolean into an object, which made
|
|
2301
2646
|
// `!(cond && obj)` false and let the hybrid accept what the boolean
|
|
2302
2647
|
// program rejected.
|
|
2303
|
-
const isFunctionKw = code.startsWith('function', i) && (i === 0 || /[^a-zA-Z_$]/.test(code[i - 1]))
|
|
2648
|
+
const isFunctionKw = code.startsWith('function', i) && (i === 0 || /[^a-zA-Z_$]/.test(code[i - 1])) && !/[a-zA-Z0-9_$]/.test(code[i + 8] || '')
|
|
2304
2649
|
const isArrowBlock = code.startsWith('=>{', i)
|
|
2305
2650
|
if (isFunctionKw || isArrowBlock) {
|
|
2306
|
-
// Skip to the opening brace, then track all braces inside the body
|
|
2651
|
+
// Skip to the opening brace, then track all braces inside the body,
|
|
2652
|
+
// strings skipped as above.
|
|
2307
2653
|
let j = isArrowBlock ? i + 2 : i + 8
|
|
2308
2654
|
while (j < code.length && code[j] !== '{') j++
|
|
2309
2655
|
result += code.slice(i, j + 1)
|
|
2310
2656
|
i = j + 1
|
|
2311
|
-
// Track braces inside this function body
|
|
2312
2657
|
let braceDepth = 1
|
|
2313
2658
|
while (i < code.length && braceDepth > 0) {
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
|
|
2659
|
+
const c = code[i]
|
|
2660
|
+
if (c === '"' || c === "'" || c === '`') {
|
|
2661
|
+
const k = skipString(code, i)
|
|
2662
|
+
result += code.slice(i, k)
|
|
2663
|
+
i = k
|
|
2664
|
+
continue
|
|
2665
|
+
}
|
|
2666
|
+
if (c === '{') braceDepth++
|
|
2667
|
+
else if (c === '}') braceDepth--
|
|
2668
|
+
result += c
|
|
2318
2669
|
i++
|
|
2319
2670
|
}
|
|
2320
|
-
} else if (code.startsWith('return false', i)) {
|
|
2671
|
+
} else if (code.startsWith('return false', i) && (i + 12 >= code.length || !/[a-zA-Z0-9_$]/.test(code[i + 12]))) {
|
|
2321
2672
|
result += onFalse
|
|
2322
2673
|
i += 12
|
|
2323
|
-
} else if (code.startsWith('return true', i) && (i + 11 >= code.length || !/[a-zA-
|
|
2674
|
+
} else if (code.startsWith('return true', i) && (i + 11 >= code.length || !/[a-zA-Z0-9_$]/.test(code[i + 11]))) {
|
|
2324
2675
|
result += onTrue
|
|
2325
2676
|
i += 11
|
|
2326
2677
|
} else {
|
|
2327
|
-
result +=
|
|
2678
|
+
result += ch
|
|
2328
2679
|
i++
|
|
2329
2680
|
}
|
|
2330
2681
|
}
|
|
@@ -2600,7 +2951,9 @@ function genCodeNode(schema, v, lines, ctx, knownType) {
|
|
|
2600
2951
|
// $ref — guard against circular references
|
|
2601
2952
|
// In 2020-12 with unevaluated*, $ref can coexist with siblings — don't early return
|
|
2602
2953
|
// Only when THIS schema has unevaluated keywords directly (not via $ref target)
|
|
2603
|
-
|
|
2954
|
+
// Any keyword beside $ref that validates is compiled after the reference
|
|
2955
|
+
// (2020-12 applies both; draft 7 drops such siblings when it is read).
|
|
2956
|
+
const hasSiblings = !!schema.$ref && Object.keys(schema).some((k) => !REF_NEUTRAL_SIBLINGS.has(k) && !k.startsWith('x-'))
|
|
2604
2957
|
if (schema.$ref) {
|
|
2605
2958
|
if (genRefNode(schema, v, lines, ctx, knownType, hasSiblings)) return
|
|
2606
2959
|
}
|
|
@@ -2808,7 +3161,7 @@ function genCodeNode(schema, v, lines, ctx, knownType) {
|
|
|
2808
3161
|
ctx.helperCode.push(`const _re${ri}=__ataSafeRe(${pattern})`);
|
|
2809
3162
|
ctx.usesSafeRe = true
|
|
2810
3163
|
} else {
|
|
2811
|
-
ctx.helperCode.push(`const _re${ri}=new RegExp(${pattern})`);
|
|
3164
|
+
ctx.helperCode.push(`const _re${ri}=new RegExp(${pattern}${reFlagArg(pattern)})`);
|
|
2812
3165
|
}
|
|
2813
3166
|
}
|
|
2814
3167
|
const ri = ctx.regExpMap.get(pattern);
|
|
@@ -3330,6 +3683,31 @@ function genRefNode(schema, v, lines, ctx, knownType, hasSiblings) {
|
|
|
3330
3683
|
}
|
|
3331
3684
|
lines.push(`if(!${fnName}(${v}))return false`)
|
|
3332
3685
|
if (!hasSiblings) return true
|
|
3686
|
+
} else if (ctx.shareDefs && !ctx.refStack.has(schema.$ref) && (ctx.sharedDefs || (ctx.sharedDefs = sharedDefNames(ctx.rootSchema, ctx.rootDefs))).has(defName)) {
|
|
3687
|
+
// A shared definition off any cycle, as a plain function in the
|
|
3688
|
+
// preamble (see compileToJSCodegen): no guard, since it cannot re-enter
|
|
3689
|
+
// itself. Generated over its own `d` like the cyclic ones above.
|
|
3690
|
+
if (!ctx.sharedDefFns) ctx.sharedDefFns = new Map()
|
|
3691
|
+
let fnName = ctx.sharedDefFns.get(defName)
|
|
3692
|
+
if (!fnName) {
|
|
3693
|
+
fnName = '_dv' + ctx.sharedDefFns.size + '_' + defName.replace(/[^A-Za-z0-9_]/g, '_')
|
|
3694
|
+
ctx.sharedDefFns.set(defName, fnName)
|
|
3695
|
+
const bodyLines = []
|
|
3696
|
+
const outerDeferred = ctx.deferredChecks
|
|
3697
|
+
ctx.deferredChecks = null
|
|
3698
|
+
ctx.refStack.add(schema.$ref)
|
|
3699
|
+
try {
|
|
3700
|
+
genCode(ctx.rootDefs[defName], 'd', bodyLines, ctx)
|
|
3701
|
+
} finally {
|
|
3702
|
+
ctx.refStack.delete(schema.$ref)
|
|
3703
|
+
}
|
|
3704
|
+
if (ctx.deferredChecks) for (const dc of ctx.deferredChecks) bodyLines.push(dc)
|
|
3705
|
+
ctx.deferredChecks = outerDeferred
|
|
3706
|
+
if (bodyLines.some((l) => /\b_validate\b/.test(l))) throw DECLINE
|
|
3707
|
+
hoist(ctx, `function ${fnName}(d){${bodyLines.join('\n ')}\n return true}`)
|
|
3708
|
+
}
|
|
3709
|
+
lines.push(`if(!${fnName}(${v}))return false`)
|
|
3710
|
+
if (!hasSiblings) return true
|
|
3333
3711
|
} else if (ctx.refStack.has(schema.$ref)) { if (ctx.nestedBoolean) throw DECLINE; if (!hasSiblings) return true }
|
|
3334
3712
|
else {
|
|
3335
3713
|
ctx.refStack.add(schema.$ref)
|
|
@@ -3508,14 +3886,32 @@ function genPatternPropertiesNode(schema, v, lines, ctx, knownType, isObj) {
|
|
|
3508
3886
|
return handledPropertyNames
|
|
3509
3887
|
}
|
|
3510
3888
|
|
|
3889
|
+
// Whether the static unevaluatedProperties model checks the anyOf itself, so
|
|
3890
|
+
// genCodeNode leaves it out. Not when the set is built at run time
|
|
3891
|
+
// (unevalBuiltAtRuntime): that form only collects, and dropping the anyOf
|
|
3892
|
+
// there accepted a document no branch matched.
|
|
3511
3893
|
function unevalChecksAnyOf(schema, ctx) {
|
|
3512
3894
|
if (schema.unevaluatedProperties !== false) return false
|
|
3513
3895
|
const r = collectEvaluated(schema, ctx.schemaMap, ctx.rootDefs)
|
|
3514
|
-
return r.dynamic && !r.allProps
|
|
3896
|
+
return r.dynamic && !r.allProps && !unevalBuiltAtRuntime(schema, ctx, r)
|
|
3897
|
+
}
|
|
3898
|
+
|
|
3899
|
+
// The unevaluatedProperties forms the static model cannot represent; those
|
|
3900
|
+
// build the evaluated set at run time (genUnevalDynamicVerdict). Decided in
|
|
3901
|
+
// one place so genCodeNode and genUnevaluatedPropertiesNode cannot disagree
|
|
3902
|
+
// about which of them checks the node's anyOf.
|
|
3903
|
+
function unevalBuiltAtRuntime(schema, ctx, r) {
|
|
3904
|
+
if (schema.unevaluatedProperties === true || schema.unevaluatedProperties === undefined) return false
|
|
3905
|
+
if (r === undefined) r = collectEvaluated(schema, ctx.schemaMap, ctx.rootDefs)
|
|
3906
|
+
return r.dynamic && (r.allProps || !unevalPropsModeled(schema, ctx.schemaMap))
|
|
3515
3907
|
}
|
|
3516
3908
|
|
|
3517
3909
|
function genUnevaluatedPropertiesNode(schema, v, lines, ctx, knownType, isObj, hoisted, earlyKeyCount) {
|
|
3518
3910
|
const evalResult = collectEvaluated(schema, ctx.schemaMap, ctx.rootDefs)
|
|
3911
|
+
if (unevalBuiltAtRuntime(schema, ctx, evalResult)) {
|
|
3912
|
+
genUnevalDynamicVerdict(schema, v, lines, ctx, isObj)
|
|
3913
|
+
return
|
|
3914
|
+
}
|
|
3519
3915
|
|
|
3520
3916
|
if (evalResult.allProps || schema.unevaluatedProperties === true) {
|
|
3521
3917
|
// All props evaluated or unevaluatedProperties:true — no-op
|
|
@@ -4086,7 +4482,11 @@ const FORMAT_CODEGEN = {
|
|
|
4086
4482
|
function esc(s) { return JSON.stringify(s).slice(1, -1).replace(/'/g, "\\'") }
|
|
4087
4483
|
// A schema pointer segment for a pattern: JSON Pointer escaping, then made
|
|
4088
4484
|
// safe for the single-quoted literal the error emitters use.
|
|
4089
|
-
|
|
4485
|
+
// A pointer segment as text for a single-quoted literal. Control characters
|
|
4486
|
+
// and the two line separators go in as \u escapes: written raw, a property
|
|
4487
|
+
// named "foo\nbar" broke the literal, the combined function failed to compile
|
|
4488
|
+
// and every schema with such a name lost it.
|
|
4489
|
+
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')) }
|
|
4090
4490
|
|
|
4091
4491
|
// Resolve child path at codegen time when parent is a static string literal.
|
|
4092
4492
|
// This enables frozen pre-allocation for ALL nested error objects.
|
|
@@ -4331,7 +4731,7 @@ function unevalLocalOk (node, key) {
|
|
|
4331
4731
|
// generic and may visit data positions (an enum value, say); judging those is
|
|
4332
4732
|
// harmless, since a data node that fails the test only makes the answer more
|
|
4333
4733
|
// conservative and the generators never read data positions as schemas.
|
|
4334
|
-
function unevalAllProvablyLocal (root) {
|
|
4734
|
+
function unevalAllProvablyLocal (root, only) {
|
|
4335
4735
|
const seen = new Set()
|
|
4336
4736
|
const stack = [root]
|
|
4337
4737
|
while (stack.length) {
|
|
@@ -4339,7 +4739,7 @@ function unevalAllProvablyLocal (root) {
|
|
|
4339
4739
|
if (n === null || typeof n !== 'object' || seen.has(n)) continue
|
|
4340
4740
|
seen.add(n)
|
|
4341
4741
|
if (!Array.isArray(n)) {
|
|
4342
|
-
if (n.unevaluatedProperties !== undefined && !unevalLocalOk(n, 'unevaluatedProperties')) return false
|
|
4742
|
+
if (only !== 'items' && n.unevaluatedProperties !== undefined && !unevalLocalOk(n, 'unevaluatedProperties')) return false
|
|
4343
4743
|
if (n.unevaluatedItems !== undefined && !unevalLocalOk(n, 'unevaluatedItems')) return false
|
|
4344
4744
|
}
|
|
4345
4745
|
for (const k of Array.isArray(n) ? n : Object.values(n)) stack.push(k)
|
|
@@ -4396,7 +4796,7 @@ function genCharCodeSwitch(keys, v) {
|
|
|
4396
4796
|
// Valid path is still fast — only error path does extra work.
|
|
4397
4797
|
function compileToJSCodegenWithErrors(schema, schemaMap, userFormats, sourceOpts) {
|
|
4398
4798
|
const inputSchema = schema
|
|
4399
|
-
schema =
|
|
4799
|
+
schema = prepareForCodegen(schema, schemaMap)
|
|
4400
4800
|
// unevaluated* is generated only where it is provably its plain counterpart
|
|
4401
4801
|
// (see unevalLocalOk); any occurrence that is not keeps the whole schema on
|
|
4402
4802
|
// the interpreted engine's error path, exactly as before.
|
|
@@ -4413,7 +4813,6 @@ function compileToJSCodegenWithErrors(schema, schemaMap, userFormats, sourceOpts
|
|
|
4413
4813
|
if (!sharedCodegenGate(schema, schemaMap)) return null
|
|
4414
4814
|
if (schema.patternProperties) {
|
|
4415
4815
|
for (const [pat, sub] of Object.entries(schema.patternProperties)) {
|
|
4416
|
-
if (/\\[pP]\{/.test(pat)) return null
|
|
4417
4816
|
if (typeof sub === 'object' && sub !== null && !codegenSafe(sub, schemaMap)) return null
|
|
4418
4817
|
}
|
|
4419
4818
|
}
|
|
@@ -4610,6 +5009,50 @@ function compileToJSCodegenWithErrors(schema, schemaMap, userFormats, sourceOpts
|
|
|
4610
5009
|
// Emit codegen for oneOf/anyOf that runs each branch with error collection,
|
|
4611
5010
|
// aggregates results, and uses __ataCollapse to surface a single best-branch
|
|
4612
5011
|
// error. Wraps the existing per-branch evaluation; does not replace it.
|
|
5012
|
+
const BRANCH_VERDICT_MAX = 4096
|
|
5013
|
+
|
|
5014
|
+
// What undoing a trial emission needs: during one, the context's arrays only
|
|
5015
|
+
// grow and its maps and sets only gain keys (a reference pushed onto refStack
|
|
5016
|
+
// by a declined branch is such a key), so their lengths and sizes are enough,
|
|
5017
|
+
// and what was added is cut off the end, in insertion order. Copying them
|
|
5018
|
+
// instead made the trial quadratic: 297 oneOf and anyOf on apollo-router,
|
|
5019
|
+
// each copying a helper list thousands long.
|
|
5020
|
+
function snapshotCtx (ctx) {
|
|
5021
|
+
const out = new Map()
|
|
5022
|
+
for (const k of Object.keys(ctx)) {
|
|
5023
|
+
const x = ctx[k]
|
|
5024
|
+
if (Array.isArray(x)) out.set(k, { kind: 'a', ref: x, n: x.length })
|
|
5025
|
+
else if (x instanceof Map || x instanceof Set) out.set(k, { kind: 'c', ref: x, n: x.size })
|
|
5026
|
+
else out.set(k, { kind: 'v', ref: x })
|
|
5027
|
+
}
|
|
5028
|
+
return out
|
|
5029
|
+
}
|
|
5030
|
+
function restoreCtx (ctx, saved) {
|
|
5031
|
+
for (const k of Object.keys(ctx)) if (!saved.has(k)) delete ctx[k]
|
|
5032
|
+
for (const [k, e] of saved) {
|
|
5033
|
+
if (e.kind === 'a') { e.ref.length = Math.min(e.ref.length, e.n); ctx[k] = e.ref }
|
|
5034
|
+
else if (e.kind === 'c') {
|
|
5035
|
+
const extra = e.ref.size - e.n
|
|
5036
|
+
if (extra > 0) {
|
|
5037
|
+
const added = []
|
|
5038
|
+
let i = 0
|
|
5039
|
+
for (const key of e.ref.keys()) if (i++ >= e.n) added.push(key)
|
|
5040
|
+
for (const key of added) e.ref.delete(key)
|
|
5041
|
+
}
|
|
5042
|
+
ctx[k] = e.ref
|
|
5043
|
+
} else ctx[k] = e.ref
|
|
5044
|
+
}
|
|
5045
|
+
}
|
|
5046
|
+
|
|
5047
|
+
// Whether a node holds a reference anywhere below it.
|
|
5048
|
+
function holdsRef (node, seen = new Set()) {
|
|
5049
|
+
if (node === null || typeof node !== 'object' || seen.has(node)) return false
|
|
5050
|
+
seen.add(node)
|
|
5051
|
+
if (typeof node.$ref === 'string' || typeof node.$dynamicRef === 'string') return true
|
|
5052
|
+
for (const k of Object.keys(node)) if (holdsRef(node[k], seen)) return true
|
|
5053
|
+
return false
|
|
5054
|
+
}
|
|
5055
|
+
|
|
4613
5056
|
function emitBranchCollapse (branches, keyword, v, pathExpr, lines, ctx, schemaPrefix) {
|
|
4614
5057
|
ctx.usesBranchCollapse = true
|
|
4615
5058
|
const fi = ctx.varCounter++
|
|
@@ -4629,7 +5072,14 @@ function emitBranchCollapse (branches, keyword, v, pathExpr, lines, ctx, schemaP
|
|
|
4629
5072
|
const subLines = []
|
|
4630
5073
|
const subSp = branchSp + '/' + i
|
|
4631
5074
|
if (typeof sub === 'object' && sub !== null) {
|
|
4632
|
-
|
|
5075
|
+
// A branch is a function of its own with a local `_e`; a collapse
|
|
5076
|
+
// inside it reports there, not into the combined function's lists.
|
|
5077
|
+
ctx.inBranchFn = (ctx.inBranchFn || 0) + 1
|
|
5078
|
+
try {
|
|
5079
|
+
genCodeE(sub, '_bv', '_bpth', subLines, ctx, subSp)
|
|
5080
|
+
} finally {
|
|
5081
|
+
ctx.inBranchFn--
|
|
5082
|
+
}
|
|
4633
5083
|
} else if (sub === false) {
|
|
4634
5084
|
// A false branch never matches; it reports the same error the
|
|
4635
5085
|
// interpreter attaches to a false schema.
|
|
@@ -4653,8 +5103,101 @@ function emitBranchCollapse (branches, keyword, v, pathExpr, lines, ctx, schemaP
|
|
|
4653
5103
|
const pp = pathExpr || '""'
|
|
4654
5104
|
ctx.helperCode.push(`const ${fnArr}=[${fns.map((b) => b === '' ? `function(){return{valid:true,errors:[]}}` : `function(${params}){${b}}`).join(',')}];const _bt${fi}=${titleArr}`)
|
|
4655
5105
|
const call = `${fnArr}[_bi](${v},_pp${fi},_all${usesSp ? ',_sp' : ''})`
|
|
4656
|
-
|
|
4657
|
-
|
|
5106
|
+
// branchSp is source text for a quoted literal, with backslashes and quotes
|
|
5107
|
+
// escaped; the ordinal is looked up by the path itself. Looked up by the
|
|
5108
|
+
// text, a path through a pattern such as `^\\d+$` found no node, or the
|
|
5109
|
+
// wrong one, and the collapsed error sorted out of place against the other
|
|
5110
|
+
// engines.
|
|
5111
|
+
// In the runtime shape there is no ordering key (the reader looks the path
|
|
5112
|
+
// up), so the collapsed error is in its final shape as it is built.
|
|
5113
|
+
const ord = ctx.noOrdinal ? 'null' : (() => { const o = ordinalFor(ctx.rootSchema, unescapeSp(branchSp)); return o === null ? 'null' : o })()
|
|
5114
|
+
// In the combined function `_e` is created on the first error and every
|
|
5115
|
+
// error is collected (`_all` is true there), so the report only pushes.
|
|
5116
|
+
const report = ctx.inCombined && !ctx.inBranchFn
|
|
5117
|
+
? `const ${collapsed}=__ataCollapse('${branchKw}',${resArr},_pp${fi},'${branchSp}',${ord},_bt${fi});if(${collapsed})(_e||(_e=[])).push(${collapsed})`
|
|
5118
|
+
: `const ${collapsed}=__ataCollapse('${branchKw}',${resArr},_pp${fi},'${branchSp}',${ord},_bt${fi});if(${collapsed}){_e.push(${collapsed});if(!_all)return{valid:false,errors:_e}}`
|
|
5119
|
+
// The node's runtime unevaluatedProperties already counted how many of
|
|
5120
|
+
// these branches pass, through the annotation functions, which answer the
|
|
5121
|
+
// same validity. A oneOf with exactly one, an anyOf with any, holds and
|
|
5122
|
+
// reports nothing, so its branches are not run a second time to say so.
|
|
5123
|
+
// Only in the combined function proper, where that count is in scope.
|
|
5124
|
+
const counted = ctx.inCombined && !ctx.inBranchFn && ctx.branchCounts ? ctx.branchCounts.get(branches) : undefined
|
|
5125
|
+
const same = counted !== undefined ? `${v}!==null&&typeof ${v}==='object'&&${counted.of}===${v}` : ''
|
|
5126
|
+
let guard = counted !== undefined
|
|
5127
|
+
? (keyword === 'anyOf' ? `if(!(${same}&&${counted.count}>=1))` : `if(!(${same}&&${counted.count}===1))`)
|
|
5128
|
+
: ''
|
|
5129
|
+
// More than one oneOf branch passed: the error names them and carries no
|
|
5130
|
+
// branch errors, so it is built from the bits the count left.
|
|
5131
|
+
if (guard !== '' && keyword === 'oneOf' && counted.mask && ctx.inCombined && !ctx.inBranchFn) {
|
|
5132
|
+
const ord0 = ctx.noOrdinal ? 'null' : (() => { const o = ordinalFor(ctx.rootSchema, unescapeSp(branchSp)); return o === null ? 'null' : o })()
|
|
5133
|
+
guard = `if(${same}&&${counted.count}>1){const _pi${fi}=[];for(let _k=0;_k<${branches.length};_k++)if((${counted.mask}>>_k)&1)_pi${fi}.push(_k);(_e||(_e=[])).push(__ataMulti(_pi${fi},${branches.length},${pp},'${branchSp}',${ord0}))}else if(!(${same}&&${counted.count}===1))`
|
|
5134
|
+
}
|
|
5135
|
+
// Without such a count, each branch also gets a verdict function, and the
|
|
5136
|
+
// passing branches are counted with those: a oneOf with exactly one and an
|
|
5137
|
+
// anyOf with any hold, and a oneOf with more than one is reported from the
|
|
5138
|
+
// bits, so the error-collecting branches run only when no branch passes
|
|
5139
|
+
// and the collapse needs their errors to pick the closest. Running them to
|
|
5140
|
+
// learn the count built a result object and an error list per branch,
|
|
5141
|
+
// which made a oneOf matched twice cost 69 ns where counting costs about
|
|
5142
|
+
// 12. A branch the verdict generator declines keeps the old path.
|
|
5143
|
+
let vfArr = null
|
|
5144
|
+
// Branches that hold a reference are left to the error branches: their
|
|
5145
|
+
// verdicts would expand every definition again, as a second copy, where the
|
|
5146
|
+
// error branches call the shared one.
|
|
5147
|
+
if (guard === '' && !branches.some((b) => holdsRef(b))) {
|
|
5148
|
+
// A trial: the generators are not exception-safe on the context (a
|
|
5149
|
+
// reference left on refStack by a declined branch made the next error
|
|
5150
|
+
// function write that branch as nothing, which read as valid), so the
|
|
5151
|
+
// context is put back as it was when the verdicts decline.
|
|
5152
|
+
const saved = snapshotCtx(ctx)
|
|
5153
|
+
try {
|
|
5154
|
+
const vfs = branches.map((sub) => {
|
|
5155
|
+
if (sub === true) return 'function(){return true}'
|
|
5156
|
+
if (sub === false) return 'function(){return false}'
|
|
5157
|
+
const bl = []
|
|
5158
|
+
nestedGenCode(sub, '_bv', bl, ctx)
|
|
5159
|
+
return bl.length === 0 ? 'function(){return true}' : `function(_bv){${bl.join(';')};return true}`
|
|
5160
|
+
})
|
|
5161
|
+
// Only while the verdicts are small: they are a second copy of the
|
|
5162
|
+
// branches, with every reference expanded, and on large branches that
|
|
5163
|
+
// copy cost more to compile than counting saves (apollo-router's error
|
|
5164
|
+
// function grew from 1.5 to 2.5 MB of source).
|
|
5165
|
+
if (!vfs.some((b) => b.includes(SP_PLACEHOLDER)) && vfs.reduce((n, b) => n + b.length, 0) <= BRANCH_VERDICT_MAX) {
|
|
5166
|
+
// One constant per branch, called directly: through an array the one
|
|
5167
|
+
// call site sees every branch and none of them is inlined.
|
|
5168
|
+
vfArr = `_bvf${fi}`
|
|
5169
|
+
ctx.helperCode.push(vfs.map((b, i) => `const ${vfArr}_${i}=${b}`).join(';'))
|
|
5170
|
+
} else {
|
|
5171
|
+
restoreCtx(ctx, saved)
|
|
5172
|
+
}
|
|
5173
|
+
} catch (e) {
|
|
5174
|
+
if (e !== DECLINE) throw e
|
|
5175
|
+
restoreCtx(ctx, saved)
|
|
5176
|
+
}
|
|
5177
|
+
}
|
|
5178
|
+
if (vfArr !== null) {
|
|
5179
|
+
const L = branches.length
|
|
5180
|
+
const top = ctx.inCombined && !ctx.inBranchFn
|
|
5181
|
+
// An accepted document is still answered by the verdict first (see the
|
|
5182
|
+
// result shape in compileToJSCombined): that is the fastest path to it,
|
|
5183
|
+
// and dropping it measured 20 ns slower per accepted oneOf.
|
|
5184
|
+
if (top) ctx.unguardedCollapse = true
|
|
5185
|
+
const tests = branches.map((_, i) => `${vfArr}_${i}(${v})`)
|
|
5186
|
+
if (keyword === 'anyOf') {
|
|
5187
|
+
lines.push(`{const _pp${fi}=${pp};if(!(${tests.join('||')})){if(_all===0){_e.push(null);return{valid:false,errors:_e}}const ${resArr}=[];for(let _bi=0;_bi<${L};_bi++){const _br=${call};${resArr}.push(_br.valid?{valid:false,errors:_br.errors}:_br)}${report}}}`)
|
|
5188
|
+
} else {
|
|
5189
|
+
const mk = L <= 30
|
|
5190
|
+
const collect = mk
|
|
5191
|
+
? `const _pi${fi}=[];for(let _k=0;_k<${L};_k++)if((_m${fi}>>_k)&1)_pi${fi}.push(_k);`
|
|
5192
|
+
: ''
|
|
5193
|
+
const multi = `__ataMulti(_pi${fi},${L},_pp${fi},'${branchSp}',${ord})`
|
|
5194
|
+
const pushMulti = top ? `(_e||(_e=[])).push(${multi})` : `_e.push(${multi});if(!_all)return{valid:false,errors:_e}`
|
|
5195
|
+
const count = tests.map((t, i) => `if(${t}){_n${fi}++;${mk ? `_m${fi}|=${1 << i}` : `_pi${fi}.push(${i})`}}`).join('')
|
|
5196
|
+
lines.push(`{const _pp${fi}=${pp};let _n${fi}=0${mk ? `,_m${fi}=0` : `,_pi${fi}=[]`};${count}if(_n${fi}!==1){if(_all===0){_e.push(null);return{valid:false,errors:_e}}if(_n${fi}>1){${collect}${pushMulti}}else{const ${resArr}=[];for(let _bi=0;_bi<${L};_bi++)${resArr}.push(${call});${report}}}}`)
|
|
5197
|
+
}
|
|
5198
|
+
return
|
|
5199
|
+
}
|
|
5200
|
+
if (guard === '' && ctx.inCombined && !ctx.inBranchFn) ctx.unguardedCollapse = true
|
|
4658
5201
|
if (keyword === 'anyOf') {
|
|
4659
5202
|
// anyOf reports nothing once a branch passes, so the branches are first
|
|
4660
5203
|
// only asked whether they pass. They are called with `_all` set to 0, a
|
|
@@ -4668,12 +5211,12 @@ function emitBranchCollapse (branches, keyword, v, pathExpr, lines, ctx, schemaP
|
|
|
4668
5211
|
// no one read. Only when no branch passes do they run again with the
|
|
4669
5212
|
// caller's `_all`, and the collapse sees them all, as before.
|
|
4670
5213
|
const probe = `${fnArr}[_bi](${v},_pp${fi},0${usesSp ? ',_sp' : ''})`
|
|
4671
|
-
lines.push(
|
|
5214
|
+
lines.push(`${guard}{const _pp${fi}=${pp};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}}const ${resArr}=[];for(let _bi=0;_bi<${fnArr}.length;_bi++){const _br=${call};${resArr}.push(_br.valid?{valid:false,errors:_br.errors}:_br)}${report}}}`)
|
|
4672
5215
|
} else {
|
|
4673
5216
|
// Asked only whether it passes (`_all` 0, see anyOf above), oneOf counts
|
|
4674
5217
|
// the passing branches and answers; the branch errors of a probe are not
|
|
4675
5218
|
// errors anyone reads and are not collapsed.
|
|
4676
|
-
lines.push(
|
|
5219
|
+
lines.push(`${guard}{const _pp${fi}=${pp};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}}}`)
|
|
4677
5220
|
}
|
|
4678
5221
|
}
|
|
4679
5222
|
|
|
@@ -4701,6 +5244,13 @@ function genCodeENode(schema, v, pathExpr, lines, ctx, schemaPrefix) {
|
|
|
4701
5244
|
}
|
|
4702
5245
|
// $ref — resolve local and cross-schema refs
|
|
4703
5246
|
if (schema.$ref) {
|
|
5247
|
+
// The reference is followed and the rest of the node is not compiled; a
|
|
5248
|
+
// keyword beside it that validates would be dropped, so the node
|
|
5249
|
+
// declines (as in genCodeCNode). Reached since unevaluatedProperties
|
|
5250
|
+
// next to a $ref stopped declining the combined function, whose oneOf
|
|
5251
|
+
// and anyOf branches come from this generator: a branch was taken as
|
|
5252
|
+
// passing with its siblings unchecked.
|
|
5253
|
+
for (const k of Object.keys(schema)) if (!REF_NEUTRAL_SIBLINGS.has(k) && !k.startsWith('x-')) throw DECLINE
|
|
4704
5254
|
// A self-reference cannot be expressed here yet: the root helper takes no
|
|
4705
5255
|
// path argument, so errors raised through it would carry the path of the
|
|
4706
5256
|
// root rather than of the value that failed. This used to fall through as
|
|
@@ -4801,7 +5351,9 @@ function genCodeENode(schema, v, pathExpr, lines, ctx, schemaPrefix) {
|
|
|
4801
5351
|
}
|
|
4802
5352
|
})
|
|
4803
5353
|
const expected = types.join(', ')
|
|
4804
|
-
|
|
5354
|
+
// As written: a one-element array stays an array, as the interpreted engine
|
|
5355
|
+
// reports it.
|
|
5356
|
+
const expectedParam = !Array.isArray(schema.type) ? `'${types[0]}'` : JSON.stringify(types)
|
|
4805
5357
|
{
|
|
4806
5358
|
const typeSp = `${schemaPrefix}/type`
|
|
4807
5359
|
const lit = buildErrorLiteral({ keyword: 'type', schemaPath: typeSp, sourceMap: ctx.sourceMap })
|
|
@@ -4832,7 +5384,7 @@ function genCodeENode(schema, v, pathExpr, lines, ctx, schemaPrefix) {
|
|
|
4832
5384
|
if (cv === null || typeof cv !== 'object') {
|
|
4833
5385
|
lines.push(`if(${v}!==${JSON.stringify(cv)}){${fail('const', 'const', `{allowedValue:${JSON.stringify(schema.const)}}`, "'must be equal to constant'")}}`)
|
|
4834
5386
|
} else {
|
|
4835
|
-
lines.push(`if(!${emitDeq(ctx)}(${v},${emitConstant(ctx, cv)})){${fail('const', 'const', `{allowedValue
|
|
5387
|
+
lines.push(`if(!${emitDeq(ctx)}(${v},${emitConstant(ctx, cv)})){${fail('const', 'const', `{allowedValue:${emitParamConstant(ctx, schema.const)}}`, "'must be equal to constant'")}}`)
|
|
4836
5388
|
}
|
|
4837
5389
|
}
|
|
4838
5390
|
|
|
@@ -4901,7 +5453,7 @@ function genCodeENode(schema, v, pathExpr, lines, ctx, schemaPrefix) {
|
|
|
4901
5453
|
ctx.helperCode.push(`const _re${ri}=__ataSafeRe(${pattern})`)
|
|
4902
5454
|
ctx.usesSafeRe = true
|
|
4903
5455
|
} else {
|
|
4904
|
-
ctx.helperCode.push(`const _re${ri}=new RegExp(${pattern})`)
|
|
5456
|
+
ctx.helperCode.push(`const _re${ri}=new RegExp(${pattern}${reFlagArg(pattern)})`)
|
|
4905
5457
|
}
|
|
4906
5458
|
}
|
|
4907
5459
|
const ri = ctx.regExpMap.get(pattern);
|
|
@@ -4976,7 +5528,7 @@ function genCodeENode(schema, v, pathExpr, lines, ctx, schemaPrefix) {
|
|
|
4976
5528
|
ctx.helperCode.push(`const _re${ri}=__ataSafeRe(${pattern})`)
|
|
4977
5529
|
ctx.usesSafeRe = true
|
|
4978
5530
|
} else {
|
|
4979
|
-
ctx.helperCode.push(`const _re${ri}=new RegExp(${pattern})`)
|
|
5531
|
+
ctx.helperCode.push(`const _re${ri}=new RegExp(${pattern}${reFlagArg(pattern)})`)
|
|
4980
5532
|
}
|
|
4981
5533
|
}
|
|
4982
5534
|
patChecks.push(`_re${ctx.regExpMap.get(pattern)}.test(_k${ci}[_i])`)
|
|
@@ -5002,6 +5554,12 @@ function genCodeENode(schema, v, pathExpr, lines, ctx, schemaPrefix) {
|
|
|
5002
5554
|
// to emit. The error keeps the keyword's own identity: the interpreted
|
|
5003
5555
|
// engine reports these as unevaluatedProperties with the key named, and the
|
|
5004
5556
|
// two engines are pinned to identical output.
|
|
5557
|
+
// Inside the combined function (a oneOf or anyOf branch, see
|
|
5558
|
+
// emitBranchCollapse) this generator only has the static form. A branch
|
|
5559
|
+
// whose evaluated set depends on its own branches would report the wrong
|
|
5560
|
+
// errors, and the collapse would pick the wrong closest branch; declined.
|
|
5561
|
+
if (ctx.inCombined && schema.unevaluatedProperties !== undefined && schema.unevaluatedProperties !== true && !unevalLocalOk(schema, 'unevaluatedProperties')) throw DECLINE
|
|
5562
|
+
if (ctx.inCombined && schema.unevaluatedItems !== undefined && schema.unevaluatedItems !== true && !unevalLocalOk(schema, 'unevaluatedItems')) throw DECLINE
|
|
5005
5563
|
if (schema.unevaluatedProperties === false && schema.additionalProperties === undefined) genUnevaluatedFalseE(schema, v, pathExpr, lines, ctx, schemaPrefix, isObj)
|
|
5006
5564
|
|
|
5007
5565
|
// unevaluatedItems: false under the same rule. The interpreted engine
|
|
@@ -5112,18 +5670,10 @@ function genDynamicRefE(schema, v, pathExpr, lines, ctx, schemaPrefix) {
|
|
|
5112
5670
|
|
|
5113
5671
|
function genUniqueItemsE(schema, v, pathExpr, lines, ctx, schemaPrefix, fail, isArr) {
|
|
5114
5672
|
const si = ctx.varCounter++
|
|
5115
|
-
const itemType = schema.items && typeof schema.items === 'object' && schema.items.type
|
|
5116
|
-
const isPrim = itemType === 'string' || itemType === 'number' || itemType === 'integer'
|
|
5117
|
-
const maxItems = schema.maxItems
|
|
5118
5673
|
const failExpr = (iVar, jVar) => fail('uniqueItems', 'uniqueItems', `{i:${iVar},j:${jVar}}`, `'must NOT have duplicate items (items ## '+${jVar}+' and '+${iVar}+' are identical)'`)
|
|
5119
|
-
|
|
5120
|
-
|
|
5121
|
-
|
|
5122
|
-
} else if (isPrim) {
|
|
5123
|
-
inner = `const _s${si}=new Map();for(let _i=0;_i<${v}.length;_i++){const _prev=_s${si}.get(${v}[_i]);if(_prev!==undefined){${failExpr('_prev', '_i')};break};_s${si}.set(${v}[_i],_i)}`
|
|
5124
|
-
} else {
|
|
5125
|
-
inner = `const _cn${si}=function(x){if(x===null||typeof x!=='object')return typeof x+':'+x;if(Array.isArray(x))return'['+x.map(_cn${si}).join(',')+']';return'{'+Object.keys(x).sort().map(function(k){return JSON.stringify(k)+':'+_cn${si}(x[k])}).join(',')+'}'};const _s${si}=new Map();for(let _i=0;_i<${v}.length;_i++){const _k=_cn${si}(${v}[_i]);const _prev=_s${si}.get(_k);if(_prev!==undefined){${failExpr('_prev', '_i')};break};_s${si}.set(_k,_i)}`
|
|
5126
|
-
}
|
|
5674
|
+
// Only reached once the verdict has rejected, or when errors are wanted, so
|
|
5675
|
+
// one strategy for every item type: the hoisted pair finder. See UQP_HELPER.
|
|
5676
|
+
const inner = `const _j${si}=${emitUqp(ctx)}(${v});if(_j${si}>=0){const _i${si}=_uqpI;${failExpr('_i' + si, '_j' + si)}}`
|
|
5127
5677
|
lines.push(isArr ? `{${inner}}` : `if(Array.isArray(${v})){${inner}}`)
|
|
5128
5678
|
}
|
|
5129
5679
|
|
|
@@ -5142,7 +5692,7 @@ function genUnevaluatedFalseE(schema, v, pathExpr, lines, ctx, schemaPrefix, isO
|
|
|
5142
5692
|
ctx.helperCode.push(`const _re${ri}=__ataSafeRe(${pattern})`)
|
|
5143
5693
|
ctx.usesSafeRe = true
|
|
5144
5694
|
} else {
|
|
5145
|
-
ctx.helperCode.push(`const _re${ri}=new RegExp(${pattern})`)
|
|
5695
|
+
ctx.helperCode.push(`const _re${ri}=new RegExp(${pattern}${reFlagArg(pattern)})`)
|
|
5146
5696
|
}
|
|
5147
5697
|
}
|
|
5148
5698
|
patChecksU.push(`_re${ctx.regExpMap.get(pattern)}.test(_k${ui}[_i])`)
|
|
@@ -5174,7 +5724,7 @@ function genPatternPropertiesE(schema, v, pathExpr, lines, ctx, schemaPrefix) {
|
|
|
5174
5724
|
ctx.helperCode.push(`const _re${ri}=__ataSafeRe(${pattern})`);
|
|
5175
5725
|
ctx.usesSafeRe = true
|
|
5176
5726
|
} else {
|
|
5177
|
-
ctx.helperCode.push(`const _re${ri}=new RegExp(${pattern})`);
|
|
5727
|
+
ctx.helperCode.push(`const _re${ri}=new RegExp(${pattern}${reFlagArg(pattern)})`);
|
|
5178
5728
|
}
|
|
5179
5729
|
}
|
|
5180
5730
|
const ri = ctx.regExpMap.get(pattern);
|
|
@@ -5204,7 +5754,7 @@ function genAdditionalSchemaE(schema, v, pathExpr, lines, ctx, schemaPrefix) {
|
|
|
5204
5754
|
ctx.helperCode.push(`const _re${ri}=__ataSafeRe(${pattern})`)
|
|
5205
5755
|
ctx.usesSafeRe = true
|
|
5206
5756
|
} else {
|
|
5207
|
-
ctx.helperCode.push(`const _re${ri}=new RegExp(${pattern})`)
|
|
5757
|
+
ctx.helperCode.push(`const _re${ri}=new RegExp(${pattern}${reFlagArg(pattern)})`)
|
|
5208
5758
|
}
|
|
5209
5759
|
}
|
|
5210
5760
|
patTests.push(`_re${ctx.regExpMap.get(pattern)}.test(_k${ki})`)
|
|
@@ -5256,7 +5806,7 @@ function genPropertyNamesE(schema, v, pathExpr, lines, ctx, schemaPrefix, fail)
|
|
|
5256
5806
|
ctx.helperCode.push(`const _re${ri}=__ataSafeRe(${pattern})`);
|
|
5257
5807
|
ctx.usesSafeRe = true
|
|
5258
5808
|
} else {
|
|
5259
|
-
ctx.helperCode.push(`const _re${ri}=new RegExp(${pattern})`);
|
|
5809
|
+
ctx.helperCode.push(`const _re${ri}=new RegExp(${pattern}${reFlagArg(pattern)})`);
|
|
5260
5810
|
}
|
|
5261
5811
|
}
|
|
5262
5812
|
const ri = ctx.regExpMap.get(pattern);
|
|
@@ -5338,29 +5888,80 @@ function genIfE(schema, v, pathExpr, lines, ctx, schemaPrefix) {
|
|
|
5338
5888
|
// Returns VALID_RESULT for valid data, {valid:false, errors} for invalid.
|
|
5339
5889
|
// Avoids double-pass (jsFn → false → errFn runs same checks again).
|
|
5340
5890
|
// Uses type-aware optimizations: after type check passes, skip guards.
|
|
5891
|
+
// An error literal the combined function shares between calls, frozen. Two
|
|
5892
|
+
// non-enumerable fields ride along, invisible to Object.keys, spread and
|
|
5893
|
+
// JSON: `_s` says the object is shared, so the reader copies it before handing
|
|
5894
|
+
// it out (Object.isFrozen, the check it replaces, cost more than the copy),
|
|
5895
|
+
// and `_o` is its schema ordinal, known here and otherwise looked up by path
|
|
5896
|
+
// on every read. An `_o` the literal already carries, for standalone output,
|
|
5897
|
+
// is kept. `_t` is an empty slot lib/enrich-error.js fills once.
|
|
5898
|
+
function sharedErr (lit, rootSchema) {
|
|
5899
|
+
Object.defineProperty(lit, '_s', { value: true })
|
|
5900
|
+
// A slot the enrichment fills on the first read with the fields that depend
|
|
5901
|
+
// only on this literal (code, expected, rank), so later reads skip them.
|
|
5902
|
+
Object.defineProperty(lit, '_t', { value: {} })
|
|
5903
|
+
if (lit._o === undefined) {
|
|
5904
|
+
const o = ordinalFor(rootSchema, lit.schemaPath)
|
|
5905
|
+
if (typeof o === 'number') Object.defineProperty(lit, '_o', { value: o })
|
|
5906
|
+
}
|
|
5907
|
+
return Object.freeze(lit)
|
|
5908
|
+
}
|
|
5909
|
+
|
|
5910
|
+
// The push for one error in the combined generator. With `ctx.rich` (the
|
|
5911
|
+
// validator's errors are being read and enriched, see validator-core) the
|
|
5912
|
+
// error is built in its final enriched shape where it happens, by makeRich in
|
|
5913
|
+
// lib/enrich-site.js with a site record made here at compile time; otherwise
|
|
5914
|
+
// it is the plain literal `lean` the generator always emitted. `valueExpr` is
|
|
5915
|
+
// the value at `pathExpr`, which enrichment otherwise finds again by walking
|
|
5916
|
+
// the document. `paramsShared` names a frozen params object hoisted for a
|
|
5917
|
+
// static site, so a static error's params stay one shared object as before.
|
|
5918
|
+
function errPushC(ctx, keyword, sp, pathExpr, paramsCode, msgCode, valueExpr, lean, paramsShared) {
|
|
5919
|
+
if (!ctx.rich) return `(_e||(_e=[])).push(${lean})`
|
|
5920
|
+
if (!ctx.richMk) {
|
|
5921
|
+
ctx.richMk = true
|
|
5922
|
+
const { makeRich } = require('./enrich-site')
|
|
5923
|
+
ctx.closureVars.push('_mk'); ctx.closureVals.push(makeRich)
|
|
5924
|
+
}
|
|
5925
|
+
let paramsVal
|
|
5926
|
+
try { paramsVal = Function('return ' + paramsCode)() } catch { paramsVal = undefined }
|
|
5927
|
+
const fmt = keyword === 'format' && paramsVal ? paramsVal.format : undefined
|
|
5928
|
+
const { errorSite } = require('./enrich-site')
|
|
5929
|
+
const siteVar = `_S${ctx.varCounter++}`
|
|
5930
|
+
ctx.closureVars.push(siteVar)
|
|
5931
|
+
// Inside a shared definition (emitSharedDefC) the site holds the path past
|
|
5932
|
+
// the definition and the reference's own comes in as `_sp`.
|
|
5933
|
+
const rel = sp.startsWith(SP_PLACEHOLDER)
|
|
5934
|
+
ctx.closureVals.push(errorSite(keyword, unescapeSp(rel ? sp.slice(SP_PLACEHOLDER.length) : sp), paramsVal, fmt))
|
|
5935
|
+
return `(_e||(_e=[])).push(_mk(${siteVar},${pathExpr || '""'},${paramsShared || paramsCode},${msgCode},${valueExpr},d${rel ? ',_sp' : ''}))`
|
|
5936
|
+
}
|
|
5937
|
+
|
|
5341
5938
|
function compileToJSCombined(schema, VALID_RESULT, schemaMap, userFormats, opts) {
|
|
5342
5939
|
const inputSchema = schema
|
|
5343
|
-
schema =
|
|
5940
|
+
schema = prepareForCodegen(schema, schemaMap)
|
|
5344
5941
|
// Same provably-local rule as the error generator above.
|
|
5345
5942
|
if (typeof schema === 'object' && schema !== null) {
|
|
5346
5943
|
const s = JSON.stringify(schema)
|
|
5347
|
-
|
|
5348
|
-
//
|
|
5349
|
-
|
|
5350
|
-
//
|
|
5351
|
-
//
|
|
5352
|
-
//
|
|
5353
|
-
if (cyclicDefNames(schema.$defs || schema.definitions).size) return null
|
|
5354
|
-
// Bail on additionalProperties as a schema — combined path doesn't emit the
|
|
5355
|
-
// per-key sub-schema loop yet. The jsFn + errFn fallback handles it correctly.
|
|
5356
|
-
if (hasAdditionalPropertiesSchema(schema)) return null
|
|
5944
|
+
// unevaluatedProperties and unevaluatedItems that are not provably local
|
|
5945
|
+
// are worked out at run time (genUnevaluatedDynamicC,
|
|
5946
|
+
// genUnevaluatedItemsDynamicC).
|
|
5947
|
+
// A schema that references itself, through `$ref: "#"` or a definition on
|
|
5948
|
+
// a cycle, has those references written as calls under the cycle guard
|
|
5949
|
+
// (emitDefFnC).
|
|
5357
5950
|
// Bail on oneOf/anyOf — branch-collapse aggregation lives in genCodeE only.
|
|
5358
5951
|
// Letting genCodeC emit the legacy "must match exactly one" placeholder
|
|
5359
5952
|
// would short-circuit the ATA4001/ATA4002/ATA4003 contract. The boolean
|
|
5360
5953
|
// fast path plus errFn fallback handles these schemas with collapse.
|
|
5361
|
-
|
|
5954
|
+
// Branches are collapsed into one error by the error generator's emitter
|
|
5955
|
+
// (emitBranchCollapse). Its errors are plain, so the rich form, which
|
|
5956
|
+
// builds every error enriched where it fails, declines them.
|
|
5957
|
+
// In the rich form the collapsed error and its branch errors come out
|
|
5958
|
+
// plain (the branches are the error generator's) and are enriched when
|
|
5959
|
+
// presented; every other error is built enriched. See richBuilder.
|
|
5362
5960
|
}
|
|
5363
5961
|
if (typeof schema === 'boolean') {
|
|
5962
|
+
// A `false` root has one fixed error; the rich form leaves it to enrich.
|
|
5963
|
+
if (!schema && opts && (opts.rich || opts.resultShape)) return null
|
|
5964
|
+
if (opts && opts.resultShape) return null
|
|
5364
5965
|
return schema
|
|
5365
5966
|
? () => VALID_RESULT
|
|
5366
5967
|
: () => ({ valid: false, errors: [{ keyword: 'not', instancePath: '', schemaPath: '#', params: {}, message: 'boolean schema is false' }] })
|
|
@@ -5369,7 +5970,6 @@ function compileToJSCombined(schema, VALID_RESULT, schemaMap, userFormats, opts)
|
|
|
5369
5970
|
if (!sharedCodegenGate(schema, schemaMap)) return null
|
|
5370
5971
|
if (schema.patternProperties) {
|
|
5371
5972
|
for (const [pat, sub] of Object.entries(schema.patternProperties)) {
|
|
5372
|
-
if (/\\[pP]\{/.test(pat)) return null
|
|
5373
5973
|
if (typeof sub === 'object' && sub !== null && !codegenSafe(sub, schemaMap)) return null
|
|
5374
5974
|
}
|
|
5375
5975
|
}
|
|
@@ -5419,7 +6019,7 @@ function compileToJSCombined(schema, VALID_RESULT, schemaMap, userFormats, opts)
|
|
|
5419
6019
|
if (needsBaseTracking(schema, schemaMap, new Set())) return null
|
|
5420
6020
|
|
|
5421
6021
|
// runtimeShape: errors without the ordinal, see compileToJSCodegenWithErrors.
|
|
5422
|
-
const ctx = { noOrdinal: !!(opts && opts.runtimeShape), varCounter: 0, helperCode: [], shared: [], closureVars: ['_cpLen'], closureVals: [_cpLen],
|
|
6022
|
+
const ctx = { noOrdinal: !!(opts && opts.runtimeShape), rich: !!(opts && opts.rich), inCombined: true, hoisted: [], rootC: schema, varCounter: 0, helperCode: [], shared: [], closureVars: ['_cpLen'], closureVals: [_cpLen],
|
|
5423
6023
|
rootDefs: cRootDefs, refStack: new Set(), schemaMap: schemaMap || null, anchors: cAnchors, rootSchema: inputSchema, userFormats: userFormats || null }
|
|
5424
6024
|
const lines = []
|
|
5425
6025
|
try {
|
|
@@ -5436,14 +6036,69 @@ function compileToJSCombined(schema, VALID_RESULT, schemaMap, userFormats, opts)
|
|
|
5436
6036
|
// helper compiles a pattern through it, which the boolean generator does for
|
|
5437
6037
|
// subschemas under not/if/contains; without the binding those threw a
|
|
5438
6038
|
// ReferenceError on the first document that reached them.
|
|
6039
|
+
if (opts && opts.resultShape) {
|
|
6040
|
+
ctx.closureVars.push('_ER', '_EE', '_SRT', '_FB', '_VF')
|
|
6041
|
+
ctx.closureVals.push(opts.resultShape.Rejection, opts.resultShape.empty, opts.resultShape.sort, opts.resultShape.fallback, opts.resultShape.verdict || null)
|
|
6042
|
+
}
|
|
5439
6043
|
const closureNames = ctx.usesSafeRe ? [...ctx.closureVars, '__ataSafeRe'] : ctx.closureVars
|
|
5440
6044
|
const closureArgs = ctx.usesSafeRe ? [...ctx.closureVals, compileSafe] : ctx.closureVals
|
|
5441
6045
|
const closureParams = closureNames.join(',')
|
|
5442
|
-
|
|
6046
|
+
// A definition the error generator wrote as a named function inside a
|
|
6047
|
+
// branch (one on a cycle, or referenced from several places) reads the
|
|
6048
|
+
// cycle-guard state the error function declares. Here that state is
|
|
6049
|
+
// declared too, and the checks run as the error function runs them: in a
|
|
6050
|
+
// body of their own, again in the guarded mode when data turns out to
|
|
6051
|
+
// point back at itself. This used to decline, which left every schema
|
|
6052
|
+
// whose oneOf or anyOf branches reach a shared definition without one
|
|
6053
|
+
// pass.
|
|
6054
|
+
const guardList = [...(ctx.defFns ? ctx.defFns.values() : []), ...(ctx.cycFnsC || [])]
|
|
6055
|
+
const guardDefs = guardList.length ? guardList : null
|
|
6056
|
+
if (guardDefs) ctx.helperCode.unshift(emitGuardState())
|
|
6057
|
+
// Collapsed oneOf/anyOf: the helper the error generator embeds, `_all` for
|
|
6058
|
+
// the branch functions (this function collects everything), and the
|
|
6059
|
+
// runtime shape the error generator gives its literals, without code and
|
|
6060
|
+
// docUrl. The collapsed error itself keeps its code, as everywhere.
|
|
6061
|
+
if (ctx.usesBranchCollapse) ctx.helperCode.push(require('./branch-collapse').embedSource())
|
|
6062
|
+
if (ctx.usesBranchCollapse && opts && opts.runtimeShape) {
|
|
6063
|
+
const shape = (src) => src.replace(/code:'ATA\d{4}',/g, '').replace(/,docUrl:'https:\/\/ata-validator\.com\/e\/ATA\d{4}'/g, '')
|
|
6064
|
+
for (let i = 0; i < ctx.helperCode.length; i++) ctx.helperCode[i] = shape(ctx.helperCode[i])
|
|
6065
|
+
for (let i = 0; i < lines.length; i++) lines[i] = shape(lines[i])
|
|
6066
|
+
}
|
|
5443
6067
|
// Lazy error array: no allocation for valid data (the common case)
|
|
5444
|
-
|
|
5445
|
-
|
|
5446
|
-
|
|
6068
|
+
// resultShape: validate() itself for a validator whose errors are being read
|
|
6069
|
+
// (see validator-core): the result objects built here, so one pass decides,
|
|
6070
|
+
// collects and answers, with nothing between the caller and the checks.
|
|
6071
|
+
const rs = opts && opts.resultShape
|
|
6072
|
+
// Counts shared between a node's unevaluatedProperties and its anyOf and
|
|
6073
|
+
// oneOf (genUnevaluatedDynamicC), visible to every line of the function.
|
|
6074
|
+
let hoistDecl = ctx.hoisted.length ? `let ${ctx.hoisted.map((n) => n + '=null').join(',')};` : ''
|
|
6075
|
+
let checks = lines.join('\n ')
|
|
6076
|
+
let allDecl = ctx.usesBranchCollapse ? 'const _all=true;' : ''
|
|
6077
|
+
if (guardDefs) {
|
|
6078
|
+
const clears = ['_stk.clear()'].concat(guardDefs.map((n) => `${n}_s.clear()`)).join(';')
|
|
6079
|
+
ctx.helperCode.push(`function _bodyC(d){${hoistDecl}${allDecl}let _e;\n ${checks}\n return _e}`)
|
|
6080
|
+
hoistDecl = ''
|
|
6081
|
+
allDecl = ''
|
|
6082
|
+
checks = `_sd=0;try{_e=_bodyC(d)}catch(_err){if(_err!==_CYC)throw _err;_sg=true;${clears};try{_e=_bodyC(d)}finally{_sg=false}}`
|
|
6083
|
+
}
|
|
6084
|
+
// In the result shape this function is validate() itself, so a document
|
|
6085
|
+
// that makes it throw is answered by the caller's previous path instead
|
|
6086
|
+
// (`_FB`). A thrown error reaching the caller is the one thing this form
|
|
6087
|
+
// must not add; a try that does not throw costs nothing measurable.
|
|
6088
|
+
const inner = rs
|
|
6089
|
+
// With collapsed branches, collecting runs every branch of every oneOf
|
|
6090
|
+
// and anyOf in full, which an accepted document does not need: the
|
|
6091
|
+
// verdict answers it first.
|
|
6092
|
+
// Not when every collapse is guarded by a count (emitBranchCollapse): an
|
|
6093
|
+
// accepted document then runs no branch twice, and a rejected one would
|
|
6094
|
+
// pay for the verdict for nothing.
|
|
6095
|
+
? `try{${hoistDecl}${ctx.usesBranchCollapse && ctx.unguardedCollapse ? 'if(_VF!==null&&_VF(d))return{valid:true,data:d,errors:_EE};' : ''}${allDecl}let _e;\n ` + checks +
|
|
6096
|
+
// A collapsed branch error is presented even alone (its ordinal and
|
|
6097
|
+
// raw branch errors stay inside); otherwise only a list of two or more
|
|
6098
|
+
// needs ordering.
|
|
6099
|
+
`\n return _e?new _ER(${ctx.usesBranchCollapse ? '_SRT(_e)' : '_e.length>1?_SRT(_e):_e'}):{valid:true,data:d,errors:_EE}}catch(_x){return _FB(d)}`
|
|
6100
|
+
: hoistDecl + allDecl + `let _e;\n ` + checks + `\n return _e?{valid:false,errors:_e}:R`
|
|
6101
|
+
const helpers = ctx.helperCode.length ? ctx.helperCode.join('\n ') + '\n ' : ''
|
|
5447
6102
|
|
|
5448
6103
|
try {
|
|
5449
6104
|
if (typeof process !== 'undefined' && process.env && process.env.ATA_DUMP_CODEGEN) console.log('=== COMBINED CODEGEN ===\n' + helpers + inner + '\n=== CLOSURE VARS: ' + ctx.closureVars.length + ' ===')
|
|
@@ -5459,9 +6114,102 @@ function compileToJSCombined(schema, VALID_RESULT, schemaMap, userFormats, opts)
|
|
|
5459
6114
|
// Combined code generator: type-aware like genCode, error-collecting like genCodeE.
|
|
5460
6115
|
// After type check passes → use optimizations (destructuring, no guards).
|
|
5461
6116
|
// If type check fails → push error, skip property checks (they'd crash).
|
|
6117
|
+
// What of a node still applies to a value that failed its `type`, for the
|
|
6118
|
+
// combined generator: the node without `type`, without what it emits outside
|
|
6119
|
+
// the type block anyway (enum, const, anyOf, oneOf), and without the keywords
|
|
6120
|
+
// of the declared types, which no value of another type meets. Null when
|
|
6121
|
+
// nothing that validates is left.
|
|
6122
|
+
const TYPE_KEYWORDS = {
|
|
6123
|
+
object: ['properties', 'patternProperties', 'additionalProperties', 'required', 'propertyNames', 'minProperties', 'maxProperties', 'dependentRequired', 'dependentSchemas', 'dependencies', 'unevaluatedProperties'],
|
|
6124
|
+
array: ['items', 'prefixItems', 'additionalItems', 'contains', 'minContains', 'maxContains', 'minItems', 'maxItems', 'uniqueItems', 'unevaluatedItems'],
|
|
6125
|
+
string: ['minLength', 'maxLength', 'pattern'],
|
|
6126
|
+
number: ['minimum', 'maximum', 'exclusiveMinimum', 'exclusiveMaximum', 'multipleOf'],
|
|
6127
|
+
}
|
|
6128
|
+
TYPE_KEYWORDS.integer = TYPE_KEYWORDS.number
|
|
6129
|
+
const NOT_VALIDATING = new Set(['$schema', '$id', '$anchor', '$comment', '$defs', 'definitions', 'title', 'description', 'default', 'examples', 'deprecated', 'readOnly', 'writeOnly', '$vocabulary'])
|
|
6130
|
+
function typeMismatchRest(schema, types) {
|
|
6131
|
+
const drop = new Set(['type', 'enum', 'const', 'anyOf', 'oneOf'])
|
|
6132
|
+
for (const t of types) for (const k of (TYPE_KEYWORDS[t] || [])) drop.add(k)
|
|
6133
|
+
// A non-integer number reaches the type block already (numberToo).
|
|
6134
|
+
let rest = null
|
|
6135
|
+
for (const k of Object.keys(schema)) {
|
|
6136
|
+
if (drop.has(k)) continue
|
|
6137
|
+
if (rest === null) rest = {}
|
|
6138
|
+
rest[k] = schema[k]
|
|
6139
|
+
}
|
|
6140
|
+
if (rest === null) return null
|
|
6141
|
+
// A node whose reference was already written (genCodeCNode) keeps that
|
|
6142
|
+
// mark, or the reference is written again here and its errors twice.
|
|
6143
|
+
if (schema[REF_DONE]) Object.defineProperty(rest, REF_DONE, { value: true })
|
|
6144
|
+
for (const k of Object.keys(rest)) if (!NOT_VALIDATING.has(k) && !k.startsWith('x-')) return rest
|
|
6145
|
+
return null
|
|
6146
|
+
}
|
|
6147
|
+
|
|
5462
6148
|
function genCodeC(schema, v, pathExpr, lines, ctx, schemaPrefix) {
|
|
5463
6149
|
return withPlain(schema, v, lines, ctx, () => genCodeCNode(schema, v, pathExpr, lines, ctx, schemaPrefix))
|
|
5464
6150
|
}
|
|
6151
|
+
// A definition referenced from several places, written once as a function
|
|
6152
|
+
// and called at each reference, instead of its whole code at every one: the
|
|
6153
|
+
// combined function of SchemaStore's cloud-run (48 KB of schema) came to
|
|
6154
|
+
// 1.1 MB of source and sarif's (79 KB) to 2.8 MB, and compiling those was most
|
|
6155
|
+
// of the 50 to 300 ms a first rejection with its errors read took. The body is
|
|
6156
|
+
// generated against a placeholder schema path that becomes the `_sp` argument,
|
|
6157
|
+
// as the error generator does (genCodeENode), so an error names the path it
|
|
6158
|
+
// was reached by. It collects into its own list, returned to the caller, and
|
|
6159
|
+
// declares its own branch counts (genUnevaluatedDynamicC). The document root
|
|
6160
|
+
// comes in as `d` for the rich errors that read it. Not for a definition on a
|
|
6161
|
+
// reference cycle, which keeps its existing path. False when not shared.
|
|
6162
|
+
function emitSharedDefC(defName, v, pathExpr, lines, ctx, schemaPrefix) {
|
|
6163
|
+
if (!ctx.cyclicDefs) ctx.cyclicDefs = cyclicDefNames(ctx.rootDefs)
|
|
6164
|
+
if (!ctx.sharedDefs) ctx.sharedDefs = sharedDefNames(ctx.rootSchema, ctx.rootDefs)
|
|
6165
|
+
const cyclic = ctx.cyclicDefs.has(defName)
|
|
6166
|
+
if (!cyclic && !ctx.sharedDefs.has(defName)) return false
|
|
6167
|
+
emitDefFnC(defName, ctx.rootDefs[defName], cyclic, v, pathExpr, lines, ctx, schemaPrefix)
|
|
6168
|
+
return true
|
|
6169
|
+
}
|
|
6170
|
+
|
|
6171
|
+
// One schema as a function of its own in the combined function (see
|
|
6172
|
+
// emitSharedDefC). The name is registered before the body is written, so a
|
|
6173
|
+
// reference back to it inside becomes a call. One that can re-enter itself,
|
|
6174
|
+
// a definition on a cycle or the root through `$ref: "#"`, goes through the
|
|
6175
|
+
// cycle guard the error generator's definitions use: depth counted, and in
|
|
6176
|
+
// the guarded pass a value already being checked is not checked again.
|
|
6177
|
+
function emitDefFnC(key, target, cyclic, v, pathExpr, lines, ctx, schemaPrefix) {
|
|
6178
|
+
if (!ctx.defFnsC) ctx.defFnsC = new Map()
|
|
6179
|
+
let fnName = ctx.defFnsC.get(key)
|
|
6180
|
+
if (!fnName) {
|
|
6181
|
+
fnName = '_defC' + ctx.defFnsC.size + '_' + (key === '#' ? 'root' : key.replace(/[^A-Za-z0-9_]/g, '_'))
|
|
6182
|
+
ctx.defFnsC.set(key, fnName)
|
|
6183
|
+
const outerHoisted = ctx.hoisted
|
|
6184
|
+
ctx.hoisted = []
|
|
6185
|
+
const bodyLines = []
|
|
6186
|
+
let hoisted
|
|
6187
|
+
try {
|
|
6188
|
+
genCodeC(target, '_dv', '_p', bodyLines, ctx, SP_PLACEHOLDER)
|
|
6189
|
+
} finally {
|
|
6190
|
+
hoisted = ctx.hoisted
|
|
6191
|
+
ctx.hoisted = outerHoisted
|
|
6192
|
+
}
|
|
6193
|
+
let body = bodyLines.join('\n ')
|
|
6194
|
+
body = body.split(`'${SP_PLACEHOLDER}`).join(`_sp+'`).split(`"${SP_PLACEHOLDER}`).join(`_sp+"`)
|
|
6195
|
+
if (body.includes(SP_PLACEHOLDER)) throw DECLINE
|
|
6196
|
+
const decl = hoisted.length ? `let ${hoisted.map((n) => n + '=null').join(',')};` : ''
|
|
6197
|
+
const fn = `function ${cyclic ? fnName + '_b' : fnName}(_dv,_p,_sp,d){const _all=true;${decl}let _e;\n ${body}\n return _e}`
|
|
6198
|
+
if (cyclic) {
|
|
6199
|
+
if (!ctx.cycFnsC) ctx.cycFnsC = []
|
|
6200
|
+
ctx.cycFnsC.push(fnName)
|
|
6201
|
+
ctx.helperCode.push(
|
|
6202
|
+
`const ${fnName}_s=new Set()\n function ${fnName}(_dv,_p,_sp,d){\n ` +
|
|
6203
|
+
`if(_sg){if(typeof _dv!=='object'||_dv===null)return ${fnName}_b(_dv,_p,_sp,d);if(${fnName}_s.has(_dv))return;${fnName}_s.add(_dv);try{return ${fnName}_b(_dv,_p,_sp,d)}finally{${fnName}_s.delete(_dv)}}\n ` +
|
|
6204
|
+
`if(++_sd>${CYCLE_DEPTH})throw _CYC\n const _r=${fnName}_b(_dv,_p,_sp,d)\n _sd--\n return _r\n }\n ` + fn)
|
|
6205
|
+
} else {
|
|
6206
|
+
ctx.helperCode.push(fn)
|
|
6207
|
+
}
|
|
6208
|
+
}
|
|
6209
|
+
lines.push(`{const _r=${fnName}(${v},${pathExpr || '""'},'${schemaPrefix}',d);if(_r!==undefined){if(_e===undefined)_e=_r;else for(let _i=0;_i<_r.length;_i++)_e.push(_r[_i])}}`)
|
|
6210
|
+
}
|
|
6211
|
+
|
|
6212
|
+
const REF_DONE = Symbol('ata.refDone')
|
|
5465
6213
|
function genCodeCNode(schema, v, pathExpr, lines, ctx, schemaPrefix) {
|
|
5466
6214
|
if (!schemaPrefix) schemaPrefix = '#'
|
|
5467
6215
|
// Per-node, for the reason spelled out in genCode.
|
|
@@ -5469,19 +6217,41 @@ function genCodeCNode(schema, v, pathExpr, lines, ctx, schemaPrefix) {
|
|
|
5469
6217
|
if (schema === false) {
|
|
5470
6218
|
// Leading semicolon: the previous generated line may end in an
|
|
5471
6219
|
// expression, and a line starting with `(` would be parsed as a call.
|
|
5472
|
-
lines.push(
|
|
6220
|
+
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'}`))
|
|
5473
6221
|
return
|
|
5474
6222
|
}
|
|
5475
6223
|
if (schema === true) return
|
|
5476
6224
|
if (typeof schema !== 'object' || schema === null) return
|
|
5477
6225
|
|
|
5478
6226
|
// $ref — resolve local, anchor, and cross-schema refs
|
|
5479
|
-
if (schema.$ref) {
|
|
5480
|
-
//
|
|
5481
|
-
|
|
6227
|
+
if (schema.$ref && !schema[REF_DONE]) {
|
|
6228
|
+
// A keyword beside $ref that validates applies too (2020-12; draft 7
|
|
6229
|
+
// drops such siblings when the schema is read). The reference is written
|
|
6230
|
+
// on its own, then the node again with the reference marked done, on a
|
|
6231
|
+
// fresh copy so the mark cannot reach the same schema object met through
|
|
6232
|
+
// another path; the copy keeps `$ref` for unevaluatedProperties and
|
|
6233
|
+
// unevaluatedItems, which read its annotations. Dropping the siblings
|
|
6234
|
+
// once accepted what they reject, which is why this declined. Not in a
|
|
6235
|
+
// branch function, which the error generator writes.
|
|
6236
|
+
if (Object.keys(schema).some((k) => !REF_NEUTRAL_SIBLINGS.has(k) && !k.startsWith('x-'))) {
|
|
6237
|
+
if (!ctx.inCombined || ctx.inBranchFn) throw DECLINE
|
|
6238
|
+
genCodeCNode({ $ref: schema.$ref }, v, pathExpr, lines, ctx, schemaPrefix)
|
|
6239
|
+
const rest = Object.assign({}, schema)
|
|
6240
|
+
Object.defineProperty(rest, REF_DONE, { value: true })
|
|
6241
|
+
genCodeCNode(rest, v, pathExpr, lines, ctx, schemaPrefix)
|
|
6242
|
+
return
|
|
6243
|
+
}
|
|
6244
|
+
// A reference back into what is being expanded is a call to that
|
|
6245
|
+
// schema's function (emitDefFnC), never nothing: an expansion that stops
|
|
6246
|
+
// and emits no lines reads as valid.
|
|
6247
|
+
if (schema.$ref === '#') {
|
|
6248
|
+
if (ctx.inCombined && !ctx.inBranchFn && ctx.rootC) { emitDefFnC('#', ctx.rootC, true, v, pathExpr, lines, ctx, schemaPrefix); return }
|
|
6249
|
+
throw DECLINE
|
|
6250
|
+
}
|
|
5482
6251
|
const m = schema.$ref.match(/^#\/(?:\$defs|definitions)\/(.+)$/)
|
|
5483
6252
|
if (m && ctx.rootDefs && ctx.rootDefs[m[1]]) {
|
|
5484
|
-
if (ctx.
|
|
6253
|
+
if (ctx.inCombined && !ctx.inBranchFn && emitSharedDefC(m[1], v, pathExpr, lines, ctx, schemaPrefix)) return
|
|
6254
|
+
if (ctx.refStack.has(schema.$ref)) throw DECLINE
|
|
5485
6255
|
ctx.refStack.add(schema.$ref)
|
|
5486
6256
|
genCodeC(ctx.rootDefs[m[1]], v, pathExpr, lines, ctx, schemaPrefix)
|
|
5487
6257
|
ctx.refStack.delete(schema.$ref)
|
|
@@ -5492,7 +6262,7 @@ function genCodeCNode(schema, v, pathExpr, lines, ctx, schemaPrefix) {
|
|
|
5492
6262
|
const entry = ctx.rootDefs && ctx.rootDefs[schema.$ref]
|
|
5493
6263
|
const anchorTarget = entry && entry.raw ? entry.raw : (ctx.anchors && ctx.anchors[schema.$ref])
|
|
5494
6264
|
if (anchorTarget) {
|
|
5495
|
-
if (ctx.refStack.has(schema.$ref))
|
|
6265
|
+
if (ctx.refStack.has(schema.$ref)) throw DECLINE
|
|
5496
6266
|
ctx.refStack.add(schema.$ref)
|
|
5497
6267
|
genCodeC(anchorTarget, v, pathExpr, lines, ctx, schemaPrefix)
|
|
5498
6268
|
ctx.refStack.delete(schema.$ref)
|
|
@@ -5500,7 +6270,7 @@ function genCodeCNode(schema, v, pathExpr, lines, ctx, schemaPrefix) {
|
|
|
5500
6270
|
}
|
|
5501
6271
|
}
|
|
5502
6272
|
if (ctx.schemaMap && ctx.schemaMap.has(schema.$ref)) {
|
|
5503
|
-
if (ctx.refStack.has(schema.$ref))
|
|
6273
|
+
if (ctx.refStack.has(schema.$ref)) throw DECLINE
|
|
5504
6274
|
ctx.refStack.add(schema.$ref)
|
|
5505
6275
|
genCodeC(ctx.schemaMap.get(schema.$ref), v, pathExpr, lines, ctx, schemaPrefix)
|
|
5506
6276
|
ctx.refStack.delete(schema.$ref)
|
|
@@ -5510,7 +6280,7 @@ function genCodeCNode(schema, v, pathExpr, lines, ctx, schemaPrefix) {
|
|
|
5510
6280
|
if (ctx.schemaMap && schema.$ref.includes('#') && !schema.$ref.startsWith('#')) {
|
|
5511
6281
|
const r = resolveCrossSchemaRef(schema.$ref, ctx.schemaMap)
|
|
5512
6282
|
if (r) {
|
|
5513
|
-
if (ctx.refStack.has(schema.$ref))
|
|
6283
|
+
if (ctx.refStack.has(schema.$ref)) throw DECLINE
|
|
5514
6284
|
ctx.refStack.add(schema.$ref)
|
|
5515
6285
|
genCodeC(r.schema, v, pathExpr, lines, ctx, schemaPrefix)
|
|
5516
6286
|
ctx.refStack.delete(schema.$ref)
|
|
@@ -5530,7 +6300,26 @@ function genCodeCNode(schema, v, pathExpr, lines, ctx, schemaPrefix) {
|
|
|
5530
6300
|
const isStaticPath = !pathExpr || (pathExpr.startsWith("'") && !pathExpr.includes('+'))
|
|
5531
6301
|
const fail = (keyword, schemaSuffix, paramsCode, msgCode) => {
|
|
5532
6302
|
const sp = schemaPrefix + '/' + schemaSuffix
|
|
5533
|
-
if (
|
|
6303
|
+
if (ctx.rich) {
|
|
6304
|
+
// A static site shares one frozen params object between calls, as its
|
|
6305
|
+
// hoisted literal does below; a dynamic one builds its params per call.
|
|
6306
|
+
let shared = null
|
|
6307
|
+
if (isStaticPath && msgCode.startsWith("'") && !msgCode.includes('+')) {
|
|
6308
|
+
let pv
|
|
6309
|
+
try { pv = Function('return ' + paramsCode)() } catch { pv = undefined }
|
|
6310
|
+
if (pv !== undefined) {
|
|
6311
|
+
shared = `_P${ctx.varCounter++}`
|
|
6312
|
+
ctx.closureVars.push(shared); ctx.closureVals.push(Object.freeze(pv))
|
|
6313
|
+
}
|
|
6314
|
+
}
|
|
6315
|
+
return errPushC(ctx, keyword, sp, pathExpr, paramsCode, msgCode, v, null, shared)
|
|
6316
|
+
}
|
|
6317
|
+
// Hoisting a static error as one shared literal keeps a function that
|
|
6318
|
+
// also answers accepted documents small. In the runtime shape this
|
|
6319
|
+
// function only runs for documents being rejected (the verdict is its own
|
|
6320
|
+
// function), and a shared literal has to be copied for the caller on
|
|
6321
|
+
// every read, so there it is written fresh.
|
|
6322
|
+
if (isStaticPath && !ctx.noOrdinal && msgCode.startsWith("'") && !msgCode.includes('+')) {
|
|
5534
6323
|
// Try to evaluate paramsCode as a static constant
|
|
5535
6324
|
let paramsVal
|
|
5536
6325
|
try { paramsVal = Function('return ' + paramsCode)() } catch { /* dynamic params — fall through */ }
|
|
@@ -5557,7 +6346,7 @@ function genCodeCNode(schema, v, pathExpr, lines, ctx, schemaPrefix) {
|
|
|
5557
6346
|
const o = ctx.noOrdinal ? null : ordinalFor(ctx.rootSchema, spVal)
|
|
5558
6347
|
const lit = {keyword, instancePath: pathVal, schemaPath: spVal, params: Object.freeze(paramsVal), message: msgVal}
|
|
5559
6348
|
if (o !== null) lit._o = o
|
|
5560
|
-
ctx.closureVals.push(
|
|
6349
|
+
ctx.closureVals.push(sharedErr(lit, ctx.rootSchema))
|
|
5561
6350
|
return `(_e||(_e=[])).push(${errVar})`
|
|
5562
6351
|
}
|
|
5563
6352
|
}
|
|
@@ -5582,7 +6371,7 @@ function genCodeCNode(schema, v, pathExpr, lines, ctx, schemaPrefix) {
|
|
|
5582
6371
|
if (cv === null || typeof cv !== 'object') {
|
|
5583
6372
|
lines.push(`if(${v}!==${JSON.stringify(cv)}){${fail('const', 'const', `{allowedValue:${JSON.stringify(schema.const)}}`, "'must be equal to constant'")}}`)
|
|
5584
6373
|
} else {
|
|
5585
|
-
lines.push(`if(!${emitDeq(ctx)}(${v},${emitConstant(ctx, cv)})){${fail('const', 'const', `{allowedValue
|
|
6374
|
+
lines.push(`if(!${emitDeq(ctx)}(${v},${emitConstant(ctx, cv)})){${fail('const', 'const', `{allowedValue:${emitParamConstant(ctx, schema.const)}}`, "'must be equal to constant'")}}`)
|
|
5586
6375
|
}
|
|
5587
6376
|
}
|
|
5588
6377
|
}
|
|
@@ -5601,7 +6390,9 @@ function genCodeCNode(schema, v, pathExpr, lines, ctx, schemaPrefix) {
|
|
|
5601
6390
|
}
|
|
5602
6391
|
})
|
|
5603
6392
|
const expected = types.join(', ')
|
|
5604
|
-
|
|
6393
|
+
// As written: a one-element array stays an array, as the interpreted engine
|
|
6394
|
+
// reports it.
|
|
6395
|
+
const expectedParam = !Array.isArray(schema.type) ? `'${types[0]}'` : JSON.stringify(types)
|
|
5605
6396
|
// Type check: push error but continue — wrap remaining in type-success block
|
|
5606
6397
|
const typeOk = `_tok${ctx.varCounter++}`
|
|
5607
6398
|
lines.push(`const ${typeOk}=${conds.join('||')}`)
|
|
@@ -5614,7 +6405,11 @@ function genCodeCNode(schema, v, pathExpr, lines, ctx, schemaPrefix) {
|
|
|
5614
6405
|
isNum = types[0] === 'number' || types[0] === 'integer'
|
|
5615
6406
|
}
|
|
5616
6407
|
emitEnumConst()
|
|
5617
|
-
|
|
6408
|
+
// A number that is not an integer fails `integer` and is still a number:
|
|
6409
|
+
// minimum, multipleOf and the rest apply to it, and the other engines
|
|
6410
|
+
// report them next to the type error. The block opens for any number.
|
|
6411
|
+
const numberToo = types.includes('integer') && !types.includes('number')
|
|
6412
|
+
lines.push(numberToo ? `if(${typeOk}||typeof ${v}==='number'){` : `if(${typeOk}){`)
|
|
5618
6413
|
} else {
|
|
5619
6414
|
emitEnumConst()
|
|
5620
6415
|
}
|
|
@@ -5641,29 +6436,29 @@ function genCodeCNode(schema, v, pathExpr, lines, ctx, schemaPrefix) {
|
|
|
5641
6436
|
}
|
|
5642
6437
|
for (const key of schema.required) {
|
|
5643
6438
|
const check = hoisted[key] ? `${hoisted[key]}===undefined` : `!${ownKeyExpr(ctx, v, key)}`
|
|
5644
|
-
if (isStaticPath) {
|
|
6439
|
+
if (isStaticPath && !ctx.rich && !ctx.noOrdinal) {
|
|
5645
6440
|
const ei = ctx.varCounter++
|
|
5646
6441
|
const errVar = `_E${ei}`
|
|
5647
6442
|
const pathVal = pathExpr ? literalValue(pathExpr) : ''
|
|
5648
6443
|
ctx.closureVars.push(errVar)
|
|
5649
|
-
ctx.closureVals.push(
|
|
6444
|
+
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))
|
|
5650
6445
|
lines.push(`if(${check}){(_e||(_e=[])).push(${errVar})}`)
|
|
5651
6446
|
} else {
|
|
5652
|
-
lines.push(`if(${check}){(
|
|
6447
|
+
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)}'"}`)}}`)
|
|
5653
6448
|
}
|
|
5654
6449
|
}
|
|
5655
6450
|
} else if (schema.required) {
|
|
5656
6451
|
for (const key of schema.required) {
|
|
5657
6452
|
const isStatic = !pathExpr || (pathExpr.startsWith("'") && !pathExpr.includes('+'))
|
|
5658
|
-
if (isStatic) {
|
|
6453
|
+
if (isStatic && !ctx.rich && !ctx.noOrdinal) {
|
|
5659
6454
|
const ei = ctx.varCounter++
|
|
5660
6455
|
const errVar = `_E${ei}`
|
|
5661
6456
|
const pathVal = pathExpr ? literalValue(pathExpr) : ''
|
|
5662
6457
|
ctx.closureVars.push(errVar)
|
|
5663
|
-
ctx.closureVals.push(
|
|
6458
|
+
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))
|
|
5664
6459
|
lines.push(`if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})&&!${ownKeyExpr(ctx, v, key)}){(_e||(_e=[])).push(${errVar})}`)
|
|
5665
6460
|
} else {
|
|
5666
|
-
lines.push(`if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})&&!${ownKeyExpr(ctx, v, key)}){(
|
|
6461
|
+
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)}'"}`)}}`)
|
|
5667
6462
|
}
|
|
5668
6463
|
}
|
|
5669
6464
|
}
|
|
@@ -5701,7 +6496,7 @@ function genCodeCNode(schema, v, pathExpr, lines, ctx, schemaPrefix) {
|
|
|
5701
6496
|
const ri = ctx.varCounter++
|
|
5702
6497
|
const reVar = `_re${ri}`
|
|
5703
6498
|
ctx.closureVars.push(reVar)
|
|
5704
|
-
ctx.closureVals.push(useSafeEngine(schema.pattern) ? compileSafe(schema.pattern) : new RegExp(schema.pattern))
|
|
6499
|
+
ctx.closureVals.push(useSafeEngine(schema.pattern) ? compileSafe(schema.pattern) : new RegExp(schema.pattern, reFlags(schema.pattern)))
|
|
5705
6500
|
const c = isStr ? `!${reVar}.test(${v})` : `typeof ${v}==='string'&&!${reVar}.test(${v})`
|
|
5706
6501
|
lines.push(`if(${c}){${fail('pattern', 'pattern', `{pattern:${JSON.stringify(schema.pattern)}}`, JSON.stringify(`must match pattern "${schema.pattern}"`))}}`)
|
|
5707
6502
|
}
|
|
@@ -5786,27 +6581,36 @@ function genCodeCNode(schema, v, pathExpr, lines, ctx, schemaPrefix) {
|
|
|
5786
6581
|
// unevaluatedProperties: false where it is provably additionalProperties
|
|
5787
6582
|
// (the entry bail guaranteed that), keeping the keyword's own identity so
|
|
5788
6583
|
// the two engines report the same error.
|
|
5789
|
-
if (schema.unevaluatedProperties
|
|
6584
|
+
if (schema.unevaluatedProperties !== undefined && schema.unevaluatedProperties !== true) {
|
|
6585
|
+
if (unevalLocalOk(schema, 'unevaluatedProperties')) {
|
|
6586
|
+
if (schema.additionalProperties === undefined && !schema.patternProperties) genUnevaluatedFalseC(schema, v, pathExpr, lines, ctx, schemaPrefix, fail, isObj)
|
|
6587
|
+
} else {
|
|
6588
|
+
genUnevaluatedDynamicC(schema, v, pathExpr, lines, ctx, schemaPrefix, fail, isObj)
|
|
6589
|
+
}
|
|
6590
|
+
}
|
|
5790
6591
|
|
|
5791
6592
|
// unevaluatedItems: false under the same rule: one error for the array,
|
|
5792
6593
|
// the first uncovered position as the limit. Adjacent schema-form `items`
|
|
5793
6594
|
// evaluates everything; the draft-07 array form acts as the prefix.
|
|
5794
|
-
if (schema.unevaluatedItems
|
|
6595
|
+
if (schema.unevaluatedItems !== undefined && schema.unevaluatedItems !== true) {
|
|
6596
|
+
if (unevalLocalOk(schema, 'unevaluatedItems')) genUnevaluatedItemsFalseC(schema, v, pathExpr, lines, ctx, schemaPrefix, fail)
|
|
6597
|
+
else genUnevaluatedItemsDynamicC(schema, v, pathExpr, lines, ctx, schemaPrefix, fail)
|
|
6598
|
+
}
|
|
5795
6599
|
|
|
5796
6600
|
// dependentRequired
|
|
5797
6601
|
if (schema.dependentRequired) {
|
|
5798
6602
|
for (const [key, deps] of Object.entries(schema.dependentRequired)) {
|
|
5799
6603
|
for (const dep of deps) {
|
|
5800
6604
|
const isStatic = !pathExpr || (pathExpr.startsWith("'") && !pathExpr.includes('+'))
|
|
5801
|
-
if (isStatic) {
|
|
6605
|
+
if (isStatic && !ctx.rich && !ctx.noOrdinal) {
|
|
5802
6606
|
const ei = ctx.varCounter++
|
|
5803
6607
|
const errVar = `_E${ei}`
|
|
5804
6608
|
const pathVal = pathExpr ? literalValue(pathExpr) : ''
|
|
5805
6609
|
ctx.closureVars.push(errVar)
|
|
5806
|
-
ctx.closureVals.push(
|
|
6610
|
+
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))
|
|
5807
6611
|
lines.push(`if(typeof ${v}==='object'&&${v}!==null&&${ownKeyExpr(ctx, v, key)}&&!${ownKeyExpr(ctx, v, dep)}){(_e||(_e=[])).push(${errVar})}`)
|
|
5808
6612
|
} else {
|
|
5809
|
-
lines.push(`if(typeof ${v}==='object'&&${v}!==null&&${ownKeyExpr(ctx, v, key)}&&!${ownKeyExpr(ctx, v, dep)}){(
|
|
6613
|
+
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)}'"}`)}}`)
|
|
5810
6614
|
}
|
|
5811
6615
|
}
|
|
5812
6616
|
}
|
|
@@ -5839,6 +6643,10 @@ function genCodeCNode(schema, v, pathExpr, lines, ctx, schemaPrefix) {
|
|
|
5839
6643
|
if (schema.patternProperties) {
|
|
5840
6644
|
if (genPatternPropertiesC(schema, v, pathExpr, lines, ctx, schemaPrefix, fail, isObj)) ppHandledPropertyNames = true
|
|
5841
6645
|
}
|
|
6646
|
+
// additionalProperties as a schema: each key that is neither declared nor
|
|
6647
|
+
// matched by a pattern, validated where it is (see genAdditionalSchemaC).
|
|
6648
|
+
if (typeof schema.additionalProperties === 'object' && schema.additionalProperties !== null) genAdditionalSchemaC(schema, v, pathExpr, lines, ctx, schemaPrefix, isObj)
|
|
6649
|
+
|
|
5842
6650
|
|
|
5843
6651
|
// dependentSchemas
|
|
5844
6652
|
if (schema.dependentSchemas) genDependentSchemasC(schema, v, pathExpr, lines, ctx, schemaPrefix)
|
|
@@ -5868,11 +6676,11 @@ function genCodeCNode(schema, v, pathExpr, lines, ctx, schemaPrefix) {
|
|
|
5868
6676
|
// allOf
|
|
5869
6677
|
if (schema.allOf) { for (let _ai = 0; _ai < schema.allOf.length; _ai++) genCodeC(schema.allOf[_ai], v, pathExpr, lines, ctx, schemaPrefix+'/allOf/'+_ai) }
|
|
5870
6678
|
|
|
5871
|
-
// anyOf
|
|
5872
|
-
|
|
5873
|
-
|
|
5874
|
-
|
|
5875
|
-
if (schema.oneOf) genOneOfC(schema, v, pathExpr, lines, ctx, schemaPrefix, fail)
|
|
6679
|
+
// anyOf, oneOf. A collapsed branch applies to a value of any type, as enum
|
|
6680
|
+
// and const do: the other engines report it next to a failed `type`, so
|
|
6681
|
+
// in the combined function it goes after the type-success block below.
|
|
6682
|
+
if (schema.anyOf && !ctx.inCombined) genAnyOfC(schema, v, pathExpr, lines, ctx, schemaPrefix, fail)
|
|
6683
|
+
if (schema.oneOf && !ctx.inCombined) genOneOfC(schema, v, pathExpr, lines, ctx, schemaPrefix, fail)
|
|
5876
6684
|
|
|
5877
6685
|
// not
|
|
5878
6686
|
if (schema.not !== undefined) genNotC(schema, v, pathExpr, lines, ctx, schemaPrefix, fail)
|
|
@@ -5883,7 +6691,22 @@ function genCodeCNode(schema, v, pathExpr, lines, ctx, schemaPrefix) {
|
|
|
5883
6691
|
// Close type-success block if opened
|
|
5884
6692
|
if (types) {
|
|
5885
6693
|
lines.push(`}`)
|
|
6694
|
+
// A value of another type still meets every keyword that applies to its
|
|
6695
|
+
// own type, and the applicators (allOf, not, if/then/else, $ref): the
|
|
6696
|
+
// other engines report those next to the type error, and the block above
|
|
6697
|
+
// skipped them all. The keywords of the declared types cannot apply to
|
|
6698
|
+
// it and are left out, so the else is small and does not repeat the
|
|
6699
|
+
// declared type's subtree.
|
|
6700
|
+
const rest = typeMismatchRest(schema, types)
|
|
6701
|
+
if (rest !== null) {
|
|
6702
|
+
lines.push(`else{`)
|
|
6703
|
+
genCodeC(rest, v, pathExpr, lines, ctx, schemaPrefix)
|
|
6704
|
+
lines.push(`}`)
|
|
6705
|
+
}
|
|
5886
6706
|
}
|
|
6707
|
+
if (schema.anyOf && ctx.inCombined) genAnyOfC(schema, v, pathExpr, lines, ctx, schemaPrefix, fail)
|
|
6708
|
+
if (schema.oneOf && ctx.inCombined) genOneOfC(schema, v, pathExpr, lines, ctx, schemaPrefix, fail)
|
|
6709
|
+
|
|
5887
6710
|
}
|
|
5888
6711
|
|
|
5889
6712
|
// Rarely used keyword families of genCodeCNode, kept out of it so their compile is
|
|
@@ -5909,20 +6732,376 @@ function genDynamicRefC(schema, v, pathExpr, lines, ctx, schemaPrefix) {
|
|
|
5909
6732
|
|
|
5910
6733
|
function genUniqueItemsC(schema, v, pathExpr, lines, ctx, schemaPrefix, fail, isArr) {
|
|
5911
6734
|
const si = ctx.varCounter++
|
|
5912
|
-
const itemType = schema.items && typeof schema.items === 'object' && schema.items.type
|
|
5913
|
-
const isPrim = itemType === 'string' || itemType === 'number' || itemType === 'integer'
|
|
5914
|
-
const maxItems = schema.maxItems
|
|
5915
6735
|
const failExpr = (iVar, jVar) => fail('uniqueItems', 'uniqueItems', `{i:${iVar},j:${jVar}}`, `'must NOT have duplicate items (items ## '+${jVar}+' and '+${iVar}+' are identical)'`)
|
|
5916
|
-
|
|
5917
|
-
|
|
5918
|
-
|
|
5919
|
-
|
|
5920
|
-
|
|
5921
|
-
|
|
6736
|
+
// Only reached once the verdict has rejected, or when errors are wanted, so
|
|
6737
|
+
// one strategy for every item type: the hoisted pair finder. See UQP_HELPER.
|
|
6738
|
+
const inner = `const _j${si}=${emitUqp(ctx)}(${v});if(_j${si}>=0){const _i${si}=_uqpI;${failExpr('_i' + si, '_j' + si)}}`
|
|
6739
|
+
lines.push(isArr ? `{${inner}}` : `if(Array.isArray(${v})){${inner}}`)
|
|
6740
|
+
}
|
|
6741
|
+
|
|
6742
|
+
// The verdict form of genUnevaluatedDynamicC: the same set, built the same
|
|
6743
|
+
// way, and the answer false at the first key outside it.
|
|
6744
|
+
function genUnevalDynamicVerdict(schema, v, lines, ctx, isObj) {
|
|
6745
|
+
if (schema.additionalProperties !== undefined) return
|
|
6746
|
+
const ui = ctx.varCounter++
|
|
6747
|
+
const S = `_ev${ui}`
|
|
6748
|
+
const body = [`const ${S}=[]`]
|
|
6749
|
+
addLocalAnnotations(schema, v, S, body, ctx)
|
|
6750
|
+
addTopApplicatorAnnotations(schema, v, S, body, ctx)
|
|
6751
|
+
const kVar = `_uk${ui}`
|
|
6752
|
+
const up = schema.unevaluatedProperties
|
|
6753
|
+
if (up === false) {
|
|
6754
|
+
body.push(`for(const ${kVar} of Object.keys(${v}))if(!${S}.includes(${kVar}))return false`)
|
|
6755
|
+
} else if (typeof up === 'object' && up !== null) {
|
|
6756
|
+
const sub = []
|
|
6757
|
+
genCode(up, `${v}[${kVar}]`, sub, ctx)
|
|
6758
|
+
if (sub.length) body.push(`for(const ${kVar} of Object.keys(${v}))if(!${S}.includes(${kVar})){${sub.join('\n ')}\n }`)
|
|
5922
6759
|
} else {
|
|
5923
|
-
|
|
6760
|
+
throw DECLINE
|
|
5924
6761
|
}
|
|
5925
|
-
lines.push(
|
|
6762
|
+
lines.push(isObj ? `{${body.join(';')}}` : `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){${body.join(';')}}`)
|
|
6763
|
+
}
|
|
6764
|
+
|
|
6765
|
+
// unevaluatedProperties where what is evaluated depends on which branches
|
|
6766
|
+
// pass: the set is built at run time with the interpreted engine's rules.
|
|
6767
|
+
// The node's own properties, patternProperties and additionalProperties
|
|
6768
|
+
// count whether or not their values pass; an allOf, anyOf or dependentSchemas
|
|
6769
|
+
// branch, an if, then or else, or a $ref target counts only when it passes,
|
|
6770
|
+
// a oneOf only when exactly one branch passes, and `not` never. Each branch is
|
|
6771
|
+
// asked through an annotation function (emitAnnot).
|
|
6772
|
+
function genUnevaluatedDynamicC(schema, v, pathExpr, lines, ctx, schemaPrefix, fail, isObj) {
|
|
6773
|
+
// additionalProperties evaluates every key the others do not, pass or fail.
|
|
6774
|
+
if (schema.additionalProperties !== undefined) return
|
|
6775
|
+
const ui = ctx.varCounter++
|
|
6776
|
+
const S = `_ev${ui}`
|
|
6777
|
+
const body = [`const ${S}=[]`]
|
|
6778
|
+
addLocalAnnotations(schema, v, S, body, ctx)
|
|
6779
|
+
// The anyOf and oneOf counts are handed to the keywords' own error code
|
|
6780
|
+
// (emitBranchCollapse), with the object they were counted for: they are
|
|
6781
|
+
// declared once at the top of the combined function, where every later
|
|
6782
|
+
// line sees them, and read only when that object is the value at hand, so
|
|
6783
|
+
// a count left from another value, another loop iteration or another place
|
|
6784
|
+
// is never taken for this one. Only in the combined function proper.
|
|
6785
|
+
const share = ctx.inCombined && !ctx.inBranchFn && ctx.hoisted !== undefined &&
|
|
6786
|
+
(Array.isArray(schema.anyOf) || Array.isArray(schema.oneOf))
|
|
6787
|
+
const counts = share ? { anyOf: `_ac${ui}`, oneOf: `_oc${ui}`, oneOfMask: `_om${ui}`, of: `_ao${ui}` } : null
|
|
6788
|
+
if (share) {
|
|
6789
|
+
ctx.hoisted.push(counts.anyOf, counts.oneOf, counts.oneOfMask, counts.of)
|
|
6790
|
+
body.push(`${counts.of}=${v};${counts.oneOfMask}=0`)
|
|
6791
|
+
if (!ctx.branchCounts) ctx.branchCounts = new Map()
|
|
6792
|
+
if (Array.isArray(schema.anyOf)) ctx.branchCounts.set(schema.anyOf, { of: counts.of, count: counts.anyOf, mask: null })
|
|
6793
|
+
if (Array.isArray(schema.oneOf)) ctx.branchCounts.set(schema.oneOf, { of: counts.of, count: counts.oneOf, mask: schema.oneOf.length <= 30 ? counts.oneOfMask : null })
|
|
6794
|
+
}
|
|
6795
|
+
addTopApplicatorAnnotations(schema, v, S, body, ctx, counts)
|
|
6796
|
+
const kVar = `_uk${ui}`
|
|
6797
|
+
const up = schema.unevaluatedProperties
|
|
6798
|
+
if (up === false) {
|
|
6799
|
+
body.push(`for(const ${kVar} of Object.keys(${v}))if(!${S}.includes(${kVar})){${fail('unevaluatedProperties', 'unevaluatedProperties', `{unevaluatedProperty:${kVar}}`, "'must NOT have unevaluated properties'")}}`)
|
|
6800
|
+
} else if (typeof up === 'object' && up !== null) {
|
|
6801
|
+
const pe = emitPtrEsc(ctx)
|
|
6802
|
+
const p = pathExpr ? `${pathExpr}+'/'+${pe}(${kVar})` : `'/'+${pe}(${kVar})`
|
|
6803
|
+
const sub = []
|
|
6804
|
+
genCodeC(up, `${v}[${kVar}]`, p, sub, ctx, schemaPrefix + '/unevaluatedProperties')
|
|
6805
|
+
if (sub.length) body.push(`for(const ${kVar} of Object.keys(${v}))if(!${S}.includes(${kVar})){${sub.join('\n ')}\n }`)
|
|
6806
|
+
} else {
|
|
6807
|
+
throw DECLINE
|
|
6808
|
+
}
|
|
6809
|
+
lines.push(isObj ? `{${body.join(';')}}` : `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){${body.join(';')}}`)
|
|
6810
|
+
}
|
|
6811
|
+
|
|
6812
|
+
// The names a node's own keywords evaluate, pushed onto the array `S`:
|
|
6813
|
+
// present declared properties, keys matching a pattern, and every key when
|
|
6814
|
+
// additionalProperties is there.
|
|
6815
|
+
function addLocalAnnotations(node, v, S, out, ctx) {
|
|
6816
|
+
if (node.additionalProperties !== undefined) {
|
|
6817
|
+
out.push(`for(const _ka in ${v})${S}.push(_ka)`)
|
|
6818
|
+
return
|
|
6819
|
+
}
|
|
6820
|
+
for (const k of Object.keys(node.properties || {})) out.push(`if(Object.prototype.hasOwnProperty.call(${v},${JSON.stringify(k)}))${S}.push(${JSON.stringify(k)})`)
|
|
6821
|
+
const pats = Object.keys(node.patternProperties || {})
|
|
6822
|
+
if (pats.length) {
|
|
6823
|
+
const tests = []
|
|
6824
|
+
const kv = `_pk${ctx.varCounter++}`
|
|
6825
|
+
for (const pat of pats) {
|
|
6826
|
+
const fast = fastPrefixCheck(pat, kv)
|
|
6827
|
+
if (fast) { tests.push(fast); continue }
|
|
6828
|
+
const ri = ctx.varCounter++
|
|
6829
|
+
ctx.closureVars.push(`_re${ri}`)
|
|
6830
|
+
ctx.closureVals.push(safeReClosure(ctx, pat))
|
|
6831
|
+
tests.push(`_re${ri}.test(${kv})`)
|
|
6832
|
+
}
|
|
6833
|
+
out.push(`for(const ${kv} in ${v})if(${tests.join('||')})${S}.push(${kv})`)
|
|
6834
|
+
}
|
|
6835
|
+
}
|
|
6836
|
+
|
|
6837
|
+
// What the applicators of the node that owns the unevaluatedProperties add.
|
|
6838
|
+
// Their validity is checked by the code around this; here a branch only
|
|
6839
|
+
// contributes, through its annotation function, when it passes, and a oneOf
|
|
6840
|
+
// only with exactly one passing branch. An annotation function that fails
|
|
6841
|
+
// leaves `S` as it found it.
|
|
6842
|
+
function addTopApplicatorAnnotations(node, v, S, out, ctx, counts) {
|
|
6843
|
+
if (node.$dynamicRef !== undefined || node.$recursiveRef !== undefined || node.dependencies !== undefined) throw DECLINE
|
|
6844
|
+
for (const b of node.allOf || []) out.push(`${emitAnnot(b, ctx)}(${v},${S})`)
|
|
6845
|
+
// With `counts`, the passing branches of anyOf and oneOf are counted into
|
|
6846
|
+
// variables the caller declared, so the keyword's own error code further
|
|
6847
|
+
// down can skip its branches when the count already says it holds (see
|
|
6848
|
+
// emitBranchCollapse).
|
|
6849
|
+
if (Array.isArray(node.anyOf)) {
|
|
6850
|
+
if (counts) out.push(`${counts.anyOf}=0;${node.anyOf.map((b) => `if(${emitAnnot(b, ctx)}(${v},${S}))${counts.anyOf}++;`).join('')}`)
|
|
6851
|
+
else for (const b of node.anyOf) out.push(`${emitAnnot(b, ctx)}(${v},${S})`)
|
|
6852
|
+
}
|
|
6853
|
+
if (Array.isArray(node.oneOf)) {
|
|
6854
|
+
const t = counts ? counts.oneOf : `_oc${ctx.varCounter++}`
|
|
6855
|
+
// Which branches passed, as bits, for a collapse that has to name them.
|
|
6856
|
+
const mask = counts && node.oneOf.length <= 30 ? counts.oneOfMask : null
|
|
6857
|
+
out.push(`{const ${t}m=${S}.length;${counts ? '' : 'let '}${t}=0;${node.oneOf.map((b, i) => `if(${emitAnnot(b, ctx)}(${v},${S})){${t}++;${mask ? `${mask}|=${1 << i};` : ''}}`).join('')}if(${t}!==1)_rb(${S},${t}m)}`)
|
|
6858
|
+
}
|
|
6859
|
+
if (node.if !== undefined) {
|
|
6860
|
+
const thenCall = node.then !== undefined ? `${emitAnnot(node.then, ctx)}(${v},${S})` : ''
|
|
6861
|
+
const elseCall = node.else !== undefined ? `${emitAnnot(node.else, ctx)}(${v},${S})` : ''
|
|
6862
|
+
out.push(`if(${emitAnnot(node.if, ctx)}(${v},${S})){${thenCall}}else{${elseCall}}`)
|
|
6863
|
+
}
|
|
6864
|
+
if (node.dependentSchemas && typeof node.dependentSchemas === 'object') {
|
|
6865
|
+
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})`)
|
|
6866
|
+
}
|
|
6867
|
+
if (typeof node.$ref === 'string') out.push(`${emitAnnot(refTargetFor(node.$ref, ctx), ctx)}(${v},${S})`)
|
|
6868
|
+
}
|
|
6869
|
+
|
|
6870
|
+
function refTargetFor(ref, ctx) {
|
|
6871
|
+
const m = ref.match(/^#\/(?:\$defs|definitions)\/(.+)$/)
|
|
6872
|
+
const t = m && ctx.rootDefs ? ctx.rootDefs[m[1]] : undefined
|
|
6873
|
+
if (t === undefined) throw DECLINE
|
|
6874
|
+
return t
|
|
6875
|
+
}
|
|
6876
|
+
|
|
6877
|
+
// The keywords an annotation function evaluates itself, through its children;
|
|
6878
|
+
// the rest of a subschema is checked by a plain verdict.
|
|
6879
|
+
const ANNOT_APPLICATORS = new Set(['allOf', 'anyOf', 'oneOf', 'if', 'then', 'else', 'dependentSchemas', '$ref', 'not', 'unevaluatedProperties'])
|
|
6880
|
+
|
|
6881
|
+
// A function `(value, S) => valid` for one subschema: whether it passes, and
|
|
6882
|
+
// when it does, what it evaluated pushed onto S; when it does not, S is left
|
|
6883
|
+
// as it was. Validity is worked out bottom up: the subschema's own keywords
|
|
6884
|
+
// by a verdict of the subschema without its applicators, each applicator from
|
|
6885
|
+
// its branches' annotation functions. Asking each branch for a full verdict
|
|
6886
|
+
// and then again for its annotations repeated every nested branch once per
|
|
6887
|
+
// level, which made nested references the slowest shape there was.
|
|
6888
|
+
function emitAnnot(sub, ctx) {
|
|
6889
|
+
if (sub === true) return '_anT'
|
|
6890
|
+
if (sub === false) return '_anF'
|
|
6891
|
+
if (sub === null || typeof sub !== 'object') throw DECLINE
|
|
6892
|
+
if (!ctx.annFns) {
|
|
6893
|
+
ctx.annFns = new Map()
|
|
6894
|
+
;(ctx.preamble || ctx.helperCode).push('function _anT(){return true}function _anF(){return false}function _rb(S,m){if(S.length!==m)S.length=m}')
|
|
6895
|
+
}
|
|
6896
|
+
const hit = ctx.annFns.get(sub)
|
|
6897
|
+
if (hit !== undefined) {
|
|
6898
|
+
if (hit === null) throw DECLINE // a reference cycle: not modelled here
|
|
6899
|
+
return hit
|
|
6900
|
+
}
|
|
6901
|
+
if (sub.$dynamicRef !== undefined || sub.$recursiveRef !== undefined || sub.dependencies !== undefined) throw DECLINE
|
|
6902
|
+
// unevaluatedItems reads what the applicators evaluate too; the local
|
|
6903
|
+
// verdict would see it without them.
|
|
6904
|
+
if (sub.unevaluatedItems !== undefined) throw DECLINE
|
|
6905
|
+
ctx.annFns.set(sub, null)
|
|
6906
|
+
const name = `_an${ctx.varCounter++}`
|
|
6907
|
+
const local = {}
|
|
6908
|
+
for (const k of Object.keys(sub)) if (!ANNOT_APPLICATORS.has(k)) local[k] = sub[k]
|
|
6909
|
+
const vl = []
|
|
6910
|
+
nestedGenCode(local, '_av', vl, ctx)
|
|
6911
|
+
const decls = [`function ${name}_v(_av){${vl.join('\n ')}\n return true}`]
|
|
6912
|
+
const body = [`if(!${name}_v(d))return false`]
|
|
6913
|
+
if (sub.not !== undefined) {
|
|
6914
|
+
const nl = []
|
|
6915
|
+
nestedGenCode(sub.not, '_nv', nl, ctx)
|
|
6916
|
+
decls.push(`function ${name}_n(_nv){${nl.join('\n ')}\n return true}`)
|
|
6917
|
+
body.push(`if(${name}_n(d))return false`)
|
|
6918
|
+
}
|
|
6919
|
+
body.push(`const _m=S.length;const _o=typeof d==='object'&&d!==null&&!Array.isArray(d)`)
|
|
6920
|
+
const locals = []
|
|
6921
|
+
addLocalAnnotations(sub, 'd', 'S', locals, ctx)
|
|
6922
|
+
if (locals.length) body.push(`if(_o){${locals.join(';')}}`)
|
|
6923
|
+
const failOut = '{_rb(S,_m);return false}'
|
|
6924
|
+
for (const b of sub.allOf || []) body.push(`if(!${emitAnnot(b, ctx)}(d,S))${failOut}`)
|
|
6925
|
+
if (Array.isArray(sub.anyOf)) body.push(`{let _a=false;${sub.anyOf.map((b) => `if(${emitAnnot(b, ctx)}(d,S))_a=true;`).join('')}if(!_a)${failOut}}`)
|
|
6926
|
+
if (Array.isArray(sub.oneOf)) body.push(`{let _c=0;${sub.oneOf.map((b) => `if(${emitAnnot(b, ctx)}(d,S))_c++;`).join('')}if(_c!==1)${failOut}}`)
|
|
6927
|
+
if (sub.if !== undefined) {
|
|
6928
|
+
const thenCheck = sub.then !== undefined ? `if(!${emitAnnot(sub.then, ctx)}(d,S))${failOut}` : ''
|
|
6929
|
+
const elseCheck = sub.else !== undefined ? `if(!${emitAnnot(sub.else, ctx)}(d,S))${failOut}` : ''
|
|
6930
|
+
body.push(`if(${emitAnnot(sub.if, ctx)}(d,S)){${thenCheck}}else{${elseCheck}}`)
|
|
6931
|
+
}
|
|
6932
|
+
if (sub.dependentSchemas && typeof sub.dependentSchemas === 'object') {
|
|
6933
|
+
for (const [k, ds] of Object.entries(sub.dependentSchemas)) body.push(`if(_o&&Object.prototype.hasOwnProperty.call(d,${JSON.stringify(k)})&&!${emitAnnot(ds, ctx)}(d,S))${failOut}`)
|
|
6934
|
+
}
|
|
6935
|
+
if (typeof sub.$ref === 'string') body.push(`if(!${emitAnnot(refTargetFor(sub.$ref, ctx), ctx)}(d,S))${failOut}`)
|
|
6936
|
+
// An unevaluatedProperties of its own: the keys this subschema evaluated
|
|
6937
|
+
// are the ones pushed since _m; what is left must pass it, and then every
|
|
6938
|
+
// key is evaluated.
|
|
6939
|
+
const up = sub.unevaluatedProperties
|
|
6940
|
+
if (up !== undefined && up !== true && sub.additionalProperties === undefined) {
|
|
6941
|
+
if (up === false) {
|
|
6942
|
+
body.push(`if(_o){for(const _k of Object.keys(d))if(S.indexOf(_k,_m)<0)${failOut}}`)
|
|
6943
|
+
} else if (typeof up === 'object' && up !== null) {
|
|
6944
|
+
const ul = []
|
|
6945
|
+
nestedGenCode(up, '_uv', ul, ctx)
|
|
6946
|
+
decls.push(`function ${name}_u(_uv){${ul.join('\n ')}\n return true}`)
|
|
6947
|
+
body.push(`if(_o){for(const _k of Object.keys(d))if(S.indexOf(_k,_m)<0&&!${name}_u(d[_k]))${failOut}}`)
|
|
6948
|
+
} else {
|
|
6949
|
+
throw DECLINE
|
|
6950
|
+
}
|
|
6951
|
+
}
|
|
6952
|
+
if (up !== undefined && sub.additionalProperties === undefined) body.push(`if(_o)for(const _ka in d)S.push(_ka)`)
|
|
6953
|
+
decls.push(`function ${name}(d,S){${body.join(';')};return true}`)
|
|
6954
|
+
;(ctx.preamble || ctx.helperCode).push(decls.join(''))
|
|
6955
|
+
ctx.annFns.set(sub, name)
|
|
6956
|
+
return name
|
|
6957
|
+
}
|
|
6958
|
+
|
|
6959
|
+
// unevaluatedItems worked out at run time, for the combined function. The
|
|
6960
|
+
// evaluated positions of an array are kept as the interpreter keeps them
|
|
6961
|
+
// (lib/plan-compiler.js): `n`, every index below it evaluated, and `x`, the
|
|
6962
|
+
// indexes past it that `contains` matched. This node's own prefixItems,
|
|
6963
|
+
// items and contains mark them whatever their own verdict, and each in-place
|
|
6964
|
+
// subschema marks them only when it passes (emitAnnotI); what is left must
|
|
6965
|
+
// pass unevaluatedItems. `false` reports once, at the first position left,
|
|
6966
|
+
// as the interpreter does.
|
|
6967
|
+
function genUnevaluatedItemsDynamicC(schema, v, pathExpr, lines, ctx, schemaPrefix, fail) {
|
|
6968
|
+
if (schema.$dynamicRef !== undefined || schema.$recursiveRef !== undefined) throw DECLINE
|
|
6969
|
+
if (Array.isArray(schema.items) || schema.additionalItems !== undefined) throw DECLINE
|
|
6970
|
+
annItemHelpers(ctx)
|
|
6971
|
+
const ui = ctx.varCounter++
|
|
6972
|
+
const S = `_es${ui}`
|
|
6973
|
+
const body = [`const ${S}={n:0,x:null}`]
|
|
6974
|
+
addLocalItemAnnotations(schema, v, S, body, ctx)
|
|
6975
|
+
for (const b of schema.allOf || []) body.push(`${emitAnnotI(b, ctx)}(${v},${S})`)
|
|
6976
|
+
if (Array.isArray(schema.anyOf)) for (const b of schema.anyOf) body.push(`${emitAnnotI(b, ctx)}(${v},${S})`)
|
|
6977
|
+
if (Array.isArray(schema.oneOf)) {
|
|
6978
|
+
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)}`)
|
|
6979
|
+
}
|
|
6980
|
+
if (schema.if !== undefined) {
|
|
6981
|
+
const thenCall = schema.then !== undefined ? `${emitAnnotI(schema.then, ctx)}(${v},${S})` : ''
|
|
6982
|
+
const elseCall = schema.else !== undefined ? `${emitAnnotI(schema.else, ctx)}(${v},${S})` : ''
|
|
6983
|
+
body.push(`if(${emitAnnotI(schema.if, ctx)}(${v},${S})){${thenCall}}else{${elseCall}}`)
|
|
6984
|
+
}
|
|
6985
|
+
if (typeof schema.$ref === 'string') body.push(`${emitAnnotI(refTargetFor(schema.$ref, ctx), ctx)}(${v},${S})`)
|
|
6986
|
+
const iv = `_ix${ui}`
|
|
6987
|
+
const ui2 = schema.unevaluatedItems
|
|
6988
|
+
if (ui2 === false) {
|
|
6989
|
+
const failI = fail('unevaluatedItems', 'unevaluatedItems', `{limit:${iv}}`, `'must NOT have more than '+${iv}+' items'`)
|
|
6990
|
+
body.push(`for(let ${iv}=0;${iv}<${v}.length;${iv}++){if(_hI(${S},${iv}))continue;${failI};break}`)
|
|
6991
|
+
} else if (typeof ui2 === 'object' && ui2 !== null) {
|
|
6992
|
+
const pe = pathExpr ? `${pathExpr}+'/'+${iv}` : `'/'+${iv}`
|
|
6993
|
+
const sub = []
|
|
6994
|
+
genCodeC(ui2, `${v}[${iv}]`, pe, sub, ctx, schemaPrefix + '/unevaluatedItems')
|
|
6995
|
+
if (sub.length) body.push(`for(let ${iv}=0;${iv}<${v}.length;${iv}++){if(_hI(${S},${iv}))continue;${sub.join('\n ')}\n }`)
|
|
6996
|
+
} else {
|
|
6997
|
+
throw DECLINE
|
|
6998
|
+
}
|
|
6999
|
+
lines.push(`if(Array.isArray(${v})){${body.join(';')}}`)
|
|
7000
|
+
}
|
|
7001
|
+
|
|
7002
|
+
function annItemHelpers(ctx) {
|
|
7003
|
+
if (ctx.annItemHelpers) return
|
|
7004
|
+
ctx.annItemHelpers = true
|
|
7005
|
+
;(ctx.preamble || ctx.helperCode).push(
|
|
7006
|
+
'function _aI(S,i){if(i<S.n)return;if(i===S.n){S.n=i+1;return}(S.x||(S.x=[])).push(i)}' +
|
|
7007
|
+
'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)}' +
|
|
7008
|
+
'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}' +
|
|
7009
|
+
'function _rbI(S,n,l){S.n=n;if(S.x!==null)S.x.length=l}' +
|
|
7010
|
+
'function _anIT(){return true}function _anIF(){return false}')
|
|
7011
|
+
}
|
|
7012
|
+
|
|
7013
|
+
// The positions a node's own keywords evaluate, marked on `S`, for an array.
|
|
7014
|
+
function addLocalItemAnnotations(node, v, S, out, ctx) {
|
|
7015
|
+
if (Array.isArray(node.items) || node.additionalItems !== undefined) throw DECLINE
|
|
7016
|
+
const prefix = Array.isArray(node.prefixItems) ? node.prefixItems.length : 0
|
|
7017
|
+
if (prefix) out.push(`_mrI(${S},0,Math.min(${v}.length,${prefix}))`)
|
|
7018
|
+
if (node.items !== undefined) out.push(`_mrI(${S},${prefix},${v}.length)`)
|
|
7019
|
+
if (node.contains !== undefined) {
|
|
7020
|
+
const cl = []
|
|
7021
|
+
nestedGenCode(node.contains, '_cv', cl, ctx)
|
|
7022
|
+
const ci = ctx.varCounter++
|
|
7023
|
+
;(ctx.preamble || ctx.helperCode).push(`function _cI${ci}(_cv){${cl.join('\n ')}\n return true}`)
|
|
7024
|
+
out.push(`for(let _i=0;_i<${v}.length;_i++)if(_cI${ci}(${v}[_i]))_aI(${S},_i)`)
|
|
7025
|
+
}
|
|
7026
|
+
}
|
|
7027
|
+
|
|
7028
|
+
// The keywords an item annotation function evaluates through its children;
|
|
7029
|
+
// the rest of a subschema is checked by a plain verdict.
|
|
7030
|
+
const ANNOT_ITEM_APPLICATORS = new Set(['allOf', 'anyOf', 'oneOf', 'if', 'then', 'else', '$ref', 'not', 'unevaluatedItems'])
|
|
7031
|
+
|
|
7032
|
+
// A function `(value, S) => valid` for one subschema, marking on S the array
|
|
7033
|
+
// positions it evaluated when it passes, and leaving S as it was when it does
|
|
7034
|
+
// not. A subschema with an unevaluatedItems of its own evaluates every
|
|
7035
|
+
// position once that passes, so it works on a record of its own and then
|
|
7036
|
+
// marks them all.
|
|
7037
|
+
function emitAnnotI(sub, ctx) {
|
|
7038
|
+
if (sub === true) return '_anIT'
|
|
7039
|
+
if (sub === false) return '_anIF'
|
|
7040
|
+
if (sub === null || typeof sub !== 'object') throw DECLINE
|
|
7041
|
+
if (!ctx.annFnsI) ctx.annFnsI = new Map()
|
|
7042
|
+
const hit = ctx.annFnsI.get(sub)
|
|
7043
|
+
if (hit !== undefined) {
|
|
7044
|
+
if (hit === null) throw DECLINE
|
|
7045
|
+
return hit
|
|
7046
|
+
}
|
|
7047
|
+
if (sub.$dynamicRef !== undefined || sub.$recursiveRef !== undefined) throw DECLINE
|
|
7048
|
+
if (Array.isArray(sub.items) || sub.additionalItems !== undefined) throw DECLINE
|
|
7049
|
+
// unevaluatedProperties reads the applicators too, which the local verdict
|
|
7050
|
+
// below leaves out.
|
|
7051
|
+
if (sub.unevaluatedProperties !== undefined && sub.unevaluatedProperties !== true) throw DECLINE
|
|
7052
|
+
ctx.annFnsI.set(sub, null)
|
|
7053
|
+
annItemHelpers(ctx)
|
|
7054
|
+
const name = `_ai${ctx.varCounter++}`
|
|
7055
|
+
const local = {}
|
|
7056
|
+
for (const k of Object.keys(sub)) if (!ANNOT_ITEM_APPLICATORS.has(k)) local[k] = sub[k]
|
|
7057
|
+
const vl = []
|
|
7058
|
+
nestedGenCode(local, '_av', vl, ctx)
|
|
7059
|
+
const decls = [`function ${name}_v(_av){${vl.join('\n ')}\n return true}`]
|
|
7060
|
+
const body = [`if(!${name}_v(d))return false`]
|
|
7061
|
+
if (sub.not !== undefined) {
|
|
7062
|
+
const nl = []
|
|
7063
|
+
nestedGenCode(sub.not, '_nv', nl, ctx)
|
|
7064
|
+
decls.push(`function ${name}_n(_nv){${nl.join('\n ')}\n return true}`)
|
|
7065
|
+
body.push(`if(${name}_n(d))return false`)
|
|
7066
|
+
}
|
|
7067
|
+
const own = sub.unevaluatedItems !== undefined && sub.unevaluatedItems !== true
|
|
7068
|
+
const R = own ? '_R' : 'S'
|
|
7069
|
+
body.push(own ? `const _R={n:0,x:null}` : `const _n0=S.n,_x0=S.x===null?0:S.x.length`)
|
|
7070
|
+
const failOut = own ? 'return false' : '{_rbI(S,_n0,_x0);return false}'
|
|
7071
|
+
const locals = []
|
|
7072
|
+
addLocalItemAnnotations(sub, 'd', R, locals, ctx)
|
|
7073
|
+
if (locals.length) body.push(`if(Array.isArray(d)){${locals.join(';')}}`)
|
|
7074
|
+
for (const b of sub.allOf || []) body.push(`if(!${emitAnnotI(b, ctx)}(d,${R}))${failOut}`)
|
|
7075
|
+
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}}`)
|
|
7076
|
+
if (Array.isArray(sub.oneOf)) body.push(`{let _c=0;${sub.oneOf.map((b) => `if(${emitAnnotI(b, ctx)}(d,${R}))_c++;`).join('')}if(_c!==1)${failOut}}`)
|
|
7077
|
+
if (sub.if !== undefined) {
|
|
7078
|
+
const thenCheck = sub.then !== undefined ? `if(!${emitAnnotI(sub.then, ctx)}(d,${R}))${failOut}` : ''
|
|
7079
|
+
const elseCheck = sub.else !== undefined ? `if(!${emitAnnotI(sub.else, ctx)}(d,${R}))${failOut}` : ''
|
|
7080
|
+
body.push(`if(${emitAnnotI(sub.if, ctx)}(d,${R})){${thenCheck}}else{${elseCheck}}`)
|
|
7081
|
+
}
|
|
7082
|
+
if (typeof sub.$ref === 'string') body.push(`if(!${emitAnnotI(refTargetFor(sub.$ref, ctx), ctx)}(d,${R}))${failOut}`)
|
|
7083
|
+
if (own) {
|
|
7084
|
+
const u = sub.unevaluatedItems
|
|
7085
|
+
if (u === false) {
|
|
7086
|
+
body.push(`if(Array.isArray(d)){for(let _i=0;_i<d.length;_i++)if(!_hI(_R,_i))return false}`)
|
|
7087
|
+
} else if (typeof u === 'object' && u !== null) {
|
|
7088
|
+
const ul = []
|
|
7089
|
+
nestedGenCode(u, '_uv', ul, ctx)
|
|
7090
|
+
decls.push(`function ${name}_u(_uv){${ul.join('\n ')}\n return true}`)
|
|
7091
|
+
body.push(`if(Array.isArray(d)){for(let _i=0;_i<d.length;_i++)if(!_hI(_R,_i)&&!${name}_u(d[_i]))return false}`)
|
|
7092
|
+
} else {
|
|
7093
|
+
throw DECLINE
|
|
7094
|
+
}
|
|
7095
|
+
body.push(`if(Array.isArray(d))_mrI(S,0,d.length)`)
|
|
7096
|
+
}
|
|
7097
|
+
// `unevaluatedItems: true` evaluates every position once the subschema
|
|
7098
|
+
// has passed, with nothing to check; marked last, so a failure above has
|
|
7099
|
+
// nothing of it to undo.
|
|
7100
|
+
if (sub.unevaluatedItems === true) body.push(`if(Array.isArray(d))_mrI(S,0,d.length)`)
|
|
7101
|
+
decls.push(`function ${name}(d,S){${body.join(';')};return true}`)
|
|
7102
|
+
;(ctx.preamble || ctx.helperCode).push(decls.join(''))
|
|
7103
|
+
ctx.annFnsI.set(sub, name)
|
|
7104
|
+
return name
|
|
5926
7105
|
}
|
|
5927
7106
|
|
|
5928
7107
|
function genUnevaluatedFalseC(schema, v, pathExpr, lines, ctx, schemaPrefix, fail, isObj) {
|
|
@@ -5952,6 +7131,41 @@ function genUnevaluatedItemsFalseC(schema, v, pathExpr, lines, ctx, schemaPrefix
|
|
|
5952
7131
|
}
|
|
5953
7132
|
}
|
|
5954
7133
|
|
|
7134
|
+
// The error generator's per-key loop (genAdditionalSchemaE), in the combined
|
|
7135
|
+
// generator: the subschema compiled by genCodeC at each additional key, with
|
|
7136
|
+
// that key's path. Patterns go through the same matchers patternProperties
|
|
7137
|
+
// uses here.
|
|
7138
|
+
function genAdditionalSchemaC(schema, v, pathExpr, lines, ctx, schemaPrefix, isObj) {
|
|
7139
|
+
const ki = ctx.varCounter++
|
|
7140
|
+
const kVar = `_ak${ki}`
|
|
7141
|
+
const known = Object.keys(schema.properties || {})
|
|
7142
|
+
const conds = []
|
|
7143
|
+
if (known.length) {
|
|
7144
|
+
const set = `_akn${ki}`
|
|
7145
|
+
ctx.closureVars.push(set)
|
|
7146
|
+
ctx.closureVals.push(new Set(known))
|
|
7147
|
+
conds.push(`!${set}.has(${kVar})`)
|
|
7148
|
+
}
|
|
7149
|
+
const pats = []
|
|
7150
|
+
for (const pat of Object.keys(schema.patternProperties || {})) {
|
|
7151
|
+
const fast = fastPrefixCheck(pat, kVar)
|
|
7152
|
+
if (fast) { pats.push(fast); continue }
|
|
7153
|
+
const ri = ctx.varCounter++
|
|
7154
|
+
ctx.closureVars.push(`_re${ri}`)
|
|
7155
|
+
ctx.closureVals.push(safeReClosure(ctx, pat))
|
|
7156
|
+
pats.push(`_re${ri}.test(${kVar})`)
|
|
7157
|
+
}
|
|
7158
|
+
if (pats.length) conds.push(`!(${pats.join('||')})`)
|
|
7159
|
+
const guard = isObj ? '' : `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v}))`
|
|
7160
|
+
const keep = conds.length ? `if(${conds.join('&&')})` : ''
|
|
7161
|
+
const pe = emitPtrEsc(ctx)
|
|
7162
|
+
const p = pathExpr ? `${pathExpr}+'/'+${pe}(${kVar})` : `'/'+${pe}(${kVar})`
|
|
7163
|
+
const sub = []
|
|
7164
|
+
genCodeC(schema.additionalProperties, `${v}[${kVar}]`, p, sub, ctx, schemaPrefix + '/additionalProperties')
|
|
7165
|
+
if (sub.length === 0) return
|
|
7166
|
+
lines.push(`${guard}{for(const ${kVar} in ${v}){${keep}{${sub.join('\n ')}\n }}}`)
|
|
7167
|
+
}
|
|
7168
|
+
|
|
5955
7169
|
function genPatternPropertiesC(schema, v, pathExpr, lines, ctx, schemaPrefix, fail, isObj) {
|
|
5956
7170
|
let handledPropertyNames = false
|
|
5957
7171
|
const ppEntries = Object.entries(schema.patternProperties)
|
|
@@ -6070,7 +7284,7 @@ function genDependentSchemasC(schema, v, pathExpr, lines, ctx, schemaPrefix) {
|
|
|
6070
7284
|
function genPropertyNamesFalseC(schema, v, pathExpr, lines, ctx, schemaPrefix) {
|
|
6071
7285
|
const ki = ctx.varCounter++
|
|
6072
7286
|
const p = pathExpr || '""'
|
|
6073
|
-
lines.push(`;if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){for(const _k${ki} in ${v}){(
|
|
7287
|
+
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'}`)}}}`)
|
|
6074
7288
|
}
|
|
6075
7289
|
|
|
6076
7290
|
function genPropertyNamesC(schema, v, pathExpr, lines, ctx, schemaPrefix, fail) {
|
|
@@ -6124,12 +7338,18 @@ function genContainsC(schema, v, pathExpr, lines, ctx, schemaPrefix, fail) {
|
|
|
6124
7338
|
}
|
|
6125
7339
|
|
|
6126
7340
|
function genAnyOfC(schema, v, pathExpr, lines, ctx, schemaPrefix, fail) {
|
|
7341
|
+
// The collapsed branch error the other engines report (ATA4001-4003),
|
|
7342
|
+
// built by the error generator's emitter; see compileToJSCombined.
|
|
7343
|
+
if (ctx.inCombined) return emitBranchCollapse(schema.anyOf, 'anyOf', v, pathExpr, lines, ctx, schemaPrefix)
|
|
6127
7344
|
const fi = ctx.varCounter++
|
|
6128
7345
|
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}` })
|
|
6129
7346
|
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'")}}}`)
|
|
6130
7347
|
}
|
|
6131
7348
|
|
|
6132
7349
|
function genOneOfC(schema, v, pathExpr, lines, ctx, schemaPrefix, fail) {
|
|
7350
|
+
// The collapsed branch error the other engines report (ATA4001-4003),
|
|
7351
|
+
// built by the error generator's emitter; see compileToJSCombined.
|
|
7352
|
+
if (ctx.inCombined) return emitBranchCollapse(schema.oneOf, 'oneOf', v, pathExpr, lines, ctx, schemaPrefix)
|
|
6133
7353
|
const fi = ctx.varCounter++
|
|
6134
7354
|
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}` })
|
|
6135
7355
|
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'")}}}`)
|