diff --git a/lib/compile/codegen/index.ts b/lib/compile/codegen/index.ts index 63bf5df65..0957ca9a7 100644 --- a/lib/compile/codegen/index.ts +++ b/lib/compile/codegen/index.ts @@ -3,7 +3,7 @@ import {_, nil, _Code, Code, Name, UsedNames, CodeItem, addCodeArg, _CodeOrName} import {Scope, varKinds} from "./scope" export {_, str, strConcat, nil, getProperty, stringify, Name, Code} from "./code" -export {Scope, ScopeStore, ValueScope, ScopeValueSets, varKinds} from "./scope" +export {Scope, ScopeStore, ValueScope, ValueScopeName, ScopeValueSets, varKinds} from "./scope" // type for expressions that can be safely inserted in code without quotes export type SafeExpr = Code | number | boolean | null diff --git a/lib/compile/index.ts b/lib/compile/index.ts index 05db0977f..94e2dfdbc 100644 --- a/lib/compile/index.ts +++ b/lib/compile/index.ts @@ -8,7 +8,7 @@ import type { } from "../types" import type Ajv from "../core" import type {InstanceOptions} from "../core" -import {CodeGen, _, nil, stringify, Name, Code} from "./codegen" +import {CodeGen, _, nil, stringify, Name, Code, ValueScopeName} from "./codegen" import {ValidationError} from "./error_classes" import N from "./names" import {LocalRefs, getFullPath, _getFullPath, inlineRef, normalizeId, resolveUrl} from "./resolve" @@ -77,7 +77,7 @@ export class SchemaEnv implements SchemaEnvArgs { readonly refs: SchemaRefs = {} readonly dynamicAnchors: {[Ref in string]?: true} = {} validate?: AnyValidateFunction - validateName?: Name + validateName?: ValueScopeName constructor(env: SchemaEnvArgs) { let schema: AnySchemaObject | undefined diff --git a/lib/standalone/index.ts b/lib/standalone/index.ts index 6df1abf6f..27a4bca1b 100644 --- a/lib/standalone/index.ts +++ b/lib/standalone/index.ts @@ -51,6 +51,10 @@ export default function standaloneCode( function validateCode(usedValues: ScopeValueSets, 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) + const scopeCode = ajv.scope.scopeCode(s.scopeValues, usedValues, refValidateCode) const code = new _Code(`${scopeCode}${_n}${s.validateCode}`) return s.evaluated ? _`${code}${s.validateName}.evaluated = ${s.evaluated};${_n}` : code diff --git a/lib/types/index.ts b/lib/types/index.ts index ef58a636e..e91f122b9 100644 --- a/lib/types/index.ts +++ b/lib/types/index.ts @@ -1,4 +1,4 @@ -import type {CodeGen, Code, Name, ScopeValueSets} from "../compile/codegen" +import type {CodeGen, Code, Name, ScopeValueSets, ValueScopeName} from "../compile/codegen" import type {SchemaEnv, SchemaCxt, SchemaObjCxt} from "../compile" import type {JSONType} from "../compile/rules" import type KeywordCxt from "../compile/context" @@ -30,7 +30,7 @@ export type AnySchema = Schema | AsyncSchema export type SchemaMap = {[Key in string]?: AnySchema} export interface SourceCode { - validateName: Name + validateName: ValueScopeName validateCode: string scopeValues: ScopeValueSets evaluated?: Code diff --git a/spec/standalone.spec.ts b/spec/standalone.spec.ts index 052af065f..f9c60953f 100644 --- a/spec/standalone.spec.ts +++ b/spec/standalone.spec.ts @@ -85,6 +85,82 @@ 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"], + } + + 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}) + }) + }) + + 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)) + }) + }) + + function assertNoDuplicateFunctions(code: string): void { + const funcs = code.match(/function\s+([a-z0-9_$]+)/gi) + assert(Array.isArray(funcs)) + assert(funcs.length > 0) + assert.strictEqual(funcs.length, new Set(funcs).size, "should have no duplicates") + } + + function testExports(validate: {[n: string]: AnyValidateFunction}): void { + assert.strictEqual(validate.user({}), false) + assert.strictEqual(validate.user({name: "usr1"}), true) + + assert.strictEqual(validate.info({}), false) + assert.strictEqual( + validate.info({ + author: {name: "usr1"}, + contributors: [{name: "usr2"}], + }), + true + ) + } + }) + it("should generate module code with a single export (ESM compatible)", () => { const ajv = new _Ajv({code: {source: true}}) const v = ajv.compile({