ata-validator 1.51.0 → 1.53.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.
@@ -36,7 +36,7 @@ function uqSources () {
36
36
  // that would have written them.
37
37
  const SHARED_RT = {
38
38
  _deqHoisted: ['_deq'], _peHoisted: ['_pe'],
39
- _own_PN: ['_PN', '_OP', '_gp'], _own_hop: ['_hop'], _own_h: ['_h'], _own_ok: ['_ok'], _own_hall: ['_hall'],
39
+ _own_PN: ['_PN', '_OP', '_gp'], _own_hop: ['_hop'], _own_h: ['_h'], _own_ok: ['_ok'], _own_hall: ['_hall'], _own_nk: ['_nk'],
40
40
  }
41
41
  let _rt = null
42
42
  function sharedRt () { return _rt || (_rt = require('./combined-runtime')) }
@@ -96,8 +96,8 @@ function apMembershipCheck (ctx, names, v) {
96
96
  }
97
97
  }
98
98
  // With nothing declared every key is additional; `if()` would not parse.
99
- if (names.length === 0) return `for(var _k in ${v})return false`
100
- return `for(var _k in ${v})if(${names.map((k) => `_k!==${JSON.stringify(k)}`).join('&&')})return false`
99
+ if (names.length === 0) return `for(var _k in ${v})if(${ownFail(ctx, v)})return false`
100
+ return `for(var _k in ${v})if(${names.map((k) => `_k!==${JSON.stringify(k)}`).join('&&')}&&${ownFail(ctx, v)})return false`
101
101
  }
102
102
 
103
103
  function emitDeq(ctx) {
@@ -376,7 +376,9 @@ function compileToJS(schema, defs, schemaMap) {
376
376
  if (!propCheck) return null // bail if sub-schema too complex
377
377
  checks.push((d) => {
378
378
  if (typeof d !== 'object' || d === null || !hasOwnKey(d, key)) return true
379
- return propCheck(d[key])
379
+ // An undefined value is not checked, as the generated code does not.
380
+ const v = d[key]
381
+ return v === undefined || propCheck(v)
380
382
  })
381
383
  }
382
384
  }
@@ -907,8 +909,9 @@ const OWN_HELPER_CODE = {
907
909
  _h: 'function _h(o,k){return _hop.call(o,k)}',
908
910
  _ok: 'function _ok(o,k){return (_PN?o.__proto__:_gp(o))===_OP||_hop.call(o,k)}',
909
911
  _hall: 'function _hall(o,ks){for(let i=0;i<ks.length;i++)if(!_hop.call(o,ks[i]))return false;return true}',
912
+ _nk: 'function _nk(o,n){return (_PN?o.__proto__:_gp(o))===_OP||Object.keys(o).length!==n}',
910
913
  }
