ata-validator 1.53.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 +13 -2
- package/lib/codegen-engine.js +2 -1
- package/lib/dialect.js +36 -1
- package/lib/draft7.js +17 -28
- 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 +46 -142
- package/lib/js-compiler-errors.js +52 -16
- package/lib/js-compiler.js +537 -65
- package/lib/metaschemas.js +5 -3
- package/lib/plan-compiler.js +24 -7
- package/lib/regex-linear.js +81 -1
- package/lib/safe-regex.js +14 -11
- package/lib/schema-scan.js +19 -28
- 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 +20 -11
- 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
|
|
|
@@ -541,7 +569,7 @@ function compileToJS(schema, defs, schemaMap) {
|
|
|
541
569
|
// format — hand-written fast checks. A format the code generator asserts
|
|
542
570
|
// but this path cannot is a decline, not a pass.
|
|
543
571
|
if (schema.format) {
|
|
544
|
-
const fc =
|
|
572
|
+
const fc = formatChecks()[schema.format]
|
|
545
573
|
if (fc) checks.push((d) => typeof d !== 'string' || fc(d))
|
|
546
574
|
else if (FORMAT_CODEGEN[schema.format]) return null
|
|
547
575
|
}
|
|
@@ -839,14 +867,26 @@ function _canonical(x) {
|
|
|
839
867
|
// schema with a `format` is compiled; loading it with this module cost every
|
|
840
868
|
// process 0.8 ms of its cold start.
|
|
841
869
|
let _formatsCache = null
|
|
842
|
-
|
|
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
|
+
}
|
|
843
878
|
// The closure path's formats. It carried its own idea of an email and knew
|
|
844
879
|
// five formats in all, so a schema that reached it was told "valid" for
|
|
845
880
|
// anything the other engines turned away; the first call on a fresh validator
|
|
846
881
|
// is the one that lands here. Every entry now names the same function the
|
|
847
882
|
// other two paths use, and the list is the list they implement.
|
|
848
|
-
|
|
849
|
-
|
|
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 = {
|
|
850
890
|
email: _fmtFns.email,
|
|
851
891
|
'idn-email': _fmtFns.idnEmail,
|
|
852
892
|
date: _fmtFns.date,
|
|
@@ -865,6 +905,8 @@ const FORMAT_CHECKS = {
|
|
|
865
905
|
'json-pointer': _fmtFns.jsonPointer,
|
|
866
906
|
'relative-json-pointer': _fmtFns.relativeJsonPointer,
|
|
867
907
|
regex: (s) => { try { new RegExp(s, 'u'); return true } catch { return false } },
|
|
908
|
+
}
|
|
909
|
+
return _formatChecks
|
|
868
910
|
}
|
|
869
911
|
|
|
870
912
|
// Dangerous JS property names that exist on Object.prototype
|
|
@@ -1238,6 +1280,51 @@ function emitPropFn (prop, ctx) {
|
|
|
1238
1280
|
hoist(ctx, `function ${fnName}(d){${bodyLines.join('\n ')}\n return true}`)
|
|
1239
1281
|
return fnName
|
|
1240
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
|
+
}
|
|
1241
1328
|
function countInline (lines, from, ctx) {
|
|
1242
1329
|
let n = 0
|
|
1243
1330
|
for (let i = from; i < lines.length; i++) n += lines[i].length
|
|
@@ -2005,6 +2092,75 @@ function sameInstanceEdges(root, node) {
|
|
|
2005
2092
|
if (typeof node.$ref === 'string') { const t = localPointerTarget(root, node.$ref); if (t) out.push(t) }
|
|
2006
2093
|
return out
|
|
2007
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
|
+
|
|
2008
2164
|
const _sameInstanceCycleCache = new WeakMap()
|
|
2009
2165
|
function hasSameInstanceRefCycle(root) {
|
|
2010
2166
|
if (root === null || typeof root !== 'object') return false
|
|
@@ -2346,6 +2502,13 @@ function normalizeRefs(root, schemaMap) {
|
|
|
2346
2502
|
return out
|
|
2347
2503
|
}
|
|
2348
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'])
|
|
2349
2512
|
// A schema whose every reference is `#` or a plain `#/$defs/name` (or
|
|
2350
2513
|
// `definitions`), with no `$id` or `$anchor` below the root to move the base
|
|
2351
2514
|
// they resolve against, needs nothing from the resolver: each reference
|
|
@@ -2431,6 +2594,9 @@ function expandDynamicScopes(root, schemaMap) {
|
|
|
2431
2594
|
}
|
|
2432
2595
|
const copies = new Map() // node (or a boolean sentinel) -> Map(signature -> entry)
|
|
2433
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()
|
|
2434
2600
|
let fail = false
|
|
2435
2601
|
// The entry for a node in a scope: its copy, made on the first ask, and
|
|
2436
2602
|
// later its name. A `$ref` slot holds the entry until names are given.
|
|
@@ -2473,7 +2639,10 @@ function expandDynamicScopes(root, schemaMap) {
|
|
|
2473
2639
|
continue
|
|
2474
2640
|
}
|
|
2475
2641
|
if (isSchema && DYN_DROP.has(k) && !(k === '$id' && node === root)) continue
|
|
2476
|
-
|
|
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))
|
|
2477
2646
|
}
|
|
2478
2647
|
return e
|
|
2479
2648
|
}
|
|
@@ -2508,7 +2677,7 @@ function expandDynamicScopes(root, schemaMap) {
|
|
|
2508
2677
|
o.$ref = v.name === '#' ? '#' : '#/' + defsKey + '/' + v.name
|
|
2509
2678
|
continue
|
|
2510
2679
|
}
|
|
2511
|
-
if (!
|
|
2680
|
+
if (!(v !== null && typeof v === 'object' && keptRaw.has(v))) fix(v)
|
|
2512
2681
|
}
|
|
2513
2682
|
}
|
|
2514
2683
|
fix(rootCopy)
|
|
@@ -2542,10 +2711,10 @@ function normalizeRefsWalk(root, schemaMap) {
|
|
|
2542
2711
|
const visited = new Set()
|
|
2543
2712
|
let fail = false
|
|
2544
2713
|
let dynamic = false
|
|
2545
|
-
const visit = (node) => {
|
|
2714
|
+
const visit = (node, inNames) => {
|
|
2546
2715
|
if (fail || node === null || typeof node !== 'object' || visited.has(node)) return
|
|
2547
2716
|
visited.add(node)
|
|
2548
|
-
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 }
|
|
2549
2718
|
if (node !== root && typeof node.$schema === 'string' && rootDialect !== null && node.$schema.replace(/#$/, '') !== rootDialect) { fail = true; return }
|
|
2550
2719
|
// A dynamic reference reached through another document is unrolled by
|
|
2551
2720
|
// scope the same way one in the root is (expandDynamicScopes, below).
|
|
@@ -2562,13 +2731,13 @@ function normalizeRefsWalk(root, schemaMap) {
|
|
|
2562
2731
|
// The root, however it is spelled, is `#`: a copy of it under its own
|
|
2563
2732
|
// definitions would contain itself.
|
|
2564
2733
|
if (t === root) { /* named '#' below */ }
|
|
2565
|
-
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) }
|
|
2566
2735
|
if (typeof t === 'boolean' && !names.has(t)) names.set(t, null)
|
|
2567
2736
|
}
|
|
2568
2737
|
}
|
|
2569
|
-
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))
|
|
2570
2739
|
}
|
|
2571
|
-
visit(root)
|
|
2740
|
+
visit(root, false)
|
|
2572
2741
|
if (fail) return null
|
|
2573
2742
|
if (dynamic) return expandDynamicScopes(root, schemaMap)
|
|
2574
2743
|
if (refTarget.size === 0) return root
|
|
@@ -2582,13 +2751,13 @@ function normalizeRefsWalk(root, schemaMap) {
|
|
|
2582
2751
|
}
|
|
2583
2752
|
// The copy: references renamed, nested `$id`s dropped.
|
|
2584
2753
|
const memo = new Map()
|
|
2585
|
-
const walk = (node) => {
|
|
2754
|
+
const walk = (node, inNames) => {
|
|
2586
2755
|
if (node === null || typeof node !== 'object') return node
|
|
2587
2756
|
if (memo.has(node)) return memo.get(node)
|
|
2588
2757
|
if (Array.isArray(node)) {
|
|
2589
2758
|
const out = []
|
|
2590
2759
|
memo.set(node, out)
|
|
2591
|
-
for (const x of node) out.push(walk(x))
|
|
2760
|
+
for (const x of node) out.push(walk(x, false))
|
|
2592
2761
|
return out
|
|
2593
2762
|
}
|
|
2594
2763
|
const out = {}
|
|
@@ -2600,13 +2769,13 @@ function normalizeRefsWalk(root, schemaMap) {
|
|
|
2600
2769
|
out.$ref = t === root ? '#' : '#/' + defsKey + '/' + names.get(t)
|
|
2601
2770
|
continue
|
|
2602
2771
|
}
|
|
2603
|
-
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)))
|
|
2604
2773
|
}
|
|
2605
2774
|
return out
|
|
2606
2775
|
}
|
|
2607
|
-
const out = walk(root)
|
|
2776
|
+
const out = walk(root, false)
|
|
2608
2777
|
const defs = { ...(out[defsKey] && typeof out[defsKey] === 'object' ? out[defsKey] : {}) }
|
|
2609
|
-
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)
|
|
2610
2779
|
out[defsKey] = defs
|
|
2611
2780
|
return out
|
|
2612
2781
|
}
|
|
@@ -2669,7 +2838,14 @@ function sourceSize (lines, ctx) {
|
|
|
2669
2838
|
|
|
2670
2839
|
function compileToJSCodegen(schema, schemaMap, userFormats, opts) {
|
|
2671
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.
|
|
2672
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)
|
|
2673
2849
|
if (typeof schema === 'boolean') return schema ? () => true : () => false
|
|
2674
2850
|
if (typeof schema !== 'object' || schema === null) return null
|
|
2675
2851
|
// removeAdditional: a verdict function that deletes the keys the remover
|
|
@@ -2807,13 +2983,21 @@ function compileToJSCodegen(schema, schemaMap, userFormats, opts) {
|
|
|
2807
2983
|
// validateJSON or get extended, and rewriting and compiling the source twice
|
|
2808
2984
|
// more on every schema was about a third of what a first call cost.
|
|
2809
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
|
|
2810
2992
|
if (ctx.usesRecursion) {
|
|
2811
2993
|
// Self-recursive: a named body reached through the cycle guard, so data
|
|
2812
2994
|
// that points back at itself settles instead of exhausting the stack.
|
|
2813
|
-
const decl = emitRootGuard('_validate', 'd', 'true') +
|
|
2995
|
+
const decl = (ctx.rootFwd ? `${ctx.rootFwd}_f=_validate;` : '') + emitRootGuard('_validate', 'd', 'true') +
|
|
2814
2996
|
`function _validate_b(d){\n ${checkStr}\n return true\n }\n `
|
|
2815
2997
|
const run = emitGuardedRun('return _validate(d)', '', defSets)
|
|
2816
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`
|
|
2817
3001
|
// Hybrid: keep the boolean answer, wrap only the outer call
|
|
2818
3002
|
hybridThunk = () => `${decl}function _run(d){\n ${run}\n }\n return _run(d)?R:E(d)`
|
|
2819
3003
|
resultThunk = () => `${decl}function _run(d){\n ${run}\n }\n return _run(d)?${RESULT_TRUE}:${RESULT_FALSE}`
|
|
@@ -2823,6 +3007,8 @@ function compileToJSCodegen(schema, schemaMap, userFormats, opts) {
|
|
|
2823
3007
|
// re-entered with the same value; the guarded functions need their state.
|
|
2824
3008
|
const run = emitGuardedRun(`return _body(d)`, '', defSets)
|
|
2825
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`
|
|
2826
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)`
|
|
2827
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}`
|
|
2828
3014
|
tailThunk = () => `function _body(d){\n ${checkStr}\n return true\n }\n function _run(d){\n ${run}\n }\n return _run(d)&&__ataTail(d)`
|
|
@@ -2841,17 +3027,21 @@ function compileToJSCodegen(schema, schemaMap, userFormats, opts) {
|
|
|
2841
3027
|
const guardStr = needsGuard ? emitGuardState() : ''
|
|
2842
3028
|
const preambleStr = guardStr +
|
|
2843
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()
|
|
2844
3033
|
|
|
2845
3034
|
try {
|
|
2846
3035
|
let boolFn
|
|
2847
|
-
if (
|
|
2848
|
-
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}}`)
|
|
2849
3041
|
boolFn = factory(...closureValues)
|
|
2850
|
-
} else if (preambleStr) {
|
|
2851
|
-
const factory = compileFunction(`${preambleStr}return function(d){${body}}`)
|
|
2852
|
-
boolFn = factory()
|
|
2853
3042
|
} else {
|
|
2854
|
-
|
|
3043
|
+
const factory = compileFunction(`${tag}${preambleStr}return function(d){${body}}`)
|
|
3044
|
+
boolFn = factory()
|
|
2855
3045
|
}
|
|
2856
3046
|
|
|
2857
3047
|
// Light by default: the pieces the tiering and standalone paths need
|
|
@@ -2871,7 +3061,7 @@ function compileToJSCodegen(schema, schemaMap, userFormats, opts) {
|
|
|
2871
3061
|
attachLazyHeavy(boolFn, inputSchema, schemaMap, userFormats, opts, !!ctx.usesSafeRe)
|
|
2872
3062
|
return boolFn
|
|
2873
3063
|
}
|
|
2874
|
-
attachHeavy(boolFn, { ctx, closureNames, closureValues, preambleStr, guardStr, hybridBody, resultThunk, tailThunk, body })
|
|
3064
|
+
attachHeavy(boolFn, { ctx, closureNames, closureValues, preambleStr, guardStr, hybridBody, resultThunk, tailThunk, body, liveDecl })
|
|
2875
3065
|
return boolFn
|
|
2876
3066
|
} catch {
|
|
2877
3067
|
return null
|
|
@@ -2893,7 +3083,13 @@ function attachLazyHeavy (boolFn, schema, schemaMap, userFormats, opts, usesSafe
|
|
|
2893
3083
|
}
|
|
2894
3084
|
// The factories and the source, as the full compile attaches them.
|
|
2895
3085
|
function attachHeavy (boolFn, P) {
|
|
2896
|
-
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()}}`)
|
|
2897
3093
|
// The hybrid: same body, return R instead of true, return E(d) instead of
|
|
2898
3094
|
// false. Compiled on first request; null when that compile is refused, and
|
|
2899
3095
|
// the caller then assembles from the error functions instead.
|
|
@@ -2901,7 +3097,7 @@ function attachHeavy (boolFn, P) {
|
|
|
2901
3097
|
boolFn._hybridFactory = (R, E) => {
|
|
2902
3098
|
if (hybridFactory === undefined) {
|
|
2903
3099
|
try {
|
|
2904
|
-
hybridFactory = compileFunction(...closureNames, 'R', 'E',
|
|
3100
|
+
hybridFactory = compileFunction(...closureNames, 'R', 'E', live('return _run(d)?R:E(d)', hybridBody))
|
|
2905
3101
|
} catch {
|
|
2906
3102
|
hybridFactory = null
|
|
2907
3103
|
}
|
|
@@ -2920,7 +3116,7 @@ function attachHeavy (boolFn, P) {
|
|
|
2920
3116
|
boolFn._resultFactory = (LR, B, W, EE) => {
|
|
2921
3117
|
if (resultFactory === undefined) {
|
|
2922
3118
|
try {
|
|
2923
|
-
resultFactory = compileFunction(...closureNames, '__ataLR', '__ataB', '__ataW', '__ataEE',
|
|
3119
|
+
resultFactory = compileFunction(...closureNames, '__ataLR', '__ataB', '__ataW', '__ataEE', live(`return _run(d)?${RESULT_TRUE}:${RESULT_FALSE}`, resultThunk))
|
|
2924
3120
|
} catch {
|
|
2925
3121
|
resultFactory = null
|
|
2926
3122
|
}
|
|
@@ -2940,7 +3136,7 @@ function attachHeavy (boolFn, P) {
|
|
|
2940
3136
|
boolFn._withTail = (tail) => {
|
|
2941
3137
|
if (tailFactory === undefined) {
|
|
2942
3138
|
try {
|
|
2943
|
-
tailFactory = compileFunction(...closureNames, '__ataTail',
|
|
3139
|
+
tailFactory = compileFunction(...closureNames, '__ataTail', live('return _run(d)&&__ataTail(d)', tailThunk))
|
|
2944
3140
|
} catch {
|
|
2945
3141
|
tailFactory = null
|
|
2946
3142
|
}
|
|
@@ -3439,7 +3635,7 @@ function genCodeNode(schema, v, lines, ctx, knownType) {
|
|
|
3439
3635
|
// object: its type check fails on undefined. A name on Object.prototype
|
|
3440
3636
|
// is never absent that way, and it is checked explicitly.
|
|
3441
3637
|
if (!hasTypeCheck || PROTO_NAMES.has(key)) {
|
|
3442
|
-
reqChecks.push(
|
|
3638
|
+
reqChecks.push(`${v}[${JSON.stringify(key)}]===undefined||!${ownGuard(ctx, v, key)}`)
|
|
3443
3639
|
} else {
|
|
3444
3640
|
typed.push(key)
|
|
3445
3641
|
}
|
|
@@ -3460,7 +3656,7 @@ function genCodeNode(schema, v, lines, ctx, knownType) {
|
|
|
3460
3656
|
lines.push(`if(!${pk || protoIs(ctx, v)}&&!_hall(${v},${JSON.stringify(typed)}))return false`)
|
|
3461
3657
|
}
|
|
3462
3658
|
} else if (schema.required && schema.required.length > 0) {
|
|
3463
|
-
const checks = schema.required.map(key =>
|
|
3659
|
+
const checks = schema.required.map(key => `${v}[${JSON.stringify(key)}]===undefined||!${ownGuard(ctx, v, key)}`)
|
|
3464
3660
|
if (isObj) {
|
|
3465
3661
|
lines.push(`if(${checks.join('||')})return false`)
|
|
3466
3662
|
} else {
|
|
@@ -3550,7 +3746,7 @@ function genCodeNode(schema, v, lines, ctx, knownType) {
|
|
|
3550
3746
|
|
|
3551
3747
|
if (schema.pattern) {
|
|
3552
3748
|
// Try inline charCode compilation for simple patterns (avoids RegExp engine)
|
|
3553
|
-
const inlineCheck = compilePatternInline(schema.pattern, v)
|
|
3749
|
+
const inlineCheck = classRunCheck(ctx, schema.pattern, v) || compilePatternInline(schema.pattern, v)
|
|
3554
3750
|
if (inlineCheck) {
|
|
3555
3751
|
lines.push(isStr ? `if(!(${inlineCheck}))return false` : `if(typeof ${v}==='string'&&!(${inlineCheck}))return false`)
|
|
3556
3752
|
} else {
|
|
@@ -3661,13 +3857,32 @@ function genCodeNode(schema, v, lines, ctx, knownType) {
|
|
|
3661
3857
|
|
|
3662
3858
|
// properties — use hoisted vars for required props, hoist optional to locals too
|
|
3663
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
|
|
3664
3876
|
for (const [key, prop] of Object.entries(schema.properties)) {
|
|
3877
|
+
if (keyLoop && !requiredSet.has(key)) { keyCases.push([key, prop]); continue }
|
|
3665
3878
|
const from = lines.length
|
|
3666
3879
|
// A large subschema as a function of its own (emitPropFn); the call
|
|
3667
3880
|
// takes the place of the inline code in each of the three shapes below.
|
|
3668
3881
|
const fn = propFnCandidate(prop, ctx) ? emitPropFn(prop, ctx) : null
|
|
3669
3882
|
if (requiredSet.has(key) && isObj) {
|
|
3670
|
-
// 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.
|
|
3671
3886
|
const access = hoisted[key] || `${v}[${JSON.stringify(key)}]`
|
|
3672
3887
|
const combined = fn ? null : tryGenCombined(prop, access, ctx)
|
|
3673
3888
|
if (fn) {
|
|
@@ -3683,7 +3898,21 @@ function genCodeNode(schema, v, lines, ctx, knownType) {
|
|
|
3683
3898
|
} else {
|
|
3684
3899
|
genCode(prop, access, lines, ctx)
|
|
3685
3900
|
}
|
|
3686
|
-
} 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 {
|
|
3687
3916
|
// Optional — hoist to local, check undefined
|
|
3688
3917
|
const oi = ctx.varCounter++
|
|
3689
3918
|
const local = `_o${oi}`
|
|
@@ -3697,14 +3926,28 @@ function genCodeNode(schema, v, lines, ctx, knownType) {
|
|
|
3697
3926
|
genCode(prop, local, lines, ctx)
|
|
3698
3927
|
}
|
|
3699
3928
|
lines.push(`}}`)
|
|
3700
|
-
} else {
|
|
3701
|
-
lines.push(`if(typeof ${v}==='object'&&${v}!==null&&${ownKeyExpr(ctx, v, key)}){`)
|
|
3702
|
-
if (fn) lines.push(`if(!${fn}(${v}[${JSON.stringify(key)}]))return false`)
|
|
3703
|
-
else genCode(prop, `${v}[${JSON.stringify(key)}]`, lines, ctx)
|
|
3704
|
-
lines.push(`}`)
|
|
3705
3929
|
}
|
|
3706
3930
|
countInline(lines, from, ctx)
|
|
3707
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(`}`)
|
|
3708
3951
|
}
|
|
3709
3952
|
|
|
3710
3953
|
// items — pass known type info to children
|
|
@@ -3714,11 +3957,26 @@ function genCodeNode(schema, v, lines, ctx, knownType) {
|
|
|
3714
3957
|
ctx.varCounter++
|
|
3715
3958
|
// items applies after the prefixItems positions (Draft 2020-12).
|
|
3716
3959
|
const start = Array.isArray(schema.prefixItems) ? schema.prefixItems.length : 0
|
|
3717
|
-
|
|
3718
|
-
|
|
3719
|
-
:
|
|
3720
|
-
|
|
3721
|
-
|
|
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
|
+
}
|
|
3722
3980
|
}
|
|
3723
3981
|
|
|
3724
3982
|
// prefixItems
|
|
@@ -3753,7 +4011,7 @@ function genCodeNode(schema, v, lines, ctx, knownType) {
|
|
|
3753
4011
|
|
|
3754
4012
|
// oneOf — branch fns hoisted to factory scope when safe (no recursion/ref).
|
|
3755
4013
|
// Falls back to inline closures if any branch touches recursive validation.
|
|
3756
|
-
if (schema.oneOf) genOneOfNode(schema, v, applicatorLines, ctx,
|
|
4014
|
+
if (schema.oneOf) genOneOfNode(schema, v, applicatorLines, ctx, effectiveType)
|
|
3757
4015
|
|
|
3758
4016
|
// not
|
|
3759
4017
|
if (schema.not !== undefined) genNotNode(schema, v, lines, ctx, knownType)
|
|
@@ -3922,7 +4180,42 @@ function genContainsNode(schema, v, lines, ctx, knownType, isArr) {
|
|
|
3922
4180
|
}
|
|
3923
4181
|
}
|
|
3924
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
|
+
|
|
3925
4217
|
function genAnyOfNode(schema, v, lines, ctx, knownType) {
|
|
4218
|
+
if (singleBranch(schema, schema.anyOf, v, lines, ctx, knownType)) return
|
|
3926
4219
|
const fi = ctx.varCounter++
|
|
3927
4220
|
const branchBodies = []
|
|
3928
4221
|
const reused = []
|
|
@@ -3958,6 +4251,13 @@ function genAnyOfNode(schema, v, lines, ctx, knownType) {
|
|
|
3958
4251
|
const checks = names.map(n => `${n}(${v})`).join('||')
|
|
3959
4252
|
lines.push(`if(!(${checks}))return false`)
|
|
3960
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
|
+
}
|
|
3961
4261
|
const fns = branchBodies.map(body => `function(_av){${body}}`)
|
|
3962
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}`)
|
|
3963
4263
|
}
|
|
@@ -4009,6 +4309,7 @@ function keyedBranches (branches) {
|
|
|
4009
4309
|
}
|
|
4010
4310
|
|
|
4011
4311
|
function genOneOfNode(schema, v, lines, ctx, knownType) {
|
|
4312
|
+
if (singleBranch(schema, schema.oneOf, v, lines, ctx, knownType)) return
|
|
4012
4313
|
const fi = ctx.varCounter++
|
|
4013
4314
|
const branchBodies = []
|
|
4014
4315
|
const reused = []
|
|
@@ -4020,19 +4321,24 @@ function genOneOfNode(schema, v, lines, ctx, knownType) {
|
|
|
4020
4321
|
// 86919 branch functions and 197 MB of source, and took seconds to build.
|
|
4021
4322
|
// A hoisted branch takes its input as a parameter and reads only
|
|
4022
4323
|
// module-level names, so the same schema object gives the same function.
|
|
4023
|
-
|
|
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
|
|
4024
4330
|
for (let i = 0; i < schema.oneOf.length; i++) {
|
|
4025
4331
|
const sub = schema.oneOf[i]
|
|
4026
4332
|
const key = canHoist ? branchKey(sub, ctx) : undefined
|
|
4027
|
-
const known = key !== undefined ?
|
|
4333
|
+
const known = key !== undefined ? branchMap.get(key) : undefined
|
|
4028
4334
|
if (known !== undefined) {
|
|
4029
4335
|
reused[i] = known.name
|
|
4030
4336
|
branchBodies.push(known.body)
|
|
4031
4337
|
continue
|
|
4032
4338
|
}
|
|
4033
4339
|
const subLines = []
|
|
4034
|
-
if (canHoist) genIsolated(ctx, () => genCode(sub, '_ov', subLines, ctx))
|
|
4035
|
-
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)
|
|
4036
4342
|
const body = subLines.length === 0 ? 'return true' : `${subLines.join(';')};return true`
|
|
4037
4343
|
// _validate is the recursive entry — hoisting branches above it breaks scope.
|
|
4038
4344
|
if (/\b_validate\b/.test(body)) canHoist = false
|
|
@@ -4044,7 +4350,7 @@ function genOneOfNode(schema, v, lines, ctx, knownType) {
|
|
|
4044
4350
|
const name = `_of${fi}_b${i}`
|
|
4045
4351
|
hoist(ctx, `function ${name}(_ov){${body}}`)
|
|
4046
4352
|
const key = branchKey(schema.oneOf[i], ctx)
|
|
4047
|
-
if (key !== undefined)
|
|
4353
|
+
if (key !== undefined) branchMap.set(key, { name, body })
|
|
4048
4354
|
return name
|
|
4049
4355
|
})
|
|
4050
4356
|
const calls = names.map(n => `if(${n}(${v})){_oc${fi}++;if(_oc${fi}>1)return false}`).join(';')
|
|
@@ -4061,12 +4367,77 @@ function genOneOfNode(schema, v, lines, ctx, knownType) {
|
|
|
4061
4367
|
}
|
|
4062
4368
|
lines.push(`{let _oc${fi}=0;${calls};if(_oc${fi}!==1)return false}`)
|
|
4063
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
|
+
}
|
|
4064
4377
|
const fns = branchBodies.map(body => `function(_ov){${body}}`)
|
|
4065
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}`)
|
|
4066
4379
|
}
|
|
4067
4380
|
}
|
|
4068
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
|
+
|
|
4069
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
|
+
}
|
|
4070
4441
|
const subLines = []
|
|
4071
4442
|
genCode(schema.not, '_nv', subLines, ctx)
|
|
4072
4443
|
if (subLines.length === 0) {
|
|
@@ -4078,6 +4449,29 @@ function genNotNode(schema, v, lines, ctx, knownType) {
|
|
|
4078
4449
|
}
|
|
4079
4450
|
|
|
4080
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
|
+
}
|
|
4081
4475
|
const ifLines = []
|
|
4082
4476
|
genCode(schema.if, '_iv', ifLines, ctx)
|
|
4083
4477
|
const fi = ctx.varCounter++
|
|
@@ -4104,12 +4498,30 @@ function genIfNode(schema, v, lines, ctx, knownType) {
|
|
|
4104
4498
|
lines.push(`if(_if${fi}(${v})){if(_th${fi}&&!_th${fi}(${v}))return false}else{if(_el${fi}&&!_el${fi}(${v}))return false}}`)
|
|
4105
4499
|
}
|
|
4106
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
|
+
|
|
4107
4516
|
function genRefNode(schema, v, lines, ctx, knownType, hasSiblings) {
|
|
4108
4517
|
// Self-reference "#" — recursive call to root validator
|
|
4109
4518
|
if (schema.$ref === '#') {
|
|
4110
4519
|
if (ctx.nestedBoolean) throw DECLINE
|
|
4111
4520
|
ctx.usesRecursion = true
|
|
4112
|
-
|
|
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`)
|
|
4113
4525
|
if (!hasSiblings) return true
|
|
4114
4526
|
}
|
|
4115
4527
|
// 1. Local ref
|
|
@@ -4136,14 +4548,14 @@ function genRefNode(schema, v, lines, ctx, knownType, hasSiblings) {
|
|
|
4136
4548
|
ctx.deferredChecks = null
|
|
4137
4549
|
const outerReuse = ctx.plainReuse
|
|
4138
4550
|
ctx.plainReuse = undefined
|
|
4139
|
-
|
|
4551
|
+
ctx.inDefFn = (ctx.inDefFn || 0) + 1
|
|
4552
|
+
try { genIsolated(ctx, () => genCode(ctx.rootDefs[defName], 'd', bodyLines, ctx)) } finally { ctx.plainReuse = outerReuse; ctx.inDefFn-- }
|
|
4140
4553
|
if (ctx.deferredChecks) for (const dc of ctx.deferredChecks) bodyLines.push(dc)
|
|
4141
4554
|
ctx.deferredChecks = outerDeferred
|
|
4142
4555
|
// The def is hoisted above the entry function, where the recursive
|
|
4143
|
-
// entry `_validate` is not in scope
|
|
4144
|
-
//
|
|
4145
|
-
//
|
|
4146
|
-
// 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.
|
|
4147
4559
|
if (bodyLines.some((l) => /\b_validate\b/.test(l))) throw DECLINE
|
|
4148
4560
|
// Entered through the cycle guard: see emitGuardState. In the fast
|
|
4149
4561
|
// pass the wrapper only counts depth; the set is used by the second,
|
|
@@ -4333,8 +4745,15 @@ function genPatternPropertiesNode(schema, v, lines, ctx, knownType, isObj) {
|
|
|
4333
4745
|
}
|
|
4334
4746
|
lines.push(`}}`)
|
|
4335
4747
|
} else {
|
|
4336
|
-
// 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.
|
|
4337
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
|
|
4338
4757
|
lines.push(`${guard}{${ownHead(ctx, v, kVar, 'const')}{`)
|
|
4339
4758
|
// propertyNames checks (merged)
|
|
4340
4759
|
if (pn) {
|
|
@@ -4359,7 +4778,7 @@ function genPatternPropertiesNode(schema, v, lines, ctx, knownType, isObj) {
|
|
|
4359
4778
|
lines.push(`if(!_es${ei}.has(${kVar}))return false`)
|
|
4360
4779
|
}
|
|
4361
4780
|
}
|
|
4362
|
-
for (
|
|
4781
|
+
for (const i of live) {
|
|
4363
4782
|
lines.push(`if(${matchers[i].check}){const _ppv${pi}=${v}[${kVar}];${subChecks[i]}}`)
|
|
4364
4783
|
}
|
|
4365
4784
|
lines.push(`}}`)
|
|
@@ -4920,8 +5339,19 @@ const FORMAT_CODEGEN = {
|
|
|
4920
5339
|
'json-pointer': (...a) => getFormats().jsonPointerSource(...a),
|
|
4921
5340
|
'relative-json-pointer': (...a) => getFormats().relativeJsonPointerSource(...a),
|
|
4922
5341
|
'uri-template': (...a) => getFormats().uriTemplateSource(...a),
|
|
4923
|
-
|
|
4924
|
-
|
|
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
|
+
},
|
|
4925
5355
|
'idn-email': (...a) => getFormats().idnEmailSource(...a),
|
|
4926
5356
|
regex: (v, isStr) => {
|
|
4927
5357
|
const inner = `try{new RegExp(${v},'u')}catch(_er){return false}`
|
|
@@ -4937,9 +5367,11 @@ const FORMAT_CODEGEN = {
|
|
|
4937
5367
|
hoistOnce(ctx, '_uriHoisted', uriHelper())
|
|
4938
5368
|
return isStr ? `if(!_uri(${v}))return false` : `if(typeof ${v}==='string'&&!_uri(${v}))return false`
|
|
4939
5369
|
},
|
|
4940
|
-
'uri-reference': (v, isStr) =>
|
|
4941
|
-
|
|
4942
|
-
|
|
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
|
+
},
|
|
4943
5375
|
ipv4: (v, isStr, ctx) => {
|
|
4944
5376
|
if (!ctx) return getFormats().ipv4Source(v, isStr)
|
|
4945
5377
|
// Hoisted: a regex literal in the body would build a new RegExp per call.
|
|
@@ -4957,6 +5389,38 @@ const FORMAT_CODEGEN = {
|
|
|
4957
5389
|
hostname: (...a) => getFormats().hostnameSource(...a),
|
|
4958
5390
|
}
|
|
4959
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
|
+
|
|
4960
5424
|
// Compile simple regex patterns to inline charCode checks — avoids RegExp engine overhead.
|
|
4961
5425
|
// Returns null if pattern is too complex for inline compilation.
|
|
4962
5426
|
// Handles: ^[charclass]{n}$, ^[charclass]+$, ^[charclass]*$, ^[charclass]{m,n}$
|
|
@@ -5143,6 +5607,14 @@ function unevalContributions (sub, kind, names, state, depth) {
|
|
|
5143
5607
|
// Detect simple prefix patterns like "^x-", "^_", "^prefix" and generate fast charCodeAt checks
|
|
5144
5608
|
// Returns a JS expression string or null if pattern is too complex
|
|
5145
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'
|
|
5146
5618
|
// Match patterns like ^literal (no regex metacharacters after ^)
|
|
5147
5619
|
const m = pattern.match(/^\^([a-zA-Z0-9_\-./]+)$/)
|
|
5148
5620
|
if (!m) return null
|
|
@@ -5570,4 +6042,4 @@ Object.defineProperty(module.exports, '_expandDynamicScopes', { value: expandDyn
|
|
|
5570
6042
|
// For tests/test_custom_keyword_codegen.js, which holds these copies to the interpreter's.
|
|
5571
6043
|
Object.defineProperty(module.exports, '_kwTypeBits', { value: { kwTypeBit, kwDataBits, KW_T_ANY }, enumerable: false })
|
|
5572
6044
|
|
|
5573
|
-
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 }
|