Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Standalone code generation generates duplicates of validate functions with referenced functions #1361

Closed
ggoodman opened this issue Dec 18, 2020 · 11 comments · Fixed by #1362 or #1418
Closed

Comments

@ggoodman
Copy link

ggoodman commented Dec 18, 2020

What version of Ajv are you using? Does the issue happen if you use the latest version?

7.0.1

Ajv options object

{
    allErrors: true,
    code: {
      es5: false, // use es6
      lines: true,
      optimize: false, // we'll let rollup do this
      source: true,
    },
    inlineRefs: false,
  }

JSON Schema

file:///User.json:

{
    type: 'object',
    properties: {
      name: {
        type: 'string',
      },
    },
    required: ['name'],
  }

file:///B.json:

{
    type: 'object',
    properties: {
      author: {
        $ref: 'file:///User.json',
      },
      contributors: {
        type: 'array',
        items: {
          $ref: 'file:///User.json',
        },
      },
    },
    required: ['author', 'contributors'],
  }

Sample data

Not a data validation issue.

Your code

//@ts-check
'use strict';

const Ajv = require('ajv').default;
const standaloneCode = require('ajv/dist/standalone').default;

const ajv = new Ajv({
  allErrors: true,
  code: {
    es5: false,
    lines: true,
    optimize: false,
    source: true,
  },
  inlineRefs: false,
  schemas: {
    'file:///User.json': {
      type: 'object',
      properties: {
        name: {
          type: 'string',
        },
      },
      required: ['name'],
    },
    'file:///B.json': {
      type: 'object',
      properties: {
        author: {
          $ref: 'file:///User.json',
        },
        contributors: {
          type: 'array',
          items: {
            $ref: 'file:///User.json',
          },
        },
      },
      required: ['author', 'contributors'],
    },
  }
});
const code = standaloneCode(ajv);

console.log(code);

Note that function validate21 is included twice in the resulting generated code. This breaks some tools while others are unable to remove the duplicate function.

Resulting code (prettified)

"use strict";
exports["file:///User.json"] = validate21;
const schema6 = {
  type: "object",
  properties: { name: { type: "string" } },
  required: ["name"],
};

function validate21(
  data,
  { dataPath = "", parentData, parentDataProperty, rootData = data } = {}
) {
  let vErrors = null;
  let errors = 0;
  if (data && typeof data == "object" && !Array.isArray(data)) {
    if (data.name === undefined) {
      const err0 = {
        keyword: "required",
        dataPath,
        schemaPath: "#/required",
        params: { missingProperty: "name" },
        message: "should have required property '" + "name" + "'",
      };
      if (vErrors === null) {
        vErrors = [err0];
      } else {
        vErrors.push(err0);
      }
      errors++;
    }
    if (data.name !== undefined) {
      let data0 = data.name;
      const _errs0 = errors;
      if (typeof data0 !== "string") {
        const err1 = {
          keyword: "type",
          dataPath: dataPath + "/name",
          schemaPath: "#/properties/name/type",
          params: { type: "string" },
          message: "should be string",
        };
        if (vErrors === null) {
          vErrors = [err1];
        } else {
          vErrors.push(err1);
        }
        errors++;
      }
      var valid0 = _errs0 === errors;
    }
  } else {
    const err2 = {
      keyword: "type",
      dataPath,
      schemaPath: "#/type",
      params: { type: "object" },
      message: "should be object",
    };
    if (vErrors === null) {
      vErrors = [err2];
    } else {
      vErrors.push(err2);
    }
    errors++;
  }
  validate21.errors = vErrors;
  return errors === 0;
}

exports["file:///B.json"] = validate22;
const schema7 = {
  type: "object",
  properties: {
    author: { $ref: "file:///User.json" },
    contributors: { type: "array", items: { $ref: "file:///User.json" } },
  },
  required: ["author", "contributors"],
};

function validate21(
  data,
  { dataPath = "", parentData, parentDataProperty, rootData = data } = {}
) {
  let vErrors = null;
  let errors = 0;
  if (data && typeof data == "object" && !Array.isArray(data)) {
    if (data.name === undefined) {
      const err0 = {
        keyword: "required",
        dataPath,
        schemaPath: "#/required",
        params: { missingProperty: "name" },
        message: "should have required property '" + "name" + "'",
      };
      if (vErrors === null) {
        vErrors = [err0];
      } else {
        vErrors.push(err0);
      }
      errors++;
    }
    if (data.name !== undefined) {
      let data0 = data.name;
      const _errs0 = errors;
      if (typeof data0 !== "string") {
        const err1 = {
          keyword: "type",
          dataPath: dataPath + "/name",
          schemaPath: "#/properties/name/type",
          params: { type: "string" },
          message: "should be string",
        };
        if (vErrors === null) {
          vErrors = [err1];
        } else {
          vErrors.push(err1);
        }
        errors++;
      }
      var valid0 = _errs0 === errors;
    }
  } else {
    const err2 = {
      keyword: "type",
      dataPath,
      schemaPath: "#/type",
      params: { type: "object" },
      message: "should be object",
    };
    if (vErrors === null) {
      vErrors = [err2];
    } else {
      vErrors.push(err2);
    }
    errors++;
  }
  validate21.errors = vErrors;
  return errors === 0;
}

