ata-validator 1.38.0 → 1.39.1
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 +2 -2
- package/index.d.ts +2 -1
- package/lib/aot-impl.js +19 -13
- package/lib/buffer-gate.js +6 -7
- package/lib/enrich-error.js +4 -2
- package/lib/js-compiler.js +291 -38
- package/lib/rejections.js +44 -6
- package/lib/safe-regex.js +424 -413
- package/lib/version.js +1 -1
- package/package.json +11 -11
- package/lib/safe-regex-source.js +0 -8
package/README.md
CHANGED
|
@@ -32,13 +32,13 @@ npm install --save-dev ata-validator
|
|
|
32
32
|
npx ata build 'schemas/*.json' --out-dir src/generated
|
|
33
33
|
```
|
|
34
34
|
|
|
35
|
-
The `ata-validator` package itself is pure JavaScript. The native accelerator (simdjson parsing, parallel NDJSON, buffer APIs) ships as per-platform optional packages that npm installs automatically where they fit, the same pattern Vite uses for esbuild. Seven targets are built: macOS on arm64 and x64, Linux on x64 and arm64 against both glibc and musl, and Windows on x64. A platform without a prebuild still installs and validates, on the pure-JS engine. For a guaranteed zero-binary install:
|
|
35
|
+
The `ata-validator` package itself is pure JavaScript. The native accelerator (simdjson parsing, parallel NDJSON, buffer APIs) ships as per-platform optional packages that npm installs automatically where they fit, the same pattern Vite uses for esbuild. Seven targets are built: macOS on arm64 and x64, Linux on x64 and arm64 against both glibc (2.17 or later, below the 2.28 Node itself needs) and musl, and Windows on x64. A platform without a prebuild still installs and validates, on the pure-JS engine. For a guaranteed zero-binary install:
|
|
36
36
|
|
|
37
37
|
```bash
|
|
38
38
|
npm install ata-validator --omit=optional
|
|
39
39
|
```
|
|
40
40
|
|
|
41
|
-
or set `ATA_NO_NATIVE=1` at runtime. Typical schemas compile to specialized JS; shapes the compiler cannot represent (some `$dynamicRef`, cyclic `$ref`, unusual keyword interactions) fall back to an interpreted engine, so every schema validates in every environment. The pure-JS setup scores the same on the official suite as the native one, 1301 of 1301 Draft 2020-12 cases.
|
|
41
|
+
or set `ATA_NO_NATIVE=1` at runtime. Typical schemas compile to specialized JS; shapes the compiler cannot represent (some `$dynamicRef`, cyclic `$ref`, unusual keyword interactions) fall back to an interpreted engine, so every schema validates in every environment. The pure-JS setup scores the same on the official suite as the native one, 1301 of 1301 Draft 2020-12 cases. The buffer and parallel APIs (`isValid` on raw buffers, `isValidPrepadded`, `isValidNDJSON`, `isValidParallel`, `countValid`, `batchIsValid`, `validateAndParse`) work without the addon too, answering through the same checks as `isValidJSON()`, slower than the addon: 177 ns against 92 for a small document on Node 25. In a browser, which has no `Buffer`, they throw and name the methods to use instead.
|
|
42
42
|
|
|
43
43
|
Those four now agree with `validate()` on every case of the official suite, 3365 across three dialects. The native walker behind them does not handle every shape (`contains`, `unevaluatedProperties`, `patternProperties`, tuple `items`, cross-document `$ref`, a few formats), so for schemas using one of those the buffer APIs parse the bytes and answer through `validate()`; the list is in `lib/buffer-gate.js`. Typical request schemas stay on the zero-copy path. `npm test` holds the disagreement count at zero.
|
|
44
44
|
|
package/index.d.ts
CHANGED
|
@@ -439,7 +439,8 @@ export interface ValidatorOptions {
|
|
|
439
439
|
* answers `validate()`, `isValidObject()` and `validateJSON()`. For a schema
|
|
440
440
|
* that arrives from outside the trust boundary. The verdict is the same on
|
|
441
441
|
* every engine; the cost is not. The buffer APIs (`isValid`, `countValid`,
|
|
442
|
-
* `batchIsValid`)
|
|
442
|
+
* `batchIsValid`) use the native addon where it loads, which this option
|
|
443
|
+
* does not affect; without it they answer through the engine it picks.
|
|
443
444
|
*/
|
|
444
445
|
engine?: 'auto' | 'interpreter';
|
|
445
446
|
}
|
package/lib/aot-impl.js
CHANGED
|
@@ -17,7 +17,7 @@ const { compileToJSCodegenWithErrors, compileToJSCodegen, unevalContributions }
|
|
|
17
17
|
const { buildTargetedPositionMap } = require('./data-positions');
|
|
18
18
|
const { schemaHash } = require('./schema-hash');
|
|
19
19
|
const ATA_VERSION = require('./version');
|
|
20
|
-
const
|
|
20
|
+
const { engineSource: safeRegexEngineSource } = require('./safe-regex');
|
|
21
21
|
|
|
22
22
|
// Embedded verbatim in standalone modules so the output file has no runtime
|
|
23
23
|
// dependency on ata-validator. ASCII fast-path plus surrogate-aware slow path.
|
|
@@ -31,21 +31,16 @@ const _CP_LEN_SOURCE = `function _cpLen(s) {
|
|
|
31
31
|
return len;
|
|
32
32
|
}`;
|
|
33
33
|
|
|
34
|
-
// The linear-time regex engine,
|
|
35
|
-
//
|
|
36
|
-
//
|
|
37
|
-
//
|
|
38
|
-
//
|
|
39
|
-
// strips the strict directive and CommonJS exports and adds the `__ataSafeRe`
|
|
40
|
-
// alias the emitted code calls. The engine has no eval/new Function, so the
|
|
34
|
+
// The linear-time regex engine, embedded in standalone modules whose patterns
|
|
35
|
+
// need it so the output does not depend on ata-validator at run time. The text
|
|
36
|
+
// is the engine's own function (lib/safe-regex.js), called once for the
|
|
37
|
+
// `__ataSafeRe` the emitted code uses. No fs, path or __dirname, so this is
|
|
38
|
+
// safe in browser bundles, and the engine has no eval or new Function, so the
|
|
41
39
|
// embed is CSP-safe.
|
|
42
40
|
let _safeRegexEmbed = null;
|
|
43
41
|
function getSafeRegexEmbed() {
|
|
44
42
|
if (_safeRegexEmbed === null) {
|
|
45
|
-
const
|
|
46
|
-
.replace(/^'use strict'\s*\n/, '')
|
|
47
|
-
.replace(/\nmodule\.exports[^\n]*\n?/, '\n');
|
|
48
|
-
_safeRegexEmbed = body.trimEnd() + '\nconst __ataSafeRe = compileSafe;';
|
|
43
|
+
_safeRegexEmbed = 'const __ataSafeRe = (' + safeRegexEngineSource() + ')().compileSafe;';
|
|
49
44
|
}
|
|
50
45
|
return _safeRegexEmbed;
|
|
51
46
|
}
|
|
@@ -213,7 +208,18 @@ function toStandaloneModule(validator, opts) {
|
|
|
213
208
|
(source && sourceMap && schemaFile) ? { sourceMap, schemaFile } : null,
|
|
214
209
|
);
|
|
215
210
|
const errSrc = jsErrFn && jsErrFn._errSource ? jsErrFn._errSource : '';
|
|
216
|
-
if (errSrc) {
|
|
211
|
+
if (errSrc && jsErrFn._errFactory) {
|
|
212
|
+
// The helpers (patterns, name sets, definition and branch functions) are
|
|
213
|
+
// built once, not on every call. With $defs the
|
|
214
|
+
// cycle guard's state sits beside them, so a call made while one is
|
|
215
|
+
// running takes a function from a fresh factory call.
|
|
216
|
+
// Built on the first error read rather than at import, so a page that
|
|
217
|
+
// only ever accepts pays nothing for them at load.
|
|
218
|
+
const fac = jsErrFn._errFactory;
|
|
219
|
+
errCore = jsErrFn._errGuarded
|
|
220
|
+
? `const _mkErr = function() {\n ${fac}\n};\nlet _errMain = null;\nlet _errBusy = false;\nconst errFn = function(d, _all) { if (_errBusy) return _mkErr()(d, _all); if (_errMain === null) _errMain = _mkErr(); _errBusy = true; try { return _errMain(d, _all); } finally { _errBusy = false; } };\n`
|
|
221
|
+
: `const _mkErr = function() {\n ${fac}\n};\nlet _errMain = null;\nconst errFn = function(d, _all) { return (_errMain || (_errMain = _mkErr()))(d, _all); };\n`;
|
|
222
|
+
} else if (errSrc) {
|
|
217
223
|
errCore = `const errFn = function(d, _all) {\n ${errSrc}\n};\n`;
|
|
218
224
|
} else if (opts && typeof opts.onWarning === 'function') {
|
|
219
225
|
// The caller asked for error detail and is not getting it: the error
|
package/lib/buffer-gate.js
CHANGED
|
@@ -122,14 +122,13 @@ function toText(input, name) {
|
|
|
122
122
|
throw new TypeError(`${name}() requires a Buffer, Uint8Array, or string. For parsed objects, use isValidObject().`);
|
|
123
123
|
}
|
|
124
124
|
|
|
125
|
-
// Replaces the instance's buffer APIs with versions that
|
|
126
|
-
// validate()
|
|
125
|
+
// Replaces the instance's buffer APIs with versions that decode the bytes and
|
|
126
|
+
// ask isValidJSON(), which answers as validate() does on the parsed value and
|
|
127
|
+
// reads text the scanner covers without parsing it (a small document went from
|
|
128
|
+
// 350 to 155 ns without the addon). Its own native route is behind this same
|
|
129
|
+
// gate, so a schema routed here never reaches the walker through it.
|
|
127
130
|
function installSlowBufferApis(validator) {
|
|
128
|
-
const isValidText = (text) =>
|
|
129
|
-
let value;
|
|
130
|
-
try { value = JSON.parse(text); } catch { return false; }
|
|
131
|
-
return validator.validate(value).valid;
|
|
132
|
-
};
|
|
131
|
+
const isValidText = (text) => validator.isValidJSON(text);
|
|
133
132
|
validator.isValid = (input) => isValidText(toText(input, 'isValid'));
|
|
134
133
|
validator.isValidPrepadded = (paddedBuffer, jsonLength) =>
|
|
135
134
|
isValidText(Buffer.from(paddedBuffer.buffer, paddedBuffer.byteOffset, jsonLength).toString('utf8'));
|
package/lib/enrich-error.js
CHANGED
|
@@ -233,8 +233,10 @@ function enrich (rawErr, opts) {
|
|
|
233
233
|
if ('schema' in rawErr) out.schema = rawErr.schema;
|
|
234
234
|
|
|
235
235
|
// oneOf/anyOf collapse: preserve the nested branch errors so the pretty
|
|
236
|
-
// renderer can surface the closest variant's diagnostics.
|
|
237
|
-
|
|
236
|
+
// renderer can surface the closest variant's diagnostics. They are enriched
|
|
237
|
+
// like any other error: passed through raw, they carried whatever the engine
|
|
238
|
+
// that answered wrote, the generator's internal ordering key included.
|
|
239
|
+
if (rawErr.branchErrors) out.branchErrors = rawErr.branchErrors.map((b) => enrich(b, data !== undefined ? { data } : null));
|
|
238
240
|
|
|
239
241
|
if (positions && positions[path]) {
|
|
240
242
|
const p = positions[path];
|
package/lib/js-compiler.js
CHANGED
|
@@ -286,6 +286,9 @@ function compileToJS(schema, defs, schemaMap) {
|
|
|
286
286
|
|
|
287
287
|
if (!defs && needsBaseTracking(schema, schemaMap, new Set())) return null
|
|
288
288
|
if (!defs && externalDocsNeedInterpreter(schema, schemaMap)) return null
|
|
289
|
+
// A $ref cycle on one value recurses here as it does in generated code; see
|
|
290
|
+
// hasSameInstanceRefCycle.
|
|
291
|
+
if (!defs && hasSameInstanceRefCycle(schema)) return null
|
|
289
292
|
|
|
290
293
|
// unevaluatedProperties / unevaluatedItems need annotation tracking this
|
|
291
294
|
// path does not do. Decline so the schema reaches an engine that does.
|
|
@@ -1300,6 +1303,76 @@ function aliasChainEnds(def, defs) {
|
|
|
1300
1303
|
return true
|
|
1301
1304
|
}
|
|
1302
1305
|
|
|
1306
|
+
// When the properties a schema evaluates depend on which subschemas pass,
|
|
1307
|
+
// genUnevaluatedPropertiesNode picks one of a few runtime models: if/then/else,
|
|
1308
|
+
// one anyOf or oneOf, dependentSchemas, or patterns (root, allOf and a lone
|
|
1309
|
+
// if). Each model reads only the keywords it was written for. A schema whose
|
|
1310
|
+
// evaluated set comes from anything else was compiled with that part ignored,
|
|
1311
|
+
// and a lone `if` with properties emitted no check at all, which accepted every
|
|
1312
|
+
// object. This says whether the model the emitter will pick covers every
|
|
1313
|
+
// source of evaluated properties the schema has; when it does not, the
|
|
1314
|
+
// schema is not generated and the interpreter answers.
|
|
1315
|
+
function unevalPropsModeled(schema, schemaMap) {
|
|
1316
|
+
const own = Object.assign({}, schema)
|
|
1317
|
+
delete own.$defs
|
|
1318
|
+
delete own.definitions
|
|
1319
|
+
if (/"\$(?:ref|dynamicRef|recursiveRef)"/.test(JSON.stringify(own))) return false
|
|
1320
|
+
// A subschema whose evaluated names are a fixed list, which is all the
|
|
1321
|
+
// branch, dependentSchemas and if/then/else models can represent.
|
|
1322
|
+
const fixed = (s) => {
|
|
1323
|
+
if (typeof s === 'boolean') return true
|
|
1324
|
+
if (typeof s !== 'object' || s === null) return false
|
|
1325
|
+
const r = collectEvaluated(s, schemaMap)
|
|
1326
|
+
const j = JSON.stringify(s)
|
|
1327
|
+
return !r.dynamic && !r.allProps && !j.includes('"patternProperties"') && !j.includes('"unevaluatedProperties"')
|
|
1328
|
+
}
|
|
1329
|
+
const allOf = Array.isArray(schema.allOf) ? schema.allOf : []
|
|
1330
|
+
for (const sub of allOf) {
|
|
1331
|
+
if (typeof sub === 'boolean') continue
|
|
1332
|
+
if (typeof sub !== 'object' || sub === null) return false
|
|
1333
|
+
const r = collectEvaluated(sub, schemaMap)
|
|
1334
|
+
if (r.dynamic || r.allProps) return false
|
|
1335
|
+
const rest = Object.assign({}, sub)
|
|
1336
|
+
delete rest.patternProperties
|
|
1337
|
+
const j = JSON.stringify(rest)
|
|
1338
|
+
if (j.includes('"patternProperties"') || j.includes('"unevaluatedProperties"')) return false
|
|
1339
|
+
}
|
|
1340
|
+
const patterns = !!schema.patternProperties || allOf.some((s) => s && typeof s === 'object' && s.patternProperties)
|
|
1341
|
+
const branch = schema.anyOf || schema.oneOf
|
|
1342
|
+
if (schema.anyOf && schema.oneOf) return false
|
|
1343
|
+
const hasIf = schema.if !== undefined
|
|
1344
|
+
const hasTE = schema.then !== undefined || schema.else !== undefined
|
|
1345
|
+
const dep = schema.dependentSchemas
|
|
1346
|
+
if (schema.unevaluatedProperties !== false) {
|
|
1347
|
+
// The schema-valued model reads the fixed names and one branch keyword.
|
|
1348
|
+
if (hasIf || hasTE || dep || patterns) return false
|
|
1349
|
+
return !branch || branch.every(fixed)
|
|
1350
|
+
}
|
|
1351
|
+
if (hasIf && hasTE && !branch && !schema.patternProperties && !dep) {
|
|
1352
|
+
if (patterns) return false
|
|
1353
|
+
if (!fixed(schema.if) || (schema.then !== undefined && !fixed(schema.then)) || (schema.else !== undefined && !fixed(schema.else))) return false
|
|
1354
|
+
// The model credits the if's own `properties` names, and only those.
|
|
1355
|
+
const ifNames = typeof schema.if === 'object' ? collectEvaluated(schema.if, schemaMap).props : []
|
|
1356
|
+
const declared = typeof schema.if === 'object' && schema.if.properties ? Object.keys(schema.if.properties) : []
|
|
1357
|
+
return ifNames.length === declared.length && ifNames.every((k) => declared.includes(k))
|
|
1358
|
+
}
|
|
1359
|
+
if (branch) return !hasIf && !hasTE && !dep && !patterns && branch.every(fixed)
|
|
1360
|
+
if (dep) return !hasIf && !hasTE && !patterns && Object.values(dep).every(fixed)
|
|
1361
|
+
if (hasTE) return false
|
|
1362
|
+
if (hasIf) {
|
|
1363
|
+
// A lone if contributes through its top-level patternProperties only.
|
|
1364
|
+
if (typeof schema.if === 'boolean') return true
|
|
1365
|
+
if (typeof schema.if !== 'object' || schema.if === null) return false
|
|
1366
|
+
const r = collectEvaluated(schema.if, schemaMap)
|
|
1367
|
+
if (r.dynamic || r.allProps || r.props.length) return false
|
|
1368
|
+
const rest = Object.assign({}, schema.if)
|
|
1369
|
+
delete rest.patternProperties
|
|
1370
|
+
const j = JSON.stringify(rest)
|
|
1371
|
+
if (j.includes('"patternProperties"') || j.includes('"unevaluatedProperties"')) return false
|
|
1372
|
+
}
|
|
1373
|
+
return true
|
|
1374
|
+
}
|
|
1375
|
+
|
|
1303
1376
|
function codegenSafe(schema, schemaMap) {
|
|
1304
1377
|
if (typeof schema === 'boolean') return true
|
|
1305
1378
|
if (typeof schema !== 'object' || schema === null) return true
|
|
@@ -1416,6 +1489,7 @@ function codegenSafe(schema, schemaMap) {
|
|
|
1416
1489
|
if (schema.unevaluatedProperties === false || (typeof schema.unevaluatedProperties === 'object' && schema.unevaluatedProperties !== null)) {
|
|
1417
1490
|
const evalResult = collectEvaluated(schema, schemaMap)
|
|
1418
1491
|
if (evalResult.dynamic && evalResult.allProps) return false
|
|
1492
|
+
if (evalResult.dynamic && !unevalPropsModeled(schema, schemaMap)) return false
|
|
1419
1493
|
}
|
|
1420
1494
|
}
|
|
1421
1495
|
// unevaluatedItems: allow boolean and schema values
|
|
@@ -1754,8 +1828,85 @@ function externalDocsNeedInterpreter(schema, schemaMap) {
|
|
|
1754
1828
|
return false
|
|
1755
1829
|
}
|
|
1756
1830
|
|
|
1831
|
+
// A local $ref cycle that returns to a schema without moving to another value,
|
|
1832
|
+
// such as `A: { oneOf: [{ $ref: '#/$defs/A' }, ...] }`, never terminates on its
|
|
1833
|
+
// own. The interpreter stops it and answers; generated code recursed until the
|
|
1834
|
+
// stack ran out and threw a RangeError into the caller, on primitives, where
|
|
1835
|
+
// the cycle guard has no object to recognise. Cycles that pass through
|
|
1836
|
+
// properties, items or any other keyword that moves to a child value are
|
|
1837
|
+
// ordinary recursion and are not affected.
|
|
1838
|
+
const SAME_INSTANCE_LISTS = ['allOf', 'anyOf', 'oneOf']
|
|
1839
|
+
const SAME_INSTANCE_ONE = ['not', 'if', 'then', 'else']
|
|
1840
|
+
function localPointerTarget(root, ref) {
|
|
1841
|
+
if (typeof ref !== 'string' || ref.charCodeAt(0) !== 35) return null
|
|
1842
|
+
if (ref === '#') return root
|
|
1843
|
+
if (ref.charCodeAt(1) !== 47) return null
|
|
1844
|
+
let t = root
|
|
1845
|
+
for (let seg of ref.slice(2).split('/')) {
|
|
1846
|
+
if (seg.indexOf('%') >= 0) { try { seg = decodeURIComponent(seg) } catch { return null } }
|
|
1847
|
+
seg = seg.replace(/~1/g, '/').replace(/~0/g, '~')
|
|
1848
|
+
if (t === null || typeof t !== 'object') return null
|
|
1849
|
+
if (!Object.prototype.hasOwnProperty.call(t, seg)) {
|
|
1850
|
+
const alias = seg === 'definitions' ? '$defs' : seg === '$defs' ? 'definitions' : null
|
|
1851
|
+
if (alias === null || !Object.prototype.hasOwnProperty.call(t, alias)) return null
|
|
1852
|
+
seg = alias
|
|
1853
|
+
}
|
|
1854
|
+
t = t[seg]
|
|
1855
|
+
}
|
|
1856
|
+
return t !== null && typeof t === 'object' ? t : null
|
|
1857
|
+
}
|
|
1858
|
+
function sameInstanceEdges(root, node) {
|
|
1859
|
+
const out = []
|
|
1860
|
+
for (const k of SAME_INSTANCE_LISTS) if (Array.isArray(node[k])) for (const x of node[k]) if (x && typeof x === 'object') out.push(x)
|
|
1861
|
+
for (const k of SAME_INSTANCE_ONE) if (node[k] && typeof node[k] === 'object') out.push(node[k])
|
|
1862
|
+
if (node.dependentSchemas && typeof node.dependentSchemas === 'object') for (const x of Object.values(node.dependentSchemas)) if (x && typeof x === 'object') out.push(x)
|
|
1863
|
+
if (typeof node.$ref === 'string') { const t = localPointerTarget(root, node.$ref); if (t) out.push(t) }
|
|
1864
|
+
return out
|
|
1865
|
+
}
|
|
1866
|
+
const _sameInstanceCycleCache = new WeakMap()
|
|
1867
|
+
function hasSameInstanceRefCycle(root) {
|
|
1868
|
+
if (root === null || typeof root !== 'object') return false
|
|
1869
|
+
const hit = _sameInstanceCycleCache.get(root)
|
|
1870
|
+
if (hit !== undefined) return hit
|
|
1871
|
+
let found = false
|
|
1872
|
+
if (JSON.stringify(root).includes('"$ref"')) {
|
|
1873
|
+
// Only what validation can reach: definitions are entered through the
|
|
1874
|
+
// references that name them, so a broken definition nothing uses (a real
|
|
1875
|
+
// SchemaStore schema has one that includes itself) does not cost the
|
|
1876
|
+
// whole schema its generated code.
|
|
1877
|
+
const nodes = []
|
|
1878
|
+
const seen = new Set()
|
|
1879
|
+
const collect = (n) => {
|
|
1880
|
+
if (n === null || typeof n !== 'object' || seen.has(n)) return
|
|
1881
|
+
seen.add(n)
|
|
1882
|
+
if (Array.isArray(n)) { for (const v of n) collect(v); return }
|
|
1883
|
+
nodes.push(n)
|
|
1884
|
+
for (const [k, v] of Object.entries(n)) {
|
|
1885
|
+
if (k === '$defs' || k === 'definitions' || k === 'enum' || k === 'const' || k === 'default' || k === 'examples') continue
|
|
1886
|
+
collect(v)
|
|
1887
|
+
}
|
|
1888
|
+
if (typeof n.$ref === 'string') collect(localPointerTarget(root, n.$ref))
|
|
1889
|
+
}
|
|
1890
|
+
collect(root)
|
|
1891
|
+
const state = new Map() // 1 on the stack, 2 done
|
|
1892
|
+
const visit = (n) => {
|
|
1893
|
+
const st = state.get(n)
|
|
1894
|
+
if (st === 1) return true
|
|
1895
|
+
if (st === 2) return false
|
|
1896
|
+
state.set(n, 1)
|
|
1897
|
+
for (const m of sameInstanceEdges(root, n)) if (visit(m)) return true
|
|
1898
|
+
state.set(n, 2)
|
|
1899
|
+
return false
|
|
1900
|
+
}
|
|
1901
|
+
for (const n of nodes) if (visit(n)) { found = true; break }
|
|
1902
|
+
}
|
|
1903
|
+
_sameInstanceCycleCache.set(root, found)
|
|
1904
|
+
return found
|
|
1905
|
+
}
|
|
1906
|
+
|
|
1757
1907
|
function sharedCodegenGate(schema, schemaMap) {
|
|
1758
1908
|
if (typeof schema !== 'object' || schema === null) return true
|
|
1909
|
+
if (hasSameInstanceRefCycle(schema)) return false
|
|
1759
1910
|
if (!codegenSafe(schema, schemaMap)) return false
|
|
1760
1911
|
if (needsBaseTracking(schema, schemaMap, new Set())) return false
|
|
1761
1912
|
if (externalDocsNeedInterpreter(schema, schemaMap)) return false
|
|
@@ -2562,8 +2713,14 @@ function genCodeNode(schema, v, lines, ctx, knownType) {
|
|
|
2562
2713
|
// No `properties` means nothing is declared, so every key is additional.
|
|
2563
2714
|
// The generators used to emit nothing for that shape, and the safety gate
|
|
2564
2715
|
// declined it everywhere; at any depth it now compiles.
|
|
2565
|
-
const
|
|
2566
|
-
const
|
|
2716
|
+
const declaredKeys = Object.keys(schema.properties || {})
|
|
2717
|
+
const propCount = declaredKeys.length
|
|
2718
|
+
// Counting keys proves there is no extra key only when every declared
|
|
2719
|
+
// name is required. Comparing the list lengths was not that: with
|
|
2720
|
+
// `properties: {c}` and `required: ['b']` the counts matched, and `{b: 1}`
|
|
2721
|
+
// was accepted although `b` is an additional property.
|
|
2722
|
+
const req = new Set(schema.required || [])
|
|
2723
|
+
const allRequired = !!schema.required && declaredKeys.every((k) => req.has(k))
|
|
2567
2724
|
const inner = allRequired
|
|
2568
2725
|
? (propCount <= 15
|
|
2569
2726
|
? `var _n=0;for(var _k in ${v})_n++;if(_n!==${propCount})return false`
|
|
@@ -2670,8 +2827,12 @@ function genCodeNode(schema, v, lines, ctx, knownType) {
|
|
|
2670
2827
|
}
|
|
2671
2828
|
|
|
2672
2829
|
// anyOf — branch fns hoisted when safe, inline fallback when recursive.
|
|
2673
|
-
//
|
|
2674
|
-
|
|
2830
|
+
// The anyOf model in genUnevaluatedPropertiesNode checks that a branch
|
|
2831
|
+
// passes while it records what the branches evaluate, so anyOf is emitted
|
|
2832
|
+
// once, there. Only that model does: with unevaluatedProperties true or a
|
|
2833
|
+
// schema, skipping it here left anyOf unchecked and accepted a document no
|
|
2834
|
+
// branch matched.
|
|
2835
|
+
if (schema.anyOf && !unevalChecksAnyOf(schema, ctx)) genAnyOfNode(schema, v, lines, ctx, knownType)
|
|
2675
2836
|
|
|
2676
2837
|
// oneOf — branch fns hoisted to factory scope when safe (no recursion/ref).
|
|
2677
2838
|
// Falls back to inline closures if any branch touches recursive validation.
|
|
@@ -3203,6 +3364,12 @@ function genPatternPropertiesNode(schema, v, lines, ctx, knownType, isObj) {
|
|
|
3203
3364
|
return handledPropertyNames
|
|
3204
3365
|
}
|
|
3205
3366
|
|
|
3367
|
+
function unevalChecksAnyOf(schema, ctx) {
|
|
3368
|
+
if (schema.unevaluatedProperties !== false) return false
|
|
3369
|
+
const r = collectEvaluated(schema, ctx.schemaMap, ctx.rootDefs)
|
|
3370
|
+
return r.dynamic && !r.allProps
|
|
3371
|
+
}
|
|
3372
|
+
|
|
3206
3373
|
function genUnevaluatedPropertiesNode(schema, v, lines, ctx, knownType, isObj, hoisted, earlyKeyCount) {
|
|
3207
3374
|
const evalResult = collectEvaluated(schema, ctx.schemaMap, ctx.rootDefs)
|
|
3208
3375
|
|
|
@@ -3481,6 +3648,16 @@ function genUnevaluatedPropertiesNode(schema, v, lines, ctx, knownType, isObj, h
|
|
|
3481
3648
|
const inner = `for(var _k in ${v}){if(${evVar}[_k])continue;${reVars.map(rv => `if(${rv}.test(_k)){${evVar}[_k]=1;continue}`).join('')}return false}`
|
|
3482
3649
|
_deferOrInline(ctx, lines, v, isObj ? inner + '}' : `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){${inner}}}`)
|
|
3483
3650
|
}
|
|
3651
|
+
} else {
|
|
3652
|
+
// Nothing dynamic contributes a name (a lone if with no properties
|
|
3653
|
+
// or patterns, say): only the fixed names are evaluated. This branch
|
|
3654
|
+
// used to emit nothing, and nothing read as valid.
|
|
3655
|
+
const ei = ctx.varCounter++
|
|
3656
|
+
const evVar = `_ev${ei}`
|
|
3657
|
+
lines.push(`{const ${evVar}={}`)
|
|
3658
|
+
for (const k of baseProps) lines.push(`${evVar}[${JSON.stringify(k)}]=1`)
|
|
3659
|
+
const inner = `for(var _k in ${v}){if(!${evVar}[_k])return false}`
|
|
3660
|
+
_deferOrInline(ctx, lines, v, isObj ? inner + '}' : `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){${inner}}}`)
|
|
3484
3661
|
}
|
|
3485
3662
|
}
|
|
3486
3663
|
} else if (typeof schema.unevaluatedProperties === 'object') {
|
|
@@ -4194,17 +4371,44 @@ function compileToJSCodegenWithErrors(schema, schemaMap, userFormats, sourceOpts
|
|
|
4194
4371
|
// function and were rebuilt on every call, which on a product schema with
|
|
4195
4372
|
// six URLs and one anyOf cost about 450 ns before a single check ran, more
|
|
4196
4373
|
// than every check and every error together. Here they run once, in a
|
|
4197
|
-
// factory, as the verdict function's preamble does
|
|
4198
|
-
//
|
|
4199
|
-
//
|
|
4200
|
-
//
|
|
4201
|
-
|
|
4202
|
-
|
|
4203
|
-
|
|
4374
|
+
// factory, as the verdict function's preamble does; with $defs or
|
|
4375
|
+
// recursion too, below. `_errSource` stays `body` either way, for the
|
|
4376
|
+
// emitters.
|
|
4377
|
+
// The factory builds the helpers once and returns the error function.
|
|
4378
|
+
// With $defs or recursion the helpers are the definitions' functions and
|
|
4379
|
+
// the cycle guard's state lives beside them; the guarded run resets that
|
|
4380
|
+
// state at its start, and a call made while one is running (a custom
|
|
4381
|
+
// keyword validating with the same validator) gets a function from a fresh
|
|
4382
|
+
// factory call instead of sharing it. `_errFactory` carries the same text
|
|
4383
|
+
// to the standalone emitter.
|
|
4384
|
+
let factoryBody
|
|
4385
|
+
if (needsGuardE) {
|
|
4386
|
+
const attempt = ctx.usesRecursion ? '_validateE(d,_all,_e)' : '_bodyE(d,_all,_e)'
|
|
4387
|
+
const runner = ctx.usesRecursion
|
|
4388
|
+
? emitRootGuard('_validateE', 'd,_all,_e', '') + `function _validateE_b(d,_all,_e){\n ${checkStr}\n }\n `
|
|
4389
|
+
: `function _bodyE(d,_all,_e){\n ${checkStr}\n }\n `
|
|
4390
|
+
factoryBody = guardStrE + helpersE + runner + `return function(d,_all){const _e=[];\n ` +
|
|
4391
|
+
emitGuardedRun(attempt, '_e.length=0;', defSetsE) + `return{valid:_e.length===0,errors:_e}}`
|
|
4392
|
+
} else {
|
|
4393
|
+
factoryBody = helpersE + 'return function(d,_all){const _e=[];\n ' + checkStr +
|
|
4204
4394
|
'\n return{valid:_e.length===0,errors:_e}}'
|
|
4205
|
-
|
|
4206
|
-
|
|
4207
|
-
|
|
4395
|
+
}
|
|
4396
|
+
const hoistable = !ctx.usesRecursion && defSetsE.length === 0 && helpersE !== ''
|
|
4397
|
+
if (hoistable || needsGuardE) {
|
|
4398
|
+
const params = [...cvars, '__ataSafeRe']
|
|
4399
|
+
const args = [...cvals, compileSafe]
|
|
4400
|
+
const make = new Function(...params, factoryBody)
|
|
4401
|
+
const main = make(...args)
|
|
4402
|
+
if (needsGuardE) {
|
|
4403
|
+
let busy = false
|
|
4404
|
+
fn = (d, _all) => {
|
|
4405
|
+
if (busy) return make(...args)(d, _all)
|
|
4406
|
+
busy = true
|
|
4407
|
+
try { return main(d, _all) } finally { busy = false }
|
|
4408
|
+
}
|
|
4409
|
+
} else {
|
|
4410
|
+
fn = main
|
|
4411
|
+
}
|
|
4208
4412
|
} else if (ctx.usesSafeRe) {
|
|
4209
4413
|
// Inlined helperCode references __ataSafeRe; bind the safe-regex factory.
|
|
4210
4414
|
// Standalone keeps the inline _errSource — the embed defines __ataSafeRe.
|
|
@@ -4219,6 +4423,8 @@ function compileToJSCodegenWithErrors(schema, schemaMap, userFormats, sourceOpts
|
|
|
4219
4423
|
fn = new Function('d', '_all', body)
|
|
4220
4424
|
}
|
|
4221
4425
|
fn._errSource = body
|
|
4426
|
+
fn._errFactory = factoryBody
|
|
4427
|
+
fn._errGuarded = needsGuardE
|
|
4222
4428
|
fn._sharedHelpers = ctx.shared ? ctx.shared.slice() : []
|
|
4223
4429
|
fn._usesSafeRe = !!ctx.usesSafeRe
|
|
4224
4430
|
if (cvars.length) {
|
|
@@ -4249,32 +4455,65 @@ function emitBranchCollapse (branches, keyword, v, pathExpr, lines, ctx, schemaP
|
|
|
4249
4455
|
const branchKw = keyword
|
|
4250
4456
|
const branchSp = schemaPrefix + '/' + branchKw
|
|
4251
4457
|
// Per-branch error-collecting functions, each returning {valid,errors}.
|
|
4458
|
+
// They take the value, its instance path, `_all` and, inside a definition
|
|
4459
|
+
// written once for all its references, that reference's schema path, so
|
|
4460
|
+
// they can be built once per call of the error function rather than each
|
|
4461
|
+
// time the keyword is reached: on a list of 1723 tokens that is thousands
|
|
4462
|
+
// of closure arrays per validation.
|
|
4252
4463
|
const fns = []
|
|
4253
4464
|
const titles = []
|
|
4465
|
+
let usesSp = false
|
|
4254
4466
|
for (let i = 0; i < branches.length; i++) {
|
|
4255
4467
|
const sub = branches[i]
|
|
4256
4468
|
const subLines = []
|
|
4257
4469
|
const subSp = branchSp + '/' + i
|
|
4258
4470
|
if (typeof sub === 'object' && sub !== null) {
|
|
4259
|
-
genCodeE(sub, '_bv',
|
|
4471
|
+
genCodeE(sub, '_bv', '_bpth', subLines, ctx, subSp)
|
|
4260
4472
|
} else if (sub === false) {
|
|
4261
4473
|
// A false branch never matches; it reports the same error the
|
|
4262
4474
|
// interpreter attaches to a false schema.
|
|
4263
|
-
subLines.push(`_e.push({keyword:'not',instancePath
|
|
4475
|
+
subLines.push(`_e.push({keyword:'not',instancePath:_bpth,schemaPath:'${subSp}'${ordinalField(ctx, `${subSp}`)},params:{},message:'boolean schema is false'})`)
|
|
4264
4476
|
}
|
|
4265
4477
|
const title = (sub && typeof sub === 'object' && typeof sub.title === 'string') ? sub.title : ''
|
|
4266
4478
|
titles.push(title)
|
|
4267
|
-
|
|
4268
|
-
|
|
4269
|
-
|
|
4479
|
+
let body = subLines.length === 0 ? '' : `const _e=[];${subLines.join(';')};return{valid:_e.length===0,errors:_e}`
|
|
4480
|
+
if (body.includes(SP_PLACEHOLDER)) {
|
|
4481
|
+
body = body.split(`'${SP_PLACEHOLDER}`).join(`_sp+'`).split(`"${SP_PLACEHOLDER}`).join(`_sp+"`)
|
|
4482
|
+
if (body.includes(SP_PLACEHOLDER)) throw DECLINE
|
|
4483
|
+
usesSp = true
|
|
4484
|
+
}
|
|
4270
4485
|
fns.push(body)
|
|
4271
4486
|
}
|
|
4487
|
+
const params = usesSp ? '_bv,_bpth,_all,_sp' : '_bv,_bpth,_all'
|
|
4272
4488
|
const fnArr = `_brf${fi}`
|
|
4273
4489
|
const resArr = `_brr${fi}`
|
|
4274
4490
|
const titleArr = JSON.stringify(titles)
|
|
4275
4491
|
const collapsed = `_brc${fi}`
|
|
4276
4492
|
const pp = pathExpr || '""'
|
|
4277
|
-
|
|
4493
|
+
ctx.helperCode.push(`const ${fnArr}=[${fns.map((b) => b === '' ? `function(){return{valid:true,errors:[]}}` : `function(${params}){${b}}`).join(',')}];const _bt${fi}=${titleArr}`)
|
|
4494
|
+
const call = `${fnArr}[_bi](${v},_pp${fi},_all${usesSp ? ',_sp' : ''})`
|
|
4495
|
+
const ord = (() => { const o = ordinalFor(ctx.rootSchema, branchSp); return o === null ? 'null' : o })()
|
|
4496
|
+
const report = `const ${collapsed}=__ataCollapse('${branchKw}',${resArr},_pp${fi},'${branchSp}',${ord});if(${collapsed}){_e.push(${collapsed});if(!_all)return{valid:false,errors:_e}}`
|
|
4497
|
+
if (keyword === 'anyOf') {
|
|
4498
|
+
// anyOf reports nothing once a branch passes, so the branches are first
|
|
4499
|
+
// only asked whether they pass. They are called with `_all` set to 0, a
|
|
4500
|
+
// third value next to true (collect everything) and false (stop at the
|
|
4501
|
+
// first error, for abortEarly): every check reads it as `!_all` and stops
|
|
4502
|
+
// at its first error, and an anyOf reached that way that fails returns
|
|
4503
|
+
// at once instead of collecting its branches and collapsing them, since
|
|
4504
|
+
// the caller only wants to know. An object tried against a branch that is
|
|
4505
|
+
// an anyOf of primitive types used to collapse that anyOf in full and then
|
|
4506
|
+
// pass the next branch, which on a list of 1723 tokens was 2353 collapses
|
|
4507
|
+
// no one read. Only when no branch passes do they run again with the
|
|
4508
|
+
// caller's `_all`, and the collapse sees them all, as before.
|
|
4509
|
+
const probe = `${fnArr}[_bi](${v},_pp${fi},0${usesSp ? ',_sp' : ''})`
|
|
4510
|
+
lines.push(`{const _pp${fi}=${pp};let _bp${fi}=false;for(let _bi=0;_bi<${fnArr}.length;_bi++){if(${probe}.valid){_bp${fi}=true;break}}if(!_bp${fi}){if(_all===0){_e.push(null);return{valid:false,errors:_e}}const ${resArr}=[];for(let _bi=0;_bi<${fnArr}.length;_bi++){const _br=${call};${resArr}.push({valid:false,errors:_br.errors,title:_bt${fi}[_bi]})}${report}}}`)
|
|
4511
|
+
} else {
|
|
4512
|
+
// Asked only whether it passes (`_all` 0, see anyOf above), oneOf counts
|
|
4513
|
+
// the passing branches and answers; the branch errors of a probe are not
|
|
4514
|
+
// errors anyone reads and are not collapsed.
|
|
4515
|
+
lines.push(`{const _pp${fi}=${pp};const ${resArr}=[];for(let _bi=0;_bi<${fnArr}.length;_bi++){const _br=${call};${resArr}.push({valid:_br.valid,errors:_br.errors,title:_bt${fi}[_bi]})}if(_all===0){let _n=0;for(const _r of ${resArr})if(_r.valid)_n++;if(_n!==1){_e.push(null);return{valid:false,errors:_e}}}else{${report}}}`)
|
|
4516
|
+
}
|
|
4278
4517
|
}
|
|
4279
4518
|
|
|
4280
4519
|
// Error-collecting code generator.
|
|
@@ -4589,7 +4828,10 @@ function genCodeENode(schema, v, pathExpr, lines, ctx, schemaPrefix) {
|
|
|
4589
4828
|
const gateChecks = patChecks.map((c) => c.split(`_k${ci}[_i]`).join(`_f${ci}`))
|
|
4590
4829
|
const gateTest = gateChecks.length ? `!_a${ci}.has(_f${ci})&&!(${gateChecks.join('||')})` : `!_a${ci}.has(_f${ci})`
|
|
4591
4830
|
const detail = `const _k${ci}=Object.keys(${v});for(let _i=0;_i<_k${ci}.length;_i++){if(${isAdditional}){_e.push({code:'${apLit.codeStr}',keyword:'additionalProperties',instancePath:${pathExpr||'""'},schemaPath:'${apSp}'${ordinalField(ctx, `${apSp}`)},params:{additionalProperty:_k${ci}[_i]},message:'must NOT have additional properties',docUrl:'${apLit.docUrl}'${apLit.frame}});if(!_all)return{valid:false,errors:_e}}}`
|
|
4592
|
-
|
|
4831
|
+
// The name set is built once per call of the error function, not at every
|
|
4832
|
+
// object it checks: on a list of 1723 tokens it was 1723 Sets.
|
|
4833
|
+
ctx.helperCode.push(`const _a${ci}=new Set([${allowed}])`)
|
|
4834
|
+
const inner = `let _x${ci}=0;for(const _f${ci} in ${v}){if(${gateTest}){_x${ci}=1;break}}if(_x${ci}){${detail}}`
|
|
4593
4835
|
lines.push(isObj ? `{${inner}}` : `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){${inner}}`)
|
|
4594
4836
|
}
|
|
4595
4837
|
|
|
@@ -4745,7 +4987,8 @@ function genUnevaluatedFalseE(schema, v, pathExpr, lines, ctx, schemaPrefix, isO
|
|
|
4745
4987
|
patChecksU.push(`_re${ctx.regExpMap.get(pattern)}.test(_k${ui}[_i])`)
|
|
4746
4988
|
}
|
|
4747
4989
|
const isUneval = patChecksU.length ? `!_a${ui}.has(_k${ui}[_i])&&!(${patChecksU.join('||')})` : `!_a${ui}.has(_k${ui}[_i])`
|
|
4748
|
-
|
|
4990
|
+
ctx.helperCode.push(`const _a${ui}=new Set([${allowedU}])`)
|
|
4991
|
+
const innerU = `const _k${ui}=Object.keys(${v});for(let _i=0;_i<_k${ui}.length;_i++){if(${isUneval}){_e.push({code:'${upLit.codeStr}',keyword:'unevaluatedProperties',instancePath:${pathExpr||'""'},schemaPath:'${upSp}'${ordinalField(ctx, `${upSp}`)},params:{unevaluatedProperty:_k${ui}[_i]},message:'must NOT have unevaluated properties',docUrl:'${upLit.docUrl}'${upLit.frame}});if(!_all)return{valid:false,errors:_e}}}`
|
|
4749
4992
|
lines.push(isObj ? `{${innerU}}` : `if(typeof ${v}==='object'&&${v}!==null&&!Array.isArray(${v})){${innerU}}`)
|
|
4750
4993
|
}
|
|
4751
4994
|
|
|
@@ -4787,7 +5030,8 @@ function genPatternPropertiesE(schema, v, pathExpr, lines, ctx, schemaPrefix) {
|
|
|
4787
5030
|
function genAdditionalSchemaE(schema, v, pathExpr, lines, ctx, schemaPrefix) {
|
|
4788
5031
|
const ki = ctx.varCounter++
|
|
4789
5032
|
const known = Object.keys(schema.properties || {})
|
|
4790
|
-
|
|
5033
|
+
if (known.length) ctx.helperCode.push(`const _ak${ki}=new Set([${known.map((k) => JSON.stringify(k)).join(',')}])`)
|
|
5034
|
+
const guard = ''
|
|
4791
5035
|
// A key matched by any patternProperties entry is not additional.
|
|
4792
5036
|
const patTests = []
|
|
4793
5037
|
for (const pat of Object.keys(schema.patternProperties || {})) {
|
|
@@ -5160,6 +5404,27 @@ function genCodeCNode(schema, v, pathExpr, lines, ctx, schemaPrefix) {
|
|
|
5160
5404
|
return `(_e||(_e=[])).push({keyword:'${keyword}',instancePath:${pathExpr||'""'},schemaPath:'${sp}',params:${paramsCode},message:${msgCode}${ordinalField(ctx, sp)}})`
|
|
5161
5405
|
}
|
|
5162
5406
|
|
|
5407
|
+
// enum and const apply to a value of any type, so a value that fails
|
|
5408
|
+
// `type` is still checked against them, as the other engines do. They sat
|
|
5409
|
+
// inside the type-success block, and `{type: 'number', enum: [1]}` reported
|
|
5410
|
+
// only `type` for 'x' where the interpreter reports both.
|
|
5411
|
+
const emitEnumConst = () => {
|
|
5412
|
+
// enum
|
|
5413
|
+
if (schema.enum) {
|
|
5414
|
+
lines.push(`if(!(${enumCondition(ctx, schema.enum, v)})){${fail('enum', 'enum', `{allowedValues:${JSON.stringify(schema.enum)}}`, "'must be equal to one of the allowed values'")}}`)
|
|
5415
|
+
}
|
|
5416
|
+
|
|
5417
|
+
// const
|
|
5418
|
+
if (schema.const !== undefined) {
|
|
5419
|
+
const cv = schema.const
|
|
5420
|
+
if (cv === null || typeof cv !== 'object') {
|
|
5421
|
+
lines.push(`if(${v}!==${JSON.stringify(cv)}){${fail('const', 'const', `{allowedValue:${JSON.stringify(schema.const)}}`, "'must be equal to constant'")}}`)
|
|
5422
|
+
} else {
|
|
5423
|
+
lines.push(`if(!${emitDeq(ctx)}(${v},${emitConstant(ctx, cv)})){${fail('const', 'const', `{allowedValue:JSON.parse(${JSON.stringify(JSON.stringify(schema.const))})}`, "'must be equal to constant'")}}`)
|
|
5424
|
+
}
|
|
5425
|
+
}
|
|
5426
|
+
}
|
|
5427
|
+
|
|
5163
5428
|
if (types) {
|
|
5164
5429
|
const conds = types.map(t => {
|
|
5165
5430
|
switch (t) {
|
|
@@ -5186,22 +5451,10 @@ function genCodeCNode(schema, v, pathExpr, lines, ctx, schemaPrefix) {
|
|
|
5186
5451
|
isStr = types[0] === 'string'
|
|
5187
5452
|
isNum = types[0] === 'number' || types[0] === 'integer'
|
|
5188
5453
|
}
|
|
5454
|
+
emitEnumConst()
|
|
5189
5455
|
lines.push(`if(${typeOk}){`)
|
|
5190
|
-
}
|
|
5191
|
-
|
|
5192
|
-
// enum
|
|
5193
|
-
if (schema.enum) {
|
|
5194
|
-
lines.push(`if(!(${enumCondition(ctx, schema.enum, v)})){${fail('enum', 'enum', `{allowedValues:${JSON.stringify(schema.enum)}}`, "'must be equal to one of the allowed values'")}}`)
|
|
5195
|
-
}
|
|
5196
|
-
|
|
5197
|
-
// const
|
|
5198
|
-
if (schema.const !== undefined) {
|
|
5199
|
-
const cv = schema.const
|
|
5200
|
-
if (cv === null || typeof cv !== 'object') {
|
|
5201
|
-
lines.push(`if(${v}!==${JSON.stringify(cv)}){${fail('const', 'const', `{allowedValue:${JSON.stringify(schema.const)}}`, "'must be equal to constant'")}}`)
|
|
5202
|
-
} else {
|
|
5203
|
-
lines.push(`if(!${emitDeq(ctx)}(${v},${emitConstant(ctx, cv)})){${fail('const', 'const', `{allowedValue:JSON.parse(${JSON.stringify(JSON.stringify(schema.const))})}`, "'must be equal to constant'")}}`)
|
|
5204
|
-
}
|
|
5456
|
+
} else {
|
|
5457
|
+
emitEnumConst()
|
|
5205
5458
|
}
|
|
5206
5459
|
|
|
5207
5460
|
// required — use destructuring when type is object (SAFE because type check already passed)
|