@dev-build-deploy/diagnose-it
Version:
Expressive Diagnostics library
332 lines (331 loc) • 13.1 kB
JavaScript
;
/*
* SPDX-FileCopyrightText: 2023 Kevin de Jong <monkaii@hotmail.com>
* SPDX-License-Identifier: MIT
*/
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.DiagnosticsMessage = exports.DiagnosticsLevelEnum = void 0;
const chalk_1 = __importDefault(require("chalk"));
const fixitHint_1 = require("./fixitHint");
const diff_1 = require("./diff");
/**
* Diagnostics Message Level
* @enum {number}
*
* @example
* ```typescript
* const level = DiagnosticsLevelEnum.Error;
* console.log(level); // 4
* console.log(DiagnosticsLevelEnum[level]); // Error
* ```
*/
var DiagnosticsLevelEnum;
(function (DiagnosticsLevelEnum) {
DiagnosticsLevelEnum[DiagnosticsLevelEnum["Ignored"] = 0] = "Ignored";
DiagnosticsLevelEnum[DiagnosticsLevelEnum["Note"] = 1] = "Note";
DiagnosticsLevelEnum[DiagnosticsLevelEnum["Remark"] = 2] = "Remark";
DiagnosticsLevelEnum[DiagnosticsLevelEnum["Warning"] = 3] = "Warning";
DiagnosticsLevelEnum[DiagnosticsLevelEnum["Error"] = 4] = "Error";
DiagnosticsLevelEnum[DiagnosticsLevelEnum["Fatal"] = 5] = "Fatal";
})(DiagnosticsLevelEnum || (exports.DiagnosticsLevelEnum = DiagnosticsLevelEnum = {}));
/**
* Diagnostics Message Level Configuration
* @type {DiagnosticsLevelType}
* @property {string} text Textual representation of the level
* @property {chalk.Chalk} color Chalk color for the level
*
* @example
* ```typescript
* const level = DiagnosticsLevelConfiguration[DiagnosticsLevelEnum.Error];
* console.log(level.text); // error
* console.log(level.color("Hello")); // Hello in red
* ```
*/
const DiagnosticsLevelConfiguration = {
[DiagnosticsLevelEnum.Ignored]: {
text: "ignored",
color: chalk_1.default.bold.gray,
},
[DiagnosticsLevelEnum.Note]: {
text: "note",
color: chalk_1.default.bold.blue,
},
[DiagnosticsLevelEnum.Remark]: {
text: "remark",
color: chalk_1.default.bold.blue,
},
[DiagnosticsLevelEnum.Warning]: {
text: "warning",
color: chalk_1.default.bold.yellow,
},
[DiagnosticsLevelEnum.Error]: {
text: "error",
color: chalk_1.default.bold.red,
},
[DiagnosticsLevelEnum.Fatal]: {
text: "fatal",
color: chalk_1.default.bold.red,
},
};
/**
* Diagnostics Message
* @static createError Creates an error message
* @static createWarning Creates a warning message
* @function setFile Sets the file name
* @function setLevel Sets the message level
* @function setMessage Sets the message
* @function setContext Sets the context
* @function addFixitHint Adds a FixIt hint
* @function toString Returns the message as a string
*
* @example Using the constructor
* ```typescript
* const message = new DiagnosticsMessage({
* file: "test.ts",
* level: DiagnosticsLevelEnum.Error,
* message: {
* text: "Unknown property `foo`",
* linenumber: 3,
* column: 10
* }
* });
* ```
*
* @example Using the static methods
* ```typescript
* const message = DiagnosticsMessage.createError("test.ts", {
* text: "Unknown property `foo`",
* linenumber: 3,
* column: 10
* });
* ```
*
* @example Creating a message with a context and FixIt Hints
* ```typescript
* const message = DiagnosticsMessage.createError("test.ts", {
* text: "Unknown property `foo`",
* linenumber: 3,
* column: 1
* })
* .setContext(2, ["monkey: banana", "foo: foo", "elephant: trunk"])
* .addFixitHint(FixItHint.createInsertion(1, "bar"));
* ```
*/
class DiagnosticsMessage {
constructor(data) {
var _a;
this.fixitHints = new Array();
this.setFile(data.file);
this.setLevel((_a = data.level) !== null && _a !== void 0 ? _a : DiagnosticsLevelEnum.Note);
this.setMessage(data.message);
}
/**
* Creates a Diagnostics message with the level `Error`
* @param file File path
* @param message Error message
* @returns Diagnostics Message
*/
static createError(file, message) {
return new DiagnosticsMessage({ file, level: DiagnosticsLevelEnum.Error, message });
}
/**
* Creates a Diagnostics message with the level `Warning`
* @param file File path
* @param message Warning message
* @returns Diagnostics Message
*/
static createWarning(file, message) {
return new DiagnosticsMessage({ file, level: DiagnosticsLevelEnum.Warning, message });
}
/**
* Sets the path towards the file associated with the Diagnostics Message.
*
* Notes:
* - The file path cannot contain newlines.
* - All special characters in filenames ( $ # & * ? ; | < > ( ) { } [ ] ' " ` ~ ! \ ) will be escaped.
* @param file File path
* @returns this
*/
setFile(file) {
if (/\r?\n/.test(file))
throw new Error("File name cannot contain newlines.");
// Escape all special characters (which are not yet escaped with the prefix `\`) in file names
file = file.replace(/\\([$#&*?;|<>(){}[\]'"`~!\\ ])/gi, "$1");
file = file.replace(/[$#&*?;|<>(){}[\]'"`~!\\ ]/g, "\\$&");
this.file = file;
return this;
}
/**
* Sets the level of the Diagnostics Message (i.e. `Error`, `Warning`, ...)
* @param level Diagnostics Level
* @returns this
*/
setLevel(level) {
this.level = level;
return this;
}
/**
* Sets the message of the Diagnostics Message.
*
* Notes:
* * The message cannot contain newlines.
* * The column number must be greater than 0.
* * The line number must be greater than 0.
*
* @param message Diagnostics Message
* @returns this
*/
setMessage(message) {
var _a, _b;
if (/\r?\n/.test(message.text))
throw new Error("Message cannot contain newlines.");
if (((_a = message.column) !== null && _a !== void 0 ? _a : 1) <= 0)
throw new RangeError("Column number must be greater than 0.");
if (((_b = message.linenumber) !== null && _b !== void 0 ? _b : 1) <= 0)
throw new RangeError("Line number must be greater than 0.");
this.message = message;
return this;
}
/**
* Sets the context of the Diagnostics Message.
*
* Notes:
* * The context can be provided as an array of strings or a single string.
* Any single string with newlines will be split into multiple lines.
* * The starting line number must be greater than 0.
* * The line number associated with the message must be within the range of the context.
*
* @param linenumber Line number for the first line provided by `context`
* @param lines Context lines
* @returns this
*/
setContext(linenumber, lines) {
var _a, _b;
if (((_a = this.message.linenumber) !== null && _a !== void 0 ? _a : 1) < linenumber || ((_b = this.message.linenumber) !== null && _b !== void 0 ? _b : 1) > linenumber + lines.length - 1) {
throw new RangeError("The line numbers provided by the context do not match the line number associated with the message.");
}
if (linenumber <= 0)
throw new RangeError("Line number must be greater than 0.");
const contextLines = Array.isArray(lines) ? lines : lines.split(/\r?\n/);
this.context = { linenumber, lines: contextLines };
return this;
}
/**
* Adds a FixIt Hint to the Diagnostics Message.
*
* Notes:
* * You must first provide a context before adding a FixIt Hint.
* * The FixIt Hint cannot overlap with an existing FixIt Hint.
*
* @param hint FixIt Hint
* @returns this
*
* @example Adding a FixIt Hint
* ```typescript
* const message = DiagnosticsMessage.createError("test.ts", {
* text: "Unknown property `foo`",
* linenumber: 3,
* column: 1
* })
* .setContext(2, ["monkey: banana", "foo: foo", "elephant: trunk"])
* .addFixitHint(FixItHint.createInsertion(1, "bar"));
* ```
*/
addFixitHint(hint) {
if (this.context === undefined)
throw new Error("Cannot add FixIt hint without a context.");
// Check if the new hint does not overlap with the ranges in the current fixit hints
if (this.fixitHints.some(fixit => fixit.range.index <= hint.range.index && hint.range.index <= fixit.range.index + fixit.range.length)) {
throw new Error("Cannot add FixIt hint that overlaps with an existing FixIt hint.");
}
// Add the new hint
this.fixitHints.push(hint);
// Sort the FixIt hints by index
this.fixitHints = this.fixitHints.sort((a, b) => a.range.index - b.range.index);
return this;
}
/**
* Returns the FixIt Hints associated with the Diagnostics Message.
* @returns Array of FixIt Hints
*/
getFixitHints() {
return this.fixitHints;
}
getFixitHintsString(prefix) {
var _a, _b;
const elements = new Array();
if (this.message.column === undefined)
return elements;
let caretLine = "";
let hintLine = "";
// Determine the maximum width of the first line based on the FixIt Hints and the message
const lastFixitHint = this.fixitHints.length > 0 ? this.fixitHints[this.fixitHints.length - 1] : undefined;
const maxWidth = lastFixitHint !== undefined
? lastFixitHint.range.index + Math.max(lastFixitHint.range.length, (_b = (_a = lastFixitHint.text) === null || _a === void 0 ? void 0 : _a.length) !== null && _b !== void 0 ? _b : 0)
: this.message.text.length;
// Append each character (either caret or fixit-hint) line by line
for (let i = 1; i <= maxWidth; i++) {
const fixit = this.fixitHints.find(fixit => i >= fixit.range.index && i < fixit.range.index + fixit.range.length);
const fixitHint = this.fixitHints.find(hint => { var _a, _b; return i >= hint.range.index && i < hint.range.index + ((_b = (_a = hint.text) === null || _a === void 0 ? void 0 : _a.length) !== null && _b !== void 0 ? _b : 0); });
const char = this.message.column === i && (fixit === null || fixit === void 0 ? void 0 : fixit.modification) !== "DEFAULT" ? "^" : fixit !== undefined ? "~" : " ";
const color = fixit !== undefined ? fixitHint_1.ModificationColorCodes[fixit.modification] : chalk_1.default.bold.green;
caretLine += char !== " " ? color(char) : char;
if (fixitHint !== undefined && fixitHint.text !== undefined) {
hintLine += fixitHint.text.charAt(i - fixitHint.range.index);
}
else {
hintLine += " ";
}
}
return hintLine.trimEnd().length === 0
? [prefix + caretLine.trimEnd()]
: [prefix + caretLine.trimEnd(), prefix + hintLine.trimEnd()];
}
getContextString() {
const result = new Array();
if (this.context === undefined) {
return result;
}
result.push("");
const maxWidth = (this.context.linenumber + this.context.lines.length).toString().length;
const spacing = " ".repeat(2);
const seperator = " | ";
for (let i = 0; i < this.context.lines.length; i++) {
const isContextLine = this.context.linenumber + i === this.message.linenumber;
const lineColor = isContextLine ? chalk_1.default.bold.whiteBright : chalk_1.default.bold.gray;
const line = this.context.lines[i];
const linenumber = (this.context.linenumber + i).toString().padStart(maxWidth, " ");
result.push(lineColor(`${spacing}${linenumber}${seperator}${line}`));
if (this.context.linenumber + i === this.message.linenumber) {
result.push(...this.getFixitHintsString(`${spacing}${" ".repeat(maxWidth)}${seperator}`));
}
}
return result;
}
/**
* Generates an Expressive Diagnostics message in formatted output.
* @returns Formatted message
*/
toString() {
var _a, _b;
const level = DiagnosticsLevelConfiguration[this.level];
const message = [
chalk_1.default.bold(`${this.file}:${(_a = this.message.linenumber) !== null && _a !== void 0 ? _a : 1}:${(_b = this.message.column) !== null && _b !== void 0 ? _b : 1}: `) +
level.color(`${level.text}: `) +
chalk_1.default.bold(this.message.text),
...this.getContextString(),
];
return message.join("\n");
}
/**
* Applies the FixIt hints to the context and returns the result as a string.
* @returns Fixed string
*/
applyFixitHints() {
return (0, diff_1.applyPatch)((0, diff_1.createPatch)(this));
}
}
exports.DiagnosticsMessage = DiagnosticsMessage;