From c699b8e22b88db06bb852bc23a41544b7d920952 Mon Sep 17 00:00:00 2001 From: David Anson Date: Tue, 11 Jul 2023 21:44:45 -0700 Subject: [PATCH] Allow a custom rule's onError implementation to override that rule's information URL for each error. --- demo/markdownlint-browser.js | 16 +- doc/CustomRules.md | 2 + example/typescript/type-check.ts | 3 +- helpers/helpers.js | 7 +- lib/markdownlint.d.ts | 4 + lib/markdownlint.js | 10 +- test/markdownlint-test-custom-rules.js | 211 +++++++++++++++++++++++-- 7 files changed, 235 insertions(+), 18 deletions(-) diff --git a/demo/markdownlint-browser.js b/demo/markdownlint-browser.js index ab8f298d..adbe25cc 100644 --- a/demo/markdownlint-browser.js +++ b/demo/markdownlint-browser.js @@ -115,7 +115,12 @@ module.exports.isEmptyString = function isEmptyString(str) { // Returns true iff the input is an object module.exports.isObject = function isObject(obj) { - return obj !== null && _typeof(obj) === "object" && !Array.isArray(obj); + return !!obj && _typeof(obj) === "object" && !Array.isArray(obj); +}; + +// Returns true iff the input is a URL +module.exports.isUrl = function isUrl(obj) { + return !!obj && Object.getPrototypeOf(obj) === URL.prototype; }; /** @@ -1712,7 +1717,7 @@ function validateRuleList(ruleList, synchronous) { result = newError(_property); } } - if (!result && rule.information && Object.getPrototypeOf(rule.information) !== URL.prototype) { + if (!result && rule.information && !helpers.isUrl(rule.information)) { result = newError("information"); } if (!result && rule.asynchronous !== undefined && typeof rule.asynchronous !== "boolean") { @@ -2401,6 +2406,9 @@ function lintContent(ruleList, aliasToRuleNames, name, content, md, config, conf if (errorInfo.context && !helpers.isString(errorInfo.context)) { throwError("context"); } + if (errorInfo.information && !helpers.isUrl(errorInfo.information)) { + throwError("information"); + } if (errorInfo.range && (!Array.isArray(errorInfo.range) || errorInfo.range.length !== 2 || !helpers.isNumber(errorInfo.range[0]) || errorInfo.range[0] < 1 || !helpers.isNumber(errorInfo.range[1]) || errorInfo.range[1] < 1 || errorInfo.range[0] + errorInfo.range[1] - 1 > lines[errorInfo.lineNumber - 1].length)) { throwError("range"); } @@ -2436,12 +2444,13 @@ function lintContent(ruleList, aliasToRuleNames, name, content, md, config, conf cleanFixInfo.insertText = fixInfo.insertText; } } + var information = errorInfo.information || rule.information; results.push({ lineNumber: lineNumber, "ruleName": rule.names[0], "ruleNames": rule.names, "ruleDescription": rule.description, - "ruleInformation": rule.information ? rule.information.href : null, + "ruleInformation": information ? information.href : null, "errorDetail": errorInfo.detail || null, "errorContext": errorInfo.context || null, "errorRange": errorInfo.range ? _toConsumableArray(errorInfo.range) : null, @@ -3036,6 +3045,7 @@ module.exports = markdownlint; * @property {number} lineNumber Line number (1-based). * @property {string} [detail] Detail about the error. * @property {string} [context] Context for the error. + * @property {URL} [information] Link to more information. * @property {number[]} [range] Column number (1-based) and length. * @property {RuleOnErrorFixInfo} [fixInfo] Fix information. */ diff --git a/doc/CustomRules.md b/doc/CustomRules.md index 59f2f3d8..691ed42d 100644 --- a/doc/CustomRules.md +++ b/doc/CustomRules.md @@ -82,6 +82,8 @@ properties: error. - `context` is an optional `String` with relevant text surrounding the error location. + - `information` is an optional (absolute) `URL` of a link to override the + same-named value provided by the rule definition. (Uncommon) - `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 diff --git a/example/typescript/type-check.ts b/example/typescript/type-check.ts index 494cf1e7..8a9871fe 100644 --- a/example/typescript/type-check.ts +++ b/example/typescript/type-check.ts @@ -112,7 +112,7 @@ markdownlint(options, assertLintResultsCallback); const testRule = { "names": [ "test-rule" ], "description": "Test rule", - "information": new URL("https://example.com/test-rule"), + "information": new URL("https://example.com/rule-information"), "tags": [ "test-tag" ], "function": function rule(params: markdownlint.RuleParams, onError: markdownlint.RuleOnError) { assert(!!params); @@ -136,6 +136,7 @@ const testRule = { "lineNumber": 1, "detail": "detail", "context": "context", + "information": new URL("https://example.com/error-information"), "range": [ 1, 2 ], "fixInfo": { "lineNumber": 1, diff --git a/helpers/helpers.js b/helpers/helpers.js index 72694910..7628db3e 100644 --- a/helpers/helpers.js +++ b/helpers/helpers.js @@ -73,7 +73,12 @@ module.exports.isEmptyString = function isEmptyString(str) { // Returns true iff the input is an object module.exports.isObject = function isObject(obj) { - return (obj !== null) && (typeof obj === "object") && !Array.isArray(obj); + return !!obj && (typeof obj === "object") && !Array.isArray(obj); +}; + +// Returns true iff the input is a URL +module.exports.isUrl = function isUrl(obj) { + return !!obj && (Object.getPrototypeOf(obj) === URL.prototype); }; /** diff --git a/lib/markdownlint.d.ts b/lib/markdownlint.d.ts index a3f72429..027d0389 100644 --- a/lib/markdownlint.d.ts +++ b/lib/markdownlint.d.ts @@ -218,6 +218,10 @@ type RuleOnErrorInfo = { * Context for the error. */ context?: string; + /** + * Link to more information. + */ + information?: URL; /** * Column number (1-based) and length. */ diff --git a/lib/markdownlint.js b/lib/markdownlint.js index 3e7ee819..697a9d09 100644 --- a/lib/markdownlint.js +++ b/lib/markdownlint.js @@ -59,7 +59,7 @@ function validateRuleList(ruleList, synchronous) { if ( !result && rule.information && - (Object.getPrototypeOf(rule.information) !== URL.prototype) + !helpers.isUrl(rule.information) ) { result = newError("information"); } @@ -629,6 +629,10 @@ function lintContent( !helpers.isString(errorInfo.context)) { throwError("context"); } + if (errorInfo.information && + !helpers.isUrl(errorInfo.information)) { + throwError("information"); + } if (errorInfo.range && (!Array.isArray(errorInfo.range) || (errorInfo.range.length !== 2) || @@ -681,12 +685,13 @@ function lintContent( cleanFixInfo.insertText = fixInfo.insertText; } } + const information = errorInfo.information || rule.information; results.push({ lineNumber, "ruleName": rule.names[0], "ruleNames": rule.names, "ruleDescription": rule.description, - "ruleInformation": rule.information ? rule.information.href : null, + "ruleInformation": information ? information.href : null, "errorDetail": errorInfo.detail || null, "errorContext": errorInfo.context || null, "errorRange": errorInfo.range ? [ ...errorInfo.range ] : null, @@ -1314,6 +1319,7 @@ module.exports = markdownlint; * @property {number} lineNumber Line number (1-based). * @property {string} [detail] Detail about the error. * @property {string} [context] Context for the error. + * @property {URL} [information] Link to more information. * @property {number[]} [range] Column number (1-based) and length. * @property {RuleOnErrorFixInfo} [fixInfo] Fix information. */ diff --git a/test/markdownlint-test-custom-rules.js b/test/markdownlint-test-custom-rules.js index 1f3c2e85..5e582de0 100644 --- a/test/markdownlint-test-custom-rules.js +++ b/test/markdownlint-test-custom-rules.js @@ -648,31 +648,30 @@ test("customRulesOnErrorNullSync", (t) => { }); test("customRulesOnErrorBad", (t) => { - t.plan(21); + t.plan(25); for (const testCase of [ { "propertyName": "lineNumber", - "subPropertyName": null, "propertyValues": [ null, "string" ] }, { "propertyName": "detail", - "subPropertyName": null, "propertyValues": [ 10, [] ] }, { "propertyName": "context", - "subPropertyName": null, "propertyValues": [ 10, [] ] }, + { + "propertyName": "information", + "propertyValues": [ 10, [], "string", "https://example.com" ] + }, { "propertyName": "range", - "subPropertyName": null, "propertyValues": [ 10, [], [ 10 ], [ 10, null ], [ 10, 11, 12 ] ] }, { "propertyName": "fixInfo", - "subPropertyName": null, "propertyValues": [ 10, "string" ] }, { @@ -744,12 +743,10 @@ test("customRulesOnErrorInvalid", (t) => { for (const testCase of [ { "propertyName": "lineNumber", - "subPropertyName": null, "propertyValues": [ -1, 0, 3, 4 ] }, { "propertyName": "range", - "subPropertyName": null, "propertyValues": [ [ 0, 1 ], [ 1, 0 ], [ 5, 1 ], [ 1, 5 ], [ 4, 2 ] ] }, { @@ -816,12 +813,10 @@ test("customRulesOnErrorValid", (t) => { for (const testCase of [ { "propertyName": "lineNumber", - "subPropertyName": null, "propertyValues": [ 1, 2 ] }, { "propertyName": "range", - "subPropertyName": null, "propertyValues": [ [ 1, 1 ], [ 1, 4 ], [ 2, 2 ], [ 3, 2 ], [ 4, 1 ] ] }, { @@ -927,6 +922,7 @@ test("customRulesOnErrorModified", (t) => new Promise((resolve) => { "lineNumber": 1, "detail": "detail", "context": "context", + "information": new URL("https://example.com/information"), "range": [ 1, 2 ], "fixInfo": { "editColumn": 1, @@ -945,6 +941,7 @@ test("customRulesOnErrorModified", (t) => new Promise((resolve) => { errorObject.lineNumber = 2; errorObject.detail = "changed"; errorObject.context = "changed"; + errorObject.information = new URL("https://example.com/changed"); errorObject.range[1] = 3; errorObject.fixInfo.editColumn = 2; errorObject.fixInfo.deleteCount = 3; @@ -964,7 +961,7 @@ test("customRulesOnErrorModified", (t) => new Promise((resolve) => { "lineNumber": 1, "ruleNames": [ "name" ], "ruleDescription": "description", - "ruleInformation": null, + "ruleInformation": "https://example.com/information", "errorDetail": "detail", "errorContext": "context", "errorRange": [ 1, 2 ], @@ -1105,6 +1102,198 @@ test("customRulesStringName", (t) => new Promise((resolve) => { }); })); +test("customRulesOnErrorInformationNotRuleNotError", (t) => { + t.plan(1); + const actualResult = markdownlint.sync({ + "customRules": [ + { + "names": [ "name" ], + "description": "description", + "tags": [ "tag" ], + "function": (params, onError) => { + onError({ + "lineNumber": 1 + }); + } + } + ], + "strings": { + "string": "# Heading\n" + } + }); + t.true(actualResult.string[0].ruleInformation === null, "Unexpected URL."); +}); + +test("customRulesOnErrorInformationRuleNotError", (t) => { + t.plan(1); + const actualResult = markdownlint.sync({ + "customRules": [ + { + "names": [ "name" ], + "description": "description", + "tags": [ "tag" ], + "information": new URL("https://example.com/rule"), + "function": (params, onError) => { + onError({ + "lineNumber": 1 + }); + } + } + ], + "strings": { + "string": "# Heading\n" + } + }); + t.is( + actualResult.string[0].ruleInformation, + "https://example.com/rule", + "Unexpected URL." + ); +}); + +test("customRulesOnErrorInformationNotRuleError", (t) => { + t.plan(1); + const actualResult = markdownlint.sync({ + "customRules": [ + { + "names": [ "name" ], + "description": "description", + "tags": [ "tag" ], + "function": (params, onError) => { + onError({ + "lineNumber": 1, + "information": new URL("https://example.com/error") + }); + } + } + ], + "strings": { + "string": "# Heading\n" + } + }); + t.is( + actualResult.string[0].ruleInformation, + "https://example.com/error", + "Unexpected URL." + ); +}); + +test("customRulesOnErrorInformationRuleError", (t) => { + t.plan(1); + const actualResult = markdownlint.sync({ + "customRules": [ + { + "names": [ "name" ], + "description": "description", + "tags": [ "tag" ], + "information": new URL("https://example.com/rule"), + "function": (params, onError) => { + onError({ + "lineNumber": 1, + "information": new URL("https://example.com/error") + }); + } + } + ], + "strings": { + "string": "# Heading\n" + } + }); + t.is( + actualResult.string[0].ruleInformation, + "https://example.com/error", + "Unexpected URL." + ); +}); + +test("customRulesOnErrorInformationRuleErrorUndefined", (t) => { + t.plan(1); + const actualResult = markdownlint.sync({ + "customRules": [ + { + "names": [ "name" ], + "description": "description", + "tags": [ "tag" ], + "information": new URL("https://example.com/rule"), + "function": (params, onError) => { + onError({ + "lineNumber": 1, + "information": undefined + }); + } + } + ], + "strings": { + "string": "# Heading\n" + } + }); + t.is( + actualResult.string[0].ruleInformation, + "https://example.com/rule", + "Unexpected URL." + ); +}); + +test("customRulesOnErrorInformationRuleErrorMultiple", (t) => { + t.plan(6); + const actualResult = markdownlint.sync({ + "customRules": [ + { + "names": [ "name" ], + "description": "description", + "tags": [ "tag" ], + "information": new URL("https://example.com/rule"), + "function": (params, onError) => { + onError({ + "lineNumber": 1, + "information": new URL("https://example.com/errorA") + }); + onError({ + "lineNumber": 3, + "information": new URL("https://example.com/errorB") + }); + onError({ + "lineNumber": 5 + }); + } + } + ], + "strings": { + "string": "# Heading\n\nText\n\nText\n" + } + }); + t.is( + actualResult.string[0].lineNumber, + 1, + "Unexpected line number." + ); + t.is( + actualResult.string[0].ruleInformation, + "https://example.com/errorA", + "Unexpected URL." + ); + t.is( + actualResult.string[1].lineNumber, + 3, + "Unexpected line number." + ); + t.is( + actualResult.string[1].ruleInformation, + "https://example.com/errorB", + "Unexpected URL." + ); + t.is( + actualResult.string[2].lineNumber, + 5, + "Unexpected line number." + ); + t.is( + actualResult.string[2].ruleInformation, + "https://example.com/rule", + "Unexpected URL." + ); +}); + test("customRulesDoc", (t) => new Promise((resolve) => { t.plan(2); markdownlint({