ata-validator 1.61.3 → 1.62.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +67 -10
- package/bin/ata.js +3 -2
- package/compat.js +15 -2
- package/index.d.ts +7 -0
- package/index.js +10 -0
- package/lib/aot-build.js +27 -1
- package/lib/aot-impl.js +108 -17
- package/lib/buffer-gate.js +25 -14
- package/lib/bun-target.js +21 -5
- package/lib/clone-emit.js +115 -10
- package/lib/codegen-engine.js +94 -18
- package/lib/compiled-common.js +11 -4
- package/lib/compiled-verdict.js +2 -2
- package/lib/compiled.js +1 -1
- package/lib/data-positions.js +18 -0
- package/lib/defaults.js +101 -23
- package/lib/describe-schema.js +38 -5
- package/lib/diagnose.js +51 -29
- package/lib/enrich-error.js +23 -1
- package/lib/error-sort-rank.js +12 -0
- package/lib/formats-source.js +12 -1
- package/lib/formats.js +19 -19
- package/lib/interpreter-core.js +23 -4
- package/lib/js-compiler-errors.js +70 -72
- package/lib/js-compiler.js +212 -72
- package/lib/json-rejections.js +1 -1
- package/lib/migrate.js +24 -5
- package/lib/native-attach.browser.js +7 -0
- package/lib/native-attach.js +76 -0
- package/lib/native-positions.js +7 -0
- package/lib/plan-compiler.js +7 -1
- package/lib/regex-linear.js +88 -0
- package/lib/rejections.js +13 -3
- package/lib/render-compact.js +5 -5
- package/lib/render-pretty.js +23 -10
- package/lib/safe-regex.js +29 -4
- package/lib/scan-compiler.js +16 -4
- package/lib/stream.js +293 -0
- package/lib/tier0.js +6 -2
- package/lib/ts-gen.js +25 -12
- package/lib/unique-items.js +11 -3
- package/lib/validator-core.js +61 -66
- package/lib/verify.js +25 -6
- package/lib/version.js +1 -1
- package/package.json +17 -8
- package/stream.d.ts +51 -0
- package/stream.js +3 -0
- package/stream.mjs +3 -0
package/lib/defaults.js
CHANGED
|
@@ -23,11 +23,21 @@ const MAX_DEPTH = 512;
|
|
|
23
23
|
|
|
24
24
|
// Build the pass for a schema: a function that fills defaults into a value in
|
|
25
25
|
// place (mutates), or null when no default is reachable from the schema.
|
|
26
|
-
|
|
26
|
+
// `keyMatcher` turns a patternProperties pattern into something with a
|
|
27
|
+
// `test` method; without one the platform RegExp is used. The runtime hands
|
|
28
|
+
// in the matcher validation uses (see safeKeyMatcher below), so a key that
|
|
29
|
+
// makes a backtracking pattern run for seconds costs no more here than it
|
|
30
|
+
// does to validate. A module written ahead of time embeds this function as
|
|
31
|
+
// text and calls it without one.
|
|
32
|
+
function buildDefaultsApplier(schema, keyMatcher) {
|
|
27
33
|
if (typeof schema !== "object" || schema === null) return null;
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
34
|
+
// `visits`: per call, the schemas already applied to each object through a
|
|
35
|
+
// `$ref` or an `allOf` member. Applying one twice to the same object fills
|
|
36
|
+
// nothing new, and following every edge of a chain such as
|
|
37
|
+
// n_i = allOf [n_(i-1), n_(i-1)] cost 2^n applications of one schema.
|
|
38
|
+
const ctx = { root: schema, memo: new Map(), reach: new Map(), path: [], res: resourceRoots(schema), match: keyMatcher || keyPattern, visits: null };
|
|
39
|
+
const fn = compileNode(schema, ctx);
|
|
40
|
+
return fn === null ? null : (data) => { ctx.path.length = 0; ctx.visits = null; fn(data, 0, null); };
|
|
31
41
|
}
|
|
32
42
|
|
|
33
43
|
// Write an own property. Plain assignment of a key named `__proto__` does
|
|
@@ -70,13 +80,45 @@ function localRef(root, ref) {
|
|
|
70
80
|
return t !== null && typeof t === "object" ? t : null;
|
|
71
81
|
}
|
|
72
82
|
|
|
83
|
+
// For a schema that embeds other resources (a subschema with its own `$id`),
|
|
84
|
+
// the resource each schema object belongs to: a fragment `$ref` inside one
|
|
85
|
+
// is read against that resource, as validation reads it, not against the
|
|
86
|
+
// outer root. Null when nothing is embedded, which is nearly always. An `$id`
|
|
87
|
+
// that is only a fragment (`#foo`, a draft-07 anchor) starts no resource.
|
|
88
|
+
// Values under `default`, `const`, `enum` and `examples` are data, not
|
|
89
|
+
// schemas, and are not walked.
|
|
90
|
+
function resourceRoots(root) {
|
|
91
|
+
if (root === null || typeof root !== "object") return null;
|
|
92
|
+
const map = new Map();
|
|
93
|
+
let embedded = false;
|
|
94
|
+
const walk = (node, res) => {
|
|
95
|
+
if (node === null || typeof node !== "object" || map.has(node)) return;
|
|
96
|
+
if (Array.isArray(node)) { map.set(node, res); for (let i = 0; i < node.length; i++) walk(node[i], res); return; }
|
|
97
|
+
if (node !== root && typeof node.$id === "string" && node.$id.charCodeAt(0) !== 35) { res = node; embedded = true; }
|
|
98
|
+
map.set(node, res);
|
|
99
|
+
for (const k of Object.keys(node)) {
|
|
100
|
+
if (k === "default" || k === "const" || k === "enum" || k === "examples") continue;
|
|
101
|
+
walk(node[k], res);
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
walk(root, root);
|
|
105
|
+
return embedded ? map : null;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// The schema a `$ref` on `node` names, read against the resource `node`
|
|
109
|
+
// belongs to.
|
|
110
|
+
function refTarget(root, res, node) {
|
|
111
|
+
const base = res !== null ? res.get(node) : undefined;
|
|
112
|
+
return localRef(base !== undefined ? base : root, node.$ref);
|
|
113
|
+
}
|
|
114
|
+
|
|
73
115
|
// Whether a default is reachable from a schema, through the same places the
|
|
74
116
|
// pass goes: 1 yes, 0 no, 2 not known yet because the walk came back to a
|
|
75
117
|
// schema it is still deciding (a recursive reference). Answers are kept per
|
|
76
118
|
// schema object in `memo`, except one that rests on an undecided schema,
|
|
77
119
|
// which is worked out again when asked; the walk used to start over for every
|
|
78
120
|
// schema object it compiled, quadratic in the depth of the schema.
|
|
79
|
-
function reachesDefault(node, root, memo, stack) {
|
|
121
|
+
function reachesDefault(node, root, memo, stack, res) {
|
|
80
122
|
if (node === null || typeof node !== "object") return 0;
|
|
81
123
|
const known = memo.get(node);
|
|
82
124
|
if (known !== undefined) return known;
|
|
@@ -85,9 +127,9 @@ function reachesDefault(node, root, memo, stack) {
|
|
|
85
127
|
const props = valuesOf(node.properties);
|
|
86
128
|
let yes = props.some((p) => p && typeof p === "object" && p.default !== undefined);
|
|
87
129
|
let open = false;
|
|
88
|
-
const below = props.concat(
|
|
130
|
+
const below = props.concat(refTarget(root, res, node), listOf(node.allOf), listOf(node.prefixItems), node.items, node.additionalProperties, valuesOf(node.patternProperties), valuesOf(node.dependentSchemas));
|
|
89
131
|
for (let i = 0; !yes && i < below.length; i++) {
|
|
90
|
-
const r = reachesDefault(below[i], root, memo, stack);
|
|
132
|
+
const r = reachesDefault(below[i], root, memo, stack, res);
|
|
91
133
|
if (r === 1) yes = true;
|
|
92
134
|
else if (r === 2) open = true;
|
|
93
135
|
}
|
|
@@ -119,10 +161,11 @@ function copyOf(v) {
|
|
|
119
161
|
// applied to the same value. A schema reached again (a recursive reference)
|
|
120
162
|
// takes the function being built for it, so a cycle in the schema is a loop
|
|
121
163
|
// in the calls, bounded by the depth of the data.
|
|
122
|
-
function compileNode(node,
|
|
164
|
+
function compileNode(node, ctx) {
|
|
123
165
|
if (node === null || typeof node !== "object") return null;
|
|
166
|
+
const memo = ctx.memo, path = ctx.path;
|
|
124
167
|
if (memo.has(node)) return memo.get(node);
|
|
125
|
-
if (reachesDefault(node, root, reach, new Set()) !== 1) { memo.set(node, null); return null; }
|
|
168
|
+
if (reachesDefault(node, ctx.root, ctx.reach, new Set(), ctx.res) !== 1) { memo.set(node, null); return null; }
|
|
126
169
|
let body = null;
|
|
127
170
|
// `path`: the schemas applied to the enclosing values, outermost first.
|
|
128
171
|
// `made`: inside a value the pass itself made from a default, the same as a
|
|
@@ -133,6 +176,16 @@ function compileNode(node, root, memo, reach, path) {
|
|
|
133
176
|
// schema, and Renovate's took this pass to the heap limit.
|
|
134
177
|
const fn = (data, depth, made) => {
|
|
135
178
|
if (depth > MAX_DEPTH) return;
|
|
179
|
+
// Only what is applied through a `$ref` or `allOf` is counted (fn.shared,
|
|
180
|
+
// set below), and only outside a made value, where every application
|
|
181
|
+
// does the same work.
|
|
182
|
+
if (fn.shared === true && made === null && typeof data === "object" && data !== null) {
|
|
183
|
+
const visits = ctx.visits || (ctx.visits = new Map());
|
|
184
|
+
let done = visits.get(data);
|
|
185
|
+
if (done === undefined) visits.set(data, (done = new Set()));
|
|
186
|
+
if (done.has(node)) return;
|
|
187
|
+
done.add(node);
|
|
188
|
+
}
|
|
136
189
|
if (made !== null) {
|
|
137
190
|
if (made.has(node)) return;
|
|
138
191
|
made.add(node);
|
|
@@ -154,31 +207,31 @@ function compileNode(node, root, memo, reach, path) {
|
|
|
154
207
|
const p = props[key];
|
|
155
208
|
if (!p || typeof p !== "object") continue;
|
|
156
209
|
if (p.default !== undefined) own.push([key, p.default]);
|
|
157
|
-
const child = compileNode(p,
|
|
210
|
+
const child = compileNode(p, ctx);
|
|
158
211
|
if (child !== null) below.push([key, child]);
|
|
159
212
|
}
|
|
160
213
|
}
|
|
161
214
|
// Arrays: every element under `items`, the elements `prefixItems` names by
|
|
162
215
|
// position (then `items` covers only the ones after them).
|
|
163
|
-
const tuple = Array.isArray(node.prefixItems) ? node.prefixItems.map((s) => compileNode(s,
|
|
164
|
-
const each = compileNode(node.items,
|
|
216
|
+
const tuple = Array.isArray(node.prefixItems) ? node.prefixItems.map((s) => compileNode(s, ctx)) : [];
|
|
217
|
+
const each = compileNode(node.items, ctx);
|
|
165
218
|
// Objects: keys a pattern matches, and the keys neither `properties` nor a
|
|
166
219
|
// pattern names; and a schema for each key present.
|
|
167
220
|
const patterns = [];
|
|
168
221
|
if (node.patternProperties && typeof node.patternProperties === "object") {
|
|
169
222
|
for (const src of Object.keys(node.patternProperties)) {
|
|
170
|
-
const re =
|
|
171
|
-
const t = compileNode(node.patternProperties[src],
|
|
223
|
+
const re = ctx.match(src);
|
|
224
|
+
const t = compileNode(node.patternProperties[src], ctx);
|
|
172
225
|
if (re === null) continue;
|
|
173
226
|
patterns.push([re, t]);
|
|
174
227
|
}
|
|
175
228
|
}
|
|
176
|
-
const extra = compileNode(node.additionalProperties,
|
|
229
|
+
const extra = compileNode(node.additionalProperties, ctx);
|
|
177
230
|
const declared = props && typeof props === "object" ? props : null;
|
|
178
231
|
const dependent = [];
|
|
179
232
|
if (node.dependentSchemas && typeof node.dependentSchemas === "object") {
|
|
180
233
|
for (const key of Object.keys(node.dependentSchemas)) {
|
|
181
|
-
const t = compileNode(node.dependentSchemas[key],
|
|
234
|
+
const t = compileNode(node.dependentSchemas[key], ctx);
|
|
182
235
|
if (t !== null) dependent.push([key, t]);
|
|
183
236
|
}
|
|
184
237
|
}
|
|
@@ -186,19 +239,22 @@ function compileNode(node, root, memo, reach, path) {
|
|
|
186
239
|
|
|
187
240
|
const also = [];
|
|
188
241
|
if (typeof node.$ref === "string") {
|
|
189
|
-
const t = compileNode(
|
|
190
|
-
if (t !== null) also.push(t);
|
|
242
|
+
const t = compileNode(refTarget(ctx.root, ctx.res, node), ctx);
|
|
243
|
+
if (t !== null) { t.shared = true; also.push(t); }
|
|
191
244
|
}
|
|
192
245
|
if (Array.isArray(node.allOf)) {
|
|
193
246
|
for (const s of node.allOf) {
|
|
194
|
-
const t = compileNode(s,
|
|
195
|
-
if (t !== null) also.push(t);
|
|
247
|
+
const t = compileNode(s, ctx);
|
|
248
|
+
if (t !== null) { t.shared = true; also.push(t); }
|
|
196
249
|
}
|
|
197
250
|
}
|
|
198
251
|
|
|
199
252
|
body = (data, depth, made) => {
|
|
253
|
+
// A value that is not an object has nothing to fill, here or in any
|
|
254
|
+
// schema applied to it.
|
|
255
|
+
if (typeof data !== "object" || data === null) return;
|
|
200
256
|
let fresh = null;
|
|
201
|
-
|
|
257
|
+
{
|
|
202
258
|
for (let i = 0; i < own.length; i++) {
|
|
203
259
|
const key = own[i][0];
|
|
204
260
|
if (!Object.hasOwn(data, key)) {
|
|
@@ -240,6 +296,28 @@ function compileNode(node, root, memo, reach, path) {
|
|
|
240
296
|
|
|
241
297
|
// The functions a module written ahead of time embeds as its applyDefaults
|
|
242
298
|
// where the generated pass declines a schema (lib/aot-impl.js).
|
|
243
|
-
const EMBED = ['buildDefaultsApplier', 'setOwn', 'localRef', 'reachesDefault', 'valuesOf', 'listOf', 'keyPattern', 'copyOf', 'compileNode'];
|
|
299
|
+
const EMBED = ['buildDefaultsApplier', 'setOwn', 'localRef', 'resourceRoots', 'refTarget', 'reachesDefault', 'valuesOf', 'listOf', 'keyPattern', 'copyOf', 'compileNode'];
|
|
300
|
+
|
|
301
|
+
// patternProperties keys tested in linear time: the linear-time engine
|
|
302
|
+
// (lib/safe-regex.js, which validation uses for these patterns too) for a
|
|
303
|
+
// pattern in its subset, the platform RegExp otherwise, as validation does.
|
|
304
|
+
// A key that makes a backtracking pattern run for seconds then costs no more
|
|
305
|
+
// here than it does to validate. lib/js-compiler.js also hands patterns the
|
|
306
|
+
// RegExp runs in linear time to the RegExp, for speed; that check is left
|
|
307
|
+
// out here so the closure pass, which lite carries, does not pull it in.
|
|
308
|
+
// Loaded on the first pattern; most schemas have none.
|
|
309
|
+
const PROP_ESCAPE = /\\[pP]\{/;
|
|
310
|
+
function safeKeyMatcher(src) {
|
|
311
|
+
if (!PROP_ESCAPE.test(src)) {
|
|
312
|
+
try {
|
|
313
|
+
const safe = require("./safe-regex");
|
|
314
|
+
if (safe.patternIsSafe(src)) return safe.compileSafe(src);
|
|
315
|
+
} catch {}
|
|
316
|
+
}
|
|
317
|
+
return keyPattern(src);
|
|
318
|
+
}
|
|
244
319
|
|
|
245
|
-
module.exports = {
|
|
320
|
+
module.exports = {
|
|
321
|
+
buildDefaultsApplier: (schema) => buildDefaultsApplier(schema, safeKeyMatcher),
|
|
322
|
+
setOwn, localRef, resourceRoots, refTarget, safeKeyMatcher, MAX_DEPTH, EMBED,
|
|
323
|
+
};
|
package/lib/describe-schema.js
CHANGED
|
@@ -26,6 +26,10 @@
|
|
|
26
26
|
// $defs. Anything else is described as `any` rather than guessed at.
|
|
27
27
|
|
|
28
28
|
const MAX_DEPTH = 12;
|
|
29
|
+
// anyOf branches that each lead to the same next definition describe
|
|
30
|
+
// width^layers lines, 16 million for 4 wide and 12 deep. A prompt has no use
|
|
31
|
+
// for more than this; past it the description stops and says so.
|
|
32
|
+
const MAX_LINES = 2000;
|
|
29
33
|
|
|
30
34
|
function lit (v) {
|
|
31
35
|
try { return JSON.stringify(v); } catch (_) { return String(v); }
|
|
@@ -95,6 +99,7 @@ function describeObjectBody (schema, depth, defs, lines) {
|
|
|
95
99
|
}
|
|
96
100
|
|
|
97
101
|
function describeNode (schema, label, depth, defs, lines) {
|
|
102
|
+
if (lines.length >= MAX_LINES) return;
|
|
98
103
|
const pad = ' '.repeat(depth);
|
|
99
104
|
if (schema === true || schema === undefined) { lines.push(`${pad}${label}: any`); return; }
|
|
100
105
|
if (schema === false) { lines.push(`${pad}${label}: nothing is allowed here`); return; }
|
|
@@ -103,19 +108,43 @@ function describeNode (schema, label, depth, defs, lines) {
|
|
|
103
108
|
return;
|
|
104
109
|
}
|
|
105
110
|
|
|
106
|
-
|
|
107
|
-
|
|
111
|
+
// A chain of references is followed here, each definition once: one that
|
|
112
|
+
// leads back to itself without a property in between describes no value.
|
|
113
|
+
let target = schema.$ref ? resolveRef(schema, defs) : null;
|
|
114
|
+
if (target) {
|
|
115
|
+
const chain = new Set([schema]);
|
|
116
|
+
while (target && typeof target === 'object' && target.$ref && !chain.has(target)) {
|
|
117
|
+
chain.add(target);
|
|
118
|
+
const next = resolveRef(target, defs);
|
|
119
|
+
if (!next) break;
|
|
120
|
+
target = next;
|
|
121
|
+
}
|
|
122
|
+
if (chain.has(target)) { lines.push(`${pad}${label}: any`); return; }
|
|
123
|
+
describeNode(target, label, depth, defs, lines);
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
108
126
|
|
|
109
|
-
// allOf is the intersection, so its parts describe the same value.
|
|
127
|
+
// allOf is the intersection, so its parts describe the same value. The
|
|
128
|
+
// parts' own allOf are merged in the same pass, each schema once: a part
|
|
129
|
+
// that led back to this schema copied its allOf back in and the merge
|
|
130
|
+
// recursed until the stack ran out.
|
|
110
131
|
if (Array.isArray(schema.allOf) && schema.allOf.length) {
|
|
111
132
|
const merged = Object.assign({}, schema);
|
|
112
133
|
delete merged.allOf;
|
|
113
|
-
|
|
134
|
+
const seen = new Set([schema]);
|
|
135
|
+
const queue = schema.allOf.slice();
|
|
136
|
+
while (queue.length) {
|
|
137
|
+
const part = queue.shift();
|
|
114
138
|
const resolved = part && part.$ref ? resolveRef(part, defs) || part : part;
|
|
115
|
-
if (resolved
|
|
139
|
+
if (!resolved || typeof resolved !== 'object' || seen.has(resolved)) continue;
|
|
140
|
+
seen.add(resolved);
|
|
141
|
+
if (Array.isArray(resolved.allOf)) queue.push(...resolved.allOf);
|
|
142
|
+
Object.assign(merged, resolved, {
|
|
116
143
|
properties: Object.assign({}, merged.properties, resolved.properties),
|
|
117
144
|
required: [].concat(merged.required || [], resolved.required || []),
|
|
118
145
|
});
|
|
146
|
+
delete merged.allOf;
|
|
147
|
+
if (resolved.$ref !== undefined && resolved !== part) delete merged.$ref;
|
|
119
148
|
}
|
|
120
149
|
if (!merged.properties) delete merged.properties;
|
|
121
150
|
if (!merged.required || !merged.required.length) delete merged.required;
|
|
@@ -170,6 +199,10 @@ function describeSchema (schema, opts) {
|
|
|
170
199
|
const defs = (schema && (schema.$defs || schema.definitions)) || null;
|
|
171
200
|
const lines = [];
|
|
172
201
|
describeNode(schema, name, 0, defs, lines);
|
|
202
|
+
if (lines.length >= MAX_LINES) {
|
|
203
|
+
lines.length = MAX_LINES;
|
|
204
|
+
lines.push(`(description cut at ${MAX_LINES} lines)`);
|
|
205
|
+
}
|
|
173
206
|
return lines.join('\n');
|
|
174
207
|
}
|
|
175
208
|
|
package/lib/diagnose.js
CHANGED
|
@@ -111,21 +111,53 @@ function frameFor (err, resolved) {
|
|
|
111
111
|
// A caret on the key token when the error names a property, on the value
|
|
112
112
|
// otherwise. Never wider than the line it is drawn under.
|
|
113
113
|
const useKey = named != null && hit.keyOffset !== undefined;
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
const lineLen = (hit.text || '').length;
|
|
117
|
-
const length = Math.max(1, Math.min(rawLen || 1, Math.max(1, lineLen - col + 1)));
|
|
114
|
+
return frameOf(hit, useKey ? hit : null, resolved.synthesized);
|
|
115
|
+
}
|
|
118
116
|
|
|
117
|
+
// The frame a renderer draws. `col` is the column in the line, for the
|
|
118
|
+
// location; `textCol` is where the caret goes in `text`, which differs when
|
|
119
|
+
// the line was long and `text` is the part of it around the value. Never
|
|
120
|
+
// wider than the text it is drawn under.
|
|
121
|
+
function frameOf (at, key, synthesized) {
|
|
122
|
+
const shift = typeof at.textCol === 'number' && (!key || key.keyLine === at.line) ? at.col - at.textCol : 0;
|
|
123
|
+
const col = key ? key.keyCol : at.col;
|
|
124
|
+
const textCol = Math.max(1, col - shift);
|
|
125
|
+
const rawLen = key ? key.keyLength : at.length;
|
|
126
|
+
const lineLen = termLine(at.text || '').length;
|
|
127
|
+
const length = Math.max(1, Math.min(rawLen || 1, Math.max(1, lineLen - textCol + 1)));
|
|
119
128
|
return {
|
|
120
|
-
line:
|
|
129
|
+
line: key ? key.keyLine : at.line,
|
|
121
130
|
col,
|
|
131
|
+
textCol,
|
|
122
132
|
length,
|
|
123
|
-
text:
|
|
133
|
+
text: termLine(at.text),
|
|
124
134
|
spans: rawLen > length,
|
|
125
|
-
synthesized
|
|
135
|
+
synthesized,
|
|
126
136
|
};
|
|
127
137
|
}
|
|
128
138
|
|
|
139
|
+
// Text from a document or a schema on its way to a terminal: control
|
|
140
|
+
// characters, DEL, the C1 range and the Unicode line separators written as
|
|
141
|
+
// escapes, so a property name cannot start a line, move the cursor or set a
|
|
142
|
+
// colour.
|
|
143
|
+
const TERM_UNSAFE = /[\x00-\x1f\x7f-\x9f\u2028\u2029]/g;
|
|
144
|
+
function term (s) {
|
|
145
|
+
if (typeof s !== 'string') return s;
|
|
146
|
+
return s.replace(TERM_UNSAFE, (c) => {
|
|
147
|
+
if (c === '\n') return '\\n';
|
|
148
|
+
if (c === '\r') return '\\r';
|
|
149
|
+
if (c === '\t') return '\\t';
|
|
150
|
+
return '\\u' + c.charCodeAt(0).toString(16).padStart(4, '0');
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
// A source line drawn above a caret: one character for one, a space for a
|
|
154
|
+
// tab or a carriage return and U+FFFD for the rest, so the caret stays under
|
|
155
|
+
// the value.
|
|
156
|
+
function termLine (s) {
|
|
157
|
+
if (typeof s !== 'string') return s;
|
|
158
|
+
return s.replace(TERM_UNSAFE, (c) => (c === '\t' || c === '\r' ? ' ' : '\ufffd'));
|
|
159
|
+
}
|
|
160
|
+
|
|
129
161
|
// No position map in hand, but the error itself may carry one: validateJSON
|
|
130
162
|
// attaches `dataFrame` and `anchor` at enrichment time, and errors handed
|
|
131
163
|
// over from another process or a fixture arrive with only those.
|
|
@@ -135,18 +167,7 @@ function frameFromError (err) {
|
|
|
135
167
|
const a = err.anchor;
|
|
136
168
|
const named = (err.params && (err.params.additionalProperty || err.params.unevaluatedProperty)) || null;
|
|
137
169
|
const useKey = named != null && a && a.keyLine !== undefined;
|
|
138
|
-
|
|
139
|
-
const rawLen = useKey ? a.keyLength : df.length;
|
|
140
|
-
const lineLen = (df.text || '').length;
|
|
141
|
-
const length = Math.max(1, Math.min(rawLen || 1, Math.max(1, lineLen - col + 1)));
|
|
142
|
-
return {
|
|
143
|
-
line: useKey ? a.keyLine : df.line,
|
|
144
|
-
col,
|
|
145
|
-
length,
|
|
146
|
-
text: df.text,
|
|
147
|
-
spans: rawLen > length,
|
|
148
|
-
synthesized: false,
|
|
149
|
-
};
|
|
170
|
+
return frameOf(df, useKey ? a : null, false);
|
|
150
171
|
}
|
|
151
172
|
|
|
152
173
|
function headlineFor (err, src) {
|
|
@@ -210,6 +231,7 @@ function decorateBranches (subs, src) {
|
|
|
210
231
|
const got = valueAt(src.data, sub.instancePath);
|
|
211
232
|
if (got !== null) copy.received = got;
|
|
212
233
|
}
|
|
234
|
+
for (const k of ['keyword', 'message', 'expected', 'received']) if (typeof copy[k] === 'string') copy[k] = term(copy[k]);
|
|
213
235
|
if (Array.isArray(sub.branchErrors)) copy.branchErrors = decorateBranches(sub.branchErrors, src);
|
|
214
236
|
return copy;
|
|
215
237
|
});
|
|
@@ -262,13 +284,13 @@ function singleDiagnostic (e, index, resolved, src) {
|
|
|
262
284
|
|
|
263
285
|
return {
|
|
264
286
|
code: e.code,
|
|
265
|
-
headline: headlineFor(e, src),
|
|
287
|
+
headline: term(headlineFor(e, src)),
|
|
266
288
|
pointer,
|
|
267
|
-
dotted: pathToDotted(anchorErr.instancePath != null ? anchorErr.instancePath : pointer),
|
|
289
|
+
dotted: term(pathToDotted(anchorErr.instancePath != null ? anchorErr.instancePath : pointer)),
|
|
268
290
|
frame,
|
|
269
|
-
found,
|
|
270
|
-
help: e.suggestion ? e.suggestion.text : null,
|
|
271
|
-
notes,
|
|
291
|
+
found: term(found),
|
|
292
|
+
help: e.suggestion ? term(e.suggestion.text) : null,
|
|
293
|
+
notes: notes.map(term),
|
|
272
294
|
branchErrors: e.branchErrors ? decorateBranches(e.branchErrors, src) : null,
|
|
273
295
|
mergedFrom: [e],
|
|
274
296
|
rank: typeof e.rank === 'number' ? e.rank : 2,
|
|
@@ -295,13 +317,13 @@ function mergedDiagnostic (errors, i, j, resolved, src) {
|
|
|
295
317
|
const pointer = missing.instancePath != null ? missing.instancePath : (missing.path || '');
|
|
296
318
|
return {
|
|
297
319
|
code: missing.code,
|
|
298
|
-
headline: `unknown property "${extraName}"
|
|
320
|
+
headline: term(`unknown property "${extraName}"`),
|
|
299
321
|
pointer,
|
|
300
|
-
dotted: pathToDotted(pointer),
|
|
322
|
+
dotted: term(pathToDotted(pointer)),
|
|
301
323
|
frame,
|
|
302
324
|
found: null,
|
|
303
|
-
help: `did you mean "${missingName}"
|
|
304
|
-
notes,
|
|
325
|
+
help: term(`did you mean "${missingName}"?`),
|
|
326
|
+
notes: notes.map(term),
|
|
305
327
|
branchErrors: null,
|
|
306
328
|
mergedFrom: [missing, extra],
|
|
307
329
|
rank: 0,
|
|
@@ -309,4 +331,4 @@ function mergedDiagnostic (errors, i, j, resolved, src) {
|
|
|
309
331
|
};
|
|
310
332
|
}
|
|
311
333
|
|
|
312
|
-
module.exports = { toDiagnostics };
|
|
334
|
+
module.exports = { term, termLine, toDiagnostics };
|
package/lib/enrich-error.js
CHANGED
|
@@ -87,6 +87,15 @@ function reprValue (v) {
|
|
|
87
87
|
// fallback prints, so it is taken once either way; the walk and the
|
|
88
88
|
// serialisation used to run on every container an error named, and a
|
|
89
89
|
// `required` or `unevaluatedProperties` error names its container.
|
|
90
|
+
// A wide object is counted once per synchronous run: every error that
|
|
91
|
+
// names the same container (additionalProperties: false on an object
|
|
92
|
+
// with N unknown keys gives N of them) counted its keys again, N times N
|
|
93
|
+
// key reads, and 16000 extra keys took 11.8 s to read. The answer is
|
|
94
|
+
// kept until the running synchronous code finishes (a microtask drops
|
|
95
|
+
// it), which covers reading one list. The limit: an object changed and
|
|
96
|
+
// read again within that same run reports the count it had first.
|
|
97
|
+
const known = _wide !== null ? _wide.get(v) : undefined;
|
|
98
|
+
if (known !== undefined) return known;
|
|
90
99
|
let n;
|
|
91
100
|
try {
|
|
92
101
|
n = Object.keys(v).length;
|
|
@@ -101,10 +110,21 @@ function reprValue (v) {
|
|
|
101
110
|
return '[object, unserializable]';
|
|
102
111
|
}
|
|
103
112
|
}
|
|
104
|
-
|
|
113
|
+
const r = `[object, ${n} ${n === 1 ? 'key' : 'keys'}]`;
|
|
114
|
+
if (n > WIDE) {
|
|
115
|
+
if (_wide === null) {
|
|
116
|
+
_wide = new Map();
|
|
117
|
+
queueMicrotask(() => { _wide = null; });
|
|
118
|
+
}
|
|
119
|
+
_wide.set(v, r);
|
|
120
|
+
}
|
|
121
|
+
return r;
|
|
105
122
|
}
|
|
106
123
|
return `[${t}]`;
|
|
107
124
|
}
|
|
125
|
+
// Past this many keys a count is worth keeping (see reprValue).
|
|
126
|
+
const WIDE = 64;
|
|
127
|
+
let _wide = null;
|
|
108
128
|
|
|
109
129
|
function expectedFor (err) {
|
|
110
130
|
switch (err.keyword) {
|
|
@@ -298,6 +318,7 @@ function enrich (rawErr, opts) {
|
|
|
298
318
|
if (positions && positions[path]) {
|
|
299
319
|
const p = positions[path];
|
|
300
320
|
out.dataFrame = { byteOffset: p.byteOffset, length: p.length, line: p.line, col: p.col, text: p.text };
|
|
321
|
+
if (p.textCol !== undefined) out.dataFrame.textCol = p.textCol;
|
|
301
322
|
} else if (!positions && opts && opts.lazyPositions) {
|
|
302
323
|
defineLazyFrames(out, path, named, opts.lazyPositions);
|
|
303
324
|
}
|
|
@@ -360,6 +381,7 @@ function defineLazyFrames (out, path, named, lazy) {
|
|
|
360
381
|
const positions = lazy();
|
|
361
382
|
const own = positions ? positions[path] : null;
|
|
362
383
|
const dataFrame = own ? { byteOffset: own.byteOffset, length: own.length, line: own.line, col: own.col, text: own.text } : undefined;
|
|
384
|
+
if (dataFrame !== undefined && own.textCol !== undefined) dataFrame.textCol = own.textCol;
|
|
363
385
|
const child = positions && named ? positions[(path === '' ? '' : path) + '/' + named] : null;
|
|
364
386
|
const src = child || own;
|
|
365
387
|
let anchor;
|
package/lib/error-sort-rank.js
CHANGED
|
@@ -46,6 +46,18 @@ function sortErrorsByRank(rootSchema, errors) {
|
|
|
46
46
|
prev = r;
|
|
47
47
|
}
|
|
48
48
|
if (sorted) return errors;
|
|
49
|
+
// Insertion sort is quadratic: a document with 20000 unknown keys under
|
|
50
|
+
// additionalProperties: false, reported out of order, took seconds here.
|
|
51
|
+
// Past a short list the platform's sort, which is stable, gives the same
|
|
52
|
+
// order, ties kept as they came.
|
|
53
|
+
if (n > 16) {
|
|
54
|
+
const idx = new Array(n);
|
|
55
|
+
for (let i = 0; i < n; i++) idx[i] = i;
|
|
56
|
+
idx.sort((a, b) => compareRanks(keys[a], keys[b]) || a - b);
|
|
57
|
+
const res = new Array(n);
|
|
58
|
+
for (let i = 0; i < n; i++) res[i] = errors[idx[i]];
|
|
59
|
+
return res;
|
|
60
|
+
}
|
|
49
61
|
const res = errors.slice();
|
|
50
62
|
for (let i = 1; i < n; i++) {
|
|
51
63
|
const k = keys[i];
|
package/lib/formats-source.js
CHANGED
|
@@ -111,6 +111,17 @@ function hostnameSource (v, isStr) {
|
|
|
111
111
|
return guard(v, isStr, hostnameRangeSource(v, '0', `${v}.length`));
|
|
112
112
|
}
|
|
113
113
|
|
|
114
|
+
// The hoisted form: two tables built once per compiled function, a character
|
|
115
|
+
// class (1 hex digit, 2 hyphen) and the class each of the 36 positions wants,
|
|
116
|
+
// and a loop that ORs the mismatches instead of branching on every character.
|
|
117
|
+
// On the 900 ids of the order body: 85 to 35 ns against the per-run loops below.
|
|
118
|
+
const uuidHelperSource = (name) =>
|
|
119
|
+
`const ${name}H=new Uint8Array(128),${name}M=new Uint8Array(36).fill(1);` +
|
|
120
|
+
`for(let _i=48;_i<58;_i++)${name}H[_i]=1;for(let _i=97;_i<103;_i++)${name}H[_i]=1;for(let _i=65;_i<71;_i++)${name}H[_i]=1;` +
|
|
121
|
+
`${name}H[45]=2;${name}M[8]=${name}M[13]=${name}M[18]=${name}M[23]=2;` +
|
|
122
|
+
`function ${name}(_s){if(_s.length!==36)return false;let _b=0;for(let _i=0;_i<36;_i++)_b|=${name}H[_s.charCodeAt(_i)]^${name}M[_i];return _b===0}`
|
|
123
|
+
|
|
124
|
+
// The inline form, for a caller with no function to hoist into.
|
|
114
125
|
function uuidSource (v, isStr) {
|
|
115
126
|
const run = (from, to) => `for(let _ui=${from};_ui<${to};_ui++){const _uc=${v}.charCodeAt(_ui);` +
|
|
116
127
|
'if(!((_uc-48>>>0)<10||((_uc|32)-97>>>0)<6))return false}';
|
|
@@ -321,4 +332,4 @@ function durationSource (v, isStr) {
|
|
|
321
332
|
const inner = `if(!${_formats().DURATION_RE.toString()}.test(${v}))return false`;
|
|
322
333
|
return isStr ? inner : `if(typeof ${v}==='string'&&${inner.slice(3)}`;
|
|
323
334
|
}
|
|
324
|
-
module.exports = { _literalByParsers, uriHelperSource, uriHelperCompactSource, uriSource, uriReferenceSource, iriSource, iriReferenceSource, idnEmailSource, dateSource, ipv4Source, dateTimeSource, ipv6Source, hostnameSource, uuidSource, timeSource, noReservedSource, jsonPointerSource, relativeJsonPointerSource, uriTemplateSource, emailSource, durationSource };
|
|
335
|
+
module.exports = { _literalByParsers, uriHelperSource, uriHelperCompactSource, uriSource, uriReferenceSource, iriSource, iriReferenceSource, idnEmailSource, dateSource, ipv4Source, dateTimeSource, ipv6Source, hostnameSource, uuidSource, uuidHelperSource, timeSource, noReservedSource, jsonPointerSource, relativeJsonPointerSource, uriTemplateSource, emailSource, durationSource };
|
package/lib/formats.js
CHANGED
|
@@ -4,9 +4,8 @@
|
|
|
4
4
|
// and no allocation. Each has a twin that emits the same check as source for
|
|
5
5
|
// the code generator; tests/test_formats_single_pass.js holds the two
|
|
6
6
|
// together and fuzzes both against the regular-expression forms they replaced.
|
|
7
|
-
// Measured, interleaved medians: date 45.7 to 14.2 ns, ipv4 54.5 to 27.1 ns
|
|
8
|
-
// uuid
|
|
9
|
-
// expression for a fixed-length hex pattern is already that fast, so it stays.
|
|
7
|
+
// Measured, interleaved medians: date 45.7 to 14.2 ns, ipv4 54.5 to 27.1 ns,
|
|
8
|
+
// uuid 85 to 35 ns with the class tables below.
|
|
10
9
|
|
|
11
10
|
function isDigit (c) { return c >= 48 && c <= 57; }
|
|
12
11
|
|
|
@@ -328,25 +327,26 @@ function noReserved (s, from) {
|
|
|
328
327
|
return true;
|
|
329
328
|
}
|
|
330
329
|
|
|
331
|
-
// 8-4-4-4-12 hexadecimal digits.
|
|
332
|
-
//
|
|
333
|
-
//
|
|
334
|
-
//
|
|
335
|
-
//
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
330
|
+
// 8-4-4-4-12 hexadecimal digits. Two tables: the class of a character (1 a
|
|
331
|
+
// hex digit in either case, 2 a hyphen, 0 anything else, including anything
|
|
332
|
+
// past ASCII, which indexes past the table and reads as undefined) and the
|
|
333
|
+
// class each of the 36 positions wants. The loop ORs the mismatches and
|
|
334
|
+
// decides once at the end, so no character costs a branch: 85 to 35 ns on
|
|
335
|
+
// the order body's ids against the earlier per-run loops with two compares a
|
|
336
|
+
// character.
|
|
337
|
+
const UUID_CLASS = new Uint8Array(128);
|
|
338
|
+
for (let i = 48; i < 58; i++) UUID_CLASS[i] = 1;
|
|
339
|
+
for (let i = 97; i < 103; i++) UUID_CLASS[i] = 1;
|
|
340
|
+
for (let i = 65; i < 71; i++) UUID_CLASS[i] = 1;
|
|
341
|
+
UUID_CLASS[45] = 2;
|
|
342
|
+
const UUID_WANT = new Uint8Array(36).fill(1);
|
|
343
|
+
UUID_WANT[8] = UUID_WANT[13] = UUID_WANT[18] = UUID_WANT[23] = 2;
|
|
343
344
|
|
|
344
345
|
function uuid (s) {
|
|
345
346
|
if (s.length !== 36) return false;
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
return
|
|
349
|
-
hexRun(s, 19, 23) && hexRun(s, 24, 36);
|
|
347
|
+
let bad = 0;
|
|
348
|
+
for (let i = 0; i < 36; i++) bad |= UUID_CLASS[s.charCodeAt(i)] ^ UUID_WANT[i];
|
|
349
|
+
return bad === 0;
|
|
350
350
|
}
|
|
351
351
|
|
|
352
352
|
// RFC 3339 full-time: HH:MM:SS, an optional fraction, an optional offset.
|
package/lib/interpreter-core.js
CHANGED
|
@@ -429,9 +429,15 @@ class Plan {
|
|
|
429
429
|
this.format = schema.format;
|
|
430
430
|
this.formatFn = null;
|
|
431
431
|
if (schema.format !== undefined) {
|
|
432
|
+
// Own properties only, as the generated code reads them: an inherited
|
|
433
|
+
// Object member ('hasOwnProperty', 'constructor') is not a check.
|
|
434
|
+
const own = Object.prototype.hasOwnProperty;
|
|
432
435
|
const uf = interp.userFormats;
|
|
433
|
-
const
|
|
434
|
-
|
|
436
|
+
const f = schema.format;
|
|
437
|
+
const table = require('./formats').formatChecks();
|
|
438
|
+
const fc = (uf && typeof f === 'string' && own.call(uf, f) && typeof uf[f] === 'function') ? uf[f]
|
|
439
|
+
: (typeof f === 'string' && own.call(table, f) && typeof table[f] === 'function' ? table[f] : null);
|
|
440
|
+
this.formatFn = fc;
|
|
435
441
|
}
|
|
436
442
|
this.hasString = this.minLength !== undefined || this.maxLength !== undefined || this.pattern !== null || this.formatFn !== null;
|
|
437
443
|
// A format compat registered for numbers (`type: 'number'`), checked on
|
|
@@ -917,14 +923,27 @@ class Interpreter {
|
|
|
917
923
|
if (!re) {
|
|
918
924
|
if (/\\[pP]\{/.test(src)) {
|
|
919
925
|
// Unicode property escapes need a real RegExp in unicode mode; the
|
|
920
|
-
// linear engine does not understand them
|
|
926
|
+
// linear engine does not understand them (compileSafe refuses them),
|
|
927
|
+
// so the RegExp answers whether or not it runs this pattern in linear
|
|
928
|
+
// time. Asking lib/regex-linear.js would change no answer and would
|
|
929
|
+
// add it to lite. A nested loop around a property escape can
|
|
930
|
+
// backtrack here; SECURITY.md says so.
|
|
921
931
|
try { re = new RegExp(src, 'u'); } catch { re = new RegExp(src); }
|
|
922
932
|
} else if (plainLinear(src)) {
|
|
923
933
|
re = new RegExp(src);
|
|
924
934
|
} else {
|
|
925
935
|
try {
|
|
926
936
|
re = compileSafe(src);
|
|
927
|
-
} catch {
|
|
937
|
+
} catch (e) {
|
|
938
|
+
// A parse failure means the engine cannot represent the pattern
|
|
939
|
+
// (a backreference, a lookaround, an escape it does not implement,
|
|
940
|
+
// a repeat past its instruction budget) and the platform RegExp
|
|
941
|
+
// takes it.
|
|
942
|
+
// The stub of a shared error runtime built without the engine
|
|
943
|
+
// (lib/error-runtime.js) throws too, and that one is rethrown: a
|
|
944
|
+
// module whose runtime lacks the engine must fail, not quietly
|
|
945
|
+
// match on a RegExp that can backtrack.
|
|
946
|
+
if (e && typeof e.message === 'string' && e.message.includes('built without the pattern engine')) throw e;
|
|
928
947
|
try { re = new RegExp(src, 'u'); } catch { re = new RegExp(src); }
|
|
929
948
|
}
|
|
930
949
|
}
|