Skip to content

Commit

Permalink
Update Error handling. Fixes #102.
Browse files Browse the repository at this point in the history
Update API for GrammarError, peg$SyntaxError
Update docs.  Update CHANGELOG.
  • Loading branch information
hildjj committed Apr 30, 2021
1 parent b3551fa commit 635c939
Show file tree
Hide file tree
Showing 24 changed files with 875 additions and 365 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,12 @@ Released: TBD
from the `options.grammarSource` property. That property can contain arbitrary
data,for example, path to the currently parsed file.
[@Mingun](https://github.com/peggyjs/peggy/pull/95)
- Made usage of `GrammarError` and `peg$SyntaxError` more consistent. Use the
`format` method to get pretty string outputs. Updated the `peggy` binary to
make pretty errors. Slight breaking change: the format of a few error
messages have changed; use the `toString()` method on `GrammarError` to get
something close to the old text.
[@hildjj](https://github.com/peggyjs/peggy/pull/116)

### Bug fixes

Expand Down
10 changes: 7 additions & 3 deletions bin/peggy
Original file line number Diff line number Diff line change
Expand Up @@ -296,8 +296,9 @@ if (inputFile === "-") {
process.stdin.resume();
inputStream = process.stdin;
inputStream.on("error", () => {
abort("Can't read from file \"" + inputFile + "\".");
abort("Can't read from stdin.");
});
options.grammarSource = "stdin";
} else {
options.grammarSource = inputFile;
inputStream = fs.createReadStream(inputFile);
Expand All @@ -318,8 +319,11 @@ readStream(inputStream, input => {
try {
source = peg.generate(input, options);
} catch (e) {
if (e.location !== undefined) {
abort(e.location.start.line + ":" + e.location.start.column + ": " + e.message);
if (typeof e.format === "function") {
abort(e.format([{
source: options.grammarSource,
text: input
}]));
} else {
abort(e.message);
}
Expand Down
5 changes: 4 additions & 1 deletion docs/documentation.html
Original file line number Diff line number Diff line change
Expand Up @@ -262,7 +262,10 @@ <h2 id="using-the-parser">Using the Parser</h2>
result (the exact value depends on the grammar used to generate the parser) or
throw an exception if the input is invalid. The exception will contain
<code>location</code>, <code>expected</code>, <code>found</code> and
<code>message</code> properties with more details about the error.</p>
<code>message</code> properties with more details about the error. The error
will have a <code>format(SourceText[])</code> function, to which you pass an array
of objects that look like <code>{source: grammarSource, text: string}</code>; this
will return a nicely-formatted error suitable for human consumption.</p>

<pre><code>parser.parse("abba"); // returns ["a", "b", "b", "a"]

Expand Down
58 changes: 50 additions & 8 deletions lib/compiler/passes/generate-js.js
Original file line number Diff line number Diff line change
Expand Up @@ -788,19 +788,61 @@ function generateJS(ast, options) {
"}",
"",
"function peg$SyntaxError(message, expected, found, location) {",
" this.message = message;",
" this.expected = expected;",
" this.found = found;",
" this.location = location;",
" this.name = \"SyntaxError\";",
"",
" if (typeof Error.captureStackTrace === \"function\") {",
" Error.captureStackTrace(this, peg$SyntaxError);",
" var self = Error.call(this, message);",
" if (Object.setPrototypeOf) {",
" Object.setPrototypeOf(self, peg$SyntaxError.prototype);",
" }",
" self.expected = expected;",
" self.found = found;",
" self.location = location;",
" self.name = \"SyntaxError\";",
" return self;",
"}",
"",
"peg$subclass(peg$SyntaxError, Error);",
"",
"function peg$padEnd(str, targetLength, padString) {",
" padString = padString || \" \";",
" if (str.length > targetLength) { return str; }",
" targetLength -= str.length;",
" padString += padString.repeat(targetLength);",
" return str + padString.slice(0, targetLength);",
"}",
"",
"peg$SyntaxError.prototype.format = function(sources) {",
" var str = \"Error: \" + this.message;",
" if (this.location) {",
" var srcLines = [];",
" var src = null;",
" var k;",
" for (k = 0; k < sources.length; k++) {",
" if (sources[k].source === this.location.source) {",
" src = sources[k].text.split(/\\r\\n|\\n|\\r/g);",
" break;",
" }",
" }",
" var maxLine = this.location.start.line.toString().length;",
" if (!src) {",
" str += \"\\n at \" + this.location.source + \":\" + this.location.start.line + \":\"",
" + this.location.start.column;",
" } else {",
" var line = src[this.location.start.line - 1];",
" str += \"\\n --> \" + this.location.source + \":\" + this.location.start.line + \":\"",
" + this.location.start.column + \"\\n\" + peg$padEnd(\"\",maxLine)",
" + \" |\\n\" + peg$padEnd(this.location.start.line.toString(), maxLine)",
" + \" | \" + line + \"\\n\" + peg$padEnd(\"\", maxLine) + \" | \"",
" + peg$padEnd(\"\", this.location.start.column - 1);",
" if (this.location.start.line === this.location.end.line) {",
" str += peg$padEnd(\"\", this.location.end.column - this.location.start.column, \"^\");",
" }",
" else {",
" str += peg$padEnd(\"\", line.length - this.location.start.column + 1, \"^\");",
" }",
" }",
" }",
" return str;",
"};",
"",
"peg$SyntaxError.buildMessage = function(expected, found) {",
" var DESCRIBE_EXPECTATION_FNS = {",
" literal: function(expectation) {",
Expand Down
10 changes: 6 additions & 4 deletions lib/compiler/passes/report-duplicate-labels.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,12 @@ function reportDuplicateLabels(ast) {
const label = node.label;
if (label && Object.prototype.hasOwnProperty.call(env, label)) {
throw new GrammarError(
"Label \"" + node.label + "\" is already defined "
+ "at line " + env[label].start.line + ", "
+ "column " + env[label].start.column + ".",
node.location
`Label "${node.label}" is already defined`,
node.location,
[{
message: "Original label location",
location: env[label]
}]
);
}

Expand Down
10 changes: 6 additions & 4 deletions lib/compiler/passes/report-duplicate-rules.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,12 @@ function reportDuplicateRules(ast) {
rule(node) {
if (Object.prototype.hasOwnProperty.call(rules, node.name)) {
throw new GrammarError(
"Rule \"" + node.name + "\" is already defined "
+ "at line " + rules[node.name].start.line + ", "
+ "column " + rules[node.name].start.column + ".",
node.location
`Rule "${node.name}" is already defined`,
node.location,
[{
message: "Original rule location",
location: rules[node.name]
}]
);
}

Expand Down
12 changes: 7 additions & 5 deletions lib/compiler/passes/report-incorrect-plucking.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,17 +11,19 @@ const visitor = require("../visitor");
function reportIncorrectPlucking(ast) {
const check = visitor.build({
action(node) {
check(node.expression, true);
check(node.expression, node);
},

labeled(node, action) {
if (node.pick) {
if (action) {
throw new GrammarError(
"\"@\" cannot be used with an action block "
+ "at line " + node.location.start.line + ", "
+ "column " + node.location.start.column + ".",
node.location
"\"@\" cannot be used with an action block",
node.location,
[{
message: "Action block location",
location: action.codeLocation
}]
);
}
}
Expand Down
2 changes: 1 addition & 1 deletion lib/compiler/passes/report-infinite-recursion.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ function reportInfiniteRecursion(ast) {
throw new GrammarError(
"Possible infinite loop when parsing (left recursion: "
+ visitedRules.join(" -> ")
+ ").",
+ ")",
node.location
);
}
Expand Down
4 changes: 2 additions & 2 deletions lib/compiler/passes/report-infinite-repetition.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ function reportInfiniteRepetition(ast) {
zero_or_more(node) {
if (!asts.alwaysConsumesOnSuccess(ast, node.expression)) {
throw new GrammarError(
"Possible infinite loop when parsing (repetition used with an expression that may not consume any input).",
"Possible infinite loop when parsing (repetition used with an expression that may not consume any input)",
node.location
);
}
Expand All @@ -20,7 +20,7 @@ function reportInfiniteRepetition(ast) {
one_or_more(node) {
if (!asts.alwaysConsumesOnSuccess(ast, node.expression)) {
throw new GrammarError(
"Possible infinite loop when parsing (repetition used with an expression that may not consume any input).",
"Possible infinite loop when parsing (repetition used with an expression that may not consume any input)",
node.location
);
}
Expand Down
2 changes: 1 addition & 1 deletion lib/compiler/passes/report-undefined-rules.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ function reportUndefinedRules(ast) {
rule_ref(node) {
if (!asts.findRule(ast, node.name)) {
throw new GrammarError(
"Rule \"" + node.name + "\" is not defined.",
`Rule "${node.name}" is not defined`,
node.location
);
}
Expand Down
111 changes: 106 additions & 5 deletions lib/grammar-error.js
Original file line number Diff line number Diff line change
@@ -1,15 +1,116 @@
"use strict";

// Thrown when the grammar contains an error.
class GrammarError {
constructor(message, location) {
class GrammarError extends Error {
constructor(message, location, diagnostics) {
super(message);
this.name = "GrammarError";
this.message = message;
this.location = location;
if (diagnostics === undefined) {
diagnostics = [];
}
this.diagnostics = diagnostics;
}

if (typeof Error.captureStackTrace === "function") {
Error.captureStackTrace(this, GrammarError);
toString() {
let str = super.toString();
if (this.location) {
str += "\n at ";
if ((this.location.source !== undefined)
&& (this.location.source !== null)) {
str += `${this.location.source}:`;
}
str += `${this.location.start.line}:${this.location.start.column}`;
}
for (const diag of this.diagnostics) {
str += "\n from ";
if ((diag.location.source !== undefined)
&& (diag.location.source !== null)) {
str += `${diag.location.source}:`;
}
str += `${diag.location.start.line}:${diag.location.start.column}: ${diag.message}`;
}

return str;
}

/**
* @typedef SourceText {source: string, text: string}
*/
/**
* Format the error with associated sources. This only works if location.source
* is a string for all locations.
*
* @param {SourceText[]} sources mapping from location source to source text
* @returns {string} the formatted error
*/
format(sources) {
/* Sample output:
Error: Label "head" is already defined
--> examples/arithmetics.pegjs:15:17
|
15 | = head:Factor head:(_ ("*" / "/") _ Factor)* {
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
note: Original label location
--> examples/arithmetics.pegjs:15:5
|
15 | = head:Factor head:(_ ("*" / "/") _ Factor)* {
| ^^^^^^^^^^^
*/
const srcLines = sources.map(({ source, text }) => ({
source,
text: text.split(/\r\n|\n|\r/g)
}));

function entry(location, indent, message = "") {
let str = "";
const src = srcLines.find(({ source }) => source === location.source);
if (src) {
const line = src.text[location.start.line - 1];
if (message) {
str += `\nnote: ${message}`;
}
str += `
--> ${location.source}:${location.start.line}:${location.start.column}
${"".padEnd(indent)} |
${location.start.line.toString().padEnd(indent)} | ${line}
${"".padEnd(indent)} | ${"".padEnd(location.start.column - 1)}`;
if (location.start.line === location.end.line) {
str += "".padEnd(location.end.column - location.start.column, "^");
} else {
str += "".padEnd(line.length - location.start.column + 1, "^");
}
} else {
str += `\n at ${location.source}:${location.start.line}:${location.start.column}`;
if (message) {
str += `: ${message}`;
}
}

return str;
}

let maxLine = -Infinity;

let str = `Error: ${this.message}`;
if (this.location) {
maxLine = this.diagnostics.reduce(
(t, { location }) => Math.max(t, location.start.line),
this.location.start.line
);
str += entry(this.location, maxLine);
} else {
maxLine = Math.max.apply(
null,
this.diagnostics.map(d => d.location.start.line)
);
}
for (const diag of this.diagnostics) {
str += entry(diag.location, maxLine, diag.message);
}

return str;
}
}

Expand Down
Loading

0 comments on commit 635c939

Please sign in to comment.