blob: 3c9bc96b307c6d9aa92eba626007b6906eb752be [file] [log] [blame] [edit]
/**
* @param {string} text
*/
function lastLine(text) {
const splitted = text.split("\n");
return splitted[splitted.length - 1];
}
function appendIfExist(base, target) {
let result = base;
if (target) {
result += ` ${target}`;
}
return result;
}
function contextAsText(node) {
const hierarchy = [node];
while (node && node.parent) {
const { parent } = node;
hierarchy.unshift(parent);
node = parent;
}
return hierarchy.map((n) => appendIfExist(n.type, n.name)).join(" -> ");
}
/**
* @typedef {object} WebIDL2ErrorOptions
* @property {"error" | "warning"} [level]
* @property {Function} [autofix]
* @property {string} [ruleName]
*
* @typedef {ReturnType<typeof error>} WebIDLErrorData
*
* @param {string} message error message
* @param {*} position
* @param {*} current
* @param {*} message
* @param {"Syntax" | "Validation"} kind error type
* @param {WebIDL2ErrorOptions=} options
*/
function error(
source,
position,
current,
message,
kind,
{ level = "error", autofix, ruleName } = {}
) {
/**
* @param {number} count
*/
function sliceTokens(count) {
return count > 0
? source.slice(position, position + count)
: source.slice(Math.max(position + count, 0), position);
}
/**
* @param {import("./tokeniser.js").Token[]} inputs
* @param {object} [options]
* @param {boolean} [options.precedes]
* @returns
*/
function tokensToText(inputs, { precedes } = {}) {
const text = inputs.map((t) => t.trivia + t.value).join("");
const nextToken = source[position];
if (nextToken.type === "eof") {
return text;
}
if (precedes) {
return text + nextToken.trivia;
}
return text.slice(nextToken.trivia.length);
}
const maxTokens = 5; // arbitrary but works well enough
const line =
source[position].type !== "eof"
? source[position].line
: source.length > 1
? source[position - 1].line
: 1;
const precedingLastLine = lastLine(
tokensToText(sliceTokens(-maxTokens), { precedes: true })
);
const subsequentTokens = sliceTokens(maxTokens);
const subsequentText = tokensToText(subsequentTokens);
const subsequentFirstLine = subsequentText.split("\n")[0];
const spaced = " ".repeat(precedingLastLine.length) + "^";
const sourceContext = precedingLastLine + subsequentFirstLine + "\n" + spaced;
const contextType = kind === "Syntax" ? "since" : "inside";
const inSourceName = source.name ? ` in ${source.name}` : "";
const grammaticalContext =
current && current.name
? `, ${contextType} \`${current.partial ? "partial " : ""}${contextAsText(
current
)}\``
: "";
const context = `${kind} error at line ${line}${inSourceName}${grammaticalContext}:\n${sourceContext}`;
return {
message: `${context} ${message}`,
bareMessage: message,
context,
line,
sourceName: source.name,
level,
ruleName,
autofix,
input: subsequentText,
tokens: subsequentTokens,
};
}
/**
* @param {string} message error message
*/
export function syntaxError(source, position, current, message) {
return error(source, position, current, message, "Syntax");
}
/**
* @param {string} message error message
* @param {WebIDL2ErrorOptions} [options]
*/
export function validationError(
token,
current,
ruleName,
message,
options = {}
) {
options.ruleName = ruleName;
return error(
current.source,
token.index,
current,
message,
"Validation",
options
);
}