"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;