From d741ba3f6977a5bb50ce291c483cd0a7c5cd3257 Mon Sep 17 00:00:00 2001 From: Nikita Skovoroda Date: Fri, 26 Jan 2024 14:03:32 +0300 Subject: [PATCH] chore: simplify tracing of known type and required in nested rules E.g. in allOf/oneOf, we just rely on the checks in parent rule, but only for type/required checks. In the following schema, the nested allOf will just check for `{ required: ['yyy'] }`: `` { type: 'object', required: ['xxx','zzz'], allOf: [{ type: 'object', required: ['xxx', 'yyy'] }] } `` Properties/items can't be inherited as they affect evaluation checks, which should be isolated from the parent. --- doc/samples/draft-next/dynamicRef.md | 1 - doc/samples/draft2020-12/dynamicRef.md | 2 -- src/compile.js | 24 ++++++++++-------------- 3 files changed, 10 insertions(+), 17 deletions(-) diff --git a/doc/samples/draft-next/dynamicRef.md b/doc/samples/draft-next/dynamicRef.md index 3b1640f..3f691e0 100644 --- a/doc/samples/draft-next/dynamicRef.md +++ b/doc/samples/draft-next/dynamicRef.md @@ -296,7 +296,6 @@ const ref0 = function validate(data, dynAnchors = []) { const dynLocal = [{}] dynLocal[0]["#meta"] = validate if (!ref1(data, [...dynAnchors, dynLocal[0] || []])) return false - if (!(typeof data === "object" && data && !Array.isArray(data))) return false if (data.foo !== undefined && hasOwn(data, "foo")) { if (!(data.foo === "pass")) return false } diff --git a/doc/samples/draft2020-12/dynamicRef.md b/doc/samples/draft2020-12/dynamicRef.md index 82adafa..c15614b 100644 --- a/doc/samples/draft2020-12/dynamicRef.md +++ b/doc/samples/draft2020-12/dynamicRef.md @@ -553,7 +553,6 @@ const ref0 = function validate(data, dynAnchors = []) { const dynLocal = [{}] dynLocal[0]["#meta"] = validate if (!ref1(data, [...dynAnchors, dynLocal[0] || []])) return false - if (!(typeof data === "object" && data && !Array.isArray(data))) return false if (data.foo !== undefined && hasOwn(data, "foo")) { if (!(data.foo === "pass")) return false } @@ -618,7 +617,6 @@ const ref0 = function validate(data, dynAnchors = []) { const dynLocal = [{}] dynLocal[0]["#meta"] = validate if (!ref1(data, [...dynAnchors, dynLocal[0] || []])) return false - if (!(typeof data === "object" && data && !Array.isArray(data))) return false if (data.foo !== undefined && hasOwn(data, "foo")) { if (!(data.foo === "pass")) return false } diff --git a/src/compile.js b/src/compile.js index aa05996..bb7cf49 100644 --- a/src/compile.js +++ b/src/compile.js @@ -204,9 +204,9 @@ const compileSchema = (schema, root, opts, scope, basePathRoot = '') => { const visit = (errors, history, current, node, schemaPath, trace = {}, { constProp } = {}) => { // e.g. top-level data and property names, OR already checked by present() in history, OR in keys and not undefined const isSub = history.length > 0 && history[history.length - 1].prop === current - const queryCurrent = () => history.filter((h) => h.prop === current) + const statHistory = history.filter((h) => h.prop === current).map((h) => h.stat) // nested stat objects only for the current node const definitelyPresent = - !current.parent || current.checked || (current.inKeys && isJSON) || queryCurrent().length > 0 + !current.parent || current.checked || (current.inKeys && isJSON) || statHistory.length > 0 const name = buildName(current) const currPropImm = (...args) => propimm(current, ...args) @@ -452,14 +452,15 @@ const compileSchema = (schema, root, opts, scope, basePathRoot = '') => { /* Preparation and methods, post-$ref validation will begin at the end of the function */ + // Trust already applied { type, required } restrictions from parent rules for the current node + // Can't apply items/properties as those affect child unevaluated*, and { fullstring } is just not needed in same-node subrules + for (const { type, required } of statHistory) evaluateDelta({ type, required }) + // This is used for typechecks, null means * here const allIn = (arr, valid) => arr && arr.every((s) => valid.includes(s)) // all arr entries are in valid const someIn = (arr, possible) => possible.some((x) => arr === null || arr.includes(x)) // all possible are in arrs - - const parentCheckedType = (...valid) => queryCurrent().some((h) => allIn(h.stat.type, valid)) - const definitelyType = (...valid) => allIn(stat.type, valid) || parentCheckedType(...valid) - const typeApplicable = (...possible) => - someIn(stat.type, possible) && queryCurrent().every((h) => someIn(h.stat.type, possible)) + const definitelyType = (...valid) => allIn(stat.type, valid) + const typeApplicable = (...possible) => someIn(stat.type, possible) const enforceRegex = (source, target = node) => { enforce(typeof source === 'string', 'Invalid pattern:', source) @@ -756,9 +757,7 @@ const compileSchema = (schema, root, opts, scope, basePathRoot = '') => { } // if allErrors is false, we can skip present check for required properties validated before - const checked = (p) => - !allErrors && - (stat.required.includes(p) || queryCurrent().some((h) => h.stat.required.includes(p))) + const checked = (p) => !allErrors && stat.required.includes(p) const checkObjects = () => { const propertiesCount = format('Object.keys(%s).length', name) @@ -1284,10 +1283,7 @@ const compileSchema = (schema, root, opts, scope, basePathRoot = '') => { evaluateDelta({ type: [current.type] }) return null } - if (parentCheckedType(...typearr)) { - evaluateDelta({ type: typearr }) - return null - } + if (definitelyType(...typearr)) return null const filteredTypes = typearr.filter((t) => typeApplicable(t)) if (filteredTypes.length === 0) fail('No valid types possible') evaluateDelta({ type: typearr }) // can be safely done here, filteredTypes already prepared