911
- const OWN_HELPER_DEPS = { _h: ['_hop'], _ok: ['_PN', '_hop'], _hall: ['_hop'] }
914
+ const OWN_HELPER_DEPS = { _h: ['_hop'], _ok: ['_PN', '_hop'], _hall: ['_hop'], _nk: ['_PN'] }
912
915
  function useOwnHelper (ctx, name) {
913
916
  for (const dep of OWN_HELPER_DEPS[name] || []) useOwnHelper(ctx, dep)
914
917
  hoistOnce(ctx, '_own' + name, OWN_HELPER_CODE[name])
@@ -1208,6 +1211,17 @@ function propFnCandidate (prop, ctx) {
1208
1211
  // inline after all": an empty body (the inline path then asserts it the
1209
1212
  // usual way) or one that calls the recursive entry, which is out of scope
1210
1213
  // in the preamble.
1214
+ // Generates a body that will become a function of its own in the preamble.
1215
+ // The ordinary-object flags withPlain declared belong to the function being
1216
+ // written now and are out of scope there; a hoisted body takes the same
1217
+ // parameter names (`d`, `_av`, `_ov`) and would otherwise read a flag the
1218
+ // enclosing node declared for a value of that name.
1219
+ function genIsolated (ctx, run) {
1220
+ const outerPlain = ctx.plainOf
1221
+ ctx.plainOf = undefined
1222
+ try { return run() } finally { ctx.plainOf = outerPlain }
1223
+ }
1224
+
1211
1225
  function emitPropFn (prop, ctx) {
1212
1226
  const bodyLines = []
1213
1227
  const outerDeferred = ctx.deferredChecks
@@ -1216,7 +1230,7 @@ function emitPropFn (prop, ctx) {
1216
1230
  ctx.plainReuse = undefined
1217
1231
  const outerBytes = ctx.fnBytes
1218
1232
  ctx.fnBytes = 0
1219
- try { genCode(prop, 'd', bodyLines, ctx) } finally { ctx.plainReuse = outerReuse; ctx.fnBytes = outerBytes }
1233
+ try { genIsolated(ctx, () => genCode(prop, 'd', bodyLines, ctx)) } finally { ctx.plainReuse = outerReuse; ctx.fnBytes = outerBytes }
1220
1234
  if (ctx.deferredChecks) for (const dc of ctx.deferredChecks) bodyLines.push(dc)
1221
1235
  ctx.deferredChecks = outerDeferred
1222
1236
  if (bodyLines.length === 0 || bodyLines.some((l) => /\b_validate\b/.test(l))) return null
@@ -1341,6 +1355,54 @@ function sharedDefNames (root, defs) {
1341
1355
  // known trade: on that shape the shared function costs 27 ns a call.
1342
1356
  const SHARED_DEF_MIN_CHARS = 120
1343
1357
 
1358
+ // A small definition applied at many places is called with objects of many
1359
+ // shapes, one or two per place, and V8 gives a function's property reads up
1360
+ // to four shapes before it falls back to a generic lookup several times
1361
+ // slower: SchemaStore's detekt sends 72 shapes through one four-property rule
1362
+ // definition. Such a definition is emitted as several functions with the same
1363
+ // body, each serving at most SITES_PER_COPY references, so every copy stays
1364
+ // within four shapes. Only small bodies are copied, so the source grows by a
1365
+ // bounded amount; a large or rarely referenced definition keeps one function.
1366
+ const SITES_PER_COPY = 4
1367
+ const COPY_MIN_SITES = 16
1368
+ const COPY_MAX_BODY = 1500
1369
+ function sharedDefCopy (ctx, defName, fnName) {
1370
+ const body = ctx.sharedDefBody && ctx.sharedDefBody.get(fnName)
1371
+ if (body === undefined || body.length > COPY_MAX_BODY) return fnName
1372
+ // Only a definition that reads properties gains: a string, a number or a
1373
+ // wrapper around another reference has no shapes to keep apart, and more
1374
+ // copies only mean more functions to warm up.
1375
+ const def = ctx.rootDefs && ctx.rootDefs[defName]
1376
+ if (def === null || typeof def !== 'object' || typeof def.properties !== 'object' || def.properties === null) return fnName
1377
+ if (!ctx.sharedDefSites) ctx.sharedDefSites = new Map()
1378
+ if (ctx.sharedDefSiteCounts === undefined) ctx.sharedDefSiteCounts = refCounts(ctx.rootSchema)
1379
+ if ((ctx.sharedDefSiteCounts.get(defName) || 0) < COPY_MIN_SITES) return fnName
1380
+ const site = ctx.sharedDefSites.get(fnName) || 0
1381
+ ctx.sharedDefSites.set(fnName, site + 1)
1382
+ const copy = Math.floor(site / SITES_PER_COPY)
1383
+ if (copy === 0) return fnName
1384
+ const name = `${fnName}_c${copy}`
1385
+ if (site % SITES_PER_COPY === 0) hoist(ctx, `function ${name}(d){${body}\n return true}`)
1386
+ return name
1387
+ }
1388
+
1389
+ // How many `$ref`s name each local definition, by name.
1390
+ function refCounts (root) {
1391
+ const counts = new Map()
1392
+ const seen = new WeakSet()
1393
+ const walk = (node) => {
1394
+ if (node === null || typeof node !== 'object' || seen.has(node)) return
1395
+ seen.add(node)
1396
+ if (typeof node.$ref === 'string') {
1397
+ const m = /^#\/(?:\$defs|definitions)\/([^/]+)$/.exec(node.$ref)
1398
+ if (m) counts.set(m[1], (counts.get(m[1]) || 0) + 1)
1399
+ }
1400
+ for (const v of Object.values(node)) walk(v)
1401
+ }
1402
+ walk(root)
1403
+ return counts
1404
+ }
1405
+
1344
1406
  function cyclicDefNames (defs) {
1345
1407
  const out = new Set()
1346
1408
  if (!defs || typeof defs !== 'object') return out
@@ -3419,7 +3481,7 @@ function genCodeNode(schema, v, lines, ctx, knownType) {
3419
3481
  if (allRequired && propCount > 0) {
3420
3482
  // Adaptive: for-in for <=15 keys (V8 fast path), Object.keys for >15
3421
3483
  if (propCount <= 15) {
3422
- lines.push(`var _n=0;for(var _k in ${v})_n++;if(_n!==${propCount})return false`)
3484
+ lines.push(`var _n=0;for(var _k in ${v})_n++;if(_n!==${propCount}&&${ownCountOff(ctx, v, propCount, plainFlag(ctx, v))})return false`)
3423
3485
  } else {
3424
3486
  lines.push(`if(Object.keys(${v}).length!==${propCount})return false`)
3425
3487
  }
@@ -3534,7 +3596,7 @@ function genCodeNode(schema, v, lines, ctx, knownType) {
3534
3596
  if (schema.additionalProperties === false && schema.properties && !schema.patternProperties && ctx.removeNodes && ctx.removeNodes.has(schema)) {
3535
3597
  const kv = `_rk${ctx.varCounter++}`
3536
3598
  const declared = Object.keys(schema.properties)
3537
- let loop = `for(var ${kv} in ${v})if(${declared.map((k) => `${kv}!==${JSON.stringify(k)}`).join('&&')})delete ${v}[${kv}]`
3599
+ let loop = `${ownHead(ctx, v, kv, 'var')}if(${declared.map((k) => `${kv}!==${JSON.stringify(k)}`).join('&&')})delete ${v}[${kv}]`
3538
3600
  // When every declared property is required, the verdict has already
3539
3601
  // established they are all there, so a key count equal to the declared
3540
3602
  // count means nothing is extra and the deleting walk, which compares every
@@ -3542,7 +3604,7 @@ function genCodeNode(schema, v, lines, ctx, knownType) {
3542
3604
  const req = new Set(schema.required || [])
3543
3605
  if (declared.length <= 15 && declared.every((k) => req.has(k))) {
3544
3606
  const n = `_rn${ctx.varCounter++}`
3545
- loop = `{var ${n}=0;for(var ${kv} in ${v})${n}++;if(${n}!==${declared.length}){${loop}}}`
3607
+ loop = `{var ${n}=0;${ownHead(ctx, v, kv, 'var')}${n}++;if(${n}!==${declared.length}){${loop}}}`
3546
3608
  }
3547
3609
  _deferOrInline(ctx, lines, v, isObj ? loop : `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){${loop}}`)
3548
3610
  } else if (schema.additionalProperties === false && !schema.patternProperties) {
@@ -3559,7 +3621,7 @@ function genCodeNode(schema, v, lines, ctx, knownType) {
3559
3621
  const allRequired = !!schema.required && declaredKeys.every((k) => req.has(k))
3560
3622
  const inner = allRequired
3561
3623
  ? (propCount <= 15
3562
- ? `var _n=0;for(var _k in ${v})_n++;if(_n!==${propCount})return false`
3624
+ ? `var _n=0;for(var _k in ${v})_n++;if(_n!==${propCount}&&${ownCountOff(ctx, v, propCount, plainFlag(ctx, v))})return false`
3563
3625
  : `if(Object.keys(${v}).length!==${propCount})return false`)
3564
3626
  : apMembershipCheck(ctx, Object.keys(schema.properties || {}), v)
3565
3627
  _deferOrInline(ctx, lines, v, isObj ? inner : `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){${inner}}`)
@@ -3590,8 +3652,8 @@ function genCodeNode(schema, v, lines, ctx, knownType) {
3590
3652
  // propertyNames: false rejects any key at all; true constrains nothing.
3591
3653
  if (schema.propertyNames === false) {
3592
3654
  lines.push(isObj
3593
- ? `for(const _k in ${v})return false`
3594
- : `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){for(const _k in ${v})return false}`)
3655
+ ? `${ownHead(ctx, v, `_k`, 'const')}return false`
3656
+ : `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){${ownHead(ctx, v, `_k`, 'const')}return false}`)
3595
3657
  }
3596
3658
 
3597
3659
  // propertyNames — only emit if not already merged into patternProperties loop
@@ -3779,8 +3841,8 @@ function genAdditionalSchemaNode(schema, v, lines, ctx, knownType, isObj) {
3779
3841
  if (subLines.length > 0) {
3780
3842
  const body = subLines.join(';')
3781
3843
  const loop = skipCheck
3782
- ? `for(var ${kVar} in ${v}){if(${skipCheck})continue;const ${avVar}=${v}[${kVar}];${body}}`
3783
- : `for(var ${kVar} in ${v}){const ${avVar}=${v}[${kVar}];${body}}`
3844
+ ? `${ownHead(ctx, v, kVar, 'var')}{if(${skipCheck})continue;const ${avVar}=${v}[${kVar}];${body}}`
3845
+ : `${ownHead(ctx, v, kVar, 'var')}{const ${avVar}=${v}[${kVar}];${body}}`
3784
3846
  _deferOrInline(ctx, lines, v, isObj ? loop : `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){${loop}}`)
3785
3847
  }
3786
3848
  }
@@ -3804,10 +3866,10 @@ function genPropertyNamesNode(schema, v, lines, ctx, knownType, isObj) {
3804
3866
  const sub = []
3805
3867
  genCode(pn, '_pv', sub, ctx)
3806
3868
  if (sub.length === 0) return
3807
- lines.push(`${guard}{const _pnf${ki}=function(_pv){${sub.join(';')};return true};for(const _k${ki} in ${v}){if(!_pnf${ki}(_k${ki}))return false}}`)
3869
+ lines.push(`${guard}{const _pnf${ki}=function(_pv){${sub.join(';')};return true};${ownHead(ctx, v, `_k${ki}`, 'const')}{if(!_pnf${ki}(_k${ki}))return false}}`)
3808
3870
  return
3809
3871
  }
3810
- lines.push(`${guard}{for(const _k${ki} in ${v}){`)
3872
+ lines.push(`${guard}{${ownHead(ctx, v, `_k${ki}`, 'const')}{`)
3811
3873
  if (pn.minLength !== undefined) lines.push(`if(_k${ki}.length<${pn.minLength})return false`)
3812
3874
  if (pn.maxLength !== undefined) lines.push(`if(_k${ki}.length>${pn.maxLength})return false`)
3813
3875
  if (pn.pattern) {
@@ -3878,7 +3940,8 @@ function genAnyOfNode(schema, v, lines, ctx, knownType) {
3878
3940
  continue
3879
3941
  }
3880
3942
  const subLines = []
3881
- genCode(sub, '_av', subLines, ctx)
3943
+ if (canHoist) genIsolated(ctx, () => genCode(sub, '_av', subLines, ctx))
3944
+ else genCode(sub, '_av', subLines, ctx)
3882
3945
  const body = subLines.length === 0 ? 'return true' : `${subLines.join(';')};return true`
3883
3946
  if (/\b_validate\b/.test(body)) canHoist = false
3884
3947
  branchBodies.push(body)
@@ -3916,6 +3979,35 @@ function branchKey (sub, ctx) {
3916
3979
  return sub
3917
3980
  }
3918
3981
 
3982
+ // A oneOf whose branches each allow only their own properties, the way
3983
+ // Traefik's middlewares and many tagged unions are written: every branch is
3984
+ // `{ properties: {...}, additionalProperties: false }` (an object `type` and
3985
+ // annotations aside), and no property belongs to two branches. Then an
3986
+ // object passes a branch only if every key it has is that branch's, so its
3987
+ // first key names the one branch that can pass, and an object with no keys
3988
+ // passes all of them. Returns the branch index per property name, or null.
3989
+ const KEYED_BRANCH_KEYS = new Set(['properties', 'additionalProperties', 'type', 'title', 'description', '$comment'])
3990
+ function keyedBranches (branches) {
3991
+ if (branches.length < 4) return null
3992
+ const owner = new Map()
3993
+ for (let i = 0; i < branches.length; i++) {
3994
+ const b = branches[i]
3995
+ if (typeof b !== 'object' || b === null || Array.isArray(b)) return null
3996
+ for (const k of Object.keys(b)) if (!KEYED_BRANCH_KEYS.has(k)) return null
3997
+ if (b.additionalProperties !== false) return null
3998
+ if (b.type !== undefined && b.type !== 'object') return null
3999
+ const props = b.properties
4000
+ if (typeof props !== 'object' || props === null || Array.isArray(props)) return null
4001
+ const names = Object.keys(props)
4002
+ if (names.length === 0) return null
4003
+ for (const n of names) {
4004
+ if (owner.has(n)) return null
4005
+ owner.set(n, i)
4006
+ }
4007
+ }
4008
+ return owner
4009
+ }
4010
+
3919
4011
  function genOneOfNode(schema, v, lines, ctx, knownType) {
3920
4012
  const fi = ctx.varCounter++
3921
4013
  const branchBodies = []
@@ -3939,7 +4031,8 @@ function genOneOfNode(schema, v, lines, ctx, knownType) {
3939
4031
  continue
3940
4032
  }
3941
4033
  const subLines = []
3942
- genCode(sub, '_ov', subLines, ctx)
4034
+ if (canHoist) genIsolated(ctx, () => genCode(sub, '_ov', subLines, ctx))
4035
+ else genCode(sub, '_ov', subLines, ctx)
3943
4036
  const body = subLines.length === 0 ? 'return true' : `${subLines.join(';')};return true`
3944
4037
  // _validate is the recursive entry — hoisting branches above it breaks scope.
3945
4038
  if (/\b_validate\b/.test(body)) canHoist = false
@@ -3955,6 +4048,17 @@ function genOneOfNode(schema, v, lines, ctx, knownType) {
3955
4048
  return name
3956
4049
  })
3957
4050
  const calls = names.map(n => `if(${n}(${v})){_oc${fi}++;if(_oc${fi}>1)return false}`).join(';')
4051
+ const owner = keyedBranches(schema.oneOf)
4052
+ if (owner !== null) {
4053
+ // An object goes to the branch its first key names; anything else, and
4054
+ // an object with no keys, takes the loop over every branch.
4055
+ const entries = [...owner].map(([k, i]) => `[${JSON.stringify(k)},${names[i]}]`).join(',')
4056
+ hoist(ctx, `const _ofm${fi}=new Map([${entries}])`)
4057
+ lines.push(`{let _ofk${fi};if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){${ownHead(ctx, v, '_q', 'const')}{_ofk${fi}=_q;break}}` +
4058
+ `if(_ofk${fi}!==undefined){const _ob${fi}=_ofm${fi}.get(_ofk${fi});if(_ob${fi}===undefined||!_ob${fi}(${v}))return false}` +
4059
+ `else{let _oc${fi}=0;${calls};if(_oc${fi}!==1)return false}}`)
4060
+ return
4061
+ }
3958
4062
  lines.push(`{let _oc${fi}=0;${calls};if(_oc${fi}!==1)return false}`)
3959
4063
  } else {
3960
4064
  const fns = branchBodies.map(body => `function(_ov){${body}}`)
@@ -4032,7 +4136,7 @@ function genRefNode(schema, v, lines, ctx, knownType, hasSiblings) {
4032
4136
  ctx.deferredChecks = null
4033
4137
  const outerReuse = ctx.plainReuse
4034
4138
  ctx.plainReuse = undefined
4035
- try { genCode(ctx.rootDefs[defName], 'd', bodyLines, ctx) } finally { ctx.plainReuse = outerReuse }
4139
+ try { genIsolated(ctx, () => genCode(ctx.rootDefs[defName], 'd', bodyLines, ctx)) } finally { ctx.plainReuse = outerReuse }
4036
4140
  if (ctx.deferredChecks) for (const dc of ctx.deferredChecks) bodyLines.push(dc)
4037
4141
  ctx.deferredChecks = outerDeferred
4038
4142
  // The def is hoisted above the entry function, where the recursive
@@ -4070,7 +4174,7 @@ function genRefNode(schema, v, lines, ctx, knownType, hasSiblings) {
4070
4174
  const outerReuse = ctx.plainReuse
4071
4175
  ctx.plainReuse = undefined
4072
4176
  try {
4073
- genCode(ctx.rootDefs[defName], 'd', bodyLines, ctx)
4177
+ genIsolated(ctx, () => genCode(ctx.rootDefs[defName], 'd', bodyLines, ctx))
4074
4178
  } finally {
4075
4179
  ctx.refStack.delete(schema.$ref)
4076
4180
  ctx.plainReuse = outerReuse
@@ -4078,9 +4182,12 @@ function genRefNode(schema, v, lines, ctx, knownType, hasSiblings) {
4078
4182
  if (ctx.deferredChecks) for (const dc of ctx.deferredChecks) bodyLines.push(dc)
4079
4183
  ctx.deferredChecks = outerDeferred
4080
4184
  if (bodyLines.some((l) => /\b_validate\b/.test(l))) throw DECLINE
4081
- hoist(ctx, `function ${fnName}(d){${bodyLines.join('\n ')}\n return true}`)
4185
+ const body = bodyLines.join('\n ')
4186
+ hoist(ctx, `function ${fnName}(d){${body}\n return true}`)
4187
+ if (!ctx.sharedDefBody) ctx.sharedDefBody = new Map()
4188
+ ctx.sharedDefBody.set(fnName, body)
4082
4189
  }
4083
- lines.push(`if(!${fnName}(${v}))return false`)
4190
+ lines.push(`if(!${sharedDefCopy(ctx, defName, fnName)}(${v}))return false`)
4084
4191
  if (!hasSiblings) return true
4085
4192
  } else if (ctx.refStack.has(schema.$ref)) { if (ctx.nestedBoolean) throw DECLINE; if (!hasSiblings) return true }
4086
4193
  else {
@@ -4177,7 +4284,7 @@ function genPatternPropertiesNode(schema, v, lines, ctx, knownType, isObj) {
4177
4284
  genCode(apSchema, `_apv${pi}`, apLines, ctx)
4178
4285
  apCheck = apLines.join(';')
4179
4286
  }
4180
- lines.push(`${guard}{for(const ${kVar} in ${v}){`)
4287
+ lines.push(`${guard}{${ownHead(ctx, v, kVar, 'const')}{`)
4181
4288
  // propertyNames checks (merged into same loop)
4182
4289
  if (pn) {
4183
4290
  if (pn.minLength !== undefined) lines.push(`if(${kVar}.length<${pn.minLength})return false`)
@@ -4228,7 +4335,7 @@ function genPatternPropertiesNode(schema, v, lines, ctx, knownType, isObj) {
4228
4335
  } else {
4229
4336
  // No additionalProperties: validate matching keys + propertyNames
4230
4337
  handledPropertyNames = !!pn
4231
- lines.push(`${guard}{for(const ${kVar} in ${v}){`)
4338
+ lines.push(`${guard}{${ownHead(ctx, v, kVar, 'const')}{`)
4232
4339
  // propertyNames checks (merged)
4233
4340
  if (pn) {
4234
4341
  if (pn.minLength !== undefined) lines.push(`if(${kVar}.length<${pn.minLength})return false`)
@@ -4307,7 +4414,7 @@ function genUnevaluatedPropertiesNode(schema, v, lines, ctx, knownType, isObj, h
4307
4414
  if (!earlyKeyCount) {
4308
4415
  // Adaptive: for-in for <=15 keys, Object.keys for >15
4309
4416
  inner = propCount <= 15
4310
- ? `var _n=0;for(var _k in ${v})_n++;if(_n!==${propCount})return false`
4417
+ ? `var _n=0;for(var _k in ${v})_n++;if(_n!==${propCount}&&${ownCountOff(ctx, v, propCount, plainFlag(ctx, v))})return false`
4311
4418
  : `if(Object.keys(${v}).length!==${propCount})return false`
4312
4419
  _deferOrInline(ctx, lines, v, isObj ? inner : `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){${inner}}`)
4313
4420
  }
@@ -4320,10 +4427,10 @@ function genUnevaluatedPropertiesNode(schema, v, lines, ctx, knownType, isObj, h
4320
4427
  // document. The hoisted name lookup is the same cure it was there.
4321
4428
  inner = propCount >= AP_LOOKUP_MIN
4322
4429
  ? apMembershipCheck(ctx, knownKeys, v)
4323
- : genCharCodeSwitch(knownKeys, v)
4430
+ : genCharCodeSwitch(knownKeys, v, ctx)
4324
4431
  _deferOrInline(ctx, lines, v, isObj ? inner : `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){${inner}}`)
4325
4432
  } else {
4326
- inner = `for(var _k in ${v})return false`
4433
+ inner = `for(var _k in ${v})if(${ownFail(ctx, v)})return false`
4327
4434
  _deferOrInline(ctx, lines, v, isObj ? inner : `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){${inner}}`)
4328
4435
  }
4329
4436
  } else if (typeof schema.unevaluatedProperties === 'object') {
@@ -4341,7 +4448,7 @@ function genUnevaluatedPropertiesNode(schema, v, lines, ctx, knownType, isObj, h
4341
4448
  } else if (knownKeys.length > 0) {
4342
4449
  skipKnown = `if(${knownKeys.map(k => `${ukVar}===${JSON.stringify(k)}`).join('||')})continue;`
4343
4450
  }
4344
- const inner = `for(var ${ukVar} in ${v}){${skipKnown}${check}}`
4451
+ const inner = `${ownHead(ctx, v, ukVar, 'var')}{${skipKnown}${check}}`
4345
4452
  _deferOrInline(ctx, lines, v, isObj ? inner : `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){${inner}}`)
4346
4453
  }
4347
4454
  }
@@ -4386,8 +4493,8 @@ function genUnevaluatedPropertiesNode(schema, v, lines, ctx, knownType, isObj, h
4386
4493
  const uniqueThen = [...new Set([...baseProps, ...ifProps, ...(thenEval.props || [])])]
4387
4494
  const uniqueElse = [...new Set([...baseProps, ...(elseEval.props || [])])]
4388
4495
 
4389
- const thenCheck = genCharCodeSwitch(uniqueThen, v)
4390
- const elseCheck = genCharCodeSwitch(uniqueElse, v)
4496
+ const thenCheck = genCharCodeSwitch(uniqueThen, v, ctx)
4497
+ const elseCheck = genCharCodeSwitch(uniqueElse, v, ctx)
4391
4498
  const guard = isObj ? '' : `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v}))`
4392
4499
  lines.push(`${guard}{const _uif${ufi}=${ifFn2};if(_uif${ufi}(${v})){${thenCheck}}else{${elseCheck}}}`)
4393
4500
  } else if (branchKeyword) {
@@ -4458,8 +4565,8 @@ function genUnevaluatedPropertiesNode(schema, v, lines, ctx, knownType, isObj, h
4458
4565
  }
4459
4566
  const dynamicCheck = `switch(_k.charCodeAt(0)){${switchCases}default:break}`
4460
4567
  const inner = staticCheck
4461
- ? `for(var _k in ${v}){if(${staticCheck})continue;${dynamicCheck}return false}`
4462
- : `for(var _k in ${v}){${dynamicCheck}return false}`
4568
+ ? `for(var _k in ${v}){if(${staticCheck})continue;${dynamicCheck}if(${ownFail(ctx, v)})return false}`
4569
+ : `for(var _k in ${v}){${dynamicCheck}if(${ownFail(ctx, v)})return false}`
4463
4570
  _deferOrInline(ctx, lines, v, isObj ? inner + '}' : `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){${inner}}}`)
4464
4571
  } else {
4465
4572
  // Fallback: plain object tracking
@@ -4484,7 +4591,7 @@ function genUnevaluatedPropertiesNode(schema, v, lines, ctx, knownType, isObj, h
4484
4591
  // Single pass: validate anyOf (at least one) + track all matching
4485
4592
  lines.push(`let _am${bfi}=false;for(let _bi=0;_bi<_bf${bfi}.length;_bi++){if(_bf${bfi}[_bi](${v})){_am${bfi}=true;for(const _p of _bk${bfi}[_bi])${evVar}[_p]=1}}if(!_am${bfi})return false`)
4486
4593
  }
4487
- const inner = `for(var _k in ${v}){if(!${evVar}[_k])return false}`
4594
+ const inner = `for(var _k in ${v}){if(!${evVar}[_k]&&${ownFail(ctx, v)})return false}`
4488
4595
  _deferOrInline(ctx, lines, v, isObj ? inner + '}' : `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){${inner}}}`)
4489
4596
  }
4490
4597
  } else if (schema.dependentSchemas) {
@@ -4499,7 +4606,7 @@ function genUnevaluatedPropertiesNode(schema, v, lines, ctx, knownType, isObj, h
4499
4606
  lines.push(`if(${ownKeyExpr(ctx, v, trigger)}){${depResult.props.map(k => `${evVar}[${JSON.stringify(k)}]=1`).join(';')}}`)
4500
4607
  }
4501
4608
  }
4502
- const inner = `for(var _k in ${v}){if(!${evVar}[_k])return false}`
4609
+ const inner = `for(var _k in ${v}){if(!${evVar}[_k]&&${ownFail(ctx, v)})return false}`
4503
4610
  _deferOrInline(ctx, lines, v, isObj ? inner + '}' : `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){${inner}}}`)
4504
4611
  } else {
4505
4612
  // General fallback: collect all patternProperties from root + allOf sub-schemas + if
@@ -4559,10 +4666,10 @@ function genUnevaluatedPropertiesNode(schema, v, lines, ctx, knownType, isObj, h
4559
4666
  }
4560
4667
  const rootPatCheck = rootReVars.map(rv => `if(${rv}.test(_k))continue;`).join('')
4561
4668
  const ifPatCheck = ifReVars.map(rv => `if(${rv}.test(_k))continue;`).join('')
4562
- const inner = `const _uif${ufi}=${ifFn};if(_uif${ufi}(${v})){for(var _k in ${v}){if(${evVar}[_k])continue;${rootPatCheck}${ifPatCheck}return false}}else{for(var _k in ${v}){if(${evVar}[_k])continue;${rootPatCheck}return false}}`
4669
+ const inner = `const _uif${ufi}=${ifFn};if(_uif${ufi}(${v})){for(var _k in ${v}){if(${evVar}[_k])continue;${rootPatCheck}${ifPatCheck}if(${ownFail(ctx, v)})return false}}else{for(var _k in ${v}){if(${evVar}[_k])continue;${rootPatCheck}if(${ownFail(ctx, v)})return false}}`
4563
4670
  _deferOrInline(ctx, lines, v, isObj ? inner + '}' : `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){${inner}}}`)
4564
4671
  } else {
4565
- 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}`
4672
+ const inner = `for(var _k in ${v}){if(${evVar}[_k])continue;${reVars.map(rv => `if(${rv}.test(_k)){${evVar}[_k]=1;continue}`).join('')}if(${ownFail(ctx, v)})return false}`
4566
4673
  _deferOrInline(ctx, lines, v, isObj ? inner + '}' : `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){${inner}}}`)
4567
4674
  }
4568
4675
  } else {
@@ -4573,7 +4680,7 @@ function genUnevaluatedPropertiesNode(schema, v, lines, ctx, knownType, isObj, h
4573
4680
  const evVar = `_ev${ei}`
4574
4681
  lines.push(`{const ${evVar}={}`)
4575
4682
  for (const k of baseProps) lines.push(`${evVar}[${JSON.stringify(k)}]=1`)
4576
- const inner = `for(var _k in ${v}){if(!${evVar}[_k])return false}`
4683
+ const inner = `for(var _k in ${v}){if(!${evVar}[_k]&&${ownFail(ctx, v)})return false}`
4577
4684
  _deferOrInline(ctx, lines, v, isObj ? inner + '}' : `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){${inner}}}`)
4578
4685
  }
4579
4686
  }
@@ -4613,7 +4720,7 @@ function genUnevaluatedPropertiesNode(schema, v, lines, ctx, knownType, isObj, h
4613
4720
  genCode(schema.unevaluatedProperties, `${v}[${ukVar}]`, subLines2, ctx)
4614
4721
  if (subLines2.length > 0) {
4615
4722
  const check = subLines2.join(';')
4616
- const inner = `for(var ${ukVar} in ${v}){if(${evVar}[${ukVar}])continue;${check}}`
4723
+ const inner = `${ownHead(ctx, v, ukVar, 'var')}{if(${evVar}[${ukVar}])continue;${check}}`
4617
4724
  _deferOrInline(ctx, lines, v, isObj ? inner + '}' : `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){${inner}}}`)
4618
4725
  } else {
4619
4726
  lines.push('}')
@@ -5051,13 +5158,57 @@ function fastPrefixCheck(pattern, keyVar) {
5051
5158
  return `${keyVar}.startsWith(${JSON.stringify(prefix)})`
5052
5159
  }
5053
5160
 
5161
+ // The head of a loop over the own enumerable keys of `v`, written as
5162
+ // `for...in`, the fast way to walk an object, which also visits enumerable
5163
+ // keys its prototype carries. An ordinary object's prototype is
5164
+ // Object.prototype, and its keys are taken as they come; any other object, a
5165
+ // class instance or one made with Object.create, has each key checked as its
5166
+ // own, as the interpreter reads them. One statement, so the body that follows
5167
+ // is written as it was: `for(var _k in v)if(...)continue;else BODY`.
5168
+ // For a loop whose only work is to reject on a key it does not expect: the
5169
+ // key is the object's own, or the object is ordinary, whose prototype's keys
5170
+ // are taken as they come. Read only on the way to `return false`, so an
5171
+ // object that has only expected keys pays nothing.
5172
+ // `_ok` reads the prototype rather than the node's flag: this check can land
5173
+ // in a hoisted branch function the flag is not declared in.
5174
+ function ownFail (ctx, v, k = '_k') {
5175
+ useOwnHelper(ctx, '_ok')
5176
+ return `_ok(${v},${k})`
5177
+ }
5178
+ // For a key count that came out wrong: whether it is wrong counting own keys
5179
+ // only. An ordinary object's count stands; any other object is counted again.
5180
+ // A oneOf of closed objects fails its other branches here on every value, so
5181
+ // this is not only a cold path.
5182
+ function ownCountOff (ctx, v, n, pk) {
5183
+ // A call to a shared helper rather than the test written in place: a oneOf
5184
+ // branch this small is inlined into its caller by V8, and the longer
5185
+ // bytecode cost that, 8% on a document of patch operations. The node's
5186
+ // ordinary-object flag is not used: this can land in a hoisted branch
5187
+ // function it is not declared in.
5188
+ useOwnHelper(ctx, '_nk')
5189
+ // `pk`: the node's own ordinary-object flag, where the caller holds it in
5190
+ // the same function as this check. An ordinary object needs no recount.
5191
+ return pk ? `(${pk}||_nk(${v},${n}))` : `_nk(${v},${n})`
5192
+ }
5193
+
5194
+ function ownHead (ctx, v, k = '_k', kind = 'var') {
5195
+ useOwnHelper(ctx, '_hop')
5196
+ // Read here rather than taken from the node's flag: generated bodies that
5197
+ // become functions of their own (branches, annotations, definitions) take
5198
+ // the same parameter names and do not see the flag. Written in place, not
5199
+ // through a shared helper, which would see every object shape of the
5200
+ // document and read the prototype megamorphically: 7% on validate().
5201
+ const i = ctx.varCounter++
5202
+ return `var _pl${i}=${v}!==null&&typeof ${v}==='object'&&${protoIs(ctx, v)};for(${kind} ${k} in ${v})if(!_pl${i}&&!_hop.call(${v},${k}))continue;else `
5203
+ }
5204
+
5054
5205
  // Generate a charCodeAt(0)-based switch tree for fast key validation.
5055
5206
  // V8 compiles switch to jump tables — O(1) dispatch vs O(n) chain.
5056
- function genCharCodeSwitch(keys, v) {
5057
- if (keys.length === 0) return `for(var _k in ${v})return false`
5207
+ function genCharCodeSwitch(keys, v, ctx) {
5208
+ if (keys.length === 0) return `for(var _k in ${v})if(${ownFail(ctx, v)})return false`
5058
5209
  if (keys.length <= 3) {
5059
5210
  // Small set: simple chain is faster than switch overhead
5060
- return `for(var _k in ${v})if(${keys.map(k => `_k!==${JSON.stringify(k)}`).join('&&')})return false`
5211
+ return `for(var _k in ${v})if(${keys.map(k => `_k!==${JSON.stringify(k)}`).join('&&')}&&${ownFail(ctx, v)})return false`
5061
5212
  }
5062
5213
 
5063
5214
  // Group keys by first charCode
@@ -5074,7 +5225,7 @@ function genCharCodeSwitch(keys, v) {
5074
5225
  cases += `case ${cc}:if(${cond})continue;break;`
5075
5226
  }
5076
5227
 
5077
- return `for(var _k in ${v}){switch(_k.charCodeAt(0)){${cases}default:break}return false}`
5228
+ return `for(var _k in ${v}){switch(_k.charCodeAt(0)){${cases}default:break}if(${ownFail(ctx, v)})return false}`
5078
5229
  }
5079
5230
 
5080
5231
  // The verdict form of genUnevaluatedDynamicC: the same set, built the same
@@ -5120,7 +5271,7 @@ function genUnevalDynamicVerdict(schema, v, lines, ctx, isObj, nonObjApplicators
5120
5271
  // additionalProperties is there.
5121
5272
  function addLocalAnnotations(node, v, S, out, ctx) {
5122
5273
  if (node.additionalProperties !== undefined) {
5123
- out.push(`for(const _ka in ${v})${S}.push(_ka)`)
5274
+ out.push(`${ownHead(ctx, v, `_ka`, 'const')}${S}.push(_ka)`)
5124
5275
  return
5125
5276
  }
5126
5277
  for (const k of Object.keys(node.properties || {})) out.push(`if(Object.prototype.hasOwnProperty.call(${v},${JSON.stringify(k)}))${S}.push(${JSON.stringify(k)})`)
@@ -5136,7 +5287,7 @@ function addLocalAnnotations(node, v, S, out, ctx) {
5136
5287
  ctx.closureVals.push(safeReClosure(ctx, pat))
5137
5288
  tests.push(`_re${ri}.test(${kv})`)
5138
5289
  }
5139
- out.push(`for(const ${kv} in ${v})if(${tests.join('||')})${S}.push(${kv})`)
5290
+ out.push(`${ownHead(ctx, v, kv, 'const')}if(${tests.join('||')})${S}.push(${kv})`)
5140
5291
  }
5141
5292
  }
5142
5293
 
@@ -5419,4 +5570,4 @@ Object.defineProperty(module.exports, '_expandDynamicScopes', { value: expandDyn
5419
5570
  // For tests/test_custom_keyword_codegen.js, which holds these copies to the interpreter's.
5420
5571
  Object.defineProperty(module.exports, '_kwTypeBits', { value: { kwTypeBit, kwDataBits, KW_T_ANY }, enumerable: false })
5421
5572
 
5422
- module.exports._internal = { 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 }
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 }
package/lib/keywords.js CHANGED
@@ -10,6 +10,12 @@
10
10
  // { macro(value, parent) -> schema } the returned schema is applied in
11
11
  // place, and the keyword reports an
12
12
  // error of its own when it fails
13
+ // with optional `dataContext: true`: the per-value function is also given
14
+ // `{ instancePath, parentData, parentDataProperty, rootData }`, as the default
15
+ // validator's keywords are, so it can read or change the value where it sits
16
+ // (validate(value, data, parent, ctx), or the compiled fn(data, ctx)). Such a
17
+ // schema runs on the interpreted engine's collecting walk, which knows where
18
+ // every value is.
13
19
  // with optional `type`: a JSON Schema type name or a list of them. A typed
14
20
  // keyword is skipped for data of any other type, which is what makes
15
21
  // `{ type: 'number', ... }` safe next to `type: ['number', 'string']`.
@@ -72,6 +78,12 @@ function normalizeKeywords(defs) {
72
78
  validate: hasValidate ? def.validate : null,
73
79
  compile: hasCompile ? def.compile : null,
74
80
  macro: hasMacro ? def.macro : null,
81
+ dataContext: def.dataContext === true,
82
+ // Order among the custom keywords of one node, as the default
83
+ // validator's keywords declare it: `before` names a keyword this one
84
+ // runs ahead of, `post` runs it after the others.
85
+ before: typeof def.before === 'string' ? def.before : null,
86
+ post: def.post === true,
75
87
  };
76
88
  }
77
89
  return out;
@@ -110,4 +122,15 @@ const NAME_MAPS = new Set([
110
122
  'dependentSchemas', 'dependentRequired', 'dependencies', 'propertyDependencies',
111
123
  ]);
112
124
 
113
- module.exports = { normalizeKeywords, schemaUsesKeywords };
125
+ // Does the schema use a registered keyword that asks for its data's context?
126
+ function schemaUsesDataContext(schema, keywords) {
127
+ if (!keywords) return false;
128
+ let any = false;
129
+ for (const name of Object.keys(keywords)) if (keywords[name].dataContext) { any = true; break; }
130
+ if (!any) return false;
131
+ const only = Object.create(null);
132
+ for (const name of Object.keys(keywords)) if (keywords[name].dataContext) only[name] = keywords[name];
133
+ return schemaUsesKeywords(schema, only);
134
+ }
135
+
136
+ module.exports = { normalizeKeywords, schemaUsesKeywords, schemaUsesDataContext };
@@ -1,20 +1,25 @@
1
1
  'use strict';
2
2
 
3
- // Bounded Levenshtein. Returns Infinity if distance > maxDistance (or, past
3
+ // Bounded edit distance. Returns Infinity if distance > maxDistance (or, past
4
4
  // the bound, possibly some number greater than it; callers compare).
5
5
  //
6
+ // Two adjacent characters swapped count as one edit, not two (the optimal
7
+ // string alignment distance): `nmae` for `name` is the commonest typo there
8
+ // is, and at two edits a four-letter name's budget of one turned it away.
9
+ //
6
10
  // Only the cells within the bound of the diagonal are computed: a path
7
11
  // through any other cell already costs more than the bound. A typo hint asks
8
12
  // with a bound of one or two and compares the missing name with every key
9
13
  // present, so a row is five cells instead of the whole key. Without a bound
10
14
  // the band is the whole row.
11
15
  //
12
- // Two rows of scratch, reused across calls. The function is not recursive
16
+ // Three rows of scratch, reused across calls. The function is not recursive
13
17
  // and the library is single-threaded, so nothing else is using them; growing
14
18
  // them once beats allocating two arrays on every comparison, which the
15
19
  // reject-path profile showed as most of this function's cost.
16
20
  let scratchA = new Int32Array(64);
17
21
  let scratchB = new Int32Array(64);
22
+ let scratchC = new Int32Array(64);
18
23
 
19
24
  function levenshtein (a, b, maxDistance) {
20
25
  if (a === b) return 0;
@@ -28,7 +33,11 @@ function levenshtein (a, b, maxDistance) {
28
33
  if (scratchA.length < m + 2) {
29
34
  scratchA = new Int32Array(m + 2);
30
35
  scratchB = new Int32Array(m + 2);
36
+ scratchC = new Int32Array(m + 2);
31
37
  }
38
+ // The row before the previous one, read for a swap: within the band of
39
+ // row i the cell two back on the diagonal is inside the band of row i - 2.
40
+ let prev2 = scratchC;
32
41
  let prev = scratchA;
33
42
  let curr = scratchB;
34
43
  for (let j = 0; j <= m; j++) prev[j] = j <= max ? j : big;
@@ -40,20 +49,27 @@ function levenshtein (a, b, maxDistance) {
40
49
  // charCodeAt rather than indexing: reading a character by index allocates
41
50
  // a one-character string, and this is the innermost loop.
42
51
  const ca = a.charCodeAt(i - 1);
52
+ const pa = i > 1 ? a.charCodeAt(i - 2) : -1;
43
53
  for (let j = lo; j <= hi; j++) {
44
- let v = prev[j - 1] + (ca === b.charCodeAt(j - 1) ? 0 : 1);
54
+ const cb = b.charCodeAt(j - 1);
55
+ let v = prev[j - 1] + (ca === cb ? 0 : 1);
45
56
  const del = curr[j - 1] + 1;
46
57
  if (del < v) v = del;
47
58
  const ins = prev[j] + 1;
48
59
  if (ins < v) v = ins;
60
+ if (j > 1 && ca !== cb && ca === b.charCodeAt(j - 2) && pa === cb) {
61
+ const swap = prev2[j - 2] + 1;
62
+ if (swap < v) v = swap;
63
+ }
49
64
  if (v > big) v = big;
50
65
  curr[j] = v;
51
66
  if (v < rowMin) rowMin = v;
52
67
  }
53
68
  if (hi < m) curr[hi + 1] = big;
54
69
  if (rowMin > max) return Infinity;
55
- // A destructured swap builds an array per row; a temporary does not.
56
- const t = prev;
70
+ // A destructured swap builds an array per row; temporaries do not.
71
+ const t = prev2;
72
+ prev2 = prev;
57
73
  prev = curr;
58
74
  curr = t;
59
75
  }