mirror of
https://github.com/DavidAnson/markdownlint.git
synced 2025-12-17 06:20:12 +01:00
Add infrastructure for rules to include fix information when logging violations, update MD047 (refs #80).
This commit is contained in:
parent
6e086114b1
commit
cdd87e647f
6 changed files with 248 additions and 19 deletions
10
README.md
10
README.md
|
|
@ -416,15 +416,19 @@ Specifies which version of the `result` object to return (see the "Usage" sectio
|
||||||
below for examples).
|
below for examples).
|
||||||
|
|
||||||
Passing a `resultVersion` of `0` corresponds to the original, simple format where
|
Passing a `resultVersion` of `0` corresponds to the original, simple format where
|
||||||
each error is identified by rule name and line number. This is deprecated.
|
each error is identified by rule name and line number. *This is deprecated.*
|
||||||
|
|
||||||
Passing a `resultVersion` of `1` corresponds to a detailed format where each error
|
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
|
includes information about the line number, rule name, alias, description, as well
|
||||||
as any additional detail or context that is available. This is deprecated.
|
as any additional detail or context that is available. *This is deprecated.*
|
||||||
|
|
||||||
Passing a `resultVersion` of `2` corresponds to a detailed format where each error
|
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
|
includes information about the line number, rule names, description, as well as any
|
||||||
additional detail or context that is available. This is the default.
|
additional detail or context that is available. *This is the default.*
|
||||||
|
|
||||||
|
Passing a `resultVersion` of `3` corresponds to the detailed version `2` format
|
||||||
|
with additional information about fixes for certain errors. All errors for each
|
||||||
|
line are included (other versions collapse multiple errors for the same rule).
|
||||||
|
|
||||||
##### options.markdownItPlugins
|
##### options.markdownItPlugins
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -52,6 +52,10 @@ A rule is implemented as an `Object` with four required properties:
|
||||||
- `details` is an optional `String` with information about what caused the error.
|
- `details` is an optional `String` with information about what caused the error.
|
||||||
- `context` is an optional `String` with relevant text surrounding the error location.
|
- `context` is an optional `String` with relevant text surrounding the error location.
|
||||||
- `range` is an optional `Array` with two `Number` values identifying the 1-based column and length of the error.
|
- `range` is an optional `Array` with two `Number` values identifying the 1-based column and length of the error.
|
||||||
|
- `fixInfo` is an optional `Object` with information about how to fix the error:
|
||||||
|
- `editColumn` is an optional `Number` specifying the 1-based column number of the edit.
|
||||||
|
- `deleteCount` is an optional `Number` specifying the count of characters to delete with the edit.
|
||||||
|
- `insertText` is an optional `String` specifying text to insert as part of the edit.
|
||||||
|
|
||||||
The collection of helper functions shared by the built-in rules is available for use by custom rules in the [markdownlint-rule-helpers package](https://www.npmjs.com/package/markdownlint-rule-helpers).
|
The collection of helper functions shared by the built-in rules is available for use by custom rules in the [markdownlint-rule-helpers package](https://www.npmjs.com/package/markdownlint-rule-helpers).
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,8 @@
|
||||||
|
|
||||||
// Regular expression for matching common newline characters
|
// Regular expression for matching common newline characters
|
||||||
// See NEWLINES_RE in markdown-it/lib/rules_core/normalize.js
|
// See NEWLINES_RE in markdown-it/lib/rules_core/normalize.js
|
||||||
module.exports.newLineRe = /\r[\n\u0085]?|[\n\u2424\u2028\u0085]/;
|
const newLineRe = /\r[\n\u0085]?|[\n\u2424\u2028\u0085]/;
|
||||||
|
module.exports.newLineRe = newLineRe;
|
||||||
|
|
||||||
// Regular expression for matching common front matter (YAML and TOML)
|
// Regular expression for matching common front matter (YAML and TOML)
|
||||||
module.exports.frontMatterRe =
|
module.exports.frontMatterRe =
|
||||||
|
|
@ -331,12 +332,13 @@ module.exports.forEachInlineCodeSpan =
|
||||||
};
|
};
|
||||||
|
|
||||||
// Adds a generic error object via the onError callback
|
// Adds a generic error object via the onError callback
|
||||||
function addError(onError, lineNumber, detail, context, range) {
|
function addError(onError, lineNumber, detail, context, range, fixInfo) {
|
||||||
onError({
|
onError({
|
||||||
"lineNumber": lineNumber,
|
lineNumber,
|
||||||
"detail": detail,
|
detail,
|
||||||
"context": context,
|
context,
|
||||||
"range": range
|
range,
|
||||||
|
fixInfo
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
module.exports.addError = addError;
|
module.exports.addError = addError;
|
||||||
|
|
@ -396,3 +398,22 @@ module.exports.frontMatterHasTitle =
|
||||||
return !ignoreFrontMatter &&
|
return !ignoreFrontMatter &&
|
||||||
frontMatterLines.some((line) => frontMatterTitleRe.test(line));
|
frontMatterLines.some((line) => frontMatterTitleRe.test(line));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Applies as many fixes as possible to the input
|
||||||
|
module.exports.fixErrors = function fixErrors(input, errors) {
|
||||||
|
const lines = input.split(newLineRe);
|
||||||
|
errors.filter((error) => !!error.fixInfo).forEach((error) => {
|
||||||
|
const { lineNumber, fixInfo } = error;
|
||||||
|
const editColumn = fixInfo.editColumn || 1;
|
||||||
|
const deleteCount = fixInfo.deleteCount || 0;
|
||||||
|
const insertText = fixInfo.insertText || "";
|
||||||
|
const lineIndex = lineNumber - 1;
|
||||||
|
const editIndex = editColumn - 1;
|
||||||
|
const line = lines[lineIndex];
|
||||||
|
lines[lineIndex] =
|
||||||
|
line.slice(0, editIndex) +
|
||||||
|
insertText +
|
||||||
|
line.slice(editIndex + deleteCount);
|
||||||
|
});
|
||||||
|
return lines.join("\n");
|
||||||
|
};
|
||||||
|
|
|
||||||
|
|
@ -381,7 +381,8 @@ function lintContent(
|
||||||
"lineNumber": errorInfo.lineNumber + frontMatterLines.length,
|
"lineNumber": errorInfo.lineNumber + frontMatterLines.length,
|
||||||
"detail": errorInfo.detail || null,
|
"detail": errorInfo.detail || null,
|
||||||
"context": errorInfo.context || null,
|
"context": errorInfo.context || null,
|
||||||
"range": errorInfo.range || null
|
"range": errorInfo.range || null,
|
||||||
|
"fixInfo": errorInfo.fixInfo || null
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
// Call (possibly external) rule function
|
// Call (possibly external) rule function
|
||||||
|
|
@ -423,6 +424,9 @@ function lintContent(
|
||||||
errorObject.errorDetail = error.detail;
|
errorObject.errorDetail = error.detail;
|
||||||
errorObject.errorContext = error.context;
|
errorObject.errorContext = error.context;
|
||||||
errorObject.errorRange = error.range;
|
errorObject.errorRange = error.range;
|
||||||
|
if (resultVersion === 3) {
|
||||||
|
errorObject.fixInfo = error.fixInfo;
|
||||||
|
}
|
||||||
return errorObject;
|
return errorObject;
|
||||||
});
|
});
|
||||||
if (filteredErrors.length) {
|
if (filteredErrors.length) {
|
||||||
|
|
|
||||||
12
lib/md047.js
12
lib/md047.js
|
|
@ -12,7 +12,17 @@ module.exports = {
|
||||||
const lastLineNumber = params.lines.length;
|
const lastLineNumber = params.lines.length;
|
||||||
const lastLine = params.lines[lastLineNumber - 1];
|
const lastLine = params.lines[lastLineNumber - 1];
|
||||||
if (!isBlankLine(lastLine)) {
|
if (!isBlankLine(lastLine)) {
|
||||||
addError(onError, lastLineNumber);
|
addError(
|
||||||
|
onError,
|
||||||
|
lastLineNumber,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
{
|
||||||
|
"insertText": "\n",
|
||||||
|
"editColumn": lastLine.length + 1
|
||||||
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -34,10 +34,11 @@ function promisify(func, ...args) {
|
||||||
|
|
||||||
function createTestForFile(file) {
|
function createTestForFile(file) {
|
||||||
return function testForFile(test) {
|
return function testForFile(test) {
|
||||||
test.expect(1);
|
test.expect(2);
|
||||||
const detailedResults = /[/\\]detailed-results-/.test(file);
|
const detailedResults = /[/\\]detailed-results-/.test(file);
|
||||||
const resultsFile = file.replace(/\.md$/, ".results.json");
|
const resultsFile = file.replace(/\.md$/, ".results.json");
|
||||||
const configFile = file.replace(/\.md$/, ".json");
|
const configFile = file.replace(/\.md$/, ".json");
|
||||||
|
let mergedConfig = null;
|
||||||
const actualPromise = promisify(fs.stat, configFile)
|
const actualPromise = promisify(fs.stat, configFile)
|
||||||
.then(
|
.then(
|
||||||
function configFileExists() {
|
function configFileExists() {
|
||||||
|
|
@ -49,16 +50,29 @@ function createTestForFile(file) {
|
||||||
})
|
})
|
||||||
.then(
|
.then(
|
||||||
function lintWithConfig(config) {
|
function lintWithConfig(config) {
|
||||||
const mergedConfig = {
|
mergedConfig = {
|
||||||
...defaultConfig,
|
...defaultConfig,
|
||||||
...config
|
...config
|
||||||
};
|
};
|
||||||
return promisify(markdownlint, {
|
return promisify(markdownlint, {
|
||||||
"files": [ file ],
|
"files": [ file ],
|
||||||
"config": mergedConfig,
|
"config": mergedConfig,
|
||||||
"resultVersion": detailedResults ? 2 : 0
|
"resultVersion": detailedResults ? 2 : 3
|
||||||
});
|
});
|
||||||
|
})
|
||||||
|
.then(
|
||||||
|
function convertResultVersion2To0(resultVersion2) {
|
||||||
|
const result0 = {};
|
||||||
|
const result2or3 = resultVersion2[file];
|
||||||
|
result2or3.forEach(function forResult(result) {
|
||||||
|
const ruleName = result.ruleNames[0];
|
||||||
|
const lineNumbers = result0[ruleName] || [];
|
||||||
|
lineNumbers.push(result.lineNumber);
|
||||||
|
result0[ruleName] = lineNumbers;
|
||||||
});
|
});
|
||||||
|
return [ result0, result2or3 ];
|
||||||
|
}
|
||||||
|
);
|
||||||
const expectedPromise = detailedResults ?
|
const expectedPromise = detailedResults ?
|
||||||
promisify(fs.readFile, resultsFile, helpers.utf8Encoding)
|
promisify(fs.readFile, resultsFile, helpers.utf8Encoding)
|
||||||
.then(
|
.then(
|
||||||
|
|
@ -96,11 +110,35 @@ function createTestForFile(file) {
|
||||||
Promise.all([ actualPromise, expectedPromise ])
|
Promise.all([ actualPromise, expectedPromise ])
|
||||||
.then(
|
.then(
|
||||||
function compareResults(fulfillments) {
|
function compareResults(fulfillments) {
|
||||||
const actual = fulfillments[0];
|
const [ [ actual0, actual2or3 ], expected ] = fulfillments;
|
||||||
const results = fulfillments[1];
|
const actual = detailedResults ? actual2or3 : actual0;
|
||||||
const expected = {};
|
|
||||||
expected[file] = results;
|
|
||||||
test.deepEqual(actual, expected, "Line numbers are not correct.");
|
test.deepEqual(actual, expected, "Line numbers are not correct.");
|
||||||
|
return actual2or3;
|
||||||
|
})
|
||||||
|
.then(
|
||||||
|
function verifyFixErrors(errors) {
|
||||||
|
if (detailedResults) {
|
||||||
|
return test.ok(true);
|
||||||
|
}
|
||||||
|
return promisify(fs.readFile, file, helpers.utf8Encoding)
|
||||||
|
.then(
|
||||||
|
function applyFixErrors(content) {
|
||||||
|
const corrections = helpers.fixErrors(content, errors);
|
||||||
|
return promisify(markdownlint, {
|
||||||
|
"strings": {
|
||||||
|
"input": corrections
|
||||||
|
},
|
||||||
|
"config": mergedConfig,
|
||||||
|
"resultVersion": 3
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.then(
|
||||||
|
function checkFixErrors(newErrors) {
|
||||||
|
const unfixed = newErrors.input
|
||||||
|
.filter((error) => !!error.fixInfo);
|
||||||
|
test.deepEqual(unfixed, [], "Fixable error was not fixed.");
|
||||||
|
}
|
||||||
|
);
|
||||||
})
|
})
|
||||||
.catch()
|
.catch()
|
||||||
.then(test.done);
|
.then(test.done);
|
||||||
|
|
@ -448,6 +486,43 @@ module.exports.resultFormattingV2 = function resultFormattingV2(test) {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
module.exports.resultFormattingV3 = function resultFormattingV3(test) {
|
||||||
|
test.expect(3);
|
||||||
|
const options = {
|
||||||
|
"strings": {
|
||||||
|
"input": "# Heading"
|
||||||
|
},
|
||||||
|
"resultVersion": 3
|
||||||
|
};
|
||||||
|
markdownlint(options, function callback(err, actualResult) {
|
||||||
|
test.ifError(err);
|
||||||
|
const expectedResult = {
|
||||||
|
"input": [
|
||||||
|
{
|
||||||
|
"lineNumber": 1,
|
||||||
|
"ruleNames": [ "MD047", "single-trailing-newline" ],
|
||||||
|
"ruleDescription": "Files should end with a single newline character",
|
||||||
|
"ruleInformation": `${homepage}/blob/v${version}/doc/Rules.md#md047`,
|
||||||
|
"errorDetail": null,
|
||||||
|
"errorContext": null,
|
||||||
|
"errorRange": null,
|
||||||
|
"fixInfo": {
|
||||||
|
"insertText": "\n",
|
||||||
|
"editColumn": 10
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
test.deepEqual(actualResult, expectedResult, "Undetected issues.");
|
||||||
|
const actualMessage = actualResult.toString();
|
||||||
|
const expectedMessage =
|
||||||
|
"input: 1: MD047/single-trailing-newline" +
|
||||||
|
" Files should end with a single newline character";
|
||||||
|
test.equal(actualMessage, expectedMessage, "Incorrect message.");
|
||||||
|
test.done();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
module.exports.stringInputLineEndings = function stringInputLineEndings(test) {
|
module.exports.stringInputLineEndings = function stringInputLineEndings(test) {
|
||||||
test.expect(2);
|
test.expect(2);
|
||||||
const options = {
|
const options = {
|
||||||
|
|
@ -1638,6 +1713,117 @@ module.exports.forEachInlineCodeSpan = function forEachInlineCodeSpan(test) {
|
||||||
test.done();
|
test.done();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
module.exports.fixErrors = function fixErrors(test) {
|
||||||
|
test.expect(8);
|
||||||
|
const testCases = [
|
||||||
|
[
|
||||||
|
"Hello world.",
|
||||||
|
[],
|
||||||
|
"Hello world."
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"Hello world.",
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"lineNumber": 1,
|
||||||
|
"fixInfo": {}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"Hello world."
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"Hello world.",
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"lineNumber": 1,
|
||||||
|
"fixInfo": {
|
||||||
|
"insertText": "Very "
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"Very Hello world."
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"Hello world.",
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"lineNumber": 1,
|
||||||
|
"fixInfo": {
|
||||||
|
"editColumn": 7,
|
||||||
|
"insertText": "big "
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"Hello big world."
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"Hello world.",
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"lineNumber": 1,
|
||||||
|
"fixInfo": {
|
||||||
|
"deleteCount": 6
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"world."
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"Hello world.",
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"lineNumber": 1,
|
||||||
|
"fixInfo": {
|
||||||
|
"editColumn": 7,
|
||||||
|
"deleteCount": 5,
|
||||||
|
"insertText": "there"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"Hello there."
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"Hello world.",
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"lineNumber": 1,
|
||||||
|
"fixInfo": {
|
||||||
|
"editColumn": 12,
|
||||||
|
"deleteCount": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"lineNumber": 1,
|
||||||
|
"fixInfo": {
|
||||||
|
"editColumn": 6,
|
||||||
|
"deleteCount": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"Helloworld"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"Hello world.",
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"lineNumber": 1,
|
||||||
|
"fixInfo": {
|
||||||
|
"editColumn": 13,
|
||||||
|
"insertText": " Hi."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"Hello world. Hi."
|
||||||
|
]
|
||||||
|
];
|
||||||
|
testCases.forEach((testCase) => {
|
||||||
|
const [ input, errors, expected ] = testCase;
|
||||||
|
const actual = helpers.fixErrors(input, errors);
|
||||||
|
test.equal(actual, expected, "Incorrect fix applied.");
|
||||||
|
});
|
||||||
|
test.done();
|
||||||
|
};
|
||||||
|
|
||||||
module.exports.configSingle = function configSingle(test) {
|
module.exports.configSingle = function configSingle(test) {
|
||||||
test.expect(2);
|
test.expect(2);
|
||||||
markdownlint.readConfig("./test/config/config-child.json",
|
markdownlint.readConfig("./test/config/config-child.json",
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue