176 lines
5.6 KiB
JavaScript
176 lines
5.6 KiB
JavaScript
"use strict";
|
|
|
|
const GrammarLocation = require("./grammar-location");
|
|
|
|
// See: https://github.com/Microsoft/TypeScript-wiki/blob/master/Breaking-Changes.md#extending-built-ins-like-error-array-and-map-may-no-longer-work
|
|
// This is roughly what typescript generates, it's not called after super(), where it's needed.
|
|
// istanbul ignore next This is a special black magic that cannot be covered everywhere
|
|
const setProtoOf = Object.setPrototypeOf
|
|
|| ({ __proto__: [] } instanceof Array
|
|
&& function(d, b) {
|
|
// eslint-disable-next-line no-proto -- Backward-compatibility
|
|
d.__proto__ = b;
|
|
})
|
|
|| function(d, b) {
|
|
for (const p in b) {
|
|
if (Object.prototype.hasOwnProperty.call(b, p)) {
|
|
d[p] = b[p];
|
|
}
|
|
}
|
|
};
|
|
|
|
// Thrown when the grammar contains an error.
|
|
/** @type {import("./peg").GrammarError} */
|
|
class GrammarError extends Error {
|
|
constructor(message, location, diagnostics) {
|
|
super(message);
|
|
setProtoOf(this, GrammarError.prototype);
|
|
this.name = "GrammarError";
|
|
this.location = location;
|
|
if (diagnostics === undefined) {
|
|
diagnostics = [];
|
|
}
|
|
this.diagnostics = diagnostics;
|
|
// All problems if this error is thrown by the plugin and not at stage
|
|
// checking phase
|
|
this.stage = null;
|
|
this.problems = [["error", message, location, diagnostics]];
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
/**
|
|
* Format the error with associated sources. The `location.source` should have
|
|
* a `toString()` representation in order the result to look nice. If source
|
|
* is `null` or `undefined`, it is skipped from the output
|
|
*
|
|
* 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)* {
|
|
* | ^^^^
|
|
* ```
|
|
*
|
|
* @param {import("./peg").SourceText[]} sources mapping from location source to source text
|
|
*
|
|
* @returns {string} the formatted error
|
|
*/
|
|
format(sources) {
|
|
const srcLines = sources.map(({ source, text }) => ({
|
|
source,
|
|
text: (text !== null && text !== undefined)
|
|
? String(text).split(/\r\n|\n|\r/g)
|
|
: [],
|
|
}));
|
|
|
|
/**
|
|
* Returns a highlighted piece of source to which the `location` points
|
|
*
|
|
* @param {import("./peg").LocationRange} location
|
|
* @param {number} indent How much width in symbols line number strip should have
|
|
* @param {string} message Additional message that will be shown after location
|
|
* @returns {string}
|
|
*/
|
|
function entry(location, indent, message = "") {
|
|
let str = "";
|
|
const src = srcLines.find(({ source }) => source === location.source);
|
|
const s = location.start;
|
|
const offset_s = GrammarLocation.offsetStart(location);
|
|
if (src) {
|
|
const e = location.end;
|
|
const line = src.text[s.line - 1];
|
|
const last = s.line === e.line ? e.column : line.length + 1;
|
|
const hatLen = (last - s.column) || 1;
|
|
if (message) {
|
|
str += `\nnote: ${message}`;
|
|
}
|
|
str += `
|
|
--> ${location.source}:${offset_s.line}:${offset_s.column}
|
|
${"".padEnd(indent)} |
|
|
${offset_s.line.toString().padStart(indent)} | ${line}
|
|
${"".padEnd(indent)} | ${"".padEnd(s.column - 1)}${"".padEnd(hatLen, "^")}`;
|
|
} else {
|
|
str += `\n at ${location.source}:${offset_s.line}:${offset_s.column}`;
|
|
if (message) {
|
|
str += `: ${message}`;
|
|
}
|
|
}
|
|
|
|
return str;
|
|
}
|
|
|
|
/**
|
|
* Returns a formatted representation of the one problem in the error.
|
|
*
|
|
* @param {import("./peg").Severity} severity Importance of the message
|
|
* @param {string} message Test message of the problem
|
|
* @param {import("./peg").LocationRange?} location Location of the problem in the source
|
|
* @param {import("./peg").DiagnosticNote[]} diagnostics Additional notes about the problem
|
|
* @returns {string}
|
|
*/
|
|
function formatProblem(severity, message, location, diagnostics = []) {
|
|
// Calculate maximum width of all lines
|
|
let maxLine = -Infinity;
|
|
if (location) {
|
|
maxLine = diagnostics.reduce(
|
|
(t, { location }) => Math.max(
|
|
t, GrammarLocation.offsetStart(location).line
|
|
),
|
|
location.start.line
|
|
);
|
|
} else {
|
|
maxLine = Math.max.apply(
|
|
null,
|
|
diagnostics.map(d => d.location.start.line)
|
|
);
|
|
}
|
|
maxLine = maxLine.toString().length;
|
|
|
|
let str = `${severity}: ${message}`;
|
|
if (location) {
|
|
str += entry(location, maxLine);
|
|
}
|
|
for (const diag of diagnostics) {
|
|
str += entry(diag.location, maxLine, diag.message);
|
|
}
|
|
|
|
return str;
|
|
}
|
|
|
|
// "info" problems are only appropriate if in verbose mode.
|
|
// Handle them separately.
|
|
return this.problems
|
|
.filter(p => p[0] !== "info")
|
|
.map(p => formatProblem(...p)).join("\n\n");
|
|
}
|
|
}
|
|
|
|
module.exports = GrammarError;
|