Skip to content

Commit

Permalink
feat: better cyclic refs detection for tracing
Browse files Browse the repository at this point in the history
  • Loading branch information
ChALkeR committed Aug 9, 2023
1 parent c5831fe commit d60e444
Show file tree
Hide file tree
Showing 2 changed files with 372 additions and 13 deletions.
32 changes: 19 additions & 13 deletions src/compile.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ const constantValue = (schema) => {
return undefined
}

const refsNeedFullValidation = new WeakSet()
const rootMeta = new WeakMap()
const generateMeta = (root, $schema, enforce, requireSchema) => {
if ($schema) {
Expand Down Expand Up @@ -1184,19 +1185,24 @@ const compileSchema = (schema, root, opts, scope, basePathRoot = '') => {
fail('failed to resolve $ref:', $ref)
if (lintOnly) return null // failures are just collected in linter mode and don't throw, this makes a ref noop
}
// evaluation prior to ref application is needed for recursion cases
// This could be done better, but for now we check only the direct rules in the $ref
if (sub.type) evaluateDelta({ type: Array.isArray(sub.type) ? sub.type : [sub.type] })
for (const t of ['additionalProperties', 'unevaluatedProperties']) {
if (functions.hasOwn(sub, t) && isSchemaish(sub[t])) evaluateDelta({ properties: [true] })
}
for (const t of ['additionalItems', 'unevaluatedItems', 'items']) {
if (functions.hasOwn(sub, t) && isSchemaish(sub[t])) evaluateDelta({ items: Infinity })
}
for (const t of ['pattern', 'format']) {
if (typeof sub[t] === 'string') evaluateDelta({ fullstring: true })
const n = compileSub(sub, subRoot, path)
enforce(sub === schema || scope[n], 'coherence check')
if ((sub === schema || !scope[n][evaluatedStatic]) && sub.type) {
const type = Array.isArray(sub.type) ? sub.type : [sub.type]
evaluateDelta({ type })
if (requireValidation) {
// We are inside a cyclic ref, label it as a one that needs full validation to support assumption in next clause
refsNeedFullValidation.add(n)
// If validation is required, then a cyclic $ref is guranteed to validate all items and properties
if (type.includes('array')) evaluateDelta({ items: Infinity })
if (type.includes('object')) evaluateDelta({ properties: [true] })
}
if (requireStringValidation && type.includes('string')) {
refsNeedFullValidation.add(n)
evaluateDelta({ fullstring: true })
}
}
return applyRef(compileSub(sub, subRoot, path), { path: ['$ref'] })
return applyRef(n, { path: ['$ref'] })
})
if (getMeta().exclusiveRefs) {
enforce(!opts[optDynamic], 'unevaluated* is supported only on draft2019-09 and above')
Expand Down Expand Up @@ -1300,7 +1306,7 @@ const compileSchema = (schema, root, opts, scope, basePathRoot = '') => {
// 'not' does not mark anything as evaluated (unlike even if/then/else), so it's safe to exclude from these
// checks, as we are sure that everything will be checked without it. It can be viewed as a pure add-on.
const isRefTop = schema !== root && node === schema // We are at the top-level of an opaque ref inside the schema object
if (!isRefTop) {
if (!isRefTop || refsNeedFullValidation.has(funname)) {
if (!stat.type) enforceValidation('type')
// This can't be true for top-level schemas, only references with #/...
if (typeApplicable('array') && stat.items !== Infinity)
Expand Down
Loading

0 comments on commit d60e444

Please sign in to comment.