Use location data in error messages

This commit is contained in:
Rene Saarsoo 2020-09-21 18:03:07 +03:00
parent 240691479f
commit 79453750e7
4 changed files with 47 additions and 21 deletions

7
src/parser/ParseError.ts Normal file
View File

@ -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}`);
}
}

View File

@ -3,11 +3,11 @@ import { parse } from ".";
describe("Parser", () => { describe("Parser", () => {
it("throws error for empty file", () => { it("throws error for empty file", () => {
expect(() => parse("")).toThrowErrorMatchingInlineSnapshot( 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( 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", () => { it("requires duration and power parameters to be specified", () => {
expect(() => expect(() =>
parseInterval("Interval: 50%") parseInterval("Interval: 50%")
).toThrowErrorMatchingInlineSnapshot(`"Duration not specified"`); ).toThrowErrorMatchingInlineSnapshot(
`"Duration not specified at line 2 char 1"`
);
expect(() => expect(() =>
parseInterval("Interval: 30:00") parseInterval("Interval: 30:00")
).toThrowErrorMatchingInlineSnapshot(`"Power not specified"`); ).toThrowErrorMatchingInlineSnapshot(
`"Power not specified at line 2 char 1"`
);
expect(() => expect(() =>
parseInterval("Interval: 10rpm") parseInterval("Interval: 10rpm")
).toThrowErrorMatchingInlineSnapshot(`"Duration not specified"`); ).toThrowErrorMatchingInlineSnapshot(
`"Duration not specified at line 2 char 1"`
);
}); });
it("allows any order for interval parameters", () => { it("allows any order for interval parameters", () => {
@ -215,22 +221,22 @@ Cooldown: 5:30 70%..45%
expect(() => expect(() =>
parseInterval("Interval: 10 50%") parseInterval("Interval: 10 50%")
).toThrowErrorMatchingInlineSnapshot( ).toThrowErrorMatchingInlineSnapshot(
`"Unrecognized interval parameter \\"10\\""` `"Unrecognized interval parameter \\"10\\" at line 2 char 11"`
); );
expect(() => expect(() =>
parseInterval("Interval: :10 50%") parseInterval("Interval: :10 50%")
).toThrowErrorMatchingInlineSnapshot( ).toThrowErrorMatchingInlineSnapshot(
`"Unrecognized interval parameter \\":10\\""` `"Unrecognized interval parameter \\":10\\" at line 2 char 11"`
); );
expect(() => expect(() =>
parseInterval("Interval: 0:100 50%") parseInterval("Interval: 0:100 50%")
).toThrowErrorMatchingInlineSnapshot( ).toThrowErrorMatchingInlineSnapshot(
`"Unrecognized interval parameter \\"0:100\\""` `"Unrecognized interval parameter \\"0:100\\" at line 2 char 11"`
); );
expect(() => expect(() =>
parseInterval("Interval: 00:00:00:10 50%") parseInterval("Interval: 00:00:00:10 50%")
).toThrowErrorMatchingInlineSnapshot( ).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(() => expect(() =>
parseInterval("Interval: 10:00 50% foobar") parseInterval("Interval: 10:00 50% foobar")
).toThrowErrorMatchingInlineSnapshot( ).toThrowErrorMatchingInlineSnapshot(
`"Unrecognized interval parameter \\"foobar\\""` `"Unrecognized interval parameter \\"foobar\\" at line 2 char 21"`
); );
expect(() => expect(() =>
parseInterval("Interval: 10:00 50% 123blah") parseInterval("Interval: 10:00 50% 123blah")
).toThrowErrorMatchingInlineSnapshot( ).toThrowErrorMatchingInlineSnapshot(
`"Unrecognized interval parameter \\"123blah\\""` `"Unrecognized interval parameter \\"123blah\\" at line 2 char 21"`
); );
expect(() => expect(() =>
parseInterval("Interval: 10:00 50% ^*&") parseInterval("Interval: 10:00 50% ^*&")
).toThrowErrorMatchingInlineSnapshot( ).toThrowErrorMatchingInlineSnapshot(
`"Unrecognized interval parameter \\"^*&\\""` `"Unrecognized interval parameter \\"^*&\\" at line 2 char 21"`
); );
}); });
@ -256,7 +262,7 @@ Cooldown: 5:30 70%..45%
expect(() => expect(() =>
parseInterval("Interval: 30:00 5% \n CustomInterval: 15:00 10%") parseInterval("Interval: 30:00 5% \n CustomInterval: 15:00 10%")
).toThrowErrorMatchingInlineSnapshot( ).toThrowErrorMatchingInlineSnapshot(
`"Unexpected token [text CustomInterval: 15:00 10%]"` `"Unexpected token [text CustomInterval: 15:00 10%] at line 3 char 1"`
); );
}); });
}); });

View File

@ -1,5 +1,6 @@
import { Interval, Workout } from "../ast"; 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">>; type Header = Partial<Omit<Workout, "intervals">>;
@ -50,7 +51,10 @@ const parseHeader = (tokens: Token[]): [Header, Token[]] => {
type IntervalData = Omit<Interval, "type">; type IntervalData = Omit<Interval, "type">;
const parseIntervalParams = (tokens: Token[]): [IntervalData, Token[]] => { const parseIntervalParams = (
tokens: Token[],
loc: SourceLocation
): [IntervalData, Token[]] => {
const data: Partial<IntervalData> = {}; const data: Partial<IntervalData> = {};
while (tokens[0]) { while (tokens[0]) {
@ -73,10 +77,10 @@ const parseIntervalParams = (tokens: Token[]): [IntervalData, Token[]] => {
} }
if (!("duration" in data)) { if (!("duration" in data)) {
throw new Error("Duration not specified"); throw new ParseError("Duration not specified", loc);
} }
if (!("intensity" in data)) { if (!("intensity" in data)) {
throw new Error("Power not specified"); throw new ParseError("Power not specified", loc);
} }
return [data as IntervalData, tokens]; return [data as IntervalData, tokens];
@ -89,7 +93,8 @@ const parseIntervals = (tokens: Token[]): Interval[] => {
const token = tokens.shift() as Token; const token = tokens.shift() as Token;
if (token.type === "label" && isIntervalLabelTokenValue(token.value)) { if (token.type === "label" && isIntervalLabelTokenValue(token.value)) {
const [{ duration, intensity, cadence }, rest] = parseIntervalParams( const [{ duration, intensity, cadence }, rest] = parseIntervalParams(
tokens tokens,
token.loc
); );
intervals.push({ intervals.push({
type: token.value, type: token.value,
@ -101,7 +106,10 @@ const parseIntervals = (tokens: Token[]): Interval[] => {
} else if (token.type === "text" && token.value === "") { } else if (token.type === "text" && token.value === "") {
// Ignore empty lines // Ignore empty lines
} else { } 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); const [header, intervalTokens] = parseHeader(tokens);
if (header.name === undefined) { 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 { return {

View File

@ -1,3 +1,5 @@
import { ParseError } from "./ParseError";
export type HeaderLabelTokenValue = "Name" | "Author" | "Description"; export type HeaderLabelTokenValue = "Name" | "Author" | "Description";
export type IntervalLabelTokenValue = export type IntervalLabelTokenValue =
| "Warmup" | "Warmup"
@ -73,7 +75,7 @@ const tokenizeValueParam = (text: string, loc: SourceLocation): Token => {
if (/^[0-9]+%$/.test(text)) { if (/^[0-9]+%$/.test(text)) {
return { type: "intensity", value: toFraction(toInteger(text)), loc }; 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 = ( const tokenizeParams = (