ata-validator 1.9.0 → 1.10.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/CHANGELOG.md CHANGED
@@ -2,6 +2,18 @@
2
2
 
3
3
  All notable changes to ata-validator are documented here. The format follows [Keep a Changelog](https://keepachangelog.com/), and this project adheres to semantic versioning.
4
4
 
5
+ ## 1.10.0 - 2026-08-30
6
+
7
+ ### Performance
8
+
9
+ - The code generator takes shapes it used to decline for no correctness reason: boolean subschemas in `items`, `properties`, `patternProperties`, `dependentSchemas`, `propertyNames`, `allOf`, `anyOf`, `not`, `contains` and `if`/`then`/`else`, recursive `#/$defs/` references as named functions, and `additionalProperties` as a schema alongside composition or `patternProperties`. Each lands in all three generators and the closure path, held to the interpreter by `tests/test_codegen_edge_shapes.js` and the entry-point agreement test over the whole official suite. Every suite group that moved off the interpreter got faster, 31 of 31 on draft 2020-12 and 28 of 28 on draft 7, summed per-group time down 70 percent. The suite-wide figure, measured interleaved against the previous release in one process, did not move outside that measurement's noise; `benchmark/verdict-bench.md` has the numbers and says why.
10
+ - `date` and `ipv4` format checks read the string once with no regular expression and no allocation: 45.7 to 14.2 ns and 54.5 to 27.1 ns on a valid value, interleaved medians. Fuzzed against the previous forms with 0 mismatches; `tests/test_formats_single_pass.js` keeps it that way.
11
+
12
+ ### Fixed
13
+
14
+ - A key matched only by the second of two `patternProperties` entries, alongside `additionalProperties: false`, was rejected: the generated key loop returned at the first pattern that missed. Found while rewriting that loop; covered by the edge-shape test.
15
+ - A declared property that also matched a `patternProperties` entry skipped the pattern's schema in the generated code when `additionalProperties` was a schema. The suite's own interaction case caught it the moment the shape was allowed to compile.
16
+
5
17
  ## 1.9.0 - 2026-08-28
6
18
 
7
19
  ### Errors
package/lib/formats.js ADDED
@@ -0,0 +1,57 @@
1
+ 'use strict';
2
+
3
+ // Format predicates that read the string once, with no regular expression
4
+ // and no allocation. Each has a twin that emits the same check as source for
5
+ // the code generator; tests/test_formats_single_pass.js holds the two
6
+ // together and fuzzes both against the regular-expression forms they replaced.
7
+ // Measured, interleaved medians: date 45.7 to 14.2 ns, ipv4 54.5 to 27.1 ns.
8
+ // uuid was tried the same way and measured 57.3 against 59.4 ns: V8's regular
9
+ // expression for a fixed-length hex pattern is already that fast, so it stays.
10
+
11
+ function isDigit (c) { return c >= 48 && c <= 57; }
12
+
13
+ function date (s) {
14
+ if (s.length !== 10) return false;
15
+ for (let i = 0; i < 10; i++) {
16
+ const c = s.charCodeAt(i);
17
+ if (i === 4 || i === 7) { if (c !== 45) return false; } else if (!isDigit(c)) return false;
18
+ }
19
+ const m = (s.charCodeAt(5) - 48) * 10 + (s.charCodeAt(6) - 48);
20
+ const d = (s.charCodeAt(8) - 48) * 10 + (s.charCodeAt(9) - 48);
21
+ return m >= 1 && m <= 12 && d >= 1 && d <= 31;
22
+ }
23
+
24
+ // Four decimal octets, no leading zeros, no empty octets, nothing else.
25
+ function ipv4 (s) {
26
+ const n = s.length;
27
+ if (n < 7 || n > 15) return false;
28
+ let octets = 0, value = 0, digits = 0;
29
+ for (let i = 0; i <= n; i++) {
30
+ const c = i < n ? s.charCodeAt(i) : 46;
31
+ if (c === 46) {
32
+ if (digits === 0 || value > 255) return false;
33
+ octets++; value = 0; digits = 0;
34
+ if (octets > 4) return false;
35
+ } else if (isDigit(c)) {
36
+ if (digits === 1 && value === 0) return false;
37
+ value = value * 10 + (c - 48); digits++;
38
+ if (digits > 3) return false;
39
+ } else return false;
40
+ }
41
+ return octets === 4;
42
+ }
43
+
44
+ // Source twins. `v` is the expression holding the string; when `isStr` is
45
+ // false the check is wrapped in a typeof guard, matching FORMAT_CODEGEN's
46
+ // convention. Each returns a statement that `return false`s on mismatch.
47
+ function guard (v, isStr, body) { return isStr ? `{${body}}` : `if(typeof ${v}==='string'){${body}}`; }
48
+
49
+ function dateSource (v, isStr) {
50
+ return guard(v, isStr, `if(${v}.length!==10)return false;for(let _i=0;_i<10;_i++){const _c=${v}.charCodeAt(_i);if(_i===4||_i===7){if(_c!==45)return false}else if(_c<48||_c>57)return false}const _m=(${v}.charCodeAt(5)-48)*10+(${v}.charCodeAt(6)-48),_d=(${v}.charCodeAt(8)-48)*10+(${v}.charCodeAt(9)-48);if(_m<1||_m>12||_d<1||_d>31)return false`);
51
+ }
52
+
53
+ function ipv4Source (v, isStr) {
54
+ return guard(v, isStr, `const _n=${v}.length;if(_n<7||_n>15)return false;let _o=0,_val=0,_dg=0;for(let _i=0;_i<=_n;_i++){const _c=_i<_n?${v}.charCodeAt(_i):46;if(_c===46){if(_dg===0||_val>255)return false;_o++;_val=0;_dg=0;if(_o>4)return false}else if(_c>=48&&_c<=57){if(_dg===1&&_val===0)return false;_val=_val*10+(_c-48);_dg++;if(_dg>3)return false}else return false}if(_o!==4)return false`);
55
+ }
56
+
57
+ module.exports = { date, ipv4, dateSource, ipv4Source };
@@ -247,16 +247,17 @@ function multipleOfOk(d, m) {
247
247
  return Math.abs(q - Math.round(q)) < 1e-9;
248
248
  }
249
249
 
250
+ const _formats = require('./formats');
250
251
  const FORMAT_CHECKS = {
251
252
  email: (s) => { const at = s.indexOf('@'); return at > 0 && at < s.length - 1 && s.indexOf('.', at) > at + 1; },
252
- date: (s) => { if (s.length !== 10 || !/^\d{4}-\d{2}-\d{2}$/.test(s)) return false; const m = +s.slice(5, 7), d = +s.slice(8, 10); return m >= 1 && m <= 12 && d >= 1 && d <= 31; },
253
+ date: _formats.date,
253
254
  'date-time': (s) => /^\d{4}-\d{2}-\d{2}[Tt]\d{2}:\d{2}:\d{2}(\.\d+)?([Zz]|[+-]\d{2}:\d{2})$/.test(s) && !isNaN(Date.parse(s)),
254
255
  time: (s) => /^([01]\d|2[0-3]):[0-5]\d:[0-5]\d(\.\d+)?([Zz]|[+-]\d{2}:\d{2})?$/.test(s),
255
256
  duration: (s) => /^P(?:\d+Y)?(?:\d+M)?(?:\d+W)?(?:\d+D)?(?:T(?:\d+H)?(?:\d+M)?(?:\d+(?:\.\d+)?S)?)?$/.test(s) && s !== 'P' && !s.endsWith('T'),
256
257
  uuid: (s) => s.length === 36 && /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(s),
257
258
  uri: (s) => /^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(s) && !/[\s\u0000-\u001f\u007f]/.test(s),
258
259
  'uri-reference': (s) => !/[\s\u0000-\u001f\u007f]/.test(s),
259
- ipv4: (s) => { const p = s.split('.'); return p.length === 4 && p.every((n) => { const v = +n; return n !== '' && v >= 0 && v <= 255 && String(v) === n; }); },
260
+ ipv4: _formats.ipv4,
260
261
  ipv6: (s) => s !== '' && /^[0-9a-fA-F:.]+$/.test(s) && s.split(':').length >= 3 && s.split(':').length <= 8,
261
262
  hostname: (s) => s.length > 0 && s.length <= 253 && /^[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/.test(s),
262
263
  regex: (s) => { try { new RegExp(s, 'u'); return true; } catch { try { new RegExp(s); return true; } catch { return false; } } },
@@ -193,11 +193,13 @@ function compileToJS(schema, defs, schemaMap) {
193
193
  // Collect $defs early so sub-schemas can resolve $ref
194
194
  const rootDefs = defs || collectDefs(schema)
195
195
 
196
- // Bail on features that are too complex for JS fast path
197
- if (schema.patternProperties ||
198
- schema.dependentSchemas ||
199
- schema.propertyDependencies ||
200
- schema.propertyNames) {
196
+ // Bail on features this path has no emitter for. Tested for presence, not
197
+ // truthiness: `propertyNames: false` is falsy, passed this bail, and then
198
+ // validated nothing, which the entry-point agreement test caught.
199
+ if (schema.patternProperties !== undefined ||
200
+ schema.dependentSchemas !== undefined ||
201
+ schema.propertyDependencies !== undefined ||
202
+ schema.propertyNames !== undefined) {
201
203
  return null
202
204
  }
203
205
 
@@ -328,7 +330,7 @@ function compileToJS(schema, defs, schemaMap) {
328
330
  }
329
331
 
330
332
  // items, applied after the prefixItems positions
331
- if (schema.items) {
333
+ if (schema.items !== undefined && schema.items !== true) {
332
334
  const itemCheck = compileToJS(schema.items, rootDefs)
333
335
  if (!itemCheck) return null
334
336
  const start = Array.isArray(schema.prefixItems) ? schema.prefixItems.length : 0
@@ -359,7 +361,7 @@ function compileToJS(schema, defs, schemaMap) {
359
361
  }
360
362
 
361
363
  // contains
362
- if (schema.contains) {
364
+ if (schema.contains !== undefined) {
363
365
  const containsCheck = compileToJS(schema.contains, rootDefs)
364
366
  if (!containsCheck) return null
365
367
  const minC = schema.minContains !== undefined ? schema.minContains : 1
@@ -517,20 +519,20 @@ function compileToJS(schema, defs, schemaMap) {
517
519
  }
518
520
 
519
521
  // not
520
- if (schema.not) {
522
+ if (schema.not !== undefined) {
521
523
  const notFn = compileToJS(schema.not, rootDefs)
522
524
  if (!notFn) return null
523
525
  checks.push((d) => !notFn(d))
524
526
  }
525
527
 
526
528
  // if/then/else
527
- if (schema.if) {
529
+ if (schema.if !== undefined) {
528
530
  const ifFn = compileToJS(schema.if, rootDefs)
529
531
  if (!ifFn) return null
530
- const thenFn = schema.then ? compileToJS(schema.then, rootDefs) : null
531
- const elseFn = schema.else ? compileToJS(schema.else, rootDefs) : null
532
- if (schema.then && !thenFn) return null
533
- if (schema.else && !elseFn) return null
532
+ const thenFn = schema.then !== undefined ? compileToJS(schema.then, rootDefs) : null
533
+ const elseFn = schema.else !== undefined ? compileToJS(schema.else, rootDefs) : null
534
+ if (schema.then !== undefined && !thenFn) return null
535
+ if (schema.else !== undefined && !elseFn) return null
534
536
  checks.push((d) => {
535
537
  if (ifFn(d)) {
536
538
  return thenFn ? thenFn(d) : true
@@ -756,11 +758,12 @@ function _canonical(x) {
756
758
  return '{' + Object.keys(x).sort().map((k) => JSON.stringify(k) + ':' + _canonical(x[k])).join(',') + '}'
757
759
  }
758
760
 
761
+ const _formats = require('./formats')
759
762
  const FORMAT_CHECKS = {
760
763
  email: (s) => { const at = s.indexOf('@'); return at > 0 && at < s.length - 1 && s.indexOf('.', at) > at + 1 },
761
- date: (s) => { if (s.length !== 10 || !/^\d{4}-\d{2}-\d{2}$/.test(s)) return false; const m = +s.slice(5, 7), d = +s.slice(8, 10); return m >= 1 && m <= 12 && d >= 1 && d <= 31 },
764
+ date: _formats.date,
762
765
  uuid: (s) => s.length === 36 && /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(s),
763
- ipv4: (s) => { const p = s.split('.'); return p.length === 4 && p.every(n => { const v = +n; return v >= 0 && v <= 255 && String(v) === n }) },
766
+ ipv4: _formats.ipv4,
764
767
  hostname: (s) => s.length > 0 && s.length <= 253 && /^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$/i.test(s),
765
768
  }
766
769
 
@@ -842,6 +845,51 @@ function hasNestedIdScope(node, isRoot = true) {
842
845
 
843
846
  // Recursively check if a schema can be safely compiled to JS codegen.
844
847
  // Returns false if any sub-schema contains features codegen gets wrong.
848
+ // Names of the defs that sit on a $ref cycle among defs. Such a def compiles
849
+ // as a named function rather than being inlined, so recursion terminates in
850
+ // the generated code the same way `$ref: "#"` does at the root. A plain
851
+ // cross-reference (A -> B where B does not refer back) is not on a cycle and
852
+ // still inlines.
853
+ function cyclicDefNames (defs) {
854
+ const out = new Set()
855
+ if (!defs || typeof defs !== 'object') return out
856
+ const defNames = new Set(Object.keys(defs))
857
+ const defRefs = {}
858
+ for (const [name, def] of Object.entries(defs)) {
859
+ const refs = []
860
+ const seen = new WeakSet()
861
+ const collectRefs = (node) => {
862
+ if (typeof node !== 'object' || node === null) return
863
+ if (seen.has(node)) return
864
+ seen.add(node)
865
+ if (node.$ref) {
866
+ const m = /^#\/(?:\$defs|definitions)\/([^/]+)$/.exec(node.$ref)
867
+ if (m && defNames.has(m[1])) refs.push(m[1])
868
+ }
869
+ for (const val of Object.values(node)) {
870
+ if (typeof val === 'object' && val !== null) {
871
+ if (Array.isArray(val)) { for (const item of val) collectRefs(item) } else collectRefs(val)
872
+ }
873
+ }
874
+ }
875
+ collectRefs(def)
876
+ defRefs[name] = refs
877
+ }
878
+ const visited = new Set()
879
+ const inStack = []
880
+ const dfs = (node) => {
881
+ const at = inStack.indexOf(node)
882
+ if (at !== -1) { for (let i = at; i < inStack.length; i++) out.add(inStack[i]); return }
883
+ if (visited.has(node)) return
884
+ visited.add(node)
885
+ inStack.push(node)
886
+ for (const neighbor of (defRefs[node] || [])) dfs(neighbor)
887
+ inStack.pop()
888
+ }
889
+ for (const name of Object.keys(defRefs)) dfs(name)
890
+ return out
891
+ }
892
+
845
893
  function codegenSafe(schema, schemaMap) {
846
894
  if (typeof schema === 'boolean') return true
847
895
  if (typeof schema !== 'object' || schema === null) return true
@@ -855,13 +903,12 @@ function codegenSafe(schema, schemaMap) {
855
903
  if (schema.$dynamicRef && !schema.$dynamicRef.startsWith('#')) return false
856
904
 
857
905
  // Boolean sub-schemas anywhere cause bail — codegen doesn't handle schema=false correctly
858
- if (schema.items === false) return false
859
- if (schema.items === true && !schema.unevaluatedItems) return false
906
+ // items: false and items: true are constants; every generator handles a
907
+ // boolean child at its head, so neither falls through as "nothing".
860
908
  if (schema.additionalProperties === true) return true // permissive — fine
861
909
  if (schema.properties) {
862
910
  for (const v of Object.values(schema.properties)) {
863
- if (v === false) return false // property: false is complex
864
- if (v === true) continue // property: true is always valid
911
+ if (typeof v === 'boolean') continue // constants; every generator handles them at its head
865
912
  if (!codegenSafe(v, schemaMap)) return false
866
913
  }
867
914
  }
@@ -926,15 +973,17 @@ function codegenSafe(schema, schemaMap) {
926
973
  // additionalProperties as schema — supported when no composition (allOf/oneOf/anyOf)
927
974
  // is in play, and no patternProperties (unified loop does not emit the
928
975
  // per-key sub-schema walk yet). Closure path handles those cases.
976
+ // additionalProperties as a schema: emitted by the per-key walk, or by the
977
+ // unified key loop when patternProperties is present. Composition alongside
978
+ // it is fine, since additionalProperties only looks at this schema's own
979
+ // properties and patternProperties.
929
980
  if (typeof schema.additionalProperties === 'object' && schema.additionalProperties !== null) {
930
- if (schema.allOf || schema.oneOf || schema.anyOf) return false
931
- if (schema.patternProperties) return false
932
981
  if (!codegenSafe(schema.additionalProperties, schemaMap)) return false
933
982
  }
934
983
  if (schema.additionalProperties === false && !schema.properties) return false
935
984
 
936
985
  // propertyNames: false — codegen doesn't handle this
937
- if (schema.propertyNames === false) return false
986
+ // propertyNames: false is emitted as a per-key rejection by every generator.
938
987
 
939
988
  // unevaluatedProperties: allow boolean and schema values
940
989
  if (schema.unevaluatedProperties !== undefined) {
@@ -967,53 +1016,8 @@ function codegenSafe(schema, schemaMap) {
967
1016
  const defs = schema.$defs || schema.definitions
968
1017
  if (defs) {
969
1018
  const defNames = new Set(Object.keys(defs))
970
- // Detect cyclic local refs: build a directed graph of def->def references and
971
- // check for cycles using DFS. A plain cross-reference (A -> B where B doesn't
972
- // refer back to A) is fine; only true cycles (A -> B -> A) must be rejected.
973
- // The JS codegen returns (() => true) on cycle (permissive), so cyclic schemas
974
- // must route to the interpreted engine instead.
975
- const defRefs = {}
976
- for (const [name, def] of Object.entries(defs)) {
977
- defRefs[name] = []
978
- if (typeof def === 'object' && def !== null) {
979
- const refs = []
980
- const seen = new WeakSet()
981
- const collectRefs = (node) => {
982
- if (typeof node !== 'object' || node === null) return
983
- if (seen.has(node)) return
984
- seen.add(node)
985
- if (node.$ref) {
986
- const m = /^#\/(?:\$defs|definitions)\/([^/]+)$/.exec(node.$ref)
987
- if (m && defNames.has(m[1])) refs.push(m[1])
988
- }
989
- for (const val of Object.values(node)) {
990
- if (typeof val === 'object' && val !== null) {
991
- if (Array.isArray(val)) { for (const item of val) collectRefs(item) }
992
- else collectRefs(val)
993
- }
994
- }
995
- }
996
- collectRefs(def)
997
- defRefs[name] = refs
998
- }
999
- }
1000
- const hasCycle = (() => {
1001
- const visited = new Set()
1002
- const inStack = new Set()
1003
- const dfs = (node) => {
1004
- if (inStack.has(node)) return true
1005
- if (visited.has(node)) return false
1006
- visited.add(node)
1007
- inStack.add(node)
1008
- for (const neighbor of (defRefs[node] || [])) {
1009
- if (dfs(neighbor)) return true
1010
- }
1011
- inStack.delete(node)
1012
- return false
1013
- }
1014
- return Object.keys(defRefs).some(dfs)
1015
- })()
1016
- if (hasCycle) return false
1019
+ // Defs on a $ref cycle compile as named functions (see genCode); nothing
1020
+ // to decline here. cyclicDefNames() is the walk that used to bail.
1017
1021
  for (const [name, def] of Object.entries(defs)) {
1018
1022
  if (/[~/"']/.test(name)) return false // special chars in def name
1019
1023
  if (typeof def === 'boolean') return false
@@ -1030,6 +1034,10 @@ function codegenSafe(schema, schemaMap) {
1030
1034
  }
1031
1035
 
1032
1036
  // Recurse into sub-schemas — bail on boolean schemas in any position
1037
+ // A boolean subschema is a constant that every generator emits at its
1038
+ // head with the child's own path. The emitters below test for presence,
1039
+ // not truthiness, because `false` is falsy and a truthiness test would
1040
+ // skip it and validate nothing.
1033
1041
  const subs = [
1034
1042
  schema.items, schema.contains, schema.not,
1035
1043
  schema.if, schema.then, schema.else,
@@ -1040,9 +1048,7 @@ function codegenSafe(schema, schemaMap) {
1040
1048
  ]
1041
1049
  if (typeof schema.additionalProperties === 'object') subs.push(schema.additionalProperties)
1042
1050
  for (const s of subs) {
1043
- if (s === undefined || s === null) continue
1044
- if (s === false) return false // boolean false sub-schema — complex
1045
- if (s === true) continue // boolean true sub-schema — always valid, fine
1051
+ if (s === undefined || s === null || typeof s === 'boolean') continue
1046
1052
  if (!codegenSafe(s, schemaMap)) return false
1047
1053
  }
1048
1054
 
@@ -1330,7 +1336,6 @@ function compileToJSCodegen(schema, schemaMap, userFormats) {
1330
1336
  // patternProperties: bail only on boolean sub-schemas or unicode property escapes
1331
1337
  if (schema.patternProperties) {
1332
1338
  for (const [pat, sub] of Object.entries(schema.patternProperties)) {
1333
- if (typeof sub === 'boolean') return null
1334
1339
  if (/\\[pP]\{/.test(pat)) return null
1335
1340
  if (typeof sub === 'object' && sub !== null && !codegenSafe(sub, schemaMap)) return null
1336
1341
  }
@@ -1338,13 +1343,11 @@ function compileToJSCodegen(schema, schemaMap, userFormats) {
1338
1343
  // dependentSchemas: bail on boolean sub-schemas
1339
1344
  if (schema.dependentSchemas) {
1340
1345
  for (const sub of Object.values(schema.dependentSchemas)) {
1341
- if (typeof sub === 'boolean') return null
1342
1346
  if (typeof sub === 'object' && sub !== null && !codegenSafe(sub, schemaMap)) return null
1343
1347
  }
1344
1348
  }
1345
1349
  // propertyNames: only codegen simple string constraints
1346
1350
  if (schema.propertyNames) {
1347
- if (typeof schema.propertyNames === 'boolean') return null
1348
1351
  const pn = schema.propertyNames
1349
1352
  const supported = ['maxLength', 'minLength', 'pattern', 'const', 'enum']
1350
1353
  const keys = Object.keys(pn).filter(k => k !== '$schema')
@@ -1654,6 +1657,11 @@ function _deferOrInline(ctx, lines, v, check) {
1654
1657
  // 'array' = we know v is an array
1655
1658
  // 'string' / 'number' / 'integer' = we know the primitive type
1656
1659
  function genCode(schema, v, lines, ctx, knownType) {
1660
+ // A boolean schema is a constant. false rejects whatever reached it; true
1661
+ // constrains nothing and emits nothing, which is correct only because the
1662
+ // caller's own checks still stand around this call.
1663
+ if (schema === false) { lines.push('return false'); return }
1664
+ if (schema === true) return
1657
1665
  if (typeof schema !== 'object' || schema === null) return
1658
1666
  if (!ctx.regExpMap) {
1659
1667
  ctx.regExpMap = new Map();
@@ -1673,10 +1681,28 @@ function genCode(schema, v, lines, ctx, knownType) {
1673
1681
  // 1. Local ref
1674
1682
  const m = schema.$ref !== '#' && schema.$ref.match(/^#\/(?:\$defs|definitions)\/(.+)$/)
1675
1683
  if (m && ctx.rootDefs && ctx.rootDefs[m[1]]) {
1676
- if (ctx.refStack.has(schema.$ref)) { if (!hasSiblings) return }
1684
+ const defName = m[1]
1685
+ if (!ctx.cyclicDefs) ctx.cyclicDefs = cyclicDefNames(ctx.rootDefs)
1686
+ if (ctx.cyclicDefs.has(defName) && ctx.preamble) {
1687
+ // A def on a cycle is a named function in the preamble, emitted once
1688
+ // and called by name, so recursion terminates the way `$ref: "#"`
1689
+ // does at the root. The name is registered before the body is
1690
+ // generated so a self-reference inside it becomes a call.
1691
+ if (!ctx.defFns) ctx.defFns = new Map()
1692
+ let fnName = ctx.defFns.get(defName)
1693
+ if (!fnName) {
1694
+ fnName = '_def' + ctx.defFns.size + '_' + defName.replace(/[^A-Za-z0-9_]/g, '_')
1695
+ ctx.defFns.set(defName, fnName)
1696
+ const bodyLines = []
1697
+ genCode(ctx.rootDefs[defName], 'd', bodyLines, ctx)
1698
+ ctx.preamble.push(`function ${fnName}(d){${bodyLines.join('\n ')}\n return true}`)
1699
+ }
1700
+ lines.push(`if(!${fnName}(${v}))return false`)
1701
+ if (!hasSiblings) return
1702
+ } else if (ctx.refStack.has(schema.$ref)) { if (!hasSiblings) return }
1677
1703
  else {
1678
1704
  ctx.refStack.add(schema.$ref)
1679
- genCode(ctx.rootDefs[m[1]], v, lines, ctx, knownType)
1705
+ genCode(ctx.rootDefs[defName], v, lines, ctx, knownType)
1680
1706
  ctx.refStack.delete(schema.$ref)
1681
1707
  if (!hasSiblings) return
1682
1708
  }
@@ -2048,11 +2074,23 @@ function genCode(schema, v, lines, ctx, knownType) {
2048
2074
 
2049
2075
  const guard = isObj ? '' : `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v}))`
2050
2076
 
2051
- if (schema.additionalProperties === false && schema.properties) {
2052
- // Unified loop: properties + patterns + propertyNames + additionalProperties
2077
+ const apSchema = typeof schema.additionalProperties === 'object' && schema.additionalProperties !== null ? schema.additionalProperties : null
2078
+ if ((schema.additionalProperties === false && schema.properties) || apSchema) {
2079
+ // Unified loop: properties + patterns + propertyNames + additionalProperties.
2080
+ // A key that is neither declared nor matched by any pattern is
2081
+ // additional: rejected outright, or run through the additionalProperties
2082
+ // schema when there is one.
2053
2083
  ctx._ppHandledAdditional = true
2054
2084
  ctx._ppHandledPropertyNames = !!pn
2055
- const propKeys = Object.keys(schema.properties)
2085
+ const propKeys = Object.keys(schema.properties || {})
2086
+ let apFn = null
2087
+ if (apSchema) {
2088
+ const apLines = []
2089
+ genCode(apSchema, '_apv', apLines, ctx)
2090
+ apFn = `_apf${pi}`
2091
+ ctx.closureVars.push(apFn)
2092
+ ctx.closureVals.push(new Function('_apv', apLines.length === 0 ? 'return true' : `${apLines.join(';')};return true`))
2093
+ }
2056
2094
  lines.push(`${guard}{for(const ${kVar} in ${v}){`)
2057
2095
  // propertyNames checks (merged into same loop)
2058
2096
  if (pn) {
@@ -2077,21 +2115,29 @@ function genCode(schema, v, lines, ctx, knownType) {
2077
2115
  lines.push(`if(!_es${ei}.has(${kVar}))return false`)
2078
2116
  }
2079
2117
  }
2080
- // Check: is key declared or matches a pattern?
2081
- // switch/case: V8 compiles string cases to jump table (faster than chained ===)
2082
- const switchCases = propKeys.map(k => `case ${JSON.stringify(k)}:`).join('')
2083
- lines.push(`switch(${kVar}){${switchCases}break;default:`)
2084
- // Default: key is not declared — must match a pattern
2085
- let patternChecks = []
2086
- for (let i = 0; i < ppEntries.length; i++) {
2087
- patternChecks.push(`if(${matchers[i].check}){if(!_ppf${pi}_${i}(${v}[${kVar}]))return false}else{return false}`)
2118
+ // Every matching pattern applies to every key, declared or not: a
2119
+ // declared key that also matches a pattern is validated by both (the
2120
+ // properties emitter handles its own subschema). A flag rather than an
2121
+ // if/else chain, because a chain returned false at the first pattern
2122
+ // that missed and wrongly rejected a key only a later pattern matched.
2123
+ if (ppEntries.length > 0) {
2124
+ lines.push(`let _pm${pi}=false`)
2125
+ for (let i = 0; i < ppEntries.length; i++) {
2126
+ lines.push(`if(${matchers[i].check}){_pm${pi}=true;if(!_ppf${pi}_${i}(${v}[${kVar}]))return false}`)
2127
+ }
2088
2128
  }
2089
- if (patternChecks.length > 0) {
2090
- lines.push(patternChecks.join(''))
2129
+ // A key that is neither declared nor matched is additional. switch on
2130
+ // the declared names (V8 compiles string cases to a jump table); no
2131
+ // switch at all when nothing is declared, since a switch with no case
2132
+ // clause is a syntax error.
2133
+ const additional = apFn ? `if(!${apFn}(${v}[${kVar}]))return false` : `return false`
2134
+ const notMatched = ppEntries.length > 0 ? `if(!_pm${pi}){${additional}}` : additional
2135
+ if (propKeys.length) {
2136
+ const switchCases = propKeys.map(k => `case ${JSON.stringify(k)}:`).join('')
2137
+ lines.push(`switch(${kVar}){${switchCases}break;default:{${notMatched}}}`)
2091
2138
  } else {
2092
- lines.push(`return false`)
2139
+ lines.push(notMatched)
2093
2140
  }
2094
- lines.push(`}`) // end switch
2095
2141
  lines.push(`}}`)
2096
2142
  } else {
2097
2143
  // No additionalProperties: validate matching keys + propertyNames
@@ -2139,6 +2185,13 @@ function genCode(schema, v, lines, ctx, knownType) {
2139
2185
  }
2140
2186
  }
2141
2187
 
2188
+ // propertyNames: false rejects any key at all; true constrains nothing.
2189
+ if (schema.propertyNames === false) {
2190
+ lines.push(isObj
2191
+ ? `for(const _k in ${v})return false`
2192
+ : `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){for(const _k in ${v})return false}`)
2193
+ }
2194
+
2142
2195
  // propertyNames — only emit if not already merged into patternProperties loop
2143
2196
  if (schema.propertyNames && typeof schema.propertyNames === 'object' && !ctx._ppHandledPropertyNames) {
2144
2197
  const pn = schema.propertyNames
@@ -2207,7 +2260,7 @@ function genCode(schema, v, lines, ctx, knownType) {
2207
2260
  }
2208
2261
 
2209
2262
  // items — pass known type info to children
2210
- if (schema.items) {
2263
+ if (schema.items !== undefined && schema.items !== true) {
2211
2264
  const idx = `_j${ctx.varCounter}`
2212
2265
  const elem = `_e${ctx.varCounter}`
2213
2266
  ctx.varCounter++
@@ -2234,7 +2287,7 @@ function genCode(schema, v, lines, ctx, knownType) {
2234
2287
  }
2235
2288
 
2236
2289
  // contains — use helper function to avoid try/catch overhead
2237
- if (schema.contains) {
2290
+ if (schema.contains !== undefined) {
2238
2291
  const ci = ctx.varCounter++
2239
2292
  const minC = schema.minContains !== undefined ? schema.minContains : 1
2240
2293
  const maxC = schema.maxContains !== undefined ? schema.maxContains : Infinity
@@ -2314,7 +2367,7 @@ function genCode(schema, v, lines, ctx, knownType) {
2314
2367
  }
2315
2368
 
2316
2369
  // not
2317
- if (schema.not) {
2370
+ if (schema.not !== undefined) {
2318
2371
  const subLines = []
2319
2372
  genCode(schema.not, '_nv', subLines, ctx)
2320
2373
  if (subLines.length === 0) {
@@ -2326,7 +2379,7 @@ function genCode(schema, v, lines, ctx, knownType) {
2326
2379
  }
2327
2380
 
2328
2381
  // if/then/else
2329
- if (schema.if) {
2382
+ if (schema.if !== undefined) {
2330
2383
  const ifLines = []
2331
2384
  genCode(schema.if, '_iv', ifLines, ctx)
2332
2385
  const fi = ctx.varCounter++
@@ -2335,14 +2388,14 @@ function genCode(schema, v, lines, ctx, knownType) {
2335
2388
  : `function(_iv){${ifLines.join(';')};return true}`
2336
2389
 
2337
2390
  let thenFn = 'null', elseFn = 'null'
2338
- if (schema.then) {
2391
+ if (schema.then !== undefined) {
2339
2392
  const thenLines = []
2340
2393
  genCode(schema.then, '_tv', thenLines, ctx)
2341
2394
  thenFn = thenLines.length === 0
2342
2395
  ? `function(_tv){return true}`
2343
2396
  : `function(_tv){${thenLines.join(';')};return true}`
2344
2397
  }
2345
- if (schema.else) {
2398
+ if (schema.else !== undefined) {
2346
2399
  const elseLines = []
2347
2400
  genCode(schema.else, '_ev', elseLines, ctx)
2348
2401
  elseFn = elseLines.length === 0
@@ -2846,9 +2899,7 @@ const FORMAT_CODEGEN = {
2846
2899
  ? `{const _at=${v}.indexOf('@');if(_at<=0||_at>=${v}.length-1||${v}.indexOf('.',_at)<=_at+1)return false}`
2847
2900
  : `if(typeof ${v}==='string'){const _at=${v}.indexOf('@');if(_at<=0||_at>=${v}.length-1||${v}.indexOf('.',_at)<=_at+1)return false}`
2848
2901
  },
2849
- date: (v, isStr) => isStr
2850
- ? `{if(${v}.length!==10||!/^\\d{4}-\\d{2}-\\d{2}$/.test(${v}))return false;const _dm=+${v}.slice(5,7),_dd=+${v}.slice(8,10);if(_dm<1||_dm>12||_dd<1||_dd>31)return false}`
2851
- : `if(typeof ${v}==='string'){if(${v}.length!==10||!/^\\d{4}-\\d{2}-\\d{2}$/.test(${v}))return false;const _dm=+${v}.slice(5,7),_dd=+${v}.slice(8,10);if(_dm<1||_dm>12||_dd<1||_dd>31)return false}`,
2902
+ date: _formats.dateSource,
2852
2903
  uuid: (v, isStr) => isStr
2853
2904
  ? `if(${v}.length!==36||!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(${v}))return false`
2854
2905
  : `if(typeof ${v}==='string'&&(${v}.length!==36||!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(${v})))return false`,
@@ -2867,9 +2918,7 @@ const FORMAT_CODEGEN = {
2867
2918
  'uri-reference': (v, isStr) => isStr
2868
2919
  ? `if(/[\\s\\u0000-\\u001f\\u007f]/.test(${v}))return false`
2869
2920
  : `if(typeof ${v}==='string'&&/[\\s\\u0000-\\u001f\\u007f]/.test(${v}))return false`,
2870
- ipv4: (v, isStr) => isStr
2871
- ? `{const _p=${v}.split('.');if(_p.length!==4||!_p.every(function(n){var x=+n;return x>=0&&x<=255&&String(x)===n}))return false}`
2872
- : `if(typeof ${v}==='string'){const _p=${v}.split('.');if(_p.length!==4||!_p.every(function(n){var x=+n;return x>=0&&x<=255&&String(x)===n}))return false}`,
2921
+ ipv4: _formats.ipv4Source,
2873
2922
  ipv6: (v, isStr) => isStr
2874
2923
  ? `{const _s=${v};if(_s===''||!/^[0-9a-fA-F:]+$/.test(_s)||_s.split(':').length<3||_s.split(':').length>8)return false}`
2875
2924
  : `if(typeof ${v}==='string'){const _s=${v};if(_s===''||!/^[0-9a-fA-F:]+$/.test(_s)||_s.split(':').length<3||_s.split(':').length>8)return false}`,
@@ -3032,19 +3081,16 @@ function compileToJSCodegenWithErrors(schema, schemaMap, userFormats, sourceOpts
3032
3081
  if (!sharedCodegenGate(schema, schemaMap)) return null
3033
3082
  if (schema.patternProperties) {
3034
3083
  for (const [pat, sub] of Object.entries(schema.patternProperties)) {
3035
- if (typeof sub === 'boolean') return null
3036
3084
  if (/\\[pP]\{/.test(pat)) return null
3037
3085
  if (typeof sub === 'object' && sub !== null && !codegenSafe(sub, schemaMap)) return null
3038
3086
  }
3039
3087
  }
3040
3088
  if (schema.dependentSchemas) {
3041
3089
  for (const sub of Object.values(schema.dependentSchemas)) {
3042
- if (typeof sub === 'boolean') return null
3043
3090
  if (typeof sub === 'object' && sub !== null && !codegenSafe(sub, schemaMap)) return null
3044
3091
  }
3045
3092
  }
3046
3093
  if (schema.propertyNames) {
3047
- if (typeof schema.propertyNames === 'boolean') return null
3048
3094
  const pn = schema.propertyNames
3049
3095
  const supported = ['maxLength', 'minLength', 'pattern', 'const', 'enum']
3050
3096
  const keys = Object.keys(pn).filter(k => k !== '$schema')
@@ -3145,6 +3191,10 @@ function emitBranchCollapse (branches, keyword, v, pathExpr, lines, ctx, schemaP
3145
3191
  const subSp = branchSp + '/' + i
3146
3192
  if (typeof sub === 'object' && sub !== null) {
3147
3193
  genCodeE(sub, '_bv', pathExpr, subLines, ctx, subSp)
3194
+ } else if (sub === false) {
3195
+ // A false branch never matches; it reports the same error the
3196
+ // interpreter attaches to a false schema.
3197
+ subLines.push(`_e.push({keyword:'not',instancePath:${pathExpr||'""'},schemaPath:'${subSp}',params:{},message:'boolean schema is false'})`)
3148
3198
  }
3149
3199
  const title = (sub && typeof sub === 'object' && typeof sub.title === 'string') ? sub.title : ''
3150
3200
  titles.push(title)
@@ -3166,6 +3216,16 @@ function emitBranchCollapse (branches, keyword, v, pathExpr, lines, ctx, schemaP
3166
3216
  // `_all` parameter: if falsy, return after first error.
3167
3217
  function genCodeE(schema, v, pathExpr, lines, ctx, schemaPrefix) {
3168
3218
  if (!schemaPrefix) schemaPrefix = '#'
3219
+ // A false schema reports one error at the value that reached it, with the
3220
+ // keyword the interpreter has always used for it, `not` (lib/plan-compiler.js
3221
+ // line 58; its `err()` discards the 'false schema' label). Other validators
3222
+ // name this keyword `false schema`; changing it here would change output
3223
+ // the interpreter path already produces, so both engines say `not`.
3224
+ if (schema === false) {
3225
+ lines.push(`;_e.push({keyword:'not',instancePath:${pathExpr||'""'},schemaPath:'${schemaPrefix}',params:{},message:'boolean schema is false'});if(!_all)return{valid:false,errors:_e}`)
3226
+ return
3227
+ }
3228
+ if (schema === true) return
3169
3229
  if (typeof schema !== 'object' || schema === null) return
3170
3230
  if (!ctx.regExpMap) {
3171
3231
  ctx.regExpMap = new Map();
@@ -3176,9 +3236,26 @@ function genCodeE(schema, v, pathExpr, lines, ctx, schemaPrefix) {
3176
3236
  if (schema.$ref === '#') return
3177
3237
  const m = schema.$ref.match(/^#\/(?:\$defs|definitions)\/(.+)$/)
3178
3238
  if (m && ctx.rootDefs && ctx.rootDefs[m[1]]) {
3239
+ const defName = m[1]
3240
+ if (!ctx.cyclicDefs) ctx.cyclicDefs = cyclicDefNames(ctx.rootDefs)
3241
+ if (ctx.cyclicDefs.has(defName)) {
3242
+ // Named helper for a def on a cycle; the path comes in as an
3243
+ // argument because every call site has its own.
3244
+ if (!ctx.defFns) ctx.defFns = new Map()
3245
+ let fnName = ctx.defFns.get(defName)
3246
+ if (!fnName) {
3247
+ fnName = '_defE' + ctx.defFns.size + '_' + defName.replace(/[^A-Za-z0-9_]/g, '_')
3248
+ ctx.defFns.set(defName, fnName)
3249
+ const bodyLines = []
3250
+ genCodeE(ctx.rootDefs[defName], 'd', '_p', bodyLines, ctx, '#/$defs/' + defName)
3251
+ ctx.helperCode.push(`function ${fnName}(d,_p,_all,_e){${bodyLines.join('\n ')}}`)
3252
+ }
3253
+ lines.push(`${fnName}(${v},${pathExpr || '""'},_all,_e);if(!_all&&_e.length)return{valid:false,errors:_e}`)
3254
+ return
3255
+ }
3179
3256
  if (ctx.refStack.has(schema.$ref)) return
3180
3257
  ctx.refStack.add(schema.$ref)
3181
- genCodeE(ctx.rootDefs[m[1]], v, pathExpr, lines, ctx, schemaPrefix)
3258
+ genCodeE(ctx.rootDefs[defName], v, pathExpr, lines, ctx, schemaPrefix)
3182
3259
  ctx.refStack.delete(schema.$ref)
3183
3260
  return
3184
3261
  }
@@ -3496,7 +3573,26 @@ function genCodeE(schema, v, pathExpr, lines, ctx, schemaPrefix) {
3496
3573
  const ki = ctx.varCounter++
3497
3574
  const known = Object.keys(schema.properties || {})
3498
3575
  const guard = known.length ? `const _ak${ki}=new Set([${known.map((k) => JSON.stringify(k)).join(',')}]);` : ''
3499
- const keep = known.length ? `if(!_ak${ki}.has(_k${ki})){` : `{`
3576
+ // A key matched by any patternProperties entry is not additional.
3577
+ const patTests = []
3578
+ for (const pat of Object.keys(schema.patternProperties || {})) {
3579
+ const pattern = JSON.stringify(pat)
3580
+ if (!ctx.regExpMap.has(pattern)) {
3581
+ const ri = ctx.varCounter++
3582
+ ctx.regExpMap.set(pattern, ri)
3583
+ if (patternIsSafe(pat)) {
3584
+ ctx.helperCode.push(`const _re${ri}=__ataSafeRe(${pattern})`)
3585
+ ctx.usesSafeRe = true
3586
+ } else {
3587
+ ctx.helperCode.push(`const _re${ri}=new RegExp(${pattern})`)
3588
+ }
3589
+ }
3590
+ patTests.push(`_re${ctx.regExpMap.get(pattern)}.test(_k${ki})`)
3591
+ }
3592
+ const conds = []
3593
+ if (known.length) conds.push(`!_ak${ki}.has(_k${ki})`)
3594
+ if (patTests.length) conds.push(`!(${patTests.join('||')})`)
3595
+ const keep = conds.length ? `if(${conds.join('&&')}){` : `{`
3500
3596
  lines.push(`if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){${guard}for(const _k${ki} in ${v}){${keep}`)
3501
3597
  const p = pathExpr ? `${pathExpr}+'/'+_k${ki}` : `'/'+_k${ki}`
3502
3598
  genCodeE(schema.additionalProperties, `${v}[_k${ki}]`, p, lines, ctx, schemaPrefix + '/additionalProperties')
@@ -3512,6 +3608,14 @@ function genCodeE(schema, v, pathExpr, lines, ctx, schemaPrefix) {
3512
3608
  }
3513
3609
  }
3514
3610
 
3611
+ // propertyNames: false reports every key, at the key's own path, the way the
3612
+ // interpreter does (plan-compiler: instancePath + '/' + key).
3613
+ if (schema.propertyNames === false) {
3614
+ const ki = ctx.varCounter++
3615
+ const p = pathExpr ? `${pathExpr}+'/'+_k${ki}` : `'/'+_k${ki}`
3616
+ lines.push(`if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){for(const _k${ki} in ${v}){_e.push({keyword:'not',instancePath:${p},schemaPath:'${schemaPrefix}/propertyNames',params:{},message:'boolean schema is false'});if(!_all)return{valid:false,errors:_e}}}`)
3617
+ }
3618
+
3515
3619
  // propertyNames
3516
3620
  if (schema.propertyNames && typeof schema.propertyNames === 'object') {
3517
3621
  const pn = schema.propertyNames
@@ -3550,7 +3654,7 @@ function genCodeE(schema, v, pathExpr, lines, ctx, schemaPrefix) {
3550
3654
  }
3551
3655
 
3552
3656
  // items — starts after prefixItems (Draft 2020-12 semantics)
3553
- if (schema.items) {
3657
+ if (schema.items !== undefined && schema.items !== true) {
3554
3658
  const startIdx = schema.prefixItems ? schema.prefixItems.length : 0
3555
3659
  const idx = `_j${ctx.varCounter}`
3556
3660
  const elem = `_ei${ctx.varCounter}`
@@ -3572,7 +3676,7 @@ function genCodeE(schema, v, pathExpr, lines, ctx, schemaPrefix) {
3572
3676
  }
3573
3677
 
3574
3678
  // contains
3575
- if (schema.contains) {
3679
+ if (schema.contains !== undefined) {
3576
3680
  const ci = ctx.varCounter++
3577
3681
  const subLines = []
3578
3682
  genCode(schema.contains, `_cv`, subLines, ctx)
@@ -3605,7 +3709,7 @@ function genCodeE(schema, v, pathExpr, lines, ctx, schemaPrefix) {
3605
3709
  }
3606
3710
 
3607
3711
  // not
3608
- if (schema.not) {
3712
+ if (schema.not !== undefined) {
3609
3713
  const subLines = []
3610
3714
  genCode(schema.not, '_nv', subLines, ctx)
3611
3715
  const nfn = subLines.length === 0 ? `function(_nv){return true}` : `function(_nv){${subLines.join(';')};return true}`
@@ -3614,7 +3718,7 @@ function genCodeE(schema, v, pathExpr, lines, ctx, schemaPrefix) {
3614
3718
  }
3615
3719
 
3616
3720
  // if/then/else
3617
- if (schema.if) {
3721
+ if (schema.if !== undefined) {
3618
3722
  const ifLines = []
3619
3723
  genCode(schema.if, '_iv', ifLines, ctx)
3620
3724
  const fi = ctx.varCounter++
@@ -3622,13 +3726,13 @@ function genCodeE(schema, v, pathExpr, lines, ctx, schemaPrefix) {
3622
3726
  ? `function(_iv){return true}`
3623
3727
  : `function(_iv){${ifLines.join(';')};return true}`
3624
3728
  lines.push(`{const _if${fi}=${ifFn}`)
3625
- if (schema.then) {
3729
+ if (schema.then !== undefined) {
3626
3730
  lines.push(`if(_if${fi}(${v})){`)
3627
3731
  genCodeE(schema.then, v, pathExpr, lines, ctx, schemaPrefix+'/then')
3628
3732
  lines.push(`}`)
3629
3733
  }
3630
- if (schema.else) {
3631
- lines.push(`${schema.then ? 'else' : `if(!_if${fi}(${v}))`}{`)
3734
+ if (schema.else !== undefined) {
3735
+ lines.push(`${schema.then !== undefined ? 'else' : `if(!_if${fi}(${v}))`}{`)
3632
3736
  genCodeE(schema.else, v, pathExpr, lines, ctx, schemaPrefix+'/else')
3633
3737
  lines.push(`}`)
3634
3738
  }
@@ -3647,6 +3751,10 @@ function compileToJSCombined(schema, VALID_RESULT, schemaMap, userFormats) {
3647
3751
  if (s.includes('unevaluatedProperties') || s.includes('unevaluatedItems')) return null
3648
3752
  // Bail on self-referencing schemas — combined codegen doesn't support recursion
3649
3753
  if (s.includes('"$ref":"#"')) return null
3754
+ // Defs on a $ref cycle compile as named functions in the boolean and
3755
+ // errors generators; this generator has no preamble for them, so it
3756
+ // declines and the router uses the errors generator.
3757
+ if (cyclicDefNames(schema.$defs || schema.definitions).size) return null
3650
3758
  // Bail on additionalProperties as a schema — combined path doesn't emit the
3651
3759
  // per-key sub-schema loop yet. The jsFn + errFn fallback handles it correctly.
3652
3760
  if (hasAdditionalPropertiesSchema(schema)) return null
@@ -3665,19 +3773,16 @@ function compileToJSCombined(schema, VALID_RESULT, schemaMap, userFormats) {
3665
3773
  if (!sharedCodegenGate(schema, schemaMap)) return null
3666
3774
  if (schema.patternProperties) {
3667
3775
  for (const [pat, sub] of Object.entries(schema.patternProperties)) {
3668
- if (typeof sub === 'boolean') return null
3669
3776
  if (/\\[pP]\{/.test(pat)) return null
3670
3777
  if (typeof sub === 'object' && sub !== null && !codegenSafe(sub, schemaMap)) return null
3671
3778
  }
3672
3779
  }
3673
3780
  if (schema.dependentSchemas) {
3674
3781
  for (const sub of Object.values(schema.dependentSchemas)) {
3675
- if (typeof sub === 'boolean') return null
3676
3782
  if (typeof sub === 'object' && sub !== null && !codegenSafe(sub, schemaMap)) return null
3677
3783
  }
3678
3784
  }
3679
3785
  if (schema.propertyNames) {
3680
- if (typeof schema.propertyNames === 'boolean') return null
3681
3786
  const pn = schema.propertyNames
3682
3787
  const supported = ['maxLength', 'minLength', 'pattern', 'const', 'enum']
3683
3788
  const keys = Object.keys(pn).filter(k => k !== '$schema')
@@ -3747,6 +3852,13 @@ function compileToJSCombined(schema, VALID_RESULT, schemaMap, userFormats) {
3747
3852
  // If type check fails → push error, skip property checks (they'd crash).
3748
3853
  function genCodeC(schema, v, pathExpr, lines, ctx, schemaPrefix) {
3749
3854
  if (!schemaPrefix) schemaPrefix = '#'
3855
+ if (schema === false) {
3856
+ // Leading semicolon: the previous generated line may end in an
3857
+ // expression, and a line starting with `(` would be parsed as a call.
3858
+ lines.push(`;(_e||(_e=[])).push({keyword:'not',instancePath:${pathExpr||'""'},schemaPath:'${schemaPrefix}',params:{},message:'boolean schema is false'})`)
3859
+ return
3860
+ }
3861
+ if (schema === true) return
3750
3862
  if (typeof schema !== 'object' || schema === null) return
3751
3863
 
3752
3864
  // $ref — resolve local, anchor, and cross-schema refs
@@ -4181,6 +4293,12 @@ function genCodeC(schema, v, pathExpr, lines, ctx, schemaPrefix) {
4181
4293
  }
4182
4294
  }
4183
4295
 
4296
+ if (schema.propertyNames === false) {
4297
+ const ki = ctx.varCounter++
4298
+ const p = pathExpr ? `${pathExpr}+'/'+_k${ki}` : `'/'+_k${ki}`
4299
+ lines.push(`;if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){for(const _k${ki} in ${v}){(_e||(_e=[])).push({keyword:'not',instancePath:${p},schemaPath:'${schemaPrefix}/propertyNames',params:{},message:'boolean schema is false'})}}`)
4300
+ }
4301
+
4184
4302
  // propertyNames — skip if already merged into patternProperties loop
4185
4303
  if (schema.propertyNames && typeof schema.propertyNames === 'object' && !ctx._ppHandledPropertyNamesC) {
4186
4304
  const pn = schema.propertyNames
@@ -4211,7 +4329,7 @@ function genCodeC(schema, v, pathExpr, lines, ctx, schemaPrefix) {
4211
4329
  }
4212
4330
 
4213
4331
  // items
4214
- if (schema.items) {
4332
+ if (schema.items !== undefined && schema.items !== true) {
4215
4333
  const startIdx = schema.prefixItems ? schema.prefixItems.length : 0
4216
4334
  const idx = `_j${ctx.varCounter}`, elem = `_ei${ctx.varCounter}`
4217
4335
  ctx.varCounter++
@@ -4232,7 +4350,7 @@ function genCodeC(schema, v, pathExpr, lines, ctx, schemaPrefix) {
4232
4350
  }
4233
4351
 
4234
4352
  // contains
4235
- if (schema.contains) {
4353
+ if (schema.contains !== undefined) {
4236
4354
  const ci = ctx.varCounter++
4237
4355
  const subLines = []
4238
4356
  genCode(schema.contains, `_cv`, subLines, ctx)
@@ -4263,7 +4381,7 @@ function genCodeC(schema, v, pathExpr, lines, ctx, schemaPrefix) {
4263
4381
  }
4264
4382
 
4265
4383
  // not
4266
- if (schema.not) {
4384
+ if (schema.not !== undefined) {
4267
4385
  const sl = []; genCode(schema.not, '_nv', sl, ctx)
4268
4386
  const nfn = sl.length === 0 ? `function(_nv){return true}` : `function(_nv){${sl.join(';')};return true}`
4269
4387
  const fi = ctx.varCounter++
@@ -4271,13 +4389,13 @@ function genCodeC(schema, v, pathExpr, lines, ctx, schemaPrefix) {
4271
4389
  }
4272
4390
 
4273
4391
  // if/then/else
4274
- if (schema.if) {
4392
+ if (schema.if !== undefined) {
4275
4393
  const sl = []; genCode(schema.if, '_iv', sl, ctx)
4276
4394
  const fi = ctx.varCounter++
4277
4395
  const ifFn = sl.length === 0 ? `function(_iv){return true}` : `function(_iv){${sl.join(';')};return true}`
4278
4396
  lines.push(`{const _if${fi}=${ifFn}`)
4279
- if (schema.then) { lines.push(`if(_if${fi}(${v})){`); genCodeC(schema.then, v, pathExpr, lines, ctx, schemaPrefix+'/then'); lines.push(`}`) }
4280
- if (schema.else) { lines.push(`${schema.then ? 'else' : `if(!_if${fi}(${v}))`}{`); genCodeC(schema.else, v, pathExpr, lines, ctx, schemaPrefix+'/else'); lines.push(`}`) }
4397
+ if (schema.then !== undefined) { lines.push(`if(_if${fi}(${v})){`); genCodeC(schema.then, v, pathExpr, lines, ctx, schemaPrefix+'/then'); lines.push(`}`) }
4398
+ if (schema.else !== undefined) { lines.push(`${schema.then !== undefined ? 'else' : `if(!_if${fi}(${v}))`}{`); genCodeC(schema.else, v, pathExpr, lines, ctx, schemaPrefix+'/else'); lines.push(`}`) }
4281
4399
  lines.push(`}`)
4282
4400
  }
4283
4401
 
@@ -4362,7 +4480,7 @@ function _collectEval(schema, result, defs, schemaMap, refStack, isRoot) {
4362
4480
 
4363
4481
  // contains: marks matching items as evaluated (not ALL items)
4364
4482
  // Always set dynamic since which items match depends on the data
4365
- if (schema.contains) {
4483
+ if (schema.contains !== undefined) {
4366
4484
  result.dynamic = true
4367
4485
  }
4368
4486
 
package/lib/version.js CHANGED
@@ -7,4 +7,4 @@
7
7
  //
8
8
  // Kept in lockstep with package.json by `tests/test_version_sync.js`.
9
9
 
10
- module.exports = '1.9.0';
10
+ module.exports = '1.10.0';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ata-validator",
3
- "version": "1.9.0",
3
+ "version": "1.10.0",
4
4
  "description": "JSON Schema validation with first-class TypeScript and zero runtime cost. AOT compile to per-schema ESM modules with zero validator dependency. Generic Validator<T> for TypeBox/Zod/Valibot composition. Optional runtime API. Standard Schema V1 compatible.",
5
5
  "main": "index.js",
6
6
  "module": "index.mjs",
@@ -44,7 +44,7 @@
44
44
  "release:check": "node scripts/regen-safe-regex-source.js && node tests/test_pack_purity.js && node scripts/check-doc-coverage.js && node tests/test_error_codes_lock.js && node tests/test_safe_regex_source_sync.js && node tests/test_version_sync.js",
45
45
  "build": "cmake-js build --target ata",
46
46
  "rebuild": "cmake-js rebuild --target ata",
47
- "test": "node test.js && node tests/test_removed_aot_methods.js && node tests/test_no_native.js && node tests/test_no_eval.js && node tests/test_property_dependencies.js && node tests/test_v1_dialect.js && node tests/test_buffer_path_parity.js && node tests/test_buffer_gate.js && node tests/test_draft7_semantics.js && node tests/test_metaschema_ref.js && node tests/test_pure_js_unsupported.js && node tests/test_native_load_order.js && node tests/test_pack_purity.js && node tests/test_make_native_package.js && node tests/test_browser_nofs.js && node tests/test_browser_imports_guard.js && node tests/test_version_sync.js && node tests/test_native_loaded.js && node tests/test_safe_regex_source_sync.js && node tests/test_t_builder.js && node tests/test_async_refine.js && node tests/test_safe_regex.js && node tests/test_safe_regex_integration.js && node tests/test_aot_build.js && node tests/test_aot_differential.js && node tests/test_aot_cli_build.js && node tests/test_aot_cli_smoke.js && node tests/test_bundle_standalone.js && node tests/test_standalone_anyof.js && node tests/test_standalone_formats.js && node tests/test_aot_format_mode.js && node tests/test_aot_additional_props_errors.js && node tests/test_id_anchor_refs.js && node tests/test_engine_routing.js && node tests/test_engine_diagnostic.js && node tests/test_format_engine_parity.js && node tests/test_defs_pointer_alias.js && node tests/test_cross_doc_root_ref.js && node tests/test_codegen_entrypoint_agreement.js && node tests/test_hybrid_agreement.js && node tests/test_ajv_errors.js && node tests/test_pattern_properties_errors.js && node tests/test_no_input_mutation.js && node tests/test_typed_validator_runner.js && node tests/test_define_schema.js && node tests/test_error_codes_lock.js && node tests/test_error_code_lookup.js && node tests/test_error_order.js && node tests/test_value_equality.js && node tests/test_vocabulary.js && node tests/test_schema_scan.js && node tests/test_lazy_errors.js && node tests/test_plan_compiler.js && node tests/test_nullable.js && node tests/test_validate_and_parse.js && node tests/test_validate_data.js && node tests/test_enrich_error.js && node tests/test_enrich_received.js && node tests/test_rich_errors_optout.js && node tests/test_error_messages.js && node tests/test_source_positions.js && node tests/fuzz_positions.js && node tests/test_data_positions.js && node tests/test_render_shared.js && node tests/test_renderers.js && node tests/test_additive_fields.js && node tests/test_diagnostic_source.js && node tests/test_diagnose.js && node tests/test_correlate.js && node tests/test_diagnostics_score.js && node tests/test_runtime_error_dx.js && node tests/test_aot_error_dx.js && node tests/test_abort_early.js && node tests/test_branch_collapse.js && node tests/test_suggestions.js && node tests/test_cli_validate.js && node tests/test_cli_version.js && node benchmark/bench_aot_size.mjs",
47
+ "test": "node test.js && node tests/test_removed_aot_methods.js && node tests/test_no_native.js && node tests/test_no_eval.js && node tests/test_property_dependencies.js && node tests/test_v1_dialect.js && node tests/test_buffer_path_parity.js && node tests/test_buffer_gate.js && node tests/test_draft7_semantics.js && node tests/test_metaschema_ref.js && node tests/test_pure_js_unsupported.js && node tests/test_native_load_order.js && node tests/test_pack_purity.js && node tests/test_make_native_package.js && node tests/test_browser_nofs.js && node tests/test_browser_imports_guard.js && node tests/test_version_sync.js && node tests/test_native_loaded.js && node tests/test_safe_regex_source_sync.js && node tests/test_t_builder.js && node tests/test_async_refine.js && node tests/test_safe_regex.js && node tests/test_safe_regex_integration.js && node tests/test_aot_build.js && node tests/test_aot_differential.js && node tests/test_aot_cli_build.js && node tests/test_aot_cli_smoke.js && node tests/test_bundle_standalone.js && node tests/test_standalone_anyof.js && node tests/test_standalone_formats.js && node tests/test_aot_format_mode.js && node tests/test_aot_additional_props_errors.js && node tests/test_id_anchor_refs.js && node tests/test_engine_routing.js && node tests/test_engine_diagnostic.js && node tests/test_format_engine_parity.js && node tests/test_formats_single_pass.js && node tests/test_defs_pointer_alias.js && node tests/test_cross_doc_root_ref.js && node tests/test_codegen_entrypoint_agreement.js && node tests/test_hybrid_agreement.js && node tests/test_codegen_edge_shapes.js && node tests/test_ajv_errors.js && node tests/test_pattern_properties_errors.js && node tests/test_no_input_mutation.js && node tests/test_typed_validator_runner.js && node tests/test_define_schema.js && node tests/test_error_codes_lock.js && node tests/test_error_code_lookup.js && node tests/test_error_order.js && node tests/test_value_equality.js && node tests/test_vocabulary.js && node tests/test_schema_scan.js && node tests/test_lazy_errors.js && node tests/test_plan_compiler.js && node tests/test_nullable.js && node tests/test_validate_and_parse.js && node tests/test_validate_data.js && node tests/test_enrich_error.js && node tests/test_enrich_received.js && node tests/test_rich_errors_optout.js && node tests/test_error_messages.js && node tests/test_source_positions.js && node tests/fuzz_positions.js && node tests/test_data_positions.js && node tests/test_render_shared.js && node tests/test_renderers.js && node tests/test_additive_fields.js && node tests/test_diagnostic_source.js && node tests/test_diagnose.js && node tests/test_correlate.js && node tests/test_diagnostics_score.js && node tests/test_runtime_error_dx.js && node tests/test_aot_error_dx.js && node tests/test_abort_early.js && node tests/test_branch_collapse.js && node tests/test_suggestions.js && node tests/test_cli_validate.js && node tests/test_cli_version.js && node benchmark/bench_aot_size.mjs",
48
48
  "bench:size": "node benchmark/bench_aot_size.mjs",
49
49
  "test:suite": "node tests/run_suite.js && node tests/run_suite.js draft7 && node tests/run_suite.js v1",
50
50
  "test:compat": "node tests/test_compat.js",
@@ -112,13 +112,13 @@
112
112
  "LICENSE"
113
113
  ],
114
114
  "optionalDependencies": {
115
- "@ata-validator/native-darwin-arm64": "1.9.0",
116
- "@ata-validator/native-darwin-x64": "1.9.0",
117
- "@ata-validator/native-linux-x64-gnu": "1.9.0",
118
- "@ata-validator/native-linux-arm64-gnu": "1.9.0",
119
- "@ata-validator/native-linux-x64-musl": "1.9.0",
120
- "@ata-validator/native-linux-arm64-musl": "1.9.0",
121
- "@ata-validator/native-win32-x64": "1.9.0"
115
+ "@ata-validator/native-darwin-arm64": "1.10.0",
116
+ "@ata-validator/native-darwin-x64": "1.10.0",
117
+ "@ata-validator/native-linux-x64-gnu": "1.10.0",
118
+ "@ata-validator/native-linux-arm64-gnu": "1.10.0",
119
+ "@ata-validator/native-linux-x64-musl": "1.10.0",
120
+ "@ata-validator/native-linux-arm64-musl": "1.10.0",
121
+ "@ata-validator/native-win32-x64": "1.10.0"
122
122
  },
123
123
  "peerDependencies": {
124
124
  "yaml": "^2.0.0"