From 79453750e79f5ca91399b7f583ac51ab00e2c031 Mon Sep 17 00:00:00 2001 From: Rene Saarsoo Date: Mon, 21 Sep 2020 18:03:07 +0300 Subject: [PATCH] Use location data in error messages --- src/parser/ParseError.ts | 7 +++++++ src/parser/parser.test.ts | 32 +++++++++++++++++++------------- src/parser/parser.ts | 25 ++++++++++++++++++------- src/parser/tokenizer.ts | 4 +++- 4 files changed, 47 insertions(+), 21 deletions(-) create mode 100644 src/parser/ParseError.ts diff --git a/src/parser/ParseError.ts b/src/parser/ParseError.ts new file mode 100644 index 0000000..d8165c8 --- /dev/null +++ b/src/parser/ParseError.ts @@ -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}`); + } +} diff --git a/src/parser/parser.test.ts b/src/parser/parser.test.ts index 01f8cf5..b7a2da0 100644 --- a/src/parser/parser.test.ts +++ b/src/parser/parser.test.ts @@ -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"` ); }); }); diff --git a/src/parser/parser.ts b/src/parser/parser.ts index a60c751..8523716 100644 --- a/src/parser/parser.ts +++ b/src/parser/parser.ts @@ -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>; @@ -50,7 +51,10 @@ const parseHeader = (tokens: Token[]): [Header, Token[]] => { type IntervalData = Omit; -const parseIntervalParams = (tokens: Token[]): [IntervalData, Token[]] => { +const parseIntervalParams = ( + tokens: Token[], + loc: SourceLocation +): [IntervalData, Token[]] => { const data: Partial = {}; 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 { diff --git a/src/parser/tokenizer.ts b/src/parser/tokenizer.ts index 887a547..cdfd483 100644 --- a/src/parser/tokenizer.ts +++ b/src/parser/tokenizer.ts @@ -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 = (