feat: enrich ParseError with schema and value context

Include the schema string and the input value in `ParseError` to
provide more helpful debugging information when parsing fails.
This commit is contained in:
hypercross 2026-04-22 16:14:23 +08:00
parent b5be558b57
commit 1061e12c81
2 changed files with 104 additions and 18 deletions

View File

@ -14,10 +14,20 @@ export class ParseError extends Error {
constructor(
message: string,
public position?: number,
public schema?: string,
public value?: string,
) {
super(
position !== undefined ? `${message} at position ${position}` : message,
);
let fullMessage = message;
if (position !== undefined) {
fullMessage += ` at position ${position}`;
}
if (schema !== undefined) {
fullMessage += `. Schema: ${schema}`;
}
if (value !== undefined) {
fullMessage += `. Value: ${value}`;
}
super(fullMessage);
this.name = "ParseError";
}
}

View File

@ -7,13 +7,16 @@ import type {
UnionSchema,
} from "./types";
import { ParseError } from "./parser";
import { schemaToTypeString } from "./type-utils";
class ValueParser {
private input: string;
private schemaString: string;
private pos: number = 0;
constructor(input: string) {
constructor(input: string, schemaString: string) {
this.input = input;
this.schemaString = schemaString;
}
private peek(): string {
@ -71,6 +74,8 @@ class ValueParser {
throw new ParseError(
`Unknown schema type: ${(schema as { type: string }).type}`,
this.pos,
this.schemaString,
this.input,
);
}
}
@ -109,7 +114,12 @@ class ValueParser {
}
const num = parseFloat(numStr);
if (isNaN(num)) {
throw new ParseError("Invalid number", this.pos - numStr.length);
throw new ParseError(
"Invalid number",
this.pos - numStr.length,
this.schemaString,
this.input,
);
}
return num;
}
@ -121,10 +131,20 @@ class ValueParser {
}
const num = parseFloat(numStr);
if (isNaN(num)) {
throw new ParseError("Invalid number", this.pos - numStr.length);
throw new ParseError(
"Invalid number",
this.pos - numStr.length,
this.schemaString,
this.input,
);
}
if (!Number.isInteger(num)) {
throw new ParseError("Expected integer value", this.pos - numStr.length);
throw new ParseError(
"Expected integer value",
this.pos - numStr.length,
this.schemaString,
this.input,
);
}
return num;
}
@ -140,7 +160,12 @@ class ValueParser {
if (this.consumeStr("false")) {
return false;
}
throw new ParseError("Expected true or false", this.pos);
throw new ParseError(
"Expected true or false",
this.pos,
this.schemaString,
this.input,
);
}
private parseStringLiteralValue(schema: StringLiteralSchema): string {
@ -174,6 +199,8 @@ class ValueParser {
throw new ParseError(
`Invalid value '"${value}"'. Expected '"${schema.value}"'`,
this.pos,
this.schemaString,
this.input,
);
}
@ -183,7 +210,12 @@ class ValueParser {
}
}
throw new ParseError("Unterminated string literal", this.pos);
throw new ParseError(
"Unterminated string literal",
this.pos,
this.schemaString,
this.input,
);
} else {
// 不带引号的字符串,像普通字符串一样解析
let value = "";
@ -201,6 +233,8 @@ class ValueParser {
throw new ParseError(
`Invalid value '${value}'. Expected '${schema.value}'`,
this.pos - value.length,
this.schemaString,
this.input,
);
}
@ -227,6 +261,8 @@ class ValueParser {
throw new ParseError(
`Value does not match any union member. Tried ${schema.members.length} alternatives.`,
this.pos,
this.schemaString,
this.input,
);
}
@ -240,7 +276,12 @@ class ValueParser {
this.consume();
hasOpenBracket = true;
} else if (!allowOmitBrackets) {
throw new ParseError("Expected [", this.pos);
throw new ParseError(
"Expected [",
this.pos,
this.schemaString,
this.input,
);
}
this.skipWhitespace();
@ -272,7 +313,12 @@ class ValueParser {
if (i < schema.elements.length - 1) {
if (!this.consumeStr(";")) {
throw new ParseError("Expected ;", this.pos);
throw new ParseError(
"Expected ;",
this.pos,
this.schemaString,
this.input,
);
}
}
}
@ -281,7 +327,12 @@ class ValueParser {
if (hasOpenBracket) {
if (!this.consumeStr("]")) {
throw new ParseError("Expected ]", this.pos);
throw new ParseError(
"Expected ]",
this.pos,
this.schemaString,
this.input,
);
}
}
@ -325,7 +376,12 @@ class ValueParser {
}
if (!hasOpenBracket && !allowOmitBrackets && !elementIsTupleOrArray) {
throw new ParseError("Expected [", this.pos);
throw new ParseError(
"Expected [",
this.pos,
this.schemaString,
this.input,
);
}
this.skipWhitespace();
@ -351,7 +407,12 @@ class ValueParser {
if (hasOpenBracket) {
if (!this.consumeStr("]")) {
throw new ParseError("Expected ]", this.pos);
throw new ParseError(
"Expected ]",
this.pos,
this.schemaString,
this.input,
);
}
}
@ -405,7 +466,12 @@ class ValueParser {
if (hasOpenBracket) {
if (!this.consumeStr("]")) {
throw new ParseError("Expected ]", this.pos);
throw new ParseError(
"Expected ]",
this.pos,
this.schemaString,
this.input,
);
}
}
@ -433,13 +499,23 @@ class ValueParser {
}
}
export function parseValue(schema: Schema, valueString: string): unknown {
const parser = new ValueParser(valueString.trim());
export function parseValue(
schema: Schema,
valueString: string,
schemaString?: string,
): unknown {
const sStr = schemaString || schemaToTypeString(schema);
const parser = new ValueParser(valueString.trim(), sStr);
const allowOmitBrackets = schema.type === "tuple" || schema.type === "array";
const value = parser.parseValue(schema, allowOmitBrackets);
if (parser.getPosition() < parser.getInputLength()) {
throw new ParseError("Unexpected input after value", parser.getPosition());
throw new ParseError(
"Unexpected input after value",
parser.getPosition(),
sStr,
valueString.trim(),
);
}
return value;