diff --git a/lib/compile/jtd/parse.ts b/lib/compile/jtd/parse.ts index f2f2fa0b9..edb560362 100644 --- a/lib/compile/jtd/parse.ts +++ b/lib/compile/jtd/parse.ts @@ -5,7 +5,7 @@ import {SchemaEnv, getCompilingSchema} from ".." import {_, str, and, nil, not, CodeGen, Code, Name, SafeExpr} from "../codegen" import {MissingRefError} from "../error_classes" import N from "../names" -import {isOwnProperty, hasPropFunc} from "../../vocabularies/code" +import {hasPropFunc} from "../../vocabularies/code" import {hasRef} from "../../vocabularies/jtd/ref" import {intRange, IntType} from "../../vocabularies/jtd/type" import {parseJson, parseJsonNumber, parseJsonString} from "../../runtime/parseJson" @@ -117,26 +117,6 @@ function parseCode(cxt: ParseCxt): void { const parseBoolean = parseBooleanToken(true, parseBooleanToken(false, jsonSyntaxError)) -// function parseEmptyCode(cxt: ParseCxt): void { -// const {gen, data, char: c} = cxt -// skipWhitespace(cxt) -// gen.assign(c, _`${N.json}[${N.jsonPos}]`) -// gen.if(_`${c} === "t" || ${c} === "f"`) -// parseBoolean(cxt) -// gen.elseIf(_`${c} === "n"`) -// tryParseToken(cxt, "null", jsonSyntaxError, () => gen.assign(data, null)) -// gen.elseIf(_`${c} === '"'`) -// parseString(cxt) -// gen.elseIf(_`${c} === "["`) -// parseElements({...cxt, schema: {elements: {}}}) -// gen.elseIf(_`${c} === "{"`) -// parseValues({...cxt, schema: {values: {}}}) -// gen.else() -// parseNumber(cxt) -// gen.endIf() -// skipWhitespace(cxt) -// } - function parseNullable(cxt: ParseCxt, parseForm: GenParse): void { const {gen, schema, data} = cxt if (!schema.nullable) return parseForm(cxt) @@ -171,15 +151,18 @@ function tryParseItems(cxt: ParseCxt, endToken: string, block: () => void): void const {gen} = cxt gen.for(_`;${N.jsonPos}<${N.jsonLen} && ${jsonSlice(1)}!==${endToken};`, () => { block() - tryParseToken(cxt, ",", () => gen.break()) + tryParseToken(cxt, ",", () => gen.break(), hasItem) }) + + function hasItem(): void { + tryParseToken(cxt, endToken, () => {}, jsonSyntaxError) + } } function parseKeyValue(cxt: ParseCxt, schema: SchemaObject): void { const {gen} = cxt const key = gen.let("key") parseString({...cxt, data: key}) - checkDuplicateProperty(cxt, key) parseToken(cxt, ":") parsePropertyValue(cxt, key, schema) } @@ -231,11 +214,6 @@ function parseSchemaProperties(cxt: ParseCxt, discriminator?: string): void { parseItems(cxt, "}", () => { const key = gen.let("key") parseString({...cxt, data: key}) - if (discriminator) { - gen.if(_`${key} !== ${discriminator}`, () => checkDuplicateProperty(cxt, key)) - } else { - checkDuplicateProperty(cxt, key) - } parseToken(cxt, ":") gen.if(false) parseDefinedProperty(cxt, key, properties) @@ -270,12 +248,6 @@ function parseDefinedProperty(cxt: ParseCxt, key: Name, schemas: SchemaObjectMap } } -function checkDuplicateProperty({gen, data}: ParseCxt, key: Name): void { - gen.if(isOwnProperty(gen, data, key), () => - gen.throw(_`new Error("JSON: duplicate property " + ${key})`) - ) -} - function parsePropertyValue(cxt: ParseCxt, key: Name, schema: SchemaObject): void { parseCode({...cxt, schema, data: _`${cxt.data}[${key}]`}) } diff --git a/lib/runtime/parseJson.ts b/lib/runtime/parseJson.ts index 2539bbc73..ba222c004 100644 --- a/lib/runtime/parseJson.ts +++ b/lib/runtime/parseJson.ts @@ -17,12 +17,13 @@ export function parseJson(s: string, pos: number): unknown { return undefined } endPos = +matches[1] + const c = s[endPos] s = s.slice(0, endPos) parseJson.position = pos + endPos try { return JSON.parse(s) } catch (e1) { - parseJson.message = `unexpected token ${s[endPos]}` + parseJson.message = `unexpected token ${c}` return undefined } } @@ -87,7 +88,8 @@ export function parseJsonNumber(s: string, pos: number, maxDigits?: number): num } function errorMessage(): void { - parseJson.message = pos < s.length ? `unexpected token ${s[pos]}` : "unexpected end" + parseJsonNumber.position = pos + parseJsonNumber.message = pos < s.length ? `unexpected token ${s[pos]}` : "unexpected end" } } @@ -106,7 +108,8 @@ const escapedChars: {[X in string]?: string} = { "\\": "\\", } -const A_CODE: number = "a".charCodeAt(0) +const CODE_A: number = "a".charCodeAt(0) +const CODE_0: number = "0".charCodeAt(0) export function parseJsonString(s: string, pos: number): string | undefined { let str = "" @@ -114,43 +117,48 @@ export function parseJsonString(s: string, pos: number): string | undefined { parseJsonString.message = undefined // eslint-disable-next-line no-constant-condition, @typescript-eslint/no-unnecessary-condition while (true) { - c = s[pos] - pos++ + c = s[pos++] if (c === '"') break if (c === "\\") { c = s[pos] if (c in escapedChars) { str += escapedChars[c] + pos++ } else if (c === "u") { + pos++ let count = 4 let code = 0 while (count--) { code <<= 4 c = s[pos].toLowerCase() if (c >= "a" && c <= "f") { - c += c.charCodeAt(0) - A_CODE + 10 + code += c.charCodeAt(0) - CODE_A + 10 } else if (c >= "0" && c <= "9") { - code += +c + code += c.charCodeAt(0) - CODE_0 } else if (c === undefined) { errorMessage("unexpected end") return undefined } else { - errorMessage(`unexpected token ${s[pos]}`) + errorMessage(`unexpected token ${c}`) return undefined } pos++ } str += String.fromCharCode(code) } else { - errorMessage(`unexpected token ${s[pos]}`) + errorMessage(`unexpected token ${c}`) return undefined } - pos++ } else if (c === undefined) { errorMessage("unexpected end") return undefined } else { - str += c + if (c.charCodeAt(0) >= 0x20) { + str += c + } else { + errorMessage(`unexpected token ${c}`) + return undefined + } } } parseJsonString.position = pos diff --git a/spec/json_parse_tests.json b/spec/json_parse_tests.json index ca8db0cd3..56b923636 100644 --- a/spec/json_parse_tests.json +++ b/spec/json_parse_tests.json @@ -1022,8 +1022,8 @@ { "name": "string space", "valid": true, - "json": "\" \"", - "data": " " + "json": "[\" \"]", + "data": [" "] }, { "name": "string start escape unclosed", diff --git a/spec/jtd-schema.spec.ts b/spec/jtd-schema.spec.ts index a5def7eb0..72669d039 100644 --- a/spec/jtd-schema.spec.ts +++ b/spec/jtd-schema.spec.ts @@ -5,6 +5,7 @@ import getAjvInstances from "./ajv_instances" import {withStandalone} from "./ajv_standalone" import jtdValidationTests = require("./json-typedef-spec/tests/validation.json") import jtdInvalidSchemasTests = require("./json-typedef-spec/tests/invalid_schemas.json") +// tests from https://github.com/nst/JSONTestSuite import jsonParseTests = require("./json_parse_tests.json") import assert = require("assert") // import AjvPack from "../dist/standalone/instance" @@ -25,6 +26,8 @@ interface JSONParseTest { valid: boolean | null json: string data?: unknown + only?: boolean + skip?: boolean } interface JSONParseTestSuite { @@ -154,26 +157,29 @@ describe("JSON Type Definition", () => { const ajv = new _AjvJTD() const parseJson: JTDParser = ajv.compileParser({}) const parse: {[K in "string" | "number" | "array" | "object"]: JTDParser} = { - string: ajv.compileParser({type: "string"}), - number: ajv.compileParser({type: "float64"}), + string: ajv.compileParser({elements: {type: "string"}}), + number: ajv.compileParser({elements: {type: "float64"}}), array: ajv.compileParser({elements: {}}), object: ajv.compileParser({values: {}}), } for (const {suite, tests} of jsonParseTests as JSONParseTestSuite[]) { describe(suite, () => { - for (const {valid, name, json, data} of tests) { + for (const test of tests) { + const {valid, name, json, data} = test if (valid) { it(`should parse ${name}`, () => shouldParse(parseJson, json, data)) if (suite in parse) { - it.skip(`should parse as ${suite}: ${name}`, () => - shouldParse(parse[suite], json, data)) + _it(test)(`should parse as ${suite}: ${name}`, () => + shouldParse(parse[suite], json, data) + ) } } else if (valid === false) { it(`should fail parsing ${name}`, () => shouldFail(parseJson, json)) if (suite in parse) { - it.skip(`should fail parsing as ${suite}: ${name}`, () => - shouldFail(parse[suite], json)) + _it(test)(`should fail parsing as ${suite}: ${name}`, () => + shouldFail(parse[suite], json) + ) } } } @@ -182,6 +188,12 @@ describe("JSON Type Definition", () => { }) }) +type TestFunc = typeof it | typeof it.only | typeof it.skip + +function _it({only, skip}: JSONParseTest): TestFunc { + return skip ? it.skip : only ? it.only : it +} + function shouldParse(parse: JTDParser, str: string, res: unknown): void { assert.deepStrictEqual(parse(str), res) assert.strictEqual(parse.message, undefined)