Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 24 additions & 7 deletions lib/vocabularies/applicator/if.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@ import type {
KeywordErrorDefinition,
AnySchema,
} from "../../types"
import type {SchemaObjCxt} from "../../compile"
import type {SchemaCxt, SchemaObjCxt} from "../../compile"
import type {KeywordCxt} from "../../compile/validate"
import {_, str, not, Name} from "../../compile/codegen"
import {alwaysValidSchema, checkStrictMode} from "../../compile/util"
import {alwaysValidSchema, checkStrictMode, evaluatedPropsToName} from "../../compile/util"

export type IfKeywordError = ErrorObject<"if", {failingKeyword: string}, AnySchema>

Expand All @@ -28,13 +28,31 @@ const def: CodeKeywordDefinition = {
}
const hasThen = hasSchema(it, "then")
const hasElse = hasSchema(it, "else")
if (!hasThen && !hasElse) return

const valid = gen.let("valid", true)
const schValid = gen.name("_valid")
validateIf()
const ifCxt = validateIf()
cxt.reset()

if (!hasThen && !hasElse) {
// "then"/"else" absent: the "if" assertion is ignored, but its
// annotations are still collected when the instance matches "if".
// Names that hold the merged evaluated props/items are declared first
// so that the conditional merge only assigns to them (when "if" passes)
// and they keep their initial value otherwise.
if (it.opts.unevaluated) {
if (it.props !== true && !(it.props instanceof Name)) {
it.props = evaluatedPropsToName(gen, it.props)
}
if (it.items !== true && !(it.items instanceof Name)) {
it.items = gen.var("items", it.items ?? 0)
}
}
cxt.mergeValidEvaluated(ifCxt, schValid)
return
}
cxt.mergeEvaluated(ifCxt)

if (hasThen && hasElse) {
const ifClause = gen.let("ifClause")
cxt.setParams({ifClause})
Expand All @@ -47,8 +65,8 @@ const def: CodeKeywordDefinition = {

cxt.pass(valid, () => cxt.error(true))

function validateIf(): void {
const schCxt = cxt.subschema(
function validateIf(): SchemaCxt {
return cxt.subschema(
{
keyword: "if",
compositeRule: true,
Expand All @@ -57,7 +75,6 @@ const def: CodeKeywordDefinition = {
},
schValid
)
cxt.mergeEvaluated(schCxt)
}

function validateClause(keyword: string, ifClause?: Name): () => void {
Expand Down
52 changes: 52 additions & 0 deletions spec/issues/2607_if_without_then_else_annotations.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import _Ajv2019 from "../ajv2019"
import _Ajv2020 from "../ajv2020"
import * as assert from "assert"

describe('"if" without "then" and "else" collects annotations for unevaluated keywords', () => {
it("can see annotations from if without then and else (items)", () => {
const ajv = new _Ajv2020({strict: false})
const schema = {
if: {prefixItems: [{const: "a"}]},
unevaluatedItems: false,
}
const validate = ajv.compile(schema)
assert.strictEqual(validate(["a"]), true)
assert.strictEqual(validate(["a", "b"]), false)
// annotations are collected only when the instance matches "if"
assert.strictEqual(validate(["b"]), false)
})

it("can see annotations from if without then and else (properties)", () => {
const ajv = new _Ajv2020({strict: false})
const schema = {
if: {patternProperties: {foo: {type: "string"}}},
unevaluatedProperties: false,
}
const validate = ajv.compile(schema)
assert.strictEqual(validate({foo: "a"}), true)
assert.strictEqual(validate({bar: 2}), false)
assert.strictEqual(validate({foo: 1}), false)
})

it("can see annotations from if without then and else (draft2019, items)", () => {
const ajv = new _Ajv2019({strict: false})
const schema = {
if: {items: [{const: "a"}]},
unevaluatedItems: false,
}
const validate = ajv.compile(schema)
assert.strictEqual(validate(["a"]), true)
assert.strictEqual(validate(["a", "b"]), false)
})

it("can see annotations from if without then and else (draft2019, properties)", () => {
const ajv = new _Ajv2019({strict: false})
const schema = {
if: {patternProperties: {foo: {type: "string"}}},
unevaluatedProperties: false,
}
const validate = ajv.compile(schema)
assert.strictEqual(validate({foo: "a"}), true)
assert.strictEqual(validate({bar: 2}), false)
})
})
Loading