Skip to content
Permalink

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: ajv-validator/ajv
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: v7.0.3
Choose a base ref
...
head repository: ajv-validator/ajv
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: v7.0.4
Choose a head ref
  • 3 commits
  • 6 files changed
  • 1 contributor

Commits on Feb 1, 2021

  1. Fix for issue #1361 - duplicate functions in standalone code with mut…

    …ually recursive schemas (#1418)
    
    * test: skipped failing test showing duplicate functions in standalone code for mutually recursive schemas, #1361
    
    * fix: duplicate functions in standalone code for mutually recursive schemas, fixes #1361
    epoberezkin authored Feb 1, 2021

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
    Copy the full SHA
    0b7567b View commit details
  2. Copy the full SHA
    c91e8ba View commit details
  3. 7.0.4

    epoberezkin committed Feb 1, 2021
    Copy the full SHA
    63486ce View commit details
Showing with 180 additions and 58 deletions.
  1. +14 −4 lib/compile/codegen/scope.ts
  2. +6 −2 lib/compile/index.ts
  3. +22 −10 lib/standalone/index.ts
  4. +1 −1 package.json
  5. +35 −0 spec/issues/1414_base_uri_change.spec.ts
  6. +102 −41 spec/standalone.spec.ts
18 changes: 14 additions & 4 deletions lib/compile/codegen/scope.ts
Original file line number Diff line number Diff line change
@@ -42,6 +42,15 @@ export type ScopeValueSets = {
[Prefix in string]?: Set<ValueScopeName>
}

export enum UsedValueState {
Started,
Completed,
}

export type UsedScopeValues = {
[Prefix in string]?: Map<ValueScopeName, UsedValueState | undefined>
}

export const varKinds = {
const: new Name("const"),
let: new Name("let"),
@@ -161,7 +170,7 @@ export class ValueScope extends Scope {

scopeCode(
values: ScopeValues | ScopeValueSets = this._values,
usedValues?: ScopeValueSets,
usedValues?: UsedScopeValues,
getCode?: (n: ValueScopeName) => Code | undefined
): Code {
return this._reduceValues(
@@ -178,17 +187,17 @@ export class ValueScope extends Scope {
private _reduceValues(
values: ScopeValues | ScopeValueSets,
valueCode: (n: ValueScopeName) => Code | undefined,
usedValues: ScopeValueSets = {},
usedValues: UsedScopeValues = {},
getCode?: (n: ValueScopeName) => Code | undefined
): Code {
let code: Code = nil
for (const prefix in values) {
const vs = values[prefix]
if (!vs) continue
const nameSet = (usedValues[prefix] = usedValues[prefix] || new Set())
const nameSet = (usedValues[prefix] = usedValues[prefix] || new Map())
vs.forEach((name: ValueScopeName) => {
if (nameSet.has(name)) return
nameSet.add(name)
nameSet.set(name, UsedValueState.Started)
let c = valueCode(name)
if (c) {
const def = this.opts.es5 ? varKinds.var : varKinds.const
@@ -198,6 +207,7 @@ export class ValueScope extends Scope {
} else {
throw new ValueError(name)
}
nameSet.set(name, UsedValueState.Completed)
})
}
return code
8 changes: 6 additions & 2 deletions lib/compile/index.ts
Original file line number Diff line number Diff line change
@@ -246,7 +246,7 @@ export function resolveSchema(
): SchemaEnv | undefined {
const p = URI.parse(ref)
const refPath = _getFullPath(p)
const baseId = getFullPath(root.baseId)
let baseId = getFullPath(root.baseId)
// TODO `Object.keys(root.schema).length > 0` should not be needed - but removing breaks 2 tests
if (Object.keys(root.schema).length > 0 && refPath === baseId) {
return getJsonPointer.call(this, p, root)
@@ -262,7 +262,11 @@ export function resolveSchema(

if (typeof schOrRef?.schema !== "object") return
if (!schOrRef.validate) compileSchema.call(this, schOrRef)
if (id === normalizeId(ref)) return new SchemaEnv({schema: schOrRef.schema, root, baseId})
if (id === normalizeId(ref)) {
const {schema} = schOrRef
if (schema.$id) baseId = resolveUrl(baseId, schema.$id)
return new SchemaEnv({schema, root, baseId})
}
return getJsonPointer.call(this, p, schOrRef)
}

32 changes: 22 additions & 10 deletions lib/standalone/index.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import type AjvCore from "../core"
import type {AnyValidateFunction, SourceCode} from "../types"
import type {SchemaEnv} from "../compile"
import {ScopeValueSets, ValueScopeName, varKinds} from "../compile/codegen/scope"
import {_, _Code, Code, getProperty} from "../compile/codegen/code"
import {UsedScopeValues, UsedValueState, ValueScopeName, varKinds} from "../compile/codegen/scope"
import {_, nil, _Code, Code, getProperty} from "../compile/codegen/code"

export default function standaloneCode(
ajv: AjvCore,
@@ -27,7 +27,7 @@ export default function standaloneCode(
}

function funcExportCode(source?: SourceCode): string {
const usedValues: ScopeValueSets = {}
const usedValues: UsedScopeValues = {}
const n = source?.validateName
const vCode = validateCode(usedValues, source)
return `"use strict";${_n}module.exports = ${n};${_n}module.exports.default = ${n};${_n}${vCode}`
@@ -37,7 +37,7 @@ export default function standaloneCode(
schemas: {[K in string]?: T},
getValidateFunc: (schOrId: T) => AnyValidateFunction | undefined
): string {
const usedValues: ScopeValueSets = {}
const usedValues: UsedScopeValues = {}
let code = _`"use strict";`
for (const name in schemas) {
const v = getValidateFunc(schemas[name] as T)
@@ -49,11 +49,10 @@ export default function standaloneCode(
return `${code}`
}

function validateCode(usedValues: ScopeValueSets, s?: SourceCode): Code {
function validateCode(usedValues: UsedScopeValues, s?: SourceCode): Code {
if (!s) throw new Error('moduleCode: function does not have "source" property')
const {prefix} = s.validateName
const nameSet = (usedValues[prefix] = usedValues[prefix] || new Set())
nameSet.add(s.validateName)
if (usedState(s.validateName) === UsedValueState.Completed) return nil
setUsedState(s.validateName, UsedValueState.Started)

const scopeCode = ajv.scope.scopeCode(s.scopeValues, usedValues, refValidateCode)
const code = new _Code(`${scopeCode}${_n}${s.validateCode}`)
@@ -66,11 +65,24 @@ export default function standaloneCode(
return validateCode(usedValues, v.source)
} else if ((n.prefix === "root" || n.prefix === "wrapper") && typeof vRef == "object") {
const {validate, validateName} = vRef as SchemaEnv
const vCode = validateCode(usedValues, validate?.source)
if (!validateName) throw new Error("ajv internal error")
const def = ajv.opts.code.es5 ? varKinds.var : varKinds.const
return _`${def} ${n} = {validate: ${validateName}};${_n}${vCode}`
const wrapper = _`${def} ${n} = {validate: ${validateName}};`
if (usedState(validateName) === UsedValueState.Started) return wrapper
const vCode = validateCode(usedValues, validate?.source)
return _`${wrapper}${_n}${vCode}`
}
return undefined
}

function usedState(name: ValueScopeName): UsedValueState | undefined {
return usedValues[name.prefix]?.get(name)
}

function setUsedState(name: ValueScopeName, state: UsedValueState): void {
const {prefix} = name
const names = (usedValues[prefix] = usedValues[prefix] || new Map())
names.set(name, state)
}
}
}
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "ajv",
"version": "7.0.3",
"version": "7.0.4",
"description": "Another JSON Schema Validator",
"main": "dist/ajv.js",
"types": "dist/ajv.d.ts",
35 changes: 35 additions & 0 deletions spec/issues/1414_base_uri_change.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import _Ajv from "../ajv"
import assert = require("assert")

const schema1 = {
$id: "one",
type: "object",
properties: {
foo: {$ref: "#/definitions/foo"},
},
definitions: {
foo: {$ref: "two"},
},
}

const schema2 = {
$id: "two",
type: "object",
properties: {
bar: {$ref: "#/definitions/bar"},
},
definitions: {
bar: {type: "string"},
},
}

describe("issue 1414: base URI change", () => {
it("should compile schema", () => {
const ajv = new _Ajv()
ajv.addSchema(schema2)
const validate = ajv.compile(schema1)
assert.strictEqual(typeof validate, "function")
assert.strictEqual(validate({foo: {bar: 1}}), false)
assert.strictEqual(validate({foo: {bar: "1"}}), true)
})
})
143 changes: 102 additions & 41 deletions spec/standalone.spec.ts
Original file line number Diff line number Diff line change
@@ -86,57 +86,118 @@ describe("standalone code generation", () => {
}
})

describe("two refs to the same schema (issue #1361)", () => {
const userSchema = {
$id: "user.json",
type: "object",
properties: {
name: {type: "string"},
},
required: ["name"],
}
describe("issue #1361", () => {
describe("two refs to the same schema", () => {
const userSchema = {
$id: "user.json",
type: "object",
properties: {
name: {type: "string"},
},
required: ["name"],
}

const infoSchema = {
$id: "info.json",
type: "object",
properties: {
author: {$ref: "user.json"},
contributors: {
type: "array",
items: {$ref: "user.json"},
const infoSchema = {
$id: "info.json",
type: "object",
properties: {
author: {$ref: "user.json"},
contributors: {
type: "array",
items: {$ref: "user.json"},
},
},
},
required: ["author", "contributors"],
}
required: ["author", "contributors"],
}

describe("all exports", () => {
it("should not have duplicate functions", () => {
const ajv = new _Ajv({
allErrors: true,
code: {optimize: false, source: true},
inlineRefs: false, // it is needed to show the issue, schemas with refs won't be inlined anyway
schemas: [userSchema, infoSchema],
describe("all exports", () => {
it("should not have duplicate functions", () => {
const ajv = new _Ajv({
allErrors: true,
code: {optimize: false, source: true},
inlineRefs: false, // it is needed to show the issue, schemas with refs won't be inlined anyway
schemas: [userSchema, infoSchema],
})

const moduleCode = standaloneCode(ajv)
assertNoDuplicateFunctions(moduleCode)
const {"user.json": user, "info.json": info} = requireFromString(moduleCode)
testExports({user, info})
})
})

const moduleCode = standaloneCode(ajv)
assertNoDuplicateFunctions(moduleCode)
const {"user.json": user, "info.json": info} = requireFromString(moduleCode)
testExports({user, info})
describe("named exports", () => {
it("should not have duplicate functions", () => {
const ajv = new _Ajv({
allErrors: true,
code: {optimize: false, source: true},
inlineRefs: false, // it is needed to show the issue, schemas with refs won't be inlined anyway
schemas: [userSchema, infoSchema],
})

const moduleCode = standaloneCode(ajv, {user: "user.json", info: "info.json"})
assertNoDuplicateFunctions(moduleCode)
testExports(requireFromString(moduleCode))
})
})
})

describe("named exports", () => {
it("should not have duplicate functions", () => {
const ajv = new _Ajv({
allErrors: true,
code: {optimize: false, source: true},
inlineRefs: false, // it is needed to show the issue, schemas with refs won't be inlined anyway
schemas: [userSchema, infoSchema],
describe("mutually recursive schemas", () => {
const userSchema = {
$id: "user.json",
type: "object",
properties: {
name: {type: "string"},
infos: {
type: "array",
items: {$ref: "info.json"},
},
},
required: ["name"],
}

const infoSchema = {
$id: "info.json",
type: "object",
properties: {
author: {$ref: "user.json"},
contributors: {
type: "array",
items: {$ref: "user.json"},
},
},
required: ["author", "contributors"],
}

describe("all exports", () => {
it("should not have duplicate functions", () => {
const ajv = new _Ajv({
allErrors: true,
code: {optimize: false, source: true},
inlineRefs: false, // it is needed to show the issue, schemas with refs won't be inlined anyway
schemas: [userSchema, infoSchema],
})

const moduleCode = standaloneCode(ajv)
assertNoDuplicateFunctions(moduleCode)
const {"user.json": user, "info.json": info} = requireFromString(moduleCode)
testExports({user, info})
})
})

const moduleCode = standaloneCode(ajv, {user: "user.json", info: "info.json"})
assertNoDuplicateFunctions(moduleCode)
testExports(requireFromString(moduleCode))
describe("named exports", () => {
it("should not have duplicate functions", () => {
const ajv = new _Ajv({
allErrors: true,
code: {optimize: false, source: true},
inlineRefs: false, // it is needed to show the issue, schemas with refs won't be inlined anyway
schemas: [userSchema, infoSchema],
})

const moduleCode = standaloneCode(ajv, {user: "user.json", info: "info.json"})
assertNoDuplicateFunctions(moduleCode)
testExports(requireFromString(moduleCode))
})
})
})