diff --git a/lib/vocabularies/applicator/if.ts b/lib/vocabularies/applicator/if.ts index 5a40d5e3a..ab2b89962 100644 --- a/lib/vocabularies/applicator/if.ts +++ b/lib/vocabularies/applicator/if.ts @@ -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> @@ -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}) @@ -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, @@ -57,7 +75,6 @@ const def: CodeKeywordDefinition = { }, schValid ) - cxt.mergeEvaluated(schCxt) } function validateClause(keyword: string, ifClause?: Name): () => void { diff --git a/spec/issues/2607_if_without_then_else_annotations.spec.ts b/spec/issues/2607_if_without_then_else_annotations.spec.ts new file mode 100644 index 000000000..7771a35d8 --- /dev/null +++ b/spec/issues/2607_if_without_then_else_annotations.spec.ts @@ -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) + }) +})