Skip to content

Commit

Permalink
feat: improve error messages color and format (#132)
Browse files Browse the repository at this point in the history
* refactor(core): improve lexer error message

* fix(ignored): trim ingored lines

* refactor(core): improve parser error with colors and new error message format

* refactor(core): improve slangroom error message with new colors and format

* test(core): update error tests to new colors and format

* fix(fs): catch more error and report them thhrough plugin context fail method

* test(fs): update failing tests

* test(core): reactivate error tests

* test: update error messages on failing tests

* refacotr(core): use colored text instead of colored backgorund on parser errors

Remove also quotation marks around worng and suggested words

* refacotr(core): use colored text instead of colored backgorund on lexer errors

* test: update failing tests error message checks

* feat(core): improve a bit the parser

In case a statment start with 'open' or 'connect' then it will be only parsed against, respectively, open or connect slangroom statements.

* refactor(core): imrpove parse error reporintg when multiple tokens are suggested as solution

* fix(pocketbase): catch error when listing and return it through context fail method
  • Loading branch information
matteo-cristino authored May 9, 2024
1 parent 5947902 commit f25a04b
Show file tree
Hide file tree
Showing 18 changed files with 337 additions and 78 deletions.
2 changes: 1 addition & 1 deletion pkg/core/src/lexer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ export class LexError extends Error {
constructor(t: Token) {
super();
this.name = 'LexError';
this.message = `unclosed single-quote at ${t.lineNo}:${t.start + 1}-${t.end + 1}: ${t.raw}`;
this.message = `at ${t.lineNo}:${t.start + 1}-${t.end + 1}\n unclosed single-quote \x1b[31m${t.raw}\x1b[0m`;
}
}

Expand Down
37 changes: 25 additions & 12 deletions pkg/core/src/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@

import { PluginMap, Token, type PluginMapKey } from '@slangroom/core';

export const errorColor = (s: string): string => '\x1b[31m' + s + '\x1b[0m';
export const suggestedColor = (s: string): string => '\x1b[32m' + s + '\x1b[0m';
export const missingColor = (s: string): string => '\x1b[36m' + s + '\x1b[0m';
export const extraColor = (s: string): string => '\x1b[35m' + s + '\x1b[0m';

/**
* Represents an error encountered during the parsing phrase.
*
Expand Down Expand Up @@ -36,11 +41,11 @@ export class ParseError extends Error {
* ```
*/
static wrong(have: Token, wantFirst: string, ...wantRest: string[]) {
const wantsQuoted = [wantFirst, ...wantRest].map((x) => JSON.stringify(x)).join(' ');
const haveQuoted = JSON.stringify(have.raw);
const wantsColored = [wantFirst, ...wantRest].map((x) => suggestedColor(x)).join(' or ');
const haveRaw = have.raw;
return new ParseError(
`${haveQuoted} at ${have.lineNo}:${have.start + 1}-${have.end + 1
} must be one of: ${wantsQuoted}`,
`at ${have.lineNo}:${have.start + 1}-${have.end + 1
}\n ${errorColor(haveRaw)} may be ${wantsColored}`,
);
}

Expand Down Expand Up @@ -69,15 +74,15 @@ export class ParseError extends Error {
* which means that there exist no prior token.
*/
static missing(prevTokenOrLineNo: Token | number, wantFirst: string, ...wantRest: string[]) {
const wantsQuoted = [wantFirst, ...wantRest].map((x) => JSON.stringify(x)).join(' ');
const wantsColored = [wantFirst, ...wantRest].map((x) => missingColor(x)).join(', ');
if (typeof prevTokenOrLineNo == 'number') {
const lineNo = prevTokenOrLineNo;
return new ParseError(`at ${lineNo}, missing one of: ${wantsQuoted}`);
return new ParseError(`at ${lineNo}\n missing one of: ${wantsColored}`);
}
const token = prevTokenOrLineNo;
return new ParseError(
`at ${token.lineNo}:${token.start + 1}-${token.end + 1
}, must be followed by one of: ${wantsQuoted}`,
}\n must be followed by one of: ${wantsColored}`,
);
}

Expand All @@ -92,7 +97,8 @@ export class ParseError extends Error {
*/
static extra(token: Token) {
return new ParseError(
`extra token ${token.lineNo}:${token.start + 1}-${token.end + 1}: ${token.raw}`,
`at ${token.lineNo}:${token.start + 1}-${token.end + 1
}\n extra token ${extraColor(token.raw)}`,
);
}

Expand Down Expand Up @@ -214,13 +220,17 @@ export const parse = (p: PluginMap, t: Token[], lineNo: number): Cst => {
errors: [],
};
let givenThen: 'given' | 'then' | undefined;

let openConnect: 'open' | 'connect' | undefined;
if (t[0]) {
if (t[0].name != 'given' && t[0].name != 'then')
cst.errors.push({ message: ParseError.wrong(t[0], 'given', 'then'), lineNo, start: t[0].start, end: t[0].end });
else givenThen = t[0].name;
if (t[1]) {
if (t[1].raw !== 'I') cst.errors.push({message: ParseError.wrong(t[1], 'I'), lineNo, start: t[1].start, end: t[1].end});
if (t[2]) {
if (t[2].raw == 'open') openConnect = 'open';
else if (t[2].raw == 'connect') openConnect = 'connect';
}
} else cst.errors.push({ message: ParseError.missing(t[0], 'I'), lineNo, start: t[0].start, end: t[0].end});
} else {
cst.errors.push({ message: ParseError.missing(lineNo, 'Given I', 'Then I'), lineNo});
Expand All @@ -241,19 +251,22 @@ export const parse = (p: PluginMap, t: Token[], lineNo: number): Cst => {
if (curErrLen !== undefined && m.err.length > curErrLen) throw lemmeout;
};
try {
// check open and connect statement only against the correct statements
if(openConnect && (openConnect !== k.openconnect)) throw lemmeout;

// Open 'ident' and|Connect to 'ident' and
if (k.openconnect === 'open') {
if (t[++i]?.name !== 'open') newErr(i, 'open');
const ident = t[++i];
if (ident?.isIdent) m.open = ident.raw.slice(1, -1);
else newErr(i, '<identifier>');
else newErr(i, '\'<identifier>\'');
if (t[++i]?.name !== 'and') newErr(i, 'and');
} else if (k.openconnect === 'connect') {
if (t[++i]?.name !== 'connect') newErr(i, 'connect');
if (t[++i]?.name !== 'to') newErr(i, 'to');
const ident = t[++i];
if (ident?.isIdent) m.connect = ident.raw.slice(1, -1);
else newErr(i, '<identifier>');
else newErr(i, '\'<identifier>\'');
if (t[++i]?.name !== 'and') newErr(i, 'and');
}

Expand All @@ -275,7 +288,7 @@ export const parse = (p: PluginMap, t: Token[], lineNo: number): Cst => {
if (ident?.isIdent) {
if (tokName) m.bindings.set(tokName.name, ident.raw.slice(1, -1));
} else {
newErr(i, '<identifier>');
newErr(i, '\'<identifier>\'');
}
if (t[++i]?.name !== 'and') newErr(i, 'and');
});
Expand Down
16 changes: 13 additions & 3 deletions pkg/core/src/slangroom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
import { getIgnoredStatements } from '@slangroom/ignored';
import { type ZenOutput, ZenParams, zencodeExec } from '@slangroom/shared';
import { lex, parse, visit, Plugin, PluginMap, PluginContextImpl } from '@slangroom/core';
// error colors
import { errorColor, suggestedColor, missingColor, extraColor } from '@slangroom/core';

/**
* Just a utility type to ease typing.
Expand Down Expand Up @@ -145,13 +147,21 @@ const thorwErrors = (errorArray: GenericError[], contract: string) => {
let e = "";
for (let i = lineStart; i < lineEnd; i++) {
const linePrefix = `${i} | `;
e = e.concat(`\x1b[33m${linePrefix}\x1b[0m${contractLines[i]}\n`);
if (i === lineNumber -1) {
let bold = '\x1b[1;30m' + contractLines[i]!.slice(colStart, colEnd) + '\x1b[0m' + '\x1b[41m';
const redBackground = '\x1b[41m' + contractLines[i]!.slice(0, colStart) + bold + contractLines[i]!.slice(colEnd) + '\x1b[0m';
e = e.concat(`\x1b[33m${linePrefix}\x1b[0m${redBackground}\n`);
e = e.concat(' '.repeat(colStart + linePrefix.length) + '\x1b[31m' + '^'.repeat(colEnd - colStart) + '\x1b[0m', '\n');
}
} else { e = e.concat(`\x1b[33m${linePrefix}\x1b[0m${contractLines[i]}\n`); }
}
e = e.concat('\n' + 'Error colors:\n');
e = e.concat(` - ${errorColor('error')}\n`);
e = e.concat(` - ${suggestedColor('suggested words')}\n`);
e = e.concat(` - ${missingColor('missing words')}\n`);
e = e.concat(` - ${extraColor('extra words')}\n`);

for (let err of errorArray) {
e = e.concat(err.message + '\n');
e = e.concat('\n' + err.message + '\n');
}
throw new Error(e);
}
123 changes: 112 additions & 11 deletions pkg/core/test/errors.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@

import { Plugin, Slangroom } from '@slangroom/core';
import test from 'ava';
// error colors
import { errorColor, suggestedColor, missingColor, extraColor } from '@slangroom/core';


test('@slangroom/core errors are shown and context is shown with line number', async (t) => {
const plugin = new Plugin();
Expand All @@ -13,17 +16,37 @@ test('@slangroom/core errors are shown and context is shown with line number', a
Then print data`)

const expected = `\x1b[33m0 | \x1b[0mRule unknown ignore
\x1b[33m1 | \x1b[0m Given I gibberish
\x1b[33m1 | \x1b[0m\x1b[41m Given I \x1b[1;30mgibberish\x1b[0m\x1b[41m\x1b[0m
\x1b[31m^^^^^^^^^\x1b[0m
\x1b[33m2 | \x1b[0m Given nothing
\x1b[33m3 | \x1b[0m Then print data
ParseError: "gibberish" at 2:9-17 must be one of: "send"
ParseError: at 2:9-17, must be followed by one of: "param"
ParseError: at 2, missing one of: "<identifier>"
ParseError: at 2, missing one of: "and"
ParseError: at 2, missing one of: "do"
ParseError: at 2, missing one of: "some"
ParseError: at 2, missing one of: "action"
Error colors:
- ${errorColor('error')}
- ${suggestedColor('suggested words')}
- ${missingColor('missing words')}
- ${extraColor('extra words')}
ParseError: at 2:9-17
${errorColor('gibberish')} may be ${suggestedColor('send')}
ParseError: at 2:9-17
must be followed by one of: ${missingColor('param')}
ParseError: at 2
missing one of: ${missingColor('\'<identifier>\'')}
ParseError: at 2
missing one of: ${missingColor('and')}
ParseError: at 2
missing one of: ${missingColor('do')}
ParseError: at 2
missing one of: ${missingColor('some')}
ParseError: at 2
missing one of: ${missingColor('action')}
`

const err = await t.throwsAsync(fn);
Expand All @@ -41,13 +64,91 @@ test('@slangroom/core lexer error', async (t) => {
Then print data`)

const expected = `\x1b[33m0 | \x1b[0mRule unknown ignore
\x1b[33m1 | \x1b[0m Given I send param 'param and do some action
\x1b[33m1 | \x1b[0m\x1b[41m Given I send param \x1b[1;30m'param and do some action\x1b[0m\x1b[41m\x1b[0m
\x1b[31m^^^^^^^^^^^^^^^^^^^^^^^^^\x1b[0m
\x1b[33m2 | \x1b[0m Given nothing
\x1b[33m3 | \x1b[0m Then print data
LexError: unclosed single-quote at 2:20-44: 'param and do some action
Error colors:
- ${errorColor('error')}
- ${suggestedColor('suggested words')}
- ${missingColor('missing words')}
- ${extraColor('extra words')}
LexError: at 2:20-44
unclosed single-quote ${errorColor('\'param and do some action')}
`

const err = await t.throwsAsync(fn);
t.is(err?.message, expected);
t.is(err?.message, expected);
});


test('@slangroom/core parser error does not start with given', async (t) => {
const plugin = new Plugin();
plugin.new('connect', ['param'], 'do some action', (_) => _.pass(null));

const slang = new Slangroom(plugin);
const fn = slang.execute(`Rule unknown ignore
Gibberish connect to 'url' and send param 'param' and do some action and aoibndwebnd
Given nothing
Then print data`)

const expected = `\x1b[33m0 | \x1b[0mRule unknown ignore
\x1b[33m1 | \x1b[0m\x1b[41m \x1b[1;30mGibberish\x1b[0m\x1b[41m connect to 'url' and send param 'param' and do some action and aoibndwebnd\x1b[0m
\x1b[31m^^^^^^^^^\x1b[0m
\x1b[33m2 | \x1b[0m Given nothing
\x1b[33m3 | \x1b[0m Then print data
Error colors:
- ${errorColor('error')}
- ${suggestedColor('suggested words')}
- ${missingColor('missing words')}
- ${extraColor('extra words')}
ParseError: at 2:1-9
${errorColor('Gibberish')} may be ${suggestedColor('given')} or ${suggestedColor('then')}
ParseError: at 2:11-17
${errorColor('connect')} may be ${suggestedColor('I')}
ParseError: at 2:19-20
${errorColor('to')} may be ${suggestedColor('connect')}
ParseError: at 2:22-26
${errorColor('\'url\'')} may be ${suggestedColor('to')}
ParseError: at 2:28-30
${errorColor('and')} may be ${suggestedColor('\'<identifier>\'')}
ParseError: at 2:32-35
${errorColor('send')} may be ${suggestedColor('and')}
ParseError: at 2:37-41
${errorColor('param')} may be ${suggestedColor('send')}
ParseError: at 2:43-49
${errorColor('\'param\'')} may be ${suggestedColor('param')}
ParseError: at 2:51-53
${errorColor('and')} may be ${suggestedColor('\'<identifier>\'')}
ParseError: at 2:55-56
${errorColor('do')} may be ${suggestedColor('and')}
ParseError: at 2:58-61
${errorColor('some')} may be ${suggestedColor('do')}
ParseError: at 2:63-68
${errorColor('action')} may be ${suggestedColor('some')}
ParseError: at 2:70-72
${errorColor('and')} may be ${suggestedColor('action')}
ParseError: at 2:74-84
extra token ${extraColor('aoibndwebnd')}
`

const err = await t.throwsAsync(fn);
t.is(err?.message, expected);
});
2 changes: 1 addition & 1 deletion pkg/core/test/lexer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ test('lexer works', (t) => {

const res = lex("When I encrypt the secret message 'message", 1);
if (res.ok) throw new Error("Lex fail to dectect unclosed single-quote");
t.is(res.error.message.message as string, `unclosed single-quote at 1:35-42: 'message`);
t.is(res.error.message.message as string, `at 1:35-42\n unclosed single-quote \x1b[31m'message\x1b[0m`);
});

test('token constructor erros', (t) => {
Expand Down
29 changes: 28 additions & 1 deletion pkg/core/test/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -571,7 +571,7 @@ test('parser works', (t) => {
{message: ParseError.wrong(new Token('send', 1, 24, 27), 'and'), lineNo: 1, start: 24, end: 27},
{message: ParseError.wrong(new Token('object', 1, 29, 34), 'send'), lineNo: 1, start: 29, end: 34},
{message: ParseError.wrong(new Token('\'myObj\'', 1, 36, 42), 'object'), lineNo: 1, start: 36, end: 42},
{message: ParseError.wrong(new Token('and', 1, 44, 46), '<identifier>'), lineNo: 1, start: 44, end: 46},
{message: ParseError.wrong(new Token('and', 1, 44, 46), '\'<identifier>\''), lineNo: 1, start: 44, end: 46},
{message: ParseError.wrong(new Token('send', 1, 48, 51), 'and'), lineNo: 1, start: 48, end: 51},
{message: ParseError.wrong(new Token('http', 1, 53, 56), 'send'), lineNo: 1, start: 53, end: 56},
{message: ParseError.wrong(new Token('request', 1, 58, 64), 'http'), lineNo: 1, start: 58, end: 64},
Expand Down Expand Up @@ -601,6 +601,33 @@ test('parser works', (t) => {
},
],
},
"Given I connect to": {
givenThen: 'given',
errors: [],
matches: [
{
key: {
openconnect: 'connect',
phrase: 'send http request',
params: ['object'],
},
bindings: new Map(),
err: [
{message: ParseError.missing(new Token('to', 1, 16, 17), '\'<identifier>\''), lineNo: 1},
{message: ParseError.missing(1, 'and'), lineNo: 1},
{message: ParseError.missing(1, 'send'), lineNo: 1},
{message: ParseError.missing(1, 'object'), lineNo: 1},
{message: ParseError.missing(1, '\'<identifier>\''), lineNo: 1},
{message: ParseError.missing(1, 'and'), lineNo: 1},
{message: ParseError.missing(1, 'send'), lineNo: 1},
{message: ParseError.missing(1, 'http'), lineNo: 1},
{message: ParseError.missing(1, 'request'), lineNo: 1},

],
lineNo: 1,
},
],
},
}).forEach(([give, want], index) => {
const lexed = lex(give, 1);
if (!lexed.ok) throw new Error(lexed.error.message.message);
Expand Down
1 change: 1 addition & 0 deletions pkg/fs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"version": "1.29.0",
"dependencies": {
"@slangroom/core": "workspace:*",
"@slangroom/shared": "workspace:*",
"axios": "^1.6.2",
"extract-zip": "^2.0.1"
},
Expand Down
Loading

0 comments on commit f25a04b

Please sign in to comment.