2017-12-15 22:55:51 -08:00
|
|
|
// @ts-check
|
|
|
|
|
2015-02-23 23:39:20 -08:00
|
|
|
"use strict";
|
|
|
|
|
2022-08-16 04:01:53 +00:00
|
|
|
const path = require("node:path");
|
|
|
|
const { promisify } = require("node:util");
|
2024-09-28 16:26:38 -07:00
|
|
|
const micromark = require("../helpers/micromark-parse.cjs");
|
2024-09-29 18:11:41 -07:00
|
|
|
const { version } = require("./constants");
|
2018-04-27 22:05:34 -07:00
|
|
|
const rules = require("./rules");
|
2019-04-13 11:18:57 -07:00
|
|
|
const helpers = require("../helpers");
|
2019-04-10 21:26:59 -07:00
|
|
|
const cache = require("./cache");
|
2015-02-23 23:39:20 -08:00
|
|
|
|
2021-02-11 22:16:07 -08:00
|
|
|
// @ts-ignore
|
2023-11-05 20:13:12 -08:00
|
|
|
// eslint-disable-next-line camelcase, no-inline-comments, no-undef
|
2021-02-11 22:16:07 -08:00
|
|
|
const dynamicRequire = (typeof __non_webpack_require__ === "undefined") ? require : /* c8 ignore next */ __non_webpack_require__;
|
|
|
|
// Capture native require implementation for dynamic loading of modules
|
|
|
|
|
2020-01-23 19:42:46 -08:00
|
|
|
/**
|
|
|
|
* Validate the list of rules for structure and reuse.
|
|
|
|
*
|
|
|
|
* @param {Rule[]} ruleList List of rules.
|
2021-12-11 21:44:25 -08:00
|
|
|
* @param {boolean} synchronous Whether to execute synchronously.
|
2022-06-11 22:40:45 -07:00
|
|
|
* @returns {Error | null} Error message if validation fails.
|
2020-01-23 19:42:46 -08:00
|
|
|
*/
|
2021-12-11 21:44:25 -08:00
|
|
|
function validateRuleList(ruleList, synchronous) {
|
2018-04-27 22:05:34 -07:00
|
|
|
let result = null;
|
2018-02-25 16:04:13 -08:00
|
|
|
if (ruleList.length === rules.length) {
|
|
|
|
// No need to validate if only using built-in rules
|
|
|
|
return result;
|
|
|
|
}
|
2018-04-27 22:05:34 -07:00
|
|
|
const allIds = {};
|
2022-06-08 22:10:27 -07:00
|
|
|
for (const [ index, rule ] of ruleList.entries()) {
|
2018-04-27 22:05:34 -07:00
|
|
|
const customIndex = index - rules.length;
|
2024-04-20 21:23:06 -07:00
|
|
|
// eslint-disable-next-line jsdoc/require-jsdoc
|
2024-03-09 16:17:50 -08:00
|
|
|
function newError(property, value) {
|
2018-02-25 16:04:13 -08:00
|
|
|
return new Error(
|
2024-03-09 16:17:50 -08:00
|
|
|
`Property '${property}' of custom rule at index ${customIndex} is incorrect: '${value}'.`);
|
2018-02-25 16:04:13 -08:00
|
|
|
}
|
2022-06-08 22:10:27 -07:00
|
|
|
for (const property of [ "names", "tags" ]) {
|
2018-04-27 22:05:34 -07:00
|
|
|
const value = rule[property];
|
2018-02-25 16:04:13 -08:00
|
|
|
if (!result &&
|
|
|
|
(!value || !Array.isArray(value) || (value.length === 0) ||
|
2019-04-13 11:18:57 -07:00
|
|
|
!value.every(helpers.isString) || value.some(helpers.isEmptyString))) {
|
2024-03-09 16:17:50 -08:00
|
|
|
result = newError(property, value);
|
2018-02-25 16:04:13 -08:00
|
|
|
}
|
2022-06-08 22:10:27 -07:00
|
|
|
}
|
|
|
|
for (const propertyInfo of [
|
2018-02-25 16:04:13 -08:00
|
|
|
[ "description", "string" ],
|
|
|
|
[ "function", "function" ]
|
2022-06-08 22:10:27 -07:00
|
|
|
]) {
|
2018-04-27 22:05:34 -07:00
|
|
|
const property = propertyInfo[0];
|
|
|
|
const value = rule[property];
|
2018-02-25 16:04:13 -08:00
|
|
|
if (!result && (!value || (typeof value !== propertyInfo[1]))) {
|
2024-03-09 16:17:50 -08:00
|
|
|
result = newError(property, value);
|
2018-02-25 16:04:13 -08:00
|
|
|
}
|
2022-06-08 22:10:27 -07:00
|
|
|
}
|
2024-03-09 16:17:50 -08:00
|
|
|
if (
|
|
|
|
!result &&
|
|
|
|
(rule.parser !== undefined) &&
|
|
|
|
(rule.parser !== "markdownit") &&
|
|
|
|
!((customIndex < 0) && (rule.parser === "micromark")) &&
|
|
|
|
(rule.parser !== "none")
|
|
|
|
) {
|
|
|
|
result = newError("parser", rule.parser);
|
|
|
|
}
|
2021-02-06 19:55:22 -08:00
|
|
|
if (
|
|
|
|
!result &&
|
|
|
|
rule.information &&
|
2023-07-11 21:44:45 -07:00
|
|
|
!helpers.isUrl(rule.information)
|
2021-02-06 19:55:22 -08:00
|
|
|
) {
|
2024-03-09 16:17:50 -08:00
|
|
|
result = newError("information", rule.information);
|
2019-01-15 21:56:38 -08:00
|
|
|
}
|
2021-12-11 21:44:25 -08:00
|
|
|
if (
|
|
|
|
!result &&
|
|
|
|
(rule.asynchronous !== undefined) &&
|
|
|
|
(typeof rule.asynchronous !== "boolean")
|
|
|
|
) {
|
2024-03-09 16:17:50 -08:00
|
|
|
result = newError("asynchronous", rule.asynchronous);
|
2021-12-11 21:44:25 -08:00
|
|
|
}
|
|
|
|
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."
|
|
|
|
);
|
|
|
|
}
|
2018-02-25 16:04:13 -08:00
|
|
|
if (!result) {
|
2022-06-08 22:10:27 -07:00
|
|
|
for (const name of rule.names) {
|
2018-04-27 22:05:34 -07:00
|
|
|
const nameUpper = name.toUpperCase();
|
2018-02-25 16:04:13 -08:00
|
|
|
if (!result && (allIds[nameUpper] !== undefined)) {
|
|
|
|
result = new Error("Name '" + name + "' of custom rule at index " +
|
|
|
|
customIndex + " is already used as a name or tag.");
|
|
|
|
}
|
|
|
|
allIds[nameUpper] = true;
|
2022-06-08 22:10:27 -07:00
|
|
|
}
|
|
|
|
for (const tag of rule.tags) {
|
2018-04-27 22:05:34 -07:00
|
|
|
const tagUpper = tag.toUpperCase();
|
2018-02-25 16:04:13 -08:00
|
|
|
if (!result && allIds[tagUpper]) {
|
|
|
|
result = new Error("Tag '" + tag + "' of custom rule at index " +
|
|
|
|
customIndex + " is already used as a name.");
|
|
|
|
}
|
|
|
|
allIds[tagUpper] = false;
|
2022-06-08 22:10:27 -07:00
|
|
|
}
|
2018-02-25 16:04:13 -08:00
|
|
|
}
|
2022-06-08 22:10:27 -07:00
|
|
|
}
|
2018-02-25 16:04:13 -08:00
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
2020-01-23 19:42:46 -08:00
|
|
|
/**
|
|
|
|
* Creates a LintResults instance with toString for pretty display.
|
|
|
|
*
|
|
|
|
* @param {Rule[]} ruleList List of rules.
|
|
|
|
* @returns {LintResults} New LintResults instance.
|
|
|
|
*/
|
2018-02-25 16:04:13 -08:00
|
|
|
function newResults(ruleList) {
|
2020-09-07 20:05:36 -07:00
|
|
|
const lintResults = {};
|
2020-01-23 19:42:46 -08:00
|
|
|
// eslint-disable-next-line jsdoc/require-jsdoc
|
2020-09-07 20:05:36 -07:00
|
|
|
function toString(useAlias) {
|
2018-04-27 22:05:34 -07:00
|
|
|
let ruleNameToRule = null;
|
|
|
|
const results = [];
|
2020-09-12 12:01:20 -07:00
|
|
|
const keys = Object.keys(lintResults);
|
|
|
|
keys.sort();
|
2022-06-08 22:10:27 -07:00
|
|
|
for (const file of keys) {
|
2020-09-07 20:05:36 -07:00
|
|
|
const fileResults = lintResults[file];
|
2018-02-15 21:35:58 -08:00
|
|
|
if (Array.isArray(fileResults)) {
|
2022-06-08 22:10:27 -07:00
|
|
|
for (const result of fileResults) {
|
2018-04-27 22:05:34 -07:00
|
|
|
const ruleMoniker = result.ruleNames ?
|
2018-02-15 21:35:58 -08:00
|
|
|
result.ruleNames.join("/") :
|
|
|
|
(result.ruleName + "/" + result.ruleAlias);
|
|
|
|
results.push(
|
2016-10-16 21:46:02 -07:00
|
|
|
file + ": " +
|
2018-02-15 21:35:58 -08:00
|
|
|
result.lineNumber + ": " +
|
|
|
|
ruleMoniker + " " +
|
|
|
|
result.ruleDescription +
|
|
|
|
(result.errorDetail ?
|
|
|
|
" [" + result.errorDetail + "]" :
|
|
|
|
"") +
|
|
|
|
(result.errorContext ?
|
|
|
|
" [Context: \"" + result.errorContext + "\"]" :
|
|
|
|
""));
|
2022-06-08 22:10:27 -07:00
|
|
|
}
|
2018-02-15 21:35:58 -08:00
|
|
|
} else {
|
|
|
|
if (!ruleNameToRule) {
|
|
|
|
ruleNameToRule = {};
|
2022-06-08 22:10:27 -07:00
|
|
|
for (const rule of ruleList) {
|
2018-04-27 22:05:34 -07:00
|
|
|
const ruleName = rule.names[0].toUpperCase();
|
2018-02-15 21:35:58 -08:00
|
|
|
ruleNameToRule[ruleName] = rule;
|
2022-06-08 22:10:27 -07:00
|
|
|
}
|
2018-02-15 21:35:58 -08:00
|
|
|
}
|
2022-06-08 22:10:27 -07:00
|
|
|
for (const [ ruleName, ruleResults ] of Object.entries(fileResults)) {
|
2018-04-27 22:05:34 -07:00
|
|
|
const rule = ruleNameToRule[ruleName.toUpperCase()];
|
2022-06-08 22:10:27 -07:00
|
|
|
for (const lineNumber of ruleResults) {
|
|
|
|
// @ts-ignore
|
2018-04-27 22:05:34 -07:00
|
|
|
const nameIndex = Math.min(useAlias ? 1 : 0, rule.names.length - 1);
|
|
|
|
const result =
|
2018-02-15 21:35:58 -08:00
|
|
|
file + ": " +
|
|
|
|
lineNumber + ": " +
|
2022-06-08 22:10:27 -07:00
|
|
|
// @ts-ignore
|
2018-02-15 21:35:58 -08:00
|
|
|
rule.names[nameIndex] + " " +
|
2022-06-08 22:10:27 -07:00
|
|
|
// @ts-ignore
|
2018-02-15 21:35:58 -08:00
|
|
|
rule.description;
|
|
|
|
results.push(result);
|
2022-06-08 22:10:27 -07:00
|
|
|
}
|
|
|
|
}
|
2018-02-15 21:35:58 -08:00
|
|
|
}
|
2022-06-08 22:10:27 -07:00
|
|
|
}
|
2018-02-15 21:35:58 -08:00
|
|
|
return results.join("\n");
|
2020-09-07 20:05:36 -07:00
|
|
|
}
|
|
|
|
Object.defineProperty(lintResults, "toString", { "value": toString });
|
2020-01-23 19:42:46 -08:00
|
|
|
// @ts-ignore
|
2020-09-07 20:05:36 -07:00
|
|
|
return lintResults;
|
2018-02-15 21:35:58 -08:00
|
|
|
}
|
2015-03-13 09:13:07 -07:00
|
|
|
|
2020-01-23 19:42:46 -08:00
|
|
|
/**
|
|
|
|
* Remove front matter (if present at beginning of content).
|
|
|
|
*
|
|
|
|
* @param {string} content Markdown content.
|
2023-01-29 21:13:17 -08:00
|
|
|
* @param {RegExp | null} frontMatter Regular expression to match front matter.
|
2020-01-23 19:42:46 -08:00
|
|
|
* @returns {Object} Trimmed content and front matter lines.
|
|
|
|
*/
|
2018-02-05 21:26:07 -08:00
|
|
|
function removeFrontMatter(content, frontMatter) {
|
2018-04-27 22:05:34 -07:00
|
|
|
let frontMatterLines = [];
|
2015-07-25 22:18:30 -07:00
|
|
|
if (frontMatter) {
|
2018-04-27 22:05:34 -07:00
|
|
|
const frontMatterMatch = content.match(frontMatter);
|
2015-07-25 22:18:30 -07:00
|
|
|
if (frontMatterMatch && !frontMatterMatch.index) {
|
2018-04-27 22:05:34 -07:00
|
|
|
const contentMatched = frontMatterMatch[0];
|
2015-07-25 22:18:30 -07:00
|
|
|
content = content.slice(contentMatched.length);
|
2019-04-13 11:18:57 -07:00
|
|
|
frontMatterLines = contentMatched.split(helpers.newLineRe);
|
2020-09-06 20:34:10 -07:00
|
|
|
if ((frontMatterLines.length > 0) &&
|
2017-05-06 15:25:14 -07:00
|
|
|
(frontMatterLines[frontMatterLines.length - 1] === "")) {
|
|
|
|
frontMatterLines.length--;
|
|
|
|
}
|
2015-07-25 22:18:30 -07:00
|
|
|
}
|
2015-07-20 14:16:52 +02:00
|
|
|
}
|
2018-02-05 21:26:07 -08:00
|
|
|
return {
|
|
|
|
"content": content,
|
|
|
|
"frontMatterLines": frontMatterLines
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2020-01-23 19:42:46 -08:00
|
|
|
/**
|
|
|
|
* Map rule names/tags to canonical rule name.
|
|
|
|
*
|
|
|
|
* @param {Rule[]} ruleList List of rules.
|
|
|
|
* @returns {Object.<string, string[]>} Map of alias to rule name.
|
|
|
|
*/
|
2018-02-05 21:26:07 -08:00
|
|
|
function mapAliasToRuleNames(ruleList) {
|
2018-04-27 22:05:34 -07:00
|
|
|
const aliasToRuleNames = {};
|
|
|
|
// const tagToRuleNames = {};
|
2022-06-08 22:10:27 -07:00
|
|
|
for (const rule of ruleList) {
|
2018-04-27 22:05:34 -07:00
|
|
|
const ruleName = rule.names[0].toUpperCase();
|
2018-02-05 21:26:07 -08:00
|
|
|
// The following is useful for updating README.md:
|
|
|
|
// console.log(
|
|
|
|
// "* **[" + ruleName + "](doc/Rules.md#" + ruleName.toLowerCase() +
|
2018-04-18 22:25:45 -07:00
|
|
|
// ")** *" + rule.names.slice(1).join("/") + "* - " + rule.description);
|
2022-06-08 22:10:27 -07:00
|
|
|
for (const name of rule.names) {
|
2018-04-27 22:05:34 -07:00
|
|
|
const nameUpper = name.toUpperCase();
|
2018-02-05 21:26:07 -08:00
|
|
|
aliasToRuleNames[nameUpper] = [ ruleName ];
|
2022-06-08 22:10:27 -07:00
|
|
|
}
|
|
|
|
for (const tag of rule.tags) {
|
2018-04-27 22:05:34 -07:00
|
|
|
const tagUpper = tag.toUpperCase();
|
|
|
|
const ruleNames = aliasToRuleNames[tagUpper] || [];
|
2018-02-05 21:26:07 -08:00
|
|
|
ruleNames.push(ruleName);
|
|
|
|
aliasToRuleNames[tagUpper] = ruleNames;
|
|
|
|
// tagToRuleNames[tag] = ruleName;
|
2022-06-08 22:10:27 -07:00
|
|
|
}
|
|
|
|
}
|
2018-02-05 21:26:07 -08:00
|
|
|
// The following is useful for updating README.md:
|
|
|
|
// Object.keys(tagToRuleNames).sort().forEach(function forTag(tag) {
|
|
|
|
// console.log("* **" + tag + "** - " +
|
|
|
|
// aliasToRuleNames[tag.toUpperCase()].join(", "));
|
|
|
|
// });
|
2020-01-23 19:42:46 -08:00
|
|
|
// @ts-ignore
|
2018-02-05 21:26:07 -08:00
|
|
|
return aliasToRuleNames;
|
|
|
|
}
|
|
|
|
|
2020-01-23 19:42:46 -08:00
|
|
|
/**
|
|
|
|
* Apply (and normalize) configuration object.
|
|
|
|
*
|
|
|
|
* @param {Rule[]} ruleList List of rules.
|
|
|
|
* @param {Configuration} config Configuration object.
|
|
|
|
* @param {Object.<string, string[]>} aliasToRuleNames Map of alias to rule
|
|
|
|
* names.
|
|
|
|
* @returns {Configuration} Effective configuration.
|
|
|
|
*/
|
2018-02-25 16:04:13 -08:00
|
|
|
function getEffectiveConfig(ruleList, config, aliasToRuleNames) {
|
2019-03-12 22:23:12 -07:00
|
|
|
const defaultKey = Object.keys(config).filter(
|
|
|
|
(key) => key.toUpperCase() === "DEFAULT"
|
|
|
|
);
|
2018-04-27 22:05:34 -07:00
|
|
|
const ruleDefault = (defaultKey.length === 0) || !!config[defaultKey[0]];
|
2023-11-08 19:49:02 -08:00
|
|
|
/** @type {Configuration} */
|
2018-04-27 22:05:34 -07:00
|
|
|
const effectiveConfig = {};
|
2022-06-08 22:10:27 -07:00
|
|
|
for (const rule of ruleList) {
|
2018-04-27 22:05:34 -07:00
|
|
|
const ruleName = rule.names[0].toUpperCase();
|
2018-02-15 21:35:58 -08:00
|
|
|
effectiveConfig[ruleName] = ruleDefault;
|
2022-06-08 22:10:27 -07:00
|
|
|
}
|
2023-11-09 19:47:15 -08:00
|
|
|
// for (const ruleName of deprecatedRuleNames) {
|
|
|
|
// effectiveConfig[ruleName] = false;
|
|
|
|
// }
|
2022-06-08 22:10:27 -07:00
|
|
|
for (const key of Object.keys(config)) {
|
2018-04-27 22:05:34 -07:00
|
|
|
let value = config[key];
|
2015-04-29 18:46:52 -07:00
|
|
|
if (value) {
|
|
|
|
if (!(value instanceof Object)) {
|
|
|
|
value = {};
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
value = false;
|
|
|
|
}
|
2018-04-27 22:05:34 -07:00
|
|
|
const keyUpper = key.toUpperCase();
|
2022-06-08 22:10:27 -07:00
|
|
|
for (const ruleName of (aliasToRuleNames[keyUpper] || [])) {
|
2018-02-15 21:35:58 -08:00
|
|
|
effectiveConfig[ruleName] = value;
|
2022-06-08 22:10:27 -07:00
|
|
|
}
|
|
|
|
}
|
2018-02-15 21:35:58 -08:00
|
|
|
return effectiveConfig;
|
2018-02-05 21:26:07 -08:00
|
|
|
}
|
|
|
|
|
2022-06-05 22:32:22 -07:00
|
|
|
/**
|
|
|
|
* Parse the content of a configuration file.
|
|
|
|
*
|
|
|
|
* @param {string} name Name of the configuration file.
|
|
|
|
* @param {string} content Configuration content.
|
2022-06-11 22:40:45 -07:00
|
|
|
* @param {ConfigurationParser[] | null} [parsers] Parsing function(s).
|
2022-06-05 22:32:22 -07:00
|
|
|
* @returns {Object} Configuration object and error message.
|
|
|
|
*/
|
|
|
|
function parseConfiguration(name, content, parsers) {
|
|
|
|
let config = null;
|
|
|
|
let message = "";
|
|
|
|
const errors = [];
|
|
|
|
let index = 0;
|
|
|
|
// Try each parser
|
|
|
|
(parsers || [ JSON.parse ]).every((parser) => {
|
|
|
|
try {
|
|
|
|
config = parser(content);
|
|
|
|
} catch (error) {
|
|
|
|
errors.push(`Parser ${index++}: ${error.message}`);
|
|
|
|
}
|
|
|
|
return !config;
|
|
|
|
});
|
|
|
|
// Message if unable to parse
|
|
|
|
if (!config) {
|
|
|
|
errors.unshift(`Unable to parse '${name}'`);
|
|
|
|
message = errors.join("; ");
|
|
|
|
}
|
|
|
|
return {
|
|
|
|
config,
|
|
|
|
message
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2020-01-23 19:42:46 -08:00
|
|
|
/**
|
|
|
|
* Create a mapping of enabled rules per line.
|
|
|
|
*
|
|
|
|
* @param {Rule[]} ruleList List of rules.
|
|
|
|
* @param {string[]} lines List of content lines.
|
|
|
|
* @param {string[]} frontMatterLines List of front matter lines.
|
|
|
|
* @param {boolean} noInlineConfig Whether to allow inline configuration.
|
2020-04-05 19:47:12 -07:00
|
|
|
* @param {Configuration} config Configuration object.
|
2022-06-11 22:40:45 -07:00
|
|
|
* @param {ConfigurationParser[] | null} configParsers Configuration parsers.
|
2020-01-23 19:42:46 -08:00
|
|
|
* @param {Object.<string, string[]>} aliasToRuleNames Map of alias to rule
|
|
|
|
* names.
|
2020-04-05 19:47:12 -07:00
|
|
|
* @returns {Object} Effective configuration and enabled rules per line number.
|
2020-01-23 19:42:46 -08:00
|
|
|
*/
|
2018-02-05 21:26:07 -08:00
|
|
|
function getEnabledRulesPerLineNumber(
|
2020-01-23 19:42:46 -08:00
|
|
|
ruleList,
|
|
|
|
lines,
|
|
|
|
frontMatterLines,
|
|
|
|
noInlineConfig,
|
2020-04-05 19:47:12 -07:00
|
|
|
config,
|
2022-06-05 22:32:22 -07:00
|
|
|
configParsers,
|
2020-01-23 19:42:46 -08:00
|
|
|
aliasToRuleNames) {
|
2020-04-05 19:47:12 -07:00
|
|
|
// Shared variables
|
2018-04-27 22:05:34 -07:00
|
|
|
let enabledRules = {};
|
2020-04-05 19:47:12 -07:00
|
|
|
let capturedRules = {};
|
2018-04-27 22:05:34 -07:00
|
|
|
const allRuleNames = [];
|
2020-04-05 19:47:12 -07:00
|
|
|
const enabledRulesPerLineNumber = new Array(1 + frontMatterLines.length);
|
|
|
|
// Helper functions
|
2020-01-23 19:42:46 -08:00
|
|
|
// eslint-disable-next-line jsdoc/require-jsdoc
|
2021-12-21 21:31:47 -08:00
|
|
|
function handleInlineConfig(input, forEachMatch, forEachLine) {
|
2022-06-08 22:10:27 -07:00
|
|
|
for (const [ lineIndex, line ] of input.entries()) {
|
2020-04-05 19:47:12 -07:00
|
|
|
if (!noInlineConfig) {
|
|
|
|
let match = null;
|
2022-02-12 17:46:46 -08:00
|
|
|
while ((match = helpers.inlineCommentStartRe.exec(line))) {
|
|
|
|
const action = match[2].toUpperCase();
|
|
|
|
const startIndex = match.index + match[1].length;
|
|
|
|
const endIndex = line.indexOf("-->", startIndex);
|
|
|
|
if (endIndex === -1) {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
const parameter = line.slice(startIndex, endIndex);
|
2020-11-22 14:02:36 -08:00
|
|
|
forEachMatch(action, parameter, lineIndex + 1);
|
2020-04-05 19:47:12 -07:00
|
|
|
}
|
2019-12-04 21:31:49 -08:00
|
|
|
}
|
2020-04-05 19:47:12 -07:00
|
|
|
if (forEachLine) {
|
|
|
|
forEachLine();
|
2019-12-04 21:31:49 -08:00
|
|
|
}
|
2022-06-08 22:10:27 -07:00
|
|
|
}
|
2020-04-05 19:47:12 -07:00
|
|
|
}
|
|
|
|
// eslint-disable-next-line jsdoc/require-jsdoc
|
|
|
|
function configureFile(action, parameter) {
|
|
|
|
if (action === "CONFIGURE-FILE") {
|
2022-06-05 22:32:22 -07:00
|
|
|
const { "config": parsed } = parseConfiguration(
|
|
|
|
"CONFIGURE-FILE", parameter, configParsers
|
|
|
|
);
|
|
|
|
if (parsed) {
|
2020-04-05 19:47:12 -07:00
|
|
|
config = {
|
|
|
|
...config,
|
2022-06-05 22:32:22 -07:00
|
|
|
...parsed
|
2020-04-05 19:47:12 -07:00
|
|
|
};
|
2019-12-04 21:31:49 -08:00
|
|
|
}
|
2019-06-08 19:26:11 -07:00
|
|
|
}
|
2015-09-26 16:55:33 -07:00
|
|
|
}
|
2020-04-05 19:47:12 -07:00
|
|
|
// eslint-disable-next-line jsdoc/require-jsdoc
|
2020-11-22 14:02:36 -08:00
|
|
|
function applyEnableDisable(action, parameter, state) {
|
2021-12-21 21:31:47 -08:00
|
|
|
state = { ...state };
|
2020-04-05 19:47:12 -07:00
|
|
|
const enabled = (action.startsWith("ENABLE"));
|
2022-02-12 17:46:46 -08:00
|
|
|
const trimmed = parameter && parameter.trim();
|
|
|
|
const items = trimmed ? trimmed.toUpperCase().split(/\s+/) : allRuleNames;
|
2022-06-08 22:10:27 -07:00
|
|
|
for (const nameUpper of items) {
|
|
|
|
for (const ruleName of (aliasToRuleNames[nameUpper] || [])) {
|
2020-11-22 14:02:36 -08:00
|
|
|
state[ruleName] = enabled;
|
2022-06-08 22:10:27 -07:00
|
|
|
}
|
|
|
|
}
|
2021-12-21 21:31:47 -08:00
|
|
|
return state;
|
2020-04-05 19:47:12 -07:00
|
|
|
}
|
|
|
|
// eslint-disable-next-line jsdoc/require-jsdoc
|
|
|
|
function enableDisableFile(action, parameter) {
|
|
|
|
if ((action === "ENABLE-FILE") || (action === "DISABLE-FILE")) {
|
2021-12-21 21:31:47 -08:00
|
|
|
enabledRules = applyEnableDisable(action, parameter, enabledRules);
|
2020-04-05 19:47:12 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
// eslint-disable-next-line jsdoc/require-jsdoc
|
|
|
|
function captureRestoreEnableDisable(action, parameter) {
|
|
|
|
if (action === "CAPTURE") {
|
2021-12-21 21:31:47 -08:00
|
|
|
capturedRules = enabledRules;
|
2020-04-05 19:47:12 -07:00
|
|
|
} else if (action === "RESTORE") {
|
2021-12-21 21:31:47 -08:00
|
|
|
enabledRules = capturedRules;
|
2020-04-05 19:47:12 -07:00
|
|
|
} else if ((action === "ENABLE") || (action === "DISABLE")) {
|
2021-12-21 21:31:47 -08:00
|
|
|
enabledRules = applyEnableDisable(action, parameter, enabledRules);
|
2020-04-05 19:47:12 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
// eslint-disable-next-line jsdoc/require-jsdoc
|
|
|
|
function updateLineState() {
|
2021-12-21 21:31:47 -08:00
|
|
|
enabledRulesPerLineNumber.push(enabledRules);
|
2020-11-22 14:02:36 -08:00
|
|
|
}
|
|
|
|
// eslint-disable-next-line jsdoc/require-jsdoc
|
2022-05-15 15:59:11 -07:00
|
|
|
function disableLineNextLine(action, parameter, lineNumber) {
|
|
|
|
const disableLine = (action === "DISABLE-LINE");
|
|
|
|
const disableNextLine = (action === "DISABLE-NEXT-LINE");
|
|
|
|
if (disableLine || disableNextLine) {
|
|
|
|
const nextLineNumber =
|
|
|
|
frontMatterLines.length + lineNumber + (disableNextLine ? 1 : 0);
|
2021-12-19 00:41:08 -05:00
|
|
|
enabledRulesPerLineNumber[nextLineNumber] =
|
2021-12-21 21:31:47 -08:00
|
|
|
applyEnableDisable(
|
|
|
|
action,
|
|
|
|
parameter,
|
2023-07-28 14:33:03 +10:00
|
|
|
enabledRulesPerLineNumber[nextLineNumber]
|
2021-12-21 21:31:47 -08:00
|
|
|
);
|
2020-11-22 14:02:36 -08:00
|
|
|
}
|
2020-04-05 19:47:12 -07:00
|
|
|
}
|
|
|
|
// Handle inline comments
|
2021-12-21 21:31:47 -08:00
|
|
|
handleInlineConfig([ lines.join("\n") ], configureFile);
|
2020-04-05 19:47:12 -07:00
|
|
|
const effectiveConfig = getEffectiveConfig(
|
|
|
|
ruleList, config, aliasToRuleNames);
|
2022-06-08 22:10:27 -07:00
|
|
|
for (const rule of ruleList) {
|
2020-04-05 19:47:12 -07:00
|
|
|
const ruleName = rule.names[0].toUpperCase();
|
|
|
|
allRuleNames.push(ruleName);
|
|
|
|
enabledRules[ruleName] = !!effectiveConfig[ruleName];
|
2022-06-08 22:10:27 -07:00
|
|
|
}
|
2020-04-05 19:47:12 -07:00
|
|
|
capturedRules = enabledRules;
|
2021-12-21 21:31:47 -08:00
|
|
|
handleInlineConfig(lines, enableDisableFile);
|
|
|
|
handleInlineConfig(lines, captureRestoreEnableDisable, updateLineState);
|
2022-05-15 15:59:11 -07:00
|
|
|
handleInlineConfig(lines, disableLineNextLine);
|
2024-08-22 20:35:01 -07:00
|
|
|
// Create the list of rules that are used at least once
|
|
|
|
const enabledRuleList = [];
|
|
|
|
for (const [ index, ruleName ] of allRuleNames.entries()) {
|
|
|
|
if (enabledRulesPerLineNumber.some((enabledRulesForLine) => enabledRulesForLine[ruleName])) {
|
|
|
|
enabledRuleList.push(ruleList[index]);
|
|
|
|
}
|
|
|
|
}
|
2020-04-05 19:47:12 -07:00
|
|
|
// Return results
|
|
|
|
return {
|
|
|
|
effectiveConfig,
|
2024-08-22 20:35:01 -07:00
|
|
|
enabledRulesPerLineNumber,
|
|
|
|
enabledRuleList
|
2020-04-05 19:47:12 -07:00
|
|
|
};
|
2018-02-05 21:26:07 -08:00
|
|
|
}
|
|
|
|
|
2020-01-23 19:42:46 -08:00
|
|
|
/**
|
|
|
|
* Lints a string containing Markdown content.
|
|
|
|
*
|
|
|
|
* @param {Rule[]} ruleList List of rules.
|
2023-03-12 20:49:41 -07:00
|
|
|
* @param {Object.<string, string[]>} aliasToRuleNames Map of alias to rule
|
|
|
|
* names.
|
2020-01-23 19:42:46 -08:00
|
|
|
* @param {string} name Identifier for the content.
|
2020-11-24 16:25:08 -08:00
|
|
|
* @param {string} content Markdown content.
|
2024-09-14 17:33:46 -07:00
|
|
|
* @param {Plugin[]} markdownItPlugins Additional plugins.
|
2020-01-23 19:42:46 -08:00
|
|
|
* @param {Configuration} config Configuration object.
|
2022-06-11 22:40:45 -07:00
|
|
|
* @param {ConfigurationParser[] | null} configParsers Configuration parsers.
|
2023-01-29 21:13:17 -08:00
|
|
|
* @param {RegExp | null} frontMatter Regular expression for front matter.
|
2020-01-23 19:42:46 -08:00
|
|
|
* @param {boolean} handleRuleFailures Whether to handle exceptions in rules.
|
|
|
|
* @param {boolean} noInlineConfig Whether to allow inline configuration.
|
|
|
|
* @param {number} resultVersion Version of the LintResults object to return.
|
2023-09-04 21:41:16 -07:00
|
|
|
* @param {LintContentCallback} callback Callback (err, result) function.
|
2020-01-23 19:42:46 -08:00
|
|
|
* @returns {void}
|
|
|
|
*/
|
2018-02-05 21:26:07 -08:00
|
|
|
function lintContent(
|
2019-01-19 12:52:13 -08:00
|
|
|
ruleList,
|
2023-03-12 20:49:41 -07:00
|
|
|
aliasToRuleNames,
|
2019-01-19 12:52:13 -08:00
|
|
|
name,
|
|
|
|
content,
|
2024-09-14 17:33:46 -07:00
|
|
|
markdownItPlugins,
|
2019-01-19 12:52:13 -08:00
|
|
|
config,
|
2022-06-05 22:32:22 -07:00
|
|
|
configParsers,
|
2019-01-19 12:52:13 -08:00
|
|
|
frontMatter,
|
2019-05-18 12:32:52 -07:00
|
|
|
handleRuleFailures,
|
2019-01-19 12:52:13 -08:00
|
|
|
noInlineConfig,
|
|
|
|
resultVersion,
|
2018-02-25 16:04:13 -08:00
|
|
|
callback) {
|
2018-02-05 21:26:07 -08:00
|
|
|
// Remove UTF-8 byte order marker (if present)
|
2020-09-06 20:34:10 -07:00
|
|
|
content = content.replace(/^\uFEFF/, "");
|
2018-02-05 21:26:07 -08:00
|
|
|
// Remove front matter
|
2018-04-27 22:05:34 -07:00
|
|
|
const removeFrontMatterResult = removeFrontMatter(content, frontMatter);
|
2022-06-04 22:59:19 -07:00
|
|
|
const { frontMatterLines } = removeFrontMatterResult;
|
|
|
|
content = removeFrontMatterResult.content;
|
|
|
|
// Get enabled rules per line (with HTML comments present)
|
2024-08-22 20:35:01 -07:00
|
|
|
const { effectiveConfig, enabledRulesPerLineNumber, enabledRuleList } =
|
2020-04-05 19:47:12 -07:00
|
|
|
getEnabledRulesPerLineNumber(
|
|
|
|
ruleList,
|
2022-06-04 22:59:19 -07:00
|
|
|
content.split(helpers.newLineRe),
|
2020-04-05 19:47:12 -07:00
|
|
|
frontMatterLines,
|
|
|
|
noInlineConfig,
|
|
|
|
config,
|
2022-06-05 22:32:22 -07:00
|
|
|
configParsers,
|
2023-03-12 20:49:41 -07:00
|
|
|
aliasToRuleNames
|
2020-04-05 19:47:12 -07:00
|
|
|
);
|
2024-08-22 20:35:01 -07:00
|
|
|
const needMarkdownItTokens = enabledRuleList.some(
|
2024-08-21 21:19:40 -07:00
|
|
|
(rule) => (rule.parser === "markdownit") || (rule.parser === undefined)
|
|
|
|
);
|
2023-01-15 21:41:22 -08:00
|
|
|
// Parse content into parser tokens
|
|
|
|
const micromarkTokens = micromark.parse(content);
|
|
|
|
// Hide the content of HTML comments from rules
|
2024-09-14 17:33:46 -07:00
|
|
|
const preClearedContent = content;
|
2022-06-04 22:59:19 -07:00
|
|
|
content = helpers.clearHtmlCommentText(content);
|
2024-09-14 17:33:46 -07:00
|
|
|
// Parse content into lines and get markdown-it tokens
|
2022-06-04 22:59:19 -07:00
|
|
|
const lines = content.split(helpers.newLineRe);
|
2024-09-14 17:33:46 -07:00
|
|
|
const markdownitTokens = needMarkdownItTokens ?
|
|
|
|
require("./markdownit.cjs").getMarkdownItTokens(markdownItPlugins, preClearedContent, lines) :
|
|
|
|
[];
|
2022-06-09 23:56:44 -07:00
|
|
|
// Create (frozen) parameters for rules
|
2024-03-09 16:17:50 -08:00
|
|
|
/** @type {MarkdownParsers} */
|
|
|
|
// @ts-ignore
|
|
|
|
const parsersMarkdownIt = Object.freeze({
|
2023-01-21 19:16:07 -08:00
|
|
|
"markdownit": Object.freeze({
|
|
|
|
"tokens": markdownitTokens
|
2024-03-09 16:17:50 -08:00
|
|
|
})
|
|
|
|
});
|
|
|
|
/** @type {MarkdownParsers} */
|
|
|
|
// @ts-ignore
|
|
|
|
const parsersMicromark = Object.freeze({
|
2023-01-21 19:16:07 -08:00
|
|
|
"micromark": Object.freeze({
|
|
|
|
"tokens": micromarkTokens
|
|
|
|
})
|
2023-01-15 21:41:22 -08:00
|
|
|
});
|
2024-03-09 16:17:50 -08:00
|
|
|
/** @type {MarkdownParsers} */
|
|
|
|
// @ts-ignore
|
|
|
|
const parsersNone = Object.freeze({});
|
2022-06-09 23:56:44 -07:00
|
|
|
const paramsBase = {
|
2022-03-20 12:59:35 -07:00
|
|
|
name,
|
2024-09-29 18:11:41 -07:00
|
|
|
version,
|
2022-06-09 23:56:44 -07:00
|
|
|
"lines": Object.freeze(lines),
|
|
|
|
"frontMatterLines": Object.freeze(frontMatterLines)
|
|
|
|
};
|
2024-08-24 22:05:16 -07:00
|
|
|
cache.initialize({
|
|
|
|
...paramsBase,
|
|
|
|
"parsers": parsersMicromark,
|
|
|
|
"config": null
|
2022-06-02 21:33:31 -07:00
|
|
|
});
|
2018-02-25 16:04:13 -08:00
|
|
|
// Function to run for each rule
|
2021-12-04 17:02:11 -08:00
|
|
|
let results = [];
|
2024-03-09 16:17:50 -08:00
|
|
|
/**
|
|
|
|
* @param {Rule} rule Rule.
|
|
|
|
* @returns {Promise<void> | null} Promise.
|
|
|
|
*/
|
|
|
|
const forRule = (rule) => {
|
2015-09-26 16:55:33 -07:00
|
|
|
// Configure rule
|
2021-12-04 22:09:20 -08:00
|
|
|
const ruleName = rule.names[0].toUpperCase();
|
2024-03-09 16:17:50 -08:00
|
|
|
const tokens = {};
|
|
|
|
let parsers = parsersNone;
|
|
|
|
if (rule.parser === undefined) {
|
|
|
|
tokens.tokens = markdownitTokens;
|
|
|
|
parsers = parsersMarkdownIt;
|
|
|
|
} else if (rule.parser === "markdownit") {
|
|
|
|
parsers = parsersMarkdownIt;
|
|
|
|
} else if (rule.parser === "micromark") {
|
|
|
|
parsers = parsersMicromark;
|
|
|
|
}
|
2022-03-20 12:59:35 -07:00
|
|
|
const params = {
|
|
|
|
...paramsBase,
|
2024-03-09 16:17:50 -08:00
|
|
|
...tokens,
|
|
|
|
parsers,
|
2022-03-20 12:59:35 -07:00
|
|
|
"config": effectiveConfig[ruleName]
|
|
|
|
};
|
2020-01-23 19:42:46 -08:00
|
|
|
// eslint-disable-next-line jsdoc/require-jsdoc
|
2018-02-27 21:14:02 -08:00
|
|
|
function throwError(property) {
|
|
|
|
throw new Error(
|
2023-10-07 19:32:29 -07:00
|
|
|
`Value of '${property}' passed to onError by '${ruleName}' is incorrect for '${name}'.`);
|
2018-02-27 21:14:02 -08:00
|
|
|
}
|
2020-01-23 19:42:46 -08:00
|
|
|
// eslint-disable-next-line jsdoc/require-jsdoc
|
2018-01-14 21:53:35 -08:00
|
|
|
function onError(errorInfo) {
|
2018-02-27 21:14:02 -08:00
|
|
|
if (!errorInfo ||
|
2019-04-23 22:30:33 -07:00
|
|
|
!helpers.isNumber(errorInfo.lineNumber) ||
|
|
|
|
(errorInfo.lineNumber < 1) ||
|
|
|
|
(errorInfo.lineNumber > lines.length)) {
|
2018-02-27 21:14:02 -08:00
|
|
|
throwError("lineNumber");
|
|
|
|
}
|
2021-12-04 22:09:20 -08:00
|
|
|
const lineNumber = errorInfo.lineNumber + frontMatterLines.length;
|
|
|
|
if (!enabledRulesPerLineNumber[lineNumber][ruleName]) {
|
|
|
|
return;
|
|
|
|
}
|
2018-03-17 22:11:01 -07:00
|
|
|
if (errorInfo.detail &&
|
2019-04-13 11:18:57 -07:00
|
|
|
!helpers.isString(errorInfo.detail)) {
|
2018-02-27 21:14:02 -08:00
|
|
|
throwError("detail");
|
|
|
|
}
|
2018-03-17 22:11:01 -07:00
|
|
|
if (errorInfo.context &&
|
2019-04-13 11:18:57 -07:00
|
|
|
!helpers.isString(errorInfo.context)) {
|
2018-02-27 21:14:02 -08:00
|
|
|
throwError("context");
|
|
|
|
}
|
2023-07-11 21:44:45 -07:00
|
|
|
if (errorInfo.information &&
|
|
|
|
!helpers.isUrl(errorInfo.information)) {
|
|
|
|
throwError("information");
|
|
|
|
}
|
2018-02-27 21:14:02 -08:00
|
|
|
if (errorInfo.range &&
|
|
|
|
(!Array.isArray(errorInfo.range) ||
|
2021-12-11 21:44:25 -08:00
|
|
|
(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) >
|
2019-04-23 22:30:33 -07:00
|
|
|
lines[errorInfo.lineNumber - 1].length))) {
|
2018-02-27 21:14:02 -08:00
|
|
|
throwError("range");
|
|
|
|
}
|
2019-09-14 13:39:27 -07:00
|
|
|
const fixInfo = errorInfo.fixInfo;
|
2019-10-19 17:34:02 -07:00
|
|
|
const cleanFixInfo = {};
|
2019-09-14 13:39:27 -07:00
|
|
|
if (fixInfo) {
|
|
|
|
if (!helpers.isObject(fixInfo)) {
|
|
|
|
throwError("fixInfo");
|
|
|
|
}
|
2019-10-19 17:34:02 -07:00
|
|
|
if (fixInfo.lineNumber !== undefined) {
|
|
|
|
if ((!helpers.isNumber(fixInfo.lineNumber) ||
|
|
|
|
(fixInfo.lineNumber < 1) ||
|
|
|
|
(fixInfo.lineNumber > lines.length))) {
|
|
|
|
throwError("fixInfo.lineNumber");
|
|
|
|
}
|
|
|
|
cleanFixInfo.lineNumber =
|
|
|
|
fixInfo.lineNumber + frontMatterLines.length;
|
2019-09-14 13:39:27 -07:00
|
|
|
}
|
|
|
|
const effectiveLineNumber = fixInfo.lineNumber || errorInfo.lineNumber;
|
2019-10-19 17:34:02 -07:00
|
|
|
if (fixInfo.editColumn !== undefined) {
|
|
|
|
if ((!helpers.isNumber(fixInfo.editColumn) ||
|
|
|
|
(fixInfo.editColumn < 1) ||
|
|
|
|
(fixInfo.editColumn >
|
|
|
|
lines[effectiveLineNumber - 1].length + 1))) {
|
|
|
|
throwError("fixInfo.editColumn");
|
|
|
|
}
|
|
|
|
cleanFixInfo.editColumn = fixInfo.editColumn;
|
2019-09-14 13:39:27 -07:00
|
|
|
}
|
2019-10-19 17:34:02 -07:00
|
|
|
if (fixInfo.deleteCount !== undefined) {
|
|
|
|
if ((!helpers.isNumber(fixInfo.deleteCount) ||
|
|
|
|
(fixInfo.deleteCount < -1) ||
|
|
|
|
(fixInfo.deleteCount >
|
|
|
|
lines[effectiveLineNumber - 1].length))) {
|
|
|
|
throwError("fixInfo.deleteCount");
|
|
|
|
}
|
|
|
|
cleanFixInfo.deleteCount = fixInfo.deleteCount;
|
2019-09-14 13:39:27 -07:00
|
|
|
}
|
2019-10-19 17:34:02 -07:00
|
|
|
if (fixInfo.insertText !== undefined) {
|
|
|
|
if (!helpers.isString(fixInfo.insertText)) {
|
|
|
|
throwError("fixInfo.insertText");
|
|
|
|
}
|
|
|
|
cleanFixInfo.insertText = fixInfo.insertText;
|
2019-09-14 13:39:27 -07:00
|
|
|
}
|
|
|
|
}
|
2023-07-11 21:44:45 -07:00
|
|
|
const information = errorInfo.information || rule.information;
|
2021-12-04 22:09:20 -08:00
|
|
|
results.push({
|
|
|
|
lineNumber,
|
|
|
|
"ruleName": rule.names[0],
|
|
|
|
"ruleNames": rule.names,
|
|
|
|
"ruleDescription": rule.description,
|
2023-07-11 21:44:45 -07:00
|
|
|
"ruleInformation": information ? information.href : null,
|
2021-12-04 22:09:20 -08:00
|
|
|
"errorDetail": errorInfo.detail || null,
|
|
|
|
"errorContext": errorInfo.context || null,
|
|
|
|
"errorRange": errorInfo.range ? [ ...errorInfo.range ] : null,
|
2019-10-19 17:34:02 -07:00
|
|
|
"fixInfo": fixInfo ? cleanFixInfo : null
|
2016-10-16 21:46:02 -07:00
|
|
|
});
|
|
|
|
}
|
2021-12-04 17:02:11 -08:00
|
|
|
// Call (possibly external) rule function to report errors
|
2021-12-11 21:44:25 -08:00
|
|
|
const catchCallsOnError = (error) => onError({
|
|
|
|
"lineNumber": 1,
|
|
|
|
"detail": `This rule threw an exception: ${error.message || error}`
|
|
|
|
});
|
|
|
|
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;
|
2024-03-20 20:24:55 -07:00
|
|
|
};
|
2021-12-11 21:44:25 -08:00
|
|
|
// 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;
|
2019-05-18 12:32:52 -07:00
|
|
|
}
|
|
|
|
} else {
|
2021-12-11 21:44:25 -08:00
|
|
|
// resultVersion 2 or 3: Remove unwanted ruleName
|
|
|
|
for (const error of results) {
|
|
|
|
delete error.ruleName;
|
|
|
|
}
|
2019-05-18 12:32:52 -07:00
|
|
|
}
|
2021-12-11 21:44:25 -08:00
|
|
|
return results;
|
2018-02-25 16:04:13 -08:00
|
|
|
}
|
|
|
|
// Run all rules
|
2024-08-22 20:35:01 -07:00
|
|
|
const ruleListAsync = enabledRuleList.filter((rule) => rule.asynchronous);
|
|
|
|
const ruleListSync = enabledRuleList.filter((rule) => !rule.asynchronous);
|
2021-12-11 21:44:25 -08:00
|
|
|
const ruleListAsyncFirst = [
|
|
|
|
...ruleListAsync,
|
|
|
|
...ruleListSync
|
|
|
|
];
|
|
|
|
const callbackSuccess = () => callback(null, formatResults());
|
|
|
|
const callbackError =
|
|
|
|
(error) => callback(error instanceof Error ? error : new Error(error));
|
2018-02-25 16:04:13 -08:00
|
|
|
try {
|
2021-12-11 21:44:25 -08:00
|
|
|
const ruleResults = ruleListAsyncFirst.map(forRule);
|
|
|
|
if (ruleListAsync.length > 0) {
|
|
|
|
Promise.all(ruleResults.slice(0, ruleListAsync.length))
|
|
|
|
.then(callbackSuccess)
|
|
|
|
.catch(callbackError);
|
|
|
|
} else {
|
|
|
|
callbackSuccess();
|
|
|
|
}
|
2020-09-06 20:34:10 -07:00
|
|
|
} catch (error) {
|
2021-12-11 21:44:25 -08:00
|
|
|
callbackError(error);
|
|
|
|
} finally {
|
2024-08-24 22:05:16 -07:00
|
|
|
cache.initialize();
|
2021-12-04 17:02:11 -08:00
|
|
|
}
|
2015-04-29 18:46:52 -07:00
|
|
|
}
|
|
|
|
|
2020-01-23 19:42:46 -08:00
|
|
|
/**
|
|
|
|
* Lints a file containing Markdown content.
|
|
|
|
*
|
|
|
|
* @param {Rule[]} ruleList List of rules.
|
2023-03-12 20:49:41 -07:00
|
|
|
* @param {Object.<string, string[]>} aliasToRuleNames Map of alias to rule
|
|
|
|
* names.
|
2020-01-23 19:42:46 -08:00
|
|
|
* @param {string} file Path of file to lint.
|
2024-09-14 17:33:46 -07:00
|
|
|
* @param {Plugin[]} markdownItPlugins Additional plugins.
|
2020-01-23 19:42:46 -08:00
|
|
|
* @param {Configuration} config Configuration object.
|
2022-06-11 22:40:45 -07:00
|
|
|
* @param {ConfigurationParser[] | null} configParsers Configuration parsers.
|
2023-01-29 21:13:17 -08:00
|
|
|
* @param {RegExp | null} frontMatter Regular expression for front matter.
|
2020-01-23 19:42:46 -08:00
|
|
|
* @param {boolean} handleRuleFailures Whether to handle exceptions in rules.
|
|
|
|
* @param {boolean} noInlineConfig Whether to allow inline configuration.
|
|
|
|
* @param {number} resultVersion Version of the LintResults object to return.
|
2021-08-12 19:38:03 -07:00
|
|
|
* @param {Object} fs File system implementation.
|
2020-01-23 19:42:46 -08:00
|
|
|
* @param {boolean} synchronous Whether to execute synchronously.
|
2023-09-04 21:41:16 -07:00
|
|
|
* @param {LintContentCallback} callback Callback (err, result) function.
|
2020-01-23 19:42:46 -08:00
|
|
|
* @returns {void}
|
|
|
|
*/
|
2016-10-16 21:46:02 -07:00
|
|
|
function lintFile(
|
2018-02-25 16:04:13 -08:00
|
|
|
ruleList,
|
2023-03-12 20:49:41 -07:00
|
|
|
aliasToRuleNames,
|
2016-10-16 21:46:02 -07:00
|
|
|
file,
|
2024-09-14 17:33:46 -07:00
|
|
|
markdownItPlugins,
|
2016-10-16 21:46:02 -07:00
|
|
|
config,
|
2022-06-05 22:32:22 -07:00
|
|
|
configParsers,
|
2016-10-16 21:46:02 -07:00
|
|
|
frontMatter,
|
2019-05-18 12:32:52 -07:00
|
|
|
handleRuleFailures,
|
2017-05-21 22:58:10 -07:00
|
|
|
noInlineConfig,
|
2016-10-16 21:46:02 -07:00
|
|
|
resultVersion,
|
2021-08-12 19:38:03 -07:00
|
|
|
fs,
|
2016-10-16 21:46:02 -07:00
|
|
|
synchronous,
|
|
|
|
callback) {
|
2020-01-23 19:42:46 -08:00
|
|
|
// eslint-disable-next-line jsdoc/require-jsdoc
|
2015-04-29 18:46:52 -07:00
|
|
|
function lintContentWrapper(err, content) {
|
|
|
|
if (err) {
|
|
|
|
return callback(err);
|
|
|
|
}
|
2022-06-05 22:32:22 -07:00
|
|
|
return lintContent(
|
|
|
|
ruleList,
|
2023-03-12 20:49:41 -07:00
|
|
|
aliasToRuleNames,
|
2022-06-05 22:32:22 -07:00
|
|
|
file,
|
|
|
|
content,
|
2024-09-14 17:33:46 -07:00
|
|
|
markdownItPlugins,
|
2022-06-05 22:32:22 -07:00
|
|
|
config,
|
|
|
|
configParsers,
|
|
|
|
frontMatter,
|
|
|
|
handleRuleFailures,
|
|
|
|
noInlineConfig,
|
|
|
|
resultVersion,
|
|
|
|
callback
|
|
|
|
);
|
2015-03-20 00:09:55 -07:00
|
|
|
}
|
|
|
|
// Make a/synchronous call to read file
|
|
|
|
if (synchronous) {
|
2021-08-05 22:01:29 -07:00
|
|
|
lintContentWrapper(null, fs.readFileSync(file, "utf8"));
|
2015-03-20 00:09:55 -07:00
|
|
|
} else {
|
2021-08-05 22:01:29 -07:00
|
|
|
fs.readFile(file, "utf8", lintContentWrapper);
|
2015-03-20 00:09:55 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-01-23 19:42:46 -08:00
|
|
|
/**
|
|
|
|
* Lint files and strings specified in the Options object.
|
|
|
|
*
|
2023-01-29 21:13:17 -08:00
|
|
|
* @param {Options | null} options Options object.
|
2020-01-23 19:42:46 -08:00
|
|
|
* @param {boolean} synchronous Whether to execute synchronously.
|
2023-09-04 21:41:16 -07:00
|
|
|
* @param {LintCallback} callback Callback (err, result) function.
|
2020-01-23 19:42:46 -08:00
|
|
|
* @returns {void}
|
|
|
|
*/
|
2017-12-15 22:55:51 -08:00
|
|
|
function lintInput(options, synchronous, callback) {
|
2015-03-15 23:39:17 -07:00
|
|
|
// Normalize inputs
|
2015-02-27 22:06:54 -08:00
|
|
|
options = options || {};
|
2015-03-11 21:13:21 -07:00
|
|
|
callback = callback || function noop() {};
|
2023-07-11 22:17:53 -07:00
|
|
|
const customRuleList =
|
|
|
|
[ options.customRules || [] ]
|
|
|
|
.flat()
|
2023-07-12 21:58:36 -07:00
|
|
|
.map((rule) => ({
|
|
|
|
"names": helpers.cloneIfArray(rule.names),
|
|
|
|
"description": rule.description,
|
|
|
|
"information": helpers.cloneIfUrl(rule.information),
|
|
|
|
"tags": helpers.cloneIfArray(rule.tags),
|
2024-03-09 16:17:50 -08:00
|
|
|
"parser": rule.parser,
|
2023-07-12 21:58:36 -07:00
|
|
|
"asynchronous": rule.asynchronous,
|
|
|
|
"function": rule.function
|
|
|
|
}));
|
2021-02-06 19:55:22 -08:00
|
|
|
// eslint-disable-next-line unicorn/prefer-spread
|
2023-07-11 22:17:53 -07:00
|
|
|
const ruleList = rules.concat(customRuleList);
|
2021-12-11 21:44:25 -08:00
|
|
|
const ruleErr = validateRuleList(ruleList, synchronous);
|
2018-02-25 16:04:13 -08:00
|
|
|
if (ruleErr) {
|
2022-06-11 22:40:45 -07:00
|
|
|
callback(ruleErr);
|
|
|
|
return;
|
2018-02-25 16:04:13 -08:00
|
|
|
}
|
2018-04-27 22:05:34 -07:00
|
|
|
let files = [];
|
2016-01-15 22:00:34 -08:00
|
|
|
if (Array.isArray(options.files)) {
|
Update dependencies: c8 to 7.7.2, eslint to 7.28.0, eslint-plugin-jsdoc to 35.1.3, eslint-plugin-unicorn to 33.0.1, globby to 11.0.3, js-yaml to 4.1.0, markdown-it-texmath to 0.9.0, markdownlint-rule-helpers to 0.14.0, ts-loader to 9.2.3, typescript to 4.3.2, webpack to 5.38.1, webpack-cli to 4.7.2.
2021-06-08 22:20:13 -07:00
|
|
|
files = [ ...options.files ];
|
2016-01-15 22:00:34 -08:00
|
|
|
} else if (options.files) {
|
|
|
|
files = [ String(options.files) ];
|
|
|
|
}
|
2018-04-27 22:05:34 -07:00
|
|
|
const strings = options.strings || {};
|
|
|
|
const stringsKeys = Object.keys(strings);
|
|
|
|
const config = options.config || { "default": true };
|
2022-06-05 22:32:22 -07:00
|
|
|
const configParsers = options.configParsers || null;
|
2018-04-27 22:05:34 -07:00
|
|
|
const frontMatter = (options.frontMatter === undefined) ?
|
2024-09-01 16:16:05 -07:00
|
|
|
helpers.frontMatterRe :
|
|
|
|
options.frontMatter;
|
2019-05-18 12:32:52 -07:00
|
|
|
const handleRuleFailures = !!options.handleRuleFailures;
|
2018-04-27 22:05:34 -07:00
|
|
|
const noInlineConfig = !!options.noInlineConfig;
|
|
|
|
const resultVersion = (options.resultVersion === undefined) ?
|
2024-09-01 16:16:05 -07:00
|
|
|
3 :
|
|
|
|
options.resultVersion;
|
2024-09-14 17:33:46 -07:00
|
|
|
const markdownItPlugins = options.markdownItPlugins || [];
|
2022-08-16 04:01:53 +00:00
|
|
|
const fs = options.fs || require("node:fs");
|
2023-03-12 20:49:41 -07:00
|
|
|
const aliasToRuleNames = mapAliasToRuleNames(ruleList);
|
2018-04-27 22:05:34 -07:00
|
|
|
const results = newResults(ruleList);
|
2020-09-12 12:01:20 -07:00
|
|
|
let done = false;
|
2021-12-03 22:43:58 -08:00
|
|
|
let concurrency = 0;
|
2020-01-23 19:42:46 -08:00
|
|
|
// eslint-disable-next-line jsdoc/require-jsdoc
|
2021-12-03 22:43:58 -08:00
|
|
|
function lintWorker() {
|
|
|
|
let currentItem = null;
|
|
|
|
// eslint-disable-next-line jsdoc/require-jsdoc
|
|
|
|
function lintWorkerCallback(err, result) {
|
|
|
|
concurrency--;
|
|
|
|
if (err) {
|
|
|
|
done = true;
|
|
|
|
return callback(err);
|
|
|
|
}
|
|
|
|
results[currentItem] = result;
|
|
|
|
if (!synchronous) {
|
|
|
|
lintWorker();
|
|
|
|
}
|
|
|
|
return null;
|
2018-02-25 16:04:13 -08:00
|
|
|
}
|
2021-12-03 22:43:58 -08:00
|
|
|
if (done) {
|
|
|
|
// Abort for error or nothing left to do
|
|
|
|
} else if (files.length > 0) {
|
|
|
|
// Lint next file
|
|
|
|
concurrency++;
|
|
|
|
currentItem = files.shift();
|
2020-09-12 12:01:20 -07:00
|
|
|
lintFile(
|
|
|
|
ruleList,
|
2023-03-12 20:49:41 -07:00
|
|
|
aliasToRuleNames,
|
2021-12-03 22:43:58 -08:00
|
|
|
currentItem,
|
2024-09-14 17:33:46 -07:00
|
|
|
markdownItPlugins,
|
2020-09-12 12:01:20 -07:00
|
|
|
config,
|
2022-06-05 22:32:22 -07:00
|
|
|
configParsers,
|
2020-09-12 12:01:20 -07:00
|
|
|
frontMatter,
|
|
|
|
handleRuleFailures,
|
|
|
|
noInlineConfig,
|
|
|
|
resultVersion,
|
2021-08-12 19:38:03 -07:00
|
|
|
fs,
|
2020-09-12 12:01:20 -07:00
|
|
|
synchronous,
|
2021-12-03 22:43:58 -08:00
|
|
|
lintWorkerCallback
|
2020-09-12 12:01:20 -07:00
|
|
|
);
|
2022-06-11 22:40:45 -07:00
|
|
|
} else if ((currentItem = stringsKeys.shift())) {
|
2021-12-03 22:43:58 -08:00
|
|
|
// Lint next string
|
2020-09-12 12:01:20 -07:00
|
|
|
concurrency++;
|
2021-12-03 22:43:58 -08:00
|
|
|
lintContent(
|
2020-09-12 12:01:20 -07:00
|
|
|
ruleList,
|
2023-03-12 20:49:41 -07:00
|
|
|
aliasToRuleNames,
|
2021-12-03 22:43:58 -08:00
|
|
|
currentItem,
|
|
|
|
strings[currentItem] || "",
|
2024-09-14 17:33:46 -07:00
|
|
|
markdownItPlugins,
|
2020-09-12 12:01:20 -07:00
|
|
|
config,
|
2022-06-05 22:32:22 -07:00
|
|
|
configParsers,
|
2020-09-12 12:01:20 -07:00
|
|
|
frontMatter,
|
|
|
|
handleRuleFailures,
|
|
|
|
noInlineConfig,
|
|
|
|
resultVersion,
|
2021-12-03 22:43:58 -08:00
|
|
|
lintWorkerCallback
|
2020-09-12 12:01:20 -07:00
|
|
|
);
|
|
|
|
} else if (concurrency === 0) {
|
2021-12-03 22:43:58 -08:00
|
|
|
// Finish
|
2020-09-12 12:01:20 -07:00
|
|
|
done = true;
|
|
|
|
return callback(null, results);
|
2015-02-24 18:40:37 -08:00
|
|
|
}
|
2020-09-12 12:01:20 -07:00
|
|
|
return null;
|
2015-02-24 18:40:37 -08:00
|
|
|
}
|
2021-12-03 22:43:58 -08:00
|
|
|
if (synchronous) {
|
|
|
|
while (!done) {
|
|
|
|
lintWorker();
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
// Testing on a Raspberry Pi 4 Model B with an artificial 5ms file access
|
|
|
|
// delay suggests that a concurrency factor of 8 can eliminate the impact
|
|
|
|
// of that delay (i.e., total time is the same as with no delay).
|
|
|
|
lintWorker();
|
|
|
|
lintWorker();
|
|
|
|
lintWorker();
|
|
|
|
lintWorker();
|
|
|
|
lintWorker();
|
|
|
|
lintWorker();
|
|
|
|
lintWorker();
|
|
|
|
lintWorker();
|
|
|
|
}
|
2017-12-15 22:55:51 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Lint specified Markdown files.
|
|
|
|
*
|
2023-01-29 21:13:17 -08:00
|
|
|
* @param {Options | null} options Configuration options.
|
2019-11-10 19:26:55 -08:00
|
|
|
* @param {LintCallback} callback Callback (err, result) function.
|
2017-12-15 22:55:51 -08:00
|
|
|
* @returns {void}
|
|
|
|
*/
|
|
|
|
function markdownlint(options, callback) {
|
|
|
|
return lintInput(options, false, callback);
|
2015-03-20 00:09:55 -07:00
|
|
|
}
|
|
|
|
|
2021-01-05 20:55:09 -08:00
|
|
|
const markdownlintPromisify = promisify && promisify(markdownlint);
|
2020-09-13 12:58:09 -07:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Lint specified Markdown files.
|
|
|
|
*
|
|
|
|
* @param {Options} options Configuration options.
|
|
|
|
* @returns {Promise<LintResults>} Results object.
|
|
|
|
*/
|
|
|
|
function markdownlintPromise(options) {
|
2022-06-11 22:40:45 -07:00
|
|
|
// @ts-ignore
|
2020-09-13 12:58:09 -07:00
|
|
|
return markdownlintPromisify(options);
|
|
|
|
}
|
|
|
|
|
2015-03-20 00:09:55 -07:00
|
|
|
/**
|
2017-05-19 22:36:46 -07:00
|
|
|
* Lint specified Markdown files synchronously.
|
2015-03-20 00:09:55 -07:00
|
|
|
*
|
2023-01-29 21:13:17 -08:00
|
|
|
* @param {Options | null} options Configuration options.
|
2019-11-10 19:26:55 -08:00
|
|
|
* @returns {LintResults} Results object.
|
2015-03-20 00:09:55 -07:00
|
|
|
*/
|
|
|
|
function markdownlintSync(options) {
|
2023-09-04 21:41:16 -07:00
|
|
|
let results = null;
|
2017-12-15 22:55:51 -08:00
|
|
|
lintInput(options, true, function callback(error, res) {
|
2018-02-25 16:04:13 -08:00
|
|
|
if (error) {
|
|
|
|
throw error;
|
|
|
|
}
|
2017-12-15 22:55:51 -08:00
|
|
|
results = res;
|
|
|
|
});
|
2022-06-11 22:40:45 -07:00
|
|
|
// @ts-ignore
|
2017-12-15 22:55:51 -08:00
|
|
|
return results;
|
2015-03-20 00:09:55 -07:00
|
|
|
}
|
|
|
|
|
2021-08-12 20:43:18 -07:00
|
|
|
/**
|
|
|
|
* Resolve referenced "extends" path in a configuration file
|
|
|
|
* using path.resolve() with require.resolve() as a fallback.
|
|
|
|
*
|
|
|
|
* @param {string} configFile Configuration file name.
|
|
|
|
* @param {string} referenceId Referenced identifier to resolve.
|
|
|
|
* @param {Object} fs File system implementation.
|
2022-06-11 22:40:45 -07:00
|
|
|
* @param {ResolveConfigExtendsCallback} callback Callback (err, result)
|
2021-08-12 20:43:18 -07:00
|
|
|
* function.
|
|
|
|
* @returns {void}
|
|
|
|
*/
|
|
|
|
function resolveConfigExtends(configFile, referenceId, fs, callback) {
|
|
|
|
const configFileDirname = path.dirname(configFile);
|
|
|
|
const resolvedExtendsFile = path.resolve(configFileDirname, referenceId);
|
|
|
|
fs.access(resolvedExtendsFile, (err) => {
|
|
|
|
if (err) {
|
|
|
|
// Not a file, try require.resolve
|
|
|
|
try {
|
|
|
|
return callback(null, dynamicRequire.resolve(
|
|
|
|
referenceId,
|
|
|
|
{ "paths": [ configFileDirname ] }
|
|
|
|
));
|
|
|
|
} catch {
|
|
|
|
// Unable to resolve, use resolvedExtendsFile
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return callback(null, resolvedExtendsFile);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2020-10-22 04:40:54 +01:00
|
|
|
/**
|
|
|
|
* Resolve referenced "extends" path in a configuration file
|
2020-10-21 20:53:30 -07:00
|
|
|
* using path.resolve() with require.resolve() as a fallback.
|
2020-10-22 04:40:54 +01:00
|
|
|
*
|
|
|
|
* @param {string} configFile Configuration file name.
|
|
|
|
* @param {string} referenceId Referenced identifier to resolve.
|
2021-08-12 19:38:03 -07:00
|
|
|
* @param {Object} fs File system implementation.
|
2020-10-22 04:40:54 +01:00
|
|
|
* @returns {string} Resolved path to file.
|
|
|
|
*/
|
2021-08-12 20:43:18 -07:00
|
|
|
function resolveConfigExtendsSync(configFile, referenceId, fs) {
|
2020-10-22 04:40:54 +01:00
|
|
|
const configFileDirname = path.dirname(configFile);
|
|
|
|
const resolvedExtendsFile = path.resolve(configFileDirname, referenceId);
|
|
|
|
try {
|
2021-08-12 19:38:03 -07:00
|
|
|
fs.accessSync(resolvedExtendsFile);
|
|
|
|
return resolvedExtendsFile;
|
2020-10-21 20:53:30 -07:00
|
|
|
} catch {
|
2021-08-12 19:38:03 -07:00
|
|
|
// Not a file, try require.resolve
|
2020-10-22 04:40:54 +01:00
|
|
|
}
|
|
|
|
try {
|
2021-02-11 22:16:07 -08:00
|
|
|
return dynamicRequire.resolve(
|
|
|
|
referenceId,
|
|
|
|
{ "paths": [ configFileDirname ] }
|
|
|
|
);
|
2020-10-21 20:53:30 -07:00
|
|
|
} catch {
|
2021-08-12 19:38:03 -07:00
|
|
|
// Unable to resolve, return resolvedExtendsFile
|
2020-10-22 04:40:54 +01:00
|
|
|
}
|
|
|
|
return resolvedExtendsFile;
|
|
|
|
}
|
|
|
|
|
2023-04-03 22:59:06 -07:00
|
|
|
/**
|
|
|
|
* Extend specified configuration object.
|
|
|
|
*
|
|
|
|
* @param {Configuration} config Configuration object.
|
|
|
|
* @param {string} file Configuration file name.
|
|
|
|
* @param {ConfigurationParser[]} parsers Parsing
|
|
|
|
* function(s).
|
|
|
|
* @param {Object} fs File system implementation.
|
|
|
|
* @param {ReadConfigCallback} callback Callback (err, result) function.
|
|
|
|
* @returns {void}
|
|
|
|
*/
|
|
|
|
function extendConfig(config, file, parsers, fs, callback) {
|
|
|
|
const configExtends = config.extends;
|
|
|
|
if (configExtends) {
|
|
|
|
return resolveConfigExtends(
|
|
|
|
file,
|
|
|
|
helpers.expandTildePath(configExtends, require("node:os")),
|
|
|
|
fs,
|
|
|
|
// eslint-disable-next-line no-use-before-define
|
|
|
|
(_, resolvedExtends) => readConfig(
|
|
|
|
// @ts-ignore
|
|
|
|
resolvedExtends,
|
|
|
|
parsers,
|
|
|
|
fs,
|
|
|
|
(err, extendsConfig) => {
|
|
|
|
if (err) {
|
|
|
|
return callback(err);
|
|
|
|
}
|
|
|
|
const result = {
|
|
|
|
...extendsConfig,
|
|
|
|
...config
|
|
|
|
};
|
|
|
|
delete result.extends;
|
|
|
|
return callback(null, result);
|
|
|
|
}
|
|
|
|
)
|
|
|
|
);
|
|
|
|
}
|
|
|
|
return callback(null, config);
|
|
|
|
}
|
|
|
|
|
|
|
|
const extendConfigPromisify = promisify && promisify(extendConfig);
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Extend specified configuration object.
|
|
|
|
*
|
|
|
|
* @param {Configuration} config Configuration object.
|
|
|
|
* @param {string} file Configuration file name.
|
|
|
|
* @param {ConfigurationParser[]} [parsers] Parsing function(s).
|
|
|
|
* @param {Object} [fs] File system implementation.
|
|
|
|
* @returns {Promise<Configuration>} Configuration object.
|
|
|
|
*/
|
|
|
|
function extendConfigPromise(config, file, parsers, fs) {
|
|
|
|
// @ts-ignore
|
|
|
|
return extendConfigPromisify(config, file, parsers, fs);
|
|
|
|
}
|
|
|
|
|
2017-05-19 22:36:46 -07:00
|
|
|
/**
|
|
|
|
* Read specified configuration file.
|
|
|
|
*
|
2019-11-10 19:26:55 -08:00
|
|
|
* @param {string} file Configuration file name.
|
2020-01-11 20:48:00 -08:00
|
|
|
* @param {ConfigurationParser[] | ReadConfigCallback} parsers Parsing
|
2020-01-19 21:01:11 -08:00
|
|
|
* function(s).
|
2021-08-12 19:38:03 -07:00
|
|
|
* @param {Object} [fs] File system implementation.
|
2020-01-11 20:48:00 -08:00
|
|
|
* @param {ReadConfigCallback} [callback] Callback (err, result) function.
|
2017-05-19 22:36:46 -07:00
|
|
|
* @returns {void}
|
|
|
|
*/
|
2021-08-12 19:38:03 -07:00
|
|
|
function readConfig(file, parsers, fs, callback) {
|
2018-05-23 22:24:40 -07:00
|
|
|
if (!callback) {
|
2021-08-12 19:38:03 -07:00
|
|
|
if (fs) {
|
|
|
|
callback = fs;
|
|
|
|
fs = null;
|
|
|
|
} else {
|
|
|
|
// @ts-ignore
|
|
|
|
callback = parsers;
|
2022-06-11 22:40:45 -07:00
|
|
|
// @ts-ignore
|
2021-08-12 19:38:03 -07:00
|
|
|
parsers = null;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (!fs) {
|
2022-08-16 04:01:53 +00:00
|
|
|
fs = require("node:fs");
|
2018-05-23 22:24:40 -07:00
|
|
|
}
|
2017-05-19 22:36:46 -07:00
|
|
|
// Read file
|
2023-04-03 22:59:06 -07:00
|
|
|
file = helpers.expandTildePath(file, require("node:os"));
|
2021-08-05 22:01:29 -07:00
|
|
|
fs.readFile(file, "utf8", (err, content) => {
|
2017-05-19 22:36:46 -07:00
|
|
|
if (err) {
|
2022-06-11 22:40:45 -07:00
|
|
|
// @ts-ignore
|
2017-05-19 22:36:46 -07:00
|
|
|
return callback(err);
|
|
|
|
}
|
2018-05-23 22:24:40 -07:00
|
|
|
// Try to parse file
|
2020-01-23 19:42:46 -08:00
|
|
|
// @ts-ignore
|
2018-05-23 22:24:40 -07:00
|
|
|
const { config, message } = parseConfiguration(file, content, parsers);
|
|
|
|
if (!config) {
|
2022-06-11 22:40:45 -07:00
|
|
|
// @ts-ignore
|
2018-05-23 22:24:40 -07:00
|
|
|
return callback(new Error(message));
|
2017-05-19 22:36:46 -07:00
|
|
|
}
|
2018-05-23 22:24:40 -07:00
|
|
|
// Extend configuration
|
2022-06-11 22:40:45 -07:00
|
|
|
// @ts-ignore
|
2023-04-03 22:59:06 -07:00
|
|
|
return extendConfig(config, file, parsers, fs, callback);
|
2017-05-19 22:36:46 -07:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2021-01-05 20:55:09 -08:00
|
|
|
const readConfigPromisify = promisify && promisify(readConfig);
|
2020-09-13 12:58:09 -07:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Read specified configuration file.
|
|
|
|
*
|
|
|
|
* @param {string} file Configuration file name.
|
|
|
|
* @param {ConfigurationParser[]} [parsers] Parsing function(s).
|
2021-08-12 19:38:03 -07:00
|
|
|
* @param {Object} [fs] File system implementation.
|
2020-09-13 12:58:09 -07:00
|
|
|
* @returns {Promise<Configuration>} Configuration object.
|
|
|
|
*/
|
2021-08-12 19:38:03 -07:00
|
|
|
function readConfigPromise(file, parsers, fs) {
|
2020-09-13 12:58:09 -07:00
|
|
|
// @ts-ignore
|
2021-08-12 19:38:03 -07:00
|
|
|
return readConfigPromisify(file, parsers, fs);
|
2020-09-13 12:58:09 -07:00
|
|
|
}
|
|
|
|
|
2017-05-19 22:36:46 -07:00
|
|
|
/**
|
|
|
|
* Read specified configuration file synchronously.
|
|
|
|
*
|
2019-11-10 19:26:55 -08:00
|
|
|
* @param {string} file Configuration file name.
|
|
|
|
* @param {ConfigurationParser[]} [parsers] Parsing function(s).
|
2021-08-12 19:38:03 -07:00
|
|
|
* @param {Object} [fs] File system implementation.
|
2019-11-10 19:26:55 -08:00
|
|
|
* @returns {Configuration} Configuration object.
|
2021-08-22 18:03:26 -07:00
|
|
|
* @throws An Error if processing fails.
|
2017-05-19 22:36:46 -07:00
|
|
|
*/
|
2021-08-12 19:38:03 -07:00
|
|
|
function readConfigSync(file, parsers, fs) {
|
|
|
|
if (!fs) {
|
2022-08-16 04:01:53 +00:00
|
|
|
fs = require("node:fs");
|
2021-08-12 19:38:03 -07:00
|
|
|
}
|
2018-05-23 22:24:40 -07:00
|
|
|
// Read file
|
2022-08-16 04:01:53 +00:00
|
|
|
const os = require("node:os");
|
2022-05-16 22:57:11 -07:00
|
|
|
file = helpers.expandTildePath(file, os);
|
2021-08-05 22:01:29 -07:00
|
|
|
const content = fs.readFileSync(file, "utf8");
|
2018-05-23 22:24:40 -07:00
|
|
|
// Try to parse file
|
|
|
|
const { config, message } = parseConfiguration(file, content, parsers);
|
|
|
|
if (!config) {
|
|
|
|
throw new Error(message);
|
|
|
|
}
|
|
|
|
// Extend configuration
|
|
|
|
const configExtends = config.extends;
|
|
|
|
if (configExtends) {
|
2017-05-19 22:36:46 -07:00
|
|
|
delete config.extends;
|
2022-05-16 22:57:11 -07:00
|
|
|
const resolvedExtends = resolveConfigExtendsSync(
|
|
|
|
file,
|
|
|
|
helpers.expandTildePath(configExtends, os),
|
|
|
|
fs
|
|
|
|
);
|
2019-05-05 22:27:01 -07:00
|
|
|
return {
|
2021-08-12 19:38:03 -07:00
|
|
|
...readConfigSync(resolvedExtends, parsers, fs),
|
2019-05-05 22:27:01 -07:00
|
|
|
...config
|
|
|
|
};
|
2017-05-19 22:36:46 -07:00
|
|
|
}
|
|
|
|
return config;
|
|
|
|
}
|
|
|
|
|
2020-10-17 14:17:35 -07:00
|
|
|
/**
|
|
|
|
* Gets the (semantic) version of the library.
|
|
|
|
*
|
|
|
|
* @returns {string} SemVer string.
|
|
|
|
*/
|
|
|
|
function getVersion() {
|
2024-09-29 18:11:41 -07:00
|
|
|
return version;
|
2020-10-17 14:17:35 -07:00
|
|
|
}
|
|
|
|
|
2020-09-13 12:58:09 -07:00
|
|
|
// Export a/synchronous/Promise APIs
|
2017-12-15 22:55:51 -08:00
|
|
|
markdownlint.sync = markdownlintSync;
|
|
|
|
markdownlint.readConfig = readConfig;
|
|
|
|
markdownlint.readConfigSync = readConfigSync;
|
2020-10-17 14:17:35 -07:00
|
|
|
markdownlint.getVersion = getVersion;
|
2020-09-13 12:58:09 -07:00
|
|
|
markdownlint.promises = {
|
|
|
|
"markdownlint": markdownlintPromise,
|
2023-04-03 22:59:06 -07:00
|
|
|
"extendConfig": extendConfigPromise,
|
2020-09-13 12:58:09 -07:00
|
|
|
"readConfig": readConfigPromise
|
|
|
|
};
|
2015-03-20 00:09:55 -07:00
|
|
|
module.exports = markdownlint;
|
2019-11-10 19:26:55 -08:00
|
|
|
|
|
|
|
// Type declarations
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Function to implement rule logic.
|
|
|
|
*
|
|
|
|
* @callback RuleFunction
|
|
|
|
* @param {RuleParams} params Rule parameters.
|
|
|
|
* @param {RuleOnError} onError Error-reporting callback.
|
|
|
|
* @returns {void}
|
|
|
|
*/
|
|
|
|
|
2024-03-09 16:17:50 -08:00
|
|
|
/* eslint-disable jsdoc/valid-types */
|
|
|
|
|
2019-11-10 19:26:55 -08:00
|
|
|
/**
|
|
|
|
* Rule parameters.
|
|
|
|
*
|
|
|
|
* @typedef {Object} RuleParams
|
|
|
|
* @property {string} name File/string name.
|
2024-02-27 20:42:09 -08:00
|
|
|
* @property {MarkdownParsers} parsers Markdown parser data.
|
2024-03-09 16:17:50 -08:00
|
|
|
* @property {readonly string[]} lines File/string lines.
|
|
|
|
* @property {readonly string[]} frontMatterLines Front matter lines.
|
2019-11-10 19:26:55 -08:00
|
|
|
* @property {RuleConfiguration} config Rule configuration.
|
2024-09-29 18:11:41 -07:00
|
|
|
* @property {string} version Version of the markdownlint library.
|
2019-11-10 19:26:55 -08:00
|
|
|
*/
|
|
|
|
|
2024-03-09 16:17:50 -08:00
|
|
|
/* eslint-enable jsdoc/valid-types */
|
|
|
|
|
2019-11-10 19:26:55 -08:00
|
|
|
/**
|
2024-02-27 20:42:09 -08:00
|
|
|
* Markdown parser data.
|
|
|
|
*
|
|
|
|
* @typedef {Object} MarkdownParsers
|
2024-03-09 16:17:50 -08:00
|
|
|
* @property {ParserMarkdownIt} markdownit Markdown parser data from markdown-it (only present when Rule.parser is "markdownit").
|
|
|
|
* @property {ParserMicromark} micromark Markdown parser data from micromark (only present when Rule.parser is "micromark").
|
2024-02-27 20:42:09 -08:00
|
|
|
*/
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Markdown parser data from markdown-it.
|
|
|
|
*
|
|
|
|
* @typedef {Object} ParserMarkdownIt
|
|
|
|
* @property {MarkdownItToken[]} tokens Token objects from markdown-it.
|
|
|
|
*/
|
|
|
|
|
2024-03-09 16:17:50 -08:00
|
|
|
/**
|
|
|
|
* Markdown parser data from micromark.
|
|
|
|
*
|
|
|
|
* @typedef {Object} ParserMicromark
|
|
|
|
* @property {MicromarkToken[]} tokens Token objects from micromark.
|
|
|
|
*/
|
|
|
|
|
2024-02-27 20:42:09 -08:00
|
|
|
/**
|
|
|
|
* markdown-it token.
|
2019-11-10 19:26:55 -08:00
|
|
|
*
|
|
|
|
* @typedef {Object} MarkdownItToken
|
|
|
|
* @property {string[][]} attrs HTML attributes.
|
|
|
|
* @property {boolean} block Block-level token.
|
|
|
|
* @property {MarkdownItToken[]} children Child nodes.
|
|
|
|
* @property {string} content Tag contents.
|
|
|
|
* @property {boolean} hidden Ignore element.
|
|
|
|
* @property {string} info Fence info.
|
|
|
|
* @property {number} level Nesting level.
|
|
|
|
* @property {number[]} map Beginning/ending line numbers.
|
|
|
|
* @property {string} markup Markup text.
|
|
|
|
* @property {Object} meta Arbitrary data.
|
|
|
|
* @property {number} nesting Level change.
|
|
|
|
* @property {string} tag HTML tag name.
|
|
|
|
* @property {string} type Token type.
|
2019-11-11 21:09:37 -08:00
|
|
|
* @property {number} lineNumber Line number (1-based).
|
|
|
|
* @property {string} line Line content.
|
2019-11-10 19:26:55 -08:00
|
|
|
*/
|
|
|
|
|
2024-03-09 16:17:50 -08:00
|
|
|
/** @typedef {import("markdownlint-micromark").TokenType} MicromarkTokenType */
|
|
|
|
|
|
|
|
/**
|
|
|
|
* micromark token.
|
|
|
|
*
|
|
|
|
* @typedef {Object} MicromarkToken
|
|
|
|
* @property {MicromarkTokenType} type Token type.
|
|
|
|
* @property {number} startLine Start line (1-based).
|
|
|
|
* @property {number} startColumn Start column (1-based).
|
|
|
|
* @property {number} endLine End line (1-based).
|
|
|
|
* @property {number} endColumn End column (1-based).
|
|
|
|
* @property {string} text Token text.
|
|
|
|
* @property {MicromarkToken[]} children Child tokens.
|
|
|
|
* @property {MicromarkToken | null} parent Parent token.
|
|
|
|
*/
|
|
|
|
|
2019-11-10 19:26:55 -08:00
|
|
|
/**
|
|
|
|
* Error-reporting callback.
|
|
|
|
*
|
|
|
|
* @callback RuleOnError
|
|
|
|
* @param {RuleOnErrorInfo} onErrorInfo Error information.
|
|
|
|
* @returns {void}
|
|
|
|
*/
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Fix information for RuleOnError callback.
|
|
|
|
*
|
|
|
|
* @typedef {Object} RuleOnErrorInfo
|
|
|
|
* @property {number} lineNumber Line number (1-based).
|
2021-09-25 16:23:37 -07:00
|
|
|
* @property {string} [detail] Detail about the error.
|
2019-11-10 19:26:55 -08:00
|
|
|
* @property {string} [context] Context for the error.
|
2023-07-11 21:44:45 -07:00
|
|
|
* @property {URL} [information] Link to more information.
|
2019-11-10 19:26:55 -08:00
|
|
|
* @property {number[]} [range] Column number (1-based) and length.
|
|
|
|
* @property {RuleOnErrorFixInfo} [fixInfo] Fix information.
|
|
|
|
*/
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Fix information for RuleOnErrorInfo.
|
|
|
|
*
|
|
|
|
* @typedef {Object} RuleOnErrorFixInfo
|
|
|
|
* @property {number} [lineNumber] Line number (1-based).
|
|
|
|
* @property {number} [editColumn] Column of the fix (1-based).
|
|
|
|
* @property {number} [deleteCount] Count of characters to delete.
|
|
|
|
* @property {string} [insertText] Text to insert (after deleting).
|
|
|
|
*/
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Rule definition.
|
|
|
|
*
|
|
|
|
* @typedef {Object} Rule
|
|
|
|
* @property {string[]} names Rule name(s).
|
|
|
|
* @property {string} description Rule description.
|
|
|
|
* @property {URL} [information] Link to more information.
|
|
|
|
* @property {string[]} tags Rule tag(s).
|
2024-03-09 16:17:50 -08:00
|
|
|
* @property {"markdownit" | "micromark" | "none"} parser Parser used.
|
2021-12-11 21:44:25 -08:00
|
|
|
* @property {boolean} [asynchronous] True if asynchronous.
|
2019-11-10 19:26:55 -08:00
|
|
|
* @property {RuleFunction} function Rule implementation.
|
|
|
|
*/
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Configuration options.
|
|
|
|
*
|
|
|
|
* @typedef {Object} Options
|
|
|
|
* @property {Configuration} [config] Configuration object.
|
2022-06-05 22:32:22 -07:00
|
|
|
* @property {ConfigurationParser[]} [configParsers] Configuration parsers.
|
2019-11-10 19:26:55 -08:00
|
|
|
* @property {Rule[] | Rule} [customRules] Custom rules.
|
2022-06-05 22:32:22 -07:00
|
|
|
* @property {string[] | string} [files] Files to lint.
|
2023-01-29 21:13:17 -08:00
|
|
|
* @property {RegExp | null} [frontMatter] Front matter pattern.
|
2022-06-05 22:32:22 -07:00
|
|
|
* @property {Object} [fs] File system implementation.
|
2019-11-10 19:26:55 -08:00
|
|
|
* @property {boolean} [handleRuleFailures] True to catch exceptions.
|
2022-06-05 22:32:22 -07:00
|
|
|
* @property {Plugin[]} [markdownItPlugins] Additional plugins.
|
2019-11-10 19:26:55 -08:00
|
|
|
* @property {boolean} [noInlineConfig] True to ignore HTML directives.
|
|
|
|
* @property {number} [resultVersion] Results object version.
|
2022-06-05 22:32:22 -07:00
|
|
|
* @property {Object.<string, string>} [strings] Strings to lint.
|
2019-11-10 19:26:55 -08:00
|
|
|
*/
|
|
|
|
|
|
|
|
/**
|
2020-11-24 16:25:08 -08:00
|
|
|
* A markdown-it plugin.
|
2019-11-10 19:26:55 -08:00
|
|
|
*
|
|
|
|
* @typedef {Array} Plugin
|
|
|
|
*/
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Function to pretty-print lint results.
|
|
|
|
*
|
|
|
|
* @callback ToStringCallback
|
|
|
|
* @param {boolean} [ruleAliases] True to use rule aliases.
|
|
|
|
* @returns {string}
|
|
|
|
*/
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Lint results (for resultVersion 3).
|
|
|
|
*
|
|
|
|
* @typedef {Object.<string, LintError[]>} LintResults
|
2020-09-13 12:58:09 -07:00
|
|
|
* @property {ToStringCallback} toString String representation.
|
2019-11-10 19:26:55 -08:00
|
|
|
*/
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Lint error.
|
|
|
|
*
|
|
|
|
* @typedef {Object} LintError
|
|
|
|
* @property {number} lineNumber Line number (1-based).
|
|
|
|
* @property {string[]} ruleNames Rule name(s).
|
|
|
|
* @property {string} ruleDescription Rule description.
|
|
|
|
* @property {string} ruleInformation Link to more information.
|
|
|
|
* @property {string} errorDetail Detail about the error.
|
|
|
|
* @property {string} errorContext Context for the error.
|
|
|
|
* @property {number[]} errorRange Column number (1-based) and length.
|
2021-01-10 20:46:00 -08:00
|
|
|
* @property {FixInfo} [fixInfo] Fix information.
|
2019-11-10 19:26:55 -08:00
|
|
|
*/
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Fix information.
|
|
|
|
*
|
|
|
|
* @typedef {Object} FixInfo
|
2021-06-14 22:30:35 -07:00
|
|
|
* @property {number} [lineNumber] Line number (1-based).
|
2019-11-10 19:26:55 -08:00
|
|
|
* @property {number} [editColumn] Column of the fix (1-based).
|
|
|
|
* @property {number} [deleteCount] Count of characters to delete.
|
|
|
|
* @property {string} [insertText] Text to insert (after deleting).
|
|
|
|
*/
|
|
|
|
|
2023-09-04 21:41:16 -07:00
|
|
|
/**
|
|
|
|
* Called with the result of linting a string or document.
|
|
|
|
*
|
|
|
|
* @callback LintContentCallback
|
|
|
|
* @param {Error | null} error Error iff failed.
|
|
|
|
* @param {LintError[]} [result] Result iff successful.
|
|
|
|
* @returns {void}
|
|
|
|
*/
|
|
|
|
|
2019-11-10 19:26:55 -08:00
|
|
|
/**
|
2021-08-12 20:43:18 -07:00
|
|
|
* Called with the result of the lint function.
|
2019-11-10 19:26:55 -08:00
|
|
|
*
|
|
|
|
* @callback LintCallback
|
2023-09-04 21:41:16 -07:00
|
|
|
* @param {Error | null} error Error object iff failed.
|
|
|
|
* @param {LintResults} [results] Lint results iff succeeded.
|
2019-11-10 19:26:55 -08:00
|
|
|
* @returns {void}
|
|
|
|
*/
|
|
|
|
|
|
|
|
/**
|
2023-11-08 19:49:02 -08:00
|
|
|
* Configuration object for linting rules. For the JSON schema, see
|
2019-11-10 19:26:55 -08:00
|
|
|
* {@link ../schema/markdownlint-config-schema.json}.
|
|
|
|
*
|
2023-11-08 19:49:02 -08:00
|
|
|
* @typedef {import("./configuration").Configuration} Configuration
|
2019-11-10 19:26:55 -08:00
|
|
|
*/
|
|
|
|
|
2024-08-27 20:47:33 -07:00
|
|
|
/**
|
|
|
|
* Configuration object for linting rules strictly. For the JSON schema, see
|
|
|
|
* {@link ../schema/markdownlint-config-schema-strict.json}.
|
|
|
|
*
|
|
|
|
* @typedef {import("./configuration-strict").ConfigurationStrict} ConfigurationStrict
|
|
|
|
*/
|
|
|
|
|
2019-11-10 19:26:55 -08:00
|
|
|
/**
|
|
|
|
* Rule configuration object.
|
|
|
|
*
|
|
|
|
* @typedef {boolean | Object} RuleConfiguration Rule configuration.
|
|
|
|
*/
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Parses a configuration string and returns a configuration object.
|
|
|
|
*
|
|
|
|
* @callback ConfigurationParser
|
|
|
|
* @param {string} text Configuration string.
|
|
|
|
* @returns {Configuration}
|
|
|
|
*/
|
|
|
|
|
|
|
|
/**
|
2021-08-12 20:43:18 -07:00
|
|
|
* Called with the result of the readConfig function.
|
2019-11-10 19:26:55 -08:00
|
|
|
*
|
|
|
|
* @callback ReadConfigCallback
|
|
|
|
* @param {Error | null} err Error object or null.
|
|
|
|
* @param {Configuration} [config] Configuration object.
|
|
|
|
* @returns {void}
|
|
|
|
*/
|
2021-08-12 20:43:18 -07:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Called with the result of the resolveConfigExtends function.
|
|
|
|
*
|
|
|
|
* @callback ResolveConfigExtendsCallback
|
|
|
|
* @param {Error | null} err Error object or null.
|
|
|
|
* @param {string} [path] Resolved path to file.
|
|
|
|
* @returns {void}
|
|
|
|
*/
|