ata-validator 1.38.0 → 1.39.2
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 +10 -9
- package/index.d.ts +2 -1
- package/index.js +12 -28
- package/lib/aot-impl.js +40 -15
- package/lib/buffer-gate.js +6 -7
- package/lib/enrich-error.js +4 -2
- package/lib/js-compiler.js +329 -46
- package/lib/rejections.js +84 -39
- package/lib/render-pretty.js +15 -11
- package/lib/safe-regex.js +424 -413
- package/lib/schema-order.js +18 -2
- package/lib/validator-core.js +99 -13
- package/lib/version.js +1 -1
- package/package.json +11 -11
- package/lib/safe-regex-source.js +0 -8
package/lib/js-compiler.js
CHANGED
|
@@ -173,6 +173,7 @@ const DOC_BASE = 'https://ata-validator.com/e/'
|
|
|
173
173
|
// reader sorts by without deriving it from the path. Empty when the path is
|
|
174
174
|
// not into the root document.
|
|
175
175
|
function ordinalField (ctx, schemaPath) {
|
|
176
|
+
if (ctx && ctx.noOrdinal) return ''
|
|
176
177
|
const o = ctx && ctx.rootSchema ? ordinalFor(ctx.rootSchema, unescapeSp(schemaPath)) : null
|
|
177
178
|
return o === null ? '' : `,_o:${o}`
|
|
178
179
|
}
|
|
@@ -198,6 +199,8 @@ function unescapeSp (sp) {
|
|
|
198
199
|
return sp.indexOf('\\') === -1 ? sp : sp.replace(/\\(.)/g, '$1')
|
|
199
200
|
}
|
|
200
201
|
|
|
202
|
+
const FRAME_INLINE_MAX = 120
|
|
203
|
+
|
|
201
204
|
function buildErrorLiteral (opts) {
|
|
202
205
|
const { keyword, format, schemaPath, sourceMap } = opts
|
|
203
206
|
let code = (keyword === 'format' && format) ? codeFor('format', format) : codeFor(keyword)
|
|
@@ -214,10 +217,24 @@ function buildErrorLiteral (opts) {
|
|
|
214
217
|
const line = Array.isArray(hit) ? hit[0] : hit.line
|
|
215
218
|
const col = Array.isArray(hit) ? hit[1] : hit.col
|
|
216
219
|
const text = Array.isArray(hit) ? hit[2] : hit.text
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
220
|
+
if (sourceMap.frames && text.length > FRAME_INLINE_MAX) {
|
|
221
|
+
// A long source line is written once at module scope (see
|
|
222
|
+
// sourceFrameDecls in aot-impl.js) and the literal names it. Inlined,
|
|
223
|
+
// every error site carried the whole line, and a schema kept on one
|
|
224
|
+
// line is one line of all of it: 181 KB minified for a 2 KB schema.
|
|
225
|
+
// Short lines stay inline: gzip folds the repeats, and measured on
|
|
226
|
+
// the 50-field fixture the table cost 1206 gzipped bytes where the
|
|
227
|
+
// inline frames cost 696.
|
|
228
|
+
const key = line + ':' + col
|
|
229
|
+
let f = sourceMap.frames.get(key)
|
|
230
|
+
if (!f) { f = { line, col, text, index: sourceMap.frames.size }; sourceMap.frames.set(key, f) }
|
|
231
|
+
frame = ',schemaSource:__ataSS[' + f.index + ']'
|
|
232
|
+
} else {
|
|
233
|
+
frame = ',schemaSource:Object.freeze({file:' + JSON.stringify(sourceMap.file) +
|
|
234
|
+
',line:' + line +
|
|
235
|
+
',col:' + col +
|
|
236
|
+
',text:' + JSON.stringify(text) + '})'
|
|
237
|
+
}
|
|
221
238
|
}
|
|
222
239
|
}
|
|
223
240
|
return { codeStr: code, docUrl, frame }
|
|
@@ -286,6 +303,9 @@ function compileToJS(schema, defs, schemaMap) {
|
|
|
286
303
|
|
|
287
304
|
if (!defs && needsBaseTracking(schema, schemaMap, new Set())) return null
|
|
288
305
|
if (!defs && externalDocsNeedInterpreter(schema, schemaMap)) return null
|
|
306
|
+
// A $ref cycle on one value recurses here as it does in generated code; see
|
|
307
|
+
// hasSameInstanceRefCycle.
|
|
308
|
+
if (!defs && hasSameInstanceRefCycle(schema)) return null
|
|
289
309
|
|
|
290
310
|
// unevaluatedProperties / unevaluatedItems need annotation tracking this
|
|
291
311
|
// path does not do. Decline so the schema reaches an engine that does.
|
|
@@ -1300,6 +1320,76 @@ function aliasChainEnds(def, defs) {
|
|
|
1300
1320
|
return true
|
|
1301
1321
|
}
|
|
1302
1322
|
|
|
1323
|
+
// When the properties a schema evaluates depend on which subschemas pass,
|
|
1324
|
+
// genUnevaluatedPropertiesNode picks one of a few runtime models: if/then/else,
|
|
1325
|
+
// one anyOf or oneOf, dependentSchemas, or patterns (root, allOf and a lone
|
|
1326
|
+
// if). Each model reads only the keywords it was written for. A schema whose
|
|
1327
|
+
// evaluated set comes from anything else was compiled with that part ignored,
|
|
1328
|
+
// and a lone `if` with properties emitted no check at all, which accepted every
|
|
1329
|
+
// object. This says whether the model the emitter will pick covers every
|
|
1330
|
+
// source of evaluated properties the schema has; when it does not, the
|
|
1331
|
+
// schema is not generated and the interpreter answers.
|
|
1332
|
+
function unevalPropsModeled(schema, schemaMap) {
|
|
1333
|
+
const own = Object.assign({}, schema)
|
|
1334
|
+
delete own.$defs
|
|
1335
|
+
delete own.definitions
|
|
1336
|
+
if (/"\$(?:ref|dynamicRef|recursiveRef)"/.test(JSON.stringify(own))) return false
|
|
1337
|
+
// A subschema whose evaluated names are a fixed list, which is all the
|
|
1338
|
+
// branch, dependentSchemas and if/then/else models can represent.
|
|
1339
|
+
const fixed = (s) => {
|
|
1340
|
+
if (typeof s === 'boolean') return true
|
|
1341
|
+
if (typeof s !== 'object' || s === null) return false
|
|
1342
|
+
const r = collectEvaluated(s, schemaMap)
|
|
1343
|
+
const j = JSON.stringify(s)
|
|
1344
|
+
return !r.dynamic && !r.allProps && !j.includes('"patternProperties"') && !j.includes('"unevaluatedProperties"')
|
|
1345
|
+
}
|
|
1346
|
+
const allOf = Array.isArray(schema.allOf) ? schema.allOf : []
|
|
1347
|
+
for (const sub of allOf) {
|
|
1348
|
+
if (typeof sub === 'boolean') continue
|
|
1349
|
+
if (typeof sub !== 'object' || sub === null) return false
|
|
1350
|
+
const r = collectEvaluated(sub, schemaMap)
|
|
1351
|
+
if (r.dynamic || r.allProps) return false
|
|
1352
|
+
const rest = Object.assign({}, sub)
|
|
1353
|
+
delete rest.patternProperties
|
|
1354
|
+
const j = JSON.stringify(rest)
|
|
1355
|
+
if (j.includes('"patternProperties"') || j.includes('"unevaluatedProperties"')) return false
|
|
1356
|
+
}
|
|
1357
|
+
const patterns = !!schema.patternProperties || allOf.some((s) => s && typeof s === 'object' && s.patternProperties)
|
|
1358
|
+
const branch = schema.anyOf || schema.oneOf
|
|
1359
|
+
if (schema.anyOf && schema.oneOf) return false
|
|
1360
|
+
const hasIf = schema.if !== undefined
|
|
1361
|
+
const hasTE = schema.then !== undefined || schema.else !== undefined
|
|
1362
|
+
const dep = schema.dependentSchemas
|
|
1363
|
+
if (schema.unevaluatedProperties !== false) {
|
|
1364
|
+
// The schema-valued model reads the fixed names and one branch keyword.
|
|
1365
|
+
if (hasIf || hasTE || dep || patterns) return false
|
|
1366
|
+
return !branch || branch.every(fixed)
|
|
1367
|
+
}
|
|
1368
|
+
if (hasIf && hasTE && !branch && !schema.patternProperties && !dep) {
|
|
1369
|
+
if (patterns) return false
|
|
1370
|
+
if (!fixed(schema.if) || (schema.then !== undefined && !fixed(schema.then)) || (schema.else !== undefined && !fixed(schema.else))) return false
|
|
1371
|
+
// The model credits the if's own `properties` names, and only those.
|
|
1372
|
+
const ifNames = typeof schema.if === 'object' ? collectEvaluated(schema.if, schemaMap).props : []
|
|
1373
|
+
const declared = typeof schema.if === 'object' && schema.if.properties ? Object.keys(schema.if.properties) : []
|
|
1374
|
+
return ifNames.length === declared.length && ifNames.every((k) => declared.includes(k))
|
|
1375
|
+
}
|
|
1376
|
+
if (branch) return !hasIf && !hasTE && !dep && !patterns && branch.every(fixed)
|
|
1377
|
+
if (dep) return !hasIf && !hasTE && !patterns && Object.values(dep).every(fixed)
|
|
1378
|
+
if (hasTE) return false
|
|
1379
|
+
if (hasIf) {
|
|
1380
|
+
// A lone if contributes through its top-level patternProperties only.
|
|
1381
|
+
if (typeof schema.if === 'boolean') return true
|
|
1382
|
+
if (typeof schema.if !== 'object' || schema.if === null) return false
|
|
1383
|
+
const r = collectEvaluated(schema.if, schemaMap)
|
|
1384
|
+
if (r.dynamic || r.allProps || r.props.length) return false
|
|
1385
|
+
const rest = Object.assign({}, schema.if)
|
|
1386
|
+
delete rest.patternProperties
|
|
1387
|
+
const j = JSON.stringify(rest)
|
|
1388
|
+
if (j.includes('"patternProperties"') || j.includes('"unevaluatedProperties"')) return false
|
|
1389
|
+
}
|
|
1390
|
+
return true
|
|
1391
|
+
}
|
|
1392
|
+
|
|
1303
1393
|
function codegenSafe(schema, schemaMap) {
|
|
1304
1394
|
if (typeof schema === 'boolean') return true
|
|
1305
1395
|
if (typeof schema !== 'object' || schema === null) return true
|
|
@@ -1416,6 +1506,7 @@ function codegenSafe(schema, schemaMap) {
|
|
|
1416
1506
|
if (schema.unevaluatedProperties === false || (typeof schema.unevaluatedProperties === 'object' && schema.unevaluatedProperties !== null)) {
|
|
1417
1507
|
const evalResult = collectEvaluated(schema, schemaMap)
|
|
1418
1508
|
if (evalResult.dynamic && evalResult.allProps) return false
|
|
1509
|
+
if (evalResult.dynamic && !unevalPropsModeled(schema, schemaMap)) return false
|
|
1419
1510
|
}
|
|
1420
1511
|
}
|
|
1421
1512
|
// unevaluatedItems: allow boolean and schema values
|
|
@@ -1754,8 +1845,85 @@ function externalDocsNeedInterpreter(schema, schemaMap) {
|
|
|
1754
1845
|
return false
|
|
1755
1846
|
}
|
|
1756
1847
|
|
|
1848
|
+
// A local $ref cycle that returns to a schema without moving to another value,
|
|
1849
|
+
// such as `A: { oneOf: [{ $ref: '#/$defs/A' }, ...] }`, never terminates on its
|
|
1850
|
+
// own. The interpreter stops it and answers; generated code recursed until the
|
|
1851
|
+
// stack ran out and threw a RangeError into the caller, on primitives, where
|
|
1852
|
+
// the cycle guard has no object to recognise. Cycles that pass through
|
|
1853
|
+
// properties, items or any other keyword that moves to a child value are
|
|
1854
|
+
// ordinary recursion and are not affected.
|
|
1855
|
+
const SAME_INSTANCE_LISTS = ['allOf', 'anyOf', 'oneOf']
|
|
1856
|
+
const SAME_INSTANCE_ONE = ['not', 'if', 'then', 'else']
|
|
1857
|
+
function localPointerTarget(root, ref) {
|
|
1858
|
+
if (typeof ref !== 'string' || ref.charCodeAt(0) !== 35) return null
|
|
1859
|
+
if (ref === '#') return root
|
|
1860
|
+
if (ref.charCodeAt(1) !== 47) return null
|
|
1861
|
+
let t = root
|
|
1862
|
+
for (let seg of ref.slice(2).split('/')) {
|
|
1863
|
+
if (seg.indexOf('%') >= 0) { try { seg = decodeURIComponent(seg) } catch { return null } }
|
|
1864
|
+
seg = seg.replace(/~1/g, '/').replace(/~0/g, '~')
|
|
1865
|
+
if (t === null || typeof t !== 'object') return null
|
|
1866
|
+
if (!Object.prototype.hasOwnProperty.call(t, seg)) {
|
|
1867
|
+
const alias = seg === 'definitions' ? '$defs' : seg === '$defs' ? 'definitions' : null
|
|
1868
|
+
if (alias === null || !Object.prototype.hasOwnProperty.call(t, alias)) return null
|
|
1869
|
+
seg = alias
|
|
1870
|
+
}
|
|
1871
|
+
t = t[seg]
|
|
1872
|
+
}
|
|
1873
|
+
return t !== null && typeof t === 'object' ? t : null
|
|
1874
|
+
}
|
|
1875
|
+
function sameInstanceEdges(root, node) {
|
|
1876
|
+
const out = []
|
|
1877
|
+
for (const k of SAME_INSTANCE_LISTS) if (Array.isArray(node[k])) for (const x of node[k]) if (x && typeof x === 'object') out.push(x)
|
|
1878
|
+
for (const k of SAME_INSTANCE_ONE) if (node[k] && typeof node[k] === 'object') out.push(node[k])
|
|
1879
|
+
if (node.dependentSchemas && typeof node.dependentSchemas === 'object') for (const x of Object.values(node.dependentSchemas)) if (x && typeof x === 'object') out.push(x)
|
|
1880
|
+
if (typeof node.$ref === 'string') { const t = localPointerTarget(root, node.$ref); if (t) out.push(t) }
|
|
1881
|
+
return out
|
|
1882
|
+
}
|
|
1883
|
+
const _sameInstanceCycleCache = new WeakMap()
|
|
1884
|
+
function hasSameInstanceRefCycle(root) {
|
|
1885
|
+
if (root === null || typeof root !== 'object') return false
|
|
1886
|
+
const hit = _sameInstanceCycleCache.get(root)
|
|
1887
|
+
if (hit !== undefined) return hit
|
|
1888
|
+
let found = false
|
|
1889
|
+
if (JSON.stringify(root).includes('"$ref"')) {
|
|
1890
|
+
// Only what validation can reach: definitions are entered through the
|
|
1891
|
+
// references that name them, so a broken definition nothing uses (a real
|
|
1892
|
+
// SchemaStore schema has one that includes itself) does not cost the
|
|
1893
|
+
// whole schema its generated code.
|
|
1894
|
+
const nodes = []
|
|
1895
|
+
const seen = new Set()
|
|
1896
|
+
const collect = (n) => {
|
|
1897
|
+
if (n === null || typeof n !== 'object' || seen.has(n)) return
|
|
1898
|
+
seen.add(n)
|
|
1899
|
+
if (Array.isArray(n)) { for (const v of n) collect(v); return }
|
|
1900
|
+
nodes.push(n)
|
|
1901
|
+
for (const [k, v] of Object.entries(n)) {
|
|
1902
|
+
if (k === '$defs' || k === 'definitions' || k === 'enum' || k === 'const' || k === 'default' || k === 'examples') continue
|
|
1903
|
+
collect(v)
|
|
1904
|
+
}
|
|
1905
|
+
if (typeof n.$ref === 'string') collect(localPointerTarget(root, n.$ref))
|
|
1906
|
+
}
|
|
1907
|
+
collect(root)
|
|
1908
|
+
const state = new Map() // 1 on the stack, 2 done
|
|
1909
|
+
const visit = (n) => {
|
|
1910
|
+
const st = state.get(n)
|
|
1911
|
+
if (st === 1) return true
|
|
1912
|
+
if (st === 2) return false
|
|
1913
|
+
state.set(n, 1)
|
|
1914
|
+
for (const m of sameInstanceEdges(root, n)) if (visit(m)) return true
|
|
1915
|
+
state.set(n, 2)
|
|
1916
|
+
return false
|
|
1917
|
+
}
|
|
1918
|
+
for (const n of nodes) if (visit(n)) { found = true; break }
|
|
1919
|
+
}
|
|
1920
|
+
_sameInstanceCycleCache.set(root, found)
|
|
1921
|
+
return found
|
|
1922
|
+
}
|
|
1923
|
+
|
|
1757
1924
|
function sharedCodegenGate(schema, schemaMap) {
|
|
1758
1925
|
if (typeof schema !== 'object' || schema === null) return true
|
|
1926
|
+
if (hasSameInstanceRefCycle(schema)) return false
|
|
1759
1927
|
if (!codegenSafe(schema, schemaMap)) return false
|
|
1760
1928
|
if (needsBaseTracking(schema, schemaMap, new Set())) return false
|
|
1761
1929
|
if (externalDocsNeedInterpreter(schema, schemaMap)) return false
|
|
@@ -2562,8 +2730,14 @@ function genCodeNode(schema, v, lines, ctx, knownType) {
|
|
|
2562
2730
|
// No `properties` means nothing is declared, so every key is additional.
|
|
2563
2731
|
// The generators used to emit nothing for that shape, and the safety gate
|
|
2564
2732
|
// declined it everywhere; at any depth it now compiles.
|
|
2565
|
-
const
|
|
2566
|
-
const
|
|
2733
|
+
const declaredKeys = Object.keys(schema.properties || {})
|
|
2734
|
+
const propCount = declaredKeys.length
|
|
2735
|
+
// Counting keys proves there is no extra key only when every declared
|
|
2736
|
+
// name is required. Comparing the list lengths was not that: with
|
|
2737
|
+
// `properties: {c}` and `required: ['b']` the counts matched, and `{b: 1}`
|
|
2738
|
+
// was accepted although `b` is an additional property.
|
|
2739
|
+
const req = new Set(schema.required || [])
|
|
2740
|
+
const allRequired = !!schema.required && declaredKeys.every((k) => req.has(k))
|
|
2567
2741
|
const inner = allRequired
|
|
2568
2742
|
? (propCount <= 15
|
|
2569
2743
|
? `var _n=0;for(var _k in ${v})_n++;if(_n!==${propCount})return false`
|
|
@@ -2670,8 +2844,12 @@ function genCodeNode(schema, v, lines, ctx, knownType) {
|
|
|
2670
2844
|
}
|
|
2671
2845
|
|
|
2672
2846
|
// anyOf — branch fns hoisted when safe, inline fallback when recursive.
|
|
2673
|
-
//
|
|
2674
|
-
|
|
2847
|
+
// The anyOf model in genUnevaluatedPropertiesNode checks that a branch
|
|
2848
|
+
// passes while it records what the branches evaluate, so anyOf is emitted
|
|
2849
|
+
// once, there. Only that model does: with unevaluatedProperties true or a
|
|
2850
|
+
// schema, skipping it here left anyOf unchecked and accepted a document no
|
|
2851
|
+
// branch matched.
|
|
2852
|
+
if (schema.anyOf && !unevalChecksAnyOf(schema, ctx)) genAnyOfNode(schema, v, lines, ctx, knownType)
|
|
2675
2853
|
|
|
2676
2854
|
// oneOf — branch fns hoisted to factory scope when safe (no recursion/ref).
|
|
2677
2855
|
// Falls back to inline closures if any branch touches recursive validation.
|
|
@@ -3203,6 +3381,12 @@ function genPatternPropertiesNode(schema, v, lines, ctx, knownType, isObj) {
|
|
|
3203
3381
|
return handledPropertyNames
|
|
3204
3382
|
}
|
|
3205
3383
|
|
|
3384
|
+
function unevalChecksAnyOf(schema, ctx) {
|
|
3385
|
+
if (schema.unevaluatedProperties !== false) return false
|
|
3386
|
+
const r = collectEvaluated(schema, ctx.schemaMap, ctx.rootDefs)
|
|
3387
|
+
return r.dynamic && !r.allProps
|
|
3388
|
+
}
|
|
3389
|
+
|
|
3206
3390
|
function genUnevaluatedPropertiesNode(schema, v, lines, ctx, knownType, isObj, hoisted, earlyKeyCount) {
|
|
3207
3391
|
const evalResult = collectEvaluated(schema, ctx.schemaMap, ctx.rootDefs)
|
|
3208
3392
|
|
|
@@ -3481,6 +3665,16 @@ function genUnevaluatedPropertiesNode(schema, v, lines, ctx, knownType, isObj, h
|
|
|
3481
3665
|
const inner = `for(var _k in ${v}){if(${evVar}[_k])continue;${reVars.map(rv => `if(${rv}.test(_k)){${evVar}[_k]=1;continue}`).join('')}return false}`
|
|
3482
3666
|
_deferOrInline(ctx, lines, v, isObj ? inner + '}' : `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){${inner}}}`)
|
|
3483
3667
|
}
|
|
3668
|
+
} else {
|
|
3669
|
+
// Nothing dynamic contributes a name (a lone if with no properties
|
|
3670
|
+
// or patterns, say): only the fixed names are evaluated. This branch
|
|
3671
|
+
// used to emit nothing, and nothing read as valid.
|
|
3672
|
+
const ei = ctx.varCounter++
|
|
3673
|
+
const evVar = `_ev${ei}`
|
|
3674
|
+
lines.push(`{const ${evVar}={}`)
|
|
3675
|
+
for (const k of baseProps) lines.push(`${evVar}[${JSON.stringify(k)}]=1`)
|
|
3676
|
+
const inner = `for(var _k in ${v}){if(!${evVar}[_k])return false}`
|
|
3677
|
+
_deferOrInline(ctx, lines, v, isObj ? inner + '}' : `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){${inner}}}`)
|
|
3484
3678
|
}
|
|
3485
3679
|
}
|
|
3486
3680
|
} else if (typeof schema.unevaluatedProperties === 'object') {
|
|
@@ -4138,7 +4332,7 @@ function compileToJSCodegenWithErrors(schema, schemaMap, userFormats, sourceOpts
|
|
|
4138
4332
|
// closure parameters below the way the other entry points do.
|
|
4139
4333
|
closureVars: [], closureVals: [],
|
|
4140
4334
|
sourceMap: (sourceOpts && sourceOpts.sourceMap && sourceOpts.schemaFile)
|
|
4141
|
-
? { file: sourceOpts.schemaFile, map: sourceOpts.sourceMap }
|
|
4335
|
+
? { file: sourceOpts.schemaFile, map: sourceOpts.sourceMap, frames: sourceOpts.frames || null }
|
|
4142
4336
|
: null }
|
|
4143
4337
|
ctx.helperCode.push('const _cpLen=s=>{let n=0;for(const _ of s)n++;return n}')
|
|
4144
4338
|
const lines = []
|
|
@@ -4194,17 +4388,56 @@ function compileToJSCodegenWithErrors(schema, schemaMap, userFormats, sourceOpts
|
|
|
4194
4388
|
// function and were rebuilt on every call, which on a product schema with
|
|
4195
4389
|
// six URLs and one anyOf cost about 450 ns before a single check ran, more
|
|
4196
4390
|
// than every check and every error together. Here they run once, in a
|
|
4197
|
-
// factory, as the verdict function's preamble does
|
|
4198
|
-
//
|
|
4199
|
-
//
|
|
4200
|
-
//
|
|
4201
|
-
|
|
4202
|
-
|
|
4203
|
-
|
|
4391
|
+
// factory, as the verdict function's preamble does; with $defs or
|
|
4392
|
+
// recursion too, below. `_errSource` stays `body` either way, for the
|
|
4393
|
+
// emitters.
|
|
4394
|
+
// The factory builds the helpers once and returns the error function.
|
|
4395
|
+
// With $defs or recursion the helpers are the definitions' functions and
|
|
4396
|
+
// the cycle guard's state lives beside them; the guarded run resets that
|
|
4397
|
+
// state at its start, and a call made while one is running (a custom
|
|
4398
|
+
// keyword validating with the same validator) gets a function from a fresh
|
|
4399
|
+
// factory call instead of sharing it. `_errFactory` carries the same text
|
|
4400
|
+
// to the standalone emitter.
|
|
4401
|
+
let factoryBody
|
|
4402
|
+
if (needsGuardE) {
|
|
4403
|
+
const attempt = ctx.usesRecursion ? '_validateE(d,_all,_e)' : '_bodyE(d,_all,_e)'
|
|
4404
|
+
const runner = ctx.usesRecursion
|
|
4405
|
+
? emitRootGuard('_validateE', 'd,_all,_e', '') + `function _validateE_b(d,_all,_e){\n ${checkStr}\n }\n `
|
|
4406
|
+
: `function _bodyE(d,_all,_e){\n ${checkStr}\n }\n `
|
|
4407
|
+
factoryBody = guardStrE + helpersE + runner + `return function(d,_all){const _e=[];\n ` +
|
|
4408
|
+
emitGuardedRun(attempt, '_e.length=0;', defSetsE) + `return{valid:_e.length===0,errors:_e}}`
|
|
4409
|
+
} else {
|
|
4410
|
+
factoryBody = helpersE + 'return function(d,_all){const _e=[];\n ' + checkStr +
|
|
4204
4411
|
'\n return{valid:_e.length===0,errors:_e}}'
|
|
4205
|
-
|
|
4206
|
-
|
|
4207
|
-
|
|
4412
|
+
}
|
|
4413
|
+
// For the runtime, the literals take the legacy shape directly. `code` and
|
|
4414
|
+
// `docUrl` are there for standalone modules, which return errors as they
|
|
4415
|
+
// are; the runtime derives both from the keyword when it enriches, and
|
|
4416
|
+
// orders errors by schemaPath, so the literals carried three fields that
|
|
4417
|
+
// every read then copied the error to remove. The patterns are this
|
|
4418
|
+
// generator's own spelling; the collapse helper writes its fields with a
|
|
4419
|
+
// space and keeps them.
|
|
4420
|
+
if (sourceOpts && sourceOpts.runtimeShape) {
|
|
4421
|
+
const shape = (src) => src.replace(/code:'ATA\d{4}',/g, '').replace(/,docUrl:'https:\/\/ata-validator\.com\/e\/ATA\d{4}'/g, '').replace(/,_o:\d+/g, '')
|
|
4422
|
+
body = shape(body)
|
|
4423
|
+
factoryBody = shape(factoryBody)
|
|
4424
|
+
}
|
|
4425
|
+
const hoistable = !ctx.usesRecursion && defSetsE.length === 0 && helpersE !== ''
|
|
4426
|
+
if (hoistable || needsGuardE) {
|
|
4427
|
+
const params = [...cvars, '__ataSafeRe']
|
|
4428
|
+
const args = [...cvals, compileSafe]
|
|
4429
|
+
const make = new Function(...params, factoryBody)
|
|
4430
|
+
const main = make(...args)
|
|
4431
|
+
if (needsGuardE) {
|
|
4432
|
+
let busy = false
|
|
4433
|
+
fn = (d, _all) => {
|
|
4434
|
+
if (busy) return make(...args)(d, _all)
|
|
4435
|
+
busy = true
|
|
4436
|
+
try { return main(d, _all) } finally { busy = false }
|
|
4437
|
+
}
|
|
4438
|
+
} else {
|
|
4439
|
+
fn = main
|
|
4440
|
+
}
|
|
4208
4441
|
} else if (ctx.usesSafeRe) {
|
|
4209
4442
|
// Inlined helperCode references __ataSafeRe; bind the safe-regex factory.
|
|
4210
4443
|
// Standalone keeps the inline _errSource — the embed defines __ataSafeRe.
|
|
@@ -4219,6 +4452,8 @@ function compileToJSCodegenWithErrors(schema, schemaMap, userFormats, sourceOpts
|
|
|
4219
4452
|
fn = new Function('d', '_all', body)
|
|
4220
4453
|
}
|
|
4221
4454
|
fn._errSource = body
|
|
4455
|
+
fn._errFactory = factoryBody
|
|
4456
|
+
fn._errGuarded = needsGuardE
|
|
4222
4457
|
fn._sharedHelpers = ctx.shared ? ctx.shared.slice() : []
|
|
4223
4458
|
fn._usesSafeRe = !!ctx.usesSafeRe
|
|
4224
4459
|
if (cvars.length) {
|
|
@@ -4249,32 +4484,65 @@ function emitBranchCollapse (branches, keyword, v, pathExpr, lines, ctx, schemaP
|
|
|
4249
4484
|
const branchKw = keyword
|
|
4250
4485
|
const branchSp = schemaPrefix + '/' + branchKw
|
|
4251
4486
|
// Per-branch error-collecting functions, each returning {valid,errors}.
|
|
4487
|
+
// They take the value, its instance path, `_all` and, inside a definition
|
|
4488
|
+
// written once for all its references, that reference's schema path, so
|
|
4489
|
+
// they can be built once per call of the error function rather than each
|
|
4490
|
+
// time the keyword is reached: on a list of 1723 tokens that is thousands
|
|
4491
|
+
// of closure arrays per validation.
|
|
4252
4492
|
const fns = []
|
|
4253
4493
|
const titles = []
|
|
4494
|
+
let usesSp = false
|
|
4254
4495
|
for (let i = 0; i < branches.length; i++) {
|
|
4255
4496
|
const sub = branches[i]
|
|
4256
4497
|
const subLines = []
|
|
4257
4498
|
const subSp = branchSp + '/' + i
|
|
4258
4499
|
if (typeof sub === 'object' && sub !== null) {
|
|
4259
|
-
genCodeE(sub, '_bv',
|
|
4500
|
+
genCodeE(sub, '_bv', '_bpth', subLines, ctx, subSp)
|
|
4260
4501
|
} else if (sub === false) {
|
|
4261
4502
|
// A false branch never matches; it reports the same error the
|
|
4262
4503
|
// interpreter attaches to a false schema.
|
|
4263
|
-
subLines.push(`_e.push({keyword:'not',instancePath
|
|
4504
|
+
subLines.push(`_e.push({keyword:'not',instancePath:_bpth,schemaPath:'${subSp}'${ordinalField(ctx, `${subSp}`)},params:{},message:'boolean schema is false'})`)
|
|
4264
4505
|
}
|
|
4265
4506
|
const title = (sub && typeof sub === 'object' && typeof sub.title === 'string') ? sub.title : ''
|
|
4266
4507
|
titles.push(title)
|
|
4267
|
-
|
|
4268
|
-
|
|
4269
|
-
|
|
4508
|
+
let body = subLines.length === 0 ? '' : `const _e=[];${subLines.join(';')};return{valid:_e.length===0,errors:_e}`
|
|
4509
|
+
if (body.includes(SP_PLACEHOLDER)) {
|
|
4510
|
+
body = body.split(`'${SP_PLACEHOLDER}`).join(`_sp+'`).split(`"${SP_PLACEHOLDER}`).join(`_sp+"`)
|
|
4511
|
+
if (body.includes(SP_PLACEHOLDER)) throw DECLINE
|
|
4512
|
+
usesSp = true
|
|
4513
|
+
}
|
|
4270
4514
|
fns.push(body)
|
|
4271
4515
|
}
|
|
4516
|
+
const params = usesSp ? '_bv,_bpth,_all,_sp' : '_bv,_bpth,_all'
|
|
4272
4517
|
const fnArr = `_brf${fi}`
|
|
4273
4518
|
const resArr = `_brr${fi}`
|
|
4274
4519
|
const titleArr = JSON.stringify(titles)
|
|
4275
4520
|
const collapsed = `_brc${fi}`
|
|
4276
4521
|
const pp = pathExpr || '""'
|
|
4277
|
-
|
|
4522
|
+
ctx.helperCode.push(`const ${fnArr}=[${fns.map((b) => b === '' ? `function(){return{valid:true,errors:[]}}` : `function(${params}){${b}}`).join(',')}];const _bt${fi}=${titleArr}`)
|
|
4523
|
+
const call = `${fnArr}[_bi](${v},_pp${fi},_all${usesSp ? ',_sp' : ''})`
|
|
4524
|
+
const ord = (() => { const o = ordinalFor(ctx.rootSchema, branchSp); return o === null ? 'null' : o })()
|
|
4525
|
+
const report = `const ${collapsed}=__ataCollapse('${branchKw}',${resArr},_pp${fi},'${branchSp}',${ord});if(${collapsed}){_e.push(${collapsed});if(!_all)return{valid:false,errors:_e}}`
|
|
4526
|
+
if (keyword === 'anyOf') {
|
|
4527
|
+
// anyOf reports nothing once a branch passes, so the branches are first
|
|
4528
|
+
// only asked whether they pass. They are called with `_all` set to 0, a
|
|
4529
|
+
// third value next to true (collect everything) and false (stop at the
|
|
4530
|
+
// first error, for abortEarly): every check reads it as `!_all` and stops
|
|
4531
|
+
// at its first error, and an anyOf reached that way that fails returns
|
|
4532
|
+
// at once instead of collecting its branches and collapsing them, since
|
|
4533
|
+
// the caller only wants to know. An object tried against a branch that is
|
|
4534
|
+
// an anyOf of primitive types used to collapse that anyOf in full and then
|
|
4535
|
+
// pass the next branch, which on a list of 1723 tokens was 2353 collapses
|
|
4536
|
+
// no one read. Only when no branch passes do they run again with the
|
|
4537
|
+
// caller's `_all`, and the collapse sees them all, as before.
|
|
4538
|
+
const probe = `${fnArr}[_bi](${v},_pp${fi},0${usesSp ? ',_sp' : ''})`
|
|
4539
|
+
lines.push(`{const _pp${fi}=${pp};let _bp${fi}=false;for(let _bi=0;_bi<${fnArr}.length;_bi++){if(${probe}.valid){_bp${fi}=true;break}}if(!_bp${fi}){if(_all===0){_e.push(null);return{valid:false,errors:_e}}const ${resArr}=[];for(let _bi=0;_bi<${fnArr}.length;_bi++){const _br=${call};${resArr}.push({valid:false,errors:_br.errors,title:_bt${fi}[_bi]})}${report}}}`)
|
|
4540
|
+
} else {
|
|
4541
|
+
// Asked only whether it passes (`_all` 0, see anyOf above), oneOf counts
|
|
4542
|
+
// the passing branches and answers; the branch errors of a probe are not
|
|
4543
|
+
// errors anyone reads and are not collapsed.
|
|
4544
|
+
lines.push(`{const _pp${fi}=${pp};const ${resArr}=[];for(let _bi=0;_bi<${fnArr}.length;_bi++){const _br=${call};${resArr}.push({valid:_br.valid,errors:_br.errors,title:_bt${fi}[_bi]})}if(_all===0){let _n=0;for(const _r of ${resArr})if(_r.valid)_n++;if(_n!==1){_e.push(null);return{valid:false,errors:_e}}}else{${report}}}`)
|
|
4545
|
+
}
|
|
4278
4546
|
}
|
|
4279
4547
|
|
|
4280
4548
|
// Error-collecting code generator.
|
|
@@ -4589,7 +4857,10 @@ function genCodeENode(schema, v, pathExpr, lines, ctx, schemaPrefix) {
|
|
|
4589
4857
|
const gateChecks = patChecks.map((c) => c.split(`_k${ci}[_i]`).join(`_f${ci}`))
|
|
4590
4858
|
const gateTest = gateChecks.length ? `!_a${ci}.has(_f${ci})&&!(${gateChecks.join('||')})` : `!_a${ci}.has(_f${ci})`
|
|
4591
4859
|
const detail = `const _k${ci}=Object.keys(${v});for(let _i=0;_i<_k${ci}.length;_i++){if(${isAdditional}){_e.push({code:'${apLit.codeStr}',keyword:'additionalProperties',instancePath:${pathExpr||'""'},schemaPath:'${apSp}'${ordinalField(ctx, `${apSp}`)},params:{additionalProperty:_k${ci}[_i]},message:'must NOT have additional properties',docUrl:'${apLit.docUrl}'${apLit.frame}});if(!_all)return{valid:false,errors:_e}}}`
|
|
4592
|
-
|
|
4860
|
+
// The name set is built once per call of the error function, not at every
|
|
4861
|
+
// object it checks: on a list of 1723 tokens it was 1723 Sets.
|
|
4862
|
+
ctx.helperCode.push(`const _a${ci}=new Set([${allowed}])`)
|
|
4863
|
+
const inner = `let _x${ci}=0;for(const _f${ci} in ${v}){if(${gateTest}){_x${ci}=1;break}}if(_x${ci}){${detail}}`
|
|
4593
4864
|
lines.push(isObj ? `{${inner}}` : `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){${inner}}`)
|
|
4594
4865
|
}
|
|
4595
4866
|
|
|
@@ -4745,7 +5016,8 @@ function genUnevaluatedFalseE(schema, v, pathExpr, lines, ctx, schemaPrefix, isO
|
|
|
4745
5016
|
patChecksU.push(`_re${ctx.regExpMap.get(pattern)}.test(_k${ui}[_i])`)
|
|
4746
5017
|
}
|
|
4747
5018
|
const isUneval = patChecksU.length ? `!_a${ui}.has(_k${ui}[_i])&&!(${patChecksU.join('||')})` : `!_a${ui}.has(_k${ui}[_i])`
|
|
4748
|
-
|
|
5019
|
+
ctx.helperCode.push(`const _a${ui}=new Set([${allowedU}])`)
|
|
5020
|
+
const innerU = `const _k${ui}=Object.keys(${v});for(let _i=0;_i<_k${ui}.length;_i++){if(${isUneval}){_e.push({code:'${upLit.codeStr}',keyword:'unevaluatedProperties',instancePath:${pathExpr||'""'},schemaPath:'${upSp}'${ordinalField(ctx, `${upSp}`)},params:{unevaluatedProperty:_k${ui}[_i]},message:'must NOT have unevaluated properties',docUrl:'${upLit.docUrl}'${upLit.frame}});if(!_all)return{valid:false,errors:_e}}}`
|
|
4749
5021
|
lines.push(isObj ? `{${innerU}}` : `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){${innerU}}`)
|
|
4750
5022
|
}
|
|
4751
5023
|
|
|
@@ -4787,7 +5059,8 @@ function genPatternPropertiesE(schema, v, pathExpr, lines, ctx, schemaPrefix) {
|
|
|
4787
5059
|
function genAdditionalSchemaE(schema, v, pathExpr, lines, ctx, schemaPrefix) {
|
|
4788
5060
|
const ki = ctx.varCounter++
|
|
4789
5061
|
const known = Object.keys(schema.properties || {})
|
|
4790
|
-
|
|
5062
|
+
if (known.length) ctx.helperCode.push(`const _ak${ki}=new Set([${known.map((k) => JSON.stringify(k)).join(',')}])`)
|
|
5063
|
+
const guard = ''
|
|
4791
5064
|
// A key matched by any patternProperties entry is not additional.
|
|
4792
5065
|
const patTests = []
|
|
4793
5066
|
for (const pat of Object.keys(schema.patternProperties || {})) {
|
|
@@ -4933,7 +5206,7 @@ function genIfE(schema, v, pathExpr, lines, ctx, schemaPrefix) {
|
|
|
4933
5206
|
// Returns VALID_RESULT for valid data, {valid:false, errors} for invalid.
|
|
4934
5207
|
// Avoids double-pass (jsFn → false → errFn runs same checks again).
|
|
4935
5208
|
// Uses type-aware optimizations: after type check passes, skip guards.
|
|
4936
|
-
function compileToJSCombined(schema, VALID_RESULT, schemaMap, userFormats) {
|
|
5209
|
+
function compileToJSCombined(schema, VALID_RESULT, schemaMap, userFormats, opts) {
|
|
4937
5210
|
const inputSchema = schema
|
|
4938
5211
|
schema = normalizePropertyNames(schema)
|
|
4939
5212
|
// Same provably-local rule as the error generator above.
|
|
@@ -5013,7 +5286,8 @@ function compileToJSCombined(schema, VALID_RESULT, schemaMap, userFormats) {
|
|
|
5013
5286
|
if (hasUnresolvableRef(schema, cRootDefs, cAnchors, schemaMap, new Set())) return null
|
|
5014
5287
|
if (needsBaseTracking(schema, schemaMap, new Set())) return null
|
|
5015
5288
|
|
|
5016
|
-
|
|
5289
|
+
// runtimeShape: errors without the ordinal, see compileToJSCodegenWithErrors.
|
|
5290
|
+
const ctx = { noOrdinal: !!(opts && opts.runtimeShape), varCounter: 0, helperCode: [], shared: [], closureVars: ['_cpLen'], closureVals: [_cpLen],
|
|
5017
5291
|
rootDefs: cRootDefs, refStack: new Set(), schemaMap: schemaMap || null, anchors: cAnchors, rootSchema: inputSchema, userFormats: userFormats || null }
|
|
5018
5292
|
const lines = []
|
|
5019
5293
|
try {
|
|
@@ -5148,7 +5422,7 @@ function genCodeCNode(schema, v, pathExpr, lines, ctx, schemaPrefix) {
|
|
|
5148
5422
|
if (pathVal !== undefined) {
|
|
5149
5423
|
const spVal = unescapeSp(sp)
|
|
5150
5424
|
ctx.closureVars.push(errVar)
|
|
5151
|
-
const o = ordinalFor(ctx.rootSchema, spVal)
|
|
5425
|
+
const o = ctx.noOrdinal ? null : ordinalFor(ctx.rootSchema, spVal)
|
|
5152
5426
|
const lit = {keyword, instancePath: pathVal, schemaPath: spVal, params: Object.freeze(paramsVal), message: msgVal}
|
|
5153
5427
|
if (o !== null) lit._o = o
|
|
5154
5428
|
ctx.closureVals.push(Object.freeze(lit))
|
|
@@ -5160,6 +5434,27 @@ function genCodeCNode(schema, v, pathExpr, lines, ctx, schemaPrefix) {
|
|
|
5160
5434
|
return `(_e||(_e=[])).push({keyword:'${keyword}',instancePath:${pathExpr||'""'},schemaPath:'${sp}',params:${paramsCode},message:${msgCode}${ordinalField(ctx, sp)}})`
|
|
5161
5435
|
}
|
|
5162
5436
|
|
|
5437
|
+
// enum and const apply to a value of any type, so a value that fails
|
|
5438
|
+
// `type` is still checked against them, as the other engines do. They sat
|
|
5439
|
+
// inside the type-success block, and `{type: 'number', enum: [1]}` reported
|
|
5440
|
+
// only `type` for 'x' where the interpreter reports both.
|
|
5441
|
+
const emitEnumConst = () => {
|
|
5442
|
+
// enum
|
|
5443
|
+
if (schema.enum) {
|
|
5444
|
+
lines.push(`if(!(${enumCondition(ctx, schema.enum, v)})){${fail('enum', 'enum', `{allowedValues:${JSON.stringify(schema.enum)}}`, "'must be equal to one of the allowed values'")}}`)
|
|
5445
|
+
}
|
|
5446
|
+
|
|
5447
|
+
// const
|
|
5448
|
+
if (schema.const !== undefined) {
|
|
5449
|
+
const cv = schema.const
|
|
5450
|
+
if (cv === null || typeof cv !== 'object') {
|
|
5451
|
+
lines.push(`if(${v}!==${JSON.stringify(cv)}){${fail('const', 'const', `{allowedValue:${JSON.stringify(schema.const)}}`, "'must be equal to constant'")}}`)
|
|
5452
|
+
} else {
|
|
5453
|
+
lines.push(`if(!${emitDeq(ctx)}(${v},${emitConstant(ctx, cv)})){${fail('const', 'const', `{allowedValue:JSON.parse(${JSON.stringify(JSON.stringify(schema.const))})}`, "'must be equal to constant'")}}`)
|
|
5454
|
+
}
|
|
5455
|
+
}
|
|
5456
|
+
}
|
|
5457
|
+
|
|
5163
5458
|
if (types) {
|
|
5164
5459
|
const conds = types.map(t => {
|
|
5165
5460
|
switch (t) {
|
|
@@ -5186,22 +5481,10 @@ function genCodeCNode(schema, v, pathExpr, lines, ctx, schemaPrefix) {
|
|
|
5186
5481
|
isStr = types[0] === 'string'
|
|
5187
5482
|
isNum = types[0] === 'number' || types[0] === 'integer'
|
|
5188
5483
|
}
|
|
5484
|
+
emitEnumConst()
|
|
5189
5485
|
lines.push(`if(${typeOk}){`)
|
|
5190
|
-
}
|
|
5191
|
-
|
|
5192
|
-
// enum
|
|
5193
|
-
if (schema.enum) {
|
|
5194
|
-
lines.push(`if(!(${enumCondition(ctx, schema.enum, v)})){${fail('enum', 'enum', `{allowedValues:${JSON.stringify(schema.enum)}}`, "'must be equal to one of the allowed values'")}}`)
|
|
5195
|
-
}
|
|
5196
|
-
|
|
5197
|
-
// const
|
|
5198
|
-
if (schema.const !== undefined) {
|
|
5199
|
-
const cv = schema.const
|
|
5200
|
-
if (cv === null || typeof cv !== 'object') {
|
|
5201
|
-
lines.push(`if(${v}!==${JSON.stringify(cv)}){${fail('const', 'const', `{allowedValue:${JSON.stringify(schema.const)}}`, "'must be equal to constant'")}}`)
|
|
5202
|
-
} else {
|
|
5203
|
-
lines.push(`if(!${emitDeq(ctx)}(${v},${emitConstant(ctx, cv)})){${fail('const', 'const', `{allowedValue:JSON.parse(${JSON.stringify(JSON.stringify(schema.const))})}`, "'must be equal to constant'")}}`)
|
|
5204
|
-
}
|
|
5486
|
+
} else {
|
|
5487
|
+
emitEnumConst()
|
|
5205
5488
|
}
|
|
5206
5489
|
|
|
5207
5490
|
// required — use destructuring when type is object (SAFE because type check already passed)
|