Add "enabled" and "severity"/"warning" to rule in Configuration object, add corresponding documentation and tests, update demo web app (fixes #254)”.
Some checks failed
Checkers / linkcheck (push) Has been cancelled
Checkers / spellcheck (push) Has been cancelled
CI / build (20, macos-latest) (push) Has been cancelled
CI / build (20, ubuntu-latest) (push) Has been cancelled
CI / build (20, windows-latest) (push) Has been cancelled
CI / build (22, macos-latest) (push) Has been cancelled
CI / build (22, ubuntu-latest) (push) Has been cancelled
CI / build (22, windows-latest) (push) Has been cancelled
CI / build (24, macos-latest) (push) Has been cancelled
CI / build (24, ubuntu-latest) (push) Has been cancelled
CI / build (24, windows-latest) (push) Has been cancelled
CI / pnpm (push) Has been cancelled
CodeQL / Analyze (push) Has been cancelled
TestRepos / build (latest, ubuntu-latest) (push) Has been cancelled
UpdateTestRepos / update (push) Has been cancelled

This commit is contained in:
David Anson 2025-09-22 21:39:09 -07:00
parent 87ddc2e022
commit 0f5a8c701e
20 changed files with 3792 additions and 867 deletions

View file

@ -236,44 +236,87 @@ function mapAliasToRuleNames(ruleList) {
return aliasToRuleNames;
}
/**
* Result object for getEffectiveConfig.
*
* @typedef {Object} GetEffectiveConfigResult
* @property {Configuration} effectiveConfig Effective configuration.
* @property {Map<string, boolean>} rulesEnabled Rules enabled.
* @property {Map<string, "error" | "warning">} rulesSeverity Rules severity.
*/
/**
* 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.
* @param {Object.<string, string[]>} aliasToRuleNames Map of alias to rule names.
* @returns {GetEffectiveConfigResult} Effective configuration and rule severities.
*/
function getEffectiveConfig(ruleList, config, aliasToRuleNames) {
const defaultKey = Object.keys(config).filter(
(key) => key.toUpperCase() === "DEFAULT"
);
const ruleDefault = (defaultKey.length === 0) || !!config[defaultKey[0]];
let ruleDefaultEnable = true;
/** @type {"error" | "warning"} */
let ruleDefaultSeverity = "error";
Object.entries(config).every(([ key, value ]) => {
if (key.toUpperCase() === "DEFAULT") {
ruleDefaultEnable = !!value;
if (value === "warning") {
ruleDefaultSeverity = "warning";
}
return false;
}
return true;
});
/** @type {Configuration} */
const effectiveConfig = {};
for (const rule of ruleList) {
const ruleName = rule.names[0].toUpperCase();
effectiveConfig[ruleName] = ruleDefault;
/** @type {Map<string, boolean>} */
const rulesEnabled = new Map();
/** @type {Map<string, "error" | "warning">} */
const rulesSeverity = new Map();
const emptyObject = Object.freeze({});
for (const ruleName of ruleList.map((rule) => rule.names[0].toUpperCase())) {
effectiveConfig[ruleName] = emptyObject;
rulesEnabled.set(ruleName, ruleDefaultEnable);
rulesSeverity.set(ruleName, ruleDefaultSeverity);
}
// for (const ruleName of deprecatedRuleNames) {
// effectiveConfig[ruleName] = false;
// }
for (const key of Object.keys(config)) {
let value = config[key];
if (value) {
value = (value instanceof Object) ?
Object.fromEntries(Object.entries(value).filter(([ k ]) => k !== "severity")) :
{};
} else {
value = false;
}
for (const [ key, value ] of Object.entries(config)) {
const keyUpper = key.toUpperCase();
/** @type {boolean} */
let enabled = false;
/** @type {"error" | "warning"} */
let severity = "error";
let effectiveValue = {};
if (value) {
if (value instanceof Object) {
/** @type {{ enabled?: boolean, severity?: "error" | "warning" }} */
const valueObject = value;
enabled = (valueObject.enabled === undefined) ? true : !!valueObject.enabled;
severity = (valueObject.severity === "warning") ? "warning" : "error";
effectiveValue = Object.fromEntries(
Object.entries(value).filter(
([ k ]) => (k !== "enabled") && (k !== "severity")
)
);
} else {
enabled = true;
severity = (value === "warning") ? "warning" : "error";
}
}
for (const ruleName of (aliasToRuleNames[keyUpper] || [])) {
effectiveConfig[ruleName] = value;
Object.freeze(effectiveValue);
effectiveConfig[ruleName] = effectiveValue;
rulesEnabled.set(ruleName, enabled);
rulesSeverity.set(ruleName, severity);
}
}
return effectiveConfig;
return {
effectiveConfig,
rulesEnabled,
rulesSeverity
};
}
/**
@ -281,8 +324,9 @@ function getEffectiveConfig(ruleList, config, aliasToRuleNames) {
*
* @typedef {Object} EnabledRulesPerLineNumberResult
* @property {Configuration} effectiveConfig Effective configuration.
* @property {any[]} enabledRulesPerLineNumber Enabled rules per line number.
* @property {Map<string, boolean>[]} enabledRulesPerLineNumber Enabled rules per line number.
* @property {Rule[]} enabledRuleList Enabled rule list.
* @property {Map<string, "error" | "warning">} rulesSeverity Rules severity.
*/
/**
@ -294,8 +338,7 @@ function getEffectiveConfig(ruleList, config, aliasToRuleNames) {
* @param {boolean} noInlineConfig Whether to allow inline configuration.
* @param {Configuration} config Configuration object.
* @param {ConfigurationParser[] | undefined} configParsers Configuration parsers.
* @param {Object.<string, string[]>} aliasToRuleNames Map of alias to rule
* names.
* @param {Object.<string, string[]>} aliasToRuleNames Map of alias to rule names.
* @returns {EnabledRulesPerLineNumberResult} Effective configuration and enabled rules per line number.
*/
function getEnabledRulesPerLineNumber(
@ -307,9 +350,10 @@ function getEnabledRulesPerLineNumber(
configParsers,
aliasToRuleNames) {
// Shared variables
let enabledRules = {};
let capturedRules = {};
const allRuleNames = [];
/** @type {Map<string, boolean>} */
let enabledRules = new Map();
/** @type {Map<string, boolean>} */
let capturedRules = enabledRules;
const enabledRulesPerLineNumber = new Array(1 + frontMatterLines.length);
// Helper functions
// eslint-disable-next-line jsdoc/require-jsdoc
@ -349,13 +393,14 @@ function getEnabledRulesPerLineNumber(
}
// eslint-disable-next-line jsdoc/require-jsdoc
function applyEnableDisable(action, parameter, state) {
state = { ...state };
state = new Map(state);
const enabled = (action.startsWith("ENABLE"));
const trimmed = parameter && parameter.trim();
// eslint-disable-next-line no-use-before-define
const items = trimmed ? trimmed.toUpperCase().split(/\s+/) : allRuleNames;
for (const nameUpper of items) {
for (const ruleName of (aliasToRuleNames[nameUpper] || [])) {
state[ruleName] = enabled;
state.set(ruleName, enabled);
}
}
return state;
@ -397,29 +442,24 @@ function getEnabledRulesPerLineNumber(
}
// Handle inline comments
handleInlineConfig([ lines.join("\n") ], configureFile);
const effectiveConfig = getEffectiveConfig(
ruleList, config, aliasToRuleNames);
for (const rule of ruleList) {
const ruleName = rule.names[0].toUpperCase();
allRuleNames.push(ruleName);
enabledRules[ruleName] = !!effectiveConfig[ruleName];
}
const { effectiveConfig, rulesEnabled, rulesSeverity } = getEffectiveConfig(ruleList, config, aliasToRuleNames);
const allRuleNames = [ ...rulesEnabled.keys() ];
enabledRules = new Map(rulesEnabled);
capturedRules = enabledRules;
handleInlineConfig(lines, enableDisableFile);
handleInlineConfig(lines, captureRestoreEnableDisable, updateLineState);
handleInlineConfig(lines, disableLineNextLine);
// 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]);
}
}
const enabledRuleList = ruleList.filter((rule) => {
const ruleName = rule.names[0].toUpperCase();
return enabledRulesPerLineNumber.some((enabledRulesForLine) => enabledRulesForLine.get(ruleName));
});
// Return results
return {
effectiveConfig,
enabledRulesPerLineNumber,
enabledRuleList
enabledRuleList,
rulesSeverity
};
}
@ -464,7 +504,7 @@ function lintContent(
const { frontMatterLines } = removeFrontMatterResult;
content = removeFrontMatterResult.content;
// Get enabled rules per line (with HTML comments present)
const { effectiveConfig, enabledRulesPerLineNumber, enabledRuleList } =
const { effectiveConfig, enabledRulesPerLineNumber, enabledRuleList, rulesSeverity } =
getEnabledRulesPerLineNumber(
ruleList,
content.split(helpers.newLineRe),
@ -541,12 +581,12 @@ function lintContent(
} else if (rule.parser === "micromark") {
parsers = parsersMicromark;
}
const params = {
const params = Object.freeze({
...paramsBase,
...tokens,
parsers,
"config": (typeof effectiveConfig[ruleName] === "object") ? { ...effectiveConfig[ruleName] } : effectiveConfig[ruleName]
};
"config": effectiveConfig[ruleName]
});
// eslint-disable-next-line jsdoc/require-jsdoc
function throwError(property) {
throw new Error(
@ -561,7 +601,7 @@ function lintContent(
throwError("lineNumber");
}
const lineNumber = errorInfo.lineNumber + frontMatterLines.length;
if (!enabledRulesPerLineNumber[lineNumber][ruleName]) {
if (!enabledRulesPerLineNumber[lineNumber].get(ruleName)) {
return;
}
if (errorInfo.detail &&
@ -638,7 +678,8 @@ function lintContent(
"errorContext": errorInfo.context?.replace(helpers.newLineRe, " ") || null,
"errorRange": errorInfo.range ? [ ...errorInfo.range ] : null,
"fixInfo": fixInfo ? cleanFixInfo : null,
"severity": "error"
// @ts-ignore
"severity": rulesSeverity.get(ruleName)
});
}
// Call (possibly external) rule function to report errors