ata-validator 1.23.0 → 1.25.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +29 -0
- package/bin/ata.js +34 -2
- package/build.d.ts +42 -5
- package/compat.d.ts +8 -0
- package/compat.js +20 -0
- package/index.d.ts +12 -1
- package/index.js +96 -0
- package/lib/aot-build.js +37 -2
- package/lib/aot-impl.js +158 -40
- package/lib/data-positions.js +6 -3
- package/lib/js-compiler.js +203 -8
- package/lib/scan-compiler.js +1007 -0
- package/lib/scan-runtime.js +158 -0
- package/lib/schema-hash.js +39 -0
- package/lib/strict-check.js +53 -0
- package/lib/version.js +1 -1
- package/package.json +9 -9
package/lib/aot-impl.js
CHANGED
|
@@ -13,7 +13,9 @@
|
|
|
13
13
|
// bundleCompact(Validator, schemas, opts)
|
|
14
14
|
// loadBundle(Validator, mods, schemas, opts)
|
|
15
15
|
|
|
16
|
-
const { compileToJSCodegenWithErrors } = require('./js-compiler');
|
|
16
|
+
const { compileToJSCodegenWithErrors, compileToJSCodegen, unevalContributions } = require('./js-compiler');
|
|
17
|
+
const { buildDataPositionMap } = require('./data-positions');
|
|
18
|
+
const { schemaHash } = require('./schema-hash');
|
|
17
19
|
const SAFE_REGEX_SOURCE = require('./safe-regex-source');
|
|
18
20
|
|
|
19
21
|
// Embedded verbatim in standalone modules so the output file has no runtime
|
|
@@ -54,6 +56,35 @@ function safeRePrelude(...fns) {
|
|
|
54
56
|
return fns.some((f) => f && f._usesSafeRe) ? getSafeRegexEmbed() + '\n' : '';
|
|
55
57
|
}
|
|
56
58
|
|
|
59
|
+
// Serialize the closure variables a compiled function referenced (regexes,
|
|
60
|
+
// sets, lookup tables, preprocessor functions) into declarations for load
|
|
61
|
+
// scope. Regex lines are no longer inlined into _source, so every emitter
|
|
62
|
+
// must declare them exactly once outside the emitted function; inlining them
|
|
63
|
+
// in the body recompiled the pattern on every call.
|
|
64
|
+
function closureDeclLines(jsFn, declKW) {
|
|
65
|
+
const kw = declKW || 'const';
|
|
66
|
+
const lines = [];
|
|
67
|
+
if (!jsFn || !jsFn._closures || jsFn._closures.length === 0) return lines;
|
|
68
|
+
for (const { name, val } of jsFn._closures) {
|
|
69
|
+
if (Array.isArray(val)) { lines.push(`${kw} ${name} = ${JSON.stringify(val)};`); continue; }
|
|
70
|
+
if (val && val.__ataSafe) {
|
|
71
|
+
lines.push(`${kw} ${name} = __ataSafeRe(${JSON.stringify(val.source)});`);
|
|
72
|
+
} else if (val instanceof RegExp) {
|
|
73
|
+
const flags = val.flags;
|
|
74
|
+
lines.push(`${kw} ${name} = new RegExp(${JSON.stringify(val.source)}${flags ? ', ' + JSON.stringify(flags) : ''});`);
|
|
75
|
+
} else if (val instanceof Set) {
|
|
76
|
+
lines.push(`${kw} ${name} = new Set(${JSON.stringify([...val])});`);
|
|
77
|
+
} else if (typeof val === 'function') {
|
|
78
|
+
// new Function('_ppv', body) — extract body from toString()
|
|
79
|
+
const str = val.toString();
|
|
80
|
+
const m = str.match(/^function[^(]*\([^)]*\)\s*\{([\s\S]*)\}$/);
|
|
81
|
+
const body = m ? m[1].trim() : str;
|
|
82
|
+
lines.push(`${kw} ${name} = function(_ppv) { ${body} };`);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return lines;
|
|
86
|
+
}
|
|
87
|
+
|
|
57
88
|
// A validator can enforce more than its schema says. `withKeywords` from
|
|
58
89
|
// @ata-project/keywords wraps an instance's entry points, and a wrapper
|
|
59
90
|
// declares that by setting `_externalChecks`. The emitters below build their
|
|
@@ -87,11 +118,12 @@ function toStandalone(validator) {
|
|
|
87
118
|
);
|
|
88
119
|
const errSrc = jsErrFn && jsErrFn._errSource ? jsErrFn._errSource : '';
|
|
89
120
|
|
|
121
|
+
const closureSrc = closureDeclLines(jsFn).join('\n');
|
|
90
122
|
return `// Auto-generated by ata-validator — do not edit
|
|
91
123
|
'use strict';
|
|
92
124
|
${_CP_LEN_SOURCE}
|
|
93
125
|
${safeRePrelude(jsFn, jsErrFn)}${preambleSrc}
|
|
94
|
-
const boolFn = function(d) {
|
|
126
|
+
${closureSrc ? closureSrc + '\n' : ''}const boolFn = function(d) {
|
|
95
127
|
${src}
|
|
96
128
|
};
|
|
97
129
|
const hybridFactory = function(R, E) {
|
|
@@ -171,17 +203,40 @@ function emitClone(node, access, depth) {
|
|
|
171
203
|
if (!node || typeof node !== 'object') return null;
|
|
172
204
|
if (depth > 12) return null;
|
|
173
205
|
// Only plain object nodes are cloneable, and only when the schema names
|
|
174
|
-
// every property it keeps. A
|
|
175
|
-
//
|
|
176
|
-
if
|
|
177
|
-
|
|
206
|
+
// every property it keeps. A $ref means the set of allowed keys is not
|
|
207
|
+
// this node's to decide. In-place applicators (allOf, anyOf, oneOf,
|
|
208
|
+
// if/then/else) are admitted under the same proof the unevaluated* error
|
|
209
|
+
// generators use: every property name they contribute must already be
|
|
210
|
+
// declared in this node's own `properties`, so they constrain values but
|
|
211
|
+
// never widen the key set. `not` never widens it. `unevaluatedProperties`
|
|
212
|
+
// is admitted only as `false` under that proof, where it is exactly
|
|
213
|
+
// `additionalProperties: false`.
|
|
214
|
+
if (node.$ref || node.patternProperties ||
|
|
178
215
|
node.additionalProperties === true ||
|
|
179
216
|
(node.additionalProperties && typeof node.additionalProperties === 'object')) {
|
|
180
217
|
return null;
|
|
181
218
|
}
|
|
219
|
+
if (node.unevaluatedProperties !== undefined && node.unevaluatedProperties !== false) return null;
|
|
182
220
|
if (node.type !== 'object' || !node.properties) return null;
|
|
183
221
|
const keys = Object.keys(node.properties);
|
|
184
222
|
if (keys.length === 0) return null;
|
|
223
|
+
if (node.allOf || node.anyOf || node.oneOf || node.if || node.then || node.else ||
|
|
224
|
+
node.unevaluatedProperties === false) {
|
|
225
|
+
if (node.$dynamicRef !== undefined || node.$recursiveRef !== undefined ||
|
|
226
|
+
node.dependentSchemas !== undefined || node.dependencies !== undefined) return null;
|
|
227
|
+
const names = new Set();
|
|
228
|
+
const state = { prefix: 0 };
|
|
229
|
+
for (const k of ['allOf', 'anyOf', 'oneOf']) {
|
|
230
|
+
if (node[k] !== undefined) {
|
|
231
|
+
if (!Array.isArray(node[k])) return null;
|
|
232
|
+
for (const b of node[k]) if (!unevalContributions(b, 'props', names, state, 0)) return null;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
for (const k of ['if', 'then', 'else']) {
|
|
236
|
+
if (node[k] !== undefined && !unevalContributions(node[k], 'props', names, state, 0)) return null;
|
|
237
|
+
}
|
|
238
|
+
for (const n of names) if (!Object.prototype.hasOwnProperty.call(node.properties, n)) return null;
|
|
239
|
+
}
|
|
185
240
|
const required = new Set(Array.isArray(node.required) ? node.required : []);
|
|
186
241
|
|
|
187
242
|
// A property is copyable only when the emitter can name everything that
|
|
@@ -196,7 +251,22 @@ function emitClone(node, access, depth) {
|
|
|
196
251
|
for (const key of keys) {
|
|
197
252
|
const prop = node.properties[key];
|
|
198
253
|
if (!prop || typeof prop !== 'object') return null;
|
|
199
|
-
if (prop.$ref
|
|
254
|
+
if (prop.$ref) return null;
|
|
255
|
+
// A property that is required and carries a default is the one place the
|
|
256
|
+
// runtime and a raw-input validation disagree: the runtime fills the
|
|
257
|
+
// default before checking `required`, so `{}` validates there and would
|
|
258
|
+
// throw here. Declining keeps parse() exactly as strict as validate().
|
|
259
|
+
if (required.has(key) && prop.default !== undefined) return null;
|
|
260
|
+
// A default the property's own schema rejects would make parse() hand
|
|
261
|
+
// back a document validate() refuses: the runtime fills defaults before
|
|
262
|
+
// validating, so it catches the bad default, and a raw-input check here
|
|
263
|
+
// never sees it. Probe the default at emit time and decline when it does
|
|
264
|
+
// not hold, or when the subschema cannot be compiled to check it.
|
|
265
|
+
if (prop.default !== undefined) {
|
|
266
|
+
let probe = null;
|
|
267
|
+
try { probe = compileToJSCodegen(prop, null, null); } catch (_) { probe = null; }
|
|
268
|
+
if (!probe || !probe(prop.default)) return null;
|
|
269
|
+
}
|
|
200
270
|
const read = `${access}[${JSON.stringify(key)}]`;
|
|
201
271
|
let value;
|
|
202
272
|
const types = Array.isArray(prop.type) ? prop.type : [prop.type];
|
|
@@ -230,7 +300,7 @@ function emitClone(node, access, depth) {
|
|
|
230
300
|
return null;
|
|
231
301
|
}
|
|
232
302
|
if (required.has(key)) fixed.push(`${JSON.stringify(key)}: ${value}`);
|
|
233
|
-
else conditional.push({ key, value });
|
|
303
|
+
else conditional.push({ key, value, dflt: prop.default });
|
|
234
304
|
}
|
|
235
305
|
if (fixed.length === 0 && conditional.length === 0) return null;
|
|
236
306
|
|
|
@@ -240,7 +310,12 @@ function emitClone(node, access, depth) {
|
|
|
240
310
|
// gains a key the input did not have.
|
|
241
311
|
const tmp = `_c${depth}`;
|
|
242
312
|
const adds = conditional
|
|
243
|
-
.map(({ key, value }) =>
|
|
313
|
+
.map(({ key, value, dflt }) => {
|
|
314
|
+
const set = `if (${JSON.stringify(key)} in ${access}) ${tmp}[${JSON.stringify(key)}] = ${value};`;
|
|
315
|
+
// The default is emitted as a literal inside the call, so an object or
|
|
316
|
+
// array default is a fresh value on every parse, never shared state.
|
|
317
|
+
return dflt === undefined ? set : `${set} else ${tmp}[${JSON.stringify(key)}] = ${JSON.stringify(dflt)};`;
|
|
318
|
+
})
|
|
244
319
|
.join(' ');
|
|
245
320
|
return `(function(){ const ${tmp} = ${literal}; ${adds} return ${tmp}; })()`;
|
|
246
321
|
}
|
|
@@ -269,6 +344,16 @@ function toStandaloneModule(validator, opts) {
|
|
|
269
344
|
const errSrc = jsErrFn && jsErrFn._errSource ? jsErrFn._errSource : '';
|
|
270
345
|
if (errSrc) {
|
|
271
346
|
errCore = `const errFn = function(d, _all) {\n ${errSrc}\n};\n`;
|
|
347
|
+
} else if (opts && typeof opts.onWarning === 'function') {
|
|
348
|
+
// The caller asked for error detail and is not getting it: the error
|
|
349
|
+
// generator declined this schema (unevaluated* and a few other shapes),
|
|
350
|
+
// and a standalone module has no interpreted engine to fall back to the
|
|
351
|
+
// way the runtime validator does. The module still ships, with the
|
|
352
|
+
// verdict exact, but every failure reports the single ATA9000 stub.
|
|
353
|
+
// Silence here cost a user a debugging session; hence the channel.
|
|
354
|
+
opts.onWarning(
|
|
355
|
+
'error detail could not be generated for this schema; the module reports failures as the single ATA9000 abort-early error. The verdict is unaffected. For detailed errors, validate failing documents with the runtime Validator.'
|
|
356
|
+
);
|
|
272
357
|
}
|
|
273
358
|
}
|
|
274
359
|
|
|
@@ -284,29 +369,8 @@ function toStandaloneModule(validator, opts) {
|
|
|
284
369
|
|
|
285
370
|
// Serialize closure vars referenced in _fn body: regex, sub-validators, sets.
|
|
286
371
|
let closureDecls = '';
|
|
287
|
-
|
|
288
|
-
const lines =
|
|
289
|
-
for (const { name, val } of jsFn._closures) {
|
|
290
|
-
if (Array.isArray(val)) {
|
|
291
|
-
lines.push(`const ${name} = ${JSON.stringify(val)};`);
|
|
292
|
-
continue;
|
|
293
|
-
}
|
|
294
|
-
if (val && val.__ataSafe) {
|
|
295
|
-
lines.push(`const ${name} = __ataSafeRe(${JSON.stringify(val.source)});`);
|
|
296
|
-
} else if (val instanceof RegExp) {
|
|
297
|
-
const flags = val.flags;
|
|
298
|
-
lines.push(`const ${name} = new RegExp(${JSON.stringify(val.source)}${flags ? ', ' + JSON.stringify(flags) : ''});`);
|
|
299
|
-
} else if (val instanceof Set) {
|
|
300
|
-
lines.push(`const ${name} = new Set(${JSON.stringify([...val])});`);
|
|
301
|
-
} else if (typeof val === 'function') {
|
|
302
|
-
// new Function('_ppv', body) — extract body from toString()
|
|
303
|
-
const str = val.toString();
|
|
304
|
-
// Matches: "function anonymous(_ppv\n) {\nbody\n}" or "function(_ppv){body}"
|
|
305
|
-
const m = str.match(/^function[^(]*\([^)]*\)\s*\{([\s\S]*)\}$/)
|
|
306
|
-
const body = m ? m[1].trim() : str;
|
|
307
|
-
lines.push(`const ${name} = function(_ppv) { ${body} };`);
|
|
308
|
-
}
|
|
309
|
-
}
|
|
372
|
+
{
|
|
373
|
+
const lines = closureDeclLines(jsFn);
|
|
310
374
|
if (lines.length) closureDecls = lines.join('\n') + '\n';
|
|
311
375
|
}
|
|
312
376
|
|
|
@@ -371,17 +435,60 @@ function toStandaloneModule(validator, opts) {
|
|
|
371
435
|
? `function _ataParse(data) {\n if (!_fn(data)) { const e = new Error('validation failed'); e.name = 'AtaValidationError'; throw e; }\n return ${cloneExpr};\n}\n`
|
|
372
436
|
: '';
|
|
373
437
|
|
|
438
|
+
// validateJSON(): parse the text, validate, and on failure attach a
|
|
439
|
+
// dataFrame (byte offset, line, col, source line) to every error by
|
|
440
|
+
// walking the original text once. Off by default for the same reason as
|
|
441
|
+
// parse(): it costs bytes in every emitted module. Ask for it with
|
|
442
|
+
// { positions: true }. The walker is the runtime's buildDataPositionMap,
|
|
443
|
+
// embedded verbatim via toString so the two cannot drift; that is why the
|
|
444
|
+
// function in lib/data-positions.js must stay self-contained.
|
|
445
|
+
const positionsCore = !(opts && opts.positions) ? '' : `const _ataPosMap = ${buildDataPositionMap.toString()};
|
|
446
|
+
function _ataFrame(p) { return { byteOffset: p.byteOffset, length: p.length, line: p.line, col: p.col, text: p.text }; }
|
|
447
|
+
function validateJSON(text) {
|
|
448
|
+
const s = String(text);
|
|
449
|
+
let data;
|
|
450
|
+
try { data = JSON.parse(s); } catch (e) {
|
|
451
|
+
const nl = s.indexOf(String.fromCharCode(10));
|
|
452
|
+
let frame = { byteOffset: 0, length: s.length, line: 1, col: 1, text: nl === -1 ? s : s.slice(0, nl) };
|
|
453
|
+
try { const m = _ataPosMap(s); if (m['']) frame = _ataFrame(m['']); } catch (_) {}
|
|
454
|
+
return { valid: false, errors: [{ code: 'ATA9001', message: 'invalid JSON document', keyword: '__parse__', path: '', instancePath: '', dataFrame: frame }] };
|
|
455
|
+
}
|
|
456
|
+
const r = validate(data);
|
|
457
|
+
if (r.valid || !r.errors || !r.errors.length) return r;
|
|
458
|
+
let map;
|
|
459
|
+
try { map = _ataPosMap(s); } catch (_) { return r; }
|
|
460
|
+
const errors = r.errors.map((e) => {
|
|
461
|
+
const ptr = e.instancePath != null ? e.instancePath : (e.path || '');
|
|
462
|
+
const p = map[ptr];
|
|
463
|
+
return p ? Object.assign({}, e, { dataFrame: _ataFrame(p) }) : e;
|
|
464
|
+
});
|
|
465
|
+
return { valid: false, errors };
|
|
466
|
+
}
|
|
467
|
+
`;
|
|
468
|
+
|
|
374
469
|
const baseNames = cloneExpr ? 'validate, isValid' : 'validate, isValid';
|
|
375
|
-
|
|
470
|
+
// The content hash of the schema this module was compiled from, over the
|
|
471
|
+
// schema as the caller wrote it (before dialect normalization), so a build
|
|
472
|
+
// can compare it against schemaHash(currentSchema) and know the module is
|
|
473
|
+
// stale without embedding its own fingerprint.
|
|
474
|
+
const hashSrc = schemaHash(validator._rawSchema !== undefined ? validator._rawSchema : validator._schemaObj);
|
|
475
|
+
const hashDecl = `const schemaHash = ${JSON.stringify(hashSrc)};\n`;
|
|
476
|
+
|
|
477
|
+
let names = fmt.exportsSetFormats ? baseNames + ', setFormats' : baseNames;
|
|
478
|
+
names += ', schemaHash';
|
|
479
|
+
if (positionsCore) names += ', validateJSON';
|
|
376
480
|
const parseAlias = cloneExpr ? ', _ataParse as parse' : '';
|
|
377
481
|
const parseProp = cloneExpr ? ', parse: _ataParse' : '';
|
|
378
482
|
const exports = format === 'esm'
|
|
379
483
|
? `export { ${names}${parseAlias} };\nexport default { ${names}${parseProp} };\n`
|
|
380
484
|
: `module.exports = { ${names}${parseProp} };\nmodule.exports.default = module.exports;\n`;
|
|
381
485
|
|
|
486
|
+
const degradedNote = (!abortEarly && !errCore)
|
|
487
|
+
? '// NOTE: error detail was requested but could not be generated for this\n// schema; failures report the single ATA9000 abort-early error. The verdict\n// is exact. Validate failing documents with the runtime Validator for detail.\n'
|
|
488
|
+
: '';
|
|
382
489
|
return `// Auto-generated by ata-validator — do not edit.
|
|
383
490
|
// Schema is embedded; runtime has zero dependency on ata-validator.
|
|
384
|
-
'use strict';
|
|
491
|
+
${degradedNote}'use strict';
|
|
385
492
|
${_CP_LEN_SOURCE}
|
|
386
493
|
${safeRePrelude(jsFn, jsErrFn)}${schemaSourceConst}const VALID = Object.freeze({ valid: true, errors: Object.freeze([]) });
|
|
387
494
|
const ABORT = Object.freeze({
|
|
@@ -398,7 +505,7 @@ ${closureDecls}${sharedBlock}${preambleDecls}${formatDecls}const _fn = function(
|
|
|
398
505
|
};
|
|
399
506
|
${errCore}function isValid(data) { return _fn(data); }
|
|
400
507
|
function validate(data) { ${validBody}; }
|
|
401
|
-
${parseCore}${exports}`;
|
|
508
|
+
${parseCore}${positionsCore}${hashDecl}${exports}`;
|
|
402
509
|
}
|
|
403
510
|
|
|
404
511
|
// Bundle multiple validators into a single JS file for fast startup.
|
|
@@ -504,6 +611,8 @@ function bundleStandalone(Validator, schemas, opts) {
|
|
|
504
611
|
? (jsFn._preambleGuard || '') + (kept.length ? kept.join('\n ') + '\n ' : '')
|
|
505
612
|
: (jsFn._preambleSource || '');
|
|
506
613
|
if (preambleSrc) preamble = preamble ? `${preamble}\n${preambleSrc}` : preambleSrc;
|
|
614
|
+
const closureSrc = closureDeclLines(jsFn, 'var').join('\n');
|
|
615
|
+
if (closureSrc) preamble = preamble ? `${preamble}\n${closureSrc}\n` : closureSrc + '\n';
|
|
507
616
|
if (opts && opts.verbose) {
|
|
508
617
|
// Embed the schema and a small resolver so errors carry parentSchema.
|
|
509
618
|
const schemaLit = JSON.stringify(v._schemaObj);
|
|
@@ -560,6 +669,7 @@ function bundleCompact(Validator, schemas, opts) {
|
|
|
560
669
|
guard: jsFn._preambleGuard || '',
|
|
561
670
|
parts: jsFn._preambleParts || null,
|
|
562
671
|
preamble: jsFn._preambleSource || '',
|
|
672
|
+
closures: closureDeclLines(jsFn, 'var').join('\n'),
|
|
563
673
|
hybridBody: jsFn._hybridSource,
|
|
564
674
|
err: jsErrFn && jsErrFn._errSource ? jsErrFn._errSource : null,
|
|
565
675
|
fmt: jsFn._formatClosures || null,
|
|
@@ -589,7 +699,11 @@ function bundleCompact(Validator, schemas, opts) {
|
|
|
589
699
|
const preamble = kept !== null
|
|
590
700
|
? e.guard + (kept.length ? kept.join('\n ') + '\n ' : '')
|
|
591
701
|
: e.preamble;
|
|
592
|
-
|
|
702
|
+
// Factory-scope code: guard state, hoisted branch helpers, and the
|
|
703
|
+
// closure declarations (regexes above all), matching where the
|
|
704
|
+
// in-process compiler places them. Keeping them per call recompiled
|
|
705
|
+
// every pattern on every validation.
|
|
706
|
+
e.factory = (preamble ? preamble + '\n' : '') + (e.closures ? e.closures + '\n' : '');
|
|
593
707
|
if (e.err) for (const h of sharedDecls) e.err = e.err.split(h).join('');
|
|
594
708
|
}
|
|
595
709
|
|
|
@@ -601,11 +715,15 @@ function bundleCompact(Validator, schemas, opts) {
|
|
|
601
715
|
|
|
602
716
|
const indices = entries.map((e) => {
|
|
603
717
|
if (!e) return [-1, -1];
|
|
604
|
-
|
|
718
|
+
// The dedupe key must include the factory code: two schemas can emit an
|
|
719
|
+
// identical body that references _re1 with different patterns, and the
|
|
720
|
+
// pattern now lives only in the factory-scope declaration.
|
|
721
|
+
const key = e.factory + '\u0000' + e.hybridBody;
|
|
722
|
+
let hi = bodyMap.get(key);
|
|
605
723
|
if (hi === undefined) {
|
|
606
724
|
hi = bodies.length;
|
|
607
|
-
bodies.push(e.
|
|
608
|
-
bodyMap.set(
|
|
725
|
+
bodies.push({ factory: e.factory, body: e.hybridBody });
|
|
726
|
+
bodyMap.set(key, hi);
|
|
609
727
|
}
|
|
610
728
|
let ei = -1;
|
|
611
729
|
if (e.err) {
|
|
@@ -648,7 +766,7 @@ function bundleCompact(Validator, schemas, opts) {
|
|
|
648
766
|
// Shared hybrid factories
|
|
649
767
|
out += `${declKW} H=[\n`;
|
|
650
768
|
out += bodies
|
|
651
|
-
.map((b) => `function(R,E){return function(d){${b}}}`)
|
|
769
|
+
.map((b) => `function(R,E){${b.factory}return function(d){${b.body}}}`)
|
|
652
770
|
.join(',\n');
|
|
653
771
|
out += '\n];\n';
|
|
654
772
|
|
package/lib/data-positions.js
CHANGED
|
@@ -6,10 +6,13 @@
|
|
|
6
6
|
* AND abortEarly is off. Zero cost on the valid path.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
// Self-contained on purpose: `toStandaloneModule({ positions: true })` embeds
|
|
10
|
+
// this function verbatim via Function.prototype.toString, so it may not close
|
|
11
|
+
// over anything in this module and may not assume Node globals. The inlined
|
|
12
|
+
// escapePtr below duplicates lib/source-positions.js for that reason.
|
|
11
13
|
function buildDataPositionMap (input) {
|
|
12
|
-
const
|
|
14
|
+
const escapePtr = (s) => s.replace(/~/g, '~0').replace(/\//g, '~1');
|
|
15
|
+
const text = (typeof Buffer !== 'undefined' && Buffer.isBuffer(input)) ? input.toString('utf8') : String(input);
|
|
13
16
|
const map = Object.create(null);
|
|
14
17
|
const lines = text.split('\n');
|
|
15
18
|
const lineStart = new Array(lines.length + 1);
|
package/lib/js-compiler.js
CHANGED
|
@@ -1610,8 +1610,14 @@ function compileToJSCodegen(schema, schemaMap, userFormats) {
|
|
|
1610
1610
|
boolFn._hybridFactory = (R, E) => hybridFactory(...closureValues, R, E)
|
|
1611
1611
|
} catch {}
|
|
1612
1612
|
|
|
1613
|
-
// Store source for standalone compilation
|
|
1614
|
-
|
|
1613
|
+
// Store source for standalone compilation. Regex declarations are NOT
|
|
1614
|
+
// inlined into the body: they are carried by _closures and the emitters
|
|
1615
|
+
// declare them once at load scope. Inlining them here put a
|
|
1616
|
+
// `const _reN=__ataSafeRe(...)` inside the emitted function, which
|
|
1617
|
+
// shadowed the module-scope binding and recompiled the pattern on every
|
|
1618
|
+
// call, 25x on a schema with one pattern.
|
|
1619
|
+
const emitHelpers = ctx.helperCode.filter((c) => !/^const _re\d+=(?:__ataSafeRe|new RegExp)\(/.test(c))
|
|
1620
|
+
const helperStr = emitHelpers.length ? emitHelpers.join('\n ') + '\n ' : ''
|
|
1615
1621
|
boolFn._source = helperStr + body
|
|
1616
1622
|
boolFn._preambleSource = preambleStr
|
|
1617
1623
|
boolFn._preambleGuard = guardStr
|
|
@@ -3219,6 +3225,110 @@ function childPathDynExpr(parentExpr, indexExpr) {
|
|
|
3219
3225
|
return `${parentExpr}+'/'+${indexExpr}`
|
|
3220
3226
|
}
|
|
3221
3227
|
|
|
3228
|
+
// unevaluated* differs from additionalProperties/items only through
|
|
3229
|
+
// annotations contributed by in-place applicators at the same schema object.
|
|
3230
|
+
// When every applicator subschema is provably free of the annotating keywords
|
|
3231
|
+
// for that kind, the keyword is exactly its plain counterpart, and the error
|
|
3232
|
+
// generators can emit real detail for it instead of declining. `required`
|
|
3233
|
+
// inside a then-branch annotates nothing, which is precisely the shape a
|
|
3234
|
+
// config schema has; a then-branch carrying `properties` does, and stays
|
|
3235
|
+
// declined. `not` never keeps its annotations, so it is skipped. A reference
|
|
3236
|
+
// or dependency keyword makes the node ineligible outright: resolving what
|
|
3237
|
+
// they might contribute is not this function's job.
|
|
3238
|
+
// Collects what an in-place applicator subschema can contribute: property
|
|
3239
|
+
// names into `names`, and the deepest tuple prefix it may evaluate into
|
|
3240
|
+
// `state.prefix`. Returns false when the contribution cannot be bounded
|
|
3241
|
+
// (patternProperties, additionalProperties, a reference, item schemas, and so
|
|
3242
|
+
// on), which makes the node ineligible outright.
|
|
3243
|
+
function unevalContributions (sub, kind, names, state, depth) {
|
|
3244
|
+
if (depth > 24) return false
|
|
3245
|
+
if (sub === true || sub === false) return true
|
|
3246
|
+
if (typeof sub !== 'object' || sub === null) return false
|
|
3247
|
+
if (kind === 'props') {
|
|
3248
|
+
if (sub.patternProperties !== undefined || sub.additionalProperties !== undefined || sub.unevaluatedProperties !== undefined) return false
|
|
3249
|
+
if (sub.properties !== undefined) {
|
|
3250
|
+
if (typeof sub.properties !== 'object' || sub.properties === null) return false
|
|
3251
|
+
for (const k of Object.keys(sub.properties)) names.add(k)
|
|
3252
|
+
}
|
|
3253
|
+
} else {
|
|
3254
|
+
if (sub.items !== undefined || sub.additionalItems !== undefined || sub.contains !== undefined || sub.unevaluatedItems !== undefined) return false
|
|
3255
|
+
if (sub.prefixItems !== undefined) {
|
|
3256
|
+
if (!Array.isArray(sub.prefixItems)) return false
|
|
3257
|
+
if (sub.prefixItems.length > state.prefix) state.prefix = sub.prefixItems.length
|
|
3258
|
+
}
|
|
3259
|
+
}
|
|
3260
|
+
if (sub.$ref !== undefined || sub.$dynamicRef !== undefined || sub.$recursiveRef !== undefined) return false
|
|
3261
|
+
if (sub.dependentSchemas !== undefined || sub.dependencies !== undefined) return false
|
|
3262
|
+
for (const k of ['allOf', 'anyOf', 'oneOf']) {
|
|
3263
|
+
if (sub[k] !== undefined) {
|
|
3264
|
+
if (!Array.isArray(sub[k])) return false
|
|
3265
|
+
for (const b of sub[k]) if (!unevalContributions(b, kind, names, state, depth + 1)) return false
|
|
3266
|
+
}
|
|
3267
|
+
}
|
|
3268
|
+
for (const k of ['if', 'then', 'else']) {
|
|
3269
|
+
if (sub[k] !== undefined && !unevalContributions(sub[k], kind, names, state, depth + 1)) return false
|
|
3270
|
+
}
|
|
3271
|
+
return true
|
|
3272
|
+
}
|
|
3273
|
+
|
|
3274
|
+
// A contribution that names only what the node's own adjacent keywords
|
|
3275
|
+
// already evaluate unconditionally changes nothing: a key in the node's own
|
|
3276
|
+
// `properties` is evaluated whether or not the branch that also names it
|
|
3277
|
+
// passes, and a branch prefix no longer than the node's own is covered
|
|
3278
|
+
// position for position. Anything beyond those bounds is conditional
|
|
3279
|
+
// evaluation the plain counterpart cannot express, and the node stays with
|
|
3280
|
+
// the interpreted engine.
|
|
3281
|
+
function unevalLocalOk (node, key) {
|
|
3282
|
+
if (node[key] !== false) return false
|
|
3283
|
+
if (node.$ref !== undefined || node.$dynamicRef !== undefined || node.$recursiveRef !== undefined) return false
|
|
3284
|
+
if (node.dependentSchemas !== undefined || node.dependencies !== undefined) return false
|
|
3285
|
+
const kind = key === 'unevaluatedProperties' ? 'props' : 'items'
|
|
3286
|
+
if (kind === 'props' && node.patternProperties !== undefined) return false
|
|
3287
|
+
if (kind === 'items' && node.contains !== undefined) return false
|
|
3288
|
+
const names = new Set()
|
|
3289
|
+
const state = { prefix: 0 }
|
|
3290
|
+
for (const k of ['allOf', 'anyOf', 'oneOf']) {
|
|
3291
|
+
if (node[k] !== undefined) {
|
|
3292
|
+
if (!Array.isArray(node[k])) return false
|
|
3293
|
+
for (const b of node[k]) if (!unevalContributions(b, kind, names, state, 0)) return false
|
|
3294
|
+
}
|
|
3295
|
+
}
|
|
3296
|
+
for (const k of ['if', 'then', 'else']) {
|
|
3297
|
+
if (node[k] !== undefined && !unevalContributions(node[k], kind, names, state, 0)) return false
|
|
3298
|
+
}
|
|
3299
|
+
if (kind === 'props') {
|
|
3300
|
+
const own = node.properties && typeof node.properties === 'object' ? node.properties : {}
|
|
3301
|
+
for (const n of names) if (!Object.prototype.hasOwnProperty.call(own, n)) return false
|
|
3302
|
+
} else {
|
|
3303
|
+
const ownPrefix = Array.isArray(node.prefixItems) ? node.prefixItems.length
|
|
3304
|
+
: (Array.isArray(node.items) ? node.items.length : 0)
|
|
3305
|
+
if (node.items !== undefined && !Array.isArray(node.items)) { /* all evaluated; any prefix is covered */ }
|
|
3306
|
+
else if (state.prefix > ownPrefix) return false
|
|
3307
|
+
}
|
|
3308
|
+
return true
|
|
3309
|
+
}
|
|
3310
|
+
|
|
3311
|
+
// Every occurrence in the document must qualify or the generator declines the
|
|
3312
|
+
// whole schema, exactly as the string scan it replaces did. The walk is
|
|
3313
|
+
// generic and may visit data positions (an enum value, say); judging those is
|
|
3314
|
+
// harmless, since a data node that fails the test only makes the answer more
|
|
3315
|
+
// conservative and the generators never read data positions as schemas.
|
|
3316
|
+
function unevalAllProvablyLocal (root) {
|
|
3317
|
+
const seen = new Set()
|
|
3318
|
+
const stack = [root]
|
|
3319
|
+
while (stack.length) {
|
|
3320
|
+
const n = stack.pop()
|
|
3321
|
+
if (n === null || typeof n !== 'object' || seen.has(n)) continue
|
|
3322
|
+
seen.add(n)
|
|
3323
|
+
if (!Array.isArray(n)) {
|
|
3324
|
+
if (n.unevaluatedProperties !== undefined && !unevalLocalOk(n, 'unevaluatedProperties')) return false
|
|
3325
|
+
if (n.unevaluatedItems !== undefined && !unevalLocalOk(n, 'unevaluatedItems')) return false
|
|
3326
|
+
}
|
|
3327
|
+
for (const k of Array.isArray(n) ? n : Object.values(n)) stack.push(k)
|
|
3328
|
+
}
|
|
3329
|
+
return true
|
|
3330
|
+
}
|
|
3331
|
+
|
|
3222
3332
|
// Detect simple prefix patterns like "^x-", "^_", "^prefix" and generate fast charCodeAt checks
|
|
3223
3333
|
// Returns a JS expression string or null if pattern is too complex
|
|
3224
3334
|
function fastPrefixCheck(pattern, keyVar) {
|
|
@@ -3267,10 +3377,12 @@ function genCharCodeSwitch(keys, v) {
|
|
|
3267
3377
|
// Returns a function: (data, allErrors) => { valid, errors }
|
|
3268
3378
|
// Valid path is still fast — only error path does extra work.
|
|
3269
3379
|
function compileToJSCodegenWithErrors(schema, schemaMap, userFormats, sourceOpts) {
|
|
3270
|
-
//
|
|
3380
|
+
// unevaluated* is generated only where it is provably its plain counterpart
|
|
3381
|
+
// (see unevalLocalOk); any occurrence that is not keeps the whole schema on
|
|
3382
|
+
// the interpreted engine's error path, exactly as before.
|
|
3271
3383
|
if (typeof schema === 'object' && schema !== null) {
|
|
3272
3384
|
const s = JSON.stringify(schema)
|
|
3273
|
-
if (s.includes('unevaluatedProperties') || s.includes('unevaluatedItems')) return null
|
|
3385
|
+
if ((s.includes('unevaluatedProperties') || s.includes('unevaluatedItems')) && !unevalAllProvablyLocal(schema)) return null
|
|
3274
3386
|
}
|
|
3275
3387
|
if (typeof schema === 'boolean') {
|
|
3276
3388
|
return schema
|
|
@@ -3409,7 +3521,11 @@ function compileToJSCodegenWithErrors(schema, schemaMap, userFormats, sourceOpts
|
|
|
3409
3521
|
})
|
|
3410
3522
|
}
|
|
3411
3523
|
return fn
|
|
3412
|
-
} catch {
|
|
3524
|
+
} catch (e) {
|
|
3525
|
+
// Declining here is safe (the interpreter takes over) but silent, which
|
|
3526
|
+
// is how every past regression in this file hid. The env hook makes the
|
|
3527
|
+
// decline visible without changing behavior.
|
|
3528
|
+
if (process.env.ATA_DEBUG_ERRGEN) console.error('error codegen declined:', e.message)
|
|
3413
3529
|
return null
|
|
3414
3530
|
}
|
|
3415
3531
|
}
|
|
@@ -3774,6 +3890,52 @@ function genCodeE(schema, v, pathExpr, lines, ctx, schemaPrefix) {
|
|
|
3774
3890
|
lines.push(isObj ? `{${inner}}` : `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){${inner}}`)
|
|
3775
3891
|
}
|
|
3776
3892
|
|
|
3893
|
+
// unevaluatedProperties: false, on a node where it is provably its plain
|
|
3894
|
+
// counterpart (the bail above guaranteed that for every occurrence). With
|
|
3895
|
+
// additionalProperties present everything is evaluated and there is nothing
|
|
3896
|
+
// to emit. The error keeps the keyword's own identity: the interpreted
|
|
3897
|
+
// engine reports these as unevaluatedProperties with the key named, and the
|
|
3898
|
+
// two engines are pinned to identical output.
|
|
3899
|
+
if (schema.unevaluatedProperties === false && schema.additionalProperties === undefined) {
|
|
3900
|
+
const allowedU = Object.keys(schema.properties || {}).map(k => `${JSON.stringify(k)}`).join(',')
|
|
3901
|
+
const ui = ctx.varCounter++
|
|
3902
|
+
const upSp = `${schemaPrefix}/unevaluatedProperties`
|
|
3903
|
+
const upLit = buildErrorLiteral({ keyword: 'unevaluatedProperties', schemaPath: upSp, sourceMap: ctx.sourceMap })
|
|
3904
|
+
const patChecksU = []
|
|
3905
|
+
for (const pat of Object.keys(schema.patternProperties || {})) {
|
|
3906
|
+
const pattern = JSON.stringify(pat)
|
|
3907
|
+
if (!ctx.regExpMap.has(pattern)) {
|
|
3908
|
+
const ri = ctx.varCounter++
|
|
3909
|
+
ctx.regExpMap.set(pattern, ri)
|
|
3910
|
+
if (patternIsSafe(pat)) {
|
|
3911
|
+
ctx.helperCode.push(`const _re${ri}=__ataSafeRe(${pattern})`)
|
|
3912
|
+
ctx.usesSafeRe = true
|
|
3913
|
+
} else {
|
|
3914
|
+
ctx.helperCode.push(`const _re${ri}=new RegExp(${pattern})`)
|
|
3915
|
+
}
|
|
3916
|
+
}
|
|
3917
|
+
patChecksU.push(`_re${ctx.regExpMap.get(pattern)}.test(_k${ui}[_i])`)
|
|
3918
|
+
}
|
|
3919
|
+
const isUneval = patChecksU.length ? `!_a${ui}.has(_k${ui}[_i])&&!(${patChecksU.join('||')})` : `!_a${ui}.has(_k${ui}[_i])`
|
|
3920
|
+
const innerU = `const _k${ui}=Object.keys(${v});const _a${ui}=new Set([${allowedU}]);for(let _i=0;_i<_k${ui}.length;_i++){if(${isUneval}){_e.push({code:'${upLit.codeStr}',keyword:'unevaluatedProperties',instancePath:${pathExpr||'""'},schemaPath:'${upSp}'${ordinalField(ctx, `${upSp}`)},params:{unevaluatedProperty:_k${ui}[_i]},message:'must NOT have unevaluated properties',docUrl:'${upLit.docUrl}'${upLit.frame}});if(!_all)return{valid:false,errors:_e}}}`
|
|
3921
|
+
lines.push(isObj ? `{${innerU}}` : `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){${innerU}}`)
|
|
3922
|
+
}
|
|
3923
|
+
|
|
3924
|
+
// unevaluatedItems: false under the same rule. The interpreted engine
|
|
3925
|
+
// reports one error for the array with the first unevaluated position as
|
|
3926
|
+
// the limit; with adjacent `items` everything is evaluated. The draft-07
|
|
3927
|
+
// array form of items acts as the prefix.
|
|
3928
|
+
if (schema.unevaluatedItems === false) {
|
|
3929
|
+
const itemsIsPrefix = Array.isArray(schema.items)
|
|
3930
|
+
const allEval = schema.items !== undefined && !itemsIsPrefix
|
|
3931
|
+
if (!allEval) {
|
|
3932
|
+
const plen = itemsIsPrefix ? schema.items.length : (Array.isArray(schema.prefixItems) ? schema.prefixItems.length : 0)
|
|
3933
|
+
const uiSp = `${schemaPrefix}/unevaluatedItems`
|
|
3934
|
+
const uiLit = buildErrorLiteral({ keyword: 'unevaluatedItems', schemaPath: uiSp, sourceMap: ctx.sourceMap })
|
|
3935
|
+
lines.push(`if(Array.isArray(${v})&&${v}.length>${plen}){_e.push({code:'${uiLit.codeStr}',keyword:'unevaluatedItems',instancePath:${pathExpr||'""'},schemaPath:'${uiSp}'${ordinalField(ctx, `${uiSp}`)},params:{limit:${plen}},message:'must NOT have more than ${plen} items',docUrl:'${uiLit.docUrl}'${uiLit.frame}});if(!_all)return{valid:false,errors:_e}}`)
|
|
3936
|
+
}
|
|
3937
|
+
}
|
|
3938
|
+
|
|
3777
3939
|
// dependentRequired
|
|
3778
3940
|
if (schema.dependentRequired) {
|
|
3779
3941
|
const drSp = `${schemaPrefix}/dependentRequired`
|
|
@@ -4004,10 +4166,10 @@ function genCodeE(schema, v, pathExpr, lines, ctx, schemaPrefix) {
|
|
|
4004
4166
|
// Avoids double-pass (jsFn → false → errFn runs same checks again).
|
|
4005
4167
|
// Uses type-aware optimizations: after type check passes, skip guards.
|
|
4006
4168
|
function compileToJSCombined(schema, VALID_RESULT, schemaMap, userFormats) {
|
|
4007
|
-
//
|
|
4169
|
+
// Same provably-local rule as the error generator above.
|
|
4008
4170
|
if (typeof schema === 'object' && schema !== null) {
|
|
4009
4171
|
const s = JSON.stringify(schema)
|
|
4010
|
-
if (s.includes('unevaluatedProperties') || s.includes('unevaluatedItems')) return null
|
|
4172
|
+
if ((s.includes('unevaluatedProperties') || s.includes('unevaluatedItems')) && !unevalAllProvablyLocal(schema)) return null
|
|
4011
4173
|
// Bail on self-referencing schemas — combined codegen doesn't support recursion
|
|
4012
4174
|
if (s.includes('"$ref":"#"')) return null
|
|
4013
4175
|
// Defs on a $ref cycle compile as named functions in the boolean and
|
|
@@ -4403,6 +4565,39 @@ function genCodeC(schema, v, pathExpr, lines, ctx, schemaPrefix) {
|
|
|
4403
4565
|
}
|
|
4404
4566
|
}
|
|
4405
4567
|
|
|
4568
|
+
// unevaluatedProperties: false where it is provably additionalProperties
|
|
4569
|
+
// (the entry bail guaranteed that), keeping the keyword's own identity so
|
|
4570
|
+
// the two engines report the same error.
|
|
4571
|
+
if (schema.unevaluatedProperties === false && schema.additionalProperties === undefined && !schema.patternProperties) {
|
|
4572
|
+
const propKeysU = Object.keys(schema.properties || {})
|
|
4573
|
+
const ui = ctx.varCounter++
|
|
4574
|
+
const failU = fail('unevaluatedProperties', 'unevaluatedProperties', `{unevaluatedProperty:_k${ui}[_i]}`, "'must NOT have unevaluated properties'")
|
|
4575
|
+
let innerU
|
|
4576
|
+
if (propKeysU.length === 0) {
|
|
4577
|
+
innerU = `const _k${ui}=Object.keys(${v});for(let _i=0;_i<_k${ui}.length;_i++){${failU}}`
|
|
4578
|
+
} else if (propKeysU.length <= 8) {
|
|
4579
|
+
const checksU = propKeysU.map(k => `_k${ui}[_i]!==${JSON.stringify(k)}`).join('&&')
|
|
4580
|
+
innerU = `const _k${ui}=Object.keys(${v});for(let _i=0;_i<_k${ui}.length;_i++)if(${checksU}){${failU}}`
|
|
4581
|
+
} else {
|
|
4582
|
+
const allowedU = propKeysU.map(k => JSON.stringify(k)).join(',')
|
|
4583
|
+
innerU = `const _k${ui}=Object.keys(${v});const _a${ui}=new Set([${allowedU}]);for(let _i=0;_i<_k${ui}.length;_i++)if(!_a${ui}.has(_k${ui}[_i])){${failU}}`
|
|
4584
|
+
}
|
|
4585
|
+
lines.push(isObj ? `{${innerU}}` : `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){${innerU}}`)
|
|
4586
|
+
}
|
|
4587
|
+
|
|
4588
|
+
// unevaluatedItems: false under the same rule: one error for the array,
|
|
4589
|
+
// the first uncovered position as the limit. Adjacent schema-form `items`
|
|
4590
|
+
// evaluates everything; the draft-07 array form acts as the prefix.
|
|
4591
|
+
if (schema.unevaluatedItems === false) {
|
|
4592
|
+
const itemsIsPrefixC = Array.isArray(schema.items)
|
|
4593
|
+
const allEvalC = schema.items !== undefined && !itemsIsPrefixC
|
|
4594
|
+
if (!allEvalC) {
|
|
4595
|
+
const plenC = itemsIsPrefixC ? schema.items.length : (Array.isArray(schema.prefixItems) ? schema.prefixItems.length : 0)
|
|
4596
|
+
const failI = fail('unevaluatedItems', 'unevaluatedItems', `{limit:${plenC}}`, `'must NOT have more than ${plenC} items'`)
|
|
4597
|
+
lines.push(`if(Array.isArray(${v})&&${v}.length>${plenC}){${failI}}`)
|
|
4598
|
+
}
|
|
4599
|
+
}
|
|
4600
|
+
|
|
4406
4601
|
// dependentRequired
|
|
4407
4602
|
if (schema.dependentRequired) {
|
|
4408
4603
|
for (const [key, deps] of Object.entries(schema.dependentRequired)) {
|
|
@@ -4807,4 +5002,4 @@ function _collectEval(schema, result, defs, schemaMap, refStack, isRoot) {
|
|
|
4807
5002
|
// not → contributes nothing (spec: annotations from not are discarded)
|
|
4808
5003
|
}
|
|
4809
5004
|
|
|
4810
|
-
module.exports = { compileToJS, compileToJSCodegen, compileToJSCodegenWithErrors, compileToJSCombined, collectEvaluated, AJV_MESSAGES }
|
|
5005
|
+
module.exports = { compileToJS, compileToJSCodegen, compileToJSCodegenWithErrors, compileToJSCombined, collectEvaluated, unevalContributions, AJV_MESSAGES }
|