From d33a36dab516bb65a6af408fc883f3ab935883d5 Mon Sep 17 00:00:00 2001 From: Jason Green Date: Sun, 4 Aug 2024 22:14:58 +0100 Subject: [PATCH] feat: add pattern and path to regexp create errors #2477 --- lib/vocabularies/code.ts | 13 +++- lib/vocabularies/validation/pattern.ts | 11 +++- .../2477_informative_pattern_errors.spec.ts | 61 +++++++++++++++++++ 3 files changed, 80 insertions(+), 5 deletions(-) create mode 100644 spec/issues/2477_informative_pattern_errors.spec.ts diff --git a/lib/vocabularies/code.ts b/lib/vocabularies/code.ts index 92cdd5b04e..1710b4aa31 100644 --- a/lib/vocabularies/code.ts +++ b/lib/vocabularies/code.ts @@ -1,4 +1,4 @@ -import type {AnySchema, SchemaMap} from "../types" +import type {AnySchema, RegExpLike, SchemaMap} from "../types" import type {SchemaCxt} from "../compile" import type {KeywordCxt} from "../compile/validate" import {CodeGen, _, and, or, not, nil, strConcat, getProperty, Code, Name} from "../compile/codegen" @@ -92,10 +92,17 @@ export function callValidateCode( const newRegExp = _`new RegExp` -export function usePattern({gen, it: {opts}}: KeywordCxt, pattern: string): Name { +export function usePattern({gen, it: {opts, errSchemaPath}}: KeywordCxt, pattern: string): Name { const u = opts.unicodeRegExp ? "u" : "" const {regExp} = opts.code - const rx = regExp(pattern, u) + + let rx: RegExpLike + try { + rx = new RegExp(pattern, u) + } catch (e) { + throw new Error(`Invalid regular expression: ${pattern} at ${errSchemaPath}`) + } + rx = regExp(pattern, u) return gen.scopeValue("pattern", { key: rx.toString(), diff --git a/lib/vocabularies/validation/pattern.ts b/lib/vocabularies/validation/pattern.ts index 7b27b7d3c0..947cd72ec6 100644 --- a/lib/vocabularies/validation/pattern.ts +++ b/lib/vocabularies/validation/pattern.ts @@ -18,9 +18,16 @@ const def: CodeKeywordDefinition = { error, code(cxt: KeywordCxt) { const {data, $data, schema, schemaCode, it} = cxt - // TODO regexp should be wrapped in try/catchs const u = it.opts.unicodeRegExp ? "u" : "" - const regExp = $data ? _`(new RegExp(${schemaCode}, ${u}))` : usePattern(cxt, schema) + const regExp = $data + ? _`(function() { + try { + return new RegExp(${schemaCode}, ${u}) + } catch (e) { + throw new Error('Invalid regular expression: ' + ${schemaCode} + ' at ' + ${it.errSchemaPath}) + } + })()` + : usePattern(cxt, schema) cxt.fail$data(_`!${regExp}.test(${data})`) }, } diff --git a/spec/issues/2477_informative_pattern_errors.spec.ts b/spec/issues/2477_informative_pattern_errors.spec.ts new file mode 100644 index 0000000000..aa19ec4019 --- /dev/null +++ b/spec/issues/2477_informative_pattern_errors.spec.ts @@ -0,0 +1,61 @@ +import _Ajv from "../ajv2020" +import * as assert from "assert" + +describe("Invalid regexp patterns should throw more informative errors (issue #2477)", () => { + it("throws with pattern and schema path", () => { + const ajv = new _Ajv() + + const rootSchema = { + type: "string", + pattern: "^[0-9]{2-4}", + } + + assert.throws( + () => ajv.compile(rootSchema), + (thrown: unknown) => { + assert.equal((thrown as Error).message, "Invalid regular expression: ^[0-9]{2-4} at #") + return true + } + ) + + const pathSchema = { + type: "object", + properties: { + foo: rootSchema, + }, + } + + assert.throws( + () => ajv.compile(pathSchema), + (thrown: unknown) => { + assert.equal( + (thrown as Error).message, + "Invalid regular expression: ^[0-9]{2-4} at #/properties/foo" + ) + return true + } + ) + }) + it("throws with pattern and schema path with $data", () => { + const ajv = new _Ajv({$data: true}) + + const schema = { + properties: { + shouldMatch: {}, + string: {pattern: {$data: "1/shouldMatch"}}, + }, + } + const validate = ajv.compile(schema) + + assert.throws( + () => ajv.compile(validate({shouldMatch: "^[0-9]{2-4}", string: "123"})), + (thrown: unknown) => { + assert.equal( + (thrown as Error).message, + "Invalid regular expression: ^[0-9]{2-4} at #/properties/string" + ) + return true + } + ) + }) +})