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
|
@ -933,9 +933,10 @@ var dynamicRequire = (typeof require === "undefined") ? __webpack_require__("../
|
||||||
* Validate the list of rules for structure and reuse.
|
* Validate the list of rules for structure and reuse.
|
||||||
*
|
*
|
||||||
* @param {Rule[]} ruleList List of rules.
|
* @param {Rule[]} ruleList List of rules.
|
||||||
|
* @param {boolean} synchronous Whether to execute synchronously.
|
||||||
* @returns {string} Error message if validation fails.
|
* @returns {string} Error message if validation fails.
|
||||||
*/
|
*/
|
||||||
function validateRuleList(ruleList) {
|
function validateRuleList(ruleList, synchronous) {
|
||||||
var result = null;
|
var result = null;
|
||||||
if (ruleList.length === rules.length) {
|
if (ruleList.length === rules.length) {
|
||||||
// No need to validate if only using built-in rules
|
// No need to validate if only using built-in rules
|
||||||
|
@ -972,6 +973,15 @@ function validateRuleList(ruleList) {
|
||||||
(Object.getPrototypeOf(rule.information) !== URL.prototype)) {
|
(Object.getPrototypeOf(rule.information) !== URL.prototype)) {
|
||||||
result = newError("information");
|
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) {
|
if (!result) {
|
||||||
rule.names.forEach(function forName(name) {
|
rule.names.forEach(function forName(name) {
|
||||||
var nameUpper = name.toUpperCase();
|
var nameUpper = name.toUpperCase();
|
||||||
|
@ -1441,75 +1451,106 @@ function lintContent(ruleList, name, content, md, config, frontMatter, handleRul
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
// Call (possibly external) rule function to report errors
|
// Call (possibly external) rule function to report errors
|
||||||
if (handleRuleFailures) {
|
// eslint-disable-next-line func-style
|
||||||
try {
|
var catchCallsOnError = function (error) { return onError({
|
||||||
rule.function(params, onError);
|
"lineNumber": 1,
|
||||||
|
"detail": "This rule threw an exception: " + (error.message || error)
|
||||||
|
}); };
|
||||||
|
// eslint-disable-next-line func-style
|
||||||
|
var invokeRuleFunction = function () { return rule.function(params, onError); };
|
||||||
|
if (rule.asynchronous) {
|
||||||
|
// Asynchronous rule, ensure it returns a Promise
|
||||||
|
var ruleFunctionPromise = Promise.resolve().then(invokeRuleFunction);
|
||||||
|
return handleRuleFailures ?
|
||||||
|
ruleFunctionPromise.catch(catchCallsOnError) :
|
||||||
|
ruleFunctionPromise;
|
||||||
|
}
|
||||||
|
// Synchronous rule
|
||||||
|
try {
|
||||||
|
invokeRuleFunction();
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
if (handleRuleFailures) {
|
||||||
|
catchCallsOnError(error);
|
||||||
}
|
}
|
||||||
catch (error) {
|
else {
|
||||||
var message = (error instanceof Error) ? error.message : error;
|
throw error;
|
||||||
onError({
|
}
|
||||||
"lineNumber": 1,
|
}
|
||||||
"detail": "This rule threw an exception: " + message
|
return null;
|
||||||
});
|
}
|
||||||
|
// eslint-disable-next-line jsdoc/require-jsdoc
|
||||||
|
function formatResults() {
|
||||||
|
// Sort results by rule name by line number
|
||||||
|
results.sort(function (a, b) { return (a.ruleName.localeCompare(b.ruleName) ||
|
||||||
|
a.lineNumber - b.lineNumber); });
|
||||||
|
if (resultVersion < 3) {
|
||||||
|
// Remove fixInfo and multiple errors for the same rule and line number
|
||||||
|
var noPrevious_1 = {
|
||||||
|
"ruleName": null,
|
||||||
|
"lineNumber": -1
|
||||||
|
};
|
||||||
|
results = results.filter(function (error, index, array) {
|
||||||
|
delete error.fixInfo;
|
||||||
|
var previous = array[index - 1] || noPrevious_1;
|
||||||
|
return ((error.ruleName !== previous.ruleName) ||
|
||||||
|
(error.lineNumber !== previous.lineNumber));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (resultVersion === 0) {
|
||||||
|
// Return a dictionary of rule->[line numbers]
|
||||||
|
var dictionary = {};
|
||||||
|
for (var _i = 0, results_1 = results; _i < results_1.length; _i++) {
|
||||||
|
var error = results_1[_i];
|
||||||
|
var 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 (var _a = 0, results_2 = results; _a < results_2.length; _a++) {
|
||||||
|
var error = results_2[_a];
|
||||||
|
error.ruleAlias = error.ruleNames[1] || error.ruleName;
|
||||||
|
delete error.ruleNames;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
rule.function(params, onError);
|
// resultVersion 2 or 3: Remove unwanted ruleName
|
||||||
|
for (var _b = 0, results_3 = results; _b < results_3.length; _b++) {
|
||||||
|
var error = results_3[_b];
|
||||||
|
delete error.ruleName;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
return results;
|
||||||
}
|
}
|
||||||
// Run all rules
|
// Run all rules
|
||||||
|
var ruleListAsync = ruleList.filter(function (rule) { return rule.asynchronous; });
|
||||||
|
var ruleListSync = ruleList.filter(function (rule) { return !rule.asynchronous; });
|
||||||
|
var ruleListAsyncFirst = __spreadArray(__spreadArray([], ruleListAsync), ruleListSync);
|
||||||
|
// eslint-disable-next-line func-style
|
||||||
|
var callbackSuccess = function () { return callback(null, formatResults()); };
|
||||||
|
// eslint-disable-next-line func-style
|
||||||
|
var callbackError = function (error) { return callback(error instanceof Error ? error : new Error(error)); };
|
||||||
try {
|
try {
|
||||||
ruleList.forEach(forRule);
|
var ruleResults = ruleListAsyncFirst.map(forRule);
|
||||||
|
if (ruleListAsync.length > 0) {
|
||||||
|
Promise.all(ruleResults.slice(0, ruleListAsync.length))
|
||||||
|
.then(callbackSuccess)
|
||||||
|
.catch(callbackError);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
callbackSuccess();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
catch (error) {
|
catch (error) {
|
||||||
|
callbackError(error);
|
||||||
|
}
|
||||||
|
finally {
|
||||||
cache.clear();
|
cache.clear();
|
||||||
return callback((error instanceof Error) ? error : new Error(error));
|
|
||||||
}
|
}
|
||||||
cache.clear();
|
|
||||||
// Sort results by rule name by line number
|
|
||||||
results.sort(function (a, b) { return (a.ruleName.localeCompare(b.ruleName) ||
|
|
||||||
a.lineNumber - b.lineNumber); });
|
|
||||||
if (resultVersion < 3) {
|
|
||||||
// Remove fixInfo and multiple errors for the same rule and line number
|
|
||||||
var noPrevious_1 = {
|
|
||||||
"ruleName": null,
|
|
||||||
"lineNumber": -1
|
|
||||||
};
|
|
||||||
results = results.filter(function (error, index, array) {
|
|
||||||
delete error.fixInfo;
|
|
||||||
var previous = array[index - 1] || noPrevious_1;
|
|
||||||
return ((error.ruleName !== previous.ruleName) ||
|
|
||||||
(error.lineNumber !== previous.lineNumber));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (resultVersion === 0) {
|
|
||||||
// Return a dictionary of rule->[line numbers]
|
|
||||||
var dictionary = {};
|
|
||||||
for (var _i = 0, results_1 = results; _i < results_1.length; _i++) {
|
|
||||||
var error = results_1[_i];
|
|
||||||
var 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 (var _b = 0, results_2 = results; _b < results_2.length; _b++) {
|
|
||||||
var error = results_2[_b];
|
|
||||||
error.ruleAlias = error.ruleNames[1] || error.ruleName;
|
|
||||||
delete error.ruleNames;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
// resultVersion 2 or 3: Remove unwanted ruleName
|
|
||||||
for (var _c = 0, results_3 = results; _c < results_3.length; _c++) {
|
|
||||||
var error = results_3[_c];
|
|
||||||
delete error.ruleName;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return callback(null, results);
|
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* Lints a file containing Markdown content.
|
* Lints a file containing Markdown content.
|
||||||
|
@ -1557,7 +1598,7 @@ function lintInput(options, synchronous, callback) {
|
||||||
callback = callback || function noop() { };
|
callback = callback || function noop() { };
|
||||||
// eslint-disable-next-line unicorn/prefer-spread
|
// eslint-disable-next-line unicorn/prefer-spread
|
||||||
var ruleList = rules.concat(options.customRules || []);
|
var ruleList = rules.concat(options.customRules || []);
|
||||||
var ruleErr = validateRuleList(ruleList);
|
var ruleErr = validateRuleList(ruleList, synchronous);
|
||||||
if (ruleErr) {
|
if (ruleErr) {
|
||||||
return callback(ruleErr);
|
return callback(ruleErr);
|
||||||
}
|
}
|
||||||
|
|
|
@ -41,7 +41,8 @@ A rule is implemented as an `Object` with one optional and four required propert
|
||||||
- `description` is a required `String` value that describes the rule in output messages.
|
- `description` is a required `String` value that describes the rule in output messages.
|
||||||
- `information` is an optional (absolute) `URL` of a link to more information about the rule.
|
- `information` is an optional (absolute) `URL` of a link to more information about the rule.
|
||||||
- `tags` is a required `Array` of `String` values that groups related rules for easier customization.
|
- `tags` is a required `Array` of `String` values that groups related rules for easier customization.
|
||||||
- `function` is a required synchronous `Function` that implements the rule and is passed two parameters:
|
- `asynchronous` is an optional `Boolean` value that indicates whether the rule returns a `Promise` and runs asynchronously.
|
||||||
|
- `function` is a required `Function` that implements the rule and is passed two parameters:
|
||||||
- `params` is an `Object` with properties that describe the content being analyzed:
|
- `params` is an `Object` with properties that describe the content being analyzed:
|
||||||
- `name` is a `String` that identifies the input file/string.
|
- `name` is a `String` that identifies the input file/string.
|
||||||
- `tokens` is an `Array` of [`markdown-it` `Token` objects](https://markdown-it.github.io/markdown-it/#Token)
|
- `tokens` is an `Array` of [`markdown-it` `Token` objects](https://markdown-it.github.io/markdown-it/#Token)
|
||||||
|
@ -66,15 +67,25 @@ A rule is implemented as an `Object` with one optional and four required propert
|
||||||
The collection of helper functions shared by the built-in rules is available for use by custom rules in the
|
The collection of helper functions shared by the built-in rules is available for use by custom rules in the
|
||||||
[markdownlint-rule-helpers package](https://www.npmjs.com/package/markdownlint-rule-helpers).
|
[markdownlint-rule-helpers package](https://www.npmjs.com/package/markdownlint-rule-helpers).
|
||||||
|
|
||||||
|
### Asynchronous Rules
|
||||||
|
|
||||||
|
If a rule needs to perform asynchronous operations (such as fetching a network resource), it can specify the value `true` for its `asynchronous` property.
|
||||||
|
Asynchronous rules should return a `Promise` from their `function` implementation that is resolved when the rule completes.
|
||||||
|
(The value passed to `resolve(...)` is ignored.)
|
||||||
|
Linting violations from asynchronous rules are reported via the `onError` function just like for synchronous rules.
|
||||||
|
|
||||||
|
**Note**: Asynchronous rules cannot be referenced in a synchronous calling context (i.e., `markdownlint.sync(...)`).
|
||||||
|
Attempting to do so throws an exception.
|
||||||
|
|
||||||
## Examples
|
## Examples
|
||||||
|
|
||||||
- [Simple rules used by the project's test cases](../test/rules)
|
- [Simple rules used by the project's test cases](../test/rules)
|
||||||
- [Code for all `markdownlint` built-in rules](../lib)
|
- [Code for all `markdownlint` built-in rules](../lib)
|
||||||
- [Package configuration for publishing to npm](../test/rules/npm)
|
- [Package configuration for publishing to npm](../test/rules/npm)
|
||||||
- Packages should export a single rule object or an `Array` of rule objects
|
- Packages should export a single rule object or an `Array` of rule objects
|
||||||
- [Custom rules from the Microsoft/vscode-docs-authoring repository](https://github.com/microsoft/vscode-docs-authoring/tree/master/packages/docs-linting/markdownlint-custom-rules)
|
- [Custom rules from the Microsoft/vscode-docs-authoring repository](https://github.com/microsoft/vscode-docs-authoring/tree/main/packages/docs-linting/markdownlint-custom-rules)
|
||||||
- [Custom rules from the axibase/docs-util repository](https://github.com/axibase/docs-util/tree/master/linting-rules)
|
- [Custom rules from the axibase/docs-util repository](https://github.com/axibase/docs-util/tree/master/linting-rules)
|
||||||
- [Custom rules from the webhintio/hint repository](https://github.com/webhintio/hint/blob/master/scripts/lint-markdown.js)
|
- [Custom rules from the webhintio/hint repository](https://github.com/webhintio/hint/blob/main/scripts/lint-markdown.js)
|
||||||
|
|
||||||
## References
|
## References
|
||||||
|
|
||||||
|
|
4
lib/markdownlint.d.ts
vendored
4
lib/markdownlint.d.ts
vendored
|
@ -263,6 +263,10 @@ type Rule = {
|
||||||
* Rule tag(s).
|
* Rule tag(s).
|
||||||
*/
|
*/
|
||||||
tags: string[];
|
tags: string[];
|
||||||
|
/**
|
||||||
|
* True if asynchronous.
|
||||||
|
*/
|
||||||
|
asynchronous?: boolean;
|
||||||
/**
|
/**
|
||||||
* Rule implementation.
|
* Rule implementation.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -19,9 +19,10 @@ const dynamicRequire = (typeof __non_webpack_require__ === "undefined") ? requir
|
||||||
* Validate the list of rules for structure and reuse.
|
* Validate the list of rules for structure and reuse.
|
||||||
*
|
*
|
||||||
* @param {Rule[]} ruleList List of rules.
|
* @param {Rule[]} ruleList List of rules.
|
||||||
|
* @param {boolean} synchronous Whether to execute synchronously.
|
||||||
* @returns {string} Error message if validation fails.
|
* @returns {string} Error message if validation fails.
|
||||||
*/
|
*/
|
||||||
function validateRuleList(ruleList) {
|
function validateRuleList(ruleList, synchronous) {
|
||||||
let result = null;
|
let result = null;
|
||||||
if (ruleList.length === rules.length) {
|
if (ruleList.length === rules.length) {
|
||||||
// No need to validate if only using built-in rules
|
// No need to validate if only using built-in rules
|
||||||
|
@ -61,6 +62,19 @@ function validateRuleList(ruleList) {
|
||||||
) {
|
) {
|
||||||
result = newError("information");
|
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) {
|
if (!result) {
|
||||||
rule.names.forEach(function forName(name) {
|
rule.names.forEach(function forName(name) {
|
||||||
const nameUpper = name.toUpperCase();
|
const nameUpper = name.toUpperCase();
|
||||||
|
@ -509,12 +523,12 @@ function lintContent(
|
||||||
}
|
}
|
||||||
if (errorInfo.range &&
|
if (errorInfo.range &&
|
||||||
(!Array.isArray(errorInfo.range) ||
|
(!Array.isArray(errorInfo.range) ||
|
||||||
(errorInfo.range.length !== 2) ||
|
(errorInfo.range.length !== 2) ||
|
||||||
!helpers.isNumber(errorInfo.range[0]) ||
|
!helpers.isNumber(errorInfo.range[0]) ||
|
||||||
(errorInfo.range[0] < 1) ||
|
(errorInfo.range[0] < 1) ||
|
||||||
!helpers.isNumber(errorInfo.range[1]) ||
|
!helpers.isNumber(errorInfo.range[1]) ||
|
||||||
(errorInfo.range[1] < 1) ||
|
(errorInfo.range[1] < 1) ||
|
||||||
((errorInfo.range[0] + errorInfo.range[1] - 1) >
|
((errorInfo.range[0] + errorInfo.range[1] - 1) >
|
||||||
lines[errorInfo.lineNumber - 1].length))) {
|
lines[errorInfo.lineNumber - 1].length))) {
|
||||||
throwError("range");
|
throwError("range");
|
||||||
}
|
}
|
||||||
|
@ -572,71 +586,105 @@ function lintContent(
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
// Call (possibly external) rule function to report errors
|
// Call (possibly external) rule function to report errors
|
||||||
if (handleRuleFailures) {
|
// eslint-disable-next-line func-style
|
||||||
try {
|
const catchCallsOnError = (error) => onError({
|
||||||
rule.function(params, onError);
|
"lineNumber": 1,
|
||||||
} catch (error) {
|
"detail": `This rule threw an exception: ${error.message || error}`
|
||||||
const message = (error instanceof Error) ? error.message : error;
|
});
|
||||||
onError({
|
// eslint-disable-next-line func-style
|
||||||
"lineNumber": 1,
|
const invokeRuleFunction = () => rule.function(params, onError);
|
||||||
"detail": `This rule threw an exception: ${message}`
|
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 {
|
} 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
|
// 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 {
|
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) {
|
} catch (error) {
|
||||||
|
callbackError(error);
|
||||||
|
} finally {
|
||||||
cache.clear();
|
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() {};
|
callback = callback || function noop() {};
|
||||||
// eslint-disable-next-line unicorn/prefer-spread
|
// eslint-disable-next-line unicorn/prefer-spread
|
||||||
const ruleList = rules.concat(options.customRules || []);
|
const ruleList = rules.concat(options.customRules || []);
|
||||||
const ruleErr = validateRuleList(ruleList);
|
const ruleErr = validateRuleList(ruleList, synchronous);
|
||||||
if (ruleErr) {
|
if (ruleErr) {
|
||||||
return callback(ruleErr);
|
return callback(ruleErr);
|
||||||
}
|
}
|
||||||
|
@ -1147,6 +1195,7 @@ module.exports = markdownlint;
|
||||||
* @property {string} description Rule description.
|
* @property {string} description Rule description.
|
||||||
* @property {URL} [information] Link to more information.
|
* @property {URL} [information] Link to more information.
|
||||||
* @property {string[]} tags Rule tag(s).
|
* @property {string[]} tags Rule tag(s).
|
||||||
|
* @property {boolean} [asynchronous] True if asynchronous.
|
||||||
* @property {RuleFunction} function Rule implementation.
|
* @property {RuleFunction} function Rule implementation.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|
|
@ -2,11 +2,11 @@
|
||||||
|
|
||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
|
const fs = require("fs").promises;
|
||||||
const test = require("ava").default;
|
const test = require("ava").default;
|
||||||
const packageJson = require("../package.json");
|
|
||||||
const markdownlint = require("../lib/markdownlint");
|
const markdownlint = require("../lib/markdownlint");
|
||||||
const customRules = require("./rules/rules.js");
|
const customRules = require("./rules/rules.js");
|
||||||
const homepage = packageJson.homepage;
|
const { homepage, version } = require("../package.json");
|
||||||
|
|
||||||
test.cb("customRulesV0", (t) => {
|
test.cb("customRulesV0", (t) => {
|
||||||
t.plan(4);
|
t.plan(4);
|
||||||
|
@ -349,7 +349,7 @@ test.cb("customRulesNpmPackage", (t) => {
|
||||||
});
|
});
|
||||||
|
|
||||||
test("customRulesBadProperty", (t) => {
|
test("customRulesBadProperty", (t) => {
|
||||||
t.plan(23);
|
t.plan(27);
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
"propertyName": "names",
|
"propertyName": "names",
|
||||||
|
@ -364,6 +364,10 @@ test("customRulesBadProperty", (t) => {
|
||||||
"propertyName": "information",
|
"propertyName": "information",
|
||||||
"propertyValues": [ 10, [], "string", "https://example.com" ]
|
"propertyValues": [ 10, [], "string", "https://example.com" ]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"propertyName": "asynchronous",
|
||||||
|
"propertyValues": [ null, 10, "", [] ]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"propertyName": "tags",
|
"propertyName": "tags",
|
||||||
"propertyValues":
|
"propertyValues":
|
||||||
|
@ -1139,6 +1143,168 @@ test.cb("customRulesLintJavaScript", (t) => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("customRulesAsyncThrowsInSyncContext", (t) => {
|
||||||
|
t.plan(1);
|
||||||
|
const options = {
|
||||||
|
"customRules": [
|
||||||
|
{
|
||||||
|
"names": [ "name1", "name2" ],
|
||||||
|
"description": "description",
|
||||||
|
"tags": [ "tag" ],
|
||||||
|
"asynchronous": true,
|
||||||
|
"function": () => {}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"strings": {
|
||||||
|
"string": "Unused"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
t.throws(
|
||||||
|
() => markdownlint.sync(options),
|
||||||
|
{
|
||||||
|
"message": "Custom rule name1/name2 at index 0 is asynchronous and " +
|
||||||
|
"can not be used in a synchronous context."
|
||||||
|
},
|
||||||
|
"Did not get correct exception for async rule in sync context."
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("customRulesAsyncReadFiles", (t) => {
|
||||||
|
t.plan(3);
|
||||||
|
const options = {
|
||||||
|
"customRules": [
|
||||||
|
{
|
||||||
|
"names": [ "name1" ],
|
||||||
|
"description": "description1",
|
||||||
|
"information": new URL("https://example.com/asyncRule1"),
|
||||||
|
"tags": [ "tag" ],
|
||||||
|
"asynchronous": true,
|
||||||
|
"function":
|
||||||
|
(params, onError) => fs.readFile(__filename, "utf8").then(
|
||||||
|
(content) => {
|
||||||
|
t.true(content.length > 0);
|
||||||
|
onError({
|
||||||
|
"lineNumber": 1,
|
||||||
|
"detail": "detail1",
|
||||||
|
"context": "context1",
|
||||||
|
"range": [ 2, 3 ]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"names": [ "name2" ],
|
||||||
|
"description": "description2",
|
||||||
|
"tags": [ "tag" ],
|
||||||
|
"asynchronous": true,
|
||||||
|
"function":
|
||||||
|
(params, onError) => fs.readFile(__filename, "utf8").then(
|
||||||
|
(content) => {
|
||||||
|
t.true(content.length > 0);
|
||||||
|
onError({
|
||||||
|
"lineNumber": 1,
|
||||||
|
"detail": "detail2",
|
||||||
|
"context": "context2"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"strings": {
|
||||||
|
"string": "# Heading"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const expected = {
|
||||||
|
"string": [
|
||||||
|
{
|
||||||
|
"lineNumber": 1,
|
||||||
|
"ruleNames": [ "MD047", "single-trailing-newline" ],
|
||||||
|
"ruleDescription": "Files should end with a single newline character",
|
||||||
|
"ruleInformation": `${homepage}/blob/v${version}/doc/Rules.md#md047`,
|
||||||
|
"errorDetail": null,
|
||||||
|
"errorContext": null,
|
||||||
|
"errorRange": [ 9, 1 ]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lineNumber": 1,
|
||||||
|
"ruleNames": [ "name1" ],
|
||||||
|
"ruleDescription": "description1",
|
||||||
|
"ruleInformation": "https://example.com/asyncRule1",
|
||||||
|
"errorDetail": "detail1",
|
||||||
|
"errorContext": "context1",
|
||||||
|
"errorRange": [ 2, 3 ]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lineNumber": 1,
|
||||||
|
"ruleNames": [ "name2" ],
|
||||||
|
"ruleDescription": "description2",
|
||||||
|
"ruleInformation": null,
|
||||||
|
"errorDetail": "detail2",
|
||||||
|
"errorContext": "context2",
|
||||||
|
"errorRange": null
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
return markdownlint.promises.markdownlint(options)
|
||||||
|
.then((actual) => t.deepEqual(actual, expected, "Unexpected issues."));
|
||||||
|
});
|
||||||
|
|
||||||
|
test("customRulesAsyncIgnoresSyncReturn", (t) => {
|
||||||
|
t.plan(1);
|
||||||
|
const options = {
|
||||||
|
"customRules": [
|
||||||
|
{
|
||||||
|
"names": [ "sync" ],
|
||||||
|
"description": "description",
|
||||||
|
"information": new URL("https://example.com/asyncRule"),
|
||||||
|
"tags": [ "tag" ],
|
||||||
|
"asynchronous": false,
|
||||||
|
"function": () => new Promise(() => {
|
||||||
|
// Never resolves
|
||||||
|
})
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"names": [ "async" ],
|
||||||
|
"description": "description",
|
||||||
|
"information": new URL("https://example.com/asyncRule"),
|
||||||
|
"tags": [ "tag" ],
|
||||||
|
"asynchronous": true,
|
||||||
|
"function": (params, onError) => new Promise((resolve) => {
|
||||||
|
onError({ "lineNumber": 1 });
|
||||||
|
resolve();
|
||||||
|
})
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"strings": {
|
||||||
|
"string": "# Heading"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const expected = {
|
||||||
|
"string": [
|
||||||
|
{
|
||||||
|
"lineNumber": 1,
|
||||||
|
"ruleNames": [ "async" ],
|
||||||
|
"ruleDescription": "description",
|
||||||
|
"ruleInformation": "https://example.com/asyncRule",
|
||||||
|
"errorDetail": null,
|
||||||
|
"errorContext": null,
|
||||||
|
"errorRange": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lineNumber": 1,
|
||||||
|
"ruleNames": [ "MD047", "single-trailing-newline" ],
|
||||||
|
"ruleDescription": "Files should end with a single newline character",
|
||||||
|
"ruleInformation": `${homepage}/blob/v${version}/doc/Rules.md#md047`,
|
||||||
|
"errorDetail": null,
|
||||||
|
"errorContext": null,
|
||||||
|
"errorRange": [ 9, 1 ]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
return markdownlint.promises.markdownlint(options)
|
||||||
|
.then((actual) => t.deepEqual(actual, expected, "Unexpected issues."));
|
||||||
|
});
|
||||||
|
|
||||||
const errorMessage = "Custom error message.";
|
const errorMessage = "Custom error message.";
|
||||||
const stringScenarios = [
|
const stringScenarios = [
|
||||||
[
|
[
|
||||||
|
@ -1260,3 +1426,102 @@ const stringScenarios = [
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
[
|
||||||
|
[
|
||||||
|
"customRulesAsyncExceptionString",
|
||||||
|
() => {
|
||||||
|
throw errorMessage;
|
||||||
|
}
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"customRulesAsyncExceptionError",
|
||||||
|
() => {
|
||||||
|
throw new Error(errorMessage);
|
||||||
|
}
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"customRulesAsyncDeferredString",
|
||||||
|
() => fs.readFile(__filename, "utf8").then(
|
||||||
|
() => {
|
||||||
|
throw errorMessage;
|
||||||
|
}
|
||||||
|
)
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"customRulesAsyncDeferredError",
|
||||||
|
() => fs.readFile(__filename, "utf8").then(
|
||||||
|
() => {
|
||||||
|
throw new Error(errorMessage);
|
||||||
|
}
|
||||||
|
)
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"customRulesAsyncRejectString",
|
||||||
|
() => Promise.reject(errorMessage)
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"customRulesAsyncRejectError",
|
||||||
|
() => Promise.reject(new Error(errorMessage))
|
||||||
|
]
|
||||||
|
].forEach((flavor) => {
|
||||||
|
const [ name, func ] = flavor;
|
||||||
|
const customRule = {
|
||||||
|
"names": [ "name" ],
|
||||||
|
"description": "description",
|
||||||
|
"tags": [ "tag" ],
|
||||||
|
"asynchronous": true,
|
||||||
|
"function": func
|
||||||
|
};
|
||||||
|
stringScenarios.forEach((inputs) => {
|
||||||
|
const [ subname, files, strings ] = inputs;
|
||||||
|
|
||||||
|
test.cb(`${name}${subname}Unhandled`, (t) => {
|
||||||
|
t.plan(4);
|
||||||
|
markdownlint({
|
||||||
|
// @ts-ignore
|
||||||
|
"customRules": [ customRule ],
|
||||||
|
// @ts-ignore
|
||||||
|
files,
|
||||||
|
// @ts-ignore
|
||||||
|
strings
|
||||||
|
}, function callback(err, result) {
|
||||||
|
t.truthy(err, "Did not get an error for rejection.");
|
||||||
|
t.true(err instanceof Error, "Error not instance of Error.");
|
||||||
|
t.is(err.message, errorMessage, "Incorrect message for rejection.");
|
||||||
|
t.true(!result, "Got result for rejection.");
|
||||||
|
t.end();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test.cb(`${name}${subname}Handled`, (t) => {
|
||||||
|
t.plan(2);
|
||||||
|
markdownlint({
|
||||||
|
// @ts-ignore
|
||||||
|
"customRules": [ customRule ],
|
||||||
|
// @ts-ignore
|
||||||
|
files,
|
||||||
|
// @ts-ignore
|
||||||
|
strings,
|
||||||
|
"handleRuleFailures": true
|
||||||
|
}, function callback(err, actualResult) {
|
||||||
|
t.falsy(err);
|
||||||
|
const expectedResult = {
|
||||||
|
"./test/custom-rules.md": [
|
||||||
|
{
|
||||||
|
"lineNumber": 1,
|
||||||
|
"ruleNames": [ "name" ],
|
||||||
|
"ruleDescription": "description",
|
||||||
|
"ruleInformation": null,
|
||||||
|
"errorDetail": `This rule threw an exception: ${errorMessage}`,
|
||||||
|
"errorContext": null,
|
||||||
|
"errorRange": null
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
t.deepEqual(actualResult, expectedResult, "Undetected issues.");
|
||||||
|
t.end();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue