ata-validator 1.31.1 → 1.31.3
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/compat.js +2 -2
- package/index.js +60 -16
- package/lib/compat-errors.js +12 -2
- package/lib/interpreter.js +6 -0
- package/lib/js-compiler.js +269 -44
- package/lib/plan-source.js +281 -0
- package/lib/shape-classifier.js +9 -0
- package/lib/tier0.js +11 -1
- package/lib/version.js +1 -1
- package/package.json +9 -9
package/compat.js
CHANGED
|
@@ -22,7 +22,7 @@ const { resolvePointer } = require('./lib/pointer.js');
|
|
|
22
22
|
|
|
23
23
|
const { Validator } = require('./index');
|
|
24
24
|
const { METASCHEMAS } = require('./lib/metaschemas');
|
|
25
|
-
const { createShaper } = require('./lib/compat-errors');
|
|
25
|
+
const { createShaper, asReferenceKeyword } = require('./lib/compat-errors');
|
|
26
26
|
|
|
27
27
|
const DRAFT7 = 'http://json-schema.org/draft-07/schema#';
|
|
28
28
|
const DRAFT2020 = 'https://json-schema.org/draft/2020-12/schema';
|
|
@@ -185,7 +185,7 @@ class Ata {
|
|
|
185
185
|
validate.errors = null;
|
|
186
186
|
return true;
|
|
187
187
|
}
|
|
188
|
-
let errors = shaper === null ? result.errors : shaper(result.errors, data);
|
|
188
|
+
let errors = shaper === null ? result.errors.map(asReferenceKeyword) : shaper(result.errors, data);
|
|
189
189
|
// Errors the shaper synthesises, the `if` wrapper among them, are built
|
|
190
190
|
// from the group rather than carried up from the validator, so verbose
|
|
191
191
|
// mode has to fill in the value they point at here or they would be the
|
package/index.js
CHANGED
|
@@ -244,26 +244,36 @@ function collectRemovals(schema, actions, path) {
|
|
|
244
244
|
// Emit the in-place strip for one schema node and everything under its
|
|
245
245
|
// `properties`. Scope matches collectRemovals(): object properties only, so
|
|
246
246
|
// the two paths keep the same answer.
|
|
247
|
+
// Each level's code, its own removal loop and its children's, sits inside the
|
|
248
|
+
// guard for that level. A child used to be guarded only on itself, so its
|
|
249
|
+
// access expression read through a parent that could be absent or null:
|
|
250
|
+
// `{ c: { properties: { d: { additionalProperties: false } } } }` threw a
|
|
251
|
+
// TypeError from validate({}) whenever the optional `c` was missing.
|
|
247
252
|
function emitRemovals(node, access, lines, depth, seen) {
|
|
248
253
|
if (!node || typeof node !== 'object' || !node.properties) return;
|
|
249
254
|
if (seen.has(node)) return;
|
|
250
255
|
seen.add(node);
|
|
251
256
|
const keys = Object.keys(node.properties);
|
|
252
|
-
|
|
257
|
+
const body = [];
|
|
258
|
+
// With no declared property every key is additional, as the interpreted
|
|
259
|
+
// engine's remover has it; this pass used to skip such a node entirely.
|
|
260
|
+
if (node.additionalProperties === false) {
|
|
253
261
|
const kv = '_k' + depth;
|
|
254
262
|
const checks = keys.map((k) => `${kv}!==${JSON.stringify(k)}`).join('&&');
|
|
255
|
-
|
|
256
|
-
?
|
|
257
|
-
: `
|
|
258
|
-
lines.push(`${guard}for(var ${kv} in ${access})if(${checks})delete ${access}[${kv}]`);
|
|
263
|
+
body.push(keys.length > 0
|
|
264
|
+
? `for(var ${kv} in ${access})if(${checks})delete ${access}[${kv}]`
|
|
265
|
+
: `for(var ${kv} in ${access})delete ${access}[${kv}]`);
|
|
259
266
|
}
|
|
260
267
|
for (const key of keys) {
|
|
261
268
|
const prop = node.properties[key];
|
|
262
269
|
if (prop && typeof prop === 'object' && prop.properties) {
|
|
263
|
-
emitRemovals(prop, `${access}[${JSON.stringify(key)}]`,
|
|
270
|
+
emitRemovals(prop, `${access}[${JSON.stringify(key)}]`, body, depth + 1, seen);
|
|
264
271
|
}
|
|
265
272
|
}
|
|
266
273
|
seen.delete(node);
|
|
274
|
+
if (body.length === 0) return;
|
|
275
|
+
if (depth === 0) lines.push(...body);
|
|
276
|
+
else lines.push(`if(${access}!==null&&typeof ${access}==='object'&&!Array.isArray(${access})){${body.join('\n')}}`);
|
|
267
277
|
}
|
|
268
278
|
|
|
269
279
|
// Generate a fast preprocess function via codegen instead of closure arrays
|
|
@@ -1306,6 +1316,23 @@ class Validator {
|
|
|
1306
1316
|
this._mutatesInput = !!(preprocess || options.coerceTypes || options.removeAdditional);
|
|
1307
1317
|
this._preprocess = preprocess;
|
|
1308
1318
|
|
|
1319
|
+
// removeAdditional alone, the common parse-and-strip use: a verdict function
|
|
1320
|
+
// that deletes unknown keys in the walk it already makes, where the pass
|
|
1321
|
+
// above walks every object a second time just to find them. It answers
|
|
1322
|
+
// the documents it accepts; anything it rejects takes the full path below,
|
|
1323
|
+
// which removes, validates and reports exactly as before, so a rejected
|
|
1324
|
+
// document is left as clean as it always was. The generator declines any
|
|
1325
|
+
// schema where deleting during the walk could change an answer.
|
|
1326
|
+
let fusedRemove = null;
|
|
1327
|
+
if (preprocess && options.removeAdditional && !options.coerceTypes && !this._interpretOnly &&
|
|
1328
|
+
!this._userFormats && !this._usesKeywords && !this._schemaStr.includes('"default"')) {
|
|
1329
|
+
try {
|
|
1330
|
+
fusedRemove = compileToJSCodegen(schemaObj, this._schemaMap.size > 0 ? this._schemaMap : null, null, { removeAdditional: true });
|
|
1331
|
+
} catch {
|
|
1332
|
+
fusedRemove = null;
|
|
1333
|
+
}
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1309
1336
|
// Detect if schema is "selective" -- doesn't recurse into arrays/deep objects.
|
|
1310
1337
|
const hasArrayTraversal =
|
|
1311
1338
|
schemaObj &&
|
|
@@ -1577,9 +1604,11 @@ class Validator {
|
|
|
1577
1604
|
// The verdict methods answer validate()'s question without building the
|
|
1578
1605
|
// error list, so they run the same preprocess pass. Skipping it made the
|
|
1579
1606
|
// two disagree on input that coercion or a default would have fixed.
|
|
1580
|
-
_bindVerdict(this,
|
|
1581
|
-
? (data) =>
|
|
1582
|
-
:
|
|
1607
|
+
_bindVerdict(this, fusedRemove
|
|
1608
|
+
? (data) => fusedRemove(data) || (preprocess(data), jsFn(data))
|
|
1609
|
+
: preprocess
|
|
1610
|
+
? (data) => { preprocess(data); return jsFn(data) }
|
|
1611
|
+
: jsFn);
|
|
1583
1612
|
// Same preference as the object path: the combined function first, since
|
|
1584
1613
|
// it validates and collects in one pass, and the error generator behind
|
|
1585
1614
|
// it. `errPreferCombined` is that order, resolved on the first rejection
|
|
@@ -1794,7 +1823,12 @@ class Validator {
|
|
|
1794
1823
|
: _validate;
|
|
1795
1824
|
_bindVerdict(this, this._fastVerdict
|
|
1796
1825
|
? this._fastVerdict
|
|
1797
|
-
|
|
1826
|
+
// The rewrite runs first here too, as it does for validate() above; a
|
|
1827
|
+
// verdict without it disagreed with validate() on input that coercion,
|
|
1828
|
+
// a default or a removed key would have fixed.
|
|
1829
|
+
: preprocess
|
|
1830
|
+
? (data) => { preprocess(data); return _validate(data).valid; }
|
|
1831
|
+
: (data) => _validate(data).valid);
|
|
1798
1832
|
this.validateAndParse = (jsonStr) => this._compiled.validateAndParse(jsonStr);
|
|
1799
1833
|
{
|
|
1800
1834
|
const slot = this._fastSlot;
|
|
@@ -1927,12 +1961,22 @@ class Validator {
|
|
|
1927
1961
|
// paths that only need a boolean.
|
|
1928
1962
|
if (this.validate) {
|
|
1929
1963
|
const _bare = this.validate;
|
|
1930
|
-
this.validate =
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1964
|
+
this.validate = fusedRemove
|
|
1965
|
+
// A document the removing verdict accepts is answered here, one call
|
|
1966
|
+
// deep; anything else takes the full path, see fusedRemove above.
|
|
1967
|
+
? (data) => {
|
|
1968
|
+
if (fusedRemove(data)) return { valid: true, data, errors: VALID_RESULT.errors };
|
|
1969
|
+
const r = _bare(data);
|
|
1970
|
+
return (r.valid === true && r.data === undefined)
|
|
1971
|
+
? { valid: true, data, errors: r.errors }
|
|
1972
|
+
: r;
|
|
1973
|
+
}
|
|
1974
|
+
: (data) => {
|
|
1975
|
+
const r = _bare(data);
|
|
1976
|
+
return (r.valid === true && r.data === undefined)
|
|
1977
|
+
? { valid: true, data, errors: r.errors }
|
|
1978
|
+
: r;
|
|
1979
|
+
};
|
|
1936
1980
|
}
|
|
1937
1981
|
|
|
1938
1982
|
// Custom error messages: if any subschema declares an `errorMessage`
|
package/lib/compat-errors.js
CHANGED
|
@@ -133,6 +133,16 @@ function escapePointer(s) {
|
|
|
133
133
|
// validate() already reports them so.
|
|
134
134
|
const WRAPPERS = new Set(['anyOf', 'oneOf', 'propertyNames', 'contains', 'not', 'if']);
|
|
135
135
|
|
|
136
|
+
// ata names the keyword of a false schema `not`, at the root and nested alike,
|
|
137
|
+
// which is what its own error codes and every consumer switch on. The
|
|
138
|
+
// reference names it `false schema`. The root case used to come out of the
|
|
139
|
+
// code generator as `false schema` by an inconsistency that has since been
|
|
140
|
+
// fixed, so this shim matched the reference there and not for a nested false
|
|
141
|
+
// schema; the rename here makes both match.
|
|
142
|
+
function asReferenceKeyword(e) {
|
|
143
|
+
return e.keyword === 'not' && e.message === 'boolean schema is false' ? { ...e, keyword: 'false schema' } : e;
|
|
144
|
+
}
|
|
145
|
+
|
|
136
146
|
function createShaper({ Validator, rootId, rootDoc, schemas, options, allErrors, macroKeywords }) {
|
|
137
147
|
const branchCache = new Map();
|
|
138
148
|
const branchValidator = (pointer) => {
|
|
@@ -360,11 +370,11 @@ function createShaper({ Validator, rootId, rootDoc, schemas, options, allErrors,
|
|
|
360
370
|
}
|
|
361
371
|
|
|
362
372
|
return function shapeErrors(errors, data) {
|
|
363
|
-
const shaped = shape(errors, data, '');
|
|
373
|
+
const shaped = shape(errors, data, '').map(asReferenceKeyword);
|
|
364
374
|
if (allErrors) return shaped;
|
|
365
375
|
const n = groupEnd(shaped);
|
|
366
376
|
return shaped.slice(0, n);
|
|
367
377
|
};
|
|
368
378
|
}
|
|
369
379
|
|
|
370
|
-
module.exports = { createShaper, sortLikeReference, REFERENCE_ORDER };
|
|
380
|
+
module.exports = { createShaper, sortLikeReference, REFERENCE_ORDER, asReferenceKeyword };
|
package/lib/interpreter.js
CHANGED
|
@@ -1255,3 +1255,9 @@ function createInterpreter(schema, options) {
|
|
|
1255
1255
|
}
|
|
1256
1256
|
|
|
1257
1257
|
module.exports = { createInterpreter };
|
|
1258
|
+
// For lib/plan-source.js, which emits source from the same plans and must call
|
|
1259
|
+
// the same helpers so the two cannot answer differently.
|
|
1260
|
+
Object.defineProperty(module.exports, '_planInternals', {
|
|
1261
|
+
value: { Plan, deepEqual, cpAtLeast, cpAtMost, multipleOfOk, dataBits, T_STRING, T_NUMBER, T_INTEGER, T_BOOLEAN, T_NULL, T_OBJECT, T_ARRAY, T_ANY },
|
|
1262
|
+
enumerable: false,
|
|
1263
|
+
});
|
package/lib/js-compiler.js
CHANGED
|
@@ -157,10 +157,31 @@ const DOC_BASE = 'https://ata-validator.com/e/'
|
|
|
157
157
|
// reader sorts by without deriving it from the path. Empty when the path is
|
|
158
158
|
// not into the root document.
|
|
159
159
|
function ordinalField (ctx, schemaPath) {
|
|
160
|
-
const o = ctx && ctx.rootSchema ? ordinalFor(ctx.rootSchema, schemaPath) : null
|
|
160
|
+
const o = ctx && ctx.rootSchema ? ordinalFor(ctx.rootSchema, unescapeSp(schemaPath)) : null
|
|
161
161
|
return o === null ? '' : `,_o:${o}`
|
|
162
162
|
}
|
|
163
163
|
|
|
164
|
+
// A schema path as the generators hold it is text for a single-quoted source
|
|
165
|
+
// literal: ptrSeg put a backslash before every backslash and quote in a key.
|
|
166
|
+
// Anything that uses the path as a value, the ordinal lookup and the frozen
|
|
167
|
+
// error objects below, has to take that backslash off again, or a key such as
|
|
168
|
+
// `it's` reads as `it\'s` and the error names a path that does not exist.
|
|
169
|
+
// The value of a string literal the generators built for source. One that does
|
|
170
|
+
// not parse on its own (a key with a raw line break) would not parse inside the
|
|
171
|
+
// generated function either, so the generator declines, as it would have when
|
|
172
|
+
// that function failed to build.
|
|
173
|
+
function literalValue (lit) {
|
|
174
|
+
try {
|
|
175
|
+
return Function('return ' + lit)()
|
|
176
|
+
} catch {
|
|
177
|
+
throw DECLINE
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function unescapeSp (sp) {
|
|
182
|
+
return sp.indexOf('\\') === -1 ? sp : sp.replace(/\\(.)/g, '$1')
|
|
183
|
+
}
|
|
184
|
+
|
|
164
185
|
function buildErrorLiteral (opts) {
|
|
165
186
|
const { keyword, format, schemaPath, sourceMap } = opts
|
|
166
187
|
let code = (keyword === 'format' && format) ? codeFor('format', format) : codeFor(keyword)
|
|
@@ -340,7 +361,7 @@ function compileToJS(schema, defs, schemaMap) {
|
|
|
340
361
|
// required applies to objects only; other types are ignored.
|
|
341
362
|
if (schema.required && Array.isArray(schema.required)) {
|
|
342
363
|
for (const key of schema.required) {
|
|
343
|
-
checks.push((d) => typeof d !== 'object' || d === null || Array.isArray(d) || d
|
|
364
|
+
checks.push((d) => typeof d !== 'object' || d === null || Array.isArray(d) || hasOwnKey(d, key))
|
|
344
365
|
}
|
|
345
366
|
}
|
|
346
367
|
|
|
@@ -350,7 +371,7 @@ function compileToJS(schema, defs, schemaMap) {
|
|
|
350
371
|
const propCheck = compileToJS(prop, rootDefs)
|
|
351
372
|
if (!propCheck) return null // bail if sub-schema too complex
|
|
352
373
|
checks.push((d) => {
|
|
353
|
-
if (typeof d !== 'object' || d === null || !(key
|
|
374
|
+
if (typeof d !== 'object' || d === null || !hasOwnKey(d, key)) return true
|
|
354
375
|
return propCheck(d[key])
|
|
355
376
|
})
|
|
356
377
|
}
|
|
@@ -387,9 +408,9 @@ function compileToJS(schema, defs, schemaMap) {
|
|
|
387
408
|
if (schema.dependentRequired) {
|
|
388
409
|
for (const [key, deps] of Object.entries(schema.dependentRequired)) {
|
|
389
410
|
checks.push((d) => {
|
|
390
|
-
if (typeof d !== 'object' || d === null || !(key
|
|
411
|
+
if (typeof d !== 'object' || d === null || !hasOwnKey(d, key)) return true
|
|
391
412
|
for (let i = 0; i < deps.length; i++) {
|
|
392
|
-
if (!(deps[i]
|
|
413
|
+
if (!hasOwnKey(d, deps[i])) return false
|
|
393
414
|
}
|
|
394
415
|
return true
|
|
395
416
|
})
|
|
@@ -872,9 +893,146 @@ const FORMAT_CHECKS = {
|
|
|
872
893
|
}
|
|
873
894
|
|
|
874
895
|
// Dangerous JS property names that exist on Object.prototype
|
|
896
|
+
// Presence is an own-property question: `required: ['toString']` is not
|
|
897
|
+
// satisfied by `{}`. `'k' in v` and `v.k !== undefined` both answer yes for a
|
|
898
|
+
// name found on the prototype chain, which accepted documents the interpreted
|
|
899
|
+
// engine rejects. Object.hasOwn would be exact but is not inlined, about 5 ns
|
|
900
|
+
// a key against well under 1 for `in`, so the check is split:
|
|
901
|
+
// - a name on Object.prototype (and `__proto__`) is always checked with
|
|
902
|
+
// hasOwnProperty; these are rare in schemas and known when compiling;
|
|
903
|
+
// - any other name can only be inherited from a prototype that is not
|
|
904
|
+
// Object.prototype, so an ordinary object answers with `in` and one
|
|
905
|
+
// comparison of its prototype, and anything else, a class instance, a
|
|
906
|
+
// null-prototype object, one from another realm, pays for hasOwnProperty.
|
|
907
|
+
// `v.__proto__` is read rather than Object.getPrototypeOf(v) because the engine
|
|
908
|
+
// inlines the accessor and not the call (0.8 ns against 8 for eleven keys). An
|
|
909
|
+
// object whose own `__proto__` is a data property reads that value, which is
|
|
910
|
+
// not Object.prototype, so it takes the exact path.
|
|
911
|
+
const PROTO_NAMES = new Set([...Object.getOwnPropertyNames(Object.prototype), '__proto__'])
|
|
912
|
+
// Declared once per compiled function and called by name: spelled out at each
|
|
913
|
+
// property, the test grew a 50-property module by a tenth. Both are small
|
|
914
|
+
// enough for the engine to inline.
|
|
915
|
+
//
|
|
916
|
+
// The `in` stays at the call site with the key as a constant: inside a shared
|
|
917
|
+
// helper the key is a parameter, the engine cannot keep that `in` monomorphic,
|
|
918
|
+
// and reading the errors of a product document went from 3.9 to 5.8 us. The
|
|
919
|
+
// helper only runs once `in` has said yes, and on an ordinary object it
|
|
920
|
+
// answers from the prototype without looking at the key.
|
|
921
|
+
const OWN_HELPERS = 'const _hop=Object.prototype.hasOwnProperty;function _h(o,k){return _hop.call(o,k)}' +
|
|
922
|
+
'function _ok(o,k){return o.__proto__===Object.prototype||_hop.call(o,k)}' +
|
|
923
|
+
'function _hall(o,ks){for(let i=0;i<ks.length;i++)if(!_hop.call(o,ks[i]))return false;return true}'
|
|
924
|
+
//
|
|
925
|
+
// Better still, the prototype is read once per object: a node that asks about
|
|
926
|
+
// presence declares `_pkN`, whether its value is an ordinary object, before
|
|
927
|
+
// anything else, and every key test in that node reads the local. A shared
|
|
928
|
+
// helper reading `__proto__` for each key was megamorphic, since it saw every
|
|
929
|
+
// object shape in the document, and cost the error path about a microsecond
|
|
930
|
+
// on a product document.
|
|
931
|
+
function ownKeyExpr (ctx, v, key) {
|
|
932
|
+
hoistOnce(ctx, '_ownHoisted', OWN_HELPERS)
|
|
933
|
+
const k = JSON.stringify(key)
|
|
934
|
+
if (PROTO_NAMES.has(key)) return `_h(${v},${k})`
|
|
935
|
+
const pk = ctx.plainOf ? ctx.plainOf.get(v) : undefined
|
|
936
|
+
return pk !== undefined ? `(${pk}?${k} in ${v}:_h(${v},${k}))` : `(${k} in ${v}&&_ok(${v},${k}))`
|
|
937
|
+
}
|
|
938
|
+
// For a value already read as `v[key]` and found not to be undefined: whether
|
|
939
|
+
// it came from `v` itself. `in` is known to hold, so only the second half runs.
|
|
940
|
+
function ownGuard (ctx, v, key) {
|
|
941
|
+
hoistOnce(ctx, '_ownHoisted', OWN_HELPERS)
|
|
942
|
+
const k = JSON.stringify(key)
|
|
943
|
+
if (PROTO_NAMES.has(key)) return `_h(${v},${k})`
|
|
944
|
+
const pk = ctx.plainOf ? ctx.plainOf.get(v) : undefined
|
|
945
|
+
return pk !== undefined ? `(${pk}||_h(${v},${k}))` : `_ok(${v},${k})`
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
// Declares the per-object prototype flag for a node that asks about presence,
|
|
949
|
+
// and records it for the key tests emitted while the node is generated. The
|
|
950
|
+
// record is restored on the way out, so a nested node over the same value
|
|
951
|
+
// expression, an allOf branch say, cannot leave its name behind.
|
|
952
|
+
function withPlain (schema, v, lines, ctx, run) {
|
|
953
|
+
if (typeof schema !== 'object' || schema === null ||
|
|
954
|
+
!(schema.required || schema.properties || schema.dependentRequired || schema.dependentSchemas)) {
|
|
955
|
+
return run()
|
|
956
|
+
}
|
|
957
|
+
if (!ctx.plainOf) ctx.plainOf = new Map()
|
|
958
|
+
const name = '_pk' + ctx.varCounter++
|
|
959
|
+
const at = lines.length
|
|
960
|
+
lines.push(`const ${name}=typeof ${v}==='object'&&${v}!==null&&${v}.__proto__===Object.prototype`)
|
|
961
|
+
const prev = ctx.plainOf.get(v)
|
|
962
|
+
ctx.plainOf.set(v, name)
|
|
963
|
+
try {
|
|
964
|
+
return run()
|
|
965
|
+
} finally {
|
|
966
|
+
if (prev === undefined) ctx.plainOf.delete(v)
|
|
967
|
+
else ctx.plainOf.set(v, prev)
|
|
968
|
+
// A node whose keys are all typed never reads the flag; declaring it
|
|
969
|
+
// anyway cost a strict-object check about five percent. Unused, it goes.
|
|
970
|
+
const re = new RegExp('\\b' + name + '\\b')
|
|
971
|
+
let used = false
|
|
972
|
+
for (let i = at + 1; i < lines.length && !used; i++) if (re.test(lines[i])) used = true
|
|
973
|
+
if (!used && lines[at] === `const ${name}=typeof ${v}==='object'&&${v}!==null&&${v}.__proto__===Object.prototype`) lines.splice(at, 1)
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
// The same test at run time, for the closure compiler.
|
|
977
|
+
const _hop = Object.prototype.hasOwnProperty
|
|
978
|
+
function hasOwnKey (d, key) {
|
|
979
|
+
if (PROTO_NAMES.has(key)) return _hop.call(d, key)
|
|
980
|
+
return key in d && (d.__proto__ === Object.prototype || _hop.call(d, key))
|
|
981
|
+
}
|
|
982
|
+
|
|
875
983
|
const UNSAFE_KEYS = new Set(['__proto__', 'constructor', 'toString', 'valueOf',
|
|
876
984
|
'hasOwnProperty', 'isPrototypeOf', 'propertyIsEnumerable', 'toLocaleString'])
|
|
877
985
|
|
|
986
|
+
// The nodes the removeAdditional pass deletes from, when deleting them during
|
|
987
|
+
// the verdict gives the answer removing them first would, or null when it may
|
|
988
|
+
// not. It may not whenever a keyword could look at a key before it is deleted
|
|
989
|
+
// and pass where it would fail after: minProperties, a const or enum object
|
|
990
|
+
// that holds the extra key, uniqueItems over objects that differ only in one,
|
|
991
|
+
// a branch that requires it, a `required` naming a key the node does not
|
|
992
|
+
// declare. So the whole schema is held to keywords that only look at declared
|
|
993
|
+
// properties or at scalar values. The node set mirrors emitRemovals in
|
|
994
|
+
// index.js: reached through `properties` from the root, with
|
|
995
|
+
// additionalProperties false and at least one declared property.
|
|
996
|
+
const FUSED_REMOVAL_KEYS = new Set(['type', 'properties', 'required', 'additionalProperties', 'items',
|
|
997
|
+
'minItems', 'maxItems', 'minLength', 'maxLength', 'pattern', 'format', 'minimum', 'maximum',
|
|
998
|
+
'exclusiveMinimum', 'exclusiveMaximum', 'multipleOf', 'enum', 'const', 'title', 'description',
|
|
999
|
+
'$comment', '$schema'])
|
|
1000
|
+
function fusedRemovalNodes (root) {
|
|
1001
|
+
const isScalar = (x) => x === null || (typeof x !== 'object' && typeof x !== 'function')
|
|
1002
|
+
const safe = (node) => {
|
|
1003
|
+
if (typeof node === 'boolean') return true
|
|
1004
|
+
if (!node || typeof node !== 'object' || Array.isArray(node)) return false
|
|
1005
|
+
for (const k of Object.keys(node)) if (!FUSED_REMOVAL_KEYS.has(k)) return false
|
|
1006
|
+
if (node.enum !== undefined && !(Array.isArray(node.enum) && node.enum.every(isScalar))) return false
|
|
1007
|
+
if (node.const !== undefined && !isScalar(node.const)) return false
|
|
1008
|
+
if (node.properties !== undefined) {
|
|
1009
|
+
if (!node.properties || typeof node.properties !== 'object' || Array.isArray(node.properties)) return false
|
|
1010
|
+
for (const k of Object.keys(node.properties)) if (!safe(node.properties[k])) return false
|
|
1011
|
+
}
|
|
1012
|
+
if (node.additionalProperties === false) {
|
|
1013
|
+
const declared = node.properties ? Object.keys(node.properties) : []
|
|
1014
|
+
if (declared.length === 0) return false
|
|
1015
|
+
if (Array.isArray(node.required) && node.required.some((r) => !declared.includes(r))) return false
|
|
1016
|
+
} else if (node.additionalProperties !== undefined && node.additionalProperties !== true && !safe(node.additionalProperties)) {
|
|
1017
|
+
return false
|
|
1018
|
+
}
|
|
1019
|
+
if (node.items !== undefined && (Array.isArray(node.items) || !safe(node.items))) return false
|
|
1020
|
+
return true
|
|
1021
|
+
}
|
|
1022
|
+
if (!safe(root)) return null
|
|
1023
|
+
const nodes = new Set()
|
|
1024
|
+
const walk = (node) => {
|
|
1025
|
+
if (!node || typeof node !== 'object' || !node.properties) return
|
|
1026
|
+
if (node.additionalProperties === false && Object.keys(node.properties).length > 0) nodes.add(node)
|
|
1027
|
+
for (const k of Object.keys(node.properties)) {
|
|
1028
|
+
const p = node.properties[k]
|
|
1029
|
+
if (p && typeof p === 'object' && p.properties) walk(p)
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
walk(root)
|
|
1033
|
+
return nodes.size > 0 ? nodes : null
|
|
1034
|
+
}
|
|
1035
|
+
|
|
878
1036
|
// Check if all $dynamicRef in a target schema can be resolved via the calling schema's anchors.
|
|
879
1037
|
function canResolveDynamicRefs(target, callingSchema, schemaMap) {
|
|
880
1038
|
// Collect anchors from calling schema
|
|
@@ -1487,9 +1645,18 @@ function sharedCodegenGate(schema, schemaMap) {
|
|
|
1487
1645
|
|
|
1488
1646
|
// --- Codegen mode: generates a single Function (NOT CSP-safe) ---
|
|
1489
1647
|
// This matches ajv's approach: one monolithic function, V8 JIT fully inlines it
|
|
1490
|
-
function compileToJSCodegen(schema, schemaMap, userFormats) {
|
|
1648
|
+
function compileToJSCodegen(schema, schemaMap, userFormats, opts) {
|
|
1491
1649
|
if (typeof schema === 'boolean') return schema ? () => true : () => false
|
|
1492
1650
|
if (typeof schema !== 'object' || schema === null) return null
|
|
1651
|
+
// removeAdditional: a verdict function that deletes the keys the remover
|
|
1652
|
+
// would delete, in the walk the verdict already makes, instead of rejecting
|
|
1653
|
+
// them. Only for schemas where that cannot change an answer; see
|
|
1654
|
+
// fusedRemovalNodes.
|
|
1655
|
+
let removeNodes = null
|
|
1656
|
+
if (opts && opts.removeAdditional) {
|
|
1657
|
+
removeNodes = fusedRemovalNodes(schema)
|
|
1658
|
+
if (removeNodes === null) return null
|
|
1659
|
+
}
|
|
1493
1660
|
|
|
1494
1661
|
if (!sharedCodegenGate(schema, schemaMap)) return null
|
|
1495
1662
|
|
|
@@ -1549,7 +1716,7 @@ function compileToJSCodegen(schema, schemaMap, userFormats) {
|
|
|
1549
1716
|
if (hasUnresolvableRef(schema, rootDefs, anchors, schemaMap, new Set())) return null
|
|
1550
1717
|
if (needsBaseTracking(schema, schemaMap, new Set())) return null
|
|
1551
1718
|
|
|
1552
|
-
const ctx = { varCounter: 0, helpers: [], helperCode: [], preamble: [], shared: [], closureVars: ['_cpLen'], closureVals: [_cpLen], rootDefs, refStack: new Set(), schemaMap: schemaMap || null, anchors, rootSchema: schema, userFormats: userFormats || null }
|
|
1719
|
+
const ctx = { varCounter: 0, helpers: [], helperCode: [], preamble: [], shared: [], closureVars: ['_cpLen'], closureVals: [_cpLen], rootDefs, refStack: new Set(), schemaMap: schemaMap || null, anchors, rootSchema: schema, userFormats: userFormats || null, removeNodes }
|
|
1553
1720
|
const lines = []
|
|
1554
1721
|
genCode(schema, 'd', lines, ctx)
|
|
1555
1722
|
|
|
@@ -1871,6 +2038,9 @@ function _deferOrInline(ctx, lines, v, check) {
|
|
|
1871
2038
|
// 'array' = we know v is an array
|
|
1872
2039
|
// 'string' / 'number' / 'integer' = we know the primitive type
|
|
1873
2040
|
function genCode(schema, v, lines, ctx, knownType) {
|
|
2041
|
+
return withPlain(schema, v, lines, ctx, () => genCodeNode(schema, v, lines, ctx, knownType))
|
|
2042
|
+
}
|
|
2043
|
+
function genCodeNode(schema, v, lines, ctx, knownType) {
|
|
1874
2044
|
// A boolean schema is a constant. false rejects whatever reached it; true
|
|
1875
2045
|
// constrains nothing and emits nothing, which is correct only because the
|
|
1876
2046
|
// caller's own checks still stand around this call.
|
|
@@ -2077,24 +2247,37 @@ function genCode(schema, v, lines, ctx, knownType) {
|
|
|
2077
2247
|
const hoisted = {} // key -> access expression
|
|
2078
2248
|
if (schema.required && schema.properties && isObj) {
|
|
2079
2249
|
const reqChecks = []
|
|
2250
|
+
const typed = []
|
|
2080
2251
|
for (const key of schema.required) {
|
|
2081
2252
|
hoisted[key] = `${v}[${JSON.stringify(key)}]`
|
|
2082
2253
|
const prop = schema.properties[key]
|
|
2083
2254
|
const hasTypeCheck = prop && (prop.type || prop.enum || prop.const !== undefined)
|
|
2084
|
-
|
|
2085
|
-
|
|
2255
|
+
// A typed property needs no separate presence check on an ordinary
|
|
2256
|
+
// object: its type check fails on undefined. A name on Object.prototype
|
|
2257
|
+
// is never absent that way, and it is checked explicitly.
|
|
2258
|
+
if (!hasTypeCheck || PROTO_NAMES.has(key)) {
|
|
2259
|
+
reqChecks.push(`!${ownKeyExpr(ctx, v, key)}`)
|
|
2260
|
+
} else {
|
|
2261
|
+
typed.push(key)
|
|
2086
2262
|
}
|
|
2087
2263
|
}
|
|
2088
2264
|
if (reqChecks.length > 0) {
|
|
2089
2265
|
lines.push(`if(${reqChecks.join('||')})return false`)
|
|
2090
2266
|
}
|
|
2267
|
+
// On an object with another prototype the typed ones may be inherited.
|
|
2268
|
+
if (typed.length > 0) {
|
|
2269
|
+
hoistOnce(ctx, '_ownHoisted', OWN_HELPERS)
|
|
2270
|
+
// Read directly: the object type is already established here, and a
|
|
2271
|
+
// flag declared for this alone costs more than it saves. The list is
|
|
2272
|
+
// built only on that path, which an ordinary object never takes.
|
|
2273
|
+
lines.push(`if(${v}.__proto__!==Object.prototype&&!_hall(${v},${JSON.stringify(typed)}))return false`)
|
|
2274
|
+
}
|
|
2091
2275
|
} else if (schema.required && schema.required.length > 0) {
|
|
2276
|
+
const checks = schema.required.map(key => `!${ownKeyExpr(ctx, v, key)}`)
|
|
2092
2277
|
if (isObj) {
|
|
2093
|
-
const checks = schema.required.map(key => `${v}[${JSON.stringify(key)}]===undefined`)
|
|
2094
2278
|
lines.push(`if(${checks.join('||')})return false`)
|
|
2095
2279
|
} else {
|
|
2096
2280
|
// required applies to objects only; other types are ignored.
|
|
2097
|
-
const checks = schema.required.map(key => `${v}[${JSON.stringify(key)}]===undefined`)
|
|
2098
2281
|
lines.push(`if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})&&(${checks.join('||')}))return false`)
|
|
2099
2282
|
}
|
|
2100
2283
|
}
|
|
@@ -2247,7 +2430,21 @@ function genCode(schema, v, lines, ctx, knownType) {
|
|
|
2247
2430
|
// additionalProperties -- deferred to end of function for better V8 optimization
|
|
2248
2431
|
// (type checks run first in hot path, expensive prop count check last)
|
|
2249
2432
|
// Skip if patternProperties is present — it will handle additionalProperties in a unified loop
|
|
2250
|
-
if (schema.additionalProperties === false && schema.properties && !schema.patternProperties) {
|
|
2433
|
+
if (schema.additionalProperties === false && schema.properties && !schema.patternProperties && ctx.removeNodes && ctx.removeNodes.has(schema)) {
|
|
2434
|
+
const kv = `_rk${ctx.varCounter++}`
|
|
2435
|
+
const declared = Object.keys(schema.properties)
|
|
2436
|
+
let loop = `for(var ${kv} in ${v})if(${declared.map((k) => `${kv}!==${JSON.stringify(k)}`).join('&&')})delete ${v}[${kv}]`
|
|
2437
|
+
// When every declared property is required, the verdict has already
|
|
2438
|
+
// established they are all there, so a key count equal to the declared
|
|
2439
|
+
// count means nothing is extra and the deleting walk, which compares every
|
|
2440
|
+
// key against the declared names, need not run.
|
|
2441
|
+
const req = new Set(schema.required || [])
|
|
2442
|
+
if (declared.length <= 15 && declared.every((k) => req.has(k))) {
|
|
2443
|
+
const n = `_rn${ctx.varCounter++}`
|
|
2444
|
+
loop = `{var ${n}=0;for(var ${kv} in ${v})${n}++;if(${n}!==${declared.length}){${loop}}}`
|
|
2445
|
+
}
|
|
2446
|
+
_deferOrInline(ctx, lines, v, isObj ? loop : `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){${loop}}`)
|
|
2447
|
+
} else if (schema.additionalProperties === false && schema.properties && !schema.patternProperties) {
|
|
2251
2448
|
const propCount = Object.keys(schema.properties).length
|
|
2252
2449
|
const allRequired = schema.required && schema.required.length === propCount
|
|
2253
2450
|
const inner = allRequired
|
|
@@ -2287,8 +2484,8 @@ function genCode(schema, v, lines, ctx, knownType) {
|
|
|
2287
2484
|
// dependentRequired
|
|
2288
2485
|
if (schema.dependentRequired) {
|
|
2289
2486
|
for (const [key, deps] of Object.entries(schema.dependentRequired)) {
|
|
2290
|
-
const depChecks = deps.map(d =>
|
|
2291
|
-
lines.push(`if(${objGuard}
|
|
2487
|
+
const depChecks = deps.map(d => `!${ownKeyExpr(ctx, v, d)}`).join('||')
|
|
2488
|
+
lines.push(`if(${objGuard}${ownKeyExpr(ctx, v, key)}&&(${depChecks}))return false`)
|
|
2292
2489
|
}
|
|
2293
2490
|
}
|
|
2294
2491
|
|
|
@@ -2425,7 +2622,7 @@ function genCode(schema, v, lines, ctx, knownType) {
|
|
|
2425
2622
|
if (schema.dependentSchemas) {
|
|
2426
2623
|
for (const [key, depSchema] of Object.entries(schema.dependentSchemas)) {
|
|
2427
2624
|
const guard = isObj ? '' : `typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})&&`
|
|
2428
|
-
lines.push(`if(${guard}${
|
|
2625
|
+
lines.push(`if(${guard}${ownKeyExpr(ctx, v, key)}){`)
|
|
2429
2626
|
ctx.condDepth = (ctx.condDepth || 0) + 1
|
|
2430
2627
|
genCode(depSchema, v, lines, ctx, effectiveType)
|
|
2431
2628
|
ctx.condDepth--
|
|
@@ -2491,7 +2688,7 @@ function genCode(schema, v, lines, ctx, knownType) {
|
|
|
2491
2688
|
// Optional — hoist to local, check undefined
|
|
2492
2689
|
const oi = ctx.varCounter++
|
|
2493
2690
|
const local = `_o${oi}`
|
|
2494
|
-
lines.push(`{const ${local}=${v}[${JSON.stringify(key)}];if(${local}!==undefined){`)
|
|
2691
|
+
lines.push(`{const ${local}=${v}[${JSON.stringify(key)}];if(${local}!==undefined&&${ownGuard(ctx, v, key)}){`)
|
|
2495
2692
|
const combined = tryGenCombined(prop, local, ctx)
|
|
2496
2693
|
if (combined) {
|
|
2497
2694
|
lines.push(combined)
|
|
@@ -2500,7 +2697,7 @@ function genCode(schema, v, lines, ctx, knownType) {
|
|
|
2500
2697
|
}
|
|
2501
2698
|
lines.push(`}}`)
|
|
2502
2699
|
} else {
|
|
2503
|
-
lines.push(`if(typeof ${v}==='object'&&${v}!==null&&${
|
|
2700
|
+
lines.push(`if(typeof ${v}==='object'&&${v}!==null&&${ownKeyExpr(ctx, v, key)}){`)
|
|
2504
2701
|
genCode(prop, `${v}[${JSON.stringify(key)}]`, lines, ctx)
|
|
2505
2702
|
lines.push(`}`)
|
|
2506
2703
|
}
|
|
@@ -2864,7 +3061,7 @@ function genCode(schema, v, lines, ctx, knownType) {
|
|
|
2864
3061
|
for (const [trigger, depSchema] of Object.entries(schema.dependentSchemas)) {
|
|
2865
3062
|
const depResult = collectEvaluated(depSchema, ctx.schemaMap, ctx.rootDefs)
|
|
2866
3063
|
if (depResult.props && depResult.props.length > 0) {
|
|
2867
|
-
lines.push(`if(${
|
|
3064
|
+
lines.push(`if(${ownKeyExpr(ctx, v, trigger)}){${depResult.props.map(k => `${evVar}[${JSON.stringify(k)}]=1`).join(';')}}`)
|
|
2868
3065
|
}
|
|
2869
3066
|
}
|
|
2870
3067
|
const inner = `for(var _k in ${v}){if(!${evVar}[_k])return false}`
|
|
@@ -3462,7 +3659,7 @@ function compileToJSCodegenWithErrors(schema, schemaMap, userFormats, sourceOpts
|
|
|
3462
3659
|
if (typeof schema === 'boolean') {
|
|
3463
3660
|
return schema
|
|
3464
3661
|
? () => ({ valid: true, errors: [] })
|
|
3465
|
-
: () => ({ valid: false, errors: [{ keyword: '
|
|
3662
|
+
: () => ({ valid: false, errors: [{ keyword: 'not', instancePath: '', schemaPath: '#', params: {}, message: 'boolean schema is false' }] })
|
|
3466
3663
|
}
|
|
3467
3664
|
if (typeof schema !== 'object' || schema === null) return null
|
|
3468
3665
|
if (!sharedCodegenGate(schema, schemaMap)) return null
|
|
@@ -3662,6 +3859,9 @@ function emitBranchCollapse (branches, keyword, v, pathExpr, lines, ctx, schemaP
|
|
|
3662
3859
|
// Instead of `return false`, pushes to `_e` array and optionally early-returns.
|
|
3663
3860
|
// `_all` parameter: if falsy, return after first error.
|
|
3664
3861
|
function genCodeE(schema, v, pathExpr, lines, ctx, schemaPrefix) {
|
|
3862
|
+
return withPlain(schema, v, lines, ctx, () => genCodeENode(schema, v, pathExpr, lines, ctx, schemaPrefix))
|
|
3863
|
+
}
|
|
3864
|
+
function genCodeENode(schema, v, pathExpr, lines, ctx, schemaPrefix) {
|
|
3665
3865
|
if (!schemaPrefix) schemaPrefix = '#'
|
|
3666
3866
|
// A false schema reports one error at the value that reached it, with the
|
|
3667
3867
|
// keyword the interpreter has always used for it, `not` (lib/plan-compiler.js
|
|
@@ -3823,7 +4023,7 @@ function genCodeE(schema, v, pathExpr, lines, ctx, schemaPrefix) {
|
|
|
3823
4023
|
const reqSp = `${schemaPrefix}/required`
|
|
3824
4024
|
const reqLit = buildErrorLiteral({ keyword: 'required', schemaPath: reqSp, sourceMap: ctx.sourceMap })
|
|
3825
4025
|
for (const key of schema.required) {
|
|
3826
|
-
lines.push(`if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})
|
|
4026
|
+
lines.push(`if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})&&!${ownKeyExpr(ctx, v, key)}){_e.push({code:'${reqLit.codeStr}',keyword:'required',instancePath:${pathExpr||'""'},schemaPath:'${reqSp}'${ordinalField(ctx, `${reqSp}`)},params:{missingProperty:'${esc(key)}'},message:"must have required property '${esc(key)}'",docUrl:'${reqLit.docUrl}'${reqLit.frame}});if(!_all)return{valid:false,errors:_e}}`)
|
|
3827
4027
|
}
|
|
3828
4028
|
}
|
|
3829
4029
|
|
|
@@ -4040,7 +4240,7 @@ function genCodeE(schema, v, pathExpr, lines, ctx, schemaPrefix) {
|
|
|
4040
4240
|
const drLit = buildErrorLiteral({ keyword: 'dependentRequired', schemaPath: drSp, sourceMap: ctx.sourceMap })
|
|
4041
4241
|
for (const [key, deps] of Object.entries(schema.dependentRequired)) {
|
|
4042
4242
|
for (const dep of deps) {
|
|
4043
|
-
lines.push(`if(typeof ${v}==='object'&&${v}!==null&&${
|
|
4243
|
+
lines.push(`if(typeof ${v}==='object'&&${v}!==null&&${ownKeyExpr(ctx, v, key)}&&!${ownKeyExpr(ctx, v, dep)}){_e.push({code:'${drLit.codeStr}',keyword:'required',instancePath:${pathExpr||'""'},schemaPath:'${drSp}'${ordinalField(ctx, `${drSp}`)},params:{missingProperty:'${esc(dep)}'},message:"must have required property '${esc(dep)}'",docUrl:'${drLit.docUrl}'${drLit.frame}});if(!_all)return{valid:false,errors:_e}}`)
|
|
4044
4244
|
}
|
|
4045
4245
|
}
|
|
4046
4246
|
}
|
|
@@ -4049,7 +4249,7 @@ function genCodeE(schema, v, pathExpr, lines, ctx, schemaPrefix) {
|
|
|
4049
4249
|
if (schema.properties) {
|
|
4050
4250
|
for (const [key, prop] of Object.entries(schema.properties)) {
|
|
4051
4251
|
const childPath = childPathExpr(pathExpr, ptrSeg(key))
|
|
4052
|
-
lines.push(`if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})&&${
|
|
4252
|
+
lines.push(`if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})&&${ownKeyExpr(ctx, v, key)}){`)
|
|
4053
4253
|
genCodeE(prop, `${v}[${JSON.stringify(key)}]`, childPath, lines, ctx, schemaPrefix+'/properties/'+ptrSeg(key))
|
|
4054
4254
|
lines.push(`}`)
|
|
4055
4255
|
}
|
|
@@ -4118,7 +4318,7 @@ function genCodeE(schema, v, pathExpr, lines, ctx, schemaPrefix) {
|
|
|
4118
4318
|
// dependentSchemas
|
|
4119
4319
|
if (schema.dependentSchemas) {
|
|
4120
4320
|
for (const [key, depSchema] of Object.entries(schema.dependentSchemas)) {
|
|
4121
|
-
lines.push(`if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})&&${
|
|
4321
|
+
lines.push(`if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})&&${ownKeyExpr(ctx, v, key)}){`)
|
|
4122
4322
|
genCodeE(depSchema, v, pathExpr, lines, ctx, schemaPrefix+'/dependentSchemas/'+ptrSeg(key))
|
|
4123
4323
|
lines.push(`}`)
|
|
4124
4324
|
}
|
|
@@ -4286,7 +4486,7 @@ function compileToJSCombined(schema, VALID_RESULT, schemaMap, userFormats) {
|
|
|
4286
4486
|
if (typeof schema === 'boolean') {
|
|
4287
4487
|
return schema
|
|
4288
4488
|
? () => VALID_RESULT
|
|
4289
|
-
: () => ({ valid: false, errors: [{ keyword: '
|
|
4489
|
+
: () => ({ valid: false, errors: [{ keyword: 'not', instancePath: '', schemaPath: '#', params: {}, message: 'boolean schema is false' }] })
|
|
4290
4490
|
}
|
|
4291
4491
|
if (typeof schema !== 'object' || schema === null) return null
|
|
4292
4492
|
if (!sharedCodegenGate(schema, schemaMap)) return null
|
|
@@ -4382,6 +4582,9 @@ function compileToJSCombined(schema, VALID_RESULT, schemaMap, userFormats) {
|
|
|
4382
4582
|
// After type check passes → use optimizations (destructuring, no guards).
|
|
4383
4583
|
// If type check fails → push error, skip property checks (they'd crash).
|
|
4384
4584
|
function genCodeC(schema, v, pathExpr, lines, ctx, schemaPrefix) {
|
|
4585
|
+
return withPlain(schema, v, lines, ctx, () => genCodeCNode(schema, v, pathExpr, lines, ctx, schemaPrefix))
|
|
4586
|
+
}
|
|
4587
|
+
function genCodeCNode(schema, v, pathExpr, lines, ctx, schemaPrefix) {
|
|
4385
4588
|
if (!schemaPrefix) schemaPrefix = '#'
|
|
4386
4589
|
// Per-node, for the reason spelled out in genCode.
|
|
4387
4590
|
let ppHandledPropertyNames = false
|
|
@@ -4473,14 +4676,28 @@ function genCodeC(schema, v, pathExpr, lines, ctx, schemaPrefix) {
|
|
|
4473
4676
|
// Static error: pre-allocate as frozen closure variable
|
|
4474
4677
|
const ei = ctx.varCounter++
|
|
4475
4678
|
const errVar = `_E${ei}`
|
|
4476
|
-
|
|
4477
|
-
|
|
4679
|
+
// The path and message are source literals; evaluate them rather than
|
|
4680
|
+
// strip the quotes, so an escaped quote or backslash in a key comes
|
|
4681
|
+
// out as the character and not as the escape.
|
|
4682
|
+
let pathVal, msgVal
|
|
4683
|
+
try {
|
|
4684
|
+
pathVal = pathExpr ? Function('return ' + pathExpr)() : ''
|
|
4685
|
+
msgVal = Function('return ' + msgCode)()
|
|
4686
|
+
} catch {
|
|
4687
|
+
// Not a literal that parses on its own (a key with a raw line break):
|
|
4688
|
+
// leave it to the inline form below, which declines the same way the
|
|
4689
|
+
// whole function would.
|
|
4690
|
+
pathVal = undefined
|
|
4691
|
+
}
|
|
4692
|
+
if (pathVal !== undefined) {
|
|
4693
|
+
const spVal = unescapeSp(sp)
|
|
4478
4694
|
ctx.closureVars.push(errVar)
|
|
4479
|
-
const o = ordinalFor(ctx.rootSchema,
|
|
4480
|
-
const lit = {keyword, instancePath: pathVal, schemaPath:
|
|
4695
|
+
const o = ordinalFor(ctx.rootSchema, spVal)
|
|
4696
|
+
const lit = {keyword, instancePath: pathVal, schemaPath: spVal, params: Object.freeze(paramsVal), message: msgVal}
|
|
4481
4697
|
if (o !== null) lit._o = o
|
|
4482
4698
|
ctx.closureVals.push(Object.freeze(lit))
|
|
4483
4699
|
return `(_e||(_e=[])).push(${errVar})`
|
|
4700
|
+
}
|
|
4484
4701
|
}
|
|
4485
4702
|
}
|
|
4486
4703
|
// Dynamic path (e.g., array index): inline as before
|
|
@@ -4543,15 +4760,22 @@ function genCodeC(schema, v, pathExpr, lines, ctx, schemaPrefix) {
|
|
|
4543
4760
|
destructKeys.push(`${JSON.stringify(key)}:${lv}`)
|
|
4544
4761
|
}
|
|
4545
4762
|
}
|
|
4546
|
-
if (destructKeys.length > 0)
|
|
4763
|
+
if (destructKeys.length > 0) {
|
|
4764
|
+
lines.push(`let{${destructKeys.join(',')}}=${v}`)
|
|
4765
|
+
// A value the object only inherits reads as absent, here and in the
|
|
4766
|
+
// property checks that use the same local below.
|
|
4767
|
+
for (const key of schema.required) {
|
|
4768
|
+
if (hoisted[key]) lines.push(`if(${hoisted[key]}!==undefined&&!${ownGuard(ctx, v, key)})${hoisted[key]}=undefined`)
|
|
4769
|
+
}
|
|
4770
|
+
}
|
|
4547
4771
|
for (const key of schema.required) {
|
|
4548
|
-
const check = hoisted[key] ? `${hoisted[key]}===undefined` :
|
|
4772
|
+
const check = hoisted[key] ? `${hoisted[key]}===undefined` : `!${ownKeyExpr(ctx, v, key)}`
|
|
4549
4773
|
if (isStaticPath) {
|
|
4550
4774
|
const ei = ctx.varCounter++
|
|
4551
4775
|
const errVar = `_E${ei}`
|
|
4552
|
-
const pathVal = pathExpr ? pathExpr
|
|
4776
|
+
const pathVal = pathExpr ? literalValue(pathExpr) : ''
|
|
4553
4777
|
ctx.closureVars.push(errVar)
|
|
4554
|
-
ctx.closureVals.push(Object.freeze({keyword: 'required', instancePath: pathVal, schemaPath: `${schemaPrefix}/required
|
|
4778
|
+
ctx.closureVals.push(Object.freeze({keyword: 'required', instancePath: pathVal, schemaPath: unescapeSp(`${schemaPrefix}/required`), params: Object.freeze({missingProperty: key}), message: `must have required property '${key}'`}))
|
|
4555
4779
|
lines.push(`if(${check}){(_e||(_e=[])).push(${errVar})}`)
|
|
4556
4780
|
} else {
|
|
4557
4781
|
lines.push(`if(${check}){(_e||(_e=[])).push({keyword:'required',instancePath:${pathExpr||'""'},schemaPath:'${schemaPrefix}/required'${ordinalField(ctx, `${schemaPrefix}/required`)},params:{missingProperty:'${esc(key)}'},message:"must have required property '${esc(key)}'"})}`)
|
|
@@ -4563,12 +4787,12 @@ function genCodeC(schema, v, pathExpr, lines, ctx, schemaPrefix) {
|
|
|
4563
4787
|
if (isStatic) {
|
|
4564
4788
|
const ei = ctx.varCounter++
|
|
4565
4789
|
const errVar = `_E${ei}`
|
|
4566
|
-
const pathVal = pathExpr ? pathExpr
|
|
4790
|
+
const pathVal = pathExpr ? literalValue(pathExpr) : ''
|
|
4567
4791
|
ctx.closureVars.push(errVar)
|
|
4568
|
-
ctx.closureVals.push(Object.freeze({keyword: 'required', instancePath: pathVal, schemaPath: `${schemaPrefix}/required
|
|
4569
|
-
lines.push(`if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})
|
|
4792
|
+
ctx.closureVals.push(Object.freeze({keyword: 'required', instancePath: pathVal, schemaPath: unescapeSp(`${schemaPrefix}/required`), params: Object.freeze({missingProperty: key}), message: `must have required property '${key}'`}))
|
|
4793
|
+
lines.push(`if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})&&!${ownKeyExpr(ctx, v, key)}){(_e||(_e=[])).push(${errVar})}`)
|
|
4570
4794
|
} else {
|
|
4571
|
-
lines.push(`if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})
|
|
4795
|
+
lines.push(`if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})&&!${ownKeyExpr(ctx, v, key)}){(_e||(_e=[])).push({keyword:'required',instancePath:${pathExpr||'""'},schemaPath:'${schemaPrefix}/required'${ordinalField(ctx, `${schemaPrefix}/required`)},params:{missingProperty:'${esc(key)}'},message:"must have required property '${esc(key)}'"})}`)
|
|
4572
4796
|
}
|
|
4573
4797
|
}
|
|
4574
4798
|
}
|
|
@@ -4728,12 +4952,12 @@ function genCodeC(schema, v, pathExpr, lines, ctx, schemaPrefix) {
|
|
|
4728
4952
|
if (isStatic) {
|
|
4729
4953
|
const ei = ctx.varCounter++
|
|
4730
4954
|
const errVar = `_E${ei}`
|
|
4731
|
-
const pathVal = pathExpr ? pathExpr
|
|
4955
|
+
const pathVal = pathExpr ? literalValue(pathExpr) : ''
|
|
4732
4956
|
ctx.closureVars.push(errVar)
|
|
4733
|
-
ctx.closureVals.push(Object.freeze({keyword: 'required', instancePath: pathVal, schemaPath: `${schemaPrefix}/dependentRequired
|
|
4734
|
-
lines.push(`if(typeof ${v}==='object'&&${v}!==null&&${
|
|
4957
|
+
ctx.closureVals.push(Object.freeze({keyword: 'required', instancePath: pathVal, schemaPath: unescapeSp(`${schemaPrefix}/dependentRequired`), params: Object.freeze({missingProperty: dep}), message: `must have required property '${dep}'`}))
|
|
4958
|
+
lines.push(`if(typeof ${v}==='object'&&${v}!==null&&${ownKeyExpr(ctx, v, key)}&&!${ownKeyExpr(ctx, v, dep)}){(_e||(_e=[])).push(${errVar})}`)
|
|
4735
4959
|
} else {
|
|
4736
|
-
lines.push(`if(typeof ${v}==='object'&&${v}!==null&&${
|
|
4960
|
+
lines.push(`if(typeof ${v}==='object'&&${v}!==null&&${ownKeyExpr(ctx, v, key)}&&!${ownKeyExpr(ctx, v, dep)}){(_e||(_e=[])).push({keyword:'required',instancePath:${pathExpr||'""'},schemaPath:'${schemaPrefix}/dependentRequired'${ordinalField(ctx, `${schemaPrefix}/dependentRequired`)},params:{missingProperty:'${esc(dep)}'},message:"must have required property '${esc(dep)}'"})}`)
|
|
4737
4961
|
}
|
|
4738
4962
|
}
|
|
4739
4963
|
}
|
|
@@ -4745,16 +4969,17 @@ function genCodeC(schema, v, pathExpr, lines, ctx, schemaPrefix) {
|
|
|
4745
4969
|
const pv = hoisted[key] || `${v}[${JSON.stringify(key)}]`
|
|
4746
4970
|
const childPath = childPathExpr(pathExpr, ptrSeg(key))
|
|
4747
4971
|
if (requiredSet.has(key) && isObj) {
|
|
4748
|
-
|
|
4972
|
+
// A hoisted local was already cleared if the value is only inherited.
|
|
4973
|
+
lines.push(`if(${pv}!==undefined${hoisted[key] ? '' : '&&' + ownGuard(ctx, v, key)}){`)
|
|
4749
4974
|
genCodeC(prop, pv, childPath, lines, ctx, schemaPrefix+'/properties/'+ptrSeg(key))
|
|
4750
4975
|
lines.push(`}`)
|
|
4751
4976
|
} else if (isObj) {
|
|
4752
4977
|
const oi = ctx.varCounter++
|
|
4753
|
-
lines.push(`{const _o${oi}=${v}[${JSON.stringify(key)}];if(_o${oi}!==undefined){`)
|
|
4978
|
+
lines.push(`{const _o${oi}=${v}[${JSON.stringify(key)}];if(_o${oi}!==undefined&&${ownGuard(ctx, v, key)}){`)
|
|
4754
4979
|
genCodeC(prop, `_o${oi}`, childPath, lines, ctx, schemaPrefix+'/properties/'+ptrSeg(key))
|
|
4755
4980
|
lines.push(`}}`)
|
|
4756
4981
|
} else {
|
|
4757
|
-
lines.push(`if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})&&${
|
|
4982
|
+
lines.push(`if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})&&${ownKeyExpr(ctx, v, key)}){`)
|
|
4758
4983
|
genCodeC(prop, `${v}[${JSON.stringify(key)}]`, childPath, lines, ctx, schemaPrefix+'/properties/'+ptrSeg(key))
|
|
4759
4984
|
lines.push(`}`)
|
|
4760
4985
|
}
|
|
@@ -4870,7 +5095,7 @@ function genCodeC(schema, v, pathExpr, lines, ctx, schemaPrefix) {
|
|
|
4870
5095
|
// dependentSchemas
|
|
4871
5096
|
if (schema.dependentSchemas) {
|
|
4872
5097
|
for (const [key, depSchema] of Object.entries(schema.dependentSchemas)) {
|
|
4873
|
-
lines.push(`if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})&&${
|
|
5098
|
+
lines.push(`if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})&&${ownKeyExpr(ctx, v, key)}){`)
|
|
4874
5099
|
genCodeC(depSchema, v, pathExpr, lines, ctx, schemaPrefix+'/dependentSchemas/'+ptrSeg(key))
|
|
4875
5100
|
lines.push(`}`)
|
|
4876
5101
|
}
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Generated source from interpreter plans: the verdict function.
|
|
4
|
+
//
|
|
5
|
+
// The interpreted engine is the reference every other engine is tested
|
|
6
|
+
// against, and its Plan is a schema node with every keyword resolved once.
|
|
7
|
+
// lib/plan-compiler.js turns plans into closures; this turns them into source,
|
|
8
|
+
// deciding at compile time exactly what plan-compiler decides, and calling the
|
|
9
|
+
// same helpers (deepEqual, the code point bounds, multipleOfOk, the format
|
|
10
|
+
// functions and compiled patterns the plan already holds), so the answers
|
|
11
|
+
// cannot drift the way three hand-written generators did.
|
|
12
|
+
//
|
|
13
|
+
// The scope is deliberately narrow for now: value-level keywords, properties
|
|
14
|
+
// and additionalProperties, prefixItems and items, and the in-place
|
|
15
|
+
// applicators. Everything else, references, unevaluated*, contains,
|
|
16
|
+
// propertyNames, patternProperties, dependentSchemas, custom keywords, makes
|
|
17
|
+
// the compiler return null. It never emits a function with a keyword missing.
|
|
18
|
+
|
|
19
|
+
const { createInterpreter, _planInternals: I } = require('./interpreter');
|
|
20
|
+
const { Plan, deepEqual, cpAtLeast, cpAtMost, multipleOfOk, T_STRING, T_NUMBER, T_INTEGER, T_BOOLEAN, T_NULL, T_OBJECT, T_ARRAY } = I;
|
|
21
|
+
|
|
22
|
+
const DECLINE = Symbol('plan-source.decline');
|
|
23
|
+
const ALL_TYPES = T_STRING | T_NUMBER | T_INTEGER | T_BOOLEAN | T_NULL | T_OBJECT | T_ARRAY;
|
|
24
|
+
|
|
25
|
+
// The own-property test the generators use; see ownKeyExpr in js-compiler.js.
|
|
26
|
+
const HELPERS = 'const _hop=Object.prototype.hasOwnProperty;';
|
|
27
|
+
|
|
28
|
+
const PROTO_NAMES = new Set([...Object.getOwnPropertyNames(Object.prototype), '__proto__']);
|
|
29
|
+
|
|
30
|
+
function isPrimitive(x) {
|
|
31
|
+
return x === null || typeof x === 'string' || typeof x === 'boolean' || (typeof x === 'number' && isFinite(x));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function lit(x) {
|
|
35
|
+
return JSON.stringify(x);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function createCtx() {
|
|
39
|
+
return { n: 0, argNames: [], argVals: [], fns: [], fnFor: new Map() };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// A value the emitted code needs by reference: a function, a RegExp, a list.
|
|
43
|
+
function arg(ctx, value) {
|
|
44
|
+
const i = ctx.argVals.indexOf(value);
|
|
45
|
+
if (i !== -1) return ctx.argNames[i];
|
|
46
|
+
const name = '_a' + ctx.argVals.length;
|
|
47
|
+
ctx.argNames.push(name);
|
|
48
|
+
ctx.argVals.push(value);
|
|
49
|
+
return name;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function local(ctx) {
|
|
53
|
+
return '_v' + ctx.n++;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// JSON Schema type bits, exactly as dataBits() assigns them: a non-finite
|
|
57
|
+
// number has no type, so it matches neither number nor integer.
|
|
58
|
+
function typeTest(mask, v) {
|
|
59
|
+
const parts = [];
|
|
60
|
+
if (mask & T_STRING) parts.push(`typeof ${v}==='string'`);
|
|
61
|
+
if (mask & T_NUMBER) parts.push(`(typeof ${v}==='number'&&isFinite(${v}))`);
|
|
62
|
+
else if (mask & T_INTEGER) parts.push(`Number.isInteger(${v})`);
|
|
63
|
+
if (mask & T_BOOLEAN) parts.push(`typeof ${v}==='boolean'`);
|
|
64
|
+
if (mask & T_NULL) parts.push(`${v}===null`);
|
|
65
|
+
if ((mask & T_OBJECT) && (mask & T_ARRAY)) parts.push(`(typeof ${v}==='object'&&${v}!==null)`);
|
|
66
|
+
else if (mask & T_OBJECT) parts.push(`(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v}))`);
|
|
67
|
+
else if (mask & T_ARRAY) parts.push(`Array.isArray(${v})`);
|
|
68
|
+
return parts.length ? parts.join('||') : 'false';
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// `plain` names a local holding `v.__proto__===Object.prototype`, read once per
|
|
72
|
+
// object. With the key a constant at the `in`, the engine keeps the check
|
|
73
|
+
// monomorphic; through a shared helper the key is a parameter and it is not.
|
|
74
|
+
function ownTest(v, key, plain) {
|
|
75
|
+
const k = lit(key);
|
|
76
|
+
return PROTO_NAMES.has(key) ? `_hop.call(${v},${k})` : `(${plain}?${k} in ${v}:_hop.call(${v},${k}))`;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const K_NUM = T_NUMBER | T_INTEGER;
|
|
80
|
+
// The body under a guard for one kind of value, given what the type check
|
|
81
|
+
// already established: unconditional when the value can only be that kind,
|
|
82
|
+
// nothing at all when it cannot be.
|
|
83
|
+
function guarded(known, kind, cond, body) {
|
|
84
|
+
if (!body) return '';
|
|
85
|
+
if ((known & kind) === 0) return '';
|
|
86
|
+
if ((known & ~kind) === 0) return body;
|
|
87
|
+
return `if(${cond}){${body}}`;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Value-level keywords, in compileLeafV's order and with its meaning.
|
|
91
|
+
function emitLeaf(ctx, P, v, known) {
|
|
92
|
+
let out = '';
|
|
93
|
+
if (P.hasType) out += `if(!(${typeTest(P.typeMask, v)}))return false;`;
|
|
94
|
+
if (P.enum !== null) {
|
|
95
|
+
const vals = P.enum;
|
|
96
|
+
if (vals.length === 0) out += 'return false;';
|
|
97
|
+
else if (vals.every(isPrimitive)) out += `if(!(${vals.map((x) => `${v}===${lit(x)}`).join('||')}))return false;`;
|
|
98
|
+
else {
|
|
99
|
+
const a = arg(ctx, vals);
|
|
100
|
+
const de = arg(ctx, deepEqual);
|
|
101
|
+
const i = local(ctx);
|
|
102
|
+
out += `{let ${i}=0;for(;${i}<${a}.length;${i}++)if(${de}(${a}[${i}],${v}))break;if(${i}===${a}.length)return false}`;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
if (P.hasConst) {
|
|
106
|
+
out += isPrimitive(P.const)
|
|
107
|
+
? `if(${v}!==${lit(P.const)})return false;`
|
|
108
|
+
: `if(!${arg(ctx, deepEqual)}(${arg(ctx, P.const)},${v}))return false;`;
|
|
109
|
+
}
|
|
110
|
+
if (P.hasNumber) {
|
|
111
|
+
let n = '';
|
|
112
|
+
if (P.minimum !== undefined) n += `if(${v}<${lit(P.minimum)})return false;`;
|
|
113
|
+
if (P.maximum !== undefined) n += `if(${v}>${lit(P.maximum)})return false;`;
|
|
114
|
+
if (P.exclusiveMinimum !== undefined) n += `if(${v}<=${lit(P.exclusiveMinimum)})return false;`;
|
|
115
|
+
if (P.exclusiveMaximum !== undefined) n += `if(${v}>=${lit(P.exclusiveMaximum)})return false;`;
|
|
116
|
+
if (P.multipleOf !== undefined) n += `if(!${arg(ctx, multipleOfOk)}(${v},${lit(P.multipleOf)}))return false;`;
|
|
117
|
+
out += guarded(known, K_NUM, `typeof ${v}==='number'&&isFinite(${v})`, n);
|
|
118
|
+
}
|
|
119
|
+
if (P.hasString) {
|
|
120
|
+
let s = '';
|
|
121
|
+
if (P.minLength !== undefined) s += `if(!${arg(ctx, cpAtLeast)}(${v},${lit(P.minLength)}))return false;`;
|
|
122
|
+
if (P.maxLength !== undefined) s += `if(!${arg(ctx, cpAtMost)}(${v},${lit(P.maxLength)}))return false;`;
|
|
123
|
+
if (P.pattern !== null) s += `if(!${arg(ctx, P.pattern)}.test(${v}))return false;`;
|
|
124
|
+
if (P.formatFn !== null) s += `if(!${arg(ctx, P.formatFn)}(${v}))return false;`;
|
|
125
|
+
out += guarded(known, T_STRING, `typeof ${v}==='string'`, s);
|
|
126
|
+
}
|
|
127
|
+
if (P.minItems !== undefined || P.maxItems !== undefined || P.uniqueItems) {
|
|
128
|
+
let a = '';
|
|
129
|
+
if (P.minItems !== undefined) a += `if(${v}.length<${lit(P.minItems)})return false;`;
|
|
130
|
+
if (P.maxItems !== undefined) a += `if(${v}.length>${lit(P.maxItems)})return false;`;
|
|
131
|
+
if (P.uniqueItems) {
|
|
132
|
+
const de = arg(ctx, deepEqual);
|
|
133
|
+
const i = local(ctx), j = local(ctx);
|
|
134
|
+
a += `for(let ${i}=0;${i}<${v}.length;${i}++)for(let ${j}=${i}+1;${j}<${v}.length;${j}++)if(${de}(${v}[${i}],${v}[${j}]))return false;`;
|
|
135
|
+
}
|
|
136
|
+
out += guarded(known, T_ARRAY, `Array.isArray(${v})`, a);
|
|
137
|
+
}
|
|
138
|
+
return out;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// The object-level keywords of a node, value-level and structural together, so
|
|
142
|
+
// the prototype of the object is read once for all of them.
|
|
143
|
+
function emitObjectLeaf(ctx, P, v, plain) {
|
|
144
|
+
let o = '';
|
|
145
|
+
if (P.required !== null || P.minProperties !== undefined || P.maxProperties !== undefined || P.dependentRequired !== null) {
|
|
146
|
+
if (P.required !== null) for (const k of P.required) o += `if(!${ownTest(v, k, plain)})return false;`;
|
|
147
|
+
if (P.minProperties !== undefined) o += `if(Object.keys(${v}).length<${lit(P.minProperties)})return false;`;
|
|
148
|
+
if (P.maxProperties !== undefined) o += `if(Object.keys(${v}).length>${lit(P.maxProperties)})return false;`;
|
|
149
|
+
if (P.dependentRequired !== null) {
|
|
150
|
+
for (const [key, deps] of P.dependentRequired) {
|
|
151
|
+
o += `if(${ownTest(v, key, plain)}){${deps.map((dep) => `if(!${ownTest(v, dep, plain)})return false;`).join('')}}`;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return o;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// A subschema asked for a verdict of its own (a branch of anyOf/oneOf, the
|
|
159
|
+
// subject of not/if) becomes a named function, emitted once per plan.
|
|
160
|
+
function branchFn(ctx, node) {
|
|
161
|
+
if (node === true) return '_t';
|
|
162
|
+
if (node === false) return '_f';
|
|
163
|
+
let name = ctx.fnFor.get(node);
|
|
164
|
+
if (name !== undefined) return name;
|
|
165
|
+
name = '_b' + ctx.fnFor.size;
|
|
166
|
+
ctx.fnFor.set(node, name);
|
|
167
|
+
const body = emitNode(ctx, node, 'd');
|
|
168
|
+
ctx.fns.push(`function ${name}(d){${body}return true}`);
|
|
169
|
+
return name;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function emitNode(ctx, node, v) {
|
|
173
|
+
if (node === true) return '';
|
|
174
|
+
if (node === false) return 'return false;';
|
|
175
|
+
if (!(node instanceof Plan)) return '';
|
|
176
|
+
const P = node;
|
|
177
|
+
if (P.tracked || P.hasUnevaluated || P.hasCustom || P.macros !== null ||
|
|
178
|
+
P.contains !== undefined || P.propertyNames !== undefined || P.patternProperties !== null ||
|
|
179
|
+
P.dependentSchemas !== null || P.propertyDependencies !== null) {
|
|
180
|
+
throw DECLINE;
|
|
181
|
+
}
|
|
182
|
+
const known = P.hasType ? P.typeMask : ALL_TYPES;
|
|
183
|
+
let out = emitLeaf(ctx, P, v, known);
|
|
184
|
+
|
|
185
|
+
if (P.prefixItems !== null || P.items !== undefined) {
|
|
186
|
+
let a = '';
|
|
187
|
+
if (P.prefixItems !== null) {
|
|
188
|
+
P.prefixItems.forEach((child, i) => {
|
|
189
|
+
const x = local(ctx);
|
|
190
|
+
const body = emitNode(ctx, child, x);
|
|
191
|
+
if (body) a += `if(${v}.length>${i}){const ${x}=${v}[${i}];${body}}`;
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
if (P.items !== undefined) {
|
|
195
|
+
const start = P.prefixItems !== null ? P.prefixItems.length : 0;
|
|
196
|
+
const i = local(ctx);
|
|
197
|
+
const x = local(ctx);
|
|
198
|
+
const body = emitNode(ctx, P.items, x);
|
|
199
|
+
if (body) a += `for(let ${i}=${start};${i}<${v}.length;${i}++){const ${x}=${v}[${i}];${body}}`;
|
|
200
|
+
}
|
|
201
|
+
out += guarded(known, T_ARRAY, `Array.isArray(${v})`, a);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const plain = local(ctx);
|
|
205
|
+
let o = emitObjectLeaf(ctx, P, v, plain);
|
|
206
|
+
if (P.properties !== null || P.additionalProperties !== undefined) {
|
|
207
|
+
const declared = P.properties !== null ? [...P.properties.keys()] : [];
|
|
208
|
+
if (P.properties !== null) {
|
|
209
|
+
for (const [key, entry] of P.properties) {
|
|
210
|
+
const x = local(ctx);
|
|
211
|
+
const body = emitNode(ctx, entry.node, x);
|
|
212
|
+
if (body) o += `if(${ownTest(v, key, plain)}){const ${x}=${v}[${lit(key)}];${body}}`;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
const ap = P.additionalProperties;
|
|
216
|
+
if (ap !== undefined && ap !== true) {
|
|
217
|
+
const k = local(ctx);
|
|
218
|
+
let isDeclared = 'false';
|
|
219
|
+
if (declared.length > 8) {
|
|
220
|
+
const set = Object.create(null);
|
|
221
|
+
for (const key of declared) set[key] = 1;
|
|
222
|
+
isDeclared = `${arg(ctx, set)}[${k}]===1`;
|
|
223
|
+
} else if (declared.length > 0) {
|
|
224
|
+
isDeclared = declared.map((key) => `${k}===${lit(key)}`).join('||');
|
|
225
|
+
}
|
|
226
|
+
let apBody;
|
|
227
|
+
if (ap === false) apBody = 'return false;';
|
|
228
|
+
else {
|
|
229
|
+
const x = local(ctx);
|
|
230
|
+
const body = emitNode(ctx, ap, x);
|
|
231
|
+
apBody = body ? `const ${x}=${v}[${k}];${body}` : '';
|
|
232
|
+
}
|
|
233
|
+
if (apBody) o += `for(const ${k} in ${v}){if(!${plain}&&!_hop.call(${v},${k}))continue;if(${isDeclared})continue;${apBody}}`;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
if (o) out += guarded(known, T_OBJECT, `typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})`, `const ${plain}=${v}.__proto__===Object.prototype;${o}`);
|
|
237
|
+
|
|
238
|
+
if (P.allOf !== null) for (const child of P.allOf) out += emitNode(ctx, child, v);
|
|
239
|
+
if (P.anyOf !== null) out += `if(!(${P.anyOf.map((c) => `${branchFn(ctx, c)}(${v})`).join('||')}))return false;`;
|
|
240
|
+
if (P.oneOf !== null) {
|
|
241
|
+
const c = local(ctx);
|
|
242
|
+
out += `{let ${c}=0;${P.oneOf.map((b) => `if(${branchFn(ctx, b)}(${v})&&++${c}>1)return false;`).join('')}if(${c}!==1)return false}`;
|
|
243
|
+
}
|
|
244
|
+
if (P.not !== undefined) out += `if(${branchFn(ctx, P.not)}(${v}))return false;`;
|
|
245
|
+
if (P.if !== undefined) {
|
|
246
|
+
const t = P.then !== undefined ? emitNode(ctx, P.then, v) : '';
|
|
247
|
+
const e = P.else !== undefined ? emitNode(ctx, P.else, v) : '';
|
|
248
|
+
if (t || e) out += `if(${branchFn(ctx, P.if)}(${v})){${t}}else{${e}}`;
|
|
249
|
+
}
|
|
250
|
+
return out;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// Returns { fn, source } or null when the schema is outside what this emits.
|
|
254
|
+
function compileVerdict(schema, options) {
|
|
255
|
+
let interp;
|
|
256
|
+
try {
|
|
257
|
+
interp = createInterpreter(schema, options || {});
|
|
258
|
+
} catch {
|
|
259
|
+
return null;
|
|
260
|
+
}
|
|
261
|
+
const ctx = createCtx();
|
|
262
|
+
let body;
|
|
263
|
+
try {
|
|
264
|
+
body = emitNode(ctx, interp.rootNode, 'd');
|
|
265
|
+
} catch (e) {
|
|
266
|
+
if (e === DECLINE) return null;
|
|
267
|
+
throw e;
|
|
268
|
+
}
|
|
269
|
+
const source = HELPERS + 'function _t(){return true}function _f(){return false}' +
|
|
270
|
+
ctx.fns.join('') + `return function(d){${body}return true}`;
|
|
271
|
+
let fn;
|
|
272
|
+
try {
|
|
273
|
+
// eslint-disable-next-line no-new-func
|
|
274
|
+
fn = new Function(...ctx.argNames, source)(...ctx.argVals);
|
|
275
|
+
} catch {
|
|
276
|
+
return null;
|
|
277
|
+
}
|
|
278
|
+
return { fn, source };
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
module.exports = { compileVerdict };
|
package/lib/shape-classifier.js
CHANGED
|
@@ -69,6 +69,15 @@ function isTier0Object(schema) {
|
|
|
69
69
|
for (const r of schema.required) if (typeof r !== 'string') return false;
|
|
70
70
|
}
|
|
71
71
|
const props = schema.properties;
|
|
72
|
+
// The tier-0 validator checks presence through the per-property constraints,
|
|
73
|
+
// so a required name with no entry in properties was never checked at all:
|
|
74
|
+
// `{ type: 'object', required: ['id'] }` accepted `{}` until the schema moved
|
|
75
|
+
// to generated code, and for good when the generator declined it.
|
|
76
|
+
if (schema.required !== undefined) {
|
|
77
|
+
for (const r of schema.required) {
|
|
78
|
+
if (props === undefined || props === null || typeof props !== 'object' || !Object.prototype.hasOwnProperty.call(props, r)) return false;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
72
81
|
if (props === undefined) return true;
|
|
73
82
|
if (typeof props !== 'object' || props === null || Array.isArray(props)) return false;
|
|
74
83
|
const keys = Object.keys(props);
|
package/lib/tier0.js
CHANGED
|
@@ -41,6 +41,12 @@ const F_MULT = 16;
|
|
|
41
41
|
|
|
42
42
|
// Build a constraint tuple for one primitive-typed property.
|
|
43
43
|
// All fields have the same layout so every constraint shares one hidden class.
|
|
44
|
+
// Names found on Object.prototype. For these a read of data[key] finds the
|
|
45
|
+
// inherited value on every ordinary object, so presence is asked of the object
|
|
46
|
+
// itself; see ownKeyExpr in js-compiler.js.
|
|
47
|
+
const PROTO_NAMES = new Set([...Object.getOwnPropertyNames(Object.prototype), '__proto__']);
|
|
48
|
+
const _hop = Object.prototype.hasOwnProperty;
|
|
49
|
+
|
|
44
50
|
function primConstraint(key, propSchema) {
|
|
45
51
|
const t = propSchema.type;
|
|
46
52
|
const hasEnum = Array.isArray(propSchema.enum);
|
|
@@ -53,6 +59,7 @@ function primConstraint(key, propSchema) {
|
|
|
53
59
|
if (typeof propSchema.multipleOf === 'number') numFlags |= F_MULT;
|
|
54
60
|
return {
|
|
55
61
|
key,
|
|
62
|
+
protoName: PROTO_NAMES.has(key),
|
|
56
63
|
typeMask: TYPE_MASK[t] | 0,
|
|
57
64
|
numFlags,
|
|
58
65
|
hasEnum,
|
|
@@ -159,7 +166,10 @@ function tier0ValidateObject(plan, data) {
|
|
|
159
166
|
let seenMask = 0;
|
|
160
167
|
for (let i = 0; i < n; i++) {
|
|
161
168
|
const c = cs[i];
|
|
162
|
-
|
|
169
|
+
let v = data[c.key];
|
|
170
|
+
// Presence is an own-property question: a value the object inherits is
|
|
171
|
+
// treated as absent, as the interpreted engine treats it.
|
|
172
|
+
if (v !== undefined && (c.protoName || data.__proto__ !== Object.prototype) && !_hop.call(data, c.key)) v = undefined;
|
|
163
173
|
if (v === undefined) {
|
|
164
174
|
if (reqMask & (1 << i)) return false;
|
|
165
175
|
continue;
|
package/lib/version.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ata-validator",
|
|
3
|
-
"version": "1.31.
|
|
3
|
+
"version": "1.31.3",
|
|
4
4
|
"description": "JSON Schema validation that compiles for speed and still runs where code generation is blocked. Compiled and interpreted engines answer identically at 100% of the official suite. TypeScript inference, Standard Schema V1, and a build step that emits dependency-free modules.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"module": "index.mjs",
|
|
@@ -51,7 +51,7 @@
|
|
|
51
51
|
"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",
|
|
52
52
|
"build": "cmake-js build --target ata",
|
|
53
53
|
"rebuild": "cmake-js rebuild --target ata",
|
|
54
|
-
"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_scanner_differential.js && node tests/test_validate_json_scanner_cost.js && node tests/test_buffer_reject_cost.js && node tests/test_nan_verdict.js && node tests/test_remove_additional_nested.js && node tests/test_exclusive_bounds.js && node tests/test_engine_differential.js && node tests/test_error_shape_differential.js && node tests/test_output_format.js && node tests/test_retry_message.js && node tests/test_describe_schema.js && node tests/test_aot_parse.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_option.js && node tests/test_compile_cache_order.js && node tests/test_engine_diagnostic.js && node tests/test_format_engine_parity.js && node tests/test_error_accumulator_collision.js && node tests/test_uri_helper_parity.js && node tests/test_uri_fast_path.js && node tests/test_extend_verdict.js && node tests/test_ipv4_regex.js && node tests/test_ipv6_fast_path.js && node tests/test_nested_recursion_decline.js && node tests/test_formats_single_pass.js && node tests/test_email_format_cost.js && node tests/test_format_single_error.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_deferred_def_scope.js && node tests/test_node_scoped_ctx_flags.js && node tests/test_fast_slot_exhaustion.js && node tests/test_ajv_errors.js && node tests/test_custom_keywords.js && node tests/test_strict_schema.js && node tests/test_aot_external_checks.js && node tests/test_ajv_parity.js && node tests/test_user_format_error_path.js && node tests/test_unevaluated_error_path.js && node tests/test_unevaluated_error_shape.js && node tests/test_pattern_properties_errors.js && node tests/test_additional_properties_scaling.js && node tests/test_additional_props_combined_cost.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_error_order_ordinal.js && node tests/test_rejection_shape.js && node tests/test_lazy_normalization.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_single_pass_errors.js && node tests/test_lazy_json_errors.js && node tests/test_lazy_instance.js && node tests/test_cyclic_input.js && node tests/test_native_error_codes.js && node tests/test_verdict_preprocess.js && node tests/test_defaults_own_keys.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_malformed_json_termination.js && node tests/test_data_positions.js && node tests/test_targeted_positions.js && node tests/test_position_map_cost.js && node tests/test_targeted_positions_cost.js && node tests/test_aot_staleness.js && node tests/test_file_positions.js && node tests/test_aot_positions_cost.js && node tests/test_schema_hash.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",
|
|
54
|
+
"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_scanner_differential.js && node tests/test_validate_json_scanner_cost.js && node tests/test_buffer_reject_cost.js && node tests/test_nan_verdict.js && node tests/test_remove_additional_nested.js && node tests/test_exclusive_bounds.js && node tests/test_engine_differential.js && node tests/test_error_shape_differential.js && node tests/test_output_format.js && node tests/test_retry_message.js && node tests/test_describe_schema.js && node tests/test_aot_parse.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_option.js && node tests/test_compile_cache_order.js && node tests/test_engine_diagnostic.js && node tests/test_format_engine_parity.js && node tests/test_error_accumulator_collision.js && node tests/test_uri_helper_parity.js && node tests/test_uri_fast_path.js && node tests/test_extend_verdict.js && node tests/test_ipv4_regex.js && node tests/test_ipv6_fast_path.js && node tests/test_nested_recursion_decline.js && node tests/test_generator_error_content.js && node tests/test_inherited_key_presence.js && node tests/test_plan_source.js && node tests/test_fused_remove_additional.js && node tests/test_formats_single_pass.js && node tests/test_email_format_cost.js && node tests/test_format_single_error.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_deferred_def_scope.js && node tests/test_node_scoped_ctx_flags.js && node tests/test_fast_slot_exhaustion.js && node tests/test_ajv_errors.js && node tests/test_custom_keywords.js && node tests/test_strict_schema.js && node tests/test_aot_external_checks.js && node tests/test_ajv_parity.js && node tests/test_user_format_error_path.js && node tests/test_unevaluated_error_path.js && node tests/test_unevaluated_error_shape.js && node tests/test_pattern_properties_errors.js && node tests/test_additional_properties_scaling.js && node tests/test_additional_props_combined_cost.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_error_order_ordinal.js && node tests/test_rejection_shape.js && node tests/test_lazy_normalization.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_single_pass_errors.js && node tests/test_lazy_json_errors.js && node tests/test_lazy_instance.js && node tests/test_cyclic_input.js && node tests/test_native_error_codes.js && node tests/test_verdict_preprocess.js && node tests/test_defaults_own_keys.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_malformed_json_termination.js && node tests/test_data_positions.js && node tests/test_targeted_positions.js && node tests/test_position_map_cost.js && node tests/test_targeted_positions_cost.js && node tests/test_aot_staleness.js && node tests/test_file_positions.js && node tests/test_aot_positions_cost.js && node tests/test_schema_hash.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",
|
|
55
55
|
"bench:size": "node benchmark/bench_aot_size.mjs",
|
|
56
56
|
"test:suite": "node tests/run_suite.js && node tests/run_suite.js draft7 && node tests/run_suite.js v1",
|
|
57
57
|
"test:compat": "node tests/test_compat.js",
|
|
@@ -121,13 +121,13 @@
|
|
|
121
121
|
"LICENSE"
|
|
122
122
|
],
|
|
123
123
|
"optionalDependencies": {
|
|
124
|
-
"@ata-validator/native-darwin-arm64": "1.31.
|
|
125
|
-
"@ata-validator/native-darwin-x64": "1.31.
|
|
126
|
-
"@ata-validator/native-linux-arm64-gnu": "1.31.
|
|
127
|
-
"@ata-validator/native-linux-arm64-musl": "1.31.
|
|
128
|
-
"@ata-validator/native-linux-x64-gnu": "1.31.
|
|
129
|
-
"@ata-validator/native-linux-x64-musl": "1.31.
|
|
130
|
-
"@ata-validator/native-win32-x64": "1.31.
|
|
124
|
+
"@ata-validator/native-darwin-arm64": "1.31.3",
|
|
125
|
+
"@ata-validator/native-darwin-x64": "1.31.3",
|
|
126
|
+
"@ata-validator/native-linux-arm64-gnu": "1.31.3",
|
|
127
|
+
"@ata-validator/native-linux-arm64-musl": "1.31.3",
|
|
128
|
+
"@ata-validator/native-linux-x64-gnu": "1.31.3",
|
|
129
|
+
"@ata-validator/native-linux-x64-musl": "1.31.3",
|
|
130
|
+
"@ata-validator/native-win32-x64": "1.31.3"
|
|
131
131
|
},
|
|
132
132
|
"peerDependencies": {
|
|
133
133
|
"yaml": "^2.0.0"
|