ata-validator 1.44.0 → 1.46.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/bin/ata.js +16 -0
- package/index.js +73 -12
- package/lib/branch-collapse.js +1 -1
- package/lib/combined-runtime.js +149 -0
- package/lib/formats.js +8 -2
- package/lib/interpreter.js +45 -52
- package/lib/js-compiler.js +1124 -91
- package/lib/migrate.js +253 -0
- package/lib/plan-compiler.js +23 -25
- package/lib/regex-linear.js +35 -26
- package/lib/unique-items.js +139 -0
- package/lib/validator-core.js +26 -4
- package/lib/version.js +1 -1
- package/package.json +9 -9
package/lib/migrate.js
ADDED
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// `ata migrate`: what switching a project from ajv to ata-validator/compat
|
|
4
|
+
// would change, and what it cannot translate. The report comes first and
|
|
5
|
+
// touches nothing; `--write` applies only the two mechanical rewrites (the
|
|
6
|
+
// import specifier, and the `addFormats()` call compat refuses on purpose),
|
|
7
|
+
// so a site the report flags is never changed behind the reader's back.
|
|
8
|
+
//
|
|
9
|
+
// Sources are read through a small masker that blanks comments and the
|
|
10
|
+
// insides of string and template literals, so a specifier is matched only
|
|
11
|
+
// where it is code: `require('ajv')` in a comment or in a longer string is
|
|
12
|
+
// not an import. Regular expression literals are not recognised; a pattern
|
|
13
|
+
// such as /from 'ajv'/ would be read as code, which only ever adds a line to
|
|
14
|
+
// the report, never a rewrite the report did not show.
|
|
15
|
+
|
|
16
|
+
const fs = require('node:fs');
|
|
17
|
+
const path = require('node:path');
|
|
18
|
+
|
|
19
|
+
const SOURCE_EXT = new Set(['.js', '.mjs', '.cjs', '.jsx', '.ts', '.mts', '.cts', '.tsx']);
|
|
20
|
+
const SKIP_DIRS = new Set(['node_modules', '.git', 'dist', 'build', 'coverage', '.next', '.nuxt', 'out', '.turbo', '.cache']);
|
|
21
|
+
|
|
22
|
+
// Module specifiers and what each means for the switch.
|
|
23
|
+
const SPECIFIERS = {
|
|
24
|
+
ajv: { kind: 'rewrite', to: 'ata-validator/compat' },
|
|
25
|
+
'ajv/dist/ajv': { kind: 'rewrite', to: 'ata-validator/compat' },
|
|
26
|
+
'ajv/dist/2020': { kind: 'rewrite-note', to: 'ata-validator/compat', note: 'compat reads a schema with no $schema as draft-07, where this import read it as Draft 2020-12: give each such schema a $schema, or keep this import until it has one' },
|
|
27
|
+
'ajv/dist/2019': { kind: 'rewrite-note', to: 'ata-validator/compat', note: 'compat reads a schema with no $schema as draft-07, where this import read it as Draft 2019-09, which ata does not implement: give each such schema a Draft 2020-12 or draft-07 $schema first' },
|
|
28
|
+
'ajv/dist/jtd': { kind: 'refuse', note: 'JSON Type Definition is not JSON Schema; ata has no equivalent' },
|
|
29
|
+
'ajv-formats': { kind: 'delete-import', note: 'the formats this plugin adds are built into ata; the import and the addFormats() call go' },
|
|
30
|
+
'ajv-errors': { kind: 'refuse', note: 'ata reads the errorMessage keyword natively (docs/migration-from-ajv.md, "Error UX"); the plugin call must be removed by hand and the keyword stays as it is' },
|
|
31
|
+
'ajv-keywords': { kind: 'refuse', note: 'not supported; each keyword used needs a definition through addKeyword with validate, compile or macro' },
|
|
32
|
+
'ajv-i18n': { kind: 'refuse', note: 'not supported; messages are English' },
|
|
33
|
+
'ajv-merge-patch': { kind: 'refuse', note: '$merge and $patch are not supported' },
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
// Masks comments and string contents with spaces, keeping every index and
|
|
37
|
+
// line break where it was, so matches in the mask map back to the source.
|
|
38
|
+
function mask (src) {
|
|
39
|
+
const out = src.split('');
|
|
40
|
+
const n = src.length;
|
|
41
|
+
let i = 0;
|
|
42
|
+
const blank = (from, to) => { for (let k = from; k < to; k++) if (out[k] !== '\n') out[k] = ' '; };
|
|
43
|
+
while (i < n) {
|
|
44
|
+
const c = src[i];
|
|
45
|
+
const d = src[i + 1];
|
|
46
|
+
if (c === '/' && d === '/') {
|
|
47
|
+
let j = i;
|
|
48
|
+
while (j < n && src[j] !== '\n') j++;
|
|
49
|
+
blank(i, j); i = j; continue;
|
|
50
|
+
}
|
|
51
|
+
if (c === '/' && d === '*') {
|
|
52
|
+
let j = src.indexOf('*/', i + 2);
|
|
53
|
+
if (j < 0) j = n; else j += 2;
|
|
54
|
+
blank(i, j); i = j; continue;
|
|
55
|
+
}
|
|
56
|
+
if (c === '"' || c === "'" || c === '`') {
|
|
57
|
+
let j = i + 1;
|
|
58
|
+
while (j < n) {
|
|
59
|
+
if (src[j] === '\\') { j += 2; continue; }
|
|
60
|
+
if (src[j] === c) break;
|
|
61
|
+
if (c !== '`' && src[j] === '\n') break;
|
|
62
|
+
j++;
|
|
63
|
+
}
|
|
64
|
+
blank(i + 1, j); i = j + 1; continue;
|
|
65
|
+
}
|
|
66
|
+
i++;
|
|
67
|
+
}
|
|
68
|
+
return out.join('');
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function lineOf (src, index) {
|
|
72
|
+
let line = 1;
|
|
73
|
+
for (let i = 0; i < index; i++) if (src.charCodeAt(i) === 10) line++;
|
|
74
|
+
return line;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Every module specifier in `src`: the string following `from`, inside
|
|
78
|
+
// `require(` or `import(`, and in a bare `import '...'`. Returns the span of
|
|
79
|
+
// the string contents, so a rewrite replaces only the specifier.
|
|
80
|
+
function specifiers (src) {
|
|
81
|
+
const m = mask(src);
|
|
82
|
+
const found = [];
|
|
83
|
+
const re = /(\bfrom\s*|\brequire\s*\(\s*|\bimport\s*\(\s*|\bimport\s+)(['"])/g;
|
|
84
|
+
let x;
|
|
85
|
+
while ((x = re.exec(m)) !== null) {
|
|
86
|
+
const quote = x[2];
|
|
87
|
+
const start = x.index + x[0].length;
|
|
88
|
+
const end = src.indexOf(quote, start);
|
|
89
|
+
if (end < 0) continue;
|
|
90
|
+
found.push({ start, end, value: src.slice(start, end), line: lineOf(src, start) });
|
|
91
|
+
}
|
|
92
|
+
return found;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// `addFormats(x)` call statements, with the span to delete when the call is a
|
|
96
|
+
// statement on its own.
|
|
97
|
+
function addFormatsCalls (src) {
|
|
98
|
+
const m = mask(src);
|
|
99
|
+
const calls = [];
|
|
100
|
+
const re = /\baddFormats\s*\(/g;
|
|
101
|
+
let x;
|
|
102
|
+
while ((x = re.exec(m)) !== null) {
|
|
103
|
+
let depth = 1;
|
|
104
|
+
let j = x.index + x[0].length;
|
|
105
|
+
while (j < m.length && depth > 0) { if (m[j] === '(') depth++; else if (m[j] === ')') depth--; j++; }
|
|
106
|
+
let end = j;
|
|
107
|
+
if (m[end] === ';') end++;
|
|
108
|
+
// A statement of its own: only whitespace between the previous line
|
|
109
|
+
// break and the call, and between the call and the next one.
|
|
110
|
+
let ls = x.index;
|
|
111
|
+
while (ls > 0 && m[ls - 1] !== '\n') ls--;
|
|
112
|
+
let le = end;
|
|
113
|
+
while (le < m.length && m[le] !== '\n') le++;
|
|
114
|
+
const alone = /^\s*$/.test(m.slice(ls, x.index)) && /^\s*$/.test(m.slice(end, le));
|
|
115
|
+
calls.push({ start: x.index, end, lineStart: ls, lineEnd: le < m.length ? le + 1 : le, alone, line: lineOf(src, x.index) });
|
|
116
|
+
}
|
|
117
|
+
return calls;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// `$data` as an object key, bare or quoted. Read from the source rather than
|
|
121
|
+
// the mask, since a quoted key is a string the mask blanks.
|
|
122
|
+
function usesDollarData (src) {
|
|
123
|
+
const re = /(['"])\$data\1\s*:|\$data\s*:/g;
|
|
124
|
+
const lines = [];
|
|
125
|
+
let x;
|
|
126
|
+
while ((x = re.exec(src)) !== null) lines.push(lineOf(src, x.index));
|
|
127
|
+
return [...new Set(lines)];
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function walk (dir, files) {
|
|
131
|
+
let entries;
|
|
132
|
+
try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { return files; }
|
|
133
|
+
for (const e of entries) {
|
|
134
|
+
if (e.isDirectory()) { if (!SKIP_DIRS.has(e.name)) walk(path.join(dir, e.name), files); continue; }
|
|
135
|
+
if (e.isFile() && SOURCE_EXT.has(path.extname(e.name))) files.push(path.join(dir, e.name));
|
|
136
|
+
}
|
|
137
|
+
return files;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// One file. Returns null when the file mentions nothing of ajv. A file that
|
|
141
|
+
// already imports ata-validator/compat is read too: what compat refuses
|
|
142
|
+
// ($data, a code keyword, addFormats) is still a finding there, so a second
|
|
143
|
+
// run after --write, or a CI check, keeps reporting it until it is fixed.
|
|
144
|
+
const COMPAT = 'ata-validator/compat';
|
|
145
|
+
function inspectFile (file, src) {
|
|
146
|
+
const specs = specifiers(src).filter((s) => s.value === 'ajv' || s.value.startsWith('ajv/') || s.value.startsWith('ajv-') || s.value === COMPAT);
|
|
147
|
+
if (specs.length === 0) return null;
|
|
148
|
+
const findings = [];
|
|
149
|
+
for (const s of specs) {
|
|
150
|
+
if (s.value === COMPAT) continue;
|
|
151
|
+
const rule = SPECIFIERS[s.value];
|
|
152
|
+
if (!rule) {
|
|
153
|
+
findings.push({ line: s.line, kind: 'flag', text: `import of "${s.value}": no translation known; check docs/migration-from-ajv.md` });
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
if (rule.kind === 'rewrite') findings.push({ line: s.line, kind: 'rewrite', text: `"${s.value}" becomes "${rule.to}"`, span: s, to: rule.to });
|
|
157
|
+
else if (rule.kind === 'rewrite-note') findings.push({ line: s.line, kind: 'rewrite', text: `"${s.value}" becomes "${rule.to}"; ${rule.note}`, span: s, to: rule.to, note: true });
|
|
158
|
+
else if (rule.kind === 'delete-import') findings.push({ line: s.line, kind: 'delete', text: `import of "${s.value}" goes: ${rule.note}`, span: s, deleteImport: true });
|
|
159
|
+
else findings.push({ line: s.line, kind: 'flag', text: `import of "${s.value}": ${rule.note}` });
|
|
160
|
+
}
|
|
161
|
+
const usesAjv = specs.some((s) => s.value === 'ajv' || s.value.startsWith('ajv/') || s.value === COMPAT);
|
|
162
|
+
if (usesAjv) {
|
|
163
|
+
for (const c of addFormatsCalls(src)) {
|
|
164
|
+
if (c.alone) findings.push({ line: c.line, kind: 'delete', text: 'addFormats() call goes: these formats are built in, and compat refuses the call', call: c });
|
|
165
|
+
else findings.push({ line: c.line, kind: 'flag', text: 'addFormats() is part of a larger statement; remove it by hand' });
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
if (usesAjv) {
|
|
169
|
+
for (const line of usesDollarData(src)) findings.push({ line, kind: 'flag', text: '$data reference: compat refuses $data; rewrite the constraint without it' });
|
|
170
|
+
const m = mask(src);
|
|
171
|
+
const kw = /\.addKeyword\s*\(/g;
|
|
172
|
+
let x;
|
|
173
|
+
while ((x = kw.exec(m)) !== null) {
|
|
174
|
+
// The definition object follows; a `code:` key in it is the form compat
|
|
175
|
+
// cannot take. Looked for up to the matching parenthesis.
|
|
176
|
+
let depth = 1; let j = x.index + x[0].length;
|
|
177
|
+
while (j < m.length && depth > 0) { if (m[j] === '(') depth++; else if (m[j] === ')') depth--; j++; }
|
|
178
|
+
if (/\bcode\s*[:(]/.test(m.slice(x.index, j))) findings.push({ line: lineOf(src, x.index), kind: 'flag', text: 'addKeyword with a code-generating definition: compat takes validate, compile or macro only' });
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
if (findings.length === 0) return null;
|
|
182
|
+
findings.sort((a, b) => a.line - b.line);
|
|
183
|
+
return findings;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// The source with the mechanical rewrites applied: specifiers replaced,
|
|
187
|
+
// standalone addFormats() statements removed, and the ajv-formats import
|
|
188
|
+
// line removed when its binding has no other use.
|
|
189
|
+
function rewriteFile (src, findings) {
|
|
190
|
+
const edits = [];
|
|
191
|
+
for (const f of findings) {
|
|
192
|
+
if (f.kind === 'rewrite') edits.push({ start: f.span.start, end: f.span.end, text: f.to });
|
|
193
|
+
else if (f.kind === 'delete' && f.call) edits.push({ start: f.call.lineStart, end: f.call.lineEnd, text: '' });
|
|
194
|
+
else if (f.kind === 'delete' && f.deleteImport) {
|
|
195
|
+
let ls = f.span.start; while (ls > 0 && src[ls - 1] !== '\n') ls--;
|
|
196
|
+
let le = f.span.end; while (le < src.length && src[le] !== '\n') le++;
|
|
197
|
+
if (le < src.length) le++;
|
|
198
|
+
const stmt = src.slice(ls, le);
|
|
199
|
+
// Only a whole import or require statement on its own line.
|
|
200
|
+
if (/^\s*(import\b[^;\n]*from\s*['"]ajv-formats['"]\s*;?\s*\n?|(const|let|var)\s+\w+\s*=\s*require\s*\(\s*['"]ajv-formats['"]\s*\)\s*;?\s*\n?)$/.test(stmt)) edits.push({ start: ls, end: le, text: '' });
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
edits.sort((a, b) => b.start - a.start);
|
|
204
|
+
let out = src;
|
|
205
|
+
for (const e of edits) out = out.slice(0, e.start) + e.text + out.slice(e.end);
|
|
206
|
+
return out;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function migrate (dir, opts = {}) {
|
|
210
|
+
const root = path.resolve(dir || '.');
|
|
211
|
+
const files = walk(root, []);
|
|
212
|
+
const report = [];
|
|
213
|
+
let flagged = 0;
|
|
214
|
+
let written = 0;
|
|
215
|
+
for (const file of files) {
|
|
216
|
+
let src;
|
|
217
|
+
try { src = fs.readFileSync(file, 'utf8'); } catch { continue; }
|
|
218
|
+
const findings = inspectFile(file, src);
|
|
219
|
+
if (findings === null) continue;
|
|
220
|
+
// Forward slashes on every platform: the path is for people to read and
|
|
221
|
+
// paste, and Windows gave `src\\plain.js` where the report and its test
|
|
222
|
+
// name `src/plain.js`.
|
|
223
|
+
report.push({ file: path.relative(root, file).split(path.sep).join('/'), findings });
|
|
224
|
+
flagged += findings.filter((f) => f.kind === 'flag').length;
|
|
225
|
+
if (opts.write && findings.some((f) => f.kind !== 'flag')) {
|
|
226
|
+
const out = rewriteFile(src, findings);
|
|
227
|
+
if (out !== src) { fs.writeFileSync(file, out); written++; }
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
return { root, scanned: files.length, report, flagged, written };
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function formatReport (result, opts = {}) {
|
|
234
|
+
const lines = [];
|
|
235
|
+
if (result.report.length === 0) {
|
|
236
|
+
lines.push(`no use of ajv found in ${result.scanned} source files under ${result.root}`);
|
|
237
|
+
return lines.join('\n');
|
|
238
|
+
}
|
|
239
|
+
const label = { rewrite: 'rewrite', delete: 'delete ', flag: 'flag ' };
|
|
240
|
+
for (const r of result.report) {
|
|
241
|
+
lines.push(r.file);
|
|
242
|
+
for (const f of r.findings) lines.push(` ${label[f.kind]} line ${String(f.line).padStart(4)} ${f.text}`);
|
|
243
|
+
}
|
|
244
|
+
const rewrites = result.report.reduce((n, r) => n + r.findings.filter((f) => f.kind !== 'flag').length, 0);
|
|
245
|
+
lines.push('');
|
|
246
|
+
lines.push(`${result.report.length} of ${result.scanned} source files use ajv: ${rewrites} mechanical change${rewrites === 1 ? '' : 's'}, ${result.flagged} site${result.flagged === 1 ? '' : 's'} to look at by hand`);
|
|
247
|
+
if (opts.write) lines.push(`${result.written} file${result.written === 1 ? '' : 's'} written; flagged sites were left as they are`);
|
|
248
|
+
else if (rewrites > 0) lines.push('nothing was changed; run again with --write to apply the mechanical changes');
|
|
249
|
+
if (result.flagged > 0) lines.push('each flagged site is explained in docs/migration-from-ajv.md');
|
|
250
|
+
return lines.join('\n');
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
module.exports = { migrate, formatReport, inspectFile, rewriteFile, mask, specifiers, SPECIFIERS };
|
package/lib/plan-compiler.js
CHANGED
|
@@ -52,12 +52,16 @@ const branchTitle = (n) => (n && n.schema && typeof n.schema.title === 'string'
|
|
|
52
52
|
|
|
53
53
|
function install(deps) {
|
|
54
54
|
const { Plan, NOERRORS, err, evalLeaf, evalLeafV, runCustomV, dataBits, escapePointer,
|
|
55
|
-
deepEqual, multipleOfOk, cpAtLeast, cpAtMost,
|
|
55
|
+
deepEqual, enumMember, allDistinct, multipleOfOk, cpAtLeast, cpAtMost,
|
|
56
56
|
T_STRING, T_ARRAY, T_OBJECT, resolveRef, splitFragment, resolveUri } = deps;
|
|
57
57
|
|
|
58
58
|
const TRUE_FN = () => true;
|
|
59
|
-
|
|
59
|
+
// `leaf`: the node compiled no subschema of its own (see compileNode). A
|
|
60
|
+
// collecting caller asks a leaf for its verdict first and builds the paths
|
|
61
|
+
// for its errors only when it fails.
|
|
62
|
+
const TRUE_PAIR = { v: TRUE_FN, c: TRUE_FN, leaf: true };
|
|
60
63
|
const FALSE_PAIR = {
|
|
64
|
+
leaf: true,
|
|
61
65
|
v: () => false,
|
|
62
66
|
c: (data, errors, instancePath, schemaPath) => {
|
|
63
67
|
if (errors !== NOERRORS) errors.push(err('false schema', 'not', instancePath, schemaPath, {}, 'boolean schema is false'));
|
|
@@ -164,12 +168,8 @@ function install(deps) {
|
|
|
164
168
|
checks.push((data, bits) => (bits & mask) !== 0);
|
|
165
169
|
}
|
|
166
170
|
if (P.enum !== null) {
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
else checks.push((data) => {
|
|
170
|
-
for (let i = 0; i < vals.length; i++) if (deepEqual(vals[i], data)) return true;
|
|
171
|
-
return false;
|
|
172
|
-
});
|
|
171
|
+
if (P.enum.length === 0) checks.push(() => false);
|
|
172
|
+
else checks.push((data) => enumMember(P, data));
|
|
173
173
|
}
|
|
174
174
|
if (P.hasConst) {
|
|
175
175
|
const c = P.const;
|
|
@@ -205,14 +205,7 @@ function install(deps) {
|
|
|
205
205
|
const arrs = [];
|
|
206
206
|
if (P.minItems !== undefined) { const m = P.minItems; arrs.push((d) => d.length >= m); }
|
|
207
207
|
if (P.maxItems !== undefined) { const m = P.maxItems; arrs.push((d) => d.length <= m); }
|
|
208
|
-
if (P.uniqueItems)
|
|
209
|
-
arrs.push((d) => {
|
|
210
|
-
for (let i = 0; i < d.length; i++) {
|
|
211
|
-
for (let j = i + 1; j < d.length; j++) if (deepEqual(d[i], d[j])) return false;
|
|
212
|
-
}
|
|
213
|
-
return true;
|
|
214
|
-
});
|
|
215
|
-
}
|
|
208
|
+
if (P.uniqueItems) arrs.push(allDistinct);
|
|
216
209
|
const run = combineValue(arrs);
|
|
217
210
|
if (run !== null) { needsBits = true; checks.push((data, bits) => bits !== T_ARRAY || run(data)); }
|
|
218
211
|
}
|
|
@@ -300,6 +293,7 @@ function install(deps) {
|
|
|
300
293
|
const box = { v: null, c: null };
|
|
301
294
|
const pair = {
|
|
302
295
|
open: true,
|
|
296
|
+
leaf: false,
|
|
303
297
|
v: (d, st, rec) => box.v(d, st, rec),
|
|
304
298
|
c: (d, e, ip, sp, st, rec) => box.c(d, e, ip, sp, st, rec),
|
|
305
299
|
};
|
|
@@ -308,8 +302,11 @@ function install(deps) {
|
|
|
308
302
|
// In-place children collect when this node does; child applicators
|
|
309
303
|
// never do. `rec` is threaded as the trailing argument of every step
|
|
310
304
|
// and is undefined on non-collecting nodes, where nothing reads it.
|
|
311
|
-
|
|
312
|
-
|
|
305
|
+
// Whether this node compiles any subschema: a node that does not is a
|
|
306
|
+
// leaf, whose verdict is a few comparisons.
|
|
307
|
+
let nested = false;
|
|
308
|
+
const inPlace = (n, b) => { nested = true; return compileNode(ctx, n, b, chain, collect); };
|
|
309
|
+
const child = (n) => { nested = true; return compileNode(ctx, n, base, chain, false); };
|
|
313
310
|
|
|
314
311
|
const steps = []; // collect variant: (data, errors, ip, sp, stack, rec)
|
|
315
312
|
const vsteps = []; // verdict variant: (data, stack, rec)
|
|
@@ -399,7 +396,7 @@ function install(deps) {
|
|
|
399
396
|
let ok = true;
|
|
400
397
|
const n = Math.min(fns.length, data.length);
|
|
401
398
|
for (let i = 0; i < n; i++) {
|
|
402
|
-
if (!fns[i].c(data[i], errors, instancePath + '/' + i, schemaPath + '/prefixItems/' + i, stack)) {
|
|
399
|
+
if (!((fns[i].leaf && fns[i].v(data[i], stack)) || fns[i].c(data[i], errors, instancePath + '/' + i, schemaPath + '/prefixItems/' + i, stack))) {
|
|
403
400
|
ok = false;
|
|
404
401
|
if (errors === NOERRORS) return false;
|
|
405
402
|
}
|
|
@@ -422,7 +419,7 @@ function install(deps) {
|
|
|
422
419
|
if (dataBits(data) !== T_ARRAY) return true;
|
|
423
420
|
let ok = true;
|
|
424
421
|
for (let i = start; i < data.length; i++) {
|
|
425
|
-
if (!fn.c(data[i], errors, instancePath + '/' + i, schemaPath + '/items', stack)) {
|
|
422
|
+
if (!((fn.leaf && fn.v(data[i], stack)) || fn.c(data[i], errors, instancePath + '/' + i, schemaPath + '/items', stack))) {
|
|
426
423
|
ok = false;
|
|
427
424
|
if (errors === NOERRORS) return false;
|
|
428
425
|
}
|
|
@@ -536,7 +533,7 @@ function install(deps) {
|
|
|
536
533
|
let evaluated = false;
|
|
537
534
|
const prop = lookup !== null ? lookup(key) : undefined;
|
|
538
535
|
if (prop !== undefined) {
|
|
539
|
-
if (!prop.fn.c(data[key], errors, instancePath + prop.seg, schemaPath + prop.schemaSeg, stack)) {
|
|
536
|
+
if (!((prop.fn.leaf && prop.fn.v(data[key], stack)) || prop.fn.c(data[key], errors, instancePath + prop.seg, schemaPath + prop.schemaSeg, stack))) {
|
|
540
537
|
ok = false;
|
|
541
538
|
if (errors === NOERRORS) return false;
|
|
542
539
|
}
|
|
@@ -546,7 +543,7 @@ function install(deps) {
|
|
|
546
543
|
for (let pi = 0; pi < patterns.length; pi++) {
|
|
547
544
|
const pp = patterns[pi];
|
|
548
545
|
if (pp.re.test(key)) {
|
|
549
|
-
if (!pp.fn.c(data[key], errors, instancePath + '/' + escapePointer(key), schemaPath + '/patternProperties/' + escapePointer(pp.src), stack)) {
|
|
546
|
+
if (!((pp.fn.leaf && pp.fn.v(data[key], stack)) || pp.fn.c(data[key], errors, instancePath + '/' + escapePointer(key), schemaPath + '/patternProperties/' + escapePointer(pp.src), stack))) {
|
|
550
547
|
ok = false;
|
|
551
548
|
if (errors === NOERRORS) return false;
|
|
552
549
|
}
|
|
@@ -559,7 +556,7 @@ function install(deps) {
|
|
|
559
556
|
ok = false;
|
|
560
557
|
if (errors === NOERRORS) return false;
|
|
561
558
|
errors.push(err('additionalProperties', 'additionalProperties', instancePath, schemaPath + '/additionalProperties', { additionalProperty: key }, 'must NOT have additional properties'));
|
|
562
|
-
} else if (!apFn.c(data[key], errors, instancePath + '/' + escapePointer(key), schemaPath + '/additionalProperties', stack)) {
|
|
559
|
+
} else if (!((apFn.leaf && apFn.v(data[key], stack)) || apFn.c(data[key], errors, instancePath + '/' + escapePointer(key), schemaPath + '/additionalProperties', stack))) {
|
|
563
560
|
ok = false;
|
|
564
561
|
if (errors === NOERRORS) return false;
|
|
565
562
|
}
|
|
@@ -927,7 +924,7 @@ function install(deps) {
|
|
|
927
924
|
for (let k = 0; k < keys.length; k++) {
|
|
928
925
|
const key = keys[k];
|
|
929
926
|
if (hasProp(rec, key)) continue;
|
|
930
|
-
if (!fn.c(data[key], errors, instancePath + '/' + escapePointer(key), schemaPath + '/unevaluatedProperties', stack)) {
|
|
927
|
+
if (!((fn.leaf && fn.v(data[key], stack)) || fn.c(data[key], errors, instancePath + '/' + escapePointer(key), schemaPath + '/unevaluatedProperties', stack))) {
|
|
931
928
|
ok = false;
|
|
932
929
|
if (errors === NOERRORS) return false;
|
|
933
930
|
}
|
|
@@ -983,7 +980,7 @@ function install(deps) {
|
|
|
983
980
|
let ok = true;
|
|
984
981
|
for (let i = 0; i < data.length; i++) {
|
|
985
982
|
if (hasItem(rec, i)) continue;
|
|
986
|
-
if (!fn.c(data[i], errors, instancePath + '/' + i, schemaPath + '/unevaluatedItems', stack)) {
|
|
983
|
+
if (!((fn.leaf && fn.v(data[i], stack)) || fn.c(data[i], errors, instancePath + '/' + i, schemaPath + '/unevaluatedItems', stack))) {
|
|
987
984
|
ok = false;
|
|
988
985
|
if (errors === NOERRORS) return false;
|
|
989
986
|
}
|
|
@@ -1068,6 +1065,7 @@ function install(deps) {
|
|
|
1068
1065
|
box.v = vfn;
|
|
1069
1066
|
pair.c = cfn;
|
|
1070
1067
|
pair.v = vfn;
|
|
1068
|
+
pair.leaf = !nested;
|
|
1071
1069
|
pair.open = false;
|
|
1072
1070
|
return pair;
|
|
1073
1071
|
}
|
package/lib/regex-linear.js
CHANGED
|
@@ -3,10 +3,12 @@
|
|
|
3
3
|
const { parse } = require('./safe-regex')
|
|
4
4
|
|
|
5
5
|
// True when the platform RegExp, a backtracking engine, also runs `src` in
|
|
6
|
-
// linear time, so it can take the pattern: every quantifier on one
|
|
7
|
-
// class or `.`; at most one
|
|
8
|
-
//
|
|
9
|
-
//
|
|
6
|
+
// linear time, so it can take the pattern: every unbounded quantifier on one
|
|
7
|
+
// character, class or `.`; at most one of them, and then anchored with `^`;
|
|
8
|
+
// a finite repeat may cover a group that holds no unbounded quantifier; few
|
|
9
|
+
// bounded choices in all; only escapes both engines read alike; nothing
|
|
10
|
+
// outside the BMP, so both see the same UTF-16 units.
|
|
11
|
+
// tests/test_regex_native_linear.js holds it.
|
|
10
12
|
const NATIVE_ESCAPES = new Set(['d', 'D', 'w', 'W', 's', 'S', 'n', 'r', 't', 'f', 'v', '.', '\\', '/', '^', '$', '|', '?', '*', '+', '(', ')', '[', ']', '{', '}', '-', 'x', 'u'])
|
|
11
13
|
function nativeIsLinear (src) {
|
|
12
14
|
if (typeof src !== 'string' || src.length > 512) return false
|
|
@@ -23,33 +25,40 @@ function nativeIsLinear (src) {
|
|
|
23
25
|
}
|
|
24
26
|
let ast
|
|
25
27
|
try { ast = parse(src) } catch { return false }
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
28
|
+
// Per subtree: how many unbounded quantifiers it holds (`u`) and how many
|
|
29
|
+
// bounded choices a backtracking engine could try (`b`), or null where the
|
|
30
|
+
// shape is not provably linear. A finite repeat of a group is linear when
|
|
31
|
+
// the group holds no unbounded quantifier: `(?:[A-F0-9]{2}:){31}` has
|
|
32
|
+
// nothing to retry, and its choices multiply through the nesting.
|
|
29
33
|
const atom = (n) => n.t === 'char' || n.t === 'class' || n.t === 'any'
|
|
30
|
-
const
|
|
31
|
-
if (!ok) return
|
|
34
|
+
const info = (n) => {
|
|
32
35
|
switch (n.t) {
|
|
33
36
|
case 'star': case 'plus':
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
if (
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
case 'group':
|
|
47
|
-
case 'concat':
|
|
48
|
-
|
|
49
|
-
|
|
37
|
+
return atom(n.child) ? { u: 1, b: 0 } : null
|
|
38
|
+
case 'quest': {
|
|
39
|
+
if (atom(n.child)) return { u: 0, b: 1 }
|
|
40
|
+
const c = info(n.child)
|
|
41
|
+
return c === null || c.u > 0 ? null : { u: 0, b: 1 + c.b }
|
|
42
|
+
}
|
|
43
|
+
case 'repeat': {
|
|
44
|
+
if (atom(n.child)) return n.max === Infinity ? { u: 1, b: 0 } : { u: 0, b: n.max - n.min }
|
|
45
|
+
if (n.max === Infinity) return null
|
|
46
|
+
const c = info(n.child)
|
|
47
|
+
return c === null || c.u > 0 ? null : { u: 0, b: (n.max - n.min) + n.max * c.b }
|
|
48
|
+
}
|
|
49
|
+
case 'group': return info(n.child)
|
|
50
|
+
case 'concat': case 'alt': {
|
|
51
|
+
let u = 0, b = 0
|
|
52
|
+
for (const part of (n.t === 'concat' ? n.parts : n.opts)) { const c = info(part); if (c === null) return null; u += c.u; b += c.b }
|
|
53
|
+
return { u, b }
|
|
54
|
+
}
|
|
55
|
+
default: return { u: 0, b: 0 }
|
|
50
56
|
}
|
|
51
57
|
}
|
|
52
|
-
|
|
58
|
+
const r = info(ast)
|
|
59
|
+
const ok = r !== null
|
|
60
|
+
const unbounded = ok ? r.u : 0
|
|
61
|
+
const bounded = ok ? r.b : 0
|
|
53
62
|
if (!ok || unbounded > 1 || bounded > 64) return false
|
|
54
63
|
if (unbounded === 1) {
|
|
55
64
|
// Anchored at the start on every top-level branch.
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// uniqueItems for every engine. These are real functions: the interpreted
|
|
4
|
+
// engine calls them, and the code generator emits their source text
|
|
5
|
+
// (`String(fn)`) into the function it builds, so both run the same code.
|
|
6
|
+
// They read only each other, `_deq` and `_uqpI`, which the generator emits
|
|
7
|
+
// beside them.
|
|
8
|
+
//
|
|
9
|
+
// Short arrays compare pairwise and allocate nothing. Longer arrays of
|
|
10
|
+
// primitives key a Set or Map by the value itself. Longer arrays that hold an
|
|
11
|
+
// object key a Map by a numeric structural hash, and only items whose hashes
|
|
12
|
+
// collide are compared with `_deq`. Equal values always hash alike: numbers
|
|
13
|
+
// by value (so 1 and 1.0, 0 and -0 agree), strings by their characters,
|
|
14
|
+
// arrays by position, and objects by the sum of their own key-value hashes,
|
|
15
|
+
// whatever the key order. The hash replaced a sorted-key canonical string
|
|
16
|
+
// built for every item on every call, which was over a quarter of the time a
|
|
17
|
+
// generated verdict spent on SchemaStore's sample documents.
|
|
18
|
+
|
|
19
|
+
// JSON equality, the same function the generator hoists as DEQ_HELPER.
|
|
20
|
+
function _deq(a, b) {
|
|
21
|
+
if (a === b) return true;
|
|
22
|
+
if (a === null || b === null || typeof a !== 'object' || typeof b !== 'object') return false;
|
|
23
|
+
var aa = Array.isArray(a);
|
|
24
|
+
if (aa !== Array.isArray(b)) return false;
|
|
25
|
+
var i;
|
|
26
|
+
if (aa) {
|
|
27
|
+
if (a.length !== b.length) return false;
|
|
28
|
+
for (i = 0; i < a.length; i++) if (!_deq(a[i], b[i])) return false;
|
|
29
|
+
return true;
|
|
30
|
+
}
|
|
31
|
+
var ka = Object.keys(a);
|
|
32
|
+
if (ka.length !== Object.keys(b).length) return false;
|
|
33
|
+
for (i = 0; i < ka.length; i++) {
|
|
34
|
+
var k = ka[i];
|
|
35
|
+
if (!Object.prototype.hasOwnProperty.call(b, k) || !_deq(a[k], b[k])) return false;
|
|
36
|
+
}
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function _uqh(x) {
|
|
41
|
+
if (x === null) return 1;
|
|
42
|
+
var t = typeof x, h, i;
|
|
43
|
+
if (t === 'boolean') return x ? 2 : 3;
|
|
44
|
+
if (t === 'number') {
|
|
45
|
+
if (x === 0) return 4;
|
|
46
|
+
h = x | 0;
|
|
47
|
+
return Math.imul(h ^ (((x - h) * 1e9) | 0), 0x9e3779b1) ^ 5;
|
|
48
|
+
}
|
|
49
|
+
if (t === 'string') {
|
|
50
|
+
h = 0x811c9dc5 ^ x.length;
|
|
51
|
+
for (i = 0; i < x.length; i++) h = Math.imul(h ^ x.charCodeAt(i), 16777619);
|
|
52
|
+
return h;
|
|
53
|
+
}
|
|
54
|
+
if (t !== 'object') return 6;
|
|
55
|
+
if (Array.isArray(x)) {
|
|
56
|
+
h = 0x2545f491 ^ x.length;
|
|
57
|
+
for (i = 0; i < x.length; i++) h = Math.imul(h ^ _uqh(x[i]), 16777619);
|
|
58
|
+
return h;
|
|
59
|
+
}
|
|
60
|
+
var ks = Object.keys(x);
|
|
61
|
+
h = 0x6c8e9cf5 ^ ks.length;
|
|
62
|
+
for (i = 0; i < ks.length; i++) h = (h + Math.imul(_uqh(ks[i]) ^ Math.imul(_uqh(x[ks[i]]), 0x5bd1e995), 0x27d4eb2d)) | 0;
|
|
63
|
+
return h;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Whether every item is distinct.
|
|
67
|
+
function _uq(a) {
|
|
68
|
+
var n = a.length, i, k;
|
|
69
|
+
if (n < 2) return true;
|
|
70
|
+
if (n <= 12) {
|
|
71
|
+
for (i = 1; i < n; i++) for (k = 0; k < i; k++) if (_deq(a[i], a[k])) return false;
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
74
|
+
var prim = true;
|
|
75
|
+
for (i = 0; i < n; i++) { var x = a[i]; if (x !== null && typeof x === 'object') { prim = false; break; } }
|
|
76
|
+
if (prim) {
|
|
77
|
+
var s = new Set();
|
|
78
|
+
for (i = 0; i < n; i++) { if (s.has(a[i])) return false; s.add(a[i]); }
|
|
79
|
+
return true;
|
|
80
|
+
}
|
|
81
|
+
var m = new Map();
|
|
82
|
+
for (i = 0; i < n; i++) {
|
|
83
|
+
var hh = _uqh(a[i]), b = m.get(hh);
|
|
84
|
+
if (b === undefined) m.set(hh, i);
|
|
85
|
+
else if (typeof b === 'number') { if (_deq(a[b], a[i])) return false; m.set(hh, [b, i]); }
|
|
86
|
+
else { for (k = 0; k < b.length; k++) if (_deq(a[b[k]], a[i])) return false; b.push(i); }
|
|
87
|
+
}
|
|
88
|
+
return true;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// The duplicate pair an error reports: the first item that has a later
|
|
92
|
+
// equal, and its first later equal. Returns that later index and leaves the
|
|
93
|
+
// first in _uqpI, or returns -1 when every item is distinct.
|
|
94
|
+
var _uqpI = 0;
|
|
95
|
+
function _uqp(a) {
|
|
96
|
+
var n = a.length, i, k;
|
|
97
|
+
if (n < 2) return -1;
|
|
98
|
+
if (n <= 12) {
|
|
99
|
+
for (i = 0; i < n - 1; i++) for (k = i + 1; k < n; k++) if (_deq(a[i], a[k])) { _uqpI = i; return k; }
|
|
100
|
+
return -1;
|
|
101
|
+
}
|
|
102
|
+
var prim = true;
|
|
103
|
+
for (i = 0; i < n; i++) { var x = a[i]; if (x !== null && typeof x === 'object') { prim = false; break; } }
|
|
104
|
+
var bi = -1, bj = -1, m = new Map();
|
|
105
|
+
if (prim) {
|
|
106
|
+
for (i = 0; i < n; i++) {
|
|
107
|
+
var p = m.get(a[i]);
|
|
108
|
+
if (p === undefined) m.set(a[i], i);
|
|
109
|
+
else if (p >= 0) { if (bi < 0 || p < bi) { bi = p; bj = i; } m.set(a[i], -1); }
|
|
110
|
+
}
|
|
111
|
+
} else {
|
|
112
|
+
// Each bucket holds the first occurrence of every distinct value that
|
|
113
|
+
// hashed there, in index order; a value is paired with its first later
|
|
114
|
+
// equal only once.
|
|
115
|
+
var paired = null;
|
|
116
|
+
for (i = 0; i < n; i++) {
|
|
117
|
+
var hh = _uqh(a[i]), b = m.get(hh), r = -1;
|
|
118
|
+
if (b === undefined) { m.set(hh, i); continue; }
|
|
119
|
+
if (typeof b === 'number') { if (_deq(a[b], a[i])) r = b; else m.set(hh, [b, i]); }
|
|
120
|
+
else { for (k = 0; k < b.length; k++) if (_deq(a[b[k]], a[i])) { r = b[k]; break; } if (r < 0) b.push(i); }
|
|
121
|
+
if (r >= 0) {
|
|
122
|
+
if (paired === null) paired = new Set();
|
|
123
|
+
if (!paired.has(r)) { paired.add(r); if (bi < 0 || r < bi) { bi = r; bj = i; } }
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
if (bi < 0) return -1;
|
|
128
|
+
_uqpI = bi;
|
|
129
|
+
return bj;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// For the interpreted engine.
|
|
133
|
+
function allDistinct(a) { return _uq(a); }
|
|
134
|
+
function duplicatePair(a) {
|
|
135
|
+
var j = _uqp(a);
|
|
136
|
+
return j < 0 ? null : [_uqpI, j];
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
module.exports = { allDistinct, duplicatePair, _deq, _uqh, _uq, _uqp };
|