Deprecate options.resultVersion via typing, continue to support at run-time, provide format converters in markdownlint/helpers for migration.
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-08-28 22:01:06 -07:00
parent 5729b279c2
commit bfd89b77de
9 changed files with 191 additions and 74 deletions

View file

@ -620,28 +620,15 @@ By default, properly-formatted inline comments can be used to create exceptions
for parts of a document. Setting `noInlineConfig` to `true` ignores all such for parts of a document. Setting `noInlineConfig` to `true` ignores all such
comments. comments.
##### options.resultVersion ##### ~~options.resultVersion~~
Type: `Number` This property is *deprecated* and should be removed. The default format of the
`result` object remains the same as setting `resultVersion` to `3`. For
continued access to other (previously *deprecated*) formats:
Specifies which version of the `result` object to return (see the "Usage" ```javascript
section below for examples). import { convertToResultVersion0, convertToResultVersion1, convertToResultVersion2 } from "markdownlint/helpers";
```
Passing a `resultVersion` of `0` corresponds to the original, simple format
where each error is identified by rule name and line number. *Deprecated*
Passing a `resultVersion` of `1` corresponds to a detailed format where each
error includes information about the line number, rule name, alias, description,
as well as any additional detail or context that is available. *Deprecated*
Passing a `resultVersion` of `2` corresponds to a detailed format where each
error includes information about the line number, rule names, description, as
well as any additional detail or context that is available. *Deprecated*
Passing a `resultVersion` of `3` corresponds to the detailed version `2` format
with additional information about how to fix automatically-fixable errors. In
this mode, all errors that occur on each line are reported (other versions
report only the first error for each rule). This is the default behavior.
##### options.strings ##### options.strings

View file

@ -538,3 +538,111 @@ function expandTildePath(file, os) {
return homedir ? file.replace(/^~($|\/|\\)/, `${homedir}$1`) : file; return homedir ? file.replace(/^~($|\/|\\)/, `${homedir}$1`) : file;
} }
module.exports.expandTildePath = expandTildePath; module.exports.expandTildePath = expandTildePath;
/** @typedef {import("../lib/markdownlint.mjs").LintError[]} LintErrors */
/** @typedef {import("../lib/markdownlint.mjs").LintResults} LintResults */
/**
* Converts lint errors from resultVersion 3 to 2.
*
* @param {LintErrors} errors Lint errors (v3).
* @returns {LintErrors} Lint errors (v2).
*/
function convertLintErrorsVersion3To2(errors) {
const noPrevious = {
"ruleNames": [],
"lineNumber": -1
};
return errors.filter((error, index, array) => {
delete error.fixInfo;
const previous = array[index - 1] || noPrevious;
return (
(error.ruleNames[0] !== previous.ruleNames[0]) ||
(error.lineNumber !== previous.lineNumber)
);
});
}
/**
* Converts lint errors from resultVersion 2 to 1.
*
* @param {LintErrors} errors Lint errors (v2).
* @returns {LintErrors} Lint errors (v1).
*/
function convertLintErrorsVersion2To1(errors) {
for (const error of errors) {
// @ts-ignore
error.ruleName = error.ruleNames[0];
// @ts-ignore
error.ruleAlias = error.ruleNames[1] || error.ruleName;
// @ts-ignore
delete error.ruleNames;
}
return errors;
}
/**
* Converts lint errors from resultVersion 2 to 0.
*
* @param {LintErrors} errors Lint errors (v2).
* @returns {LintErrors} Lint errors (v0).
*/
function convertLintErrorsVersion2To0(errors) {
const dictionary = {};
for (const error of errors) {
const ruleName = error.ruleNames[0];
const ruleLines = dictionary[ruleName] || [];
ruleLines.push(error.lineNumber);
dictionary[ruleName] = ruleLines;
}
// @ts-ignore
return dictionary;
}
/**
* Copies and transforms lint results from resultVersion 3 to ?.
*
* @param {LintResults} results Lint results (v3).
* @param {(LintErrors) => LintErrors} transform Lint errors (v?).
* @returns {LintResults} Lint results (v?).
*/
function copyAndTransformResults(results, transform) {
const newResults = {};
Object.defineProperty(newResults, "toString", { "value": results.toString });
for (const key of Object.keys(results)) {
const arr = results[key].map((r) => ({ ...r }));
newResults[key] = transform(arr);
}
// @ts-ignore
return newResults;
}
/**
* Converts lint results from resultVersion 3 to 0.
*
* @param {LintResults} results Lint results (v3).
* @returns {LintResults} Lint results (v0).
*/
module.exports.convertToResultVersion0 = function convertToResultVersion0(results) {
return copyAndTransformResults(results, (r) => convertLintErrorsVersion2To0(convertLintErrorsVersion3To2(r)));
};
/**
* Converts lint results from resultVersion 3 to 1.
*
* @param {LintResults} results Lint results (v3).
* @returns {LintResults} Lint results (v1).
*/
module.exports.convertToResultVersion1 = function convertToResultVersion1(results) {
return copyAndTransformResults(results, (r) => convertLintErrorsVersion2To1(convertLintErrorsVersion3To2(r)));
};
/**
* Converts lint results from resultVersion 3 to 2.
*
* @param {LintResults} results Lint results (v3).
* @returns {LintResults} Lint results (v2).
*/
module.exports.convertToResultVersion2 = function convertToResultVersion2(results) {
return copyAndTransformResults(results, convertLintErrorsVersion3To2);
};

