Update MD034/no-bare-urls to re-scan documents with potential violations using proper reference definition handling to avoid false positives (fixes #787).

This commit is contained in:
David Anson 2023-05-23 04:01:55 +00:00
parent 054f208e9a
commit 488813f7f7
7 changed files with 136 additions and 102 deletions

View file

@ -1374,10 +1374,12 @@ var _require = __webpack_require__(/*! markdownlint-micromark */ "markdownlint-m
* *
* @param {string} markdown Markdown document. * @param {string} markdown Markdown document.
* @param {Object} [options] Options for micromark. * @param {Object} [options] Options for micromark.
* @param {boolean} [refsDefined] Whether to treat references as defined.
* @returns {Object[]} Micromark events. * @returns {Object[]} Micromark events.
*/ */
function getMicromarkEvents(markdown) { function getMicromarkEvents(markdown) {
var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
var refsDefined = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : true;
// Customize options object to add useful extensions // Customize options object to add useful extensions
options.extensions = options.extensions || []; options.extensions = options.extensions || [];
options.extensions.push(gfmAutolinkLiteral, gfmFootnote(), gfmTable); options.extensions.push(gfmAutolinkLiteral, gfmFootnote(), gfmTable);
@ -1386,10 +1388,12 @@ function getMicromarkEvents(markdown) {
var encoding = undefined; var encoding = undefined;
var eol = true; var eol = true;
var parseContext = parse(options); var parseContext = parse(options);
// Customize ParseContext to treat all references as defined if (refsDefined) {
parseContext.defined.includes = function (searchElement) { // Customize ParseContext to treat all references as defined
return searchElement.length > 0; parseContext.defined.includes = function (searchElement) {
}; return searchElement.length > 0;
};
}
var chunks = preprocess()(markdown, encoding, eol); var chunks = preprocess()(markdown, encoding, eol);
var events = postprocess(parseContext.document().write(chunks)); var events = postprocess(parseContext.document().write(chunks));
return events; return events;
@ -1400,12 +1404,14 @@ function getMicromarkEvents(markdown) {
* *
* @param {string} markdown Markdown document. * @param {string} markdown Markdown document.
* @param {Object} [options] Options for micromark. * @param {Object} [options] Options for micromark.
* @param {boolean} [refsDefined] Whether to treat references as defined.
* @returns {Token[]} Micromark tokens (frozen). * @returns {Token[]} Micromark tokens (frozen).
*/ */
function micromarkParse(markdown) { function micromarkParse(markdown) {
var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
var refsDefined = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : true;
// Use micromark to parse document into Events // Use micromark to parse document into Events
var events = getMicromarkEvents(markdown, options); var events = getMicromarkEvents(markdown, options, refsDefined);
// Create Token objects // Create Token objects
var document = []; var document = [];
@ -5008,60 +5014,68 @@ var _require = __webpack_require__(/*! ../helpers */ "../helpers/helpers.js"),
addErrorContext = _require.addErrorContext; addErrorContext = _require.addErrorContext;
var _require2 = __webpack_require__(/*! ../helpers/micromark.cjs */ "../helpers/micromark.cjs"), var _require2 = __webpack_require__(/*! ../helpers/micromark.cjs */ "../helpers/micromark.cjs"),
filterByPredicate = _require2.filterByPredicate, filterByPredicate = _require2.filterByPredicate,
getHtmlTagInfo = _require2.getHtmlTagInfo; getHtmlTagInfo = _require2.getHtmlTagInfo,
parse = _require2.parse;
module.exports = { module.exports = {
"names": ["MD034", "no-bare-urls"], "names": ["MD034", "no-bare-urls"],
"description": "Bare URL used", "description": "Bare URL used",
"tags": ["links", "url"], "tags": ["links", "url"],
"function": function MD034(params, onError) { "function": function MD034(params, onError) {
var literalAutolinks = filterByPredicate(params.parsers.micromark.tokens, function (token) { var literalAutolinks = function literalAutolinks(tokens) {
return token.type === "literalAutolink"; return filterByPredicate(tokens, function (token) {
}, function (token) { return token.type === "literalAutolink";
var children = token.children; }, function (token) {
var result = []; var children = token.children;
for (var i = 0; i < children.length; i++) { var result = [];
var openToken = children[i]; for (var i = 0; i < children.length; i++) {
var openTagInfo = getHtmlTagInfo(openToken); var openToken = children[i];
if (openTagInfo && !openTagInfo.close) { var openTagInfo = getHtmlTagInfo(openToken);
var count = 1; if (openTagInfo && !openTagInfo.close) {
for (var j = i + 1; j < children.length; j++) { var count = 1;
var closeToken = children[j]; for (var j = i + 1; j < children.length; j++) {
var closeTagInfo = getHtmlTagInfo(closeToken); var closeToken = children[j];
if (closeTagInfo && openTagInfo.name === closeTagInfo.name) { var closeTagInfo = getHtmlTagInfo(closeToken);
if (closeTagInfo.close) { if (closeTagInfo && openTagInfo.name === closeTagInfo.name) {
count--; if (closeTagInfo.close) {
if (count === 0) { count--;
i = j; if (count === 0) {
break; i = j;
break;
}
} else {
count++;
} }
} else {
count++;
} }
} }
} else {
result.push(openToken);
} }
} else {
result.push(openToken);
} }
return result;
});
};
if (literalAutolinks(params.parsers.micromark.tokens).length > 0) {
// Re-parse with correct link/image reference definition handling
var document = params.lines.join("\n");
var tokens = parse(document, undefined, false);
var _iterator = _createForOfIteratorHelper(literalAutolinks(tokens)),
_step;
try {
for (_iterator.s(); !(_step = _iterator.n()).done;) {
var token = _step.value;
var range = [token.startColumn, token.endColumn - token.startColumn];
var fixInfo = {
"editColumn": range[0],
"deleteCount": range[1],
"insertText": "<".concat(token.text, ">")
};
addErrorContext(onError, token.startLine, token.text, null, null, range, fixInfo);
}
} catch (err) {
_iterator.e(err);
} finally {
_iterator.f();
} }
return result;
});
var _iterator = _createForOfIteratorHelper(literalAutolinks),
_step;
try {
for (_iterator.s(); !(_step = _iterator.n()).done;) {
var token = _step.value;
var range = [token.startColumn, token.endColumn - token.startColumn];
var fixInfo = {
"editColumn": range[0],
"deleteCount": range[1],
"insertText": "<".concat(token.text, ">")
};
addErrorContext(onError, token.startLine, token.text, null, null, range, fixInfo);
}
} catch (err) {
_iterator.e(err);
} finally {
_iterator.f();
} }
} }
}; };

View file

@ -26,9 +26,10 @@ const {
* *
* @param {string} markdown Markdown document. * @param {string} markdown Markdown document.
* @param {Object} [options] Options for micromark. * @param {Object} [options] Options for micromark.
* @param {boolean} [refsDefined] Whether to treat references as defined.
* @returns {Object[]} Micromark events. * @returns {Object[]} Micromark events.
*/ */
function getMicromarkEvents(markdown, options = {}) { function getMicromarkEvents(markdown, options = {}, refsDefined = true) {
// Customize options object to add useful extensions // Customize options object to add useful extensions
options.extensions = options.extensions || []; options.extensions = options.extensions || [];
@ -38,8 +39,10 @@ function getMicromarkEvents(markdown, options = {}) {
const encoding = undefined; const encoding = undefined;
const eol = true; const eol = true;
const parseContext = parse(options); const parseContext = parse(options);
// Customize ParseContext to treat all references as defined if (refsDefined) {
parseContext.defined.includes = (searchElement) => searchElement.length > 0; // Customize ParseContext to treat all references as defined
parseContext.defined.includes = (searchElement) => searchElement.length > 0;
}
const chunks = preprocess()(markdown, encoding, eol); const chunks = preprocess()(markdown, encoding, eol);
const events = postprocess(parseContext.document().write(chunks)); const events = postprocess(parseContext.document().write(chunks));
return events; return events;
@ -50,12 +53,14 @@ function getMicromarkEvents(markdown, options = {}) {
* *
* @param {string} markdown Markdown document. * @param {string} markdown Markdown document.
* @param {Object} [options] Options for micromark. * @param {Object} [options] Options for micromark.
* @param {boolean} [refsDefined] Whether to treat references as defined.
* @returns {Token[]} Micromark tokens (frozen). * @returns {Token[]} Micromark tokens (frozen).
*/ */
function micromarkParse(markdown, options = {}) { function micromarkParse(markdown, options = {}, refsDefined = true) {
// Use micromark to parse document into Events // Use micromark to parse document into Events
const events = getMicromarkEvents(markdown, options); const events =
getMicromarkEvents(markdown, options, refsDefined);
// Create Token objects // Create Token objects
const document = []; const document = [];

View file

@ -3,7 +3,7 @@
"use strict"; "use strict";
const { addErrorContext } = require("../helpers"); const { addErrorContext } = require("../helpers");
const { filterByPredicate, getHtmlTagInfo } = const { filterByPredicate, getHtmlTagInfo, parse } =
require("../helpers/micromark.cjs"); require("../helpers/micromark.cjs");
module.exports = { module.exports = {
@ -11,9 +11,9 @@ module.exports = {
"description": "Bare URL used", "description": "Bare URL used",
"tags": [ "links", "url" ], "tags": [ "links", "url" ],
"function": function MD034(params, onError) { "function": function MD034(params, onError) {
const literalAutolinks = const literalAutolinks = (tokens) => (
filterByPredicate( filterByPredicate(
params.parsers.micromark.tokens, tokens,
(token) => token.type === "literalAutolink", (token) => token.type === "literalAutolink",
(token) => { (token) => {
const { children } = token; const { children } = token;
@ -43,26 +43,33 @@ module.exports = {
} }
} }
return result; return result;
}); }
for (const token of literalAutolinks) { )
const range = [ );
token.startColumn, if (literalAutolinks(params.parsers.micromark.tokens).length > 0) {
token.endColumn - token.startColumn // Re-parse with correct link/image reference definition handling
]; const document = params.lines.join("\n");
const fixInfo = { const tokens = parse(document, undefined, false);
"editColumn": range[0], for (const token of literalAutolinks(tokens)) {
"deleteCount": range[1], const range = [
"insertText": `<${token.text}>` token.startColumn,
}; token.endColumn - token.startColumn
addErrorContext( ];
onError, const fixInfo = {
token.startLine, "editColumn": range[0],
token.text, "deleteCount": range[1],
null, "insertText": `<${token.text}>`
null, };
range, addErrorContext(
fixInfo onError,
); token.startLine,
token.text,
null,
null,
range,
fixInfo
);
}
} }
} }
}; };

View file

@ -83,8 +83,7 @@ Angle brackets work the same for email: <user@example.com>
Links bind to the innermost [link that [is-a-valid] link](https://example.com) {MD034} Links bind to the innermost [link that [is-a-valid] link](https://example.com) {MD034}
But not if the [link [is-not-a-valid] link](https://example.com) {MD034} But not if the [link [is-not-a-valid] link](https://example.com)
HOWEVER this scenario could have an invalid shortcut and IS reported
Escaping both inner square brackets avoids the unwanted report: Escaping both inner square brackets avoids confusion:
[link \[is-not-a-valid\] link](https://example.com) [link \[is-not-a-valid\] link](https://example.com)

View file

@ -25,3 +25,8 @@ Duplicate links in tables should be handled:
| Link | Same Link | Violation | | Link | Same Link | Violation |
|----------------------|----------------------|-----------| |----------------------|----------------------|-----------|
| https://example.com/ | https://example.com/ | {MD034} | | https://example.com/ | https://example.com/ | {MD034} |
This is not a bare URL: [text [undefined] text](https://example.com).
This is a bare URL: [text [defined] text](https://example.com). {MD034}
[defined]: https://example.com

View file

@ -3247,26 +3247,6 @@ Generated by [AVA](https://avajs.dev).
'no-bare-urls', 'no-bare-urls',
], ],
}, },
{
errorContext: 'https://example.com',
errorDetail: null,
errorRange: [
45,
19,
],
fixInfo: {
deleteCount: 19,
editColumn: 45,
insertText: '<https://example.com>',
},
lineNumber: 86,
ruleDescription: 'Bare URL used',
ruleInformation: 'https://github.com/DavidAnson/markdownlint/blob/v0.0.0/doc/md034.md',
ruleNames: [
'MD034',
'no-bare-urls',
],
},
], ],
fixed: `# Detailed Results Bare URLs␊ fixed: `# Detailed Results Bare URLs␊
@ -3353,10 +3333,9 @@ Generated by [AVA](https://avajs.dev).
Links bind to the innermost [link that [is-a-valid] link](<https://example.com>) {MD034}␊ Links bind to the innermost [link that [is-a-valid] link](<https://example.com>) {MD034}␊
But not if the [link [is-not-a-valid] link](<https://example.com>) {MD034}␊ But not if the [link [is-not-a-valid] link](https://example.com)␊
HOWEVER this scenario could have an invalid shortcut and IS reported␊
Escaping both inner square brackets avoids the unwanted report:␊ Escaping both inner square brackets avoids confusion:␊
[link \\[is-not-a-valid\\] link](https://example.com)␊ [link \\[is-not-a-valid\\] link](https://example.com)␊
`, `,
} }
@ -23985,6 +23964,26 @@ Generated by [AVA](https://avajs.dev).
'no-bare-urls', 'no-bare-urls',
], ],
}, },
{
errorContext: 'https://example.com',
errorDetail: null,
errorRange: [
43,
19,
],
fixInfo: {
deleteCount: 19,
editColumn: 43,
insertText: '<https://example.com>',
},
lineNumber: 30,
ruleDescription: 'Bare URL used',
ruleInformation: 'https://github.com/DavidAnson/markdownlint/blob/v0.0.0/doc/md034.md',
ruleNames: [
'MD034',
'no-bare-urls',
],
},
], ],
fixed: `# Link test␊ fixed: `# Link test␊
@ -24013,6 +24012,11 @@ Generated by [AVA](https://avajs.dev).
| Link | Same Link | Violation |␊ | Link | Same Link | Violation |␊
|----------------------|----------------------|-----------|␊ |----------------------|----------------------|-----------|␊
| <https://example.com/> | <https://example.com/> | {MD034} |␊ | <https://example.com/> | <https://example.com/> | {MD034} |␊
This is not a bare URL: [text [undefined] text](https://example.com).␊
This is a bare URL: [text [defined] text](<https://example.com>). {MD034}␊
[defined]: https://example.com␊
`, `,
} }