ata-validator 1.63.1 → 1.63.2
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/bin/ata.js +8 -4
- package/lib/ir/emit.mjs +10 -2
- package/lib/js-compiler-errors.js +17 -5
- package/lib/js-compiler.js +11 -3
- package/lib/regex-linear.js +40 -2
- package/lib/scan-compiler.js +6 -0
- package/lib/version.js +1 -1
- package/package.json +8 -8
package/bin/ata.js
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
'use strict';
|
|
3
3
|
const { env } = require('../lib/env');
|
|
4
|
+
// Text that comes from a schema file (a $ref, a keyword name in a warning) is
|
|
5
|
+
// written with its control characters as escapes, as `ata validate` writes
|
|
6
|
+
// it: a schema could otherwise clear the screen or forge an `ata:` line.
|
|
7
|
+
const term = (t) => require('../lib/diagnose').term(String(t));
|
|
4
8
|
|
|
5
9
|
const fs = require('fs');
|
|
6
10
|
const path = require('path');
|
|
@@ -299,7 +303,7 @@ function cmdCompile(args) {
|
|
|
299
303
|
for (const w of compileWarnings) reportCompileError(input, w);
|
|
300
304
|
process.exit(1);
|
|
301
305
|
}
|
|
302
|
-
for (const w of compileWarnings) process.stderr.write(`ata: warning: ${input}: ${w}\n`);
|
|
306
|
+
for (const w of compileWarnings) process.stderr.write(`ata: warning: ${input}: ${term(w)}\n`);
|
|
303
307
|
}
|
|
304
308
|
if (!src) {
|
|
305
309
|
reportCompileError(input, 'schema is too complex for standalone compilation');
|
|
@@ -397,7 +401,7 @@ function cmdBuild(args) {
|
|
|
397
401
|
for (const c of report.compiled) {
|
|
398
402
|
process.stdout.write(`ata: ${c.input} -> ${c.output} (${c.bytes.toLocaleString()} bytes${c.errors === 'compact' ? ', shared errors' : c.errors === 'schema' ? ', schema module' : ''})\n`);
|
|
399
403
|
for (const t of (report.bunTargets || []).filter((t) => t.input === c.input)) process.stdout.write(`ata: bun target -> ${t.output} (for bun build --compile --aot)\n`);
|
|
400
|
-
for (const d of (report.bunDeclined || []).filter((d) => d.input === c.input)) process.stderr.write(`ata: warning: ${d.input}: ${d.reason}\n`);
|
|
404
|
+
for (const d of (report.bunDeclined || []).filter((d) => d.input === c.input)) process.stderr.write(`ata: warning: ${d.input}: ${term(d.reason)}\n`);
|
|
401
405
|
}
|
|
402
406
|
if (report.runtime) {
|
|
403
407
|
process.stdout.write(`ata: error runtime -> ${report.runtime.output} (${report.runtime.bytes.toLocaleString()} bytes, shared by the modules above)\n`);
|
|
@@ -412,7 +416,7 @@ function cmdBuild(args) {
|
|
|
412
416
|
noDetail++;
|
|
413
417
|
process.stderr.write(`ata: warning ${w.input}: no error detail, failures report the single ATA9000 error (the verdict is exact)\n`);
|
|
414
418
|
} else {
|
|
415
|
-
process.stderr.write(`ata: warning ${w.input}: ${w.warning}\n`);
|
|
419
|
+
process.stderr.write(`ata: warning ${w.input}: ${term(w.warning)}\n`);
|
|
416
420
|
}
|
|
417
421
|
}
|
|
418
422
|
if (noDetail > 0 && args.opts.mode !== 'schema') {
|
|
@@ -422,7 +426,7 @@ function cmdBuild(args) {
|
|
|
422
426
|
process.stdout.write(`ata: cached ${c.input}\n`);
|
|
423
427
|
}
|
|
424
428
|
for (const s of report.skipped) {
|
|
425
|
-
process.stdout.write(`ata: skipped ${s.input}: ${s.reason}\n`);
|
|
429
|
+
process.stdout.write(`ata: skipped ${s.input}: ${term(s.reason)}\n`);
|
|
426
430
|
}
|
|
427
431
|
for (const f of report.failed) {
|
|
428
432
|
reportCompileError(f.input, f.error);
|
package/lib/ir/emit.mjs
CHANGED
|
@@ -16,8 +16,16 @@ const FAST = {
|
|
|
16
16
|
pattern: (n, x) => compilePatternInline(n, x),
|
|
17
17
|
// A string has between length/2 and length code points, so the length
|
|
18
18
|
// alone settles most cases and the count runs only near the bound.
|
|
19
|
-
|
|
20
|
-
|
|
19
|
+
// The bound is written into the module as a number, so it has to be one: a
|
|
20
|
+
// schema that gave a string here had it written as source, and what the
|
|
21
|
+
// string said ran in whatever imported the module. Refused, as the closure
|
|
22
|
+
// writer refuses it.
|
|
23
|
+
minLength: (n, x) => { const b = bound(n, 'minLength'); return `${x}.length>=${2 * b}||${x}.length>=${b}&&cpLen(${x})>=${b}` },
|
|
24
|
+
maxLength: (n, x) => { const b = bound(n, 'maxLength'); return `${x}.length<=${b}||${x}.length<=${2 * b}&&cpLen(${x})<=${b}` },
|
|
25
|
+
}
|
|
26
|
+
function bound (n, k) {
|
|
27
|
+
if (typeof n !== 'number' || !Number.isFinite(n)) throw new Error(`ir: ${k} is not a number`)
|
|
28
|
+
return n
|
|
21
29
|
}
|
|
22
30
|
// enum and const with only primitive values need no deep equality: the
|
|
23
31
|
// value is one of them exactly when it is === one of them (JSON numbers have
|
|
@@ -1147,11 +1147,16 @@ function genCodeENode(schema, v, pathExpr, lines, ctx, schemaPrefix) {
|
|
|
1147
1147
|
const expected = types.join(', ')
|
|
1148
1148
|
// As written: a one-element array stays an array, as the interpreted engine
|
|
1149
1149
|
// reports it.
|
|
1150
|
-
|
|
1150
|
+
// Escaped like every other schema string written into this source: a
|
|
1151
|
+
// type name is the schema author's text, and a quote in one that is not
|
|
1152
|
+
// a JSON Schema type closed the literal and ran what followed as code,
|
|
1153
|
+
// in the process that compiled the schema and in the module written
|
|
1154
|
+
// from it.
|
|
1155
|
+
const expectedParam = !Array.isArray(schema.type) ? `'${esc(String(types[0]))}'` : JSON.stringify(types)
|
|
1151
1156
|
{
|
|
1152
1157
|
const typeSp = `${schemaPrefix}/type`
|
|
1153
1158
|
const lit = buildErrorLiteral({ keyword: 'type', schemaPath: typeSp, sourceMap: ctx.sourceMap })
|
|
1154
|
-
lines.push(`if(!(${conds.join('||')})){_e.push({code:'${lit.codeStr}',keyword:'type',instancePath:${pathExpr||'""'},schemaPath:'${typeSp}'${ordinalField(ctx, `${typeSp}`)},params:{type:${expectedParam}},message:'must be ${expected}',docUrl:'${lit.docUrl}'${lit.frame}});if(!_all)return{valid:false,errors:_e}}`)
|
|
1159
|
+
lines.push(`if(!(${conds.join('||')})){_e.push({code:'${lit.codeStr}',keyword:'type',instancePath:${pathExpr||'""'},schemaPath:'${typeSp}'${ordinalField(ctx, `${typeSp}`)},params:{type:${expectedParam}},message:'must be ${esc(expected)}',docUrl:'${lit.docUrl}'${lit.frame}});if(!_all)return{valid:false,errors:_e}}`)
|
|
1155
1160
|
}
|
|
1156
1161
|
}
|
|
1157
1162
|
|
|
@@ -2038,7 +2043,9 @@ TYPE_KEYWORDS.integer = TYPE_KEYWORDS.number
|
|
|
2038
2043
|
const NOT_VALIDATING = new Set(['$schema', '$id', '$anchor', '$comment', '$defs', 'definitions', 'title', 'description', 'default', 'examples', 'deprecated', 'readOnly', 'writeOnly', '$vocabulary'])
|
|
2039
2044
|
function typeMismatchRest(schema, types) {
|
|
2040
2045
|
const drop = new Set(['type', 'enum', 'const', 'anyOf', 'oneOf'])
|
|
2041
|
-
|
|
2046
|
+
// Own names only: a type called `constructor` or `__proto__` read
|
|
2047
|
+
// Object.prototype's member here and threw while compiling.
|
|
2048
|
+
for (const t of types) if (Object.prototype.hasOwnProperty.call(TYPE_KEYWORDS, t)) for (const k of TYPE_KEYWORDS[t]) drop.add(k)
|
|
2042
2049
|
// A built-in format is written for strings only (`typeof v==='string'&&`
|
|
2043
2050
|
// off the string path), so for a value that failed `type: string` it can
|
|
2044
2051
|
// never fire: carried here it was dead code in every module that checks one.
|
|
@@ -2435,11 +2442,16 @@ function genCodeCNode(schema, v, pathExpr, lines, ctx, schemaPrefix) {
|
|
|
2435
2442
|
const expected = types.join(', ')
|
|
2436
2443
|
// As written: a one-element array stays an array, as the interpreted engine
|
|
2437
2444
|
// reports it.
|
|
2438
|
-
|
|
2445
|
+
// Escaped like every other schema string written into this source: a
|
|
2446
|
+
// type name is the schema author's text, and a quote in one that is not
|
|
2447
|
+
// a JSON Schema type closed the literal and ran what followed as code,
|
|
2448
|
+
// in the process that compiled the schema and in the module written
|
|
2449
|
+
// from it.
|
|
2450
|
+
const expectedParam = !Array.isArray(schema.type) ? `'${esc(String(types[0]))}'` : JSON.stringify(types)
|
|
2439
2451
|
// Type check: push error but continue — wrap remaining in type-success block
|
|
2440
2452
|
const typeOk = `_tok${ctx.varCounter++}`
|
|
2441
2453
|
lines.push(`const ${typeOk}=${conds.join('||')}`)
|
|
2442
|
-
lines.push(`if(!${typeOk}){${fail('type', 'type', `{type:${expectedParam}}`, `'must be ${expected}'`)}}`)
|
|
2454
|
+
lines.push(`if(!${typeOk}){${fail('type', 'type', `{type:${expectedParam}}`, `'must be ${esc(expected)}'`)}}`)
|
|
2443
2455
|
// Subsequent optimized code runs inside if(typeOk){...}
|
|
2444
2456
|
if (types.length === 1) {
|
|
2445
2457
|
isObj = types[0] === 'object'
|
package/lib/js-compiler.js
CHANGED
|
@@ -832,9 +832,9 @@ function resolveRef(ref, defs, schemaMap) {
|
|
|
832
832
|
|
|
833
833
|
function buildTypeCheck(types) {
|
|
834
834
|
if (types.length === 1) {
|
|
835
|
-
return
|
|
835
|
+
return typeCheckOf(types[0]) || (() => true)
|
|
836
836
|
}
|
|
837
|
-
const fns = types.map(
|
|
837
|
+
const fns = types.map(typeCheckOf).filter(Boolean)
|
|
838
838
|
return (d) => {
|
|
839
839
|
for (let i = 0; i < fns.length; i++) {
|
|
840
840
|
if (fns[i](d)) return true
|
|
@@ -902,6 +902,10 @@ function multipleOfBad(v, m) {
|
|
|
902
902
|
return `!(Number.isInteger(${v}/${m})||Math.abs(${v}/${m}-Math.round(${v}/${m}))<1e-9)`
|
|
903
903
|
}
|
|
904
904
|
|
|
905
|
+
// Own names only: a type called `__proto__` or `valueOf` read
|
|
906
|
+
// Object.prototype's member and was handed out as the check, and the
|
|
907
|
+
// Validator then threw on the first document (`_fast is not a function`).
|
|
908
|
+
const typeCheckOf = (t) => (Object.prototype.hasOwnProperty.call(TYPE_CHECKS, t) ? TYPE_CHECKS[t] : undefined)
|
|
905
909
|
const TYPE_CHECKS = {
|
|
906
910
|
string: (d) => typeof d === 'string',
|
|
907
911
|
number: (d) => Number.isFinite(d),
|
|
@@ -4198,7 +4202,11 @@ function genDynamicRefNode(schema, v, lines, ctx, knownType) {
|
|
|
4198
4202
|
function genUniqueItemsNode(schema, v, lines, ctx, knownType, hoisted, isArr, types) {
|
|
4199
4203
|
const si = ctx.varCounter++
|
|
4200
4204
|
const itemType = schema.items && typeof schema.items === 'object' && schema.items.type
|
|
4201
|
-
|
|
4205
|
+
// `items` speaks for the elements after the tuple only: with `prefixItems`
|
|
4206
|
+
// the first positions may hold objects, and comparing those by identity
|
|
4207
|
+
// took two equal objects for different ones and accepted the array.
|
|
4208
|
+
const noTuple = !Array.isArray(schema.prefixItems) || schema.prefixItems.length === 0
|
|
4209
|
+
const isPrimItems = noTuple && (itemType === 'string' || itemType === 'number' || itemType === 'integer')
|
|
4202
4210
|
const maxItems = schema.maxItems
|
|
4203
4211
|
// Small primitive arrays (maxItems <= 16): nested loop is 6x faster than Set
|
|
4204
4212
|
// No allocation, no hash computation — just direct === comparison
|
package/lib/regex-linear.js
CHANGED
|
@@ -36,6 +36,15 @@ const { parse } = require('./safe-regex')
|
|
|
36
36
|
// holds them: every accepted pattern answers as the linear engine does and
|
|
37
37
|
// stays fast on inputs built to make a backtracking engine retry.
|
|
38
38
|
const NATIVE_ESCAPES = new Set(['d', 'D', 'w', 'W', 's', 'S', 'n', 'r', 't', 'f', 'v', '.', '\\', '/', '^', '$', '|', '?', '*', '+', '(', ')', '[', ']', '{', '}', '-', 'x', 'u'])
|
|
39
|
+
// How many ways a backtracking engine can split one string over the bounded
|
|
40
|
+
// choices of a pattern. Choices in a row multiply: `(a?){30}` is thirty
|
|
41
|
+
// choices and two to the thirtieth ways, which the engine walks through on a
|
|
42
|
+
// string that does not match. The count used to be a sum capped at 64, so
|
|
43
|
+
// that pattern and `^([a-z]?){64}$` passed as linear and a string of 17
|
|
44
|
+
// characters ran for more than half a minute. Now the product, capped where
|
|
45
|
+
// the worst case stays under a millisecond for a hundred characters.
|
|
46
|
+
const MAX_WAYS = 4096
|
|
47
|
+
// The sum of bounded choices, which both rules below also hold to.
|
|
39
48
|
const MAX_BOUNDED_CHOICES = 64
|
|
40
49
|
|
|
41
50
|
const atom = (n) => n.t === 'char' || n.t === 'class' || n.t === 'any'
|
|
@@ -52,7 +61,9 @@ function anchored (ast) {
|
|
|
52
61
|
function simpleRule (ast) {
|
|
53
62
|
// Per subtree: how many unbounded quantifiers it holds (`u`) and how many
|
|
54
63
|
// bounded choices a backtracking engine could try (`b`), or null where the
|
|
55
|
-
// shape is not provably linear.
|
|
64
|
+
// shape is not provably linear. The choices are held to a sum
|
|
65
|
+
// (MAX_BOUNDED_CHOICES: a long counted run is slow to compile and to
|
|
66
|
+
// unwind) and to a product (MAX_WAYS, waysOf below).
|
|
56
67
|
const info = (n) => {
|
|
57
68
|
switch (n.t) {
|
|
58
69
|
case 'star': case 'plus':
|
|
@@ -78,7 +89,7 @@ function simpleRule (ast) {
|
|
|
78
89
|
}
|
|
79
90
|
}
|
|
80
91
|
const r = info(ast)
|
|
81
|
-
if (r === null || r.u > 1 || r.b > MAX_BOUNDED_CHOICES) return false
|
|
92
|
+
if (r === null || r.u > 1 || r.b > MAX_BOUNDED_CHOICES || !(waysOf(ast) <= MAX_WAYS)) return false
|
|
82
93
|
return r.u === 0 || anchored(ast)
|
|
83
94
|
}
|
|
84
95
|
|
|
@@ -207,9 +218,31 @@ function deterministicRule (ast) {
|
|
|
207
218
|
for (let p = 0; p < sets.length && ok; p++) disjoint(follow[p])
|
|
208
219
|
if (!ok) return false
|
|
209
220
|
if (bounded > MAX_BOUNDED_CHOICES) return false
|
|
221
|
+
// A counted repeat of a body that can end in more than one place is not a
|
|
222
|
+
// loop to the walk above (`(a?){30}`, `(a{1,8}){8}`), and it is where the
|
|
223
|
+
// ways multiply: the same product as the simple rule holds here too.
|
|
224
|
+
if (!(waysOf(ast) <= MAX_WAYS)) return false
|
|
210
225
|
return unbounded === 0 || anchored(ast)
|
|
211
226
|
}
|
|
212
227
|
|
|
228
|
+
// The product the simple rule counts, over any shape: an unbounded
|
|
229
|
+
// quantifier adds no bounded choice of its own.
|
|
230
|
+
function waysOf (n) {
|
|
231
|
+
switch (n.t) {
|
|
232
|
+
case 'star': case 'plus': return waysOf(n.child)
|
|
233
|
+
case 'quest': return atom(n.child) ? 2 : 1 + waysOf(n.child)
|
|
234
|
+
case 'repeat': {
|
|
235
|
+
if (n.max === Infinity) return waysOf(n.child)
|
|
236
|
+
if (atom(n.child)) return n.max - n.min + 1
|
|
237
|
+
return (n.max - n.min + 1) * Math.pow(waysOf(n.child), n.max)
|
|
238
|
+
}
|
|
239
|
+
case 'group': return waysOf(n.child)
|
|
240
|
+
case 'concat': { let w = 1; for (const part of n.parts) w *= waysOf(part); return w }
|
|
241
|
+
case 'alt': { let w = 0; for (const part of n.opts) w += waysOf(part); return w }
|
|
242
|
+
default: return 1
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
213
246
|
// Two more shapes the rules above decline, both anchored with `^` and `$`
|
|
214
247
|
// around one top-level sequence.
|
|
215
248
|
//
|
|
@@ -393,6 +426,11 @@ function nativeIsLinear (src) {
|
|
|
393
426
|
}
|
|
394
427
|
let ast
|
|
395
428
|
try { ast = parse(src) } catch { return false }
|
|
429
|
+
// The product of the bounded choices holds for every rule below, so it is
|
|
430
|
+
// asked here, once: the one-choice and delimited-loop rules did not ask it,
|
|
431
|
+
// and `^(a*b)*` followed by thirty `a?` reached the platform RegExp, where
|
|
432
|
+
// each `a?` more doubles the time on a string that does not match.
|
|
433
|
+
if (!(waysOf(ast) <= MAX_WAYS)) return false
|
|
396
434
|
return simpleRule(ast) || deterministicRule(ast) || oneChoiceRule(ast) || delimitedLoopRule(ast)
|
|
397
435
|
}
|
|
398
436
|
|
package/lib/scan-compiler.js
CHANGED
|
@@ -981,6 +981,12 @@ function emitObject(schema, ctx, out, depth) {
|
|
|
981
981
|
const reqMasks = new Array(words).fill(0);
|
|
982
982
|
if (required) for (const k of required) { const x = bit.get(k); reqMasks[x.w] |= x.b; }
|
|
983
983
|
|
|
984
|
+
// Many names are dispatched on a hash of the key, and two names with one
|
|
985
|
+
// hash cannot be: the code below then took the other branch, which reads
|
|
986
|
+
// variables only the few-names form declares, and the scanner threw a
|
|
987
|
+
// ReferenceError on every document that carried a declared key. Such a
|
|
988
|
+
// schema is left to the parser.
|
|
989
|
+
if (names.length >= HASH_DISPATCH_MIN && new Set(names.map(spanHash)).size !== names.length) decline('properties:hash-collision');
|
|
984
990
|
out.push('c=s.charCodeAt(++i);');
|
|
985
991
|
const useHash = names.length >= HASH_DISPATCH_MIN;
|
|
986
992
|
if (useHash) {
|
package/lib/version.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ata-validator",
|
|
3
|
-
"version": "1.63.
|
|
3
|
+
"version": "1.63.2",
|
|
4
4
|
"description": "JSON Schema validator that compiles to plain JavaScript, still runs where code generation is blocked, and can be compiled away at build time. Passes the full official test suite.",
|
|
5
5
|
"type": "commonjs",
|
|
6
6
|
"main": "index.js",
|
|
@@ -266,13 +266,13 @@
|
|
|
266
266
|
"codegen.d.ts"
|
|
267
267
|
],
|
|
268
268
|
"optionalDependencies": {
|
|
269
|
-
"@ata-validator/native-darwin-arm64": "1.63.
|
|
270
|
-
"@ata-validator/native-darwin-x64": "1.63.
|
|
271
|
-
"@ata-validator/native-linux-arm64-gnu": "1.63.
|
|
272
|
-
"@ata-validator/native-linux-arm64-musl": "1.63.
|
|
273
|
-
"@ata-validator/native-linux-x64-gnu": "1.63.
|
|
274
|
-
"@ata-validator/native-linux-x64-musl": "1.63.
|
|
275
|
-
"@ata-validator/native-win32-x64": "1.63.
|
|
269
|
+
"@ata-validator/native-darwin-arm64": "1.63.2",
|
|
270
|
+
"@ata-validator/native-darwin-x64": "1.63.2",
|
|
271
|
+
"@ata-validator/native-linux-arm64-gnu": "1.63.2",
|
|
272
|
+
"@ata-validator/native-linux-arm64-musl": "1.63.2",
|
|
273
|
+
"@ata-validator/native-linux-x64-gnu": "1.63.2",
|
|
274
|
+
"@ata-validator/native-linux-x64-musl": "1.63.2",
|
|
275
|
+
"@ata-validator/native-win32-x64": "1.63.2"
|
|
276
276
|
},
|
|
277
277
|
"peerDependencies": {
|
|
278
278
|
"yaml": "^2.0.0"
|