ata-validator 1.52.0 → 1.54.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -0
- package/compat.js +325 -29
- package/lib/codegen-engine.js +2 -1
- package/lib/compat-errors.js +131 -17
- package/lib/dialect.js +36 -1
- package/lib/draft7.js +80 -61
- package/lib/enrich-site.js +29 -2
- package/lib/error-runtime.js +6 -2
- package/lib/formats-source.js +43 -66
- package/lib/formats.js +37 -231
- package/lib/interpreter-compat.js +144 -0
- package/lib/interpreter-core.js +204 -17
- package/lib/js-compiler-errors.js +52 -16
- package/lib/js-compiler.js +540 -66
- package/lib/keywords.js +24 -1
- package/lib/levenshtein.js +21 -5
- package/lib/metaschemas.js +5 -3
- package/lib/plan-compiler.js +28 -6
- package/lib/regex-linear.js +81 -1
- package/lib/safe-regex.js +14 -11
- package/lib/schema-scan.js +20 -1
- package/lib/tier0.js +1 -0
- package/lib/unique-items.js +16 -5
- package/lib/uri-core.js +155 -0
- package/lib/validator-core.js +118 -23
- package/lib/version.js +1 -1
- package/package.json +9 -9
package/lib/js-compiler.js
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
const { compileFunction } = require('./compile-fn')
|
|
3
|
+
// A comment that differs at every compile, put in front of every program
|
|
4
|
+
// built at run time. V8 keeps one compiled function per source text of a
|
|
5
|
+
// `new Function` call, with one feedback vector: two schemas that generate
|
|
6
|
+
// the same code (two copies of a record schema under different `$id`s) then
|
|
7
|
+
// shared their inline caches, and each other's data shapes, so both ran
|
|
8
|
+
// megamorphic. The emitted (standalone) sources carry no tag, so a build is
|
|
9
|
+
// the same text every time.
|
|
10
|
+
let _compileSeq = 0
|
|
11
|
+
const seqTag = () => `/*${++_compileSeq}*/`
|
|
3
12
|
|
|
4
13
|
// uniqueItems helpers for the boolean generator, hoisted once per compiled
|
|
5
14
|
// function (and carried into standalone output through the preamble).
|
|
@@ -9,7 +18,7 @@ const { compileFunction } = require('./compile-fn')
|
|
|
9
18
|
// `enum` compare instances by value: two objects with the same members are
|
|
10
19
|
// equal whatever order their keys were written in. Comparing serialized
|
|
11
20
|
// forms answers a different question and costs a string per call.
|
|
12
|
-
const DEQ_HELPER = "function _deq(a,b){if(a===b)return true;if(a===null||b===null||typeof a!=='object'||typeof b!=='object')return false;var aa=Array.isArray(a);if(aa!==Array.isArray(b))return false;var i;if(aa){if(a.length!==b.length)return false;for(i=
|
|
21
|
+
const DEQ_HELPER = "function _deq(a,b){if(a===b)return true;if(a===null||b===null||typeof a!=='object'||typeof b!=='object')return false;var aa=Array.isArray(a);if(aa!==Array.isArray(b))return false;var i,k,x,y;if(aa){if(a.length!==b.length)return false;for(i=a.length;i--!==0;)if(!_deq(a[i],b[i]))return false;return true}var ka=Object.keys(a);if(ka.length!==Object.keys(b).length)return false;for(i=ka.length;i--!==0;){k=ka[i];if(!Object.prototype.hasOwnProperty.call(b,k))return false;x=a[k];y=b[k];if(x!==y&&(x===null||y===null||typeof x!=='object'||typeof y!=='object'))return false}for(i=ka.length;i--!==0;){k=ka[i];x=a[k];if(x!==null&&typeof x==='object'&&!_deq(x,b[k]))return false}return true}"
|
|
13
22
|
|
|
14
23
|
// uniqueItems helpers, emitted from the functions the interpreted engine
|
|
15
24
|
// calls (lib/unique-items.js): `_uq` answers whether every item is distinct.
|
|
@@ -152,7 +161,26 @@ function enumOperand(ctx, vals, v, lines) {
|
|
|
152
161
|
|
|
153
162
|
// One `enum` test: primitives compare with `===`, which needs no call, and
|
|
154
163
|
// object members go through structural equality.
|
|
164
|
+
// An enum of 64 or more strings is a Set in the preamble, looked up once: the
|
|
165
|
+
// chain of comparisons grows with the list and the lookup does not. Below
|
|
166
|
+
// that the chain wins on real documents, whose values tend to sit early in
|
|
167
|
+
// the list: on SchemaStore's samples a Set made jscsrc (62 values) 16%
|
|
168
|
+
// slower and bigquery-table (18) 19% slower, while from 77 values up it
|
|
169
|
+
// made every schema faster (chrome-manifest 77, tsconfig 108, jscpd 224 at
|
|
170
|
+
// 6x, cryproj 2461 at 28x).
|
|
171
|
+
const ENUM_SET_MIN = 64
|
|
155
172
|
function enumCondition(ctx, vals, v) {
|
|
173
|
+
if (vals.length >= ENUM_SET_MIN && ctx && Array.isArray(ctx.preamble) && vals.every((x) => typeof x === 'string')) {
|
|
174
|
+
if (!ctx.enumSets) ctx.enumSets = new Map()
|
|
175
|
+
const key = JSON.stringify(vals)
|
|
176
|
+
let name = ctx.enumSets.get(key)
|
|
177
|
+
if (name === undefined) {
|
|
178
|
+
name = '_es' + ctx.varCounter++
|
|
179
|
+
ctx.enumSets.set(key, name)
|
|
180
|
+
hoist(ctx, `const ${name}=new Set(${key})`)
|
|
181
|
+
}
|
|
182
|
+
return `${name}.has(${v})`
|
|
183
|
+
}
|
|
156
184
|
const parts = []
|
|
157
185
|
const prims = vals.filter((x) => x === null || typeof x !== 'object')
|
|
158
186
|
const objs = vals.filter((x) => x !== null && typeof x === 'object')
|
|
@@ -365,7 +393,7 @@ function compileToJS(schema, defs, schemaMap) {
|
|
|
365
393
|
// required applies to objects only; other types are ignored.
|
|
366
394
|
if (schema.required && Array.isArray(schema.required)) {
|
|
367
395
|
for (const key of schema.required) {
|
|
368
|
-
checks.push((d) => typeof d !== 'object' || d === null || Array.isArray(d) || hasOwnKey(d, key))
|
|
396
|
+
checks.push((d) => typeof d !== 'object' || d === null || Array.isArray(d) || (hasOwnKey(d, key) && d[key] !== undefined))
|
|
369
397
|
}
|
|
370
398
|
}
|
|
371
399
|
|
|
@@ -376,7 +404,9 @@ function compileToJS(schema, defs, schemaMap) {
|
|
|
376
404
|
if (!propCheck) return null // bail if sub-schema too complex
|
|
377
405
|
checks.push((d) => {
|
|
378
406
|
if (typeof d !== 'object' || d === null || !hasOwnKey(d, key)) return true
|
|
379
|
-
|
|
407
|
+
// An undefined value is not checked, as the generated code does not.
|
|
408
|
+
const v = d[key]
|
|
409
|
+
return v === undefined || propCheck(v)
|
|
380
410
|
})
|
|
381
411
|
}
|
|
382
412
|
}
|
|
@@ -539,7 +569,7 @@ function compileToJS(schema, defs, schemaMap) {
|
|
|
539
569
|
// format — hand-written fast checks. A format the code generator asserts
|
|
540
570
|
// but this path cannot is a decline, not a pass.
|
|
541
571
|
if (schema.format) {
|
|
542
|
-
const fc =
|
|
572
|
+
const fc = formatChecks()[schema.format]
|
|
543
573
|
if (fc) checks.push((d) => typeof d !== 'string' || fc(d))
|
|
544
574
|
else if (FORMAT_CODEGEN[schema.format]) return null
|
|
545
575
|
}
|
|
@@ -837,14 +867,26 @@ function _canonical(x) {
|
|
|
837
867
|
// schema with a `format` is compiled; loading it with this module cost every
|
|
838
868
|
// process 0.8 ms of its cold start.
|
|
839
869
|
let _formatsCache = null
|
|
840
|
-
|
|
870
|
+
// The format source generators, and the few runtime values from
|
|
871
|
+
// lib/formats.js they are written with, read from that file on first use.
|
|
872
|
+
function getFormats () {
|
|
873
|
+
if (_formatsCache !== null) return _formatsCache
|
|
874
|
+
const out = { ...require('./formats-source') }
|
|
875
|
+
for (const k of ['IPV4', 'IPV6_FULL', 'ipv4Range']) Object.defineProperty(out, k, { get: () => require('./formats')[k], enumerable: true })
|
|
876
|
+
return (_formatsCache = out)
|
|
877
|
+
}
|
|
841
878
|
// The closure path's formats. It carried its own idea of an email and knew
|
|
842
879
|
// five formats in all, so a schema that reached it was told "valid" for
|
|
843
880
|
// anything the other engines turned away; the first call on a fresh validator
|
|
844
881
|
// is the one that lands here. Every entry now names the same function the
|
|
845
882
|
// other two paths use, and the list is the list they implement.
|
|
846
|
-
|
|
847
|
-
|
|
883
|
+
// Built on first use: lib/formats.js loads only for a schema with a format
|
|
884
|
+
// this path checks, not with the compiler.
|
|
885
|
+
let _formatChecks = null
|
|
886
|
+
function formatChecks () {
|
|
887
|
+
if (_formatChecks !== null) return _formatChecks
|
|
888
|
+
const _fmtFns = require('./formats')
|
|
889
|
+
_formatChecks = {
|
|
848
890
|
email: _fmtFns.email,
|
|
849
891
|
'idn-email': _fmtFns.idnEmail,
|
|
850
892
|
date: _fmtFns.date,
|
|
@@ -863,6 +905,8 @@ const FORMAT_CHECKS = {
|
|
|
863
905
|
'json-pointer': _fmtFns.jsonPointer,
|
|
864
906
|
'relative-json-pointer': _fmtFns.relativeJsonPointer,
|
|
865
907
|
regex: (s) => { try { new RegExp(s, 'u'); return true } catch { return false } },
|
|
908
|
+
}
|
|
909
|
+
return _formatChecks
|
|
866
910
|
}
|
|
867
911
|
|
|
868
912
|
// Dangerous JS property names that exist on Object.prototype
|
|
@@ -1236,6 +1280,51 @@ function emitPropFn (prop, ctx) {
|
|
|
1236
1280
|
hoist(ctx, `function ${fnName}(d){${bodyLines.join('\n ')}\n return true}`)
|
|
1237
1281
|
return fnName
|
|
1238
1282
|
}
|
|
1283
|
+
// An array's item schema that gets two item functions (see the items case in
|
|
1284
|
+
// genCodeNode): an object of many optional properties and nothing else that
|
|
1285
|
+
// reads keys. 32 or more optional properties, no names on Object.prototype
|
|
1286
|
+
// (the key loop reads `v[k]`, which for `__proto__` is not the data), and no
|
|
1287
|
+
// keyword that needs every key or the evaluated set.
|
|
1288
|
+
const KEY_LOOP_MIN = 32
|
|
1289
|
+
const KEY_LOOP_KEYS = new Set(['type', 'properties', 'required', 'title', 'description', '$comment', 'examples', 'default', 'deprecated', 'readOnly', 'writeOnly'])
|
|
1290
|
+
function keyLoopEligible (item) {
|
|
1291
|
+
if (item === null || typeof item !== 'object' || Array.isArray(item) || item.type !== 'object') return false
|
|
1292
|
+
const props = item.properties
|
|
1293
|
+
if (props === null || typeof props !== 'object' || Array.isArray(props)) return false
|
|
1294
|
+
for (const k of Object.keys(item)) if (!KEY_LOOP_KEYS.has(k)) return false
|
|
1295
|
+
const req = new Set(Array.isArray(item.required) ? item.required : [])
|
|
1296
|
+
let optional = 0
|
|
1297
|
+
for (const k of Object.keys(props)) {
|
|
1298
|
+
if (PROTO_NAMES.has(k) || k === '__proto__') return false
|
|
1299
|
+
if (!req.has(k)) optional++
|
|
1300
|
+
}
|
|
1301
|
+
return optional >= KEY_LOOP_MIN
|
|
1302
|
+
}
|
|
1303
|
+
// Whether two values list the same keys in the same order, which objects of
|
|
1304
|
+
// one shape do. Anything that is not two objects counts as alike.
|
|
1305
|
+
const SAME_KEYS_HELPER = 'function _sameKeys(a,b){if(a===null||b===null||typeof a!==\'object\'||typeof b!==\'object\')return true;const ka=[];for(const k in a)ka.push(k);let i=0;for(const k in b){if(ka[i++]!==k)return false}return i===ka.length}'
|
|
1306
|
+
// One item function: the schema's ordinary code, or (keyLoop) the same with
|
|
1307
|
+
// its optional properties checked from the item's own keys. Null when either
|
|
1308
|
+
// cannot be written as a function of its own.
|
|
1309
|
+
function emitItemFn (item, ctx, keyLoop) {
|
|
1310
|
+
if (!ctx.preamble) return null
|
|
1311
|
+
const bodyLines = []
|
|
1312
|
+
const outerDeferred = ctx.deferredChecks
|
|
1313
|
+
ctx.deferredChecks = null
|
|
1314
|
+
const outerReuse = ctx.plainReuse
|
|
1315
|
+
ctx.plainReuse = undefined
|
|
1316
|
+
const outerBytes = ctx.fnBytes
|
|
1317
|
+
ctx.fnBytes = 0
|
|
1318
|
+
const outerNode = ctx.keyLoopNode
|
|
1319
|
+
ctx.keyLoopNode = keyLoop ? item : null
|
|
1320
|
+
try { genIsolated(ctx, () => genCode(item, 'd', bodyLines, ctx)) } finally { ctx.plainReuse = outerReuse; ctx.fnBytes = outerBytes; ctx.keyLoopNode = outerNode }
|
|
1321
|
+
if (ctx.deferredChecks) for (const dc of ctx.deferredChecks) bodyLines.push(dc)
|
|
1322
|
+
ctx.deferredChecks = outerDeferred
|
|
1323
|
+
if (bodyLines.length === 0 || bodyLines.some((l) => /\b_validate\b/.test(l))) return null
|
|
1324
|
+
const fnName = '_it' + (ctx.itemFns = (ctx.itemFns || 0) + 1)
|
|
1325
|
+
hoist(ctx, `function ${fnName}(d){${bodyLines.join('\n ')}\n return true}`)
|
|
1326
|
+
return fnName
|
|
1327
|
+
}
|
|
1239
1328
|
function countInline (lines, from, ctx) {
|
|
1240
1329
|
let n = 0
|
|
1241
1330
|
for (let i = from; i < lines.length; i++) n += lines[i].length
|
|
@@ -2003,6 +2092,75 @@ function sameInstanceEdges(root, node) {
|
|
|
2003
2092
|
if (typeof node.$ref === 'string') { const t = localPointerTarget(root, node.$ref); if (t) out.push(t) }
|
|
2004
2093
|
return out
|
|
2005
2094
|
}
|
|
2095
|
+
// A definition that reaches a `$ref` to itself without moving to another value
|
|
2096
|
+
// and through no other reference (`X: { allOf: [{ $ref: X }, ...] }`, as
|
|
2097
|
+
// SchemaStore's cloudify has) re-enters itself on the same value. The
|
|
2098
|
+
// interpreted engine evaluates X once more there and answers the next
|
|
2099
|
+
// re-entry with true, its fixed point: X comes to f(f(true)), where f is X
|
|
2100
|
+
// with the reference as its argument. Reached through allOf and anyOf alone,
|
|
2101
|
+
// f is monotone, a conjunction with a disjunction, and then f(f(true)) is
|
|
2102
|
+
// f(true), so the `$ref` can be taken as true. The copy given to the
|
|
2103
|
+
// generators drops it, and the same-instance gate below lets the schema
|
|
2104
|
+
// through. Through oneOf, not, if, then or else the two differ, and those are
|
|
2105
|
+
// left as they are, as is a cycle through two or more references or a
|
|
2106
|
+
// definition some `$ref` points inside of: they still decline.
|
|
2107
|
+
const MONOTONE_LISTS = ['allOf', 'anyOf']
|
|
2108
|
+
const _selfLoopCache = new WeakMap()
|
|
2109
|
+
function breakSelfLoops(root) {
|
|
2110
|
+
if (root === null || typeof root !== 'object') return root
|
|
2111
|
+
const hit = _selfLoopCache.get(root)
|
|
2112
|
+
if (hit !== undefined) return hit
|
|
2113
|
+
let out = root
|
|
2114
|
+
const key = root.$defs && typeof root.$defs === 'object' ? '$defs' : root.definitions && typeof root.definitions === 'object' ? 'definitions' : null
|
|
2115
|
+
// The document's text is read only once a self-reference is found: almost
|
|
2116
|
+
// no schema has one, and serializing every schema with definitions on every
|
|
2117
|
+
// compile cost a large one a third of a millisecond for nothing.
|
|
2118
|
+
let text = null
|
|
2119
|
+
const doc = () => (text === null ? (text = JSON.stringify(root)) : text)
|
|
2120
|
+
if (key !== null) {
|
|
2121
|
+
const defs = root[key]
|
|
2122
|
+
let copies = null
|
|
2123
|
+
for (const name of Object.keys(defs)) {
|
|
2124
|
+
const def = defs[name]
|
|
2125
|
+
if (def === null || typeof def !== 'object' || Array.isArray(def)) continue
|
|
2126
|
+
// Nodes of X reached on the same value without a reference, and those
|
|
2127
|
+
// among them whose `$ref` names X.
|
|
2128
|
+
const back = new Set()
|
|
2129
|
+
const seen = new Set()
|
|
2130
|
+
const walk = (n) => {
|
|
2131
|
+
if (n === null || typeof n !== 'object' || seen.has(n)) return
|
|
2132
|
+
seen.add(n)
|
|
2133
|
+
if (typeof n.$ref === 'string' && localPointerTarget(root, n.$ref) === def) back.add(n)
|
|
2134
|
+
for (const k of MONOTONE_LISTS) if (Array.isArray(n[k])) for (const x of n[k]) walk(x)
|
|
2135
|
+
}
|
|
2136
|
+
walk(def)
|
|
2137
|
+
if (back.size === 0) continue
|
|
2138
|
+
if (doc().includes('"unevaluated')) { copies = null; break }
|
|
2139
|
+
// A reference into X's inside could enter one of those nodes some other way.
|
|
2140
|
+
const enc = name.replace(/~/g, '~0').replace(/\//g, '~1')
|
|
2141
|
+
const inside = ['#/' + key + '/' + enc + '/', '#/' + key + '/' + encodeURIComponent(enc) + '/']
|
|
2142
|
+
if (inside.some((p) => doc().includes('"$ref":"' + p))) continue
|
|
2143
|
+
const copy = (n, inNames) => {
|
|
2144
|
+
if (n === null || typeof n !== 'object') return n
|
|
2145
|
+
if (Array.isArray(n)) return n.map((x) => copy(x, false))
|
|
2146
|
+
const o = {}
|
|
2147
|
+
for (const k of Object.keys(n)) {
|
|
2148
|
+
if (k === '$ref' && back.has(n)) continue
|
|
2149
|
+
const v = !inNames && SKIP_VALUE_KEYS.has(k) ? n[k] : copy(n[k], !inNames && NAME_MAPS.has(k))
|
|
2150
|
+
if (k === '__proto__') Object.defineProperty(o, k, { value: v, writable: true, enumerable: true, configurable: true })
|
|
2151
|
+
else o[k] = v
|
|
2152
|
+
}
|
|
2153
|
+
return o
|
|
2154
|
+
}
|
|
2155
|
+
if (copies === null) copies = { ...defs }
|
|
2156
|
+
copies[name] = copy(def)
|
|
2157
|
+
}
|
|
2158
|
+
if (copies !== null) out = { ...root, [key]: copies }
|
|
2159
|
+
}
|
|
2160
|
+
_selfLoopCache.set(root, out)
|
|
2161
|
+
return out
|
|
2162
|
+
}
|
|
2163
|
+
|
|
2006
2164
|
const _sameInstanceCycleCache = new WeakMap()
|
|
2007
2165
|
function hasSameInstanceRefCycle(root) {
|
|
2008
2166
|
if (root === null || typeof root !== 'object') return false
|
|
@@ -2344,6 +2502,13 @@ function normalizeRefs(root, schemaMap) {
|
|
|
2344
2502
|
return out
|
|
2345
2503
|
}
|
|
2346
2504
|
const SKIP_VALUE_KEYS = new Set(['enum', 'const', 'default', 'examples'])
|
|
2505
|
+
// The keywords whose value is a map from names to schemas. Inside one, a key
|
|
2506
|
+
// is a name, so `default` or `examples` there is a property with a schema,
|
|
2507
|
+
// not a value: the walks below skip SKIP_VALUE_KEYS only in a schema node.
|
|
2508
|
+
// OpenAPI 3.1's own schema has a property named `examples` whose schema is a
|
|
2509
|
+
// `$ref`, and the dynamic-scope copy kept it unrenamed, so the whole schema
|
|
2510
|
+
// fell to the interpreter.
|
|
2511
|
+
const NAME_MAPS = new Set(['properties', 'patternProperties', '$defs', 'definitions', 'dependentSchemas'])
|
|
2347
2512
|
// A schema whose every reference is `#` or a plain `#/$defs/name` (or
|
|
2348
2513
|
// `definitions`), with no `$id` or `$anchor` below the root to move the base
|
|
2349
2514
|
// they resolve against, needs nothing from the resolver: each reference
|
|
@@ -2429,6 +2594,9 @@ function expandDynamicScopes(root, schemaMap) {
|
|
|
2429
2594
|
}
|
|
2430
2595
|
const copies = new Map() // node (or a boolean sentinel) -> Map(signature -> entry)
|
|
2431
2596
|
const order = []
|
|
2597
|
+
// Values kept as they are (an enum's list, a default): not copies, so the
|
|
2598
|
+
// naming pass below does not walk them.
|
|
2599
|
+
const keptRaw = new WeakSet()
|
|
2432
2600
|
let fail = false
|
|
2433
2601
|
// The entry for a node in a scope: its copy, made on the first ask, and
|
|
2434
2602
|
// later its name. A `$ref` slot holds the entry until names are given.
|
|
@@ -2471,7 +2639,10 @@ function expandDynamicScopes(root, schemaMap) {
|
|
|
2471
2639
|
continue
|
|
2472
2640
|
}
|
|
2473
2641
|
if (isSchema && DYN_DROP.has(k) && !(k === '$id' && node === root)) continue
|
|
2474
|
-
|
|
2642
|
+
if (isSchema && SKIP_VALUE_KEYS.has(k)) {
|
|
2643
|
+
if (v !== null && typeof v === 'object') keptRaw.add(v)
|
|
2644
|
+
setOwn(out, k, v)
|
|
2645
|
+
} else setOwn(out, k, copyValue(v, base, chain))
|
|
2475
2646
|
}
|
|
2476
2647
|
return e
|
|
2477
2648
|
}
|
|
@@ -2506,7 +2677,7 @@ function expandDynamicScopes(root, schemaMap) {
|
|
|
2506
2677
|
o.$ref = v.name === '#' ? '#' : '#/' + defsKey + '/' + v.name
|
|
2507
2678
|
continue
|
|
2508
2679
|
}
|
|
2509
|
-
if (!
|
|
2680
|
+
if (!(v !== null && typeof v === 'object' && keptRaw.has(v))) fix(v)
|
|
2510
2681
|
}
|
|
2511
2682
|
}
|
|
2512
2683
|
fix(rootCopy)
|
|
@@ -2540,10 +2711,10 @@ function normalizeRefsWalk(root, schemaMap) {
|
|
|
2540
2711
|
const visited = new Set()
|
|
2541
2712
|
let fail = false
|
|
2542
2713
|
let dynamic = false
|
|
2543
|
-
const visit = (node) => {
|
|
2714
|
+
const visit = (node, inNames) => {
|
|
2544
2715
|
if (fail || node === null || typeof node !== 'object' || visited.has(node)) return
|
|
2545
2716
|
visited.add(node)
|
|
2546
|
-
if (Array.isArray(node)) { for (const n of node) visit(n); return }
|
|
2717
|
+
if (Array.isArray(node)) { for (const n of node) visit(n, false); return }
|
|
2547
2718
|
if (node !== root && typeof node.$schema === 'string' && rootDialect !== null && node.$schema.replace(/#$/, '') !== rootDialect) { fail = true; return }
|
|
2548
2719
|
// A dynamic reference reached through another document is unrolled by
|
|
2549
2720
|
// scope the same way one in the root is (expandDynamicScopes, below).
|
|
@@ -2560,13 +2731,13 @@ function normalizeRefsWalk(root, schemaMap) {
|
|
|
2560
2731
|
// The root, however it is spelled, is `#`: a copy of it under its own
|
|
2561
2732
|
// definitions would contain itself.
|
|
2562
2733
|
if (t === root) { /* named '#' below */ }
|
|
2563
|
-
else if (typeof t === 'object' && !names.has(t)) { names.set(t, null); visit(t) }
|
|
2734
|
+
else if (typeof t === 'object' && !names.has(t)) { names.set(t, null); visit(t, false) }
|
|
2564
2735
|
if (typeof t === 'boolean' && !names.has(t)) names.set(t, null)
|
|
2565
2736
|
}
|
|
2566
2737
|
}
|
|
2567
|
-
for (const k of Object.keys(node)) if (!SKIP_VALUE_KEYS.has(k)) visit(node[k])
|
|
2738
|
+
for (const k of Object.keys(node)) if (inNames || !SKIP_VALUE_KEYS.has(k)) visit(node[k], !inNames && NAME_MAPS.has(k))
|
|
2568
2739
|
}
|
|
2569
|
-
visit(root)
|
|
2740
|
+
visit(root, false)
|
|
2570
2741
|
if (fail) return null
|
|
2571
2742
|
if (dynamic) return expandDynamicScopes(root, schemaMap)
|
|
2572
2743
|
if (refTarget.size === 0) return root
|
|
@@ -2580,13 +2751,13 @@ function normalizeRefsWalk(root, schemaMap) {
|
|
|
2580
2751
|
}
|
|
2581
2752
|
// The copy: references renamed, nested `$id`s dropped.
|
|
2582
2753
|
const memo = new Map()
|
|
2583
|
-
const walk = (node) => {
|
|
2754
|
+
const walk = (node, inNames) => {
|
|
2584
2755
|
if (node === null || typeof node !== 'object') return node
|
|
2585
2756
|
if (memo.has(node)) return memo.get(node)
|
|
2586
2757
|
if (Array.isArray(node)) {
|
|
2587
2758
|
const out = []
|
|
2588
2759
|
memo.set(node, out)
|
|
2589
|
-
for (const x of node) out.push(walk(x))
|
|
2760
|
+
for (const x of node) out.push(walk(x, false))
|
|
2590
2761
|
return out
|
|
2591
2762
|
}
|
|
2592
2763
|
const out = {}
|
|
@@ -2598,13 +2769,13 @@ function normalizeRefsWalk(root, schemaMap) {
|
|
|
2598
2769
|
out.$ref = t === root ? '#' : '#/' + defsKey + '/' + names.get(t)
|
|
2599
2770
|
continue
|
|
2600
2771
|
}
|
|
2601
|
-
setOwn(out, k, SKIP_VALUE_KEYS.has(k) ? node[k] : walk(node[k]))
|
|
2772
|
+
setOwn(out, k, !inNames && SKIP_VALUE_KEYS.has(k) ? node[k] : walk(node[k], !inNames && NAME_MAPS.has(k)))
|
|
2602
2773
|
}
|
|
2603
2774
|
return out
|
|
2604
2775
|
}
|
|
2605
|
-
const out = walk(root)
|
|
2776
|
+
const out = walk(root, false)
|
|
2606
2777
|
const defs = { ...(out[defsKey] && typeof out[defsKey] === 'object' ? out[defsKey] : {}) }
|
|
2607
|
-
for (const [t, name] of names) defs[name] = typeof t === 'boolean' ? t : walk(t)
|
|
2778
|
+
for (const [t, name] of names) defs[name] = typeof t === 'boolean' ? t : walk(t, false)
|
|
2608
2779
|
out[defsKey] = defs
|
|
2609
2780
|
return out
|
|
2610
2781
|
}
|
|
@@ -2667,7 +2838,14 @@ function sourceSize (lines, ctx) {
|
|
|
2667
2838
|
|
|
2668
2839
|
function compileToJSCodegen(schema, schemaMap, userFormats, opts) {
|
|
2669
2840
|
const inputSchema = schema
|
|
2841
|
+
// The verdict alone takes a self-reference on the same value as true (see
|
|
2842
|
+
// breakSelfLoops): the interpreted engine reports the errors of the second
|
|
2843
|
+
// visit as well, so the error and combined generators keep declining such a
|
|
2844
|
+
// schema and its errors come from that engine.
|
|
2670
2845
|
schema = prepareForCodegen(schema, schemaMap)
|
|
2846
|
+
// Only a schema with definitions can hold a self-reference; the others do
|
|
2847
|
+
// not pay for compiling the function on their first schema.
|
|
2848
|
+
if (schema !== null && typeof schema === 'object' && (schema.$defs || schema.definitions)) schema = breakSelfLoops(schema)
|
|
2671
2849
|
if (typeof schema === 'boolean') return schema ? () => true : () => false
|
|
2672
2850
|
if (typeof schema !== 'object' || schema === null) return null
|
|
2673
2851
|
// removeAdditional: a verdict function that deletes the keys the remover
|
|
@@ -2805,13 +2983,21 @@ function compileToJSCodegen(schema, schemaMap, userFormats, opts) {
|
|
|
2805
2983
|
// validateJSON or get extended, and rewriting and compiling the source twice
|
|
2806
2984
|
// more on every schema was about a third of what a first call cost.
|
|
2807
2985
|
let body, hybridThunk, tailThunk, resultThunk
|
|
2986
|
+
// The live verdict of a guarded schema declares its functions once, beside
|
|
2987
|
+
// the preamble, and returns the entry: written in the body they were made
|
|
2988
|
+
// again on every call, three closures per document (jsonld's recursive root
|
|
2989
|
+
// spent a third of its 46 ns on them). The source the emitters read, `body`
|
|
2990
|
+
// and the forms below, is unchanged.
|
|
2991
|
+
let liveTail = null, liveDecl = null
|
|
2808
2992
|
if (ctx.usesRecursion) {
|
|
2809
2993
|
// Self-recursive: a named body reached through the cycle guard, so data
|
|
2810
2994
|
// that points back at itself settles instead of exhausting the stack.
|
|
2811
|
-
const decl = emitRootGuard('_validate', 'd', 'true') +
|
|
2995
|
+
const decl = (ctx.rootFwd ? `${ctx.rootFwd}_f=_validate;` : '') + emitRootGuard('_validate', 'd', 'true') +
|
|
2812
2996
|
`function _validate_b(d){\n ${checkStr}\n return true\n }\n `
|
|
2813
2997
|
const run = emitGuardedRun('return _validate(d)', '', defSets)
|
|
2814
2998
|
body = `${decl}function _run(d){\n ${run}\n }\n return _run(d)`
|
|
2999
|
+
liveDecl = `${decl}function _run(d){\n ${run}\n }\n `
|
|
3000
|
+
liveTail = `${liveDecl}return _run`
|
|
2815
3001
|
// Hybrid: keep the boolean answer, wrap only the outer call
|
|
2816
3002
|
hybridThunk = () => `${decl}function _run(d){\n ${run}\n }\n return _run(d)?R:E(d)`
|
|
2817
3003
|
resultThunk = () => `${decl}function _run(d){\n ${run}\n }\n return _run(d)?${RESULT_TRUE}:${RESULT_FALSE}`
|
|
@@ -2821,6 +3007,8 @@ function compileToJSCodegen(schema, schemaMap, userFormats, opts) {
|
|
|
2821
3007
|
// re-entered with the same value; the guarded functions need their state.
|
|
2822
3008
|
const run = emitGuardedRun(`return _body(d)`, '', defSets)
|
|
2823
3009
|
body = `function _body(d){\n ${checkStr}\n return true\n }\n function _run(d){\n ${run}\n }\n return _run(d)`
|
|
3010
|
+
liveDecl = `function _body(d){\n ${checkStr}\n return true\n }\n function _run(d){\n ${run}\n }\n `
|
|
3011
|
+
liveTail = `${liveDecl}return _run`
|
|
2824
3012
|
hybridThunk = () => `function _body(d){\n ${checkStr}\n return true\n }\n function _run(d){\n ${run}\n }\n return _run(d)?R:E(d)`
|
|
2825
3013
|
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}`
|
|
2826
3014
|
tailThunk = () => `function _body(d){\n ${checkStr}\n return true\n }\n function _run(d){\n ${run}\n }\n return _run(d)&&__ataTail(d)`
|
|
@@ -2839,17 +3027,21 @@ function compileToJSCodegen(schema, schemaMap, userFormats, opts) {
|
|
|
2839
3027
|
const guardStr = needsGuard ? emitGuardState() : ''
|
|
2840
3028
|
const preambleStr = guardStr +
|
|
2841
3029
|
(ctx.preamble && ctx.preamble.length ? ctx.preamble.join('\n ') + '\n ' : '')
|
|
3030
|
+
// The tag goes in front of what is compiled here, never into the sources
|
|
3031
|
+
// the emitters read (_preambleSource, _source).
|
|
3032
|
+
const tag = seqTag()
|
|
2842
3033
|
|
|
2843
3034
|
try {
|
|
2844
3035
|
let boolFn
|
|
2845
|
-
if (
|
|
2846
|
-
const factory = compileFunction(...closureNames, `${preambleStr}
|
|
3036
|
+
if (liveTail !== null) {
|
|
3037
|
+
const factory = compileFunction(...closureNames, `${tag}${preambleStr}${liveTail}`)
|
|
3038
|
+
boolFn = factory(...closureValues)
|
|
3039
|
+
} else if (closureNames.length > 0) {
|
|
3040
|
+
const factory = compileFunction(...closureNames, `${tag}${preambleStr}return function(d){${body}}`)
|
|
2847
3041
|
boolFn = factory(...closureValues)
|
|
2848
|
-
} else if (preambleStr) {
|
|
2849
|
-
const factory = compileFunction(`${preambleStr}return function(d){${body}}`)
|
|
2850
|
-
boolFn = factory()
|
|
2851
3042
|
} else {
|
|
2852
|
-
|
|
3043
|
+
const factory = compileFunction(`${tag}${preambleStr}return function(d){${body}}`)
|
|
3044
|
+
boolFn = factory()
|
|
2853
3045
|
}
|
|
2854
3046
|
|
|
2855
3047
|
// Light by default: the pieces the tiering and standalone paths need
|
|
@@ -2869,7 +3061,7 @@ function compileToJSCodegen(schema, schemaMap, userFormats, opts) {
|
|
|
2869
3061
|
attachLazyHeavy(boolFn, inputSchema, schemaMap, userFormats, opts, !!ctx.usesSafeRe)
|
|
2870
3062
|
return boolFn
|
|
2871
3063
|
}
|
|
2872
|
-
attachHeavy(boolFn, { ctx, closureNames, closureValues, preambleStr, guardStr, hybridBody, resultThunk, tailThunk, body })
|
|
3064
|
+
attachHeavy(boolFn, { ctx, closureNames, closureValues, preambleStr, guardStr, hybridBody, resultThunk, tailThunk, body, liveDecl })
|
|
2873
3065
|
return boolFn
|
|
2874
3066
|
} catch {
|
|
2875
3067
|
return null
|
|
@@ -2891,7 +3083,13 @@ function attachLazyHeavy (boolFn, schema, schemaMap, userFormats, opts, usesSafe
|
|
|
2891
3083
|
}
|
|
2892
3084
|
// The factories and the source, as the full compile attaches them.
|
|
2893
3085
|
function attachHeavy (boolFn, P) {
|
|
2894
|
-
const { ctx, closureNames, closureValues, preambleStr, guardStr, hybridBody, resultThunk, tailThunk, body } = P
|
|
3086
|
+
const { ctx, closureNames, closureValues, preambleStr, guardStr, hybridBody, resultThunk, tailThunk, body, liveDecl } = P
|
|
3087
|
+
// These factories are compiled later than the verdict, so they take a tag
|
|
3088
|
+
// of their own (seqTag).
|
|
3089
|
+
const tag = seqTag()
|
|
3090
|
+
// A guarded schema's functions are declared once in each factory, as in the
|
|
3091
|
+
// live verdict (see compileToJSCodegen), and each form only calls `_run`.
|
|
3092
|
+
const live = (ret, thunk) => (liveDecl ? `${tag}${preambleStr}${liveDecl}return function(d){${ret}}` : `${tag}${preambleStr}return function(d){${thunk()}}`)
|
|
2895
3093
|
// The hybrid: same body, return R instead of true, return E(d) instead of
|
|
2896
3094
|
// false. Compiled on first request; null when that compile is refused, and
|
|
2897
3095
|
// the caller then assembles from the error functions instead.
|
|
@@ -2899,7 +3097,7 @@ function attachHeavy (boolFn, P) {
|
|
|
2899
3097
|
boolFn._hybridFactory = (R, E) => {
|
|
2900
3098
|
if (hybridFactory === undefined) {
|
|
2901
3099
|
try {
|
|
2902
|
-
hybridFactory = compileFunction(...closureNames, 'R', 'E',
|
|
3100
|
+
hybridFactory = compileFunction(...closureNames, 'R', 'E', live('return _run(d)?R:E(d)', hybridBody))
|
|
2903
3101
|
} catch {
|
|
2904
3102
|
hybridFactory = null
|
|
2905
3103
|
}
|
|
@@ -2918,7 +3116,7 @@ function attachHeavy (boolFn, P) {
|
|
|
2918
3116
|
boolFn._resultFactory = (LR, B, W, EE) => {
|
|
2919
3117
|
if (resultFactory === undefined) {
|
|
2920
3118
|
try {
|
|
2921
|
-
resultFactory = compileFunction(...closureNames, '__ataLR', '__ataB', '__ataW', '__ataEE',
|
|
3119
|
+
resultFactory = compileFunction(...closureNames, '__ataLR', '__ataB', '__ataW', '__ataEE', live(`return _run(d)?${RESULT_TRUE}:${RESULT_FALSE}`, resultThunk))
|
|
2922
3120
|
} catch {
|
|
2923
3121
|
resultFactory = null
|
|
2924
3122
|
}
|
|
@@ -2938,7 +3136,7 @@ function attachHeavy (boolFn, P) {
|
|
|
2938
3136
|
boolFn._withTail = (tail) => {
|
|
2939
3137
|
if (tailFactory === undefined) {
|
|
2940
3138
|
try {
|
|
2941
|
-
tailFactory = compileFunction(...closureNames, '__ataTail',
|
|
3139
|
+
tailFactory = compileFunction(...closureNames, '__ataTail', live('return _run(d)&&__ataTail(d)', tailThunk))
|
|
2942
3140
|
} catch {
|
|
2943
3141
|
tailFactory = null
|
|
2944
3142
|
}
|
|
@@ -3437,7 +3635,7 @@ function genCodeNode(schema, v, lines, ctx, knownType) {
|
|
|
3437
3635
|
// object: its type check fails on undefined. A name on Object.prototype
|
|
3438
3636
|
// is never absent that way, and it is checked explicitly.
|
|
3439
3637
|
if (!hasTypeCheck || PROTO_NAMES.has(key)) {
|
|
3440
|
-
reqChecks.push(
|
|
3638
|
+
reqChecks.push(`${v}[${JSON.stringify(key)}]===undefined||!${ownGuard(ctx, v, key)}`)
|
|
3441
3639
|
} else {
|
|
3442
3640
|
typed.push(key)
|
|
3443
3641
|
}
|
|
@@ -3458,7 +3656,7 @@ function genCodeNode(schema, v, lines, ctx, knownType) {
|
|
|
3458
3656
|
lines.push(`if(!${pk || protoIs(ctx, v)}&&!_hall(${v},${JSON.stringify(typed)}))return false`)
|
|
3459
3657
|
}
|
|
3460
3658
|
} else if (schema.required && schema.required.length > 0) {
|
|
3461
|
-
const checks = schema.required.map(key =>
|
|
3659
|
+
const checks = schema.required.map(key => `${v}[${JSON.stringify(key)}]===undefined||!${ownGuard(ctx, v, key)}`)
|
|
3462
3660
|
if (isObj) {
|
|
3463
3661
|
lines.push(`if(${checks.join('||')})return false`)
|
|
3464
3662
|
} else {
|
|
@@ -3548,7 +3746,7 @@ function genCodeNode(schema, v, lines, ctx, knownType) {
|
|
|
3548
3746
|
|
|
3549
3747
|
if (schema.pattern) {
|
|
3550
3748
|
// Try inline charCode compilation for simple patterns (avoids RegExp engine)
|
|
3551
|
-
const inlineCheck = compilePatternInline(schema.pattern, v)
|
|
3749
|
+
const inlineCheck = classRunCheck(ctx, schema.pattern, v) || compilePatternInline(schema.pattern, v)
|
|
3552
3750
|
if (inlineCheck) {
|
|
3553
3751
|
lines.push(isStr ? `if(!(${inlineCheck}))return false` : `if(typeof ${v}==='string'&&!(${inlineCheck}))return false`)
|
|
3554
3752
|
} else {
|
|
@@ -3659,13 +3857,32 @@ function genCodeNode(schema, v, lines, ctx, knownType) {
|
|
|
3659
3857
|
|
|
3660
3858
|
// properties — use hoisted vars for required props, hoist optional to locals too
|
|
3661
3859
|
if (schema.properties) {
|
|
3860
|
+
// Without a known object type, one test that the value is an object
|
|
3861
|
+
// covers every property below, instead of one test per property: a
|
|
3862
|
+
// definition with many optional properties stays small enough for V8 to
|
|
3863
|
+
// optimize (tslint's rules, 200 of them, did not).
|
|
3864
|
+
// Not an array: `properties` applies to objects only, and an array's own
|
|
3865
|
+
// `length` was checked against a property of that name, rejecting a list
|
|
3866
|
+
// that every other engine accepts. Only `length` and index names can be an
|
|
3867
|
+
// array's own key (an inherited one fails the own-key test), so the array
|
|
3868
|
+
// test is written only for those: on every other node it cost a reject
|
|
3869
|
+
// path 18% in its first calls (github-issue-forms).
|
|
3870
|
+
const arrayOwnable = Object.keys(schema.properties).some((k) => k === 'length' || /^(?:0|[1-9][0-9]*)$/.test(k))
|
|
3871
|
+
if (!isObj) lines.push(`if(typeof ${v}==='object'&&${v}!==null${arrayOwnable ? `&&!Array.isArray(${v})` : ''}){`)
|
|
3872
|
+
// The key-loop form (see emitItemFn): the optional properties are checked
|
|
3873
|
+
// from the object's own keys in one loop instead of one read each.
|
|
3874
|
+
const keyLoop = isObj && ctx.keyLoopNode === schema
|
|
3875
|
+
const keyCases = keyLoop ? [] : null
|
|
3662
3876
|
for (const [key, prop] of Object.entries(schema.properties)) {
|
|
3877
|
+
if (keyLoop && !requiredSet.has(key)) { keyCases.push([key, prop]); continue }
|
|
3663
3878
|
const from = lines.length
|
|
3664
3879
|
// A large subschema as a function of its own (emitPropFn); the call
|
|
3665
3880
|
// takes the place of the inline code in each of the three shapes below.
|
|
3666
3881
|
const fn = propFnCandidate(prop, ctx) ? emitPropFn(prop, ctx) : null
|
|
3667
3882
|
if (requiredSet.has(key) && isObj) {
|
|
3668
|
-
// Required + type:object — hoist to local to reduce repeated property lookups
|
|
3883
|
+
// Required + type:object — hoist to local to reduce repeated property lookups.
|
|
3884
|
+
// A key holding undefined is missing, as in the default validator: a
|
|
3885
|
+
// typed key's check fails on undefined, the others are tested above.
|
|
3669
3886
|
const access = hoisted[key] || `${v}[${JSON.stringify(key)}]`
|
|
3670
3887
|
const combined = fn ? null : tryGenCombined(prop, access, ctx)
|
|
3671
3888
|
if (fn) {
|
|
@@ -3681,7 +3898,21 @@ function genCodeNode(schema, v, lines, ctx, knownType) {
|
|
|
3681
3898
|
} else {
|
|
3682
3899
|
genCode(prop, access, lines, ctx)
|
|
3683
3900
|
}
|
|
3684
|
-
} else if (isObj) {
|
|
3901
|
+
} else if (!isObj) {
|
|
3902
|
+
// A value not known to be an object (tested once above): the key test
|
|
3903
|
+
// first, as before, then the value, read once. An absent key costs
|
|
3904
|
+
// what it did; a key holding undefined is skipped. A required key is
|
|
3905
|
+
// there on any object that got this far, so its value is read first
|
|
3906
|
+
// and only then asked whether it is the object's own: one test fewer,
|
|
3907
|
+
// and the read an absent key would make expensive does not happen.
|
|
3908
|
+
const oi = ctx.varCounter++
|
|
3909
|
+
const local = `_o${oi}`
|
|
3910
|
+
if (requiredSet.has(key)) lines.push(`{const ${local}=${v}[${JSON.stringify(key)}];if(${local}!==undefined&&${ownGuard(ctx, v, key)}){`)
|
|
3911
|
+
else lines.push(`if(${ownKeyExpr(ctx, v, key)}){const ${local}=${v}[${JSON.stringify(key)}];if(${local}!==undefined){`)
|
|
3912
|
+
if (fn) lines.push(`if(!${fn}(${local}))return false`)
|
|
3913
|
+
else genCode(prop, local, lines, ctx)
|
|
3914
|
+
lines.push(`}}`)
|
|
3915
|
+
} else {
|
|
3685
3916
|
// Optional — hoist to local, check undefined
|
|
3686
3917
|
const oi = ctx.varCounter++
|
|
3687
3918
|
const local = `_o${oi}`
|
|
@@ -3695,14 +3926,28 @@ function genCodeNode(schema, v, lines, ctx, knownType) {
|
|
|
3695
3926
|
genCode(prop, local, lines, ctx)
|
|
3696
3927
|
}
|
|
3697
3928
|
lines.push(`}}`)
|
|
3698
|
-
} else {
|
|
3699
|
-
lines.push(`if(typeof ${v}==='object'&&${v}!==null&&${ownKeyExpr(ctx, v, key)}){`)
|
|
3700
|
-
if (fn) lines.push(`if(!${fn}(${v}[${JSON.stringify(key)}]))return false`)
|
|
3701
|
-
else genCode(prop, `${v}[${JSON.stringify(key)}]`, lines, ctx)
|
|
3702
|
-
lines.push(`}`)
|
|
3703
3929
|
}
|
|
3704
3930
|
countInline(lines, from, ctx)
|
|
3705
3931
|
}
|
|
3932
|
+
if (keyCases !== null && keyCases.length > 0) {
|
|
3933
|
+
const kk = `_kk${ctx.varCounter++}`
|
|
3934
|
+
const kv = `_kv${ctx.varCounter++}`
|
|
3935
|
+
// The switch is one line: callers join lines with `;`, and a `;`
|
|
3936
|
+
// before a `case` is a syntax error (the error generator's guard did
|
|
3937
|
+
// that, and the rich error program silently fell back to the plain one).
|
|
3938
|
+
let cases = ''
|
|
3939
|
+
for (const [key, prop] of keyCases) {
|
|
3940
|
+
const caseLines = []
|
|
3941
|
+
const fn = propFnCandidate(prop, ctx) ? emitPropFn(prop, ctx) : null
|
|
3942
|
+
const combined = fn ? null : tryGenCombined(prop, kv, ctx)
|
|
3943
|
+
if (fn) caseLines.push(`if(!${fn}(${kv}))return false`)
|
|
3944
|
+
else if (combined) caseLines.push(combined)
|
|
3945
|
+
else genCode(prop, kv, caseLines, ctx)
|
|
3946
|
+
cases += `case ${JSON.stringify(key)}:{${caseLines.join(';')};break}`
|
|
3947
|
+
}
|
|
3948
|
+
lines.push(`{${ownHead(ctx, v, kk, 'const')}{const ${kv}=${v}[${kk}];if(${kv}!==undefined){switch(${kk}){${cases}}}}}`)
|
|
3949
|
+
}
|
|
3950
|
+
if (!isObj) lines.push(`}`)
|
|
3706
3951
|
}
|
|
3707
3952
|
|
|
3708
3953
|
// items — pass known type info to children
|
|
@@ -3712,11 +3957,26 @@ function genCodeNode(schema, v, lines, ctx, knownType) {
|
|
|
3712
3957
|
ctx.varCounter++
|
|
3713
3958
|
// items applies after the prefixItems positions (Draft 2020-12).
|
|
3714
3959
|
const start = Array.isArray(schema.prefixItems) ? schema.prefixItems.length : 0
|
|
3715
|
-
|
|
3716
|
-
|
|
3717
|
-
:
|
|
3718
|
-
|
|
3719
|
-
|
|
3960
|
+
const fk = keyLoopEligible(schema.items) ? emitItemFn(schema.items, ctx, true) : null
|
|
3961
|
+
if (fk !== null) {
|
|
3962
|
+
// Records with many optional properties: when three of them (first,
|
|
3963
|
+
// middle, last) list their keys alike, they likely share one shape and
|
|
3964
|
+
// reading each declared property, written in the loop as always, is
|
|
3965
|
+
// cheapest; when they do not, each record walks its own keys instead
|
|
3966
|
+
// (fk), which costs the same whatever their shapes. Decided per call,
|
|
3967
|
+
// from three items.
|
|
3968
|
+
hoistOnce(ctx, 'sameKeysHelper', SAME_KEYS_HELPER)
|
|
3969
|
+
const n = `_n${ctx.varCounter++}`
|
|
3970
|
+
lines.push(`${isArr ? '' : `if(Array.isArray(${v}))`}{const ${n}=${v}.length;if(${n}-${start}>2&&(!_sameKeys(${v}[${start}],${v}[(${n}+${start})>>1])||!_sameKeys(${v}[${start}],${v}[${n}-1]))){for(let ${idx}=${start};${idx}<${n};${idx}++)if(!${fk}(${v}[${idx}]))return false}else{for(let ${idx}=${start};${idx}<${n};${idx}++){const ${elem}=${v}[${idx}]`)
|
|
3971
|
+
genCode(schema.items, elem, lines, ctx)
|
|
3972
|
+
lines.push(`}}}`)
|
|
3973
|
+
} else {
|
|
3974
|
+
lines.push(isArr
|
|
3975
|
+
? `for(let ${idx}=${start};${idx}<${v}.length;${idx}++){const ${elem}=${v}[${idx}]`
|
|
3976
|
+
: `if(Array.isArray(${v})){for(let ${idx}=${start};${idx}<${v}.length;${idx}++){const ${elem}=${v}[${idx}]`)
|
|
3977
|
+
genCode(schema.items, elem, lines, ctx)
|
|
3978
|
+
lines.push(isArr ? `}` : `}}`)
|
|
3979
|
+
}
|
|
3720
3980
|
}
|
|
3721
3981
|
|
|
3722
3982
|
// prefixItems
|
|
@@ -3751,7 +4011,7 @@ function genCodeNode(schema, v, lines, ctx, knownType) {
|
|
|
3751
4011
|
|
|
3752
4012
|
// oneOf — branch fns hoisted to factory scope when safe (no recursion/ref).
|
|
3753
4013
|
// Falls back to inline closures if any branch touches recursive validation.
|
|
3754
|
-
if (schema.oneOf) genOneOfNode(schema, v, applicatorLines, ctx,
|
|
4014
|
+
if (schema.oneOf) genOneOfNode(schema, v, applicatorLines, ctx, effectiveType)
|
|
3755
4015
|
|
|
3756
4016
|
// not
|
|
3757
4017
|
if (schema.not !== undefined) genNotNode(schema, v, lines, ctx, knownType)
|
|
@@ -3920,7 +4180,42 @@ function genContainsNode(schema, v, lines, ctx, knownType, isArr) {
|
|
|
3920
4180
|
}
|
|
3921
4181
|
}
|
|
3922
4182
|
|
|
4183
|
+
// A branch of anyOf or oneOf that cannot be a function in the preamble (it
|
|
4184
|
+
// calls `_validate`, the recursive entry, which is not in scope there) was a
|
|
4185
|
+
// closure in an array built on every call. Written in place instead, as a
|
|
4186
|
+
// labelled block whose `return false` becomes the break out of it: jsonld's
|
|
4187
|
+
// root anyOf, one branch of which recurses, took 61 ns a document against 30
|
|
4188
|
+
// for the default validator. Only a body with no function of its own, whose
|
|
4189
|
+
// returns are all `return false`; the parameter becomes a local of the block,
|
|
4190
|
+
// and `\b` leaves longer names that start the same alone.
|
|
4191
|
+
function inlineBranchBody (body, param, label, local) {
|
|
4192
|
+
if (/\bfunction\b|=>|\bthis\b|\barguments\b/.test(body)) return null
|
|
4193
|
+
let core
|
|
4194
|
+
if (body === 'return true') core = ''
|
|
4195
|
+
else if (body.endsWith(';return true')) core = body.slice(0, -';return true'.length)
|
|
4196
|
+
else return null
|
|
4197
|
+
if (/\breturn\b(?! false\b)/.test(core)) return null
|
|
4198
|
+
const re = new RegExp('\\b' + param + '\\b', 'g')
|
|
4199
|
+
return core.replace(/\breturn false\b/g, 'break ' + label).replace(re, local)
|
|
4200
|
+
}
|
|
4201
|
+
|
|
4202
|
+
// A single-branch anyOf or oneOf holds exactly when its branch does, so the
|
|
4203
|
+
// branch is written in place. When the branch is only a $ref that an allOf
|
|
4204
|
+
// beside it also applies, it is already checked and nothing is written:
|
|
4205
|
+
// devinit's items are `{ allOf: [{ $ref: R }], anyOf: [{ $ref: R }] }`, and
|
|
4206
|
+
// each run object was checked twice. The verdict only; errors keep the
|
|
4207
|
+
// keyword's own shape.
|
|
4208
|
+
const refOnly = (b) => b !== null && typeof b === 'object' && typeof b.$ref === 'string' && Object.keys(b).every((k) => k === '$ref' || REF_NEUTRAL_SIBLINGS.has(k))
|
|
4209
|
+
function singleBranch(schema, branches, v, lines, ctx, knownType) {
|
|
4210
|
+
if (branches.length !== 1) return false
|
|
4211
|
+
const b = branches[0]
|
|
4212
|
+
if (refOnly(b) && Array.isArray(schema.allOf) && schema.allOf.some((x) => refOnly(x) && x.$ref === b.$ref)) return true
|
|
4213
|
+
genCode(b, v, lines, ctx, knownType)
|
|
4214
|
+
return true
|
|
4215
|
+
}
|
|
4216
|
+
|
|
3923
4217
|
function genAnyOfNode(schema, v, lines, ctx, knownType) {
|
|
4218
|
+
if (singleBranch(schema, schema.anyOf, v, lines, ctx, knownType)) return
|
|
3924
4219
|
const fi = ctx.varCounter++
|
|
3925
4220
|
const branchBodies = []
|
|
3926
4221
|
const reused = []
|
|
@@ -3956,6 +4251,13 @@ function genAnyOfNode(schema, v, lines, ctx, knownType) {
|
|
|
3956
4251
|
const checks = names.map(n => `${n}(${v})`).join('||')
|
|
3957
4252
|
lines.push(`if(!(${checks}))return false`)
|
|
3958
4253
|
} else {
|
|
4254
|
+
const inl = branchBodies.map((body, i) => inlineBranchBody(body, '_av', `_al${fi}_${i}`, `_ai${fi}_${i}`))
|
|
4255
|
+
if (inl.every((x) => x !== null)) {
|
|
4256
|
+
const m = `_am${fi}`
|
|
4257
|
+
const blocks = inl.map((core, i) => `_al${fi}_${i}:{const _ai${fi}_${i}=${v};${core};${m}=true}`)
|
|
4258
|
+
lines.push(`{let ${m}=false;${blocks.map((b, i) => (i === 0 ? b : `if(!${m}){${b}}`)).join('')}if(!${m})return false}`)
|
|
4259
|
+
return
|
|
4260
|
+
}
|
|
3959
4261
|
const fns = branchBodies.map(body => `function(_av){${body}}`)
|
|
3960
4262
|
lines.push(`{const _af${fi}=[${fns.join(',')}];let _am${fi}=false;for(let _ai=0;_ai<_af${fi}.length;_ai++){if(_af${fi}[_ai](${v})){_am${fi}=true;break}}if(!_am${fi})return false}`)
|
|
3961
4263
|
}
|
|
@@ -4007,6 +4309,7 @@ function keyedBranches (branches) {
|
|
|
4007
4309
|
}
|
|
4008
4310
|
|
|
4009
4311
|
function genOneOfNode(schema, v, lines, ctx, knownType) {
|
|
4312
|
+
if (singleBranch(schema, schema.oneOf, v, lines, ctx, knownType)) return
|
|
4010
4313
|
const fi = ctx.varCounter++
|
|
4011
4314
|
const branchBodies = []
|
|
4012
4315
|
const reused = []
|
|
@@ -4018,19 +4321,24 @@ function genOneOfNode(schema, v, lines, ctx, knownType) {
|
|
|
4018
4321
|
// 86919 branch functions and 197 MB of source, and took seconds to build.
|
|
4019
4322
|
// A hoisted branch takes its input as a parameter and reads only
|
|
4020
4323
|
// module-level names, so the same schema object gives the same function.
|
|
4021
|
-
|
|
4324
|
+
// A branch reached with an object already checked is written for one, a
|
|
4325
|
+
// different function, so it is reused only for the same known type.
|
|
4326
|
+
const kt = knownType === 'object' ? 'object' : undefined
|
|
4327
|
+
const mapName = kt ? 'hoistedBranchObj' : 'hoistedBranch'
|
|
4328
|
+
if (canHoist && !ctx[mapName]) ctx[mapName] = new Map()
|
|
4329
|
+
const branchMap = canHoist ? ctx[mapName] : null
|
|
4022
4330
|
for (let i = 0; i < schema.oneOf.length; i++) {
|
|
4023
4331
|
const sub = schema.oneOf[i]
|
|
4024
4332
|
const key = canHoist ? branchKey(sub, ctx) : undefined
|
|
4025
|
-
const known = key !== undefined ?
|
|
4333
|
+
const known = key !== undefined ? branchMap.get(key) : undefined
|
|
4026
4334
|
if (known !== undefined) {
|
|
4027
4335
|
reused[i] = known.name
|
|
4028
4336
|
branchBodies.push(known.body)
|
|
4029
4337
|
continue
|
|
4030
4338
|
}
|
|
4031
4339
|
const subLines = []
|
|
4032
|
-
if (canHoist) genIsolated(ctx, () => genCode(sub, '_ov', subLines, ctx))
|
|
4033
|
-
else genCode(sub, '_ov', subLines, ctx)
|
|
4340
|
+
if (canHoist) genIsolated(ctx, () => genCode(sub, '_ov', subLines, ctx, kt))
|
|
4341
|
+
else genCode(sub, '_ov', subLines, ctx, kt)
|
|
4034
4342
|
const body = subLines.length === 0 ? 'return true' : `${subLines.join(';')};return true`
|
|
4035
4343
|
// _validate is the recursive entry — hoisting branches above it breaks scope.
|
|
4036
4344
|
if (/\b_validate\b/.test(body)) canHoist = false
|
|
@@ -4042,7 +4350,7 @@ function genOneOfNode(schema, v, lines, ctx, knownType) {
|
|
|
4042
4350
|
const name = `_of${fi}_b${i}`
|
|
4043
4351
|
hoist(ctx, `function ${name}(_ov){${body}}`)
|
|
4044
4352
|
const key = branchKey(schema.oneOf[i], ctx)
|
|
4045
|
-
if (key !== undefined)
|
|
4353
|
+
if (key !== undefined) branchMap.set(key, { name, body })
|
|
4046
4354
|
return name
|
|
4047
4355
|
})
|
|
4048
4356
|
const calls = names.map(n => `if(${n}(${v})){_oc${fi}++;if(_oc${fi}>1)return false}`).join(';')
|
|
@@ -4059,12 +4367,77 @@ function genOneOfNode(schema, v, lines, ctx, knownType) {
|
|
|
4059
4367
|
}
|
|
4060
4368
|
lines.push(`{let _oc${fi}=0;${calls};if(_oc${fi}!==1)return false}`)
|
|
4061
4369
|
} else {
|
|
4370
|
+
const inl = branchBodies.map((body, i) => inlineBranchBody(body, '_ov', `_ol${fi}_${i}`, `_oj${fi}_${i}`))
|
|
4371
|
+
if (inl.every((x) => x !== null)) {
|
|
4372
|
+
const c = `_oc${fi}`
|
|
4373
|
+
const blocks = inl.map((core, i) => `_ol${fi}_${i}:{const _oj${fi}_${i}=${v};${core};if(++${c}>1)return false}`)
|
|
4374
|
+
lines.push(`{let ${c}=0;${blocks.join('')}if(${c}!==1)return false}`)
|
|
4375
|
+
return
|
|
4376
|
+
}
|
|
4062
4377
|
const fns = branchBodies.map(body => `function(_ov){${body}}`)
|
|
4063
4378
|
lines.push(`{const _of${fi}=[${fns.join(',')}];let _oc${fi}=0;for(let _oi=0;_oi<_of${fi}.length;_oi++){if(_of${fi}[_oi](${v}))_oc${fi}++;if(_oc${fi}>1)return false}if(_oc${fi}!==1)return false}`)
|
|
4064
4379
|
}
|
|
4065
4380
|
}
|
|
4066
4381
|
|
|
4382
|
+
// A subschema checked on the same value (`not`, `if`, `then`, `else`) as a
|
|
4383
|
+
// named function in the preamble, so the check allocates nothing: written in
|
|
4384
|
+
// the body, each was a closure created on every call, and SchemaStore's
|
|
4385
|
+
// cloudify, about 3000 if/then pairs, spent most of its time making them.
|
|
4386
|
+
// Generated over its own parameter, like a hoisted oneOf branch. Null where
|
|
4387
|
+
// it cannot be hoisted, and the caller writes the closure as before.
|
|
4388
|
+
// The function also takes whether the value is an ordinary object (see
|
|
4389
|
+
// withPlain), which the caller has usually worked out already: each of
|
|
4390
|
+
// minecraft-predicate's 25 if/then pairs read the object's prototype again.
|
|
4391
|
+
function hoistedCheck (ctx, sub, param, prefix) {
|
|
4392
|
+
if (!ctx.preamble) return null
|
|
4393
|
+
const bodyLines = []
|
|
4394
|
+
const outerDeferred = ctx.deferredChecks
|
|
4395
|
+
ctx.deferredChecks = null
|
|
4396
|
+
const outerReuse = ctx.plainReuse
|
|
4397
|
+
const flag = param + 'pk'
|
|
4398
|
+
ctx.plainReuse = param
|
|
4399
|
+
try {
|
|
4400
|
+
genIsolated(ctx, () => {
|
|
4401
|
+
ctx.plainOf = new Map([[param, flag]])
|
|
4402
|
+
genCode(sub, param, bodyLines, ctx)
|
|
4403
|
+
})
|
|
4404
|
+
} finally { ctx.plainReuse = outerReuse }
|
|
4405
|
+
if (ctx.deferredChecks) for (const dc of ctx.deferredChecks) bodyLines.push(dc)
|
|
4406
|
+
ctx.deferredChecks = outerDeferred
|
|
4407
|
+
const body = bodyLines.length === 0 ? 'return true' : `${bodyLines.join(';')};return true`
|
|
4408
|
+
// _validate is the recursive entry, not in scope above it.
|
|
4409
|
+
if (/\b_validate\b/.test(body)) return null
|
|
4410
|
+
const name = prefix + ctx.varCounter++
|
|
4411
|
+
// A body that never asks takes no flag, so the caller does not work it out.
|
|
4412
|
+
if (new RegExp('\\b' + flag + '\\b').test(body)) {
|
|
4413
|
+
hoist(ctx, `function ${name}(${param},${flag}){${body}}`)
|
|
4414
|
+
} else {
|
|
4415
|
+
if (!ctx.flaglessChecks) ctx.flaglessChecks = new Set()
|
|
4416
|
+
ctx.flaglessChecks.add(name)
|
|
4417
|
+
hoist(ctx, `function ${name}(${param}){${body}}`)
|
|
4418
|
+
}
|
|
4419
|
+
return name
|
|
4420
|
+
}
|
|
4421
|
+
// A call to a function from hoistedCheck, with the flag argument only where
|
|
4422
|
+
// the function takes one.
|
|
4423
|
+
function callHoisted (ctx, name, v, pk) {
|
|
4424
|
+
return ctx.flaglessChecks && ctx.flaglessChecks.has(name) ? `${name}(${v})` : `${name}(${v},${pk})`
|
|
4425
|
+
}
|
|
4426
|
+
// The second argument to a function from hoistedCheck: the caller's flag for
|
|
4427
|
+
// the value when it has one, else the test itself. Not inside the boolean
|
|
4428
|
+
// code the error and combined generators nest in their own functions, where a
|
|
4429
|
+
// flag recorded for the value may belong to a scope the call is not in.
|
|
4430
|
+
function plainArg (ctx, v) {
|
|
4431
|
+
const pk = ctx.nestedBoolean ? undefined : plainFlag(ctx, v)
|
|
4432
|
+
return pk || `(typeof ${v}==='object'&&${v}!==null&&${protoIs(ctx, v)})`
|
|
4433
|
+
}
|
|
4434
|
+
|
|
4067
4435
|
function genNotNode(schema, v, lines, ctx, knownType) {
|
|
4436
|
+
const hoisted = hoistedCheck(ctx, schema.not, '_nv', '_nh')
|
|
4437
|
+
if (hoisted !== null) {
|
|
4438
|
+
lines.push(`if(${callHoisted(ctx, hoisted, v, plainArg(ctx, v))})return false`)
|
|
4439
|
+
return
|
|
4440
|
+
}
|
|
4068
4441
|
const subLines = []
|
|
4069
4442
|
genCode(schema.not, '_nv', subLines, ctx)
|
|
4070
4443
|
if (subLines.length === 0) {
|
|
@@ -4076,6 +4449,29 @@ function genNotNode(schema, v, lines, ctx, knownType) {
|
|
|
4076
4449
|
}
|
|
4077
4450
|
|
|
4078
4451
|
function genIfNode(schema, v, lines, ctx, knownType) {
|
|
4452
|
+
if (ctx.preamble) {
|
|
4453
|
+
const ifName = hoistedCheck(ctx, schema.if, '_iv', '_ih')
|
|
4454
|
+
const thenName = ifName !== null && schema.then !== undefined ? hoistedCheck(ctx, schema.then, '_tv', '_th') : null
|
|
4455
|
+
const elseName = ifName !== null && schema.else !== undefined ? hoistedCheck(ctx, schema.else, '_ev', '_eh') : null
|
|
4456
|
+
if (ifName !== null && (schema.then === undefined || thenName !== null) && (schema.else === undefined || elseName !== null)) {
|
|
4457
|
+
if (thenName === null && elseName === null) return
|
|
4458
|
+
// The flag is worked out once: from the caller when it has one, else in
|
|
4459
|
+
// a local when more than one of the functions takes it.
|
|
4460
|
+
const takers = [ifName, thenName, elseName].filter((n) => n !== null && !(ctx.flaglessChecks && ctx.flaglessChecks.has(n))).length
|
|
4461
|
+
let pk = plainArg(ctx, v)
|
|
4462
|
+
let open = '', close = ''
|
|
4463
|
+
if (takers > 1 && !/^[\w$]+$/.test(pk)) {
|
|
4464
|
+
const local = `_ipk${ctx.varCounter++}`
|
|
4465
|
+
open = `{const ${local}=${pk};`
|
|
4466
|
+
close = '}'
|
|
4467
|
+
pk = local
|
|
4468
|
+
}
|
|
4469
|
+
const thenCheck = thenName !== null ? `if(!${callHoisted(ctx, thenName, v, pk)})return false` : ''
|
|
4470
|
+
const elseCheck = elseName !== null ? `if(!${callHoisted(ctx, elseName, v, pk)})return false` : ''
|
|
4471
|
+
lines.push(`${open}if(${callHoisted(ctx, ifName, v, pk)}){${thenCheck}}else{${elseCheck}}${close}`)
|
|
4472
|
+
return
|
|
4473
|
+
}
|
|
4474
|
+
}
|
|
4079
4475
|
const ifLines = []
|
|
4080
4476
|
genCode(schema.if, '_iv', ifLines, ctx)
|
|
4081
4477
|
const fi = ctx.varCounter++
|
|
@@ -4102,12 +4498,30 @@ function genIfNode(schema, v, lines, ctx, knownType) {
|
|
|
4102
4498
|
lines.push(`if(_if${fi}(${v})){if(_th${fi}&&!_th${fi}(${v}))return false}else{if(_el${fi}&&!_el${fi}(${v}))return false}}`)
|
|
4103
4499
|
}
|
|
4104
4500
|
|
|
4501
|
+
// The root as a definition function in the preamble reaches it: a function
|
|
4502
|
+
// declared there that calls whatever the entry stored in its slot, which the
|
|
4503
|
+
// entry's declarations fill in (see compileToJSCodegen). Named per compile,
|
|
4504
|
+
// and written with hoist, not hoistOnce, so a bundle never shares one slot
|
|
4505
|
+
// between two schemas. SchemaStore's jsone, whose definitions refer back to
|
|
4506
|
+
// the root, ran on the interpreter for want of it.
|
|
4507
|
+
function rootForwarder (ctx) {
|
|
4508
|
+
if (!ctx.rootFwd) {
|
|
4509
|
+
const name = `_root${ctx.varCounter++}`
|
|
4510
|
+
ctx.rootFwd = name
|
|
4511
|
+
hoist(ctx, `let ${name}_f=null;function ${name}(d){return ${name}_f(d)}`)
|
|
4512
|
+
}
|
|
4513
|
+
return ctx.rootFwd
|
|
4514
|
+
}
|
|
4515
|
+
|
|
4105
4516
|
function genRefNode(schema, v, lines, ctx, knownType, hasSiblings) {
|
|
4106
4517
|
// Self-reference "#" — recursive call to root validator
|
|
4107
4518
|
if (schema.$ref === '#') {
|
|
4108
4519
|
if (ctx.nestedBoolean) throw DECLINE
|
|
4109
4520
|
ctx.usesRecursion = true
|
|
4110
|
-
|
|
4521
|
+
// Inside a definition function, which sits in the preamble where the
|
|
4522
|
+
// entry is not in scope, the root is reached through a forwarder the
|
|
4523
|
+
// entry fills in (rootForwarder).
|
|
4524
|
+
lines.push(`if(!${ctx.inDefFn ? rootForwarder(ctx) : '_validate'}(${v}))return false`)
|
|
4111
4525
|
if (!hasSiblings) return true
|
|
4112
4526
|
}
|
|
4113
4527
|
// 1. Local ref
|
|
@@ -4134,14 +4548,14 @@ function genRefNode(schema, v, lines, ctx, knownType, hasSiblings) {
|
|
|
4134
4548
|
ctx.deferredChecks = null
|
|
4135
4549
|
const outerReuse = ctx.plainReuse
|
|
4136
4550
|
ctx.plainReuse = undefined
|
|
4137
|
-
|
|
4551
|
+
ctx.inDefFn = (ctx.inDefFn || 0) + 1
|
|
4552
|
+
try { genIsolated(ctx, () => genCode(ctx.rootDefs[defName], 'd', bodyLines, ctx)) } finally { ctx.plainReuse = outerReuse; ctx.inDefFn-- }
|
|
4138
4553
|
if (ctx.deferredChecks) for (const dc of ctx.deferredChecks) bodyLines.push(dc)
|
|
4139
4554
|
ctx.deferredChecks = outerDeferred
|
|
4140
4555
|
// The def is hoisted above the entry function, where the recursive
|
|
4141
|
-
// entry `_validate` is not in scope
|
|
4142
|
-
//
|
|
4143
|
-
//
|
|
4144
|
-
// oneOf and anyOf do not hoist such a branch.
|
|
4556
|
+
// entry `_validate` is not in scope: a `$ref: '#'` inside it goes
|
|
4557
|
+
// through the forwarder (rootForwarder). Anything else naming the
|
|
4558
|
+
// entry still declines, as oneOf and anyOf do not hoist such a branch.
|
|
4145
4559
|
if (bodyLines.some((l) => /\b_validate\b/.test(l))) throw DECLINE
|
|
4146
4560
|
// Entered through the cycle guard: see emitGuardState. In the fast
|
|
4147
4561
|
// pass the wrapper only counts depth; the set is used by the second,
|
|
@@ -4331,8 +4745,15 @@ function genPatternPropertiesNode(schema, v, lines, ctx, knownType, isObj) {
|
|
|
4331
4745
|
}
|
|
4332
4746
|
lines.push(`}}`)
|
|
4333
4747
|
} else {
|
|
4334
|
-
// No additionalProperties: validate matching keys + propertyNames
|
|
4748
|
+
// No additionalProperties: validate matching keys + propertyNames.
|
|
4749
|
+
// A pattern whose subschema checks nothing (`{}`, `true`), as OpenAPI's
|
|
4750
|
+
// `^x-` extensions are on nearly every object, needs no matching here:
|
|
4751
|
+
// the loop read every key's value and did nothing with it. With no such
|
|
4752
|
+
// check left and no propertyNames, there is no loop at all.
|
|
4335
4753
|
handledPropertyNames = !!pn
|
|
4754
|
+
const live = []
|
|
4755
|
+
for (let i = 0; i < ppEntries.length; i++) if (subChecks[i] !== '') live.push(i)
|
|
4756
|
+
if (!pn && live.length === 0) return handledPropertyNames
|
|
4336
4757
|
lines.push(`${guard}{${ownHead(ctx, v, kVar, 'const')}{`)
|
|
4337
4758
|
// propertyNames checks (merged)
|
|
4338
4759
|
if (pn) {
|
|
@@ -4357,7 +4778,7 @@ function genPatternPropertiesNode(schema, v, lines, ctx, knownType, isObj) {
|
|
|
4357
4778
|
lines.push(`if(!_es${ei}.has(${kVar}))return false`)
|
|
4358
4779
|
}
|
|
4359
4780
|
}
|
|
4360
|
-
for (
|
|
4781
|
+
for (const i of live) {
|
|
4361
4782
|
lines.push(`if(${matchers[i].check}){const _ppv${pi}=${v}[${kVar}];${subChecks[i]}}`)
|
|
4362
4783
|
}
|
|
4363
4784
|
lines.push(`}}`)
|
|
@@ -4918,8 +5339,19 @@ const FORMAT_CODEGEN = {
|
|
|
4918
5339
|
'json-pointer': (...a) => getFormats().jsonPointerSource(...a),
|
|
4919
5340
|
'relative-json-pointer': (...a) => getFormats().relativeJsonPointerSource(...a),
|
|
4920
5341
|
'uri-template': (...a) => getFormats().uriTemplateSource(...a),
|
|
4921
|
-
|
|
4922
|
-
|
|
5342
|
+
// The URI family shares one hoisted walk (uriHelper): `_uri` is the uri
|
|
5343
|
+
// check with its regex in front, `_urif(s, mode)` the other three with
|
|
5344
|
+
// theirs, `_uriw(s, mode)` the walk itself.
|
|
5345
|
+
iri: (v, isStr, ctx) => {
|
|
5346
|
+
if (!ctx) return getFormats().iriSource(v, isStr)
|
|
5347
|
+
hoistOnce(ctx, '_uriHoisted', uriHelper())
|
|
5348
|
+
return isStr ? `if(!_urif(${v},3))return false` : `if(typeof ${v}==='string'&&!_urif(${v},3))return false`
|
|
5349
|
+
},
|
|
5350
|
+
'iri-reference': (v, isStr, ctx) => {
|
|
5351
|
+
if (!ctx) return getFormats().iriReferenceSource(v, isStr)
|
|
5352
|
+
hoistOnce(ctx, '_uriHoisted', uriHelper())
|
|
5353
|
+
return isStr ? `if(!_urif(${v},2))return false` : `if(typeof ${v}==='string'&&!_urif(${v},2))return false`
|
|
5354
|
+
},
|
|
4923
5355
|
'idn-email': (...a) => getFormats().idnEmailSource(...a),
|
|
4924
5356
|
regex: (v, isStr) => {
|
|
4925
5357
|
const inner = `try{new RegExp(${v},'u')}catch(_er){return false}`
|
|
@@ -4935,9 +5367,11 @@ const FORMAT_CODEGEN = {
|
|
|
4935
5367
|
hoistOnce(ctx, '_uriHoisted', uriHelper())
|
|
4936
5368
|
return isStr ? `if(!_uri(${v}))return false` : `if(typeof ${v}==='string'&&!_uri(${v}))return false`
|
|
4937
5369
|
},
|
|
4938
|
-
'uri-reference': (v, isStr) =>
|
|
4939
|
-
|
|
4940
|
-
|
|
5370
|
+
'uri-reference': (v, isStr, ctx) => {
|
|
5371
|
+
if (!ctx) return getFormats().uriReferenceSource(v, isStr)
|
|
5372
|
+
hoistOnce(ctx, '_uriHoisted', uriHelper())
|
|
5373
|
+
return isStr ? `if(!_urif(${v},0))return false` : `if(typeof ${v}==='string'&&!_urif(${v},0))return false`
|
|
5374
|
+
},
|
|
4941
5375
|
ipv4: (v, isStr, ctx) => {
|
|
4942
5376
|
if (!ctx) return getFormats().ipv4Source(v, isStr)
|
|
4943
5377
|
// Hoisted: a regex literal in the body would build a new RegExp per call.
|
|
@@ -4955,6 +5389,38 @@ const FORMAT_CODEGEN = {
|
|
|
4955
5389
|
hostname: (...a) => getFormats().hostnameSource(...a),
|
|
4956
5390
|
}
|
|
4957
5391
|
|
|
5392
|
+
// `^[class]+$`, `^[class]{m,n}$` and `^[class]{n}$` for a class of ASCII
|
|
5393
|
+
// letters, digits, `_` and `-`: a function in the preamble that reads each
|
|
5394
|
+
// character once and looks it up in a 128-entry table. The inline form below
|
|
5395
|
+
// read it again for every range it compared and built an arrow function on
|
|
5396
|
+
// every call; component.json's `^[0-9a-z-_]+$` cost the default validator's
|
|
5397
|
+
// RegExp the same. Null without a preamble, or for any other shape.
|
|
5398
|
+
function classRunCheck (ctx, pattern, v) {
|
|
5399
|
+
if (!ctx || !ctx.preamble) return null
|
|
5400
|
+
let m = pattern.match(/^\^(\[[\w\-]+\])(\+|\{(\d+)\}|\{(\d+),(\d+)\})\$$/)
|
|
5401
|
+
if (!m) return null
|
|
5402
|
+
const cls = m[1]
|
|
5403
|
+
let min, max
|
|
5404
|
+
if (m[2] === '+') { min = 1; max = Infinity }
|
|
5405
|
+
else if (m[3] !== undefined) { min = max = +m[3]; if (max <= 16) return null }
|
|
5406
|
+
else { min = +m[4]; max = +m[5] }
|
|
5407
|
+
if (!(min <= max)) return null
|
|
5408
|
+
let re
|
|
5409
|
+
try { re = new RegExp('^' + cls + '$') } catch { return null }
|
|
5410
|
+
if (!ctx.classRuns) ctx.classRuns = new Map()
|
|
5411
|
+
const key = cls + '|' + min + '|' + max
|
|
5412
|
+
let name = ctx.classRuns.get(key)
|
|
5413
|
+
if (name === undefined) {
|
|
5414
|
+
name = '_cr' + ctx.varCounter++
|
|
5415
|
+
ctx.classRuns.set(key, name)
|
|
5416
|
+
const bits = []
|
|
5417
|
+
for (let c = 0; c < 128; c++) bits.push(re.test(String.fromCharCode(c)) ? 1 : 0)
|
|
5418
|
+
const lenCheck = max === Infinity ? `n<${min}` : min === max ? `n!==${min}` : `n<${min}||n>${max}`
|
|
5419
|
+
hoist(ctx, `const ${name}_t=new Uint8Array([${bits.join(',')}])\n function ${name}(s){const n=s.length;if(${lenCheck})return false;for(let i=0;i<n;i++){const c=s.charCodeAt(i);if(c>127||${name}_t[c]===0)return false}return true}`)
|
|
5420
|
+
}
|
|
5421
|
+
return `${name}(${v})`
|
|
5422
|
+
}
|
|
5423
|
+
|
|
4958
5424
|
// Compile simple regex patterns to inline charCode checks — avoids RegExp engine overhead.
|
|
4959
5425
|
// Returns null if pattern is too complex for inline compilation.
|
|
4960
5426
|
// Handles: ^[charclass]{n}$, ^[charclass]+$, ^[charclass]*$, ^[charclass]{m,n}$
|
|
@@ -5141,6 +5607,14 @@ function unevalContributions (sub, kind, names, state, depth) {
|
|
|
5141
5607
|
// Detect simple prefix patterns like "^x-", "^_", "^prefix" and generate fast charCodeAt checks
|
|
5142
5608
|
// Returns a JS expression string or null if pattern is too complex
|
|
5143
5609
|
function fastPrefixCheck(pattern, keyVar) {
|
|
5610
|
+
// ^name$ or ^(a|b)$ of plain characters is string equality (`$` without
|
|
5611
|
+
// the m flag ends the input); `.*` matches every string.
|
|
5612
|
+
const ex = pattern.match(/^\^(?:\(([a-zA-Z0-9_-]+(?:\|[a-zA-Z0-9_-]+)*)\)|([a-zA-Z0-9_-]+))\$$/)
|
|
5613
|
+
if (ex) {
|
|
5614
|
+
const alts = (ex[1] || ex[2]).split('|')
|
|
5615
|
+
if (alts.length <= 8) return `(${alts.map((a) => `${keyVar}===${JSON.stringify(a)}`).join('||')})`
|
|
5616
|
+
}
|
|
5617
|
+
if (pattern === '.*') return 'true'
|
|
5144
5618
|
// Match patterns like ^literal (no regex metacharacters after ^)
|
|
5145
5619
|
const m = pattern.match(/^\^([a-zA-Z0-9_\-./]+)$/)
|
|
5146
5620
|
if (!m) return null
|
|
@@ -5568,4 +6042,4 @@ Object.defineProperty(module.exports, '_expandDynamicScopes', { value: expandDyn
|
|
|
5568
6042
|
// For tests/test_custom_keyword_codegen.js, which holds these copies to the interpreter's.
|
|
5569
6043
|
Object.defineProperty(module.exports, '_kwTypeBits', { value: { kwTypeBit, kwDataBits, KW_T_ANY }, enumerable: false })
|
|
5570
6044
|
|
|
5571
|
-
module.exports._internal = { ownHead, hasConstraints, assertEmitted, prepareForCodegen, sharedCodegenGate, codegenSafe, isSimplePN, hasUnresolvableRef, needsBaseTracking, _cpLen, sharedRt, DECLINE, bindRt, compileSafe, emitGuardState, withPlain, REF_NEUTRAL_SIBLINGS, resolveCrossSchemaRef, enumCondition, enumOperand, emitDeq, emitConstant, ownGuard, ownKeyExpr, multipleOfBad, compilePatternInline, useSafeEngine, reFlags, FORMAT_CODEGEN, PROTO_NAMES, setOwn, nestedGenCode, compileToJSCodegen, SKIP_VALUE_KEYS, skipString, CYCLE_DEPTH, cyclicDefNames, sharedDefNames, reFlagArg, hoistOnce, PTR_ESC_HELPER, unevalContributions, emitUq, uqSources, safeReClosure, fastPrefixCheck, replaceTopLevel, refTargetFor, genCode, addLocalAnnotations, addTopApplicatorAnnotations, protoIs, ANNOT_APPLICATORS, SIZE_SKIPS, emitRootGuard, emitGuardedRun }
|
|
6045
|
+
module.exports._internal = { ownHead, hasConstraints, assertEmitted, prepareForCodegen, sharedCodegenGate, codegenSafe, isSimplePN, hasUnresolvableRef, needsBaseTracking, _cpLen, sharedRt, DECLINE, bindRt, compileSafe, emitGuardState, withPlain, REF_NEUTRAL_SIBLINGS, resolveCrossSchemaRef, enumCondition, enumOperand, emitDeq, emitConstant, ownGuard, ownKeyExpr, multipleOfBad, compilePatternInline, useSafeEngine, reFlags, FORMAT_CODEGEN, PROTO_NAMES, setOwn, nestedGenCode, compileToJSCodegen, SKIP_VALUE_KEYS, skipString, CYCLE_DEPTH, cyclicDefNames, sharedDefNames, reFlagArg, hoistOnce, PTR_ESC_HELPER, unevalContributions, emitUq, uqSources, safeReClosure, fastPrefixCheck, replaceTopLevel, refTargetFor, genCode, addLocalAnnotations, addTopApplicatorAnnotations, protoIs, ANNOT_APPLICATORS, SIZE_SKIPS, emitRootGuard, emitGuardedRun, keyLoopEligible, SAME_KEYS_HELPER, seqTag }
|