ata-validator 1.48.0 → 1.49.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +26 -3
- package/index.js +22 -4
- package/lib/aot-impl.js +9 -1
- package/lib/draft7.js +106 -8
- package/lib/enrich-error.js +42 -11
- package/lib/enrich-site.js +1 -1
- package/lib/js-compiler-errors.js +6 -4
- package/lib/js-compiler.js +50 -2
- package/lib/metaschemas.js +21 -2
- package/lib/regex-linear.js +194 -37
- package/lib/safe-regex.js +23 -9
- package/lib/validator-core.js +18 -15
- package/lib/version.js +1 -1
- package/lib/vocabularies.js +12 -0
- package/package.json +9 -9
package/README.md
CHANGED
|
@@ -109,6 +109,29 @@ file.
|
|
|
109
109
|
passes with nothing failed, errored or skipped under Bowtie's own runner. The v1 dialect is
|
|
110
110
|
declared in a pending harness change that waits for a Bowtie release.
|
|
111
111
|
|
|
112
|
+
### Side by side
|
|
113
|
+
|
|
114
|
+
[validator-benchmarks](https://github.com/ata-core/validator-benchmarks) is ata's own
|
|
115
|
+
comparison, so it comes after the ones above: eleven validators on three realistic bodies
|
|
116
|
+
and four scenes, each library in its own process, 27 samples per cell, every schema and
|
|
117
|
+
document in the repository. On an order body with formats and a pattern, 900 valid
|
|
118
|
+
documents, Node 24:
|
|
119
|
+
|
|
120
|
+
[](https://github.com/ata-core/validator-benchmarks/blob/main/media/race-order-verdict-valid.mp4)
|
|
121
|
+
|
|
122
|
+
| | documents a second, the verdict | same, `new Function` refused |
|
|
123
|
+
|---|---:|---:|
|
|
124
|
+
| ata | 3,295,880 | 607,614 |
|
|
125
|
+
| ata, compiled module | 3,319,594 | 3,344,948 |
|
|
126
|
+
| typebox | 1,638,813 | EvalError |
|
|
127
|
+
| ajv | 1,285,492 | EvalError |
|
|
128
|
+
| typia | 939,572 | 964,507 |
|
|
129
|
+
| zod | 440,473 | 237,176 |
|
|
130
|
+
|
|
131
|
+
Where ata is behind is in the same tables: on a four-field body that fails, ajv answers the
|
|
132
|
+
verdict faster, and with every error read it does 6.6M a second against ata's 2.9M with rich
|
|
133
|
+
errors (7.6M with `richErrors: false`).
|
|
134
|
+
|
|
112
135
|
## Why AOT
|
|
113
136
|
|
|
114
137
|
| Dimension | Schema | ata-AOT | runtime validator | Difference |
|
|
@@ -254,13 +277,13 @@ is the most common way to get a misleading number out of this library.
|
|
|
254
277
|
|
|
255
278
|
| | compiled with `ata build` | runtime `new Validator(schema)` |
|
|
256
279
|
|---|---|---|
|
|
257
|
-
| In a bundle, gzipped | **2.1 KB** |
|
|
280
|
+
| In a bundle, gzipped | **2.1 KB** | 126.1 KB |
|
|
258
281
|
| Time to a served request | **3.5 ms** | 7.6 ms |
|
|
259
282
|
| Schema known when | build time | any time |
|
|
260
283
|
|
|
261
284
|
The bundle row is the ten-field user schema in
|
|
262
285
|
`tests/fixtures/error-dx/user.schema.json`, every export of the compiled module against
|
|
263
|
-
`new Validator(schema)`, built with `bun build --minify --target=browser` on ata 1.
|
|
286
|
+
`new Validator(schema)`, built with `bun build --minify --target=browser` on ata 1.48.0.
|
|
264
287
|
The startup row is a Hono route on Bun 1.4, the median of three rounds of best-of-seven, from
|
|
265
288
|
`benchmark/bundle`, against 3.5 ms for the same app doing no validation at all, so the
|
|
266
289
|
compiled path costs nothing measurable to start. The runtime figure is what it is because
|
|
@@ -703,7 +726,7 @@ Copy-paste recipes for the common frameworks. Most need 10-20 lines of glue. See
|
|
|
703
726
|
|
|
704
727
|
### Dialects
|
|
705
728
|
|
|
706
|
-
`$schema` selects the dialect. Draft 2020-12 is the default
|
|
729
|
+
`$schema` selects the dialect. Draft 2020-12 is the default; draft-07, draft-06 and Draft 2019-09 are rewritten to the 2020-12 spelling on the way in (draft-06 as draft-07 without `if`/`then`/`else`, 2019-09's tuple `items` and `$recursiveRef` as `prefixItems` and `$dynamicRef`); and `https://json-schema.org/v1` (or the dated `https://json-schema.org/v1/2026`) selects JSON Schema v1. The official suite passes in full on all five: 1301 of 1301 on 2020-12, 1261 of 1261 on 2019-09, 929 of 929 on draft-07, 841 of 841 on draft-06 and 1135 of 1135 on v1, with code generation allowed and blocked. Draft-04 and earlier are not supported; draft-04's boolean `exclusiveMinimum`/`exclusiveMaximum` is read wherever it appears.
|
|
707
730
|
|
|
708
731
|
Two things differ under v1. `propertyDependencies` selects a subschema by the value of a property rather than by its presence, which is what `dependentSchemas` does. And `$dynamicRef` no longer requires bookending: the reference resolves through the dynamic scope whether or not the schema it first lands on carries a matching `$dynamicAnchor`, so the outermost matching anchor still in scope wins. Everything else ata implements is identical under both dialects, so a schema that declares no `$schema` behaves exactly as before.
|
|
709
732
|
|
package/index.js
CHANGED
|
@@ -814,22 +814,40 @@ function hasDefaultsInside(node, seen) {
|
|
|
814
814
|
function emitDefaults(node, ov, lines, st) {
|
|
815
815
|
if (st.seen.has(node)) { st.cycle = true; return; }
|
|
816
816
|
st.seen.add(node);
|
|
817
|
+
// A default is written only where the key is not an own property, and the
|
|
818
|
+
// own-property test used to be Object.hasOwn for every default on every
|
|
819
|
+
// call: on a configuration schema of 82 defaults that was 820 ns a call
|
|
820
|
+
// for a document that already carried every key, against 12 ns with the
|
|
821
|
+
// pass off. For a plain object (prototype Object.prototype) a key the
|
|
822
|
+
// prototype does not have is own exactly when reading it gives a value, so
|
|
823
|
+
// the read, an inline-cached load, settles the common case and hasOwn is
|
|
824
|
+
// consulted only when the read gives undefined. Keys Object.prototype does
|
|
825
|
+
// have (constructor, toString, __proto__) keep the hasOwn test, since the
|
|
826
|
+
// read would find the inherited one; so does any object with another
|
|
827
|
+
// prototype, where a read may find an inherited value of the same name.
|
|
828
|
+
const pk = '_pk' + st.n++;
|
|
829
|
+
lines.push(`const ${pk}=Object.getPrototypeOf(${ov})===Object.prototype`);
|
|
817
830
|
for (const [key, prop] of Object.entries(node.properties || {})) {
|
|
818
831
|
if (!prop || typeof prop !== 'object') continue;
|
|
819
832
|
const k = JSON.stringify(key);
|
|
833
|
+
const inherited = key in Object.prototype;
|
|
834
|
+
const absent = inherited ? `!Object.hasOwn(${ov},${k})` : `(${ov}[${k}]===undefined||!${pk})&&!Object.hasOwn(${ov},${k})`;
|
|
820
835
|
if (prop.default !== undefined) {
|
|
821
836
|
const def = JSON.stringify(prop.default);
|
|
822
837
|
// Assignment to a key named __proto__ hits the prototype setter
|
|
823
838
|
// instead of creating a property; defineProperty writes an own key.
|
|
824
839
|
lines.push(key === '__proto__'
|
|
825
|
-
? `if(
|
|
826
|
-
: `if(
|
|
840
|
+
? `if(${absent})Object.defineProperty(${ov},${k},{value:${def},writable:true,enumerable:true,configurable:true})`
|
|
841
|
+
: `if(${absent})${ov}[${k}]=${def}`);
|
|
827
842
|
}
|
|
828
843
|
// Into an own property that holds an object, arrays included, as the
|
|
829
|
-
// closure pass walks it.
|
|
844
|
+
// closure pass walks it. Object.prototype's own properties are functions
|
|
845
|
+
// apart from the __proto__ accessor, so on a plain object a read that
|
|
846
|
+
// gives an object is an own property for every other key.
|
|
830
847
|
if (prop.properties && hasDefaultsInside(prop, new Set())) {
|
|
831
848
|
const n = '_d' + st.n++;
|
|
832
|
-
|
|
849
|
+
const own = inherited ? `Object.hasOwn(${ov},${k})` : `(${pk}||Object.hasOwn(${ov},${k}))`;
|
|
850
|
+
lines.push(`{const ${n}=${ov}[${k}];if(typeof ${n}==='object'&&${n}!==null&&${own}){`);
|
|
833
851
|
emitDefaults(prop, n, lines, st);
|
|
834
852
|
lines.push('}}');
|
|
835
853
|
}
|
package/lib/aot-impl.js
CHANGED
|
@@ -113,6 +113,9 @@ function toStandalone(validator) {
|
|
|
113
113
|
// Also capture error function source for zero-compile standalone load
|
|
114
114
|
const jsErrFn = compileToJSCodegenWithErrors(
|
|
115
115
|
typeof validator._schemaObj === 'object' ? validator._schemaObj : {},
|
|
116
|
+
null,
|
|
117
|
+
null,
|
|
118
|
+
{ noOrdinal: true },
|
|
116
119
|
);
|
|
117
120
|
const errSrc = jsErrFn && jsErrFn._errSource ? jsErrFn._errSource : '';
|
|
118
121
|
|
|
@@ -388,11 +391,14 @@ function toStandaloneModule(validator, opts) {
|
|
|
388
391
|
`const _ER = function(e) { _ataStamp(e); this.valid = false; this.errors = e; };\n` +
|
|
389
392
|
`${codeTableSource(onePass.keywords, onePass.formats)}const _vC = (function() {\n${onePass.core}\n})();\n`;
|
|
390
393
|
} else if (!abortEarly) {
|
|
394
|
+
// A module returns the collector's errors as they are and never sorts
|
|
395
|
+
// them, so the ordinal the runtime sorts by (`_o`) has no reader there;
|
|
396
|
+
// it leaked into every error object a module reported.
|
|
391
397
|
jsErrFn = compileToJSCodegenWithErrors(
|
|
392
398
|
typeof validator._schemaObj === 'object' ? validator._schemaObj : {},
|
|
393
399
|
null,
|
|
394
400
|
validator._userFormats,
|
|
395
|
-
(source && sourceMap && schemaFile) ? { sourceMap, schemaFile, frames: sourceFrames } :
|
|
401
|
+
(source && sourceMap && schemaFile) ? { sourceMap, schemaFile, frames: sourceFrames, noOrdinal: true } : { noOrdinal: true },
|
|
396
402
|
);
|
|
397
403
|
const errSrc = jsErrFn && jsErrFn._errSource ? jsErrFn._errSource : '';
|
|
398
404
|
if (errSrc && jsErrFn._errFactory) {
|
|
@@ -665,6 +671,7 @@ function bundleStandalone(Validator, schemas, opts) {
|
|
|
665
671
|
v._schemaObj,
|
|
666
672
|
v._schemaMap,
|
|
667
673
|
v._userFormats,
|
|
674
|
+
{ noOrdinal: true },
|
|
668
675
|
);
|
|
669
676
|
if (jsFn._usesSafeRe || (jsErrFn && jsErrFn._usesSafeRe)) bundleUsesSafeRe = true;
|
|
670
677
|
return { v, jsFn, jsErrFn };
|
|
@@ -757,6 +764,7 @@ function bundleCompact(Validator, schemas, opts) {
|
|
|
757
764
|
v._schemaObj,
|
|
758
765
|
v._schemaMap,
|
|
759
766
|
v._userFormats,
|
|
767
|
+
{ noOrdinal: true },
|
|
760
768
|
);
|
|
761
769
|
if (jsFn._usesSafeRe || (jsErrFn && jsErrFn._usesSafeRe)) bundleUsesSafeRe = true;
|
|
762
770
|
// Hoisted anyOf/oneOf branch helpers (e.g. `_af1_b0`) must travel with the
|
package/lib/draft7.js
CHANGED
|
@@ -1,19 +1,36 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
|
+
// draft-06 is read through the same rules: it differs from draft-07 only in
|
|
4
|
+
// lacking `if`/`then`/`else` (and the content and readOnly annotations), so a
|
|
5
|
+
// draft-06 schema is a draft-07 schema with those three keywords inert. Before
|
|
6
|
+
// this, a draft-06 schema was read under 2020-12 and its tuple `items`,
|
|
7
|
+
// `additionalItems` and `dependencies` were not applied: 30 cases of the
|
|
8
|
+
// official draft-06 suite accepted documents they must reject.
|
|
3
9
|
const DRAFT7_SCHEMAS = new Set([
|
|
4
10
|
'http://json-schema.org/draft-07/schema#',
|
|
5
11
|
'http://json-schema.org/draft-07/schema',
|
|
12
|
+
'http://json-schema.org/draft-06/schema#',
|
|
13
|
+
'http://json-schema.org/draft-06/schema',
|
|
14
|
+
])
|
|
15
|
+
const DRAFT6_SCHEMAS = new Set([
|
|
16
|
+
'http://json-schema.org/draft-06/schema#',
|
|
17
|
+
'http://json-schema.org/draft-06/schema',
|
|
6
18
|
])
|
|
7
19
|
|
|
8
20
|
function isDraft7(schema) {
|
|
9
|
-
return !!(schema && schema.$schema && DRAFT7_SCHEMAS.has(schema.$schema))
|
|
21
|
+
return !!(schema && typeof schema.$schema === 'string' && DRAFT7_SCHEMAS.has(schema.$schema))
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function isDraft6(schema) {
|
|
25
|
+
return !!(schema && typeof schema.$schema === 'string' && DRAFT6_SCHEMAS.has(schema.$schema))
|
|
10
26
|
}
|
|
11
27
|
|
|
12
28
|
// `force` applies the draft-07 rules to a document that declares no dialect
|
|
13
29
|
// of its own, which is how a retrieved schema inherits the root's draft.
|
|
14
|
-
|
|
30
|
+
// `draft6` says the document, or the root it inherits from, is draft-06.
|
|
31
|
+
function normalizeDraft7(schema, force, draft6) {
|
|
15
32
|
if (!force && !isDraft7(schema)) return schema
|
|
16
|
-
_normalize(schema)
|
|
33
|
+
_normalize(schema, draft6 === true || isDraft6(schema))
|
|
17
34
|
return schema
|
|
18
35
|
}
|
|
19
36
|
|
|
@@ -21,9 +38,13 @@ function normalizeDraft7(schema, force) {
|
|
|
21
38
|
// definitions are addressable by pointer, annotations are inert.
|
|
22
39
|
const REF_SIBLINGS_KEPT = new Set(['$ref', '$defs', 'definitions', '$schema', '$comment', 'title', 'description', 'examples', 'default', 'readOnly', 'writeOnly'])
|
|
23
40
|
|
|
24
|
-
function _normalize(schema) {
|
|
41
|
+
function _normalize(schema, d6) {
|
|
25
42
|
if (typeof schema !== 'object' || schema === null) return
|
|
26
43
|
|
|
44
|
+
// draft-06 has no conditional: `if`, `then` and `else` are unknown keywords
|
|
45
|
+
// there, so they are dropped rather than applied.
|
|
46
|
+
if (d6) { delete schema.if; delete schema.then; delete schema.else }
|
|
47
|
+
|
|
27
48
|
// In draft-07 an object with `$ref` is that reference and nothing else:
|
|
28
49
|
// sibling keywords, `$id` included, are ignored. Dropping them here gives
|
|
29
50
|
// every engine the same reading without each having to know the draft.
|
|
@@ -74,7 +95,7 @@ function _normalize(schema) {
|
|
|
74
95
|
for (const key of objSubs) {
|
|
75
96
|
if (schema[key] && typeof schema[key] === 'object') {
|
|
76
97
|
for (const v of Object.values(schema[key])) {
|
|
77
|
-
if (typeof v === 'object' && v !== null) _normalize(v)
|
|
98
|
+
if (typeof v === 'object' && v !== null) _normalize(v, d6)
|
|
78
99
|
}
|
|
79
100
|
}
|
|
80
101
|
}
|
|
@@ -84,7 +105,7 @@ function _normalize(schema) {
|
|
|
84
105
|
for (const key of arrSubs) {
|
|
85
106
|
if (Array.isArray(schema[key])) {
|
|
86
107
|
for (const s of schema[key]) {
|
|
87
|
-
if (typeof s === 'object' && s !== null) _normalize(s)
|
|
108
|
+
if (typeof s === 'object' && s !== null) _normalize(s, d6)
|
|
88
109
|
}
|
|
89
110
|
}
|
|
90
111
|
}
|
|
@@ -94,7 +115,7 @@ function _normalize(schema) {
|
|
|
94
115
|
'additionalProperties', 'propertyNames']
|
|
95
116
|
for (const key of singleSubs) {
|
|
96
117
|
if (typeof schema[key] === 'object' && schema[key] !== null) {
|
|
97
|
-
_normalize(schema[key])
|
|
118
|
+
_normalize(schema[key], d6)
|
|
98
119
|
}
|
|
99
120
|
}
|
|
100
121
|
}
|
|
@@ -248,4 +269,81 @@ function _stripFormat(schema, seen) {
|
|
|
248
269
|
}
|
|
249
270
|
}
|
|
250
271
|
|
|
251
|
-
|
|
272
|
+
// Draft 2019-09 differs from 2020-12, which every engine here reads, in two
|
|
273
|
+
// places: the array form of `items` with `additionalItems` (2020-12 spells
|
|
274
|
+
// them `prefixItems` and `items`), and `$recursiveRef`/`$recursiveAnchor`,
|
|
275
|
+
// which 2020-12 generalised into `$dynamicRef`/`$dynamicAnchor`. Both are
|
|
276
|
+
// rewritten to the 2020-12 spelling. Before, a 2019-09 schema was read as
|
|
277
|
+
// 2020-12 as written, and 38 cases of the official 2019-09 suite accepted
|
|
278
|
+
// documents they must reject.
|
|
279
|
+
//
|
|
280
|
+
// The recursive pair maps one to one: a resource whose root says
|
|
281
|
+
// `$recursiveAnchor: true` gets a `$dynamicAnchor` of a reserved name, and
|
|
282
|
+
// `$recursiveRef: "#"` becomes a `$dynamicRef` to that name when its resource
|
|
283
|
+
// carries the anchor, and a plain `$ref: "#"` when it does not, which is what
|
|
284
|
+
// 2019-09 says it means then. The outward search for the outermost anchored
|
|
285
|
+
// resource is the same rule in both drafts.
|
|
286
|
+
const DRAFT2019_SCHEMAS = new Set([
|
|
287
|
+
'https://json-schema.org/draft/2019-09/schema',
|
|
288
|
+
'https://json-schema.org/draft/2019-09/schema#',
|
|
289
|
+
])
|
|
290
|
+
const RECURSIVE_ANCHOR = '__ata_recursive_anchor'
|
|
291
|
+
|
|
292
|
+
function isDraft2019(schema) {
|
|
293
|
+
return !!(schema && typeof schema.$schema === 'string' && DRAFT2019_SCHEMAS.has(schema.$schema))
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const SUB_OBJECT = ['properties', 'patternProperties', '$defs', 'definitions', 'dependentSchemas']
|
|
297
|
+
const SUB_ARRAY = ['allOf', 'anyOf', 'oneOf', 'prefixItems', 'items']
|
|
298
|
+
const SUB_SINGLE = ['items', 'additionalItems', 'contains', 'not', 'if', 'then', 'else', 'additionalProperties',
|
|
299
|
+
'propertyNames', 'unevaluatedItems', 'unevaluatedProperties', 'contentSchema']
|
|
300
|
+
|
|
301
|
+
function normalizeDraft2019(schema, force) {
|
|
302
|
+
if (!force && !isDraft2019(schema)) return schema
|
|
303
|
+
_normalize2019(schema, schema, false)
|
|
304
|
+
return schema
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function _normalize2019(node, resourceRoot, isRoot) {
|
|
308
|
+
if (typeof node !== 'object' || node === null || Array.isArray(node)) return
|
|
309
|
+
// A subschema with its own `$id` starts a new resource.
|
|
310
|
+
const root = !isRoot && typeof node.$id === 'string' && !node.$id.startsWith('#') ? node : resourceRoot
|
|
311
|
+
if (node.$recursiveAnchor === true && node === root) node.$dynamicAnchor = RECURSIVE_ANCHOR
|
|
312
|
+
delete node.$recursiveAnchor
|
|
313
|
+
if (typeof node.$recursiveRef === 'string') {
|
|
314
|
+
if (node.$recursiveRef === '#' && root.$dynamicAnchor === RECURSIVE_ANCHOR) node.$dynamicRef = '#' + RECURSIVE_ANCHOR
|
|
315
|
+
else if (node.$ref === undefined) node.$ref = node.$recursiveRef
|
|
316
|
+
delete node.$recursiveRef
|
|
317
|
+
}
|
|
318
|
+
// Recurse before the items rewrite, so tuple members are walked as members.
|
|
319
|
+
for (const key of SUB_OBJECT) {
|
|
320
|
+
const sub = node[key]
|
|
321
|
+
if (sub && typeof sub === 'object') for (const v of Object.values(sub)) _normalize2019(v, root, false)
|
|
322
|
+
}
|
|
323
|
+
for (const key of SUB_ARRAY) {
|
|
324
|
+
if (Array.isArray(node[key])) for (const v of node[key]) _normalize2019(v, root, false)
|
|
325
|
+
}
|
|
326
|
+
for (const key of SUB_SINGLE) {
|
|
327
|
+
const sub = node[key]
|
|
328
|
+
if (sub && typeof sub === 'object' && !Array.isArray(sub)) _normalize2019(sub, root, false)
|
|
329
|
+
}
|
|
330
|
+
if (Array.isArray(node.items)) {
|
|
331
|
+
node.prefixItems = node.items
|
|
332
|
+
if (node.additionalItems !== undefined) { node.items = node.additionalItems; delete node.additionalItems } else delete node.items
|
|
333
|
+
} else {
|
|
334
|
+
delete node.additionalItems
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// The draft a `$schema` value names, for the normalizers: 2 draft-06,
|
|
339
|
+
// 1 draft-07, 3 2019-09, 0 anything else. Takes the string so the caller
|
|
340
|
+
// reads the property once.
|
|
341
|
+
function draftCode(uri) {
|
|
342
|
+
if (typeof uri !== 'string') return 0
|
|
343
|
+
if (DRAFT6_SCHEMAS.has(uri)) return 2
|
|
344
|
+
if (DRAFT7_SCHEMAS.has(uri)) return 1
|
|
345
|
+
if (DRAFT2019_SCHEMAS.has(uri)) return 3
|
|
346
|
+
return 0
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
module.exports = { draftCode, isDraft7, isDraft6, isDraft2019, normalizeDraft2019, normalizeDraft7, normalizeNullable, normalizeExclusiveBounds, stripFormatAssertions }
|
package/lib/enrich-error.js
CHANGED
|
@@ -77,7 +77,19 @@ function reprValue (v) {
|
|
|
77
77
|
if (t === 'number' || t === 'boolean') return String(v);
|
|
78
78
|
if (Array.isArray(v)) return `[array, ${v.length} items]`;
|
|
79
79
|
if (t === 'object') {
|
|
80
|
-
|
|
80
|
+
// The keys first: every key costs at least five characters serialised
|
|
81
|
+
// (`"k":` and a one-character value), so past twelve keys the 60-character
|
|
82
|
+
// body is out of reach and the size walk is skipped. The count is what the
|
|
83
|
+
// fallback prints, so it is taken once either way; the walk and the
|
|
84
|
+
// serialisation used to run on every container an error named, and a
|
|
85
|
+
// `required` or `unevaluatedProperties` error names its container.
|
|
86
|
+
let n;
|
|
87
|
+
try {
|
|
88
|
+
n = Object.keys(v).length;
|
|
89
|
+
} catch {
|
|
90
|
+
return '[object, unserializable]';
|
|
91
|
+
}
|
|
92
|
+
if (n <= 12 && jsonSizeWithin(v, 60) >= 0) {
|
|
81
93
|
try {
|
|
82
94
|
const s = JSON.stringify(v);
|
|
83
95
|
if (s !== undefined && s.length <= 60) return s;
|
|
@@ -85,12 +97,6 @@ function reprValue (v) {
|
|
|
85
97
|
return '[object, unserializable]';
|
|
86
98
|
}
|
|
87
99
|
}
|
|
88
|
-
let n;
|
|
89
|
-
try {
|
|
90
|
-
n = Object.keys(v).length;
|
|
91
|
-
} catch {
|
|
92
|
-
return '[object, unserializable]';
|
|
93
|
-
}
|
|
94
100
|
return `[object, ${n} ${n === 1 ? 'key' : 'keys'}]`;
|
|
95
101
|
}
|
|
96
102
|
return `[${t}]`;
|
|
@@ -131,11 +137,12 @@ function resolveAt (err, data) {
|
|
|
131
137
|
*/
|
|
132
138
|
// Observation-first wording. `message` stays as it is for parity, so this is a
|
|
133
139
|
// separate field a consumer opts into.
|
|
134
|
-
function detailFor (err, out) {
|
|
140
|
+
function detailFor (err, out, at) {
|
|
135
141
|
const p = err.params || {};
|
|
136
142
|
switch (err.keyword) {
|
|
137
143
|
case 'type':
|
|
138
|
-
|
|
144
|
+
// From the value when the caller resolved it; from the repr otherwise.
|
|
145
|
+
return `expected ${p.type}, found ${(at === undefined || at === MISSING ? null : typeNameOfValue(at)) || typeNameOf(out.received)}`;
|
|
139
146
|
case 'required':
|
|
140
147
|
return `missing required property "${p.missingProperty}"`;
|
|
141
148
|
case 'additionalProperties':
|
|
@@ -157,6 +164,24 @@ function detailFor (err, out) {
|
|
|
157
164
|
}
|
|
158
165
|
}
|
|
159
166
|
|
|
167
|
+
// The same names from the value itself, for the callers that hold it: the
|
|
168
|
+
// repr is a string built to be read, and parsing it back cost a regex per
|
|
169
|
+
// type error. Agrees with typeNameOf on every value reprValue can produce:
|
|
170
|
+
// a number that is not finite prints as NaN or Infinity, which the repr
|
|
171
|
+
// parser calls a value, and so does this.
|
|
172
|
+
// An object with its own toJSON prints as whatever that returns, so it is
|
|
173
|
+
// left to the repr route (null here).
|
|
174
|
+
function typeNameOfValue (v) {
|
|
175
|
+
if (v === null) return 'null';
|
|
176
|
+
switch (typeof v) {
|
|
177
|
+
case 'boolean': return 'boolean';
|
|
178
|
+
case 'string': return 'string';
|
|
179
|
+
case 'number': return Number.isFinite(v) ? 'number' : 'value';
|
|
180
|
+
case 'object': return Array.isArray(v) ? 'array' : typeof v.toJSON === 'function' ? null : 'object';
|
|
181
|
+
default: return 'value';
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
160
185
|
// Derived from the repr in `received`, which is already a JSON-ish string.
|
|
161
186
|
function typeNameOf (received) {
|
|
162
187
|
if (received === undefined) return 'nothing';
|
|
@@ -252,7 +277,13 @@ function enrich (rawErr, opts) {
|
|
|
252
277
|
// renderer can surface the closest variant's diagnostics. They are enriched
|
|
253
278
|
// like any other error: passed through raw, they carried whatever the engine
|
|
254
279
|
// that answered wrote, the generator's internal ordering key included.
|
|
255
|
-
|
|
280
|
+
// A branch error the rich combined function already built is kept:
|
|
281
|
+
// enriching it again recomputed every field to the same values, and a
|
|
282
|
+
// collapsed oneOf paid that for each branch error on top of its own
|
|
283
|
+
// enrichment. Such an error carries `rank`, which only this enrichment and
|
|
284
|
+
// makeRich write; a standalone module's error carries a docUrl but no
|
|
285
|
+
// rank and is enriched as before (fromCompiled wraps those).
|
|
286
|
+
if (rawErr.branchErrors) out.branchErrors = rawErr.branchErrors.map((b) => (b !== null && typeof b === 'object' && b.rank !== undefined && b.docUrl !== undefined && b._o === undefined) ? b : enrich(b, data !== undefined ? { data } : null));
|
|
256
287
|
|
|
257
288
|
// The frames come from the position map, which is a walk of the document.
|
|
258
289
|
// Given lazily (validateJSON's rejections), the walk waits for the first
|
|
@@ -286,7 +317,7 @@ function enrich (rawErr, opts) {
|
|
|
286
317
|
const sugg = suggestFor(out, data, at === MISSING ? undefined : at);
|
|
287
318
|
if (sugg) out.suggestion = sugg;
|
|
288
319
|
|
|
289
|
-
const detail = detailFor(rawErr, out);
|
|
320
|
+
const detail = detailFor(rawErr, out, at);
|
|
290
321
|
if (detail !== undefined) out.detail = detail;
|
|
291
322
|
out.rank = st.rank;
|
|
292
323
|
|
package/lib/enrich-site.js
CHANGED
|
@@ -49,7 +49,7 @@ function makeRich (site, path, params, message, value, root, spPrefix) {
|
|
|
49
49
|
};
|
|
50
50
|
const sugg = suggestFor(out, root, at === MISSING ? undefined : at);
|
|
51
51
|
if (sugg) out.suggestion = sugg;
|
|
52
|
-
const detail = detailFor({ keyword: site.keyword, params }, out);
|
|
52
|
+
const detail = detailFor({ keyword: site.keyword, params }, out, at);
|
|
53
53
|
if (detail !== undefined) out.detail = detail;
|
|
54
54
|
out.rank = site.rank;
|
|
55
55
|
return out;
|
|
@@ -9,7 +9,7 @@ const { compileFunction } = require('./compile-fn')
|
|
|
9
9
|
// the first call that compiles errors. Everything here was written as part
|
|
10
10
|
// of js-compiler.js and reads its helpers from that module's _internal.
|
|
11
11
|
const core = require('./js-compiler')
|
|
12
|
-
const { hasConstraints, assertEmitted, prepareForCodegen, sharedCodegenGate, codegenSafe, isSimplePN, hasUnresolvableRef, needsBaseTracking, _cpLen, sharedRt, DECLINE, bindRt, compileSafe, emitGuardState, withPlain, REF_NEUTRAL_SIBLINGS, resolveCrossSchemaRef, enumCondition, 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 } = core._internal
|
|
12
|
+
const { 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 } = core._internal
|
|
13
13
|
|
|
14
14
|
|
|
15
15
|
function emitPtrEsc(ctx) {
|
|
@@ -289,7 +289,7 @@ function compileToJSCodegenWithErrors(schema, schemaMap, userFormats, sourceOpts
|
|
|
289
289
|
if (hasUnresolvableRef(schema, eRootDefs, eAnchors, schemaMap, new Set())) return null
|
|
290
290
|
if (needsBaseTracking(schema, schemaMap, new Set())) return null
|
|
291
291
|
|
|
292
|
-
const ctx = { varCounter: 0, helperCode: [], rootDefs: eRootDefs, shared: [], refStack: new Set(), schemaMap: schemaMap || null, anchors: eAnchors, rootE: schema, rootSchema: inputSchema, userFormats: userFormats || null,
|
|
292
|
+
const ctx = { noOrdinal: !!(sourceOpts && sourceOpts.noOrdinal), varCounter: 0, helperCode: [], rootDefs: eRootDefs, shared: [], refStack: new Set(), schemaMap: schemaMap || null, anchors: eAnchors, rootE: schema, rootSchema: inputSchema, userFormats: userFormats || null,
|
|
293
293
|
// Custom format checkers referenced by the body, bound as
|
|
294
294
|
// closure parameters below the way the other entry points do.
|
|
295
295
|
closureVars: [], closureVals: [],
|
|
@@ -1143,7 +1143,8 @@ function genCodeENode(schema, v, pathExpr, lines, ctx, schemaPrefix) {
|
|
|
1143
1143
|
|
|
1144
1144
|
// enum
|
|
1145
1145
|
if (schema.enum) {
|
|
1146
|
-
|
|
1146
|
+
const ev = enumOperand(ctx, schema.enum, v, lines)
|
|
1147
|
+
lines.push(`if(!(${enumCondition(ctx, schema.enum, ev)})){${fail('enum', 'enum', `{allowedValues:${JSON.stringify(schema.enum)}}`, "'must be equal to one of the allowed values'")}}`)
|
|
1147
1148
|
}
|
|
1148
1149
|
|
|
1149
1150
|
// const — use canonical (sorted-key) comparison for objects
|
|
@@ -2330,7 +2331,8 @@ function genCodeCNode(schema, v, pathExpr, lines, ctx, schemaPrefix) {
|
|
|
2330
2331
|
const emitEnumConst = () => {
|
|
2331
2332
|
// enum
|
|
2332
2333
|
if (schema.enum) {
|
|
2333
|
-
|
|
2334
|
+
const ev = enumOperand(ctx, schema.enum, v, lines)
|
|
2335
|
+
lines.push(`if(!(${enumCondition(ctx, schema.enum, ev)})){${fail('enum', 'enum', `{allowedValues:${JSON.stringify(schema.enum)}}`, "'must be equal to one of the allowed values'")}}`)
|
|
2334
2336
|
}
|
|
2335
2337
|
|
|
2336
2338
|
// const
|
package/lib/js-compiler.js
CHANGED
|
@@ -135,6 +135,21 @@ function emitConstant(ctx, value) {
|
|
|
135
135
|
return name
|
|
136
136
|
}
|
|
137
137
|
|
|
138
|
+
// The operand an `enum` test compares: `v` itself when it is a plain
|
|
139
|
+
// identifier, otherwise a local bound once. The test is a chain of `===`,
|
|
140
|
+
// one per member, and each used to repeat the access expression, so a
|
|
141
|
+
// 32-member enum on a required property read `_e3["tool"]` 32 times; on an
|
|
142
|
+
// array of items in several shapes those reads are polymorphic and cost
|
|
143
|
+
// 59 ns against 20 with the value in a local. The binding goes to `lines`
|
|
144
|
+
// ahead of the test.
|
|
145
|
+
const IDENT_RE = /^[A-Za-z_$][\w$]*$/
|
|
146
|
+
function enumOperand(ctx, vals, v, lines) {
|
|
147
|
+
if (IDENT_RE.test(v) || vals.length < 2) return v
|
|
148
|
+
const name = `_ev${ctx.varCounter++}`
|
|
149
|
+
lines.push(`const ${name}=${v}`)
|
|
150
|
+
return name
|
|
151
|
+
}
|
|
152
|
+
|
|
138
153
|
// One `enum` test: primitives compare with `===`, which needs no call, and
|
|
139
154
|
// object members go through structural equality.
|
|
140
155
|
function enumCondition(ctx, vals, v) {
|
|
@@ -1316,6 +1331,14 @@ function sharedDefNames (root, defs) {
|
|
|
1316
1331
|
}
|
|
1317
1332
|
return out
|
|
1318
1333
|
}
|
|
1334
|
+
// Raising this to inline small definitions at every site was measured and
|
|
1335
|
+
// rejected: SchemaStore's detekt applies one 200-character rule definition
|
|
1336
|
+
// at 293 sites, and inlining it took the verdict from 5287 ns to 452 but the
|
|
1337
|
+
// standalone module from 168 KB to 1692 KB and its compile to 33 ms. The
|
|
1338
|
+
// slowness of the shared function is megamorphic property reads (474 objects
|
|
1339
|
+
// of 72 shapes through one function); a for-in body for the shared function
|
|
1340
|
+
// was tried too and gave 1.5x, not enough to carry its complexity. Left as a
|
|
1341
|
+
// known trade: on that shape the shared function costs 27 ns a call.
|
|
1319
1342
|
const SHARED_DEF_MIN_CHARS = 120
|
|
1320
1343
|
|
|
1321
1344
|
function cyclicDefNames (defs) {
|
|
@@ -1986,8 +2009,32 @@ function sharedCodegenGate(schema, schemaMap) {
|
|
|
1986
2009
|
_gateSchema = schema; _gateMap = schemaMap; _gateOk = ok
|
|
1987
2010
|
return ok
|
|
1988
2011
|
}
|
|
2012
|
+
// `unevaluatedProperties` or `unevaluatedItems` beside a `$ref` back to the
|
|
2013
|
+
// document root: the generated evaluated-set tracking does not credit what
|
|
2014
|
+
// the root evaluates through that reference, so a tree whose branches say
|
|
2015
|
+
// `{ unevaluatedProperties: false, $ref: '#' }` rejected valid branches (a
|
|
2016
|
+
// wrong reject, found through the 2019-09 `$recursiveRef` tests, and the same
|
|
2017
|
+
// in 2020-12). The interpreted engine answers it. A text check first: almost
|
|
2018
|
+
// no schema has both.
|
|
2019
|
+
function unevaluatedBesideRootRef(schema) {
|
|
2020
|
+
const text = JSON.stringify(schema)
|
|
2021
|
+
if (!text.includes('"$ref":"#"') || !/"unevaluated(?:Properties|Items)"/.test(text)) return false
|
|
2022
|
+
let found = false
|
|
2023
|
+
const seen = new Set()
|
|
2024
|
+
const walk = (n) => {
|
|
2025
|
+
if (found || n === null || typeof n !== 'object' || seen.has(n)) return
|
|
2026
|
+
seen.add(n)
|
|
2027
|
+
if (Array.isArray(n)) { for (const x of n) walk(x); return }
|
|
2028
|
+
if (n.$ref === '#' && (n.unevaluatedProperties !== undefined || n.unevaluatedItems !== undefined)) { found = true; return }
|
|
2029
|
+
for (const [k, v] of Object.entries(n)) if (k !== 'enum' && k !== 'const' && k !== 'default' && k !== 'examples') walk(v)
|
|
2030
|
+
}
|
|
2031
|
+
walk(schema)
|
|
2032
|
+
return found
|
|
2033
|
+
}
|
|
2034
|
+
|
|
1989
2035
|
function sharedCodegenGateUncached(schema, schemaMap) {
|
|
1990
2036
|
if (bothDefsContainers(schema)) return false
|
|
2037
|
+
if (unevaluatedBesideRootRef(schema)) return false
|
|
1991
2038
|
if (hasSameInstanceRefCycle(schema)) return false
|
|
1992
2039
|
if (!codegenSafe(schema, schemaMap)) return false
|
|
1993
2040
|
if (needsBaseTracking(schema, schemaMap, new Set())) return false
|
|
@@ -3297,7 +3344,8 @@ function genCodeNode(schema, v, lines, ctx, knownType) {
|
|
|
3297
3344
|
|
|
3298
3345
|
// enum
|
|
3299
3346
|
if (schema.enum) {
|
|
3300
|
-
|
|
3347
|
+
const ev = enumOperand(ctx, schema.enum, v, lines)
|
|
3348
|
+
lines.push(`if(!(${enumCondition(ctx, schema.enum, ev)}))return false`)
|
|
3301
3349
|
}
|
|
3302
3350
|
|
|
3303
3351
|
// const
|
|
@@ -5371,4 +5419,4 @@ Object.defineProperty(module.exports, '_expandDynamicScopes', { value: expandDyn
|
|
|
5371
5419
|
// For tests/test_custom_keyword_codegen.js, which holds these copies to the interpreter's.
|
|
5372
5420
|
Object.defineProperty(module.exports, '_kwTypeBits', { value: { kwTypeBit, kwDataBits, KW_T_ANY }, enumerable: false })
|
|
5373
5421
|
|
|
5374
|
-
module.exports._internal = { hasConstraints, assertEmitted, prepareForCodegen, sharedCodegenGate, codegenSafe, isSimplePN, hasUnresolvableRef, needsBaseTracking, _cpLen, sharedRt, DECLINE, bindRt, compileSafe, emitGuardState, withPlain, REF_NEUTRAL_SIBLINGS, resolveCrossSchemaRef, enumCondition, 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 }
|
|
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 }
|
package/lib/metaschemas.js
CHANGED
|
@@ -2,8 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
// The official meta-schemas, vendored so a `$ref` to one of them resolves
|
|
4
4
|
// without a network. Draft 2020-12 is eight documents joined by $dynamicRef;
|
|
5
|
-
// draft-07
|
|
6
|
-
//
|
|
5
|
+
// draft-07 and draft-06 are one each; 2019-09 is seven. Sources:
|
|
6
|
+
// https://json-schema.org/draft/2020-12/schema and
|
|
7
|
+
// http://json-schema.org/draft-07/schema, fetched 2026-08-23, and
|
|
8
|
+
// http://json-schema.org/draft-06/schema and
|
|
9
|
+
// https://json-schema.org/draft/2019-09/schema, fetched 2026-10-10, unmodified.
|
|
10
|
+
// The 2019-09 documents are read through the same 2019-09 rewrite as a
|
|
11
|
+
// caller's schema (lib/draft7.js) once, at load, since they use
|
|
12
|
+
// `$recursiveRef`, which the engines read as `$dynamicRef`.
|
|
7
13
|
// Keyed by $id; index.js also registers the http/https and trailing-# spellings.
|
|
8
14
|
|
|
9
15
|
const METASCHEMAS = new Map([
|
|
@@ -16,6 +22,19 @@ const METASCHEMAS = new Map([
|
|
|
16
22
|
["https://json-schema.org/draft/2020-12/meta/content", {"$schema":"https://json-schema.org/draft/2020-12/schema","$id":"https://json-schema.org/draft/2020-12/meta/content","$dynamicAnchor":"meta","title":"Content vocabulary meta-schema","type":["object","boolean"],"properties":{"contentEncoding":{"type":"string"},"contentMediaType":{"type":"string"},"contentSchema":{"$dynamicRef":"#meta"}}}],
|
|
17
23
|
["https://json-schema.org/draft/2020-12/meta/unevaluated", {"$schema":"https://json-schema.org/draft/2020-12/schema","$id":"https://json-schema.org/draft/2020-12/meta/unevaluated","$dynamicAnchor":"meta","title":"Unevaluated applicator vocabulary meta-schema","type":["object","boolean"],"properties":{"unevaluatedItems":{"$dynamicRef":"#meta"},"unevaluatedProperties":{"$dynamicRef":"#meta"}}}],
|
|
18
24
|
["http://json-schema.org/draft-07/schema#", {"$schema":"http://json-schema.org/draft-07/schema#","$id":"http://json-schema.org/draft-07/schema#","title":"Core schema meta-schema","definitions":{"schemaArray":{"type":"array","minItems":1,"items":{"$ref":"#"}},"nonNegativeInteger":{"type":"integer","minimum":0},"nonNegativeIntegerDefault0":{"allOf":[{"$ref":"#/definitions/nonNegativeInteger"},{"default":0}]},"simpleTypes":{"enum":["array","boolean","integer","null","number","object","string"]},"stringArray":{"type":"array","items":{"type":"string"},"uniqueItems":true,"default":[]}},"type":["object","boolean"],"properties":{"$id":{"type":"string","format":"uri-reference"},"$schema":{"type":"string","format":"uri"},"$ref":{"type":"string","format":"uri-reference"},"$comment":{"type":"string"},"title":{"type":"string"},"description":{"type":"string"},"default":true,"readOnly":{"type":"boolean","default":false},"writeOnly":{"type":"boolean","default":false},"examples":{"type":"array","items":true},"multipleOf":{"type":"number","exclusiveMinimum":0},"maximum":{"type":"number"},"exclusiveMaximum":{"type":"number"},"minimum":{"type":"number"},"exclusiveMinimum":{"type":"number"},"maxLength":{"$ref":"#/definitions/nonNegativeInteger"},"minLength":{"$ref":"#/definitions/nonNegativeIntegerDefault0"},"pattern":{"type":"string","format":"regex"},"additionalItems":{"$ref":"#"},"items":{"anyOf":[{"$ref":"#"},{"$ref":"#/definitions/schemaArray"}],"default":true},"maxItems":{"$ref":"#/definitions/nonNegativeInteger"},"minItems":{"$ref":"#/definitions/nonNegativeIntegerDefault0"},"uniqueItems":{"type":"boolean","default":false},"contains":{"$ref":"#"},"maxProperties":{"$ref":"#/definitions/nonNegativeInteger"},"minProperties":{"$ref":"#/definitions/nonNegativeIntegerDefault0"},"required":{"$ref":"#/definitions/stringArray"},"additionalProperties":{"$ref":"#"},"definitions":{"type":"object","additionalProperties":{"$ref":"#"},"default":{}},"properties":{"type":"object","additionalProperties":{"$ref":"#"},"default":{}},"patternProperties":{"type":"object","additionalProperties":{"$ref":"#"},"propertyNames":{"format":"regex"},"default":{}},"dependencies":{"type":"object","additionalProperties":{"anyOf":[{"$ref":"#"},{"$ref":"#/definitions/stringArray"}]}},"propertyNames":{"$ref":"#"},"const":true,"enum":{"type":"array","items":true,"minItems":1,"uniqueItems":true},"type":{"anyOf":[{"$ref":"#/definitions/simpleTypes"},{"type":"array","items":{"$ref":"#/definitions/simpleTypes"},"minItems":1,"uniqueItems":true}]},"format":{"type":"string"},"contentMediaType":{"type":"string"},"contentEncoding":{"type":"string"},"if":{"$ref":"#"},"then":{"$ref":"#"},"else":{"$ref":"#"},"allOf":{"$ref":"#/definitions/schemaArray"},"anyOf":{"$ref":"#/definitions/schemaArray"},"oneOf":{"$ref":"#/definitions/schemaArray"},"not":{"$ref":"#"}},"default":true}],
|
|
25
|
+
["http://json-schema.org/draft-06/schema#", {"$schema":"http://json-schema.org/draft-06/schema#","$id":"http://json-schema.org/draft-06/schema#","title":"Core schema meta-schema","definitions":{"schemaArray":{"type":"array","minItems":1,"items":{"$ref":"#"}},"nonNegativeInteger":{"type":"integer","minimum":0},"nonNegativeIntegerDefault0":{"allOf":[{"$ref":"#/definitions/nonNegativeInteger"},{"default":0}]},"simpleTypes":{"enum":["array","boolean","integer","null","number","object","string"]},"stringArray":{"type":"array","items":{"type":"string"},"uniqueItems":true,"default":[]}},"type":["object","boolean"],"properties":{"$id":{"type":"string","format":"uri-reference"},"$schema":{"type":"string","format":"uri"},"$ref":{"type":"string","format":"uri-reference"},"title":{"type":"string"},"description":{"type":"string"},"default":{},"examples":{"type":"array","items":{}},"multipleOf":{"type":"number","exclusiveMinimum":0},"maximum":{"type":"number"},"exclusiveMaximum":{"type":"number"},"minimum":{"type":"number"},"exclusiveMinimum":{"type":"number"},"maxLength":{"$ref":"#/definitions/nonNegativeInteger"},"minLength":{"$ref":"#/definitions/nonNegativeIntegerDefault0"},"pattern":{"type":"string","format":"regex"},"additionalItems":{"$ref":"#"},"items":{"anyOf":[{"$ref":"#"},{"$ref":"#/definitions/schemaArray"}],"default":{}},"maxItems":{"$ref":"#/definitions/nonNegativeInteger"},"minItems":{"$ref":"#/definitions/nonNegativeIntegerDefault0"},"uniqueItems":{"type":"boolean","default":false},"contains":{"$ref":"#"},"maxProperties":{"$ref":"#/definitions/nonNegativeInteger"},"minProperties":{"$ref":"#/definitions/nonNegativeIntegerDefault0"},"required":{"$ref":"#/definitions/stringArray"},"additionalProperties":{"$ref":"#"},"definitions":{"type":"object","additionalProperties":{"$ref":"#"},"default":{}},"properties":{"type":"object","additionalProperties":{"$ref":"#"},"default":{}},"patternProperties":{"type":"object","additionalProperties":{"$ref":"#"},"propertyNames":{"format":"regex"},"default":{}},"dependencies":{"type":"object","additionalProperties":{"anyOf":[{"$ref":"#"},{"$ref":"#/definitions/stringArray"}]}},"propertyNames":{"$ref":"#"},"const":{},"enum":{"type":"array","minItems":1,"uniqueItems":true},"type":{"anyOf":[{"$ref":"#/definitions/simpleTypes"},{"type":"array","items":{"$ref":"#/definitions/simpleTypes"},"minItems":1,"uniqueItems":true}]},"format":{"type":"string"},"allOf":{"$ref":"#/definitions/schemaArray"},"anyOf":{"$ref":"#/definitions/schemaArray"},"oneOf":{"$ref":"#/definitions/schemaArray"},"not":{"$ref":"#"}},"default":{}}],
|
|
26
|
+
["https://json-schema.org/draft/2019-09/schema", {"$schema":"https://json-schema.org/draft/2019-09/schema","$id":"https://json-schema.org/draft/2019-09/schema","$vocabulary":{"https://json-schema.org/draft/2019-09/vocab/core":true,"https://json-schema.org/draft/2019-09/vocab/applicator":true,"https://json-schema.org/draft/2019-09/vocab/validation":true,"https://json-schema.org/draft/2019-09/vocab/meta-data":true,"https://json-schema.org/draft/2019-09/vocab/format":false,"https://json-schema.org/draft/2019-09/vocab/content":true},"$recursiveAnchor":true,"title":"Core and Validation specifications meta-schema","allOf":[{"$ref":"meta/core"},{"$ref":"meta/applicator"},{"$ref":"meta/validation"},{"$ref":"meta/meta-data"},{"$ref":"meta/format"},{"$ref":"meta/content"}],"type":["object","boolean"],"properties":{"definitions":{"$comment":"While no longer an official keyword as it is replaced by $defs, this keyword is retained in the meta-schema to prevent incompatible extensions as it remains in common use.","type":"object","additionalProperties":{"$recursiveRef":"#"},"default":{}},"dependencies":{"$comment":"\"dependencies\" is no longer a keyword, but schema authors should avoid redefining it to facilitate a smooth transition to \"dependentSchemas\" and \"dependentRequired\"","type":"object","additionalProperties":{"anyOf":[{"$recursiveRef":"#"},{"$ref":"meta/validation#/$defs/stringArray"}]}}}}],
|
|
27
|
+
["https://json-schema.org/draft/2019-09/meta/core", {"$schema":"https://json-schema.org/draft/2019-09/schema","$id":"https://json-schema.org/draft/2019-09/meta/core","$recursiveAnchor":true,"title":"Core vocabulary meta-schema","type":["object","boolean"],"properties":{"$id":{"type":"string","format":"uri-reference","$comment":"Non-empty fragments not allowed.","pattern":"^[^#]*#?$"},"$schema":{"type":"string","format":"uri"},"$anchor":{"type":"string","pattern":"^[A-Za-z][-A-Za-z0-9.:_]*$"},"$ref":{"type":"string","format":"uri-reference"},"$recursiveRef":{"type":"string","format":"uri-reference"},"$recursiveAnchor":{"type":"boolean","default":false},"$vocabulary":{"type":"object","propertyNames":{"type":"string","format":"uri"},"additionalProperties":{"type":"boolean"}},"$comment":{"type":"string"},"$defs":{"type":"object","additionalProperties":{"$recursiveRef":"#"},"default":{}}}}],
|
|
28
|
+
["https://json-schema.org/draft/2019-09/meta/applicator", {"$schema":"https://json-schema.org/draft/2019-09/schema","$id":"https://json-schema.org/draft/2019-09/meta/applicator","$recursiveAnchor":true,"title":"Applicator vocabulary meta-schema","type":["object","boolean"],"properties":{"additionalItems":{"$recursiveRef":"#"},"unevaluatedItems":{"$recursiveRef":"#"},"items":{"anyOf":[{"$recursiveRef":"#"},{"$ref":"#/$defs/schemaArray"}]},"contains":{"$recursiveRef":"#"},"additionalProperties":{"$recursiveRef":"#"},"unevaluatedProperties":{"$recursiveRef":"#"},"properties":{"type":"object","additionalProperties":{"$recursiveRef":"#"},"default":{}},"patternProperties":{"type":"object","additionalProperties":{"$recursiveRef":"#"},"propertyNames":{"format":"regex"},"default":{}},"dependentSchemas":{"type":"object","additionalProperties":{"$recursiveRef":"#"}},"propertyNames":{"$recursiveRef":"#"},"if":{"$recursiveRef":"#"},"then":{"$recursiveRef":"#"},"else":{"$recursiveRef":"#"},"allOf":{"$ref":"#/$defs/schemaArray"},"anyOf":{"$ref":"#/$defs/schemaArray"},"oneOf":{"$ref":"#/$defs/schemaArray"},"not":{"$recursiveRef":"#"}},"$defs":{"schemaArray":{"type":"array","minItems":1,"items":{"$recursiveRef":"#"}}}}],
|
|
29
|
+
["https://json-schema.org/draft/2019-09/meta/validation", {"$schema":"https://json-schema.org/draft/2019-09/schema","$id":"https://json-schema.org/draft/2019-09/meta/validation","$recursiveAnchor":true,"title":"Validation vocabulary meta-schema","type":["object","boolean"],"properties":{"multipleOf":{"type":"number","exclusiveMinimum":0},"maximum":{"type":"number"},"exclusiveMaximum":{"type":"number"},"minimum":{"type":"number"},"exclusiveMinimum":{"type":"number"},"maxLength":{"$ref":"#/$defs/nonNegativeInteger"},"minLength":{"$ref":"#/$defs/nonNegativeIntegerDefault0"},"pattern":{"type":"string","format":"regex"},"maxItems":{"$ref":"#/$defs/nonNegativeInteger"},"minItems":{"$ref":"#/$defs/nonNegativeIntegerDefault0"},"uniqueItems":{"type":"boolean","default":false},"maxContains":{"$ref":"#/$defs/nonNegativeInteger"},"minContains":{"$ref":"#/$defs/nonNegativeInteger","default":1},"maxProperties":{"$ref":"#/$defs/nonNegativeInteger"},"minProperties":{"$ref":"#/$defs/nonNegativeIntegerDefault0"},"required":{"$ref":"#/$defs/stringArray"},"dependentRequired":{"type":"object","additionalProperties":{"$ref":"#/$defs/stringArray"}},"const":true,"enum":{"type":"array","items":true},"type":{"anyOf":[{"$ref":"#/$defs/simpleTypes"},{"type":"array","items":{"$ref":"#/$defs/simpleTypes"},"minItems":1,"uniqueItems":true}]}},"$defs":{"nonNegativeInteger":{"type":"integer","minimum":0},"nonNegativeIntegerDefault0":{"$ref":"#/$defs/nonNegativeInteger","default":0},"simpleTypes":{"enum":["array","boolean","integer","null","number","object","string"]},"stringArray":{"type":"array","items":{"type":"string"},"uniqueItems":true,"default":[]}}}],
|
|
30
|
+
["https://json-schema.org/draft/2019-09/meta/meta-data", {"$schema":"https://json-schema.org/draft/2019-09/schema","$id":"https://json-schema.org/draft/2019-09/meta/meta-data","$recursiveAnchor":true,"title":"Meta-data vocabulary meta-schema","type":["object","boolean"],"properties":{"title":{"type":"string"},"description":{"type":"string"},"default":true,"deprecated":{"type":"boolean","default":false},"readOnly":{"type":"boolean","default":false},"writeOnly":{"type":"boolean","default":false},"examples":{"type":"array","items":true}}}],
|
|
31
|
+
["https://json-schema.org/draft/2019-09/meta/format", {"$schema":"https://json-schema.org/draft/2019-09/schema","$id":"https://json-schema.org/draft/2019-09/meta/format","$recursiveAnchor":true,"title":"Format vocabulary meta-schema","type":["object","boolean"],"properties":{"format":{"type":"string"}}}],
|
|
32
|
+
["https://json-schema.org/draft/2019-09/meta/content", {"$schema":"https://json-schema.org/draft/2019-09/schema","$id":"https://json-schema.org/draft/2019-09/meta/content","$recursiveAnchor":true,"title":"Content vocabulary meta-schema","type":["object","boolean"],"properties":{"contentMediaType":{"type":"string"},"contentEncoding":{"type":"string"},"contentSchema":{"$recursiveRef":"#"}}}],
|
|
19
33
|
]);
|
|
20
34
|
|
|
35
|
+
{
|
|
36
|
+
const { isDraft2019, normalizeDraft2019 } = require('./draft7')
|
|
37
|
+
for (const [id, doc] of METASCHEMAS) if (isDraft2019(doc)) METASCHEMAS.set(id, normalizeDraft2019(JSON.parse(JSON.stringify(doc)), true))
|
|
38
|
+
}
|
|
39
|
+
|
|
21
40
|
module.exports = { METASCHEMAS };
|
package/lib/regex-linear.js
CHANGED
|
@@ -3,34 +3,56 @@
|
|
|
3
3
|
const { parse } = require('./safe-regex')
|
|
4
4
|
|
|
5
5
|
// True when the platform RegExp, a backtracking engine, also runs `src` in
|
|
6
|
-
// linear time, so it can take the pattern
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
6
|
+
// linear time, so it can take the pattern instead of the linear-time engine
|
|
7
|
+
// in safe-regex.js, which costs three to five times as much per character.
|
|
8
|
+
// Two rules, and a pattern passes on either:
|
|
9
|
+
//
|
|
10
|
+
// The simple rule, the original one: every unbounded quantifier is on one
|
|
11
|
+
// character, class or `.`; at most one of them, and then the pattern is
|
|
12
|
+
// anchored with `^` on every branch; a finite repeat may cover a group that
|
|
13
|
+
// holds no unbounded quantifier; few bounded choices in all. One loop whose
|
|
14
|
+
// body cannot be split two ways, exited at most once per position, with
|
|
15
|
+
// bounded work after it.
|
|
16
|
+
//
|
|
17
|
+
// The deterministic rule, for the patterns the simple one declines: dotted
|
|
18
|
+
// identifiers, `[^/]+/[^/]+`, version numbers. On the Glushkov construction
|
|
19
|
+
// the characters, classes and dots are positions. Around every unbounded
|
|
20
|
+
// loop the next character must decide the single way to continue: the
|
|
21
|
+
// positions that can follow a position inside a loop accept pairwise
|
|
22
|
+
// disjoint characters, and so do the positions a match can start at when a
|
|
23
|
+
// loop is among them; a position reaches a follower by one route only, since
|
|
24
|
+
// `(a+)+` reaches `a` from `a` through the inner loop and again through the
|
|
25
|
+
// outer one and that double route is what a backtracking engine explores
|
|
26
|
+
// exponentially; a loop body must not match the empty string, since `(a*)*`
|
|
27
|
+
// iterates emptily without bound. Ambiguity away from any loop, `(pwa|PWA)`,
|
|
28
|
+
// costs a bounded factor and is allowed, as the simple rule allows it. A
|
|
29
|
+
// bounded repeat is a loop only when its body holds an unbounded one, since
|
|
30
|
+
// `(\w+\s?){5}` splits five ways where `(?:[A-F0-9]{2}:){31}` counts. With
|
|
31
|
+
// an unbounded quantifier the pattern must be anchored with `^` on every
|
|
32
|
+
// branch, since an unanchored search retries from each position.
|
|
33
|
+
//
|
|
34
|
+
// Both rules allow only escapes both engines read alike and nothing outside
|
|
35
|
+
// the BMP, so both see the same UTF-16 units. tests/test_regex_native_linear.js
|
|
36
|
+
// holds them: every accepted pattern answers as the linear engine does and
|
|
37
|
+
// stays fast on inputs built to make a backtracking engine retry.
|
|
12
38
|
const NATIVE_ESCAPES = new Set(['d', 'D', 'w', 'W', 's', 'S', 'n', 'r', 't', 'f', 'v', '.', '\\', '/', '^', '$', '|', '?', '*', '+', '(', ')', '[', ']', '{', '}', '-', 'x', 'u'])
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
try { ast = parse(src) } catch { return false }
|
|
39
|
+
const MAX_BOUNDED_CHOICES = 64
|
|
40
|
+
|
|
41
|
+
const atom = (n) => n.t === 'char' || n.t === 'class' || n.t === 'any'
|
|
42
|
+
|
|
43
|
+
// Anchored at the start on every top-level branch.
|
|
44
|
+
function anchored (ast) {
|
|
45
|
+
let top = ast
|
|
46
|
+
while (top.t === 'group') top = top.child
|
|
47
|
+
const branches = top.t === 'alt' ? top.opts : [top]
|
|
48
|
+
const first = (n) => { while (n && n.t === 'group') n = n.child; return n && n.t === 'concat' ? first(n.parts[0]) : n }
|
|
49
|
+
return branches.every((b) => { const f = first(b); return f && f.t === 'bol' })
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function simpleRule (ast) {
|
|
28
53
|
// Per subtree: how many unbounded quantifiers it holds (`u`) and how many
|
|
29
54
|
// bounded choices a backtracking engine could try (`b`), or null where the
|
|
30
|
-
// shape is not provably linear.
|
|
31
|
-
// the group holds no unbounded quantifier: `(?:[A-F0-9]{2}:){31}` has
|
|
32
|
-
// nothing to retry, and its choices multiply through the nesting.
|
|
33
|
-
const atom = (n) => n.t === 'char' || n.t === 'class' || n.t === 'any'
|
|
55
|
+
// shape is not provably linear.
|
|
34
56
|
const info = (n) => {
|
|
35
57
|
switch (n.t) {
|
|
36
58
|
case 'star': case 'plus':
|
|
@@ -56,19 +78,154 @@ function nativeIsLinear (src) {
|
|
|
56
78
|
}
|
|
57
79
|
}
|
|
58
80
|
const r = info(ast)
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
81
|
+
if (r === null || r.u > 1 || r.b > MAX_BOUNDED_CHOICES) return false
|
|
82
|
+
return r.u === 0 || anchored(ast)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// The characters a position accepts: 128 ASCII bits and one bit for "some
|
|
86
|
+
// code point at 128 or above", which keeps two classes with non-ASCII
|
|
87
|
+
// members apart only when one of them has none. Conservative in the right
|
|
88
|
+
// direction: an overlap that is not there only sends a pattern to the
|
|
89
|
+
// linear engine.
|
|
90
|
+
function charSet (node) {
|
|
91
|
+
const bits = new Uint8Array(128)
|
|
92
|
+
let high = 0
|
|
93
|
+
if (node.t === 'any') {
|
|
94
|
+
bits.fill(1); bits[10] = 0; bits[13] = 0; high = 1
|
|
95
|
+
} else if (node.t === 'char') {
|
|
96
|
+
if (node.c < 128) bits[node.c] = 1; else high = 1
|
|
97
|
+
} else {
|
|
98
|
+
for (const [lo, hi] of node.ranges) {
|
|
99
|
+
for (let c = Math.max(0, lo), top = Math.min(127, hi); c <= top; c++) bits[c] = 1
|
|
100
|
+
if (hi >= 128) high = 1
|
|
101
|
+
}
|
|
102
|
+
if (node.neg) { for (let c = 0; c < 128; c++) bits[c] ^= 1; high = 1 }
|
|
70
103
|
}
|
|
71
|
-
return
|
|
104
|
+
return { bits, high }
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function overlap (a, b) {
|
|
108
|
+
if (a.high && b.high) return true
|
|
109
|
+
for (let c = 0; c < 128; c++) if (a.bits[c] & b.bits[c]) return true
|
|
110
|
+
return false
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Whether a subtree holds an unbounded quantifier.
|
|
114
|
+
function hasUnbounded (n) {
|
|
115
|
+
switch (n.t) {
|
|
116
|
+
case 'star': case 'plus': return true
|
|
117
|
+
case 'repeat': return n.max === Infinity || hasUnbounded(n.child)
|
|
118
|
+
case 'quest': case 'group': return hasUnbounded(n.child)
|
|
119
|
+
case 'concat': return n.parts.some(hasUnbounded)
|
|
120
|
+
case 'alt': return n.opts.some(hasUnbounded)
|
|
121
|
+
default: return false
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function deterministicRule (ast) {
|
|
126
|
+
const sets = []
|
|
127
|
+
const follow = []
|
|
128
|
+
const inLoop = []
|
|
129
|
+
let ok = true
|
|
130
|
+
let unbounded = 0
|
|
131
|
+
let bounded = 0
|
|
132
|
+
let depth = 0
|
|
133
|
+
// Pairwise disjoint, required when a loop position is among them.
|
|
134
|
+
const disjoint = (ids) => {
|
|
135
|
+
const arr = Array.from(ids)
|
|
136
|
+
if (!arr.some((p) => inLoop[p])) return
|
|
137
|
+
for (let i = 0; i < arr.length && ok; i++) {
|
|
138
|
+
for (let j = i + 1; j < arr.length; j++) if (overlap(sets[arr[i]], sets[arr[j]])) { ok = false; return }
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
const link = (from, to) => {
|
|
142
|
+
for (const p of to) {
|
|
143
|
+
if (follow[from].has(p)) { ok = false; return }
|
|
144
|
+
follow[from].add(p)
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
const empty = () => ({ nullable: true, first: new Set(), last: new Set() })
|
|
148
|
+
const walk = (n) => {
|
|
149
|
+
if (!ok) return empty()
|
|
150
|
+
switch (n.t) {
|
|
151
|
+
case 'char': case 'class': case 'any': {
|
|
152
|
+
const id = sets.length
|
|
153
|
+
sets.push(charSet(n))
|
|
154
|
+
follow.push(new Set())
|
|
155
|
+
inLoop.push(depth > 0)
|
|
156
|
+
return { nullable: false, first: new Set([id]), last: new Set([id]) }
|
|
157
|
+
}
|
|
158
|
+
case 'bol': case 'eol': return empty()
|
|
159
|
+
case 'group': return walk(n.child)
|
|
160
|
+
case 'star': case 'plus': case 'quest': case 'repeat': {
|
|
161
|
+
const infinite = n.t === 'star' || n.t === 'plus' || (n.t === 'repeat' && n.max === Infinity)
|
|
162
|
+
const loops = infinite || (n.t === 'repeat' && n.max > 1 && hasUnbounded(n.child))
|
|
163
|
+
const optional = n.t === 'star' || n.t === 'quest' || (n.t === 'repeat' && n.min === 0)
|
|
164
|
+
if (loops) depth++
|
|
165
|
+
const c = walk(n.child)
|
|
166
|
+
if (loops) depth--
|
|
167
|
+
if (infinite) {
|
|
168
|
+
unbounded++
|
|
169
|
+
if (c.nullable) ok = false
|
|
170
|
+
} else if (n.t === 'quest') {
|
|
171
|
+
bounded += 1
|
|
172
|
+
} else {
|
|
173
|
+
bounded += (n.max - n.min)
|
|
174
|
+
if (!atom(n.child)) bounded += n.max
|
|
175
|
+
}
|
|
176
|
+
if (loops) for (const l of c.last) link(l, c.first)
|
|
177
|
+
return { nullable: c.nullable || optional, first: c.first, last: c.last }
|
|
178
|
+
}
|
|
179
|
+
case 'concat': {
|
|
180
|
+
let acc = empty()
|
|
181
|
+
for (const part of n.parts) {
|
|
182
|
+
const r = walk(part)
|
|
183
|
+
for (const l of acc.last) link(l, r.first)
|
|
184
|
+
const first = new Set(acc.first)
|
|
185
|
+
if (acc.nullable) for (const f of r.first) first.add(f)
|
|
186
|
+
const last = new Set(r.last)
|
|
187
|
+
if (r.nullable) for (const l of acc.last) last.add(l)
|
|
188
|
+
acc = { nullable: acc.nullable && r.nullable, first, last }
|
|
189
|
+
}
|
|
190
|
+
return acc
|
|
191
|
+
}
|
|
192
|
+
case 'alt': {
|
|
193
|
+
const rs = n.opts.map(walk)
|
|
194
|
+
const first = new Set()
|
|
195
|
+
const last = new Set()
|
|
196
|
+
for (const r of rs) { for (const f of r.first) first.add(f); for (const l of r.last) last.add(l) }
|
|
197
|
+
return { nullable: rs.some((r) => r.nullable), first, last }
|
|
198
|
+
}
|
|
199
|
+
default:
|
|
200
|
+
ok = false
|
|
201
|
+
return empty()
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
const root = walk(ast)
|
|
205
|
+
if (!ok) return false
|
|
206
|
+
disjoint(root.first)
|
|
207
|
+
for (let p = 0; p < sets.length && ok; p++) disjoint(follow[p])
|
|
208
|
+
if (!ok) return false
|
|
209
|
+
if (bounded > MAX_BOUNDED_CHOICES) return false
|
|
210
|
+
return unbounded === 0 || anchored(ast)
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function nativeIsLinear (src) {
|
|
214
|
+
if (typeof src !== 'string' || src.length > 512) return false
|
|
215
|
+
for (let i = 0; i < src.length; i++) {
|
|
216
|
+
const c = src.charCodeAt(i)
|
|
217
|
+
if (c >= 0xd800 && c <= 0xdfff) return false
|
|
218
|
+
if (src[i] === '\\') {
|
|
219
|
+
const e = src[i + 1]
|
|
220
|
+
if (!NATIVE_ESCAPES.has(e)) return false
|
|
221
|
+
if (e === 'x' && !/^[0-9a-fA-F]{2}$/.test(src.slice(i + 2, i + 4))) return false
|
|
222
|
+
if (e === 'u' && !/^[0-9a-fA-F]{4}$/.test(src.slice(i + 2, i + 6))) return false
|
|
223
|
+
i++
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
let ast
|
|
227
|
+
try { ast = parse(src) } catch { return false }
|
|
228
|
+
return simpleRule(ast) || deterministicRule(ast)
|
|
72
229
|
}
|
|
73
230
|
|
|
74
231
|
module.exports = { nativeIsLinear }
|
package/lib/safe-regex.js
CHANGED
|
@@ -410,7 +410,8 @@ function __ataSafeRegex () {
|
|
|
410
410
|
id = states.length
|
|
411
411
|
let isMatch = false
|
|
412
412
|
for (let k = 0; k < pcs.length; k++) if (ops[pcs[k]] === OP_MATCH) { isMatch = true; break }
|
|
413
|
-
|
|
413
|
+
// `dead`: an anchored match with no thread left cannot recover.
|
|
414
|
+
states.push({ pcs: Int32Array.from(pcs), isMatch, dead: anchored && pcs.length === 0, next: new Int32Array(128).fill(-2), nextEnd: new Int32Array(128).fill(-2) })
|
|
414
415
|
stateIds.set(key, id)
|
|
415
416
|
return id
|
|
416
417
|
}
|
|
@@ -452,23 +453,36 @@ function __ataSafeRegex () {
|
|
|
452
453
|
}
|
|
453
454
|
if (id < 0) return testNFA(s)
|
|
454
455
|
let state = states[id]
|
|
455
|
-
|
|
456
|
+
if (len === 0) return state.isMatch
|
|
457
|
+
// The last character is stepped with the end-of-input flag set and is
|
|
458
|
+
// handled after the loop, so the loop body selects no table and tests
|
|
459
|
+
// no position.
|
|
460
|
+
const last = len - 1
|
|
461
|
+
for (let pos = 0; pos < last; pos++) {
|
|
456
462
|
if (state.isMatch) return true
|
|
457
463
|
const c = s.charCodeAt(pos)
|
|
458
|
-
const atEnd = pos + 1 === len
|
|
459
464
|
let nid
|
|
460
465
|
if (c < 128) {
|
|
461
|
-
|
|
462
|
-
nid =
|
|
463
|
-
if (nid === -2) { nid = step(state, c, atEnd); table[c] = nid }
|
|
466
|
+
nid = state.next[c]
|
|
467
|
+
if (nid === -2) { nid = step(state, c, false); state.next[c] = nid }
|
|
464
468
|
} else {
|
|
465
|
-
nid = step(state, c,
|
|
469
|
+
nid = step(state, c, false)
|
|
466
470
|
}
|
|
467
471
|
if (nid < 0) return testNFA(s)
|
|
468
472
|
state = states[nid]
|
|
469
|
-
if (
|
|
473
|
+
if (state.dead) return false
|
|
470
474
|
}
|
|
471
|
-
|
|
475
|
+
if (state.isMatch) return true
|
|
476
|
+
const c = s.charCodeAt(last)
|
|
477
|
+
let nid
|
|
478
|
+
if (c < 128) {
|
|
479
|
+
nid = state.nextEnd[c]
|
|
480
|
+
if (nid === -2) { nid = step(state, c, true); state.nextEnd[c] = nid }
|
|
481
|
+
} else {
|
|
482
|
+
nid = step(state, c, true)
|
|
483
|
+
}
|
|
484
|
+
if (nid < 0) return testNFA(s)
|
|
485
|
+
return states[nid].isMatch
|
|
472
486
|
}
|
|
473
487
|
|
|
474
488
|
return function test (s) {
|
package/lib/validator-core.js
CHANGED
|
@@ -25,7 +25,7 @@ function _registerCodegen(engine) {
|
|
|
25
25
|
_codegen = engine;
|
|
26
26
|
({ compileToJS, compileToJSCodegen, compileToJSCodegenWithErrors, compileToJSCombined } = engine.jsCompiler);
|
|
27
27
|
}
|
|
28
|
-
const { normalizeDraft7, normalizeNullable, normalizeExclusiveBounds, stripFormatAssertions } = require("./draft7");
|
|
28
|
+
const { draftCode, normalizeDraft2019, normalizeDraft7, normalizeNullable, normalizeExclusiveBounds, stripFormatAssertions } = require("./draft7");
|
|
29
29
|
const { enabledKeywords, stripDisabledKeywords } = require("./vocabularies");
|
|
30
30
|
const { needsNormalization } = require("./schema-scan");
|
|
31
31
|
const { isV1Dialect } = require("./dialect");
|
|
@@ -426,21 +426,26 @@ function _deepCloneWithSymbols(v) {
|
|
|
426
426
|
// Normalize a caller-provided schema without mutating the original.
|
|
427
427
|
// Clones only when normalization would change the object (draft-07 keys
|
|
428
428
|
// present or nullable fields present). Internal-only — not exported.
|
|
429
|
+
// `inheritDraft7` is the root's draft for a document that declares none of
|
|
430
|
+
// its own: 0 or absent for none, 1 for draft-07, 2 for draft-06, 3 for 2019-09.
|
|
429
431
|
function _normalizeCallerSchema(s, inheritDraft7) {
|
|
430
|
-
const
|
|
431
|
-
const
|
|
432
|
-
|
|
433
|
-
|
|
432
|
+
const declared = s && typeof s === 'object' ? s.$schema : undefined
|
|
433
|
+
const code = declared !== undefined ? draftCode(declared) : (inheritDraft7 || 0)
|
|
434
|
+
const needsDraft7 = code === 1 || code === 2
|
|
435
|
+
const draft6 = code === 2
|
|
436
|
+
const draft2019 = code === 3
|
|
434
437
|
// One walk answers whether there is anything to do. Almost always there is
|
|
435
438
|
// not, and then the serialize, clone, normalize, serialize, compare below is
|
|
436
439
|
// work spent to find that out. The walk over-reports rather than under, so a
|
|
437
440
|
// schema it clears is one no normalizer would have touched;
|
|
438
441
|
// `tests/test_schema_scan.js` holds that direction against the whole suite.
|
|
439
|
-
if
|
|
442
|
+
// The scan does not look for `if`/`then`/`else`, which draft-06 drops.
|
|
443
|
+
if (!draft6 && !draft2019 && !needsNormalization(s, needsDraft7)) return s
|
|
440
444
|
|
|
441
445
|
const str = JSON.stringify(s)
|
|
442
446
|
const copy = _deepCloneWithSymbols(s)
|
|
443
|
-
if (needsDraft7) normalizeDraft7(copy, true)
|
|
447
|
+
if (needsDraft7) normalizeDraft7(copy, true, draft6)
|
|
448
|
+
if (draft2019) normalizeDraft2019(copy, true)
|
|
444
449
|
normalizeNullable(copy)
|
|
445
450
|
normalizeExclusiveBounds(copy)
|
|
446
451
|
// Return original when normalization produced no change, copy otherwise.
|
|
@@ -467,7 +472,7 @@ function declaredId(original, normalized) {
|
|
|
467
472
|
return undefined
|
|
468
473
|
}
|
|
469
474
|
|
|
470
|
-
// `inheritDraft7` is
|
|
475
|
+
// `inheritDraft7` is non-zero when the root schema is draft-07 or draft-06: a retrieved
|
|
471
476
|
// document that declares no dialect is read under the root's draft.
|
|
472
477
|
// The map is derived entirely from what the caller passed, so the same
|
|
473
478
|
// `schemas` gives the same map. A server building one validator per route over
|
|
@@ -485,12 +490,12 @@ function buildSchemaMap(schemas, inheritDraft7) {
|
|
|
485
490
|
if (!schemas) return null
|
|
486
491
|
const byDraft = _schemaMapCache.get(schemas)
|
|
487
492
|
if (byDraft) {
|
|
488
|
-
const hit = byDraft[inheritDraft7
|
|
493
|
+
const hit = byDraft[inheritDraft7 || 0]
|
|
489
494
|
if (hit) return hit
|
|
490
495
|
}
|
|
491
496
|
const map = _buildSchemaMap(schemas, inheritDraft7)
|
|
492
|
-
const slot = byDraft || [null, null]
|
|
493
|
-
slot[inheritDraft7
|
|
497
|
+
const slot = byDraft || [null, null, null, null]
|
|
498
|
+
slot[inheritDraft7 || 0] = map
|
|
494
499
|
if (!byDraft) _schemaMapCache.set(schemas, slot)
|
|
495
500
|
return map
|
|
496
501
|
}
|
|
@@ -676,8 +681,7 @@ class Validator {
|
|
|
676
681
|
// uses, and a benchmark timing construction measures construction.
|
|
677
682
|
// When schema is a string, JSON.parse already produces a fresh object.
|
|
678
683
|
const raw = typeof schema === "string" ? JSON.parse(schema) : schema;
|
|
679
|
-
const rootIsDraft7 =
|
|
680
|
-
(raw.$schema === 'http://json-schema.org/draft-07/schema#' || raw.$schema === 'http://json-schema.org/draft-07/schema'));
|
|
684
|
+
const rootIsDraft7 = raw && typeof raw === 'object' ? draftCode(raw.$schema) : 0;
|
|
681
685
|
|
|
682
686
|
// Built here rather than below because `$vocabulary` is resolved against
|
|
683
687
|
// it, and that resolution waits until compilation so a meta-schema
|
|
@@ -1561,8 +1565,7 @@ class Validator {
|
|
|
1561
1565
|
// Normalize a copy so the caller's object is never mutated. A document
|
|
1562
1566
|
// without a dialect of its own is read under the root's draft.
|
|
1563
1567
|
const root = this._schemaObj
|
|
1564
|
-
const rootIsDraft7 =
|
|
1565
|
-
(root.$schema === 'http://json-schema.org/draft-07/schema#' || root.$schema === 'http://json-schema.org/draft-07/schema'))
|
|
1568
|
+
const rootIsDraft7 = root && typeof root === 'object' ? draftCode(root.$schema) : 0
|
|
1566
1569
|
const normalized = _normalizeCallerSchema(schema, rootIsDraft7)
|
|
1567
1570
|
this._ownSchemaMap()
|
|
1568
1571
|
this._schemaMap.set(normalized.$id, normalized)
|
package/lib/version.js
CHANGED
package/lib/vocabularies.js
CHANGED
|
@@ -50,6 +50,18 @@ function keywordsByVocabulary() {
|
|
|
50
50
|
new Set(Object.keys(meta.properties)),
|
|
51
51
|
)
|
|
52
52
|
}
|
|
53
|
+
// Draft 2019-09's vocabularies, from its own vendored meta-schemas. A
|
|
54
|
+
// 2019-09 dialect names these URIs in `$vocabulary`.
|
|
55
|
+
for (const name of ['core', 'applicator', 'validation', 'meta-data', 'format', 'content']) {
|
|
56
|
+
const meta = require('./metaschemas').METASCHEMAS.get(
|
|
57
|
+
`https://json-schema.org/draft/2019-09/meta/${name}`,
|
|
58
|
+
)
|
|
59
|
+
if (!meta || !meta.properties) continue
|
|
60
|
+
byVocabulary.set(
|
|
61
|
+
`https://json-schema.org/draft/2019-09/vocab/${name}`,
|
|
62
|
+
new Set(Object.keys(meta.properties)),
|
|
63
|
+
)
|
|
64
|
+
}
|
|
53
65
|
// The one vocabulary with no document to read. `format-assertion` is an
|
|
54
66
|
// alternative to `format-annotation` rather than part of the standard
|
|
55
67
|
// dialect, so its meta-schema is not among the vendored ones. It defines
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ata-validator",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.49.0",
|
|
4
4
|
"description": "JSON Schema validator that compiles to plain JavaScript, still runs where code generation is blocked, and can be compiled away at build time. Passes the full official test suite.",
|
|
5
5
|
"type": "commonjs",
|
|
6
6
|
"main": "index.js",
|
|
@@ -72,7 +72,7 @@
|
|
|
72
72
|
"rebuild": "cmake-js rebuild --target ata",
|
|
73
73
|
"test": "node tests/run.js",
|
|
74
74
|
"bench:size": "node benchmark/bench_aot_size.mjs",
|
|
75
|
-
"test:suite": "node tests/run_suite.js && node tests/run_suite.js draft7 && node tests/run_suite.js v1",
|
|
75
|
+
"test:suite": "node tests/run_suite.js && node tests/run_suite.js draft7 && node tests/run_suite.js v1 && node tests/run_suite.js draft6 && node tests/run_suite.js draft2019-09",
|
|
76
76
|
"test:compat": "node tests/test_compat.js",
|
|
77
77
|
"test:standard-schema": "node tests/test_standard_schema.js",
|
|
78
78
|
"test:browser": "node tests/test_browser.js",
|
|
@@ -152,13 +152,13 @@
|
|
|
152
152
|
"LICENSE"
|
|
153
153
|
],
|
|
154
154
|
"optionalDependencies": {
|
|
155
|
-
"@ata-validator/native-darwin-arm64": "1.
|
|
156
|
-
"@ata-validator/native-darwin-x64": "1.
|
|
157
|
-
"@ata-validator/native-linux-arm64-gnu": "1.
|
|
158
|
-
"@ata-validator/native-linux-arm64-musl": "1.
|
|
159
|
-
"@ata-validator/native-linux-x64-gnu": "1.
|
|
160
|
-
"@ata-validator/native-linux-x64-musl": "1.
|
|
161
|
-
"@ata-validator/native-win32-x64": "1.
|
|
155
|
+
"@ata-validator/native-darwin-arm64": "1.49.0",
|
|
156
|
+
"@ata-validator/native-darwin-x64": "1.49.0",
|
|
157
|
+
"@ata-validator/native-linux-arm64-gnu": "1.49.0",
|
|
158
|
+
"@ata-validator/native-linux-arm64-musl": "1.49.0",
|
|
159
|
+
"@ata-validator/native-linux-x64-gnu": "1.49.0",
|
|
160
|
+
"@ata-validator/native-linux-x64-musl": "1.49.0",
|
|
161
|
+
"@ata-validator/native-win32-x64": "1.49.0"
|
|
162
162
|
},
|
|
163
163
|
"peerDependencies": {
|
|
164
164
|
"yaml": "^2.0.0"
|