View file

@ -431,10 +431,6 @@ export type Options = {
* True to ignore HTML directives. * True to ignore HTML directives.
*/ */
noInlineConfig?: boolean; noInlineConfig?: boolean;
/**
* Results object version.
*/
resultVersion?: number;
/** /**
* Strings to lint. * Strings to lint.
*/ */
@ -451,7 +447,7 @@ export type Plugin = any[];
*/ */
export type ToStringCallback = (ruleAliases?: boolean) => string; export type ToStringCallback = (ruleAliases?: boolean) => string;
/** /**
* Lint results (for resultVersion 3). * Lint results.
*/ */
export type LintResults = { export type LintResults = {
[x: string]: LintError[]; [x: string]: LintError[];

View file

@ -108,9 +108,15 @@ function validateRuleList(ruleList, synchronous) {
* @returns {LintResults} New LintResults instance. * @returns {LintResults} New LintResults instance.
*/ */
function newResults(ruleList) { function newResults(ruleList) {
const lintResults = {}; /**
// eslint-disable-next-line jsdoc/require-jsdoc * Returns the string representation of a LintResults instance.
*
* @param {boolean} useAlias True if rule alias should be used instead of name.
* @returns {string} String representation of the instance.
*/
function toString(useAlias) { function toString(useAlias) {
// eslint-disable-next-line consistent-this, no-invalid-this, unicorn/no-this-assignment
const lintResults = this;
let ruleNameToRule = null; let ruleNameToRule = null;
const results = []; const results = [];
const keys = Object.keys(lintResults); const keys = Object.keys(lintResults);
@ -161,6 +167,7 @@ function newResults(ruleList) {
} }
return results.join("\n"); return results.join("\n");
} }
const lintResults = {};
Object.defineProperty(lintResults, "toString", { "value": toString }); Object.defineProperty(lintResults, "toString", { "value": toString });
// @ts-ignore // @ts-ignore
return lintResults; return lintResults;
@ -515,7 +522,7 @@ function lintContent(
"config": null "config": null
}); });
// Function to run for each rule // Function to run for each rule
let results = []; const results = [];
/** /**
* @param {Rule} rule Rule. * @param {Rule} rule Rule.
* @returns {Promise<void> | null} Promise. * @returns {Promise<void> | null} Promise.
@ -623,7 +630,6 @@ function lintContent(
const information = errorInfo.information || rule.information; const information = errorInfo.information || rule.information;
results.push({ results.push({
lineNumber, lineNumber,
"ruleName": rule.names[0],
"ruleNames": rule.names, "ruleNames": rule.names,
"ruleDescription": rule.description, "ruleDescription": rule.description,
"ruleInformation": information ? information.href : null, "ruleInformation": information ? information.href : null,
@ -662,46 +668,9 @@ function lintContent(
const formatResults = () => { const formatResults = () => {
// Sort results by rule name by line number // Sort results by rule name by line number
results.sort((a, b) => ( results.sort((a, b) => (
a.ruleName.localeCompare(b.ruleName) || a.ruleNames[0].localeCompare(b.ruleNames[0]) ||
a.lineNumber - b.lineNumber 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 results; return results;
}; };
// Run all rules // Run all rules
@ -854,9 +823,8 @@ function lintInput(options, synchronous, callback) {
options.frontMatter; options.frontMatter;
const handleRuleFailures = !!options.handleRuleFailures; const handleRuleFailures = !!options.handleRuleFailures;
const noInlineConfig = !!options.noInlineConfig; const noInlineConfig = !!options.noInlineConfig;
const resultVersion = (options.resultVersion === undefined) ? // eslint-disable-next-line dot-notation
3 : const resultVersion = (options["resultVersion"] === undefined) ? 3 : options["resultVersion"];
options.resultVersion;
const markdownItFactory = const markdownItFactory =
options.markdownItFactory || options.markdownItFactory ||
(() => { throw new Error("The option 'markdownItFactory' was required (due to the option 'customRules' including a rule requiring the 'markdown-it' parser), but 'markdownItFactory' was not set."); }); (() => { throw new Error("The option 'markdownItFactory' was required (due to the option 'customRules' including a rule requiring the 'markdown-it' parser), but 'markdownItFactory' was not set."); });
@ -923,7 +891,16 @@ function lintInput(options, synchronous, callback) {
} else if (concurrency === 0) { } else if (concurrency === 0) {
// Finish // Finish
done = true; done = true;
return callback(null, results); // Deprecated: Convert results to specified resultVersion
let convertedResults = results;
if (resultVersion === 0) {
convertedResults = helpers.convertToResultVersion0(results);
} else if (resultVersion === 1) {
convertedResults = helpers.convertToResultVersion1(results);
} else if (resultVersion === 2) {
convertedResults = helpers.convertToResultVersion2(results);
}
return callback(null, convertedResults);
} }
return null; return null;
} }
@ -1506,7 +1483,6 @@ export function getVersion() {
* @property {boolean} [handleRuleFailures] True to catch exceptions. * @property {boolean} [handleRuleFailures] True to catch exceptions.
* @property {MarkdownItFactory} [markdownItFactory] Function to create a markdown-it parser. * @property {MarkdownItFactory} [markdownItFactory] Function to create a markdown-it parser.
* @property {boolean} [noInlineConfig] True to ignore HTML directives. * @property {boolean} [noInlineConfig] True to ignore HTML directives.
* @property {number} [resultVersion] Results object version.
* @property {Object.<string, string>} [strings] Strings to lint. * @property {Object.<string, string>} [strings] Strings to lint.
*/ */
@ -1525,7 +1501,7 @@ export function getVersion() {
*/ */
/** /**
* Lint results (for resultVersion 3). * Lint results.
* *
* @typedef {Object.<string, LintError[]>} LintResults * @typedef {Object.<string, LintError[]>} LintResults
* @property {ToStringCallback} toString String representation. * @property {ToStringCallback} toString String representation.

View file

@ -24,6 +24,7 @@ test("customRulesV0", (t) => new Promise((resolve) => {
"customRules": customRules.all, "customRules": customRules.all,
"files": [ customRulesMd ], "files": [ customRulesMd ],
markdownItFactory, markdownItFactory,
// @ts-ignore
"resultVersion": 0 "resultVersion": 0
}; };
lintAsync(options, function callback(err, actualResult) { lintAsync(options, function callback(err, actualResult) {
@ -97,6 +98,7 @@ test("customRulesV1", (t) => new Promise((resolve) => {
"customRules": customRules.all, "customRules": customRules.all,
"files": [ customRulesMd ], "files": [ customRulesMd ],
markdownItFactory, markdownItFactory,
// @ts-ignore
"resultVersion": 1 "resultVersion": 1
}; };
lintAsync(options, function callback(err, actualResult) { lintAsync(options, function callback(err, actualResult) {
@ -229,6 +231,7 @@ test("customRulesV2", (t) => new Promise((resolve) => {
"customRules": customRules.all, "customRules": customRules.all,
"files": [ customRulesMd ], "files": [ customRulesMd ],
markdownItFactory, markdownItFactory,
// @ts-ignore
"resultVersion": 2 "resultVersion": 2
}; };
lintAsync(options, function callback(err, actualResult) { lintAsync(options, function callback(err, actualResult) {
@ -358,6 +361,7 @@ test("customRulesConfig", (t) => new Promise((resolve) => {
"letters-e-x": false "letters-e-x": false
}, },
markdownItFactory, markdownItFactory,
// @ts-ignore
"resultVersion": 0 "resultVersion": 0
}; };
lintAsync(options, function callback(err, actualResult) { lintAsync(options, function callback(err, actualResult) {
@ -387,6 +391,7 @@ test("customRulesNpmPackage", (t) => new Promise((resolve) => {
"strings": { "strings": {
"string": "# Text\n\n---\n\nText ✅\n" "string": "# Text\n\n---\n\nText ✅\n"
}, },
// @ts-ignore
"resultVersion": 0 "resultVersion": 0
}; };
lintAsync(options, function callback(err, actualResult) { lintAsync(options, function callback(err, actualResult) {

View file

@ -2,7 +2,9 @@
import test from "ava"; import test from "ava";
import { lint as lintAsync } from "markdownlint/async"; import { lint as lintAsync } from "markdownlint/async";
import { lint as lintPromise } from "markdownlint/promise";
import { lint as lintSync } from "markdownlint/sync"; import { lint as lintSync } from "markdownlint/sync";
import { convertToResultVersion0, convertToResultVersion1, convertToResultVersion2 } from "markdownlint/helpers";
import { importWithTypeJson } from "./esm-helpers.mjs"; import { importWithTypeJson } from "./esm-helpers.mjs";
const packageJson = await importWithTypeJson(import.meta, "../package.json"); const packageJson = await importWithTypeJson(import.meta, "../package.json");
const { homepage, version } = packageJson; const { homepage, version } = packageJson;
@ -623,3 +625,40 @@ test("frontMatterResultVersion3", (t) => new Promise((resolve) => {
resolve(); resolve();
}); });
})); }));
test("convertToResultVersionN", async(t) => {
t.plan(8);
const options = {
"files": [
"./test/break-all-the-rules.md",
"./test/inline-disable-enable.md"
],
"strings": {
"first": "# Heading",
"second": "## Heading"
}
};
const [ base, version3, version2, version1, version0 ] = await Promise.all([
lintPromise(options),
// @ts-ignore
lintPromise({ ...options, "resultVersion": 3 }),
// @ts-ignore
lintPromise({ ...options, "resultVersion": 2 }),
// @ts-ignore
lintPromise({ ...options, "resultVersion": 1 }),
// @ts-ignore
lintPromise({ ...options, "resultVersion": 0 })
]);
const v3 = version3;
t.deepEqual(v3, base);
t.is(v3.toString(), base.toString());
const v2 = convertToResultVersion2(base);
t.deepEqual(v2, version2);
t.is(v2.toString(), version2.toString());
const v1 = convertToResultVersion1(base);
t.deepEqual(v1, version1);
t.is(v1.toString(), version1.toString());
const v0 = convertToResultVersion0(base);
t.deepEqual(v0, version0);
t.is(v0.toString(), version0.toString());
});

View file

@ -546,6 +546,7 @@ test("nullFrontMatter", (t) => new Promise((resolve) => {
"default": false, "default": false,
"MD010": true "MD010": true
}, },
// @ts-ignore
"resultVersion": 0 "resultVersion": 0
}, function callback(err, result) { }, function callback(err, result) {
t.falsy(err); t.falsy(err);
@ -598,6 +599,7 @@ test("noInlineConfig", (t) => new Promise((resolve) => {
].join("\n") ].join("\n")
}, },
"noInlineConfig": true, "noInlineConfig": true,
// @ts-ignore
"resultVersion": 0 "resultVersion": 0
}, function callback(err, result) { }, function callback(err, result) {
t.falsy(err); t.falsy(err);
@ -646,7 +648,7 @@ test("readmeHeadings", (t) => new Promise((resolve) => {
"##### options.handleRuleFailures", "##### options.handleRuleFailures",
"##### options.markdownItFactory", "##### options.markdownItFactory",
"##### options.noInlineConfig", "##### options.noInlineConfig",
"##### options.resultVersion", "##### ~~options.resultVersion~~",
"##### options.strings", "##### options.strings",
"#### callback", "#### callback",
"#### result", "#### result",
@ -1229,6 +1231,7 @@ Text with: [^footnote]
[reference]: https://example.com [reference]: https://example.com
` `
}, },
// @ts-ignore
"resultVersion": 0 "resultVersion": 0
}, (err, actual) => { }, (err, actual) => {
t.falsy(err); t.falsy(err);

View file

@ -28,6 +28,9 @@ Generated by [AVA](https://avajs.dev).
'clearHtmlCommentText', 'clearHtmlCommentText',
'cloneIfArray', 'cloneIfArray',
'cloneIfUrl', 'cloneIfUrl',
'convertToResultVersion0',
'convertToResultVersion1',
'convertToResultVersion2',
'default', 'default',
'ellipsify', 'ellipsify',
'endOfLineGemojiCodeRe', 'endOfLineGemojiCodeRe',