Use location data in error messages
This commit is contained in:
parent
240691479f
commit
79453750e7
|
|
@ -0,0 +1,7 @@
|
|||
import { SourceLocation } from "./tokenizer";
|
||||
|
||||
export class ParseError extends Error {
|
||||
constructor(msg: string, { row, col }: SourceLocation) {
|
||||
super(`${msg} at line ${row + 1} char ${col + 1}`);
|
||||
}
|
||||
}
|
||||
|
|
@ -3,11 +3,11 @@ import { parse } from ".";
|
|||
describe("Parser", () => {
|
||||
it("throws error for empty file", () => {
|
||||
expect(() => parse("")).toThrowErrorMatchingInlineSnapshot(
|
||||
`"Workout is missing a name. Use \`Name:\` to declare one."`
|
||||
`"Workout is missing a name. Use \`Name:\` to declare one. at line 1 char 1"`
|
||||
);
|
||||
|
||||
expect(() => parse(" \n \n \t")).toThrowErrorMatchingInlineSnapshot(
|
||||
`"Workout is missing a name. Use \`Name:\` to declare one."`
|
||||
`"Workout is missing a name. Use \`Name:\` to declare one. at line 1 char 1"`
|
||||
);
|
||||
});
|
||||
|
||||
|
|
@ -128,13 +128,19 @@ Cooldown: 5:30 70%..45%
|
|||
it("requires duration and power parameters to be specified", () => {
|
||||
expect(() =>
|
||||
parseInterval("Interval: 50%")
|
||||
).toThrowErrorMatchingInlineSnapshot(`"Duration not specified"`);
|
||||
).toThrowErrorMatchingInlineSnapshot(
|
||||
`"Duration not specified at line 2 char 1"`
|
||||
);
|
||||
expect(() =>
|
||||
parseInterval("Interval: 30:00")
|
||||
).toThrowErrorMatchingInlineSnapshot(`"Power not specified"`);
|
||||
).toThrowErrorMatchingInlineSnapshot(
|
||||
`"Power not specified at line 2 char 1"`
|
||||
);
|
||||
expect(() =>
|
||||
parseInterval("Interval: 10rpm")
|
||||
).toThrowErrorMatchingInlineSnapshot(`"Duration not specified"`);
|
||||
).toThrowErrorMatchingInlineSnapshot(
|
||||
`"Duration not specified at line 2 char 1"`
|
||||
);
|
||||
});
|
||||
|
||||
it("allows any order for interval parameters", () => {
|
||||
|
|
@ -215,22 +221,22 @@ Cooldown: 5:30 70%..45%
|
|||
expect(() =>
|
||||
parseInterval("Interval: 10 50%")
|
||||
).toThrowErrorMatchingInlineSnapshot(
|
||||
`"Unrecognized interval parameter \\"10\\""`
|
||||
`"Unrecognized interval parameter \\"10\\" at line 2 char 11"`
|
||||
);
|
||||
expect(() =>
|
||||
parseInterval("Interval: :10 50%")
|
||||
).toThrowErrorMatchingInlineSnapshot(
|
||||
`"Unrecognized interval parameter \\":10\\""`
|
||||
`"Unrecognized interval parameter \\":10\\" at line 2 char 11"`
|
||||
);
|
||||
expect(() =>
|
||||
parseInterval("Interval: 0:100 50%")
|
||||
).toThrowErrorMatchingInlineSnapshot(
|
||||
`"Unrecognized interval parameter \\"0:100\\""`
|
||||
`"Unrecognized interval parameter \\"0:100\\" at line 2 char 11"`
|
||||
);
|
||||
expect(() =>
|
||||
parseInterval("Interval: 00:00:00:10 50%")
|
||||
).toThrowErrorMatchingInlineSnapshot(
|
||||
`"Unrecognized interval parameter \\"00:00:00:10\\""`
|
||||
`"Unrecognized interval parameter \\"00:00:00:10\\" at line 2 char 11"`
|
||||
);
|
||||
});
|
||||
|
||||
|
|
@ -238,17 +244,17 @@ Cooldown: 5:30 70%..45%
|
|||
expect(() =>
|
||||
parseInterval("Interval: 10:00 50% foobar")
|
||||
).toThrowErrorMatchingInlineSnapshot(
|
||||
`"Unrecognized interval parameter \\"foobar\\""`
|
||||
`"Unrecognized interval parameter \\"foobar\\" at line 2 char 21"`
|
||||
);
|
||||
expect(() =>
|
||||
parseInterval("Interval: 10:00 50% 123blah")
|
||||
).toThrowErrorMatchingInlineSnapshot(
|
||||
`"Unrecognized interval parameter \\"123blah\\""`
|
||||
`"Unrecognized interval parameter \\"123blah\\" at line 2 char 21"`
|
||||
);
|
||||
expect(() =>
|
||||
parseInterval("Interval: 10:00 50% ^*&")
|
||||
).toThrowErrorMatchingInlineSnapshot(
|
||||
`"Unrecognized interval parameter \\"^*&\\""`
|
||||
`"Unrecognized interval parameter \\"^*&\\" at line 2 char 21"`
|
||||
);
|
||||
});
|
||||
|
||||
|
|
@ -256,7 +262,7 @@ Cooldown: 5:30 70%..45%
|
|||
expect(() =>
|
||||
parseInterval("Interval: 30:00 5% \n CustomInterval: 15:00 10%")
|
||||
).toThrowErrorMatchingInlineSnapshot(
|
||||
`"Unexpected token [text CustomInterval: 15:00 10%]"`
|
||||
`"Unexpected token [text CustomInterval: 15:00 10%] at line 3 char 1"`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { Interval, Workout } from "../ast";
|
||||
import { isIntervalLabelTokenValue, Token } from "./tokenizer";
|
||||
import { ParseError } from "./ParseError";
|
||||
import { isIntervalLabelTokenValue, SourceLocation, Token } from "./tokenizer";
|
||||
|
||||
type Header = Partial<Omit<Workout, "intervals">>;
|
||||
|
||||
|
|
@ -50,7 +51,10 @@ const parseHeader = (tokens: Token[]): [Header, Token[]] => {
|
|||
|
||||
type IntervalData = Omit<Interval, "type">;
|
||||
|
||||
const parseIntervalParams = (tokens: Token[]): [IntervalData, Token[]] => {
|
||||
const parseIntervalParams = (
|
||||
tokens: Token[],
|
||||
loc: SourceLocation
|
||||
): [IntervalData, Token[]] => {
|
||||
const data: Partial<IntervalData> = {};
|
||||
|
||||
while (tokens[0]) {
|
||||
|
|
@ -73,10 +77,10 @@ const parseIntervalParams = (tokens: Token[]): [IntervalData, Token[]] => {
|
|||
}
|
||||
|
||||
if (!("duration" in data)) {
|
||||
throw new Error("Duration not specified");
|
||||
throw new ParseError("Duration not specified", loc);
|
||||
}
|
||||
if (!("intensity" in data)) {
|
||||
throw new Error("Power not specified");
|
||||
throw new ParseError("Power not specified", loc);
|
||||
}
|
||||
|
||||
return [data as IntervalData, tokens];
|
||||
|
|
@ -89,7 +93,8 @@ const parseIntervals = (tokens: Token[]): Interval[] => {
|
|||
const token = tokens.shift() as Token;
|
||||
if (token.type === "label" && isIntervalLabelTokenValue(token.value)) {
|
||||
const [{ duration, intensity, cadence }, rest] = parseIntervalParams(
|
||||
tokens
|
||||
tokens,
|
||||
token.loc
|
||||
);
|
||||
intervals.push({
|
||||
type: token.value,
|
||||
|
|
@ -101,7 +106,10 @@ const parseIntervals = (tokens: Token[]): Interval[] => {
|
|||
} else if (token.type === "text" && token.value === "") {
|
||||
// Ignore empty lines
|
||||
} else {
|
||||
throw new Error(`Unexpected token [${token.type} ${token.value}]`);
|
||||
throw new ParseError(
|
||||
`Unexpected token [${token.type} ${token.value}]`,
|
||||
token.loc
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -112,7 +120,10 @@ export const parseTokens = (tokens: Token[]): Workout => {
|
|||
const [header, intervalTokens] = parseHeader(tokens);
|
||||
|
||||
if (header.name === undefined) {
|
||||
throw new Error("Workout is missing a name. Use `Name:` to declare one.");
|
||||
throw new ParseError(
|
||||
"Workout is missing a name. Use `Name:` to declare one.",
|
||||
{ row: 0, col: 0 }
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
import { ParseError } from "./ParseError";
|
||||
|
||||
export type HeaderLabelTokenValue = "Name" | "Author" | "Description";
|
||||
export type IntervalLabelTokenValue =
|
||||
| "Warmup"
|
||||
|
|
@ -73,7 +75,7 @@ const tokenizeValueParam = (text: string, loc: SourceLocation): Token => {
|
|||
if (/^[0-9]+%$/.test(text)) {
|
||||
return { type: "intensity", value: toFraction(toInteger(text)), loc };
|
||||
}
|
||||
throw new Error(`Unrecognized interval parameter "${text}"`);
|
||||
throw new ParseError(`Unrecognized interval parameter "${text}"`, loc);
|
||||
};
|
||||
|
||||
const tokenizeParams = (
|
||||
|
|
|
|||
Loading…
Reference in New Issue