ata-validator 1.31.2 → 1.32.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.d.ts +10 -0
- package/index.js +113 -16
- package/lib/aot-impl.js +1 -271
- package/lib/clone-emit.js +302 -0
- package/lib/interpreter.js +10 -4
- package/lib/js-compiler.js +155 -21
- package/lib/plan-compiler.js +1 -1
- package/lib/plan-source.js +281 -0
- package/lib/tier0.js +2 -2
- package/lib/version.js +1 -1
- package/package.json +9 -9
package/lib/js-compiler.js
CHANGED
|
@@ -489,24 +489,24 @@ function compileToJS(schema, defs, schemaMap) {
|
|
|
489
489
|
// answer differently for it.
|
|
490
490
|
if (schema.minimum !== undefined) {
|
|
491
491
|
const min = schema.minimum
|
|
492
|
-
checks.push((d) =>
|
|
492
|
+
checks.push((d) => !Number.isFinite(d) || d >= min)
|
|
493
493
|
}
|
|
494
494
|
if (schema.maximum !== undefined) {
|
|
495
495
|
const max = schema.maximum
|
|
496
|
-
checks.push((d) =>
|
|
496
|
+
checks.push((d) => !Number.isFinite(d) || d <= max)
|
|
497
497
|
}
|
|
498
498
|
if (schema.exclusiveMinimum !== undefined) {
|
|
499
499
|
const min = schema.exclusiveMinimum
|
|
500
|
-
checks.push((d) =>
|
|
500
|
+
checks.push((d) => !Number.isFinite(d) || d > min)
|
|
501
501
|
}
|
|
502
502
|
if (schema.exclusiveMaximum !== undefined) {
|
|
503
503
|
const max = schema.exclusiveMaximum
|
|
504
|
-
checks.push((d) =>
|
|
504
|
+
checks.push((d) => !Number.isFinite(d) || d < max)
|
|
505
505
|
}
|
|
506
506
|
if (schema.multipleOf !== undefined) {
|
|
507
507
|
const div = schema.multipleOf
|
|
508
508
|
checks.push((d) => {
|
|
509
|
-
if (
|
|
509
|
+
if (!Number.isFinite(d)) return true
|
|
510
510
|
if (div === 0) return false
|
|
511
511
|
const q = d / div
|
|
512
512
|
return Number.isInteger(q) || Math.abs(q - Math.round(q)) < 1e-9
|
|
@@ -850,7 +850,7 @@ function multipleOfBad(v, m) {
|
|
|
850
850
|
|
|
851
851
|
const TYPE_CHECKS = {
|
|
852
852
|
string: (d) => typeof d === 'string',
|
|
853
|
-
number: (d) =>
|
|
853
|
+
number: (d) => Number.isFinite(d),
|
|
854
854
|
integer: (d) => Number.isInteger(d),
|
|
855
855
|
boolean: (d) => typeof d === 'boolean',
|
|
856
856
|
null: (d) => d === null,
|
|
@@ -912,17 +912,66 @@ const PROTO_NAMES = new Set([...Object.getOwnPropertyNames(Object.prototype), '_
|
|
|
912
912
|
// Declared once per compiled function and called by name: spelled out at each
|
|
913
913
|
// property, the test grew a 50-property module by a tenth. Both are small
|
|
914
914
|
// enough for the engine to inline.
|
|
915
|
-
|
|
916
|
-
|
|
915
|
+
//
|
|
916
|
+
// The `in` stays at the call site with the key as a constant: inside a shared
|
|
917
|
+
// helper the key is a parameter, the engine cannot keep that `in` monomorphic,
|
|
918
|
+
// and reading the errors of a product document went from 3.9 to 5.8 us. The
|
|
919
|
+
// helper only runs once `in` has said yes, and on an ordinary object it
|
|
920
|
+
// answers from the prototype without looking at the key.
|
|
921
|
+
const OWN_HELPERS = 'const _hop=Object.prototype.hasOwnProperty;function _h(o,k){return _hop.call(o,k)}' +
|
|
922
|
+
'function _ok(o,k){return o.__proto__===Object.prototype||_hop.call(o,k)}' +
|
|
923
|
+
'function _hall(o,ks){for(let i=0;i<ks.length;i++)if(!_hop.call(o,ks[i]))return false;return true}'
|
|
924
|
+
//
|
|
925
|
+
// Better still, the prototype is read once per object: a node that asks about
|
|
926
|
+
// presence declares `_pkN`, whether its value is an ordinary object, before
|
|
927
|
+
// anything else, and every key test in that node reads the local. A shared
|
|
928
|
+
// helper reading `__proto__` for each key was megamorphic, since it saw every
|
|
929
|
+
// object shape in the document, and cost the error path about a microsecond
|
|
930
|
+
// on a product document.
|
|
917
931
|
function ownKeyExpr (ctx, v, key) {
|
|
918
932
|
hoistOnce(ctx, '_ownHoisted', OWN_HELPERS)
|
|
919
933
|
const k = JSON.stringify(key)
|
|
920
|
-
|
|
934
|
+
if (PROTO_NAMES.has(key)) return `_h(${v},${k})`
|
|
935
|
+
const pk = ctx.plainOf ? ctx.plainOf.get(v) : undefined
|
|
936
|
+
return pk !== undefined ? `(${pk}?${k} in ${v}:_h(${v},${k}))` : `(${k} in ${v}&&_ok(${v},${k}))`
|
|
921
937
|
}
|
|
922
938
|
// For a value already read as `v[key]` and found not to be undefined: whether
|
|
923
|
-
// it came from `v` itself.
|
|
939
|
+
// it came from `v` itself. `in` is known to hold, so only the second half runs.
|
|
924
940
|
function ownGuard (ctx, v, key) {
|
|
925
|
-
|
|
941
|
+
hoistOnce(ctx, '_ownHoisted', OWN_HELPERS)
|
|
942
|
+
const k = JSON.stringify(key)
|
|
943
|
+
if (PROTO_NAMES.has(key)) return `_h(${v},${k})`
|
|
944
|
+
const pk = ctx.plainOf ? ctx.plainOf.get(v) : undefined
|
|
945
|
+
return pk !== undefined ? `(${pk}||_h(${v},${k}))` : `_ok(${v},${k})`
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
// Declares the per-object prototype flag for a node that asks about presence,
|
|
949
|
+
// and records it for the key tests emitted while the node is generated. The
|
|
950
|
+
// record is restored on the way out, so a nested node over the same value
|
|
951
|
+
// expression, an allOf branch say, cannot leave its name behind.
|
|
952
|
+
function withPlain (schema, v, lines, ctx, run) {
|
|
953
|
+
if (typeof schema !== 'object' || schema === null ||
|
|
954
|
+
!(schema.required || schema.properties || schema.dependentRequired || schema.dependentSchemas)) {
|
|
955
|
+
return run()
|
|
956
|
+
}
|
|
957
|
+
if (!ctx.plainOf) ctx.plainOf = new Map()
|
|
958
|
+
const name = '_pk' + ctx.varCounter++
|
|
959
|
+
const at = lines.length
|
|
960
|
+
lines.push(`const ${name}=typeof ${v}==='object'&&${v}!==null&&${v}.__proto__===Object.prototype`)
|
|
961
|
+
const prev = ctx.plainOf.get(v)
|
|
962
|
+
ctx.plainOf.set(v, name)
|
|
963
|
+
try {
|
|
964
|
+
return run()
|
|
965
|
+
} finally {
|
|
966
|
+
if (prev === undefined) ctx.plainOf.delete(v)
|
|
967
|
+
else ctx.plainOf.set(v, prev)
|
|
968
|
+
// A node whose keys are all typed never reads the flag; declaring it
|
|
969
|
+
// anyway cost a strict-object check about five percent. Unused, it goes.
|
|
970
|
+
const re = new RegExp('\\b' + name + '\\b')
|
|
971
|
+
let used = false
|
|
972
|
+
for (let i = at + 1; i < lines.length && !used; i++) if (re.test(lines[i])) used = true
|
|
973
|
+
if (!used && lines[at] === `const ${name}=typeof ${v}==='object'&&${v}!==null&&${v}.__proto__===Object.prototype`) lines.splice(at, 1)
|
|
974
|
+
}
|
|
926
975
|
}
|
|
927
976
|
// The same test at run time, for the closure compiler.
|
|
928
977
|
const _hop = Object.prototype.hasOwnProperty
|
|
@@ -934,6 +983,56 @@ function hasOwnKey (d, key) {
|
|
|
934
983
|
const UNSAFE_KEYS = new Set(['__proto__', 'constructor', 'toString', 'valueOf',
|
|
935
984
|
'hasOwnProperty', 'isPrototypeOf', 'propertyIsEnumerable', 'toLocaleString'])
|
|
936
985
|
|
|
986
|
+
// The nodes the removeAdditional pass deletes from, when deleting them during
|
|
987
|
+
// the verdict gives the answer removing them first would, or null when it may
|
|
988
|
+
// not. It may not whenever a keyword could look at a key before it is deleted
|
|
989
|
+
// and pass where it would fail after: minProperties, a const or enum object
|
|
990
|
+
// that holds the extra key, uniqueItems over objects that differ only in one,
|
|
991
|
+
// a branch that requires it, a `required` naming a key the node does not
|
|
992
|
+
// declare. So the whole schema is held to keywords that only look at declared
|
|
993
|
+
// properties or at scalar values. The node set mirrors emitRemovals in
|
|
994
|
+
// index.js: reached through `properties` from the root, with
|
|
995
|
+
// additionalProperties false and at least one declared property.
|
|
996
|
+
const FUSED_REMOVAL_KEYS = new Set(['type', 'properties', 'required', 'additionalProperties', 'items',
|
|
997
|
+
'minItems', 'maxItems', 'minLength', 'maxLength', 'pattern', 'format', 'minimum', 'maximum',
|
|
998
|
+
'exclusiveMinimum', 'exclusiveMaximum', 'multipleOf', 'enum', 'const', 'title', 'description',
|
|
999
|
+
'$comment', '$schema'])
|
|
1000
|
+
function fusedRemovalNodes (root) {
|
|
1001
|
+
const isScalar = (x) => x === null || (typeof x !== 'object' && typeof x !== 'function')
|
|
1002
|
+
const safe = (node) => {
|
|
1003
|
+
if (typeof node === 'boolean') return true
|
|
1004
|
+
if (!node || typeof node !== 'object' || Array.isArray(node)) return false
|
|
1005
|
+
for (const k of Object.keys(node)) if (!FUSED_REMOVAL_KEYS.has(k)) return false
|
|
1006
|
+
if (node.enum !== undefined && !(Array.isArray(node.enum) && node.enum.every(isScalar))) return false
|
|
1007
|
+
if (node.const !== undefined && !isScalar(node.const)) return false
|
|
1008
|
+
if (node.properties !== undefined) {
|
|
1009
|
+
if (!node.properties || typeof node.properties !== 'object' || Array.isArray(node.properties)) return false
|
|
1010
|
+
for (const k of Object.keys(node.properties)) if (!safe(node.properties[k])) return false
|
|
1011
|
+
}
|
|
1012
|
+
if (node.additionalProperties === false) {
|
|
1013
|
+
const declared = node.properties ? Object.keys(node.properties) : []
|
|
1014
|
+
if (declared.length === 0) return false
|
|
1015
|
+
if (Array.isArray(node.required) && node.required.some((r) => !declared.includes(r))) return false
|
|
1016
|
+
} else if (node.additionalProperties !== undefined && node.additionalProperties !== true && !safe(node.additionalProperties)) {
|
|
1017
|
+
return false
|
|
1018
|
+
}
|
|
1019
|
+
if (node.items !== undefined && (Array.isArray(node.items) || !safe(node.items))) return false
|
|
1020
|
+
return true
|
|
1021
|
+
}
|
|
1022
|
+
if (!safe(root)) return null
|
|
1023
|
+
const nodes = new Set()
|
|
1024
|
+
const walk = (node) => {
|
|
1025
|
+
if (!node || typeof node !== 'object' || !node.properties) return
|
|
1026
|
+
if (node.additionalProperties === false && Object.keys(node.properties).length > 0) nodes.add(node)
|
|
1027
|
+
for (const k of Object.keys(node.properties)) {
|
|
1028
|
+
const p = node.properties[k]
|
|
1029
|
+
if (p && typeof p === 'object' && p.properties) walk(p)
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
walk(root)
|
|
1033
|
+
return nodes.size > 0 ? nodes : null
|
|
1034
|
+
}
|
|
1035
|
+
|
|
937
1036
|
// Check if all $dynamicRef in a target schema can be resolved via the calling schema's anchors.
|
|
938
1037
|
function canResolveDynamicRefs(target, callingSchema, schemaMap) {
|
|
939
1038
|
// Collect anchors from calling schema
|
|
@@ -1546,9 +1645,18 @@ function sharedCodegenGate(schema, schemaMap) {
|
|
|
1546
1645
|
|
|
1547
1646
|
// --- Codegen mode: generates a single Function (NOT CSP-safe) ---
|
|
1548
1647
|
// This matches ajv's approach: one monolithic function, V8 JIT fully inlines it
|
|
1549
|
-
function compileToJSCodegen(schema, schemaMap, userFormats) {
|
|
1648
|
+
function compileToJSCodegen(schema, schemaMap, userFormats, opts) {
|
|
1550
1649
|
if (typeof schema === 'boolean') return schema ? () => true : () => false
|
|
1551
1650
|
if (typeof schema !== 'object' || schema === null) return null
|
|
1651
|
+
// removeAdditional: a verdict function that deletes the keys the remover
|
|
1652
|
+
// would delete, in the walk the verdict already makes, instead of rejecting
|
|
1653
|
+
// them. Only for schemas where that cannot change an answer; see
|
|
1654
|
+
// fusedRemovalNodes.
|
|
1655
|
+
let removeNodes = null
|
|
1656
|
+
if (opts && opts.removeAdditional) {
|
|
1657
|
+
removeNodes = fusedRemovalNodes(schema)
|
|
1658
|
+
if (removeNodes === null) return null
|
|
1659
|
+
}
|
|
1552
1660
|
|
|
1553
1661
|
if (!sharedCodegenGate(schema, schemaMap)) return null
|
|
1554
1662
|
|
|
@@ -1608,7 +1716,7 @@ function compileToJSCodegen(schema, schemaMap, userFormats) {
|
|
|
1608
1716
|
if (hasUnresolvableRef(schema, rootDefs, anchors, schemaMap, new Set())) return null
|
|
1609
1717
|
if (needsBaseTracking(schema, schemaMap, new Set())) return null
|
|
1610
1718
|
|
|
1611
|
-
const ctx = { varCounter: 0, helpers: [], helperCode: [], preamble: [], shared: [], closureVars: ['_cpLen'], closureVals: [_cpLen], rootDefs, refStack: new Set(), schemaMap: schemaMap || null, anchors, rootSchema: schema, userFormats: userFormats || null }
|
|
1719
|
+
const ctx = { varCounter: 0, helpers: [], helperCode: [], preamble: [], shared: [], closureVars: ['_cpLen'], closureVals: [_cpLen], rootDefs, refStack: new Set(), schemaMap: schemaMap || null, anchors, rootSchema: schema, userFormats: userFormats || null, removeNodes }
|
|
1612
1720
|
const lines = []
|
|
1613
1721
|
genCode(schema, 'd', lines, ctx)
|
|
1614
1722
|
|
|
@@ -1897,7 +2005,7 @@ function tryGenCombined(schema, access, ctx) {
|
|
|
1897
2005
|
}
|
|
1898
2006
|
|
|
1899
2007
|
if (t === 'number') {
|
|
1900
|
-
const conds = [
|
|
2008
|
+
const conds = [`!Number.isFinite(_v)`]
|
|
1901
2009
|
if (schema.minimum !== undefined) conds.push(`_v<${schema.minimum}`)
|
|
1902
2010
|
if (schema.maximum !== undefined) conds.push(`_v>${schema.maximum}`)
|
|
1903
2011
|
if (schema.exclusiveMinimum !== undefined) conds.push(`_v<=${schema.exclusiveMinimum}`)
|
|
@@ -1930,6 +2038,9 @@ function _deferOrInline(ctx, lines, v, check) {
|
|
|
1930
2038
|
// 'array' = we know v is an array
|
|
1931
2039
|
// 'string' / 'number' / 'integer' = we know the primitive type
|
|
1932
2040
|
function genCode(schema, v, lines, ctx, knownType) {
|
|
2041
|
+
return withPlain(schema, v, lines, ctx, () => genCodeNode(schema, v, lines, ctx, knownType))
|
|
2042
|
+
}
|
|
2043
|
+
function genCodeNode(schema, v, lines, ctx, knownType) {
|
|
1933
2044
|
// A boolean schema is a constant. false rejects whatever reached it; true
|
|
1934
2045
|
// constrains nothing and emits nothing, which is correct only because the
|
|
1935
2046
|
// caller's own checks still stand around this call.
|
|
@@ -2079,7 +2190,7 @@ function genCode(schema, v, lines, ctx, knownType) {
|
|
|
2079
2190
|
case 'object': lines.push(`if(typeof ${v}!=='object'||${v}===null||Array.isArray(${v}))return false`); break
|
|
2080
2191
|
case 'array': lines.push(`if(!Array.isArray(${v}))return false`); break
|
|
2081
2192
|
case 'string': lines.push(`if(typeof ${v}!=='string')return false`); break
|
|
2082
|
-
case 'number': lines.push(`if(
|
|
2193
|
+
case 'number': lines.push(`if(!Number.isFinite(${v}))return false`); break
|
|
2083
2194
|
case 'integer': lines.push(`if(!Number.isInteger(${v}))return false`); break
|
|
2084
2195
|
case 'boolean': lines.push(`if(typeof ${v}!=='boolean')return false`); break
|
|
2085
2196
|
case 'null': lines.push(`if(${v}!==null)return false`); break
|
|
@@ -2090,7 +2201,7 @@ function genCode(schema, v, lines, ctx, knownType) {
|
|
|
2090
2201
|
case 'object': return `(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v}))`
|
|
2091
2202
|
case 'array': return `Array.isArray(${v})`
|
|
2092
2203
|
case 'string': return `typeof ${v}==='string'`
|
|
2093
|
-
case 'number': return `
|
|
2204
|
+
case 'number': return `Number.isFinite(${v})`
|
|
2094
2205
|
case 'integer': return `Number.isInteger(${v})`
|
|
2095
2206
|
case 'boolean': return `typeof ${v}==='boolean'`
|
|
2096
2207
|
case 'null': return `${v}===null`
|
|
@@ -2156,7 +2267,10 @@ function genCode(schema, v, lines, ctx, knownType) {
|
|
|
2156
2267
|
// On an object with another prototype the typed ones may be inherited.
|
|
2157
2268
|
if (typed.length > 0) {
|
|
2158
2269
|
hoistOnce(ctx, '_ownHoisted', OWN_HELPERS)
|
|
2159
|
-
|
|
2270
|
+
// Read directly: the object type is already established here, and a
|
|
2271
|
+
// flag declared for this alone costs more than it saves. The list is
|
|
2272
|
+
// built only on that path, which an ordinary object never takes.
|
|
2273
|
+
lines.push(`if(${v}.__proto__!==Object.prototype&&!_hall(${v},${JSON.stringify(typed)}))return false`)
|
|
2160
2274
|
}
|
|
2161
2275
|
} else if (schema.required && schema.required.length > 0) {
|
|
2162
2276
|
const checks = schema.required.map(key => `!${ownKeyExpr(ctx, v, key)}`)
|
|
@@ -2194,7 +2308,7 @@ function genCode(schema, v, lines, ctx, knownType) {
|
|
|
2194
2308
|
// non-finite value through: it carries no type, an empty schema accepts it,
|
|
2195
2309
|
// and the interpreted engine reads it the same way. With `type: number` the
|
|
2196
2310
|
// type check has already turned it away, so the guard is only needed here.
|
|
2197
|
-
const numGuard = isNum ? '' : `
|
|
2311
|
+
const numGuard = isNum ? '' : `Number.isFinite(${v})&&`
|
|
2198
2312
|
if (schema.minimum !== undefined) lines.push(`if(${numGuard}${v}<${schema.minimum})return false`)
|
|
2199
2313
|
if (schema.maximum !== undefined) lines.push(`if(${numGuard}${v}>${schema.maximum})return false`)
|
|
2200
2314
|
if (schema.exclusiveMinimum !== undefined) lines.push(`if(${numGuard}${v}<=${schema.exclusiveMinimum})return false`)
|
|
@@ -2316,7 +2430,21 @@ function genCode(schema, v, lines, ctx, knownType) {
|
|
|
2316
2430
|
// additionalProperties -- deferred to end of function for better V8 optimization
|
|
2317
2431
|
// (type checks run first in hot path, expensive prop count check last)
|
|
2318
2432
|
// Skip if patternProperties is present — it will handle additionalProperties in a unified loop
|
|
2319
|
-
if (schema.additionalProperties === false && schema.properties && !schema.patternProperties) {
|
|
2433
|
+
if (schema.additionalProperties === false && schema.properties && !schema.patternProperties && ctx.removeNodes && ctx.removeNodes.has(schema)) {
|
|
2434
|
+
const kv = `_rk${ctx.varCounter++}`
|
|
2435
|
+
const declared = Object.keys(schema.properties)
|
|
2436
|
+
let loop = `for(var ${kv} in ${v})if(${declared.map((k) => `${kv}!==${JSON.stringify(k)}`).join('&&')})delete ${v}[${kv}]`
|
|
2437
|
+
// When every declared property is required, the verdict has already
|
|
2438
|
+
// established they are all there, so a key count equal to the declared
|
|
2439
|
+
// count means nothing is extra and the deleting walk, which compares every
|
|
2440
|
+
// key against the declared names, need not run.
|
|
2441
|
+
const req = new Set(schema.required || [])
|
|
2442
|
+
if (declared.length <= 15 && declared.every((k) => req.has(k))) {
|
|
2443
|
+
const n = `_rn${ctx.varCounter++}`
|
|
2444
|
+
loop = `{var ${n}=0;for(var ${kv} in ${v})${n}++;if(${n}!==${declared.length}){${loop}}}`
|
|
2445
|
+
}
|
|
2446
|
+
_deferOrInline(ctx, lines, v, isObj ? loop : `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){${loop}}`)
|
|
2447
|
+
} else if (schema.additionalProperties === false && schema.properties && !schema.patternProperties) {
|
|
2320
2448
|
const propCount = Object.keys(schema.properties).length
|
|
2321
2449
|
const allRequired = schema.required && schema.required.length === propCount
|
|
2322
2450
|
const inner = allRequired
|
|
@@ -3731,6 +3859,9 @@ function emitBranchCollapse (branches, keyword, v, pathExpr, lines, ctx, schemaP
|
|
|
3731
3859
|
// Instead of `return false`, pushes to `_e` array and optionally early-returns.
|
|
3732
3860
|
// `_all` parameter: if falsy, return after first error.
|
|
3733
3861
|
function genCodeE(schema, v, pathExpr, lines, ctx, schemaPrefix) {
|
|
3862
|
+
return withPlain(schema, v, lines, ctx, () => genCodeENode(schema, v, pathExpr, lines, ctx, schemaPrefix))
|
|
3863
|
+
}
|
|
3864
|
+
function genCodeENode(schema, v, pathExpr, lines, ctx, schemaPrefix) {
|
|
3734
3865
|
if (!schemaPrefix) schemaPrefix = '#'
|
|
3735
3866
|
// A false schema reports one error at the value that reached it, with the
|
|
3736
3867
|
// keyword the interpreter has always used for it, `not` (lib/plan-compiler.js
|
|
@@ -3842,7 +3973,7 @@ function genCodeE(schema, v, pathExpr, lines, ctx, schemaPrefix) {
|
|
|
3842
3973
|
case 'object': return `(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v}))`
|
|
3843
3974
|
case 'array': return `Array.isArray(${v})`
|
|
3844
3975
|
case 'string': return `typeof ${v}==='string'`
|
|
3845
|
-
case 'number': return `
|
|
3976
|
+
case 'number': return `Number.isFinite(${v})`
|
|
3846
3977
|
case 'integer': return `Number.isInteger(${v})`
|
|
3847
3978
|
case 'boolean': return `typeof ${v}==='boolean'`
|
|
3848
3979
|
case 'null': return `${v}===null`
|
|
@@ -4451,6 +4582,9 @@ function compileToJSCombined(schema, VALID_RESULT, schemaMap, userFormats) {
|
|
|
4451
4582
|
// After type check passes → use optimizations (destructuring, no guards).
|
|
4452
4583
|
// If type check fails → push error, skip property checks (they'd crash).
|
|
4453
4584
|
function genCodeC(schema, v, pathExpr, lines, ctx, schemaPrefix) {
|
|
4585
|
+
return withPlain(schema, v, lines, ctx, () => genCodeCNode(schema, v, pathExpr, lines, ctx, schemaPrefix))
|
|
4586
|
+
}
|
|
4587
|
+
function genCodeCNode(schema, v, pathExpr, lines, ctx, schemaPrefix) {
|
|
4454
4588
|
if (!schemaPrefix) schemaPrefix = '#'
|
|
4455
4589
|
// Per-node, for the reason spelled out in genCode.
|
|
4456
4590
|
let ppHandledPropertyNames = false
|
|
@@ -4576,7 +4710,7 @@ function genCodeC(schema, v, pathExpr, lines, ctx, schemaPrefix) {
|
|
|
4576
4710
|
case 'object': return `(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v}))`
|
|
4577
4711
|
case 'array': return `Array.isArray(${v})`
|
|
4578
4712
|
case 'string': return `typeof ${v}==='string'`
|
|
4579
|
-
case 'number': return `
|
|
4713
|
+
case 'number': return `Number.isFinite(${v})`
|
|
4580
4714
|
case 'integer': return `Number.isInteger(${v})`
|
|
4581
4715
|
case 'boolean': return `typeof ${v}==='boolean'`
|
|
4582
4716
|
case 'null': return `${v}===null`
|
package/lib/plan-compiler.js
CHANGED
|
@@ -182,7 +182,7 @@ function install(deps) {
|
|
|
182
182
|
// no type bit, and an empty schema accepts it. So the numeric keywords
|
|
183
183
|
// must not apply to it either, or `{ minimum: 3 }` rejects a value that
|
|
184
184
|
// `{}` accepts and the compiled engine admits.
|
|
185
|
-
if (run !== null) checks.push((data) =>
|
|
185
|
+
if (run !== null) checks.push((data) => !Number.isFinite(data) || run(data));
|
|
186
186
|
}
|
|
187
187
|
if (P.hasString) {
|
|
188
188
|
const strs = [];
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Generated source from interpreter plans: the verdict function.
|
|
4
|
+
//
|
|
5
|
+
// The interpreted engine is the reference every other engine is tested
|
|
6
|
+
// against, and its Plan is a schema node with every keyword resolved once.
|
|
7
|
+
// lib/plan-compiler.js turns plans into closures; this turns them into source,
|
|
8
|
+
// deciding at compile time exactly what plan-compiler decides, and calling the
|
|
9
|
+
// same helpers (deepEqual, the code point bounds, multipleOfOk, the format
|
|
10
|
+
// functions and compiled patterns the plan already holds), so the answers
|
|
11
|
+
// cannot drift the way three hand-written generators did.
|
|
12
|
+
//
|
|
13
|
+
// The scope is deliberately narrow for now: value-level keywords, properties
|
|
14
|
+
// and additionalProperties, prefixItems and items, and the in-place
|
|
15
|
+
// applicators. Everything else, references, unevaluated*, contains,
|
|
16
|
+
// propertyNames, patternProperties, dependentSchemas, custom keywords, makes
|
|
17
|
+
// the compiler return null. It never emits a function with a keyword missing.
|
|
18
|
+
|
|
19
|
+
const { createInterpreter, _planInternals: I } = require('./interpreter');
|
|
20
|
+
const { Plan, deepEqual, cpAtLeast, cpAtMost, multipleOfOk, T_STRING, T_NUMBER, T_INTEGER, T_BOOLEAN, T_NULL, T_OBJECT, T_ARRAY } = I;
|
|
21
|
+
|
|
22
|
+
const DECLINE = Symbol('plan-source.decline');
|
|
23
|
+
const ALL_TYPES = T_STRING | T_NUMBER | T_INTEGER | T_BOOLEAN | T_NULL | T_OBJECT | T_ARRAY;
|
|
24
|
+
|
|
25
|
+
// The own-property test the generators use; see ownKeyExpr in js-compiler.js.
|
|
26
|
+
const HELPERS = 'const _hop=Object.prototype.hasOwnProperty;';
|
|
27
|
+
|
|
28
|
+
const PROTO_NAMES = new Set([...Object.getOwnPropertyNames(Object.prototype), '__proto__']);
|
|
29
|
+
|
|
30
|
+
function isPrimitive(x) {
|
|
31
|
+
return x === null || typeof x === 'string' || typeof x === 'boolean' || Number.isFinite(x);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function lit(x) {
|
|
35
|
+
return JSON.stringify(x);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function createCtx() {
|
|
39
|
+
return { n: 0, argNames: [], argVals: [], fns: [], fnFor: new Map() };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// A value the emitted code needs by reference: a function, a RegExp, a list.
|
|
43
|
+
function arg(ctx, value) {
|
|
44
|
+
const i = ctx.argVals.indexOf(value);
|
|
45
|
+
if (i !== -1) return ctx.argNames[i];
|
|
46
|
+
const name = '_a' + ctx.argVals.length;
|
|
47
|
+
ctx.argNames.push(name);
|
|
48
|
+
ctx.argVals.push(value);
|
|
49
|
+
return name;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function local(ctx) {
|
|
53
|
+
return '_v' + ctx.n++;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// JSON Schema type bits, exactly as dataBits() assigns them: a non-finite
|
|
57
|
+
// number has no type, so it matches neither number nor integer.
|
|
58
|
+
function typeTest(mask, v) {
|
|
59
|
+
const parts = [];
|
|
60
|
+
if (mask & T_STRING) parts.push(`typeof ${v}==='string'`);
|
|
61
|
+
if (mask & T_NUMBER) parts.push(`Number.isFinite(${v})`);
|
|
62
|
+
else if (mask & T_INTEGER) parts.push(`Number.isInteger(${v})`);
|
|
63
|
+
if (mask & T_BOOLEAN) parts.push(`typeof ${v}==='boolean'`);
|
|
64
|
+
if (mask & T_NULL) parts.push(`${v}===null`);
|
|
65
|
+
if ((mask & T_OBJECT) && (mask & T_ARRAY)) parts.push(`(typeof ${v}==='object'&&${v}!==null)`);
|
|
66
|
+
else if (mask & T_OBJECT) parts.push(`(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v}))`);
|
|
67
|
+
else if (mask & T_ARRAY) parts.push(`Array.isArray(${v})`);
|
|
68
|
+
return parts.length ? parts.join('||') : 'false';
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// `plain` names a local holding `v.__proto__===Object.prototype`, read once per
|
|
72
|
+
// object. With the key a constant at the `in`, the engine keeps the check
|
|
73
|
+
// monomorphic; through a shared helper the key is a parameter and it is not.
|
|
74
|
+
function ownTest(v, key, plain) {
|
|
75
|
+
const k = lit(key);
|
|
76
|
+
return PROTO_NAMES.has(key) ? `_hop.call(${v},${k})` : `(${plain}?${k} in ${v}:_hop.call(${v},${k}))`;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const K_NUM = T_NUMBER | T_INTEGER;
|
|
80
|
+
// The body under a guard for one kind of value, given what the type check
|
|
81
|
+
// already established: unconditional when the value can only be that kind,
|
|
82
|
+
// nothing at all when it cannot be.
|
|
83
|
+
function guarded(known, kind, cond, body) {
|
|
84
|
+
if (!body) return '';
|
|
85
|
+
if ((known & kind) === 0) return '';
|
|
86
|
+
if ((known & ~kind) === 0) return body;
|
|
87
|
+
return `if(${cond}){${body}}`;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Value-level keywords, in compileLeafV's order and with its meaning.
|
|
91
|
+
function emitLeaf(ctx, P, v, known) {
|
|
92
|
+
let out = '';
|
|
93
|
+
if (P.hasType) out += `if(!(${typeTest(P.typeMask, v)}))return false;`;
|
|
94
|
+
if (P.enum !== null) {
|
|
95
|
+
const vals = P.enum;
|
|
96
|
+
if (vals.length === 0) out += 'return false;';
|
|
97
|
+
else if (vals.every(isPrimitive)) out += `if(!(${vals.map((x) => `${v}===${lit(x)}`).join('||')}))return false;`;
|
|
98
|
+
else {
|
|
99
|
+
const a = arg(ctx, vals);
|
|
100
|
+
const de = arg(ctx, deepEqual);
|
|
101
|
+
const i = local(ctx);
|
|
102
|
+
out += `{let ${i}=0;for(;${i}<${a}.length;${i}++)if(${de}(${a}[${i}],${v}))break;if(${i}===${a}.length)return false}`;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
if (P.hasConst) {
|
|
106
|
+
out += isPrimitive(P.const)
|
|
107
|
+
? `if(${v}!==${lit(P.const)})return false;`
|
|
108
|
+
: `if(!${arg(ctx, deepEqual)}(${arg(ctx, P.const)},${v}))return false;`;
|
|
109
|
+
}
|
|
110
|
+
if (P.hasNumber) {
|
|
111
|
+
let n = '';
|
|
112
|
+
if (P.minimum !== undefined) n += `if(${v}<${lit(P.minimum)})return false;`;
|
|
113
|
+
if (P.maximum !== undefined) n += `if(${v}>${lit(P.maximum)})return false;`;
|
|
114
|
+
if (P.exclusiveMinimum !== undefined) n += `if(${v}<=${lit(P.exclusiveMinimum)})return false;`;
|
|
115
|
+
if (P.exclusiveMaximum !== undefined) n += `if(${v}>=${lit(P.exclusiveMaximum)})return false;`;
|
|
116
|
+
if (P.multipleOf !== undefined) n += `if(!${arg(ctx, multipleOfOk)}(${v},${lit(P.multipleOf)}))return false;`;
|
|
117
|
+
out += guarded(known, K_NUM, `Number.isFinite(${v})`, n);
|
|
118
|
+
}
|
|
119
|
+
if (P.hasString) {
|
|
120
|
+
let s = '';
|
|
121
|
+
if (P.minLength !== undefined) s += `if(!${arg(ctx, cpAtLeast)}(${v},${lit(P.minLength)}))return false;`;
|
|
122
|
+
if (P.maxLength !== undefined) s += `if(!${arg(ctx, cpAtMost)}(${v},${lit(P.maxLength)}))return false;`;
|
|
123
|
+
if (P.pattern !== null) s += `if(!${arg(ctx, P.pattern)}.test(${v}))return false;`;
|
|
124
|
+
if (P.formatFn !== null) s += `if(!${arg(ctx, P.formatFn)}(${v}))return false;`;
|
|
125
|
+
out += guarded(known, T_STRING, `typeof ${v}==='string'`, s);
|
|
126
|
+
}
|
|
127
|
+
if (P.minItems !== undefined || P.maxItems !== undefined || P.uniqueItems) {
|
|
128
|
+
let a = '';
|
|
129
|
+
if (P.minItems !== undefined) a += `if(${v}.length<${lit(P.minItems)})return false;`;
|
|
130
|
+
if (P.maxItems !== undefined) a += `if(${v}.length>${lit(P.maxItems)})return false;`;
|
|
131
|
+
if (P.uniqueItems) {
|
|
132
|
+
const de = arg(ctx, deepEqual);
|
|
133
|
+
const i = local(ctx), j = local(ctx);
|
|
134
|
+
a += `for(let ${i}=0;${i}<${v}.length;${i}++)for(let ${j}=${i}+1;${j}<${v}.length;${j}++)if(${de}(${v}[${i}],${v}[${j}]))return false;`;
|
|
135
|
+
}
|
|
136
|
+
out += guarded(known, T_ARRAY, `Array.isArray(${v})`, a);
|
|
137
|
+
}
|
|
138
|
+
return out;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// The object-level keywords of a node, value-level and structural together, so
|
|
142
|
+
// the prototype of the object is read once for all of them.
|
|
143
|
+
function emitObjectLeaf(ctx, P, v, plain) {
|
|
144
|
+
let o = '';
|
|
145
|
+
if (P.required !== null || P.minProperties !== undefined || P.maxProperties !== undefined || P.dependentRequired !== null) {
|
|
146
|
+
if (P.required !== null) for (const k of P.required) o += `if(!${ownTest(v, k, plain)})return false;`;
|
|
147
|
+
if (P.minProperties !== undefined) o += `if(Object.keys(${v}).length<${lit(P.minProperties)})return false;`;
|
|
148
|
+
if (P.maxProperties !== undefined) o += `if(Object.keys(${v}).length>${lit(P.maxProperties)})return false;`;
|
|
149
|
+
if (P.dependentRequired !== null) {
|
|
150
|
+
for (const [key, deps] of P.dependentRequired) {
|
|
151
|
+
o += `if(${ownTest(v, key, plain)}){${deps.map((dep) => `if(!${ownTest(v, dep, plain)})return false;`).join('')}}`;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return o;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// A subschema asked for a verdict of its own (a branch of anyOf/oneOf, the
|
|
159
|
+
// subject of not/if) becomes a named function, emitted once per plan.
|
|
160
|
+
function branchFn(ctx, node) {
|
|
161
|
+
if (node === true) return '_t';
|
|
162
|
+
if (node === false) return '_f';
|
|
163
|
+
let name = ctx.fnFor.get(node);
|
|
164
|
+
if (name !== undefined) return name;
|
|
165
|
+
name = '_b' + ctx.fnFor.size;
|
|
166
|
+
ctx.fnFor.set(node, name);
|
|
167
|
+
const body = emitNode(ctx, node, 'd');
|
|
168
|
+
ctx.fns.push(`function ${name}(d){${body}return true}`);
|
|
169
|
+
return name;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function emitNode(ctx, node, v) {
|
|
173
|
+
if (node === true) return '';
|
|
174
|
+
if (node === false) return 'return false;';
|
|
175
|
+
if (!(node instanceof Plan)) return '';
|
|
176
|
+
const P = node;
|
|
177
|
+
if (P.tracked || P.hasUnevaluated || P.hasCustom || P.macros !== null ||
|
|
178
|
+
P.contains !== undefined || P.propertyNames !== undefined || P.patternProperties !== null ||
|
|
179
|
+
P.dependentSchemas !== null || P.propertyDependencies !== null) {
|
|
180
|
+
throw DECLINE;
|
|
181
|
+
}
|
|
182
|
+
const known = P.hasType ? P.typeMask : ALL_TYPES;
|
|
183
|
+
let out = emitLeaf(ctx, P, v, known);
|
|
184
|
+
|
|
185
|
+
if (P.prefixItems !== null || P.items !== undefined) {
|
|
186
|
+
let a = '';
|
|
187
|
+
if (P.prefixItems !== null) {
|
|
188
|
+
P.prefixItems.forEach((child, i) => {
|
|
189
|
+
const x = local(ctx);
|
|
190
|
+
const body = emitNode(ctx, child, x);
|
|
191
|
+
if (body) a += `if(${v}.length>${i}){const ${x}=${v}[${i}];${body}}`;
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
if (P.items !== undefined) {
|
|
195
|
+
const start = P.prefixItems !== null ? P.prefixItems.length : 0;
|
|
196
|
+
const i = local(ctx);
|
|
197
|
+
const x = local(ctx);
|
|
198
|
+
const body = emitNode(ctx, P.items, x);
|
|
199
|
+
if (body) a += `for(let ${i}=${start};${i}<${v}.length;${i}++){const ${x}=${v}[${i}];${body}}`;
|
|
200
|
+
}
|
|
201
|
+
out += guarded(known, T_ARRAY, `Array.isArray(${v})`, a);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const plain = local(ctx);
|
|
205
|
+
let o = emitObjectLeaf(ctx, P, v, plain);
|
|
206
|
+
if (P.properties !== null || P.additionalProperties !== undefined) {
|
|
207
|
+
const declared = P.properties !== null ? [...P.properties.keys()] : [];
|
|
208
|
+
if (P.properties !== null) {
|
|
209
|
+
for (const [key, entry] of P.properties) {
|
|
210
|
+
const x = local(ctx);
|
|
211
|
+
const body = emitNode(ctx, entry.node, x);
|
|
212
|
+
if (body) o += `if(${ownTest(v, key, plain)}){const ${x}=${v}[${lit(key)}];${body}}`;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
const ap = P.additionalProperties;
|
|
216
|
+
if (ap !== undefined && ap !== true) {
|
|
217
|
+
const k = local(ctx);
|
|
218
|
+
let isDeclared = 'false';
|
|
219
|
+
if (declared.length > 8) {
|
|
220
|
+
const set = Object.create(null);
|
|
221
|
+
for (const key of declared) set[key] = 1;
|
|
222
|
+
isDeclared = `${arg(ctx, set)}[${k}]===1`;
|
|
223
|
+
} else if (declared.length > 0) {
|
|
224
|
+
isDeclared = declared.map((key) => `${k}===${lit(key)}`).join('||');
|
|
225
|
+
}
|
|
226
|
+
let apBody;
|
|
227
|
+
if (ap === false) apBody = 'return false;';
|
|
228
|
+
else {
|
|
229
|
+
const x = local(ctx);
|
|
230
|
+
const body = emitNode(ctx, ap, x);
|
|
231
|
+
apBody = body ? `const ${x}=${v}[${k}];${body}` : '';
|
|
232
|
+
}
|
|
233
|
+
if (apBody) o += `for(const ${k} in ${v}){if(!${plain}&&!_hop.call(${v},${k}))continue;if(${isDeclared})continue;${apBody}}`;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
if (o) out += guarded(known, T_OBJECT, `typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})`, `const ${plain}=${v}.__proto__===Object.prototype;${o}`);
|
|
237
|
+
|
|
238
|
+
if (P.allOf !== null) for (const child of P.allOf) out += emitNode(ctx, child, v);
|
|
239
|
+
if (P.anyOf !== null) out += `if(!(${P.anyOf.map((c) => `${branchFn(ctx, c)}(${v})`).join('||')}))return false;`;
|
|
240
|
+
if (P.oneOf !== null) {
|
|
241
|
+
const c = local(ctx);
|
|
242
|
+
out += `{let ${c}=0;${P.oneOf.map((b) => `if(${branchFn(ctx, b)}(${v})&&++${c}>1)return false;`).join('')}if(${c}!==1)return false}`;
|
|
243
|
+
}
|
|
244
|
+
if (P.not !== undefined) out += `if(${branchFn(ctx, P.not)}(${v}))return false;`;
|
|
245
|
+
if (P.if !== undefined) {
|
|
246
|
+
const t = P.then !== undefined ? emitNode(ctx, P.then, v) : '';
|
|
247
|
+
const e = P.else !== undefined ? emitNode(ctx, P.else, v) : '';
|
|
248
|
+
if (t || e) out += `if(${branchFn(ctx, P.if)}(${v})){${t}}else{${e}}`;
|
|
249
|
+
}
|
|
250
|
+
return out;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// Returns { fn, source } or null when the schema is outside what this emits.
|
|
254
|
+
function compileVerdict(schema, options) {
|
|
255
|
+
let interp;
|
|
256
|
+
try {
|
|
257
|
+
interp = createInterpreter(schema, options || {});
|
|
258
|
+
} catch {
|
|
259
|
+
return null;
|
|
260
|
+
}
|
|
261
|
+
const ctx = createCtx();
|
|
262
|
+
let body;
|
|
263
|
+
try {
|
|
264
|
+
body = emitNode(ctx, interp.rootNode, 'd');
|
|
265
|
+
} catch (e) {
|
|
266
|
+
if (e === DECLINE) return null;
|
|
267
|
+
throw e;
|
|
268
|
+
}
|
|
269
|
+
const source = HELPERS + 'function _t(){return true}function _f(){return false}' +
|
|
270
|
+
ctx.fns.join('') + `return function(d){${body}return true}`;
|
|
271
|
+
let fn;
|
|
272
|
+
try {
|
|
273
|
+
// eslint-disable-next-line no-new-func
|
|
274
|
+
fn = new Function(...ctx.argNames, source)(...ctx.argVals);
|
|
275
|
+
} catch {
|
|
276
|
+
return null;
|
|
277
|
+
}
|
|
278
|
+
return { fn, source };
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
module.exports = { compileVerdict };
|
package/lib/tier0.js
CHANGED
|
@@ -137,7 +137,7 @@ function checkPrimitive(c, v) {
|
|
|
137
137
|
if ((f & F_MULT) && !_multipleOk(v, c.multipleOf)) return false;
|
|
138
138
|
}
|
|
139
139
|
} else if (m === T_NUMBER) {
|
|
140
|
-
if (
|
|
140
|
+
if (!Number.isFinite(v)) return false;
|
|
141
141
|
const f = c.numFlags;
|
|
142
142
|
if (f !== 0) {
|
|
143
143
|
if ((f & F_MIN) && v < c.min) return false;
|
|
@@ -194,7 +194,7 @@ function tier0ValidateObject(plan, data) {
|
|
|
194
194
|
if ((f & F_MULT) && !_multipleOk(v, c.multipleOf)) return false;
|
|
195
195
|
}
|
|
196
196
|
} else if (m === T_NUMBER) {
|
|
197
|
-
if (
|
|
197
|
+
if (!Number.isFinite(v)) return false;
|
|
198
198
|
const f = c.numFlags;
|
|
199
199
|
if (f !== 0) {
|
|
200
200
|
if ((f & F_MIN) && v < c.min) return false;
|
package/lib/version.js
CHANGED