ata-validator 1.8.2 → 1.10.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.
@@ -2,14 +2,25 @@
2
2
 
3
3
  // Bounded Levenshtein. Returns Infinity if distance > maxDistance.
4
4
  // Single-row DP. O(n*m) worst case but typical strings are <30 chars.
5
+ // Two rows of scratch, reused across calls. The function is not recursive
6
+ // and the library is single-threaded, so nothing else is using them; growing
7
+ // them once beats allocating two arrays on every comparison, which the
8
+ // reject-path profile showed as most of this function's cost.
9
+ let scratchA = new Int32Array(64);
10
+ let scratchB = new Int32Array(64);
11
+
5
12
  function levenshtein (a, b, maxDistance) {
6
13
  const max = maxDistance == null ? Infinity : maxDistance;
7
14
  if (a === b) return 0;
8
15
  if (Math.abs(a.length - b.length) > max) return Infinity;
9
16
  if (a.length === 0) return b.length;
10
17
  if (b.length === 0) return a.length;
11
- let prev = new Array(b.length + 1);
12
- let curr = new Array(b.length + 1);
18
+ if (scratchA.length < b.length + 1) {
19
+ scratchA = new Int32Array(b.length + 1);
20
+ scratchB = new Int32Array(b.length + 1);
21
+ }
22
+ let prev = scratchA;
23
+ let curr = scratchB;
13
24
  for (let j = 0; j <= b.length; j++) prev[j] = j;
14
25
  for (let i = 1; i <= a.length; i++) {
15
26
  curr[0] = i;
@@ -1,31 +1,41 @@
1
1
  'use strict';
2
2
 
3
- const { color, ANSI, resolveColor, pathToDotted, trimCwd } = require('./render-shared');
3
+ const { color, ANSI, resolveColor, trimCwd } = require('./render-shared');
4
+ const { toDiagnostics } = require('./diagnose');
5
+
6
+ const { getDiagnosticSource } = require('./diagnostic-source');
4
7
 
5
8
  function renderCompact (errors, opts) {
6
9
  if (!Array.isArray(errors) || errors.length === 0) return '';
7
10
  opts = opts || {};
8
11
  const useColor = resolveColor(opts.color || 'auto');
9
12
  const cwd = opts.cwd;
13
+ const carried = getDiagnosticSource(errors) || {};
14
+ const source = opts.data !== undefined ? Object.assign({}, carried, { data: opts.data }) : carried;
15
+ const diags = toDiagnostics(errors, source);
10
16
  const lines = [];
11
17
 
12
- for (const err of errors) {
18
+ for (const d of diags) {
13
19
  let prefix = '';
14
- if (err.schemaSource) {
15
- const f = trimCwd(err.schemaSource.file, cwd);
16
- prefix = color(useColor, ANSI.cyan, `${f}:${err.schemaSource.line}:${err.schemaSource.col}`) + ' - ';
20
+ const raw = d.mergedFrom[0];
21
+ if (raw && raw.schemaSource) {
22
+ const f = trimCwd(raw.schemaSource.file, cwd);
23
+ prefix = color(useColor, ANSI.cyan, `${f}:${raw.schemaSource.line}:${raw.schemaSource.col}`) + ' - ';
17
24
  }
18
- const codeStr = color(useColor, ANSI.red + ANSI.bold, `error ${err.code}`);
19
- const pathStr = color(useColor, ANSI.cyan, pathToDotted(err.path));
20
- const got = err.received != null ? `got ${err.received}` : '';
21
- const sugg = err.suggestion ? color(useColor, ANSI.yellow, `, ${err.suggestion.text}`) : '';
25
+ const codeStr = color(useColor, ANSI.red + ANSI.bold, d.code ? `error ${d.code}` : 'error');
26
+ const pathStr = color(useColor, ANSI.cyan, d.dotted);
27
+ const got = raw && raw.received != null ? `got ${raw.received}` : '';
28
+ const sugg = d.help ? color(useColor, ANSI.yellow, `, ${d.help}`) : '';
22
29
  const tail = got || sugg ? ` (${got}${sugg})` : '';
23
- lines.push(`${prefix}${codeStr}: ${pathStr} ${err.message}${tail}`);
30
+ lines.push(`${prefix}${codeStr}: ${pathStr} ${d.headline}${tail}`);
24
31
  }
25
32
 
26
33
  const n = errors.length;
34
+ const shown = diags.length;
27
35
  lines.push('');
28
- lines.push(`Found ${n} error${n === 1 ? '' : 's'} in ${opts.context || 'input'}.`);
36
+ let summary = `Found ${n} error${n === 1 ? '' : 's'} in ${opts.context || 'input'}`;
37
+ if (shown !== n) summary += `, shown as ${shown} diagnostic${shown === 1 ? '' : 's'}`;
38
+ lines.push(summary + '.');
29
39
  if (!(process.stdout && process.stdout.isTTY)) {
30
40
  lines.push('(run with --pretty for source frames)');
31
41
  }
@@ -1,60 +1,93 @@
1
1
  'use strict';
2
2
 
3
3
  const { color, ANSI, resolveColor, trimCwd, truncateLine, terminalWidth } = require('./render-shared');
4
+ const { toDiagnostics } = require('./diagnose');
4
5
 
6
+ const { getDiagnosticSource } = require('./diagnostic-source');
7
+
8
+ function sourceFor (errors, opts) {
9
+ const carried = getDiagnosticSource(errors) || {};
10
+ if (opts.data !== undefined) return Object.assign({}, carried, { data: opts.data });
11
+ return carried;
12
+ }
13
+
14
+ // Never wider than the terminal. The old form repeated the caret for the
15
+ // full span of the value, which for a root-level error was the whole
16
+ // document drawn under a one-character line.
5
17
  function caretLine (col, length, gutter) {
6
18
  const pad = ' '.repeat(gutter);
7
19
  const lead = ' '.repeat(Math.max(0, col - 1));
8
- const carets = '^'.repeat(Math.max(1, length || 1));
20
+ const carets = '^'.repeat(Math.max(1, Math.min(length || 1, terminalWidth())));
9
21
  return pad + '| ' + lead + carets;
10
22
  }
11
23
 
12
- function renderOne (err, useColor, opts) {
24
+ // What sits beside the caret. For a value error, the value that was found.
25
+ // For a missing property there is no value to show, and the useful fact is
26
+ // what was expected. For errors about the container's shape or about which
27
+ // branch matched, printing the whole container is noise, so nothing.
28
+ const NO_SUFFIX = new Set(['additionalProperties', 'unevaluatedProperties', 'unevaluatedItems', 'dependentRequired', 'propertyNames', 'oneOf', 'anyOf', 'allOf', 'not']);
29
+ function caretSuffix (d, raw, useColor) {
30
+ if (!raw) return '';
31
+ if (raw.keyword === 'required') {
32
+ return raw.expected ? ' ' + color(useColor, ANSI.dim, `expected ${raw.expected}`) : '';
33
+ }
34
+ // A composition error anchored inside the closest branch has a value there.
35
+ if (d.found != null && (raw.keyword === 'oneOf' || raw.keyword === 'anyOf')) {
36
+ return ' ' + color(useColor, ANSI.dim, `found ${d.found}`);
37
+ }
38
+ if (NO_SUFFIX.has(raw.keyword)) return '';
39
+ return d.found != null ? ' ' + color(useColor, ANSI.dim, `found ${d.found}`) : '';
40
+ }
41
+
42
+ function renderOne (d, useColor, opts) {
13
43
  const lines = [];
14
- const cwd = opts.cwd;
15
44
  const width = terminalWidth();
16
45
  const gutter = 3;
17
46
 
18
- // Headline
19
- const headline = `${err.message}`;
20
- lines.push(color(useColor, ANSI.red + ANSI.bold, `error[${err.code}]: `) + headline);
47
+ // Headline carries the code when there is one. A code-less error, which the
48
+ // LazyRejection fallback can produce, must not render as "error[undefined]".
49
+ const label = d.code ? `error[${d.code}]: ` : 'error: ';
50
+ lines.push(color(useColor, ANSI.red + ANSI.bold, label) + d.headline);
21
51
 
22
- // Schema source frame
23
- if (err.schemaSource) {
24
- const f = trimCwd(err.schemaSource.file, cwd);
25
- lines.push(` --> ${color(useColor, ANSI.cyan, `${f}:${err.schemaSource.line}:${err.schemaSource.col}`)}`);
52
+ // Schema source frame, when the Validator was built with a `source` option.
53
+ // This is the part that makes the output read like a compiler, and it must
54
+ // survive the rewrite: it points at the rule, where the data frame below
55
+ // points at the value.
56
+ const raw = d.mergedFrom[0];
57
+ if (raw && raw.schemaSource) {
58
+ const f = trimCwd(raw.schemaSource.file, opts.cwd);
59
+ lines.push(` --> ${color(useColor, ANSI.cyan, `${f}:${raw.schemaSource.line}:${raw.schemaSource.col}`)}`);
26
60
  lines.push(' |');
27
- const ln = String(err.schemaSource.line).padStart(2, ' ');
28
- const srcText = truncateLine(err.schemaSource.text, width - 8);
29
- lines.push(` ${ln} | ${srcText}`);
30
- const inlineHint = err.expected ? ' ' + color(useColor, ANSI.dim, `expected ${err.expected}`) : '';
31
- lines.push(caretLine(err.schemaSource.col, 1, gutter) + inlineHint);
61
+ const sln = String(raw.schemaSource.line).padStart(2, ' ');
62
+ lines.push(` ${sln} | ${truncateLine(raw.schemaSource.text, width - 8)}`);
63
+ const inlineHint = raw.expected ? ' ' + color(useColor, ANSI.dim, `expected ${raw.expected}`) : '';
64
+ lines.push(caretLine(raw.schemaSource.col, 1, gutter) + inlineHint);
32
65
  lines.push(' |');
33
66
  }
34
67
 
35
- // Data frame
36
- if (err.dataFrame) {
37
- lines.push(` --> ${color(useColor, ANSI.dim, `input, byte ${err.dataFrame.byteOffset}`)}`);
68
+ // Data location, always, even without a frame. This is the single most
69
+ // common reason today's output cannot be acted on.
70
+ if (d.frame) {
71
+ const where = `input:${d.frame.line}:${d.frame.col}`;
72
+ lines.push(` --> ${color(useColor, ANSI.cyan, where)} ${color(useColor, ANSI.dim, '(' + d.dotted + ')')}`);
38
73
  lines.push(' |');
39
- const ln = String(err.dataFrame.line).padStart(2, ' ');
40
- const srcText = truncateLine(err.dataFrame.text, width - 8);
41
- lines.push(` ${ln} | ${srcText}`);
42
- const got = err.received != null ? ' ' + color(useColor, ANSI.dim, `got ${err.received}`) : '';
43
- lines.push(caretLine(err.dataFrame.col, err.dataFrame.length, gutter) + got);
74
+ const ln = String(d.frame.line).padStart(2, ' ');
75
+ lines.push(` ${ln} | ${truncateLine(d.frame.text, width - 8)}`);
76
+ lines.push(caretLine(d.frame.col, d.frame.length, gutter) + caretSuffix(d, raw, useColor));
44
77
  lines.push(' |');
78
+ } else {
79
+ lines.push(` --> ${color(useColor, ANSI.cyan, 'at ' + d.dotted)}`);
45
80
  }
46
81
 
47
- if (err.suggestion) {
48
- lines.push(' = ' + color(useColor, ANSI.yellow, 'help: ') + err.suggestion.text);
49
- }
50
- if (err.branchErrors && err.branchErrors.length) {
51
- const variant = (err.params && err.params.closestName) || 'closest variant';
52
- const n = err.branchErrors.length;
82
+ if (d.help) lines.push(' = ' + color(useColor, ANSI.yellow, 'help: ') + d.help);
83
+ if (d.branchErrors && d.branchErrors.length) {
84
+ const variant = (raw && raw.params && raw.params.closestName) || 'closest variant';
85
+ const n = d.branchErrors.length;
53
86
  lines.push(' = ' + color(useColor, ANSI.dim, 'note: ') + `closest match was ${variant} with ${n} error${n === 1 ? '' : 's'}:`);
54
- renderBranchErrors(err.branchErrors, 1, lines, useColor);
87
+ renderBranchErrors(d.branchErrors, 1, lines, useColor);
55
88
  }
56
- if (err.docUrl) {
57
- lines.push(' = ' + color(useColor, ANSI.dim, 'note: see ') + err.docUrl);
89
+ for (const note of d.notes) {
90
+ lines.push(' = ' + color(useColor, ANSI.dim, 'note: ') + note);
58
91
  }
59
92
  return lines.join('\n');
60
93
  }
@@ -70,7 +103,12 @@ function renderBranchErrors (subs, depth, lines, useColor) {
70
103
  const max = 3;
71
104
  const shown = subs.slice(0, max);
72
105
  for (const sub of shown) {
73
- lines.push(' ' + color(useColor, ANSI.dim, `${sub.keyword}: ${sub.message || ''}`));
106
+ // Observation first when the branch error carries one; the rule otherwise.
107
+ let text = sub.message || '';
108
+ if (sub.expected !== undefined) {
109
+ text = `expected ${sub.expected}` + (sub.received !== undefined ? `, found ${sub.received}` : '');
110
+ }
111
+ lines.push(' ' + color(useColor, ANSI.dim, `${sub.keyword}: ${text}`));
74
112
  if (sub.branchErrors && sub.branchErrors.length) renderBranchErrors(sub.branchErrors, depth + 1, lines, useColor);
75
113
  }
76
114
  if (subs.length > max) {
@@ -85,17 +123,28 @@ function renderPretty (errors, opts) {
85
123
  const maxErrors = opts.maxErrors != null ? opts.maxErrors : 20;
86
124
  const context = opts.context || 'input';
87
125
 
126
+ const diags = toDiagnostics(errors, sourceFor(errors, opts));
127
+
88
128
  const blocks = [];
89
- const limit = maxErrors === 0 ? errors.length : Math.min(maxErrors, errors.length);
90
- for (let i = 0; i < limit; i++) {
91
- blocks.push(renderOne(errors[i], useColor, opts));
92
- }
129
+ const limit = maxErrors === 0 ? diags.length : Math.min(maxErrors, diags.length);
130
+ for (let i = 0; i < limit; i++) blocks.push(renderOne(diags[i], useColor, opts));
131
+
93
132
  let out = blocks.join('\n\n');
94
- if (limit < errors.length) {
95
- out += `\n\n... and ${errors.length - limit} more errors (run with --pretty --max-errors=0 to see all)`;
133
+ if (limit < diags.length) {
134
+ out += `\n\n... and ${diags.length - limit} more errors (run with --pretty --max-errors=0 to see all)`;
96
135
  }
136
+ // The count never drifts from errors.length. When correlation collapsed a
137
+ // pair the second clause says so, so the reader can reconcile the two.
97
138
  const n = errors.length;
98
- out += `\n\n` + color(useColor, ANSI.red + ANSI.bold, `error: `) + `${n} schema violation${n === 1 ? '' : 's'} in ${context}`;
139
+ const shown = diags.length;
140
+ let summary = `${n} schema violation${n === 1 ? '' : 's'} in ${context}`;
141
+ if (shown !== n) summary += `, shown as ${shown} diagnostic${shown === 1 ? '' : 's'}`;
142
+ out += '\n\n' + color(useColor, ANSI.red + ANSI.bold, 'error: ') + summary;
143
+ // Said once, not under every block. The reader needs to know their line
144
+ // numbers will not match a file, and needs to be told one time.
145
+ if (diags.some((d) => d.frame && d.frame.synthesized)) {
146
+ out += '\n' + color(useColor, ANSI.dim, 'note: frames were reconstructed from the value, not from your input text; line numbers refer to that reconstruction');
147
+ }
99
148
  return out;
100
149
  }
101
150
 
package/lib/version.js CHANGED
@@ -7,4 +7,4 @@
7
7
  //
8
8
  // Kept in lockstep with package.json by `tests/test_version_sync.js`.
9
9
 
10
- module.exports = '1.8.2';
10
+ module.exports = '1.10.0';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ata-validator",
3
- "version": "1.8.2",
3
+ "version": "1.10.0",
4
4
  "description": "JSON Schema validation with first-class TypeScript and zero runtime cost. AOT compile to per-schema ESM modules with zero validator dependency. Generic Validator<T> for TypeBox/Zod/Valibot composition. Optional runtime API. Standard Schema V1 compatible.",
5
5
  "main": "index.js",
6
6
  "module": "index.mjs",
@@ -44,7 +44,7 @@
44
44
  "release:check": "node scripts/regen-safe-regex-source.js && node tests/test_pack_purity.js && node scripts/check-doc-coverage.js && node tests/test_error_codes_lock.js && node tests/test_safe_regex_source_sync.js && node tests/test_version_sync.js",
45
45
  "build": "cmake-js build --target ata",
46
46
  "rebuild": "cmake-js rebuild --target ata",
47
- "test": "node test.js && node tests/test_removed_aot_methods.js && node tests/test_no_native.js && node tests/test_no_eval.js && node tests/test_property_dependencies.js && node tests/test_v1_dialect.js && node tests/test_buffer_path_parity.js && node tests/test_buffer_gate.js && node tests/test_draft7_semantics.js && node tests/test_metaschema_ref.js && node tests/test_pure_js_unsupported.js && node tests/test_native_load_order.js && node tests/test_pack_purity.js && node tests/test_make_native_package.js && node tests/test_browser_nofs.js && node tests/test_browser_imports_guard.js && node tests/test_version_sync.js && node tests/test_native_loaded.js && node tests/test_safe_regex_source_sync.js && node tests/test_t_builder.js && node tests/test_async_refine.js && node tests/test_safe_regex.js && node tests/test_safe_regex_integration.js && node tests/test_aot_build.js && node tests/test_aot_differential.js && node tests/test_aot_cli_build.js && node tests/test_aot_cli_smoke.js && node tests/test_bundle_standalone.js && node tests/test_standalone_anyof.js && node tests/test_standalone_formats.js && node tests/test_aot_format_mode.js && node tests/test_aot_additional_props_errors.js && node tests/test_id_anchor_refs.js && node tests/test_engine_routing.js && node tests/test_engine_diagnostic.js && node tests/test_format_engine_parity.js && node tests/test_defs_pointer_alias.js && node tests/test_cross_doc_root_ref.js && node tests/test_codegen_entrypoint_agreement.js && node tests/test_hybrid_agreement.js && node tests/test_ajv_errors.js && node tests/test_pattern_properties_errors.js && node tests/test_no_input_mutation.js && node tests/test_typed_validator_runner.js && node tests/test_define_schema.js && node tests/test_error_codes_lock.js && node tests/test_error_code_lookup.js && node tests/test_error_order.js && node tests/test_value_equality.js && node tests/test_vocabulary.js && node tests/test_schema_scan.js && node tests/test_lazy_errors.js && node tests/test_plan_compiler.js && node tests/test_nullable.js && node tests/test_validate_and_parse.js && node tests/test_validate_data.js && node tests/test_enrich_error.js && node tests/test_enrich_received.js && node tests/test_rich_errors_optout.js && node tests/test_error_messages.js && node tests/test_source_positions.js && node tests/fuzz_positions.js && node tests/test_data_positions.js && node tests/test_render_shared.js && node tests/test_renderers.js && node tests/test_runtime_error_dx.js && node tests/test_aot_error_dx.js && node tests/test_abort_early.js && node tests/test_branch_collapse.js && node tests/test_suggestions.js && node tests/test_cli_validate.js && node tests/test_cli_version.js && node benchmark/bench_aot_size.mjs",
47
+ "test": "node test.js && node tests/test_removed_aot_methods.js && node tests/test_no_native.js && node tests/test_no_eval.js && node tests/test_property_dependencies.js && node tests/test_v1_dialect.js && node tests/test_buffer_path_parity.js && node tests/test_buffer_gate.js && node tests/test_draft7_semantics.js && node tests/test_metaschema_ref.js && node tests/test_pure_js_unsupported.js && node tests/test_native_load_order.js && node tests/test_pack_purity.js && node tests/test_make_native_package.js && node tests/test_browser_nofs.js && node tests/test_browser_imports_guard.js && node tests/test_version_sync.js && node tests/test_native_loaded.js && node tests/test_safe_regex_source_sync.js && node tests/test_t_builder.js && node tests/test_async_refine.js && node tests/test_safe_regex.js && node tests/test_safe_regex_integration.js && node tests/test_aot_build.js && node tests/test_aot_differential.js && node tests/test_aot_cli_build.js && node tests/test_aot_cli_smoke.js && node tests/test_bundle_standalone.js && node tests/test_standalone_anyof.js && node tests/test_standalone_formats.js && node tests/test_aot_format_mode.js && node tests/test_aot_additional_props_errors.js && node tests/test_id_anchor_refs.js && node tests/test_engine_routing.js && node tests/test_engine_diagnostic.js && node tests/test_format_engine_parity.js && node tests/test_formats_single_pass.js && node tests/test_defs_pointer_alias.js && node tests/test_cross_doc_root_ref.js && node tests/test_codegen_entrypoint_agreement.js && node tests/test_hybrid_agreement.js && node tests/test_codegen_edge_shapes.js && node tests/test_ajv_errors.js && node tests/test_pattern_properties_errors.js && node tests/test_no_input_mutation.js && node tests/test_typed_validator_runner.js && node tests/test_define_schema.js && node tests/test_error_codes_lock.js && node tests/test_error_code_lookup.js && node tests/test_error_order.js && node tests/test_value_equality.js && node tests/test_vocabulary.js && node tests/test_schema_scan.js && node tests/test_lazy_errors.js && node tests/test_plan_compiler.js && node tests/test_nullable.js && node tests/test_validate_and_parse.js && node tests/test_validate_data.js && node tests/test_enrich_error.js && node tests/test_enrich_received.js && node tests/test_rich_errors_optout.js && node tests/test_error_messages.js && node tests/test_source_positions.js && node tests/fuzz_positions.js && node tests/test_data_positions.js && node tests/test_render_shared.js && node tests/test_renderers.js && node tests/test_additive_fields.js && node tests/test_diagnostic_source.js && node tests/test_diagnose.js && node tests/test_correlate.js && node tests/test_diagnostics_score.js && node tests/test_runtime_error_dx.js && node tests/test_aot_error_dx.js && node tests/test_abort_early.js && node tests/test_branch_collapse.js && node tests/test_suggestions.js && node tests/test_cli_validate.js && node tests/test_cli_version.js && node benchmark/bench_aot_size.mjs",
48
48
  "bench:size": "node benchmark/bench_aot_size.mjs",
49
49
  "test:suite": "node tests/run_suite.js && node tests/run_suite.js draft7 && node tests/run_suite.js v1",
50
50
  "test:compat": "node tests/test_compat.js",
@@ -112,13 +112,13 @@
112
112
  "LICENSE"
113
113
  ],
114
114
  "optionalDependencies": {
115
- "@ata-validator/native-darwin-arm64": "1.8.2",
116
- "@ata-validator/native-darwin-x64": "1.8.2",
117
- "@ata-validator/native-linux-x64-gnu": "1.8.2",
118
- "@ata-validator/native-linux-arm64-gnu": "1.8.2",
119
- "@ata-validator/native-linux-x64-musl": "1.8.2",
120
- "@ata-validator/native-linux-arm64-musl": "1.8.2",
121
- "@ata-validator/native-win32-x64": "1.8.2"
115
+ "@ata-validator/native-darwin-arm64": "1.10.0",
116
+ "@ata-validator/native-darwin-x64": "1.10.0",
117
+ "@ata-validator/native-linux-x64-gnu": "1.10.0",
118
+ "@ata-validator/native-linux-arm64-gnu": "1.10.0",
119
+ "@ata-validator/native-linux-x64-musl": "1.10.0",
120
+ "@ata-validator/native-linux-arm64-musl": "1.10.0",
121
+ "@ata-validator/native-win32-x64": "1.10.0"
122
122
  },
123
123
  "peerDependencies": {
124
124
  "yaml": "^2.0.0"