Add options.handleRuleFailures for custom rule exceptions.

This commit is contained in:
David Anson 2019-05-18 12:32:52 -07:00
parent 0e5c44617f
commit 0f72bf054b
4 changed files with 232 additions and 67 deletions

View file

@ -21,7 +21,7 @@
"linebreak-style": "off", "linebreak-style": "off",
"max-lines": "off", "max-lines": "off",
"max-lines-per-function": "off", "max-lines-per-function": "off",
"max-params": ["error", 9], "max-params": ["error", 10],
"max-statements": ["error", 33], "max-statements": ["error", 33],
"multiline-comment-style": ["error", "separate-lines"], "multiline-comment-style": ["error", "separate-lines"],
"multiline-ternary": "off", "multiline-ternary": "off",

View file

@ -360,6 +360,21 @@ title: Title
Note: Matches must occur at the start of the file. Note: Matches must occur at the start of the file.
##### options.handleRuleFailures
Type: `Boolean`
Catches exceptions thrown during rule processing and reports the problem as a
rule violation.
By default, exceptions thrown by rules (or the library itself) are unhandled
and bubble up the stack to the caller in the conventional manner. By setting
`handleRuleFailures` to `true`, exceptions thrown by failing rules will be
handled by the library and the exception message logged as a rule violation.
This setting can be useful in the presence of (custom) rules that encounter
unexpected syntax and fail. By enabling this option, the linting process is
allowed to continue and report any violations that were found.
##### options.noInlineConfig ##### options.noInlineConfig
Type: `Boolean` Type: `Boolean`

View file

