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 acba62e
Show file tree
Hide file tree
Showing 2 changed files with 190 additions and 13 deletions.
28 changes: 15 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,20 @@ 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] })
}
}
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 +1302,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
175 changes: 175 additions & 0 deletions test/regressions/strong-refs.js
Original file line number Diff line number Diff line change
Expand Up @@ -278,3 +278,178 @@ tape('partially validating local ref can be used (strings)', (t) => {
})
t.end()
})

tape('cyclic local refs do full validation', (t) => {
const compile = ($defs) =>
validator(
{
$schema: 'https://json-schema.org/draft/2020-12/schema',
$defs,
$ref: '#/$defs/A',
},
{ mode: 'strong' }
)

t.throws(() => {
compile({
A: {
type: ['array'],
},
})
})

t.throws(() => {
compile({
A: {
type: ['array', 'object'],
items: {
$ref: '#/$defs/A',
},
},
})
})

t.doesNotThrow(() => {
const validate = compile({
A: {
type: ['array'],
items: {
$ref: '#/$defs/A',
},
},
})
t.notOk(validate({}), '{}')
t.ok(validate([]), '[]')
t.ok(validate([[]]), '[[]]')
t.ok(validate([[[]]]), '[[[]]]')
t.notOk(validate([1]), '[1]')
t.notOk(validate([{}]), '[{}]')
})

t.doesNotThrow(() => {
const validate = compile({
A: {
type: ['array', 'object'],
oneOf: [
{
type: 'array',
items: {
$ref: '#/$defs/A',
},
},
{
type: 'object',
required: [],
additionalProperties: false,
properties: {
x: { $ref: '#/$defs/A' },
},
},
],
},
})
t.ok(validate({}), '{}')
t.ok(validate([]), '[]')
t.ok(validate([[]]), '[[]]')
t.ok(validate([[[]]]), '[[[]]]')
t.notOk(validate([1]), '[1]')
t.ok(validate([{}]), '[{}]')
})

t.throws(() => {
compile({
A: {
type: ['array', 'object'],
oneOf: [
{
type: 'array',
items: {
$ref: '#/$defs/B',
},
},
{
type: 'object',
required: [],
additionalProperties: false,
properties: {
x: { $ref: '#/$defs/B' },
},
},
],
},
B: {
type: ['array', 'object'],
oneOf: [
{
type: 'array',
},
{
type: 'object',
required: [],
additionalProperties: false,
properties: {
x: { $ref: '#/$defs/A' },
},
},
],
},
})
t.ok(validate({}), '{}')

Check failure on line 397 in test/regressions/strong-refs.js

View workflow job for this annotation

GitHub Actions / build (18.x)

'validate' is not defined

Check failure on line 397 in test/regressions/strong-refs.js

View workflow job for this annotation

GitHub Actions / build (16.x)

'validate' is not defined

Check failure on line 397 in test/regressions/strong-refs.js

View workflow job for this annotation

GitHub Actions / build (10.x)

'validate' is not defined

Check failure on line 397 in test/regressions/strong-refs.js

View workflow job for this annotation

GitHub Actions / build (14.x)

'validate' is not defined

Check failure on line 397 in test/regressions/strong-refs.js

View workflow job for this annotation

GitHub Actions / build (14.x)

'validate' is not defined

Check failure on line 397 in test/regressions/strong-refs.js

View workflow job for this annotation

GitHub Actions / build (16.x)

'validate' is not defined

Check failure on line 397 in test/regressions/strong-refs.js

View workflow job for this annotation

GitHub Actions / build (18.x)

'validate' is not defined
t.ok(validate([]), '[]')

Check failure on line 398 in test/regressions/strong-refs.js

View workflow job for this annotation

GitHub Actions / build (18.x)

'validate' is not defined

Check failure on line 398 in test/regressions/strong-refs.js

View workflow job for this annotation

GitHub Actions / build (16.x)

'validate' is not defined

Check failure on line 398 in test/regressions/strong-refs.js

View workflow job for this annotation

GitHub Actions / build (10.x)

'validate' is not defined

Check failure on line 398 in test/regressions/strong-refs.js

View workflow job for this annotation

GitHub Actions / build (14.x)

'validate' is not defined

Check failure on line 398 in test/regressions/strong-refs.js

View workflow job for this annotation

GitHub Actions / build (14.x)

'validate' is not defined

Check failure on line 398 in test/regressions/strong-refs.js

View workflow job for this annotation

GitHub Actions / build (16.x)

'validate' is not defined

Check failure on line 398 in test/regressions/strong-refs.js

View workflow job for this annotation

GitHub Actions / build (18.x)

'validate' is not defined
t.ok(validate([[]]), '[[]]')

Check failure on line 399 in test/regressions/strong-refs.js

View workflow job for this annotation

GitHub Actions / build (18.x)

'validate' is not defined

Check failure on line 399 in test/regressions/strong-refs.js

View workflow job for this annotation

GitHub Actions / build (16.x)

'validate' is not defined

Check failure on line 399 in test/regressions/strong-refs.js

View workflow job for this annotation

GitHub Actions / build (10.x)

'validate' is not defined

Check failure on line 399 in test/regressions/strong-refs.js

View workflow job for this annotation

GitHub Actions / build (14.x)

'validate' is not defined

Check failure on line 399 in test/regressions/strong-refs.js

View workflow job for this annotation

GitHub Actions / build (14.x)

'validate' is not defined

Check failure on line 399 in test/regressions/strong-refs.js

View workflow job for this annotation

GitHub Actions / build (16.x)

'validate' is not defined

Check failure on line 399 in test/regressions/strong-refs.js

View workflow job for this annotation

GitHub Actions / build (18.x)

'validate' is not defined
t.ok(validate([[[]]]), '[[[]]]')

Check failure on line 400 in test/regressions/strong-refs.js

View workflow job for this annotation

GitHub Actions / build (18.x)

'validate' is not defined

Check failure on line 400 in test/regressions/strong-refs.js

View workflow job for this annotation

GitHub Actions / build (16.x)

'validate' is not defined

Check failure on line 400 in test/regressions/strong-refs.js

View workflow job for this annotation

GitHub Actions / build (10.x)

'validate' is not defined

Check failure on line 400 in test/regressions/strong-refs.js

View workflow job for this annotation

GitHub Actions / build (14.x)

'validate' is not defined

Check failure on line 400 in test/regressions/strong-refs.js

View workflow job for this annotation

GitHub Actions / build (14.x)

'validate' is not defined

Check failure on line 400 in test/regressions/strong-refs.js

View workflow job for this annotation

GitHub Actions / build (16.x)

'validate' is not defined

Check failure on line 400 in test/regressions/strong-refs.js

View workflow job for this annotation

GitHub Actions / build (18.x)

'validate' is not defined
t.notOk(validate([1]), '[1]')

Check failure on line 401 in test/regressions/strong-refs.js

View workflow job for this annotation

GitHub Actions / build (18.x)

'validate' is not defined

Check failure on line 401 in test/regressions/strong-refs.js

View workflow job for this annotation

GitHub Actions / build (16.x)

'validate' is not defined

Check failure on line 401 in test/regressions/strong-refs.js

View workflow job for this annotation

GitHub Actions / build (10.x)

'validate' is not defined

Check failure on line 401 in test/regressions/strong-refs.js

View workflow job for this annotation

GitHub Actions / build (14.x)

'validate' is not defined

Check failure on line 401 in test/regressions/strong-refs.js

View workflow job for this annotation

GitHub Actions / build (14.x)

'validate' is not defined

Check failure on line 401 in test/regressions/strong-refs.js

View workflow job for this annotation

GitHub Actions / build (16.x)

'validate' is not defined

Check failure on line 401 in test/regressions/strong-refs.js

View workflow job for this annotation

GitHub Actions / build (18.x)

'validate' is not defined
t.ok(validate([{}]), '[{}]')

Check failure on line 402 in test/regressions/strong-refs.js

View workflow job for this annotation

GitHub Actions / build (18.x)

'validate' is not defined

Check failure on line 402 in test/regressions/strong-refs.js

View workflow job for this annotation

GitHub Actions / build (16.x)

'validate' is not defined

Check failure on line 402 in test/regressions/strong-refs.js

View workflow job for this annotation

GitHub Actions / build (10.x)

'validate' is not defined

Check failure on line 402 in test/regressions/strong-refs.js

View workflow job for this annotation

GitHub Actions / build (14.x)

'validate' is not defined

Check failure on line 402 in test/regressions/strong-refs.js

View workflow job for this annotation

GitHub Actions / build (14.x)

'validate' is not defined

Check failure on line 402 in test/regressions/strong-refs.js

View workflow job for this annotation

GitHub Actions / build (16.x)

'validate' is not defined

Check failure on line 402 in test/regressions/strong-refs.js

View workflow job for this annotation

GitHub Actions / build (18.x)

'validate' is not defined
})

t.doesNotThrow(() => {
const validate = compile({
A: {
type: ['array', 'object'],
oneOf: [
{
type: 'array',
items: {
$ref: '#/$defs/B',
},
},
{
type: 'object',
required: [],
additionalProperties: false,
properties: {
x: { $ref: '#/$defs/B' },
},
},
],
},
B: {
type: ['array', 'object'],
oneOf: [
{
type: 'array',
items: {
$ref: '#/$defs/A',
},
},
{
type: 'object',
required: [],
additionalProperties: false,
properties: {
x: { $ref: '#/$defs/A' },
},
},
],
},
})
t.ok(validate({}), '{}')
t.ok(validate([]), '[]')
t.ok(validate([[]]), '[[]]')
t.ok(validate([[[]]]), '[[[]]]')
t.notOk(validate([1]), '[1]')
t.ok(validate([{}]), '[{}]')
})

t.end()
})

0 comments on commit acba62e

Please sign in to comment.