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.
@@ -1,7 +1,14 @@
1
1
  'use strict';
2
2
 
3
- // Bounded Levenshtein. Returns Infinity if distance > maxDistance.
4
- // Single-row DP. O(n*m) worst case but typical strings are <30 chars.
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
- if (Math.abs(a.length - b.length) > max) return Infinity;
16
- if (a.length === 0) return b.length;
17
- if (b.length === 0) return a.length;
18
- if (scratchA.length < b.length + 1) {
19
- scratchA = new Int32Array(b.length + 1);
20
- scratchB = new Int32Array(b.length + 1);
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
- const bn = b.length;
25
- for (let j = 0; j <= bn; j++) prev[j] = j;
26
- for (let i = 1; i <= a.length; i++) {
27
- curr[0] = i;
28
- let rowMin = i;
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 = 1; j <= bn; j++) {
33
- const cost = ca === b.charCodeAt(j - 1) ? 0 : 1;
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 < m) m = del;
46
+ if (del < v) v = del;
37
47
  const ins = prev[j] + 1;
38
- if (ins < m) m = ins;
39
- curr[j] = m;
40
- if (m < rowMin) rowMin = m;
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[bn];
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
- constructor(result, jsonStr, self, enrich) {
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
- try { parsedData = JSON.parse(jsonStr); } catch { parsedData = undefined; }
164
- // What was checked is the document after defaults, coercion and removal,
165
- // so `received` has to come from that, as it does for validate(). A
166
- // default that failed its own schema has no value in the text at all.
167
- if (parsedData !== undefined && self._preprocess) self._preprocess(parsedData);
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
@@ -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
- const token = decodeURIComponent(rawToken).split('~1').join('/').split('~0').join('~');
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
- let fn;
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, out, depth);
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
- const lines = [];
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
- emitStringSpan(ctx, out, kn, false, hashById !== null);
832
- // A key written with an escape decodes to a name the raw span does not show,
833
- // so the dispatch below would read it as a different property.
834
- out.push('if(_x' + kn + ')return -1;');
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 + ':{');
@@ -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
- module.exports = { hex4, skipValue, skipString, skipNumber, span, MALFORMED, TOO_DEEP, MAX_SKIP_DEPTH };
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 };