@ -301,6 +301,7 @@ function lintContent(
md, md,
config, config,
frontMatter, frontMatter,
handleRuleFailures,
noInlineConfig, noInlineConfig,
resultVersion, resultVersion,
callback) { callback) {
@ -376,7 +377,18 @@ function lintContent(
}); });
} }
// Call (possibly external) rule function // Call (possibly external) rule function
rule.function(params, onError); if (handleRuleFailures) {
try {
rule.function(params, onError);
} catch (ex) {
onError({
"lineNumber": 1,
"detail": `This rule threw an exception: ${ex.message}`
});
}
} else {
rule.function(params, onError);
}
// Record any errors (significant performance benefit from length check) // Record any errors (significant performance benefit from length check)
if (errors.length) { if (errors.length) {
errors.sort(lineNumberComparison); errors.sort(lineNumberComparison);
@ -432,6 +444,7 @@ function lintFile(
md, md,
config, config,
frontMatter, frontMatter,
handleRuleFailures,
noInlineConfig, noInlineConfig,
resultVersion, resultVersion,
synchronous, synchronous,
@ -441,7 +454,7 @@ function lintFile(
return callback(err); return callback(err);
} }
return lintContent(ruleList, file, content, md, config, frontMatter, return lintContent(ruleList, file, content, md, config, frontMatter,
noInlineConfig, resultVersion, callback); handleRuleFailures, noInlineConfig, resultVersion, callback);
} }
// Make a/synchronous call to read file // Make a/synchronous call to read file
if (synchronous) { if (synchronous) {
@ -472,6 +485,7 @@ function lintInput(options, synchronous, callback) {
const config = options.config || { "default": true }; const config = options.config || { "default": true };
const frontMatter = (options.frontMatter === undefined) ? const frontMatter = (options.frontMatter === undefined) ?
helpers.frontMatterRe : options.frontMatter; helpers.frontMatterRe : options.frontMatter;
const handleRuleFailures = !!options.handleRuleFailures;
const noInlineConfig = !!options.noInlineConfig; const noInlineConfig = !!options.noInlineConfig;
const resultVersion = (options.resultVersion === undefined) ? const resultVersion = (options.resultVersion === undefined) ?
2 : options.resultVersion; 2 : options.resultVersion;
@ -504,6 +518,7 @@ function lintInput(options, synchronous, callback) {
md, md,
config, config,
frontMatter, frontMatter,
handleRuleFailures,
noInlineConfig, noInlineConfig,
resultVersion, resultVersion,
lintNextItemCallback); lintNextItemCallback);
@ -515,6 +530,7 @@ function lintInput(options, synchronous, callback) {
md, md,
config, config,
frontMatter, frontMatter,
handleRuleFailures,
noInlineConfig, noInlineConfig,
resultVersion, resultVersion,
synchronous, synchronous,

View file

@ -966,6 +966,7 @@ module.exports.readmeHeadings = function readmeHeadings(test) {
"##### options.strings", "##### options.strings",
"##### options.config", "##### options.config",
"##### options.frontMatter", "##### options.frontMatter",
"##### options.handleRuleFailures",
"##### options.noInlineConfig", "##### options.noInlineConfig",
"##### options.resultVersion", "##### options.resultVersion",
"##### options.markdownItPlugins", "##### options.markdownItPlugins",
@ -2408,37 +2409,10 @@ function customRulesUsedTagName(test) {
}; };
module.exports.customRulesThrowForFile = module.exports.customRulesThrowForFile =
function customRulesThrowForFile(test) { function customRulesThrowForFile(test) {
test.expect(4); test.expect(4);
const exceptionMessage = "Test exception message"; const exceptionMessage = "Test exception message";
markdownlint({ markdownlint({
"customRules": [
{
"names": [ "name" ],
"description": "description",
"tags": [ "tag" ],
"function": function throws() {
throw new Error(exceptionMessage);
}
}
],
"files": [ "./test/custom-rules.md" ]
}, function callback(err, result) {
test.ok(err, "Did not get an error for function thrown.");
test.ok(err instanceof Error, "Error not instance of Error.");
test.equal(err.message, exceptionMessage,
"Incorrect message for function thrown.");
test.ok(!result, "Got result for function thrown.");
test.done();
});
};
module.exports.customRulesThrowForFileSync =
function customRulesThrowForFileSync(test) {
test.expect(4);
const exceptionMessage = "Test exception message";
test.throws(function customRuleThrowsCall() {
markdownlint.sync({
"customRules": [ "customRules": [
{ {
"names": [ "name" ], "names": [ "name" ],
@ -2450,44 +2424,71 @@ function customRulesThrowForFileSync(test) {
} }
], ],
"files": [ "./test/custom-rules.md" ] "files": [ "./test/custom-rules.md" ]
}, function callback(err, result) {
test.ok(err, "Did not get an error for function thrown.");
test.ok(err instanceof Error, "Error not instance of Error.");
test.equal(err.message, exceptionMessage,
"Incorrect message for function thrown.");
test.ok(!result, "Got result for function thrown.");
test.done();
}); });
}, function testError(err) { };
test.ok(err, "Did not get an error for function thrown.");
test.ok(err instanceof Error, "Error not instance of Error."); module.exports.customRulesThrowForFileSync =
test.equal(err.message, exceptionMessage, function customRulesThrowForFileSync(test) {
"Incorrect message for function thrown."); test.expect(4);
return true; const exceptionMessage = "Test exception message";
}, "Did not get exception for function thrown."); test.throws(function customRuleThrowsCall() {
test.done(); markdownlint.sync({
}; "customRules": [
{
"names": [ "name" ],
"description": "description",
"tags": [ "tag" ],
"function": function throws() {
throw new Error(exceptionMessage);
}
}
],
"files": [ "./test/custom-rules.md" ]
});
}, function testError(err) {
test.ok(err, "Did not get an error for function thrown.");
test.ok(err instanceof Error, "Error not instance of Error.");
test.equal(err.message, exceptionMessage,
"Incorrect message for function thrown.");
return true;
}, "Did not get exception for function thrown.");
test.done();
};
module.exports.customRulesThrowForString = module.exports.customRulesThrowForString =
function customRulesThrowForString(test) { function customRulesThrowForString(test) {
test.expect(4); test.expect(4);
const exceptionMessage = "Test exception message"; const exceptionMessage = "Test exception message";
markdownlint({ markdownlint({
"customRules": [ "customRules": [
{ {
"names": [ "name" ], "names": [ "name" ],
"description": "description", "description": "description",
"tags": [ "tag" ], "tags": [ "tag" ],
"function": function throws() { "function": function throws() {
throw new Error(exceptionMessage); throw new Error(exceptionMessage);
}
} }
],
"strings": {
"string": "String"
} }
], }, function callback(err, result) {
"strings": { test.ok(err, "Did not get an error for function thrown.");
"string": "String" test.ok(err instanceof Error, "Error not instance of Error.");
} test.equal(err.message, exceptionMessage,
}, function callback(err, result) { "Incorrect message for function thrown.");
test.ok(err, "Did not get an error for function thrown."); test.ok(!result, "Got result for function thrown.");
test.ok(err instanceof Error, "Error not instance of Error."); test.done();
test.equal(err.message, exceptionMessage, });
"Incorrect message for function thrown."); };
test.ok(!result, "Got result for function thrown.");
test.done();
});
};
module.exports.customRulesOnErrorNull = function customRulesOnErrorNull(test) { module.exports.customRulesOnErrorNull = function customRulesOnErrorNull(test) {
test.expect(4); test.expect(4);
@ -2683,6 +2684,139 @@ module.exports.customRulesOnErrorLazy = function customRulesOnErrorLazy(test) {
}); });
}; };
module.exports.customRulesThrowForFileHandled =
function customRulesThrowForFileHandled(test) {
test.expect(2);
const exceptionMessage = "Test exception message";
markdownlint({
"customRules": [
{
"names": [ "name" ],
"description": "description",
"tags": [ "tag" ],
"function": function throws() {
throw new Error(exceptionMessage);
}
}
],
"files": [ "./test/custom-rules.md" ],
"handleRuleFailures": true
}, function callback(err, actualResult) {
test.ifError(err);
const expectedResult = {
"./test/custom-rules.md": [
{
"lineNumber": 1,
"ruleNames": [ "name" ],
"ruleDescription": "description",
"ruleInformation": null,
"errorDetail":
`This rule threw an exception: ${exceptionMessage}`,
"errorContext": null,
"errorRange": null
}
]
};
test.deepEqual(actualResult, expectedResult, "Undetected issues.");
test.done();
});
};
module.exports.customRulesThrowForStringHandled =
function customRulesThrowForStringHandled(test) {
test.expect(2);
const exceptionMessage = "Test exception message";
const informationUrl = "https://example.com/rule";
markdownlint({
"customRules": [
{
"names": [ "name" ],
"description": "description",
"information": new URL(informationUrl),
"tags": [ "tag" ],
"function": function throws() {
throw new Error(exceptionMessage);
}
}
],
"strings": {
"string": "String\n"
},
"handleRuleFailures": true
}, function callback(err, actualResult) {
test.ifError(err);
const expectedResult = {
"string": [
{
"lineNumber": 1,
"ruleNames": [ "MD041", "first-line-heading", "first-line-h1" ],
"ruleDescription":
"First line in file should be a top level heading",
"ruleInformation":
`${homepage}/blob/v${version}/doc/Rules.md#md041`,
"errorDetail": null,
"errorContext": "String",
"errorRange": null
},
{
"lineNumber": 1,
"ruleNames": [ "name" ],
"ruleDescription": "description",
"ruleInformation": informationUrl,
"errorDetail":
`This rule threw an exception: ${exceptionMessage}`,
"errorContext": null,
"errorRange": null
}
]
};
test.deepEqual(actualResult, expectedResult, "Undetected issues.");
test.done();
});
};
module.exports.customRulesOnErrorInvalidHandled =
function customRulesOnErrorInvalidHandled(test) {
test.expect(2);
markdownlint({
"customRules": [
{
"names": [ "name" ],
"description": "description",
"tags": [ "tag" ],
"function": function onErrorInvalid(params, onError) {
onError({
"lineNumber": 13,
"detail": "N/A"
});
}
}
],
"strings": {
"string": "# Heading\n"
},
"handleRuleFailures": true
}, function callback(err, actualResult) {
test.ifError(err);
const expectedResult = {
"string": [
{
"lineNumber": 1,
"ruleNames": [ "name" ],
"ruleDescription": "description",
"ruleInformation": null,
"errorDetail": "This rule threw an exception: " +
"Property 'lineNumber' of onError parameter is incorrect.",
"errorContext": null,
"errorRange": null
}
]
};
test.deepEqual(actualResult, expectedResult, "Undetected issues.");
test.done();
});
};
module.exports.customRulesFileName = function customRulesFileName(test) { module.exports.customRulesFileName = function customRulesFileName(test) {
test.expect(2); test.expect(2);
const options = { const options = {