mirror of
https://github.com/DavidAnson/markdownlint.git
synced 2025-09-22 05:40:48 +02:00
Add support for asynchronous custom rules (ex: to read a file or make a network request).
This commit is contained in:
parent
5167f0e576
commit
2056d81682
5 changed files with 499 additions and 129 deletions
|
@ -19,9 +19,10 @@ const dynamicRequire = (typeof __non_webpack_require__ === "undefined") ? requir
|
|||
* Validate the list of rules for structure and reuse.
|
||||
*
|
||||
* @param {Rule[]} ruleList List of rules.
|
||||
* @param {boolean} synchronous Whether to execute synchronously.
|
||||
* @returns {string} Error message if validation fails.
|
||||
*/
|
||||
function validateRuleList(ruleList) {
|
||||
function validateRuleList(ruleList, synchronous) {
|
||||
let result = null;
|
||||
if (ruleList.length === rules.length) {
|
||||
// No need to validate if only using built-in rules
|
||||
|
@ -61,6 +62,19 @@ function validateRuleList(ruleList) {
|
|||
) {
|
||||
result = newError("information");
|
||||
}
|
||||
if (
|
||||
!result &&
|
||||
(rule.asynchronous !== undefined) &&
|
||||
(typeof rule.asynchronous !== "boolean")
|
||||
) {
|
||||
result = newError("asynchronous");
|
||||
}
|
||||
if (!result && rule.asynchronous && synchronous) {
|
||||
result = new Error(
|
||||
"Custom rule " + rule.names.join("/") + " at index " + customIndex +
|
||||
" is asynchronous and can not be used in a synchronous context."
|
||||
);
|
||||
}
|
||||
if (!result) {
|
||||
rule.names.forEach(function forName(name) {
|
||||
const nameUpper = name.toUpperCase();
|
||||
|
@ -509,12 +523,12 @@ function lintContent(
|
|||
}
|
||||
if (errorInfo.range &&
|
||||
(!Array.isArray(errorInfo.range) ||
|
||||
(errorInfo.range.length !== 2) ||
|
||||
!helpers.isNumber(errorInfo.range[0]) ||
|
||||
(errorInfo.range[0] < 1) ||
|
||||
!helpers.isNumber(errorInfo.range[1]) ||
|
||||
(errorInfo.range[1] < 1) ||
|
||||
((errorInfo.range[0] + errorInfo.range[1] - 1) >
|
||||
(errorInfo.range.length !== 2) ||
|
||||
!helpers.isNumber(errorInfo.range[0]) ||
|
||||
(errorInfo.range[0] < 1) ||
|
||||
!helpers.isNumber(errorInfo.range[1]) ||
|
||||
(errorInfo.range[1] < 1) ||
|
||||
((errorInfo.range[0] + errorInfo.range[1] - 1) >
|
||||
lines[errorInfo.lineNumber - 1].length))) {
|
||||
throwError("range");
|
||||
}
|
||||
|
@ -572,71 +586,105 @@ function lintContent(
|
|||
});
|
||||
}
|
||||
// Call (possibly external) rule function to report errors
|
||||
if (handleRuleFailures) {
|
||||
try {
|
||||
rule.function(params, onError);
|
||||
} catch (error) {
|
||||
const message = (error instanceof Error) ? error.message : error;
|
||||
onError({
|
||||
"lineNumber": 1,
|
||||
"detail": `This rule threw an exception: ${message}`
|
||||
});
|
||||
// eslint-disable-next-line func-style
|
||||
const catchCallsOnError = (error) => onError({
|
||||
"lineNumber": 1,
|
||||
"detail": `This rule threw an exception: ${error.message || error}`
|
||||
});
|
||||
// eslint-disable-next-line func-style
|
||||
const invokeRuleFunction = () => rule.function(params, onError);
|
||||
if (rule.asynchronous) {
|
||||
// Asynchronous rule, ensure it returns a Promise
|
||||
const ruleFunctionPromise =
|
||||
Promise.resolve().then(invokeRuleFunction);
|
||||
return handleRuleFailures ?
|
||||
ruleFunctionPromise.catch(catchCallsOnError) :
|
||||
ruleFunctionPromise;
|
||||
}
|
||||
// Synchronous rule
|
||||
try {
|
||||
invokeRuleFunction();
|
||||
} catch (error) {
|
||||
if (handleRuleFailures) {
|
||||
catchCallsOnError(error);
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
// eslint-disable-next-line jsdoc/require-jsdoc
|
||||
function formatResults() {
|
||||
// Sort results by rule name by line number
|
||||
results.sort((a, b) => (
|
||||
a.ruleName.localeCompare(b.ruleName) ||
|
||||
a.lineNumber - b.lineNumber
|
||||
));
|
||||
if (resultVersion < 3) {
|
||||
// Remove fixInfo and multiple errors for the same rule and line number
|
||||
const noPrevious = {
|
||||
"ruleName": null,
|
||||
"lineNumber": -1
|
||||
};
|
||||
results = results.filter((error, index, array) => {
|
||||
delete error.fixInfo;
|
||||
const previous = array[index - 1] || noPrevious;
|
||||
return (
|
||||
(error.ruleName !== previous.ruleName) ||
|
||||
(error.lineNumber !== previous.lineNumber)
|
||||
);
|
||||
});
|
||||
}
|
||||
if (resultVersion === 0) {
|
||||
// Return a dictionary of rule->[line numbers]
|
||||
const dictionary = {};
|
||||
for (const error of results) {
|
||||
const ruleLines = dictionary[error.ruleName] || [];
|
||||
ruleLines.push(error.lineNumber);
|
||||
dictionary[error.ruleName] = ruleLines;
|
||||
}
|
||||
// @ts-ignore
|
||||
results = dictionary;
|
||||
} else if (resultVersion === 1) {
|
||||
// Use ruleAlias instead of ruleNames
|
||||
for (const error of results) {
|
||||
error.ruleAlias = error.ruleNames[1] || error.ruleName;
|
||||
delete error.ruleNames;
|
||||
}
|
||||
} else {
|
||||
rule.function(params, onError);
|
||||
// resultVersion 2 or 3: Remove unwanted ruleName
|
||||
for (const error of results) {
|
||||
delete error.ruleName;
|
||||
}
|
||||
}
|
||||
return results;
|
||||
}
|
||||
// Run all rules
|
||||
const ruleListAsync = ruleList.filter((rule) => rule.asynchronous);
|
||||
const ruleListSync = ruleList.filter((rule) => !rule.asynchronous);
|
||||
const ruleListAsyncFirst = [
|
||||
...ruleListAsync,
|
||||
...ruleListSync
|
||||
];
|
||||
// eslint-disable-next-line func-style
|
||||
const callbackSuccess = () => callback(null, formatResults());
|
||||
// eslint-disable-next-line func-style
|
||||
const callbackError =
|
||||
(error) => callback(error instanceof Error ? error : new Error(error));
|
||||
try {
|
||||
ruleList.forEach(forRule);
|
||||
const ruleResults = ruleListAsyncFirst.map(forRule);
|
||||
if (ruleListAsync.length > 0) {
|
||||
Promise.all(ruleResults.slice(0, ruleListAsync.length))
|
||||
.then(callbackSuccess)
|
||||
.catch(callbackError);
|
||||
} else {
|
||||
callbackSuccess();
|
||||
}
|
||||
} catch (error) {
|
||||
callbackError(error);
|
||||
} finally {
|
||||
cache.clear();
|
||||
return callback((error instanceof Error) ? error : new Error(error));
|
||||
}
|
||||
cache.clear();
|
||||
// Sort results by rule name by line number
|
||||
results.sort((a, b) => (
|
||||
a.ruleName.localeCompare(b.ruleName) ||
|
||||
a.lineNumber - b.lineNumber
|
||||
));
|
||||
if (resultVersion < 3) {
|
||||
// Remove fixInfo and multiple errors for the same rule and line number
|
||||
const noPrevious = {
|
||||
"ruleName": null,
|
||||
"lineNumber": -1
|
||||
};
|
||||
results = results.filter((error, index, array) => {
|
||||
delete error.fixInfo;
|
||||
const previous = array[index - 1] || noPrevious;
|
||||
return (
|
||||
(error.ruleName !== previous.ruleName) ||
|
||||
(error.lineNumber !== previous.lineNumber)
|
||||
);
|
||||
});
|
||||
}
|
||||
if (resultVersion === 0) {
|
||||
// Return a dictionary of rule->[line numbers]
|
||||
const dictionary = {};
|
||||
for (const error of results) {
|
||||
const ruleLines = dictionary[error.ruleName] || [];
|
||||
ruleLines.push(error.lineNumber);
|
||||
dictionary[error.ruleName] = ruleLines;
|
||||
}
|
||||
// @ts-ignore
|
||||
results = dictionary;
|
||||
} else if (resultVersion === 1) {
|
||||
// Use ruleAlias instead of ruleNames
|
||||
for (const error of results) {
|
||||
error.ruleAlias = error.ruleNames[1] || error.ruleName;
|
||||
delete error.ruleNames;
|
||||
}
|
||||
} else {
|
||||
// resultVersion 2 or 3: Remove unwanted ruleName
|
||||
for (const error of results) {
|
||||
delete error.ruleName;
|
||||
}
|
||||
}
|
||||
return callback(null, results);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -697,7 +745,7 @@ function lintInput(options, synchronous, callback) {
|
|||
callback = callback || function noop() {};
|
||||
// eslint-disable-next-line unicorn/prefer-spread
|
||||
const ruleList = rules.concat(options.customRules || []);
|
||||
const ruleErr = validateRuleList(ruleList);
|
||||
const ruleErr = validateRuleList(ruleList, synchronous);
|
||||
if (ruleErr) {
|
||||
return callback(ruleErr);
|
||||
}
|
||||
|
@ -1147,6 +1195,7 @@ module.exports = markdownlint;
|
|||
* @property {string} description Rule description.
|
||||
* @property {URL} [information] Link to more information.
|
||||
* @property {string[]} tags Rule tag(s).
|
||||
* @property {boolean} [asynchronous] True if asynchronous.
|
||||
* @property {RuleFunction} function Rule implementation.
|
||||
*/
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue