ata-validator 1.42.0 → 1.44.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 +10 -10
- package/index.js +56 -2
- package/lib/branch-collapse.js +18 -12
- package/lib/clone-emit.js +5 -2
- package/lib/data-position-cache.js +12 -6
- package/lib/draft7.js +5 -7
- package/lib/enrich-site.js +2 -2
- package/lib/js-compiler.js +788 -172
- package/lib/levenshtein.js +34 -22
- package/lib/native-positions.js +100 -0
- package/lib/rejections.js +14 -7
- package/lib/scan-compiler.js +210 -25
- package/lib/scan-runtime.js +13 -1
- package/lib/suggestions.js +27 -20
- package/lib/validator-core.js +94 -14
- package/lib/version.js +1 -1
- package/package.json +9 -9
package/lib/levenshtein.js
CHANGED
|
@@ -1,7 +1,14 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
// Bounded Levenshtein. Returns Infinity if distance > maxDistance
|
|
4
|
-
//
|
|
3
|
+
// Bounded Levenshtein. Returns Infinity if distance > maxDistance (or, past
|
|
4
|
+
// the bound, possibly some number greater than it; callers compare).
|
|
5
|
+
//
|
|
6
|
+
// Only the cells within the bound of the diagonal are computed: a path
|
|
7
|
+
// through any other cell already costs more than the bound. A typo hint asks
|
|
8
|
+
// with a bound of one or two and compares the missing name with every key
|
|
9
|
+
// present, so a row is five cells instead of the whole key. Without a bound
|
|
10
|
+
// the band is the whole row.
|
|
11
|
+
//
|
|
5
12
|
// Two rows of scratch, reused across calls. The function is not recursive
|
|
6
13
|
// and the library is single-threaded, so nothing else is using them; growing
|
|
7
14
|
// them once beats allocating two arrays on every comparison, which the
|
|
@@ -10,42 +17,47 @@ let scratchA = new Int32Array(64);
|
|
|
10
17
|
let scratchB = new Int32Array(64);
|
|
11
18
|
|
|
12
19
|
function levenshtein (a, b, maxDistance) {
|
|
13
|
-
const max = maxDistance == null ? Infinity : maxDistance;
|
|
14
20
|
if (a === b) return 0;
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
if (
|
|
19
|
-
|
|
20
|
-
|
|
21
|
+
const n = a.length;
|
|
22
|
+
const m = b.length;
|
|
23
|
+
const max = maxDistance == null || maxDistance > n + m ? n + m : maxDistance;
|
|
24
|
+
if (Math.abs(n - m) > max) return Infinity;
|
|
25
|
+
if (n === 0) return m;
|
|
26
|
+
if (m === 0) return n;
|
|
27
|
+
const big = max + 1;
|
|
28
|
+
if (scratchA.length < m + 2) {
|
|
29
|
+
scratchA = new Int32Array(m + 2);
|
|
30
|
+
scratchB = new Int32Array(m + 2);
|
|
21
31
|
}
|
|
22
32
|
let prev = scratchA;
|
|
23
33
|
let curr = scratchB;
|
|
24
|
-
|
|
25
|
-
for (let
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
34
|
+
for (let j = 0; j <= m; j++) prev[j] = j <= max ? j : big;
|
|
35
|
+
for (let i = 1; i <= n; i++) {
|
|
36
|
+
const lo = i - max > 1 ? i - max : 1;
|
|
37
|
+
const hi = i + max < m ? i + max : m;
|
|
38
|
+
curr[lo - 1] = lo === 1 ? i : big;
|
|
39
|
+
let rowMin = curr[lo - 1];
|
|
29
40
|
// charCodeAt rather than indexing: reading a character by index allocates
|
|
30
41
|
// a one-character string, and this is the innermost loop.
|
|
31
42
|
const ca = a.charCodeAt(i - 1);
|
|
32
|
-
for (let j =
|
|
33
|
-
|
|
34
|
-
let m = prev[j - 1] + cost;
|
|
43
|
+
for (let j = lo; j <= hi; j++) {
|
|
44
|
+
let v = prev[j - 1] + (ca === b.charCodeAt(j - 1) ? 0 : 1);
|
|
35
45
|
const del = curr[j - 1] + 1;
|
|
36
|
-
if (del <
|
|
46
|
+
if (del < v) v = del;
|
|
37
47
|
const ins = prev[j] + 1;
|
|
38
|
-
if (ins <
|
|
39
|
-
|
|
40
|
-
|
|
48
|
+
if (ins < v) v = ins;
|
|
49
|
+
if (v > big) v = big;
|
|
50
|
+
curr[j] = v;
|
|
51
|
+
if (v < rowMin) rowMin = v;
|
|
41
52
|
}
|
|
53
|
+
if (hi < m) curr[hi + 1] = big;
|
|
42
54
|
if (rowMin > max) return Infinity;
|
|
43
55
|
// A destructured swap builds an array per row; a temporary does not.
|
|
44
56
|
const t = prev;
|
|
45
57
|
prev = curr;
|
|
46
58
|
curr = t;
|
|
47
59
|
}
|
|
48
|
-
return prev[
|
|
60
|
+
return prev[m] > max ? Infinity : prev[m];
|
|
49
61
|
}
|
|
50
62
|
|
|
51
63
|
module.exports = { levenshtein };
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// The entries buildTargetedPositionMap gives, found by the native addon where
|
|
4
|
+
// it can: simdjson indexes the document's structure once and walks that
|
|
5
|
+
// index to each pointer, where the script walk reads every character. On a
|
|
6
|
+
// 5 MB log with the error near its end that is 3.1 ms against 8.3. Only for
|
|
7
|
+
// a large ASCII string, where the addon's UTF-8 byte offsets are the string's
|
|
8
|
+
// indexes, and only when every wanted pointer is found; otherwise null, and
|
|
9
|
+
// the caller builds the map in script as before.
|
|
10
|
+
const NATIVE_MIN = 32 * 1024;
|
|
11
|
+
let _native;
|
|
12
|
+
function nativeTargeted (text, wanted) {
|
|
13
|
+
if (typeof text !== 'string' || text.length < NATIVE_MIN) return null;
|
|
14
|
+
if (_native === undefined) {
|
|
15
|
+
try { _native = require('./native-load')(); } catch { _native = null; }
|
|
16
|
+
if (_native && typeof _native.locatePointers !== 'function') _native = null;
|
|
17
|
+
}
|
|
18
|
+
if (_native === null || typeof Buffer === 'undefined' || Buffer.byteLength(text, 'utf8') !== text.length) return null;
|
|
19
|
+
const ptrs = Array.from(wanted);
|
|
20
|
+
let found;
|
|
21
|
+
try { found = _native.locatePointers(text, ptrs); } catch { return null; }
|
|
22
|
+
const offs = [];
|
|
23
|
+
for (let k = 0; k < ptrs.length; k++) {
|
|
24
|
+
if (!(found[2 * k] >= 0)) return null;
|
|
25
|
+
offs.push(found[2 * k]);
|
|
26
|
+
}
|
|
27
|
+
// Line starts for the offsets that are asked about, in one forward pass.
|
|
28
|
+
const needed = [];
|
|
29
|
+
for (let k = 0; k < ptrs.length; k++) {
|
|
30
|
+
needed.push(offs[k]);
|
|
31
|
+
const ks = keyStartBefore(text, offs[k]);
|
|
32
|
+
if (ks >= 0) needed.push(ks);
|
|
33
|
+
}
|
|
34
|
+
const lineOf = lineFinder(text, needed);
|
|
35
|
+
const map = Object.create(null);
|
|
36
|
+
for (let k = 0; k < ptrs.length; k++) {
|
|
37
|
+
const off = offs[k];
|
|
38
|
+
const at = lineOf(off);
|
|
39
|
+
const entry = { byteOffset: off, length: found[2 * k + 1], line: at.line, col: off - at.start + 1, text: at.text };
|
|
40
|
+
const ks = keyStartBefore(text, off);
|
|
41
|
+
if (ks >= 0 && ptrs[k] !== '') {
|
|
42
|
+
const kat = lineOf(ks);
|
|
43
|
+
entry.keyOffset = ks;
|
|
44
|
+
entry.keyLength = keyEndAfter(text, ks) - ks + 1;
|
|
45
|
+
entry.keyLine = kat.line;
|
|
46
|
+
entry.keyCol = ks - kat.start + 1;
|
|
47
|
+
}
|
|
48
|
+
map[ptrs[k]] = entry;
|
|
49
|
+
}
|
|
50
|
+
return map;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// The opening quote of the member name before a value at `off`, or -1 where
|
|
54
|
+
// the value is an array element or the root.
|
|
55
|
+
function keyStartBefore (text, off) {
|
|
56
|
+
let j = off - 1;
|
|
57
|
+
while (j >= 0) { const c = text.charCodeAt(j); if (c === 0x20 || c === 0x09 || c === 0x0a || c === 0x0d) j--; else break; }
|
|
58
|
+
if (j < 0 || text.charCodeAt(j) !== 0x3a) return -1;
|
|
59
|
+
j--;
|
|
60
|
+
while (j >= 0) { const c = text.charCodeAt(j); if (c === 0x20 || c === 0x09 || c === 0x0a || c === 0x0d) j--; else break; }
|
|
61
|
+
if (j < 0 || text.charCodeAt(j) !== 0x22) return -1;
|
|
62
|
+
// Back to the quote that opens the name: one not preceded by an odd run of
|
|
63
|
+
// backslashes.
|
|
64
|
+
for (let q = j - 1; q >= 0; q--) {
|
|
65
|
+
if (text.charCodeAt(q) !== 0x22) continue;
|
|
66
|
+
let b = q - 1;
|
|
67
|
+
while (b >= 0 && text.charCodeAt(b) === 0x5c) b--;
|
|
68
|
+
if (((q - 1 - b) & 1) === 0) return q;
|
|
69
|
+
}
|
|
70
|
+
return -1;
|
|
71
|
+
}
|
|
72
|
+
function keyEndAfter (text, start) {
|
|
73
|
+
let q = start + 1;
|
|
74
|
+
for (;;) {
|
|
75
|
+
const c = text.charCodeAt(q);
|
|
76
|
+
if (c === 0x5c) { q += 2; continue; }
|
|
77
|
+
if (c === 0x22 || q >= text.length) return q;
|
|
78
|
+
q++;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
// Line number, line start and line text for any of `offsets`, from one pass.
|
|
82
|
+
function lineFinder (text, offsets) {
|
|
83
|
+
const sorted = Array.from(new Set(offsets)).sort((a, b) => a - b);
|
|
84
|
+
const info = new Map();
|
|
85
|
+
let line = 1, start = 0, s = 0;
|
|
86
|
+
while (s < sorted.length) {
|
|
87
|
+
const nl = text.indexOf('\n', start);
|
|
88
|
+
const end = nl === -1 ? text.length : nl;
|
|
89
|
+
while (s < sorted.length && sorted[s] <= end) {
|
|
90
|
+
info.set(sorted[s], { line, start, text: text.slice(start, end) });
|
|
91
|
+
s++;
|
|
92
|
+
}
|
|
93
|
+
if (nl === -1) break;
|
|
94
|
+
line++;
|
|
95
|
+
start = nl + 1;
|
|
96
|
+
}
|
|
97
|
+
return (off) => info.get(off);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
module.exports = { nativeTargeted };
|
package/lib/rejections.js
CHANGED
|
@@ -133,13 +133,18 @@ Object.defineProperty(RichRejection.prototype, 'errors', {
|
|
|
133
133
|
// touches none of it. The JSON text is held here rather than read back off the
|
|
134
134
|
// validator, so the result does not depend on what the instance does next.
|
|
135
135
|
class LazyJsonRejection {
|
|
136
|
-
|
|
136
|
+
// `parsed`, when given, is the document the inner path parsed from
|
|
137
|
+
// `jsonStr` and validated, after defaults, coercion and removal: reading the
|
|
138
|
+
// errors uses it instead of parsing the text again, which on a 5 MB
|
|
139
|
+
// document was a third of what reading them cost.
|
|
140
|
+
constructor(result, jsonStr, self, enrich, parsed) {
|
|
137
141
|
this.valid = false;
|
|
138
142
|
this._result = result;
|
|
139
143
|
this._jsonStr = jsonStr;
|
|
140
144
|
this._self = self;
|
|
141
145
|
this._enrich = enrich;
|
|
142
146
|
this._cached = null;
|
|
147
|
+
this._parsed = parsed;
|
|
143
148
|
}
|
|
144
149
|
toJSON() {
|
|
145
150
|
return { valid: false, errors: this.errors };
|
|
@@ -159,12 +164,14 @@ Object.defineProperty(LazyJsonRejection.prototype, 'errors', {
|
|
|
159
164
|
|
|
160
165
|
// The enrich pass plucks `received` from the value tree and the suggestion
|
|
161
166
|
// engine (required-typo, format hints, coercion nudges) walks it too.
|
|
162
|
-
let parsedData;
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
167
|
+
let parsedData = this._parsed;
|
|
168
|
+
if (parsedData === undefined) {
|
|
169
|
+
try { parsedData = JSON.parse(jsonStr); } catch { parsedData = undefined; }
|
|
170
|
+
// What was checked is the document after defaults, coercion and removal,
|
|
171
|
+
// so `received` has to come from that, as it does for validate(). A
|
|
172
|
+
// default that failed its own schema has no value in the text at all.
|
|
173
|
+
if (parsedData !== undefined && self._preprocess) self._preprocess(parsedData);
|
|
174
|
+
}
|
|
168
175
|
|
|
169
176
|
// Errors the inner path already enriched carry a `received` key: enrich()
|
|
170
177
|
// always sets one, even when there is no value to show. `code` is not a
|
package/lib/scan-compiler.js
CHANGED
|
@@ -90,6 +90,22 @@ const BASE_CHANGING = new Set(['$id', '$anchor', '$dynamicAnchor', '$dynamicRef'
|
|
|
90
90
|
// it. Sized for a node per declared property of a large generated schema,
|
|
91
91
|
// not just for hand-written ones.
|
|
92
92
|
const MAX_NODES = 8192;
|
|
93
|
+
// See the size checks in compileScanner. One function past 64 KB is what V8's
|
|
94
|
+
// Maglev tier aborted on: SchemaStore scanners of 0.5 to 1 MB, single
|
|
95
|
+
// functions before references became calls. The total bounds the memory a
|
|
96
|
+
// scanner takes.
|
|
97
|
+
const MAX_FUNCTION_SOURCE = 64 * 1024;
|
|
98
|
+
const MAX_SOURCE = 2 * 1024 * 1024;
|
|
99
|
+
// A reference target is expanded at each reference while its expansions
|
|
100
|
+
// together are at most this much source, and called otherwise.
|
|
101
|
+
const INLINE_REF_MAX = 4096;
|
|
102
|
+
// An object member whose scan is more than this much source is moved into a
|
|
103
|
+
// function of its own (emitMember).
|
|
104
|
+
const SPLIT_MIN = 4 * 1024;
|
|
105
|
+
// The member source an object keeps inline before the rest move out.
|
|
106
|
+
const SPLIT_OBJECT = 16 * 1024;
|
|
107
|
+
// Declared names at least this long on average find a key's end with indexOf.
|
|
108
|
+
const INDEX_KEY_MIN_AVG = 10;
|
|
93
109
|
|
|
94
110
|
// Each position in a tuple emits its own branch inside the element loop, so a
|
|
95
111
|
// long one is both a lot of source and a linear test per element.
|
|
@@ -155,9 +171,19 @@ function resolveLocalRef(root, ref) {
|
|
|
155
171
|
if (ref === '#') return root;
|
|
156
172
|
if (!ref.startsWith('#/')) decline('$ref:not-local');
|
|
157
173
|
let node = root;
|
|
174
|
+
let first = true;
|
|
158
175
|
for (const rawToken of ref.slice(2).split('/')) {
|
|
159
|
-
|
|
176
|
+
let token = decodeURIComponent(rawToken).split('~1').join('/').split('~0').join('~');
|
|
160
177
|
if (node === null || typeof node !== 'object') decline('$ref:unresolved');
|
|
178
|
+
// Draft 7 schemas reach the scanner with `definitions` renamed to `$defs`
|
|
179
|
+
// (lib/draft7.js) and their references as written; the other engines
|
|
180
|
+
// read either name, and so does this. Without it no draft 7 schema that
|
|
181
|
+
// used a definition got a scanner.
|
|
182
|
+
if (first && node === root && !Array.isArray(node) && !Object.prototype.hasOwnProperty.call(node, token)) {
|
|
183
|
+
if (token === 'definitions' && Object.prototype.hasOwnProperty.call(node, '$defs')) token = '$defs';
|
|
184
|
+
else if (token === '$defs' && Object.prototype.hasOwnProperty.call(node, 'definitions')) token = 'definitions';
|
|
185
|
+
}
|
|
186
|
+
first = false;
|
|
161
187
|
if (Array.isArray(node)) {
|
|
162
188
|
if (!/^\d+$/.test(token)) decline('$ref:unresolved');
|
|
163
189
|
node = node[Number(token)];
|
|
@@ -387,11 +413,45 @@ function spanHash(name) {
|
|
|
387
413
|
// is a jump table.
|
|
388
414
|
const HASH_DISPATCH_MIN = 48;
|
|
389
415
|
|
|
416
|
+
// The scan is first emitted as one function with every reference expanded
|
|
417
|
+
// where it is used, which is the fastest form: a call per member measured
|
|
418
|
+
// about twice the cost of csslintrc's scan, a small enum referenced thirty
|
|
419
|
+
// times. Only when a function would pass MAX_FUNCTION_SOURCE is it emitted
|
|
420
|
+
// again with large reference targets and large object members moved into
|
|
421
|
+
// functions of their own, so the source grows with the schema rather than
|
|
422
|
+
// with the number of references to it.
|
|
390
423
|
function compileScanner(schema, options) {
|
|
391
424
|
const opts = options || {};
|
|
425
|
+
const whole = build(schema, opts, false);
|
|
426
|
+
if (whole !== null || declineReason !== 'size') return finish(whole, opts);
|
|
427
|
+
return finish(build(schema, opts, true), opts);
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
function finish(built, opts) {
|
|
431
|
+
if (built === null) {
|
|
432
|
+
if (opts.onDecline) opts.onDecline(declineReason);
|
|
433
|
+
return null;
|
|
434
|
+
}
|
|
435
|
+
const { ctx, body } = built;
|
|
436
|
+
let fn;
|
|
437
|
+
try {
|
|
438
|
+
// eslint-disable-next-line no-new-func
|
|
439
|
+
fn = new Function('_h', '_r', ctx.subSource.join('\n') + '\nreturn function scan(s){\n' + body + '\n}')(ctx.helpers, require('./scan-runtime'));
|
|
440
|
+
} catch {
|
|
441
|
+
return null; // a runtime without code generation has no scanner, by design
|
|
442
|
+
}
|
|
443
|
+
return { scan: fn, source: body, helpers: ctx.helpers.length, functions: [body, ...ctx.subSource] };
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
function build(schema, opts, split) {
|
|
392
447
|
const ctx = {
|
|
393
448
|
uid: 0, helpers: [], userFormats: opts.userFormats || null,
|
|
394
449
|
root: schema, refStack: [], nodes: 0,
|
|
450
|
+
// With `split`, reference targets map to null when expanded in place,
|
|
451
|
+
// else to the name of the function in subSource that scans one (see the
|
|
452
|
+
// $ref case in emitValue), and large members move out too (emitMember).
|
|
453
|
+
split, subs: new Map(), subSource: [], refCounts: split ? countRefs(schema) : null,
|
|
454
|
+
inlineBytes: new Map(),
|
|
395
455
|
};
|
|
396
456
|
declineReason = null;
|
|
397
457
|
let body;
|
|
@@ -404,21 +464,19 @@ function compileScanner(schema, options) {
|
|
|
404
464
|
out.push('if(i<len)return 0;');
|
|
405
465
|
out.push('return 1;');
|
|
406
466
|
body = out.join('\n');
|
|
467
|
+
// A function this large is a cost of its own, and V8's Maglev tier
|
|
468
|
+
// aborted the process compiling scanners of 500 KB and more (traefik,
|
|
469
|
+
// detekt from SchemaStore), in plain JavaScript, on Node 24 and 25, after
|
|
470
|
+
// about a hundred calls. Unsplit, past the bound the scan is emitted
|
|
471
|
+
// split; split, past either bound the schema has no scanner and the text
|
|
472
|
+
// path parses as it does without one.
|
|
473
|
+
if (body.length > MAX_FUNCTION_SOURCE || ctx.subSource.some((x) => x.length > MAX_FUNCTION_SOURCE)) decline('size');
|
|
474
|
+
if (body.length + ctx.subSource.reduce((n, x) => n + x.length, 0) > MAX_SOURCE) decline('size');
|
|
407
475
|
} catch (e) {
|
|
408
|
-
if (e === DECLINE)
|
|
409
|
-
if (opts.onDecline) opts.onDecline(declineReason);
|
|
410
|
-
return null;
|
|
411
|
-
}
|
|
476
|
+
if (e === DECLINE) return null;
|
|
412
477
|
throw e;
|
|
413
478
|
}
|
|
414
|
-
|
|
415
|
-
try {
|
|
416
|
-
// eslint-disable-next-line no-new-func
|
|
417
|
-
fn = new Function('_h', '_r', 'return function scan(s){\n' + body + '\n}')(ctx.helpers, require('./scan-runtime'));
|
|
418
|
-
} catch {
|
|
419
|
-
return null; // a runtime without code generation has no scanner, by design
|
|
420
|
-
}
|
|
421
|
-
return { scan: fn, source: body, helpers: ctx.helpers.length };
|
|
479
|
+
return { ctx, body };
|
|
422
480
|
}
|
|
423
481
|
|
|
424
482
|
function emitValue(schema, ctx, out, depth) {
|
|
@@ -443,9 +501,51 @@ function emitValue(schema, ctx, out, depth) {
|
|
|
443
501
|
const ref = schema.$ref;
|
|
444
502
|
if (ctx.refStack.includes(ref)) decline('$ref:recursive');
|
|
445
503
|
const target = resolveLocalRef(ctx.root, ref);
|
|
504
|
+
if (!ctx.split) {
|
|
505
|
+
ctx.refStack.push(ref);
|
|
506
|
+
emitValue(target, ctx, out, depth);
|
|
507
|
+
ctx.refStack.pop();
|
|
508
|
+
return;
|
|
509
|
+
}
|
|
510
|
+
// Split (see compileScanner): a target whose expansions add up to more
|
|
511
|
+
// than a little source is emitted once, as a function of its own, and
|
|
512
|
+
// called wherever it is referenced. Expanded at every reference, the
|
|
513
|
+
// source grows with the number of references rather than with the
|
|
514
|
+
// schema: SchemaStore's traefik and detekt reach the same definitions
|
|
515
|
+
// from hundreds of places, and their scanners came to 0.5 to 1 MB, which
|
|
516
|
+
// V8's Maglev tier aborted the process compiling.
|
|
517
|
+
const sub = ctx.subs.get(target);
|
|
518
|
+
if (sub !== undefined) {
|
|
519
|
+
if (sub === null) {
|
|
520
|
+
ctx.refStack.push(ref);
|
|
521
|
+
emitValue(target, ctx, out, depth);
|
|
522
|
+
ctx.refStack.pop();
|
|
523
|
+
} else {
|
|
524
|
+
emitSubCall(sub, ctx, out);
|
|
525
|
+
}
|
|
526
|
+
return;
|
|
527
|
+
}
|
|
528
|
+
const body = [];
|
|
446
529
|
ctx.refStack.push(ref);
|
|
447
|
-
emitValue(target, ctx,
|
|
530
|
+
emitValue(target, ctx, body, depth);
|
|
448
531
|
ctx.refStack.pop();
|
|
532
|
+
const text = body.join('\n');
|
|
533
|
+
// Expanded in place only while all its expansions together stay small:
|
|
534
|
+
// a small definition referenced from hundreds of places is source in
|
|
535
|
+
// hundreds of places.
|
|
536
|
+
if (text.length * (ctx.refCounts.get(ref) || 1) <= INLINE_REF_MAX) {
|
|
537
|
+
ctx.subs.set(target, null);
|
|
538
|
+
for (const line of body) out.push(line);
|
|
539
|
+
return;
|
|
540
|
+
}
|
|
541
|
+
const name = '_s' + (ctx.subSource.length + ctx.subs.size + ctx.uid++);
|
|
542
|
+
ctx.subs.set(target, name);
|
|
543
|
+
// The emitted scan of a value starts with `i` at its first character and
|
|
544
|
+
// `c` holding it, and ends with `i` past it; it answers 0 (invalid) or -1
|
|
545
|
+
// (bail) by returning. Here it returns the new position instead, which is
|
|
546
|
+
// at least 1, so the three cannot be confused.
|
|
547
|
+
ctx.subSource.push('function ' + name + '(s,i){var len=s.length,c=s.charCodeAt(i);\n' + text + '\nreturn i;}');
|
|
548
|
+
emitSubCall(name, ctx, out);
|
|
449
549
|
return;
|
|
450
550
|
}
|
|
451
551
|
|
|
@@ -478,6 +578,12 @@ function emitValue(schema, ctx, out, depth) {
|
|
|
478
578
|
// present, not that it lists anything.
|
|
479
579
|
let compositeAllowed = true;
|
|
480
580
|
let compositeDelegate = null;
|
|
581
|
+
// The kinds the listed values have. A value of any other kind equals none
|
|
582
|
+
// of them, so its branch is left out and the value falls to the final
|
|
583
|
+
// `return 0`: csslintrc's `enum: [2, true, false]` used to scan strings
|
|
584
|
+
// only to reject them, at thirty references, and the dead branches alone
|
|
585
|
+
// pushed its scan past the size bound into calls.
|
|
586
|
+
let listedKinds = null;
|
|
481
587
|
if (has('const') || has('enum')) {
|
|
482
588
|
compositeAllowed = false;
|
|
483
589
|
const listed = [];
|
|
@@ -486,6 +592,7 @@ function emitValue(schema, ctx, out, depth) {
|
|
|
486
592
|
if (!Array.isArray(schema.enum)) decline('enum:not-array');
|
|
487
593
|
for (const v of schema.enum) listed.push(v);
|
|
488
594
|
}
|
|
595
|
+
listedKinds = new Set(listed.map((v) => (v === null ? 'null' : Array.isArray(v) ? 'array' : typeof v)));
|
|
489
596
|
// An object or an array can only be compared as a whole. The scan steps
|
|
490
597
|
// over that one subtree, parses its own text, and hands it to the check
|
|
491
598
|
// the validator itself runs, so the document around it is still never
|
|
@@ -508,7 +615,7 @@ function emitValue(schema, ctx, out, depth) {
|
|
|
508
615
|
+ 'if(!' + compositeDelegate + '(_cv' + k + '))return 0;'
|
|
509
616
|
+ 'i=_ce' + k + ';c=s.charCodeAt(i);'];
|
|
510
617
|
};
|
|
511
|
-
if (allows(types, 'object')) {
|
|
618
|
+
if (allows(types, 'object') && (listedKinds === null || listedKinds.has('object'))) {
|
|
512
619
|
if (compositeDelegate) branches.push(['c===123', compositeBranch()]);
|
|
513
620
|
else if (!compositeAllowed) branches.push(['c===123', ['return 0;']]);
|
|
514
621
|
else {
|
|
@@ -517,7 +624,7 @@ function emitValue(schema, ctx, out, depth) {
|
|
|
517
624
|
branches.push(['c===123', b]);
|
|
518
625
|
}
|
|
519
626
|
}
|
|
520
|
-
if (allows(types, 'array')) {
|
|
627
|
+
if (allows(types, 'array') && (listedKinds === null || listedKinds.has('array'))) {
|
|
521
628
|
if (compositeDelegate) branches.push(['c===91', compositeBranch()]);
|
|
522
629
|
else if (!compositeAllowed) branches.push(['c===91', ['return 0;']]);
|
|
523
630
|
else {
|
|
@@ -526,22 +633,22 @@ function emitValue(schema, ctx, out, depth) {
|
|
|
526
633
|
branches.push(['c===91', b]);
|
|
527
634
|
}
|
|
528
635
|
}
|
|
529
|
-
if (allows(types, 'string')) {
|
|
636
|
+
if (allows(types, 'string') && (listedKinds === null || listedKinds.has('string'))) {
|
|
530
637
|
const b = [];
|
|
531
638
|
emitString(schema, ctx, b);
|
|
532
639
|
branches.push(['c===34', b]);
|
|
533
640
|
}
|
|
534
|
-
if (allows(types, 'number')) {
|
|
641
|
+
if (allows(types, 'number') && (listedKinds === null || listedKinds.has('number'))) {
|
|
535
642
|
const b = [];
|
|
536
643
|
emitNumber(schema, ctx, b, types);
|
|
537
644
|
branches.push(['c===45||(c>=48&&c<=57)', b]);
|
|
538
645
|
}
|
|
539
|
-
if (allows(types, 'boolean')) {
|
|
646
|
+
if (allows(types, 'boolean') && (listedKinds === null || listedKinds.has('boolean'))) {
|
|
540
647
|
const b = [];
|
|
541
648
|
emitLiteral(schema, ctx, b, 'boolean');
|
|
542
649
|
branches.push(['c===116||c===102', b]);
|
|
543
650
|
}
|
|
544
|
-
if (allows(types, 'null')) {
|
|
651
|
+
if (allows(types, 'null') && (listedKinds === null || listedKinds.has('null'))) {
|
|
545
652
|
const b = [];
|
|
546
653
|
emitLiteral(schema, ctx, b, 'null');
|
|
547
654
|
branches.push(['c===110', b]);
|
|
@@ -558,6 +665,42 @@ function emitValue(schema, ctx, out, depth) {
|
|
|
558
665
|
out.push(parts.join('\n'));
|
|
559
666
|
}
|
|
560
667
|
|
|
668
|
+
// How many times each `$ref` string occurs in the schema, for the expand or
|
|
669
|
+
// call decision. Schema positions and other values alike: a string that is not
|
|
670
|
+
// a reference is never looked up.
|
|
671
|
+
function countRefs(root) {
|
|
672
|
+
const counts = new Map();
|
|
673
|
+
const seen = new Set();
|
|
674
|
+
const walk = (n) => {
|
|
675
|
+
if (n === null || typeof n !== 'object' || seen.has(n)) return;
|
|
676
|
+
seen.add(n);
|
|
677
|
+
if (Array.isArray(n)) { for (const x of n) walk(x); return; }
|
|
678
|
+
if (typeof n.$ref === 'string') counts.set(n.$ref, (counts.get(n.$ref) || 0) + 1);
|
|
679
|
+
for (const k of Object.keys(n)) walk(n[k]);
|
|
680
|
+
};
|
|
681
|
+
walk(root);
|
|
682
|
+
return counts;
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
// A name with no character a JSON string would have to escape, so its raw
|
|
686
|
+
// text in a document is the name itself.
|
|
687
|
+
function plainName(name) {
|
|
688
|
+
for (let k = 0; k < name.length; k++) {
|
|
689
|
+
const c = name.charCodeAt(k);
|
|
690
|
+
if (c < 32 || c === 34 || c === 92) return false;
|
|
691
|
+
}
|
|
692
|
+
return true;
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
// A call answers invalid with the literal `return 0;`, as inline code does:
|
|
696
|
+
// an object member rewrites that text into its own failure path, which goes on
|
|
697
|
+
// scanning (a later duplicate key can replace the value), and a different
|
|
698
|
+
// spelling would end the whole scan instead.
|
|
699
|
+
function emitSubCall(name, ctx, out) {
|
|
700
|
+
const r = '_rq' + (ctx.uid++);
|
|
701
|
+
out.push('{var ' + r + '=' + name + '(s,i);if(' + r + '===-1)return -1;if(' + r + '===0)return 0;i=' + r + ';c=s.charCodeAt(i);}');
|
|
702
|
+
}
|
|
703
|
+
|
|
561
704
|
// Any JSON value, validated as JSON and stepped over.
|
|
562
705
|
function emitSkip(out) {
|
|
563
706
|
out.push('{var _j=_r.skipValue(s,i);if(_j===-2)return -1;if(_j<0)return 0;i=_j;c=s.charCodeAt(i);}');
|
|
@@ -749,8 +892,27 @@ function emitArray(schema, ctx, out, depth) {
|
|
|
749
892
|
// the rule reaches nested objects: an inner object's own verdict is a value
|
|
750
893
|
// failure at the level above it.
|
|
751
894
|
function emitMember(sub, ctx, out, depth, objId) {
|
|
752
|
-
|
|
895
|
+
let lines = [];
|
|
753
896
|
emitValue(sub, ctx, lines, depth + 1);
|
|
897
|
+
// A large member value becomes a function of its own, so no one function
|
|
898
|
+
// grows with the whole schema: detekt nests hundreds of rule objects inline,
|
|
899
|
+
// no references involved. The value's code keeps its own labels and
|
|
900
|
+
// variables, so it moves as it is; the call answers invalid with
|
|
901
|
+
// `return 0;`, which the rewrite below turns into this member's failure.
|
|
902
|
+
// An object's members are judged together as well as one by one: thirty
|
|
903
|
+
// members just under SPLIT_MIN each would stay inline and add up to a
|
|
904
|
+
// function past the bound, so once the members kept inline in this object
|
|
905
|
+
// pass SPLIT_OBJECT, the rest move out whatever their size.
|
|
906
|
+
const size = lines.join('\n').length;
|
|
907
|
+
const kept = ctx.split ? (ctx.inlineBytes.get(objId) || 0) : 0;
|
|
908
|
+
if (ctx.split && (size > SPLIT_MIN || kept + size > SPLIT_OBJECT)) {
|
|
909
|
+
const name = '_s' + (ctx.subSource.length + ctx.subs.size + ctx.uid++);
|
|
910
|
+
ctx.subSource.push('function ' + name + '(s,i){var len=s.length,c=s.charCodeAt(i);\n' + lines.join('\n') + '\nreturn i;}');
|
|
911
|
+
lines = [];
|
|
912
|
+
emitSubCall(name, ctx, lines);
|
|
913
|
+
} else if (ctx.split) {
|
|
914
|
+
ctx.inlineBytes.set(objId, kept + size);
|
|
915
|
+
}
|
|
754
916
|
const body = lines.join('\n');
|
|
755
917
|
if (!body.includes('return 0;')) { out.push(body); return; }
|
|
756
918
|
const label = '_mb' + (ctx.uid++);
|
|
@@ -828,10 +990,32 @@ function emitObject(schema, ctx, out, depth) {
|
|
|
828
990
|
hashById = '_h[' + (ctx.helpers.length - 1) + ']';
|
|
829
991
|
}
|
|
830
992
|
}
|
|
831
|
-
|
|
832
|
-
//
|
|
833
|
-
//
|
|
834
|
-
|
|
993
|
+
// A key's end is found with indexOf, which is native, where the character
|
|
994
|
+
// loop below costs three compares a character: on csslintrc's 35 keys of
|
|
995
|
+
// twenty-odd characters the loop was most of the scan and made it slower
|
|
996
|
+
// than parsing. Every declared name is plain text, so a key that compares
|
|
997
|
+
// equal to one, character by character, is that name as written, and a
|
|
998
|
+
// key that does not is checked for an escape or a control character before
|
|
999
|
+
// it is read as additional. Either one bails, and so does a span that ends
|
|
1000
|
+
// on a backslash, since that quote may be escaped: the parser decides what
|
|
1001
|
+
// the key really is.
|
|
1002
|
+
// indexOf costs about the same whatever the key's length, the loop about a
|
|
1003
|
+
// nanosecond a character: on short keys the loop wins (problem-object's
|
|
1004
|
+
// `type`, `title`, `status` measured 10 to 15 percent slower by indexOf),
|
|
1005
|
+
// and from ten characters indexOf wins by 40 percent and more, so the
|
|
1006
|
+
// choice follows the declared names' average length.
|
|
1007
|
+
const indexKey = !useHash && names.length > 0 && names.every(plainName)
|
|
1008
|
+
&& names.reduce((n, x) => n + x.length, 0) >= INDEX_KEY_MIN_AVG * names.length;
|
|
1009
|
+
if (indexKey) {
|
|
1010
|
+
// A quote with a backslash before it may be escaped, and then the key
|
|
1011
|
+
// goes on past it: bail before reading what follows as the colon.
|
|
1012
|
+
out.push('var _p' + kn + '=++i,_e' + kn + '=s.indexOf(\'"\',i);if(_e' + kn + '<0)return 0;if(s.charCodeAt(_e' + kn + '-1)===92)return -1;i=_e' + kn + ';c=s.charCodeAt(++i);');
|
|
1013
|
+
} else {
|
|
1014
|
+
emitStringSpan(ctx, out, kn, false, hashById !== null);
|
|
1015
|
+
// A key written with an escape decodes to a name the raw span does not
|
|
1016
|
+
// show, so the dispatch below would read it as a different property.
|
|
1017
|
+
out.push('if(_x' + kn + ')return -1;');
|
|
1018
|
+
}
|
|
835
1019
|
out.push(WS);
|
|
836
1020
|
out.push('if(c!==58)return 0;');
|
|
837
1021
|
out.push('c=s.charCodeAt(++i);');
|
|
@@ -958,6 +1142,7 @@ function emitObject(schema, ctx, out, depth) {
|
|
|
958
1142
|
additional.push(lines.join('\n').split('return 0;').join('return -1;'));
|
|
959
1143
|
}
|
|
960
1144
|
|
|
1145
|
+
if (indexKey) additional.unshift('if(!_r.plainKey(s,_p' + kn + ',_e' + kn + '))return -1;');
|
|
961
1146
|
out.push('switch(_kl' + kn + '){');
|
|
962
1147
|
for (const [len, group] of byLen) {
|
|
963
1148
|
out.push('case ' + len + ':{');
|
package/lib/scan-runtime.js
CHANGED
|
@@ -155,4 +155,16 @@ function span(s, p, e, escaped) {
|
|
|
155
155
|
return JSON.parse(s.slice(p - 1, e + 1));
|
|
156
156
|
}
|
|
157
157
|
|
|
158
|
-
|
|
158
|
+
// Whether the raw text of a key between `p` and `e` is its name as written:
|
|
159
|
+
// no escape, no control character. A scanner that found the key's end with
|
|
160
|
+
// indexOf calls this for a name it did not recognise; one it recognised was
|
|
161
|
+
// compared character by character against a name that has neither.
|
|
162
|
+
function plainKey(s, p, e) {
|
|
163
|
+
for (let k = p; k < e; k++) {
|
|
164
|
+
const c = s.charCodeAt(k);
|
|
165
|
+
if (c < 32 || c === 92) return false;
|
|
166
|
+
}
|
|
167
|
+
return true;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
module.exports = { hex4, skipValue, skipString, skipNumber, span, plainKey, MALFORMED, TOO_DEEP, MAX_SKIP_DEPTH };
|