Skip to content

Commit dd70a4d

Browse files
committed
add position property to errors
1 parent 6e1d25e commit dd70a4d

File tree

3 files changed

+110
-63
lines changed

3 files changed

+110
-63
lines changed

deno.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,6 @@
5555
"no-self-compare",
5656
"no-sparse-arrays",
5757
"no-sync-fn-in-async-fn",
58-
"no-throw-literal",
5958
"no-useless-rename",
6059
"prefer-ascii",
6160
"single-var-declarator"

dictionary/parser.ts

Lines changed: 20 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ import { escape as escapeHtml } from "@std/html/entities";
33
import { escape as escapeRegex } from "@std/regexp/escape";
44
import nlp from "compromise/three";
55
import { nullableAsArray, throwError } from "../misc/misc.ts";
6-
import { ArrayResultError } from "../src/array_result.ts";
76
import {
87
all,
98
allAtLeastOnceWithCheck,
@@ -108,7 +107,7 @@ const nounOnly = checkedSequence(
108107
sequence(optionalAll(keyword("gerund")), optionalNumber)
109108
.skip(closeParenthesis),
110109
)
111-
.map(([[noun, plural], [gerund, number]]) => {
110+
.mapWithPositionedError(([[noun, plural], [gerund, number]]) => {
112111
if (plural == null) {
113112
if (number == null) {
114113
const sentence = nlp(noun);
@@ -122,16 +121,12 @@ const nounOnly = checkedSequence(
122121
.toPlural()
123122
.text();
124123
if (singular === "" || plural === "") {
125-
throw new ArrayResultError(
126-
`no singular or plural form found for "${noun}". consider ` +
127-
"providing both singular and plural forms instead",
128-
);
124+
throw `no singular or plural form found for "${noun}". consider ` +
125+
"providing both singular and plural forms instead";
129126
}
130127
if (noun !== singular) {
131-
throw new ArrayResultError(
132-
`conjugation error: "${noun}" is not "${singular}". ` +
133-
"consider providing both singular and plural forms instead",
134-
);
128+
throw `conjugation error: "${noun}" is not "${singular}". ` +
129+
"consider providing both singular and plural forms instead";
135130
}
136131
return {
137132
singular: escapeHtml(singular),
@@ -156,10 +151,8 @@ const nounOnly = checkedSequence(
156151
}
157152
} else {
158153
if (number != null) {
159-
throw new ArrayResultError(
160-
"plural or singular keyword within tag " +
161-
"must not be provided when singular and plural forms are defined",
162-
);
154+
throw "plural or singular keyword within tag " +
155+
"must not be provided when singular and plural forms are defined";
163156
}
164157
return {
165158
singular: escapeHtml(noun),
@@ -287,10 +280,10 @@ const prepositionDefinition = simpleDefinitionWithTemplate(
287280
)
288281
.map((preposition) => ({ type: "preposition", preposition }) as const);
289282
const numeralDefinition = simpleDefinition(keyword("num"))
290-
.map((num) => {
283+
.mapWithPositionedError((num) => {
291284
const numeral = Number.parseInt(num);
292285
if (Number.isNaN(numeral)) {
293-
throw new ArrayResultError(`"${num}" is not a number`);
286+
throw `"${num}" is not a number`;
294287
} else {
295288
return { type: "numeral", numeral } as const;
296289
}
@@ -307,7 +300,7 @@ const fillerDefinition = checkedSequence(
307300
.map(([first, rest]) => [first, ...rest]),
308301
closeParenthesis,
309302
)
310-
.map(([forms]) => {
303+
.mapWithPositionedError(([forms]) => {
311304
if (forms.length === 1) {
312305
return {
313306
type: "filler",
@@ -329,9 +322,7 @@ const fillerDefinition = checkedSequence(
329322
return { type: "filler", before, repeat: repeatString, after } as const;
330323
}
331324
}
332-
throw new ArrayResultError(
333-
`"${forms.join("/")}" has no repetition pattern found`,
334-
);
325+
throw `"${forms.join("/")}" has no repetition pattern found`;
335326
});
336327
const fourFormPersonalPronounDefinition = checkedSequence(
337328
sequence(
@@ -411,9 +402,9 @@ const compoundAdjectiveDefinition = checkedSequence(
411402
closeParenthesis.with(adjective.parser),
412403
)
413404
.map((adjective) => ({ type: "compound adjective", adjective }) as const)
414-
.filter(({ adjective }) =>
405+
.filterWithPositionedError(({ adjective }) =>
415406
adjective.every((adjective) => adjective.adverb.length === 0) ||
416-
throwError(new ArrayResultError("compound adjective cannot have adverb"))
407+
throwError("compound adjective cannot have adverb")
417408
);
418409
const verbDefinition = checkedSequence(
419410
sequence(
@@ -519,10 +510,10 @@ const verbDefinition = checkedSequence(
519510
}),
520511
),
521512
)
522-
.map<Definition>(([[verb, forms], rest]) => {
513+
.mapWithPositionedError<Definition>(([[verb, forms], rest]) => {
523514
if (rest == null) {
524515
if (forms != null) {
525-
throw new ArrayResultError("modal verbs shouldn't be conjugated");
516+
throw "modal verbs shouldn't be conjugated";
526517
}
527518
return { type: "modal verb", verb: escapeHtml(verb) };
528519
} else {
@@ -540,17 +531,13 @@ const verbDefinition = checkedSequence(
540531
FutureTense: string;
541532
};
542533
if (conjugations == null) {
543-
throw new ArrayResultError(
544-
`no verb conjugation found for "${verb}". consider providing ` +
545-
"all conjugations instead",
546-
);
534+
throw `no verb conjugation found for "${verb}". consider providing ` +
535+
"all conjugations instead";
547536
}
548537
if (verb !== conjugations.Infinitive) {
549-
throw new ArrayResultError(
550-
`conjugation error: "${verb}" is not ` +
551-
`"${conjugations.Infinitive}". consider providing all ` +
552-
"conjugations instead",
553-
);
538+
throw `conjugation error: "${verb}" is not ` +
539+
`"${conjugations.Infinitive}". consider providing all ` +
540+
"conjugations instead";
554541
}
555542
presentPlural = escapeHtml(conjugations.Infinitive);
556543
presentSingular = escapeHtml(conjugations.PresentTense);

src/parser/parser_lib.ts

Lines changed: 90 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -37,11 +37,20 @@ export class Parser<T> {
3737
.map(({ value, length }) => ({ value: mapper(value), length }))
3838
);
3939
}
40+
mapWithPositionedError<U>(mapper: (value: T) => U): Parser<U> {
41+
return withPosition(this)
42+
.map((value) => withPositionedError(() => mapper(value.value), value));
43+
}
4044
filter(mapper: (value: T) => boolean): Parser<T> {
4145
return new Parser((input) =>
4246
this.rawParser(input).filter(({ value }) => mapper(value))
4347
);
4448
}
49+
filterWithPositionedError(mapper: (value: T) => boolean): Parser<T> {
50+
return withPosition(this)
51+
.filter((value) => withPositionedError(() => mapper(value.value), value))
52+
.map(({ value }) => value);
53+
}
4554
then<U>(mapper: (value: T) => Parser<U>): Parser<U> {
4655
return new Parser((position) =>
4756
this.rawParser(position)
@@ -71,13 +80,36 @@ export class Parser<T> {
7180
return sequence(this, parser).map(([arrayResult]) => arrayResult);
7281
}
7382
}
74-
export class UnexpectedError extends ArrayResultError {
75-
constructor(unexpected: string, expected: string) {
76-
super(`unexpected ${unexpected}. ${expected} were expected instead`);
83+
export type Position = Readonly<{ position: number; length: number }>;
84+
export class PositionedError extends ArrayResultError {
85+
public position: null | Position;
86+
constructor(message: string, position?: Position) {
87+
super(message);
88+
this.position = position ?? null;
89+
this.name = "PositionedError";
90+
}
91+
}
92+
function withPositionedError<T>(fn: () => T, position: Position): T {
93+
try {
94+
return fn();
95+
} catch (error) {
96+
if (typeof error === "string") {
97+
throw new PositionedError(error, position);
98+
} else {
99+
throw error;
100+
}
101+
}
102+
}
103+
export class UnexpectedError extends PositionedError {
104+
constructor(unexpected: string, expected: string, position?: Position) {
105+
super(
106+
`unexpected ${unexpected}. ${expected} were expected instead`,
107+
position,
108+
);
77109
this.name = "UnexpectedError";
78110
}
79111
}
80-
export class UnrecognizedError extends ArrayResultError {
112+
export class UnrecognizedError extends PositionedError {
81113
constructor(element: string) {
82114
super(`${element} is unrecognized`);
83115
this.name = "UnrecognizedError";
@@ -183,36 +215,47 @@ export function count(
183215
): Parser<number> {
184216
return parser.map(({ length }) => length);
185217
}
186-
function describeSource(source: string): string {
187-
if (source === "") {
188-
return "end of text";
218+
function generateError(
219+
position: number,
220+
expected: string,
221+
): ArrayResult<never> {
222+
let source: string;
223+
let length: number;
224+
if (position === currentSource.length) {
225+
source = "end of text";
226+
length = 0;
189227
} else {
190-
const [token] = source.match(/\S*/)!;
228+
const sourceString = currentSource.slice(position);
229+
const [token] = sourceString.match(/\S*/)!;
191230
if (token === "") {
192-
if (/^\r?\n/.test(source)) {
193-
return "newline";
231+
if (/^\r?\n/.test(sourceString)) {
232+
source = "newline";
233+
length = 0;
194234
} else {
195-
return "space";
235+
const [token] = sourceString.match(/\s*?(?=\r?\n)/)!;
236+
source = "space";
237+
length = token.length;
196238
}
197239
} else {
198-
return `"${token}"`;
240+
source = `"${token}"`;
241+
length = sourceString.length;
199242
}
200243
}
244+
return new ArrayResult(
245+
new UnexpectedError(source, expected, { position, length }),
246+
);
201247
}
202248
export function matchCapture(
203249
regex: RegExp,
204250
description: string,
205251
): Parser<RegExpMatchArray> {
206252
const newRegex = new RegExp(`^${regex.source}`, regex.flags);
207253
return new Parser((position) => {
208-
const sourceString = currentSource.slice(position);
209-
const match = sourceString.match(newRegex);
254+
const match = currentSource.slice(position).match(newRegex);
210255
if (match != null) {
211256
return new ArrayResult([{ value: match, length: match[0].length }]);
212257
} else {
213-
return new ArrayResult(
214-
new UnexpectedError(describeSource(sourceString), description),
215-
);
258+
return generateError(position, description);
216259
}
217260
});
218261
}
@@ -233,12 +276,7 @@ export function matchString(
233276
length: match.length,
234277
}]);
235278
} else {
236-
return new ArrayResult(
237-
new UnexpectedError(
238-
describeSource(currentSource.slice(position)),
239-
description,
240-
),
241-
);
279+
return generateError(position, description);
242280
}
243281
});
244282
}
@@ -250,19 +288,20 @@ export const allRest = new Parser((position) =>
250288
);
251289
export const end = new Parser((position) =>
252290
position === currentSource.length
291+
? new ArrayResult([{ value: null, length: 0 }])
292+
: generateError(position, "end of text")
293+
);
294+
export const notEnd = new Parser((position) =>
295+
position < currentSource.length
253296
? new ArrayResult([{ value: null, length: 0 }])
254297
: new ArrayResult(
255298
new UnexpectedError(
256-
describeSource(currentSource.slice(position)),
257299
"end of text",
300+
"not end of text",
301+
{ position, length: currentSource.length - position },
258302
),
259303
)
260304
);
261-
export const notEnd = new Parser((position) =>
262-
position < currentSource.length
263-
? new ArrayResult([{ value: null, length: 0 }])
264-
: new ArrayResult(new UnexpectedError("end of text", "not end of text"))
265-
);
266305
export function withSource<T>(
267306
parser: Parser<T>,
268307
): Parser<readonly [value: T, source: string]> {
@@ -276,14 +315,36 @@ export function withSource<T>(
276315
}))
277316
);
278317
}
318+
export function withPosition<T>(
319+
parser: Parser<T>,
320+
): Parser<Readonly<{ value: T }> & Position> {
321+
return new Parser((position) =>
322+
parser.rawParser(position).map(({ value, length }) => ({
323+
value: { value, position, length },
324+
length,
325+
}))
326+
);
327+
}
279328
export class CheckedParser<T> {
280329
constructor(public check: Parser<unknown>, public parser: Parser<T>) {}
281330
map<U>(mapper: (value: T) => U): CheckedParser<U> {
282331
return new CheckedParser(this.check, this.parser.map(mapper));
283332
}
333+
mapWithPositionedError<U>(mapper: (value: T) => U): CheckedParser<U> {
334+
return new CheckedParser(
335+
this.check,
336+
this.parser.mapWithPositionedError(mapper),
337+
);
338+
}
284339
filter(check: (value: T) => boolean): CheckedParser<T> {
285340
return new CheckedParser(this.check, this.parser.filter(check));
286341
}
342+
filterWithPositionedError(check: (value: T) => boolean): CheckedParser<T> {
343+
return new CheckedParser(
344+
this.check,
345+
this.parser.filterWithPositionedError(check),
346+
);
347+
}
287348
}
288349
export function checkedSequence<T, U>(
289350
check: Parser<T>,

0 commit comments

Comments
 (0)