function validate22(
  data,
  { dataPath = "", parentData, parentDataProperty, rootData = data } = {}
) {
  let vErrors = null;
  let errors = 0;
  if (data && typeof data == "object" && !Array.isArray(data)) {
    if (data.author === undefined) {
      const err0 = {
        keyword: "required",
        dataPath,
        schemaPath: "#/required",
        params: { missingProperty: "author" },
        message: "should have required property '" + "author" + "'",
      };
      if (vErrors === null) {
        vErrors = [err0];
      } else {
        vErrors.push(err0);
      }
      errors++;
    }
    if (data.contributors === undefined) {
      const err1 = {
        keyword: "required",
        dataPath,
        schemaPath: "#/required",
        params: { missingProperty: "contributors" },
        message: "should have required property '" + "contributors" + "'",
      };
      if (vErrors === null) {
        vErrors = [err1];
      } else {
        vErrors.push(err1);
      }
      errors++;
    }
    if (data.author !== undefined) {
      let data0 = data.author;
      const _errs0 = errors;
      if (
        !validate21(data0, {
          dataPath: dataPath + "/author",
          parentData: data,
          parentDataProperty: "author",
          rootData,
        })
      ) {
        vErrors =
          vErrors === null
            ? validate21.errors
            : vErrors.concat(validate21.errors);
        errors = vErrors.length;
      } else {
      }
      var valid0 = _errs0 === errors;
    }
    if (data.contributors !== undefined) {
      let data1 = data.contributors;
      const _errs1 = errors;
      if (Array.isArray(data1)) {
        const len0 = data1.length;
        for (let i0 = 0; i0 < len0; i0++) {
          let data2 = data1[i0];
          const _errs2 = errors;
          if (
            !validate21(data2, {
              dataPath: dataPath + "/contributors/" + i0,
              parentData: data1,
              parentDataProperty: i0,
              rootData,
            })
          ) {
            vErrors =
              vErrors === null
                ? validate21.errors
                : vErrors.concat(validate21.errors);
            errors = vErrors.length;
          } else {
          }
          var valid1 = _errs2 === errors;
        }
      } else {
        const err2 = {
          keyword: "type",
          dataPath: dataPath + "/contributors",
          schemaPath: "#/properties/contributors/type",
          params: { type: "array" },
          message: "should be array",
        };
        if (vErrors === null) {
          vErrors = [err2];
        } else {
          vErrors.push(err2);
        }
        errors++;
      }
      var valid0 = _errs1 === errors;
    }
  } else {
    const err3 = {
      keyword: "type",
      dataPath,
      schemaPath: "#/type",
      params: { type: "object" },
      message: "should be object",
    };
    if (vErrors === null) {
      vErrors = [err3];
    } else {
      vErrors.push(err3);
    }
    errors++;
  }
  validate22.errors = vErrors;
  return errors === 0;
}

Validation result, data AFTER validation, error messages

N/A

What results did you expect?

I expected each validation function to be included exactly once.

Are you going to resolve the issue?

I'm trying to wrap my head around the codegen but am unlikely to be able to handle this.

@epoberezkin
Copy link
Member

good catch. I think I know why it might be happening - and the normal tests wouldn’t have caught it because JS allows redefining functions, probably even in strict mode...

@epoberezkin
Copy link
Member

@ggoodman it would be great if you could test with the full code you have before I release. I have a suspicion there may be some edge cases when this is not sufficient, but it should be fixed for the majority of cases.

@ggoodman
Copy link
Author

ggoodman commented Dec 19, 2020

@epoberezkin I've given it a try and in my limited testing it works as advertised! 🎉

Great work! I love the first-class codegen in v7 ❤️

@epoberezkin
Copy link
Member

Great - thank you :)

@jafaircl
Copy link

jafaircl commented Jan 21, 2021

@epoberezkin this still happens in 7.0.3 if the user schema references back to the info schema like so:

import Ajv from 'ajv';
import standaloneCode from 'ajv/dist/standalone';

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'],
};

const ajv = new Ajv({
  allErrors: true,
  code: { optimize: false, source: true, lines: 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);
console.log(moduleCode);

is there any way around this or is this something the library just can't do?

@epoberezkin
Copy link
Member

epoberezkin commented Jan 21, 2021

Need to have a look - mutual recursion is always tricky to compile - but should be possible to fix. I’ll try to reproduce. Just to confirm - it works, but duplicates functions in the module, correct?

@epoberezkin epoberezkin reopened this Jan 21, 2021
@jafaircl
Copy link

Yes that’s correct. It outputs the correct validators for user and info it just does it twice.

@3nvi
Copy link

3nvi commented Jan 27, 2021

Can also validate that it happens in 7.0.3. Webpack's message is:

Syntax error: Identifier 'validate32' has already been declared

Code does indeed seem the same, just duplicated 😺

epoberezkin added a commit that referenced this issue Jan 31, 2021
@epoberezkin
Copy link
Member

epoberezkin commented Jan 31, 2021

I can reproduce ^^...

@epoberezkin
Copy link
Member

@jafaircl please check the fix with your code too

@jafaircl
Copy link

jafaircl commented Feb 1, 2021

@epoberezkin yes the tests pass with my schema as well and copy/pasting the generated validators from this branch into my project doesn't throw any duplicate function errors. Thank you so much!

epoberezkin added a commit that referenced this issue Feb 1, 2021
…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
andriyl pushed a commit to Redocly/ajv that referenced this issue Jun 16, 2021
…code with mutually recursive schemas (ajv-validator#1418)

* test: skipped failing test showing duplicate functions in standalone code for mutually recursive schemas, ajv-validator#1361

* fix: duplicate functions in standalone code for mutually recursive schemas, fixes ajv-validator#1361
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment