mirror of
https://github.com/DavidAnson/markdownlint.git
synced 2025-09-22 05:40:48 +02: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).
|
||||
|
||||
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
|
||||
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
|
||||
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
|
||||
|
||||
|
|
|
@ -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.
|
||||
- `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.
|
||||
- `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).
|
||||
|
||||
|
|
|
@ -4,7 +4,8 @@
|
|||
|
||||
// Regular expression for matching common newline characters
|
||||
// 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)
|
||||
module.exports.frontMatterRe =
|
||||
|
@ -331,12 +332,13 @@ module.exports.forEachInlineCodeSpan =
|
|||
};
|
||||
|
||||
// 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({
|
||||
"lineNumber": lineNumber,
|
||||
"detail": detail,
|
||||
"context": context,
|
||||
"range": range
|
||||
lineNumber,
|
||||
detail,
|
||||
context,
|
||||
range,
|
||||
fixInfo
|
||||
});
|
||||
}
|
||||
module.exports.addError = addError;
|
||||
|
@ -396,3 +398,22 @@ module.exports.frontMatterHasTitle =
|
|||
return !ignoreFrontMatter &&
|
||||
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,
|
||||
"detail": errorInfo.detail || null,
|
||||
"context": errorInfo.context || null,
|
||||
"range": errorInfo.range || null
|
||||
"range": errorInfo.range || null,
|
||||
"fixInfo": errorInfo.fixInfo || null
|
||||
});
|
||||
}
|
||||
// Call (possibly external) rule function
|
||||
|
@ -423,6 +424,9 @@ function lintContent(
|
|||
errorObject.errorDetail = error.detail;
|
||||
errorObject.errorContext = error.context;
|
||||
errorObject.errorRange = error.range;
|
||||
if (resultVersion === 3) {
|
||||
errorObject.fixInfo = error.fixInfo;
|
||||
}
|
||||
return errorObject;
|
||||
});
|
||||
if (filteredErrors.length) {
|
||||
|
|
12
lib/md047.js
12
lib/md047.js
|
@ -12,7 +12,17 @@ module.exports = {
|
|||
const lastLineNumber = params.lines.length;
|
||||
const lastLine = params.lines[lastLineNumber - 1];
|
||||
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) {
|
||||
return function testForFile(test) {
|
||||
test.expect(1);
|
||||
test.expect(2);
|
||||
const detailedResults = /[/\\]detailed-results-/.test(file);
|
||||
const resultsFile = file.replace(/\.md$/, ".results.json");
|
||||
const configFile = file.replace(/\.md$/, ".json");
|
||||
let mergedConfig = null;
|
||||
const actualPromise = promisify(fs.stat, configFile)
|
||||
.then(
|
||||
function configFileExists() {
|
||||
|
@ -49,16 +50,29 @@ function createTestForFile(file) {
|
|||
})
|
||||
.then(
|
||||
function lintWithConfig(config) {
|
||||
const mergedConfig = {
|
||||
mergedConfig = {
|
||||
...defaultConfig,
|
||||
...config
|
||||
};
|
||||
return promisify(markdownlint, {
|
||||
"files": [ file ],
|
||||
"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 ?
|
||||
promisify(fs.readFile, resultsFile, helpers.utf8Encoding)
|
||||
.then(
|
||||
|
@ -96,11 +110,35 @@ function createTestForFile(file) {
|
|||
Promise.all([ actualPromise, expectedPromise ])
|
||||
.then(
|
||||
function compareResults(fulfillments) {
|
||||
const actual = fulfillments[0];
|
||||
const results = fulfillments[1];
|
||||
const expected = {};
|
||||
expected[file] = results;
|
||||
const [ [ actual0, actual2or3 ], expected ] = fulfillments;
|
||||
const actual = detailedResults ? actual2or3 : actual0;
|
||||
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()
|
||||
.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) {
|
||||
test.expect(2);
|
||||
const options = {
|
||||
|
@ -1638,6 +1713,117 @@ module.exports.forEachInlineCodeSpan = function forEachInlineCodeSpan(test) {
|
|||
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) {
|
||||
test.expect(2);
|
||||
markdownlint.readConfig("./test/config/config-child.json",
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue