Re-implement MD044/proper-names for better accuracy (range and fixInfo are now always valid) (fixes #402, fixes #403).

This commit is contained in:
David Anson 2021-06-12 17:10:59 -07:00
parent fb5f647368
commit 4db40256d9
7 changed files with 135 additions and 150 deletions

View file

@ -2,11 +2,9 @@
"use strict";
const { addErrorDetailIf, bareUrlRe, escapeForRegExp, filterTokens,
forEachInlineChild, newLineRe } = require("../helpers");
const startNonWordRe = /^\W/;
const endNonWordRe = /\W$/;
const { addErrorDetailIf, bareUrlRe, escapeForRegExp, forEachLine, newLineRe,
forEachInlineCodeSpan } = require("../helpers");
const { lineMetadata } = require("./cache");
module.exports = {
"names": [ "MD044", "proper-names" ],
@ -15,80 +13,66 @@ module.exports = {
"function": function MD044(params, onError) {
let names = params.config.names;
names = Array.isArray(names) ? names : [];
names.sort((a, b) => (b.length - a.length) || a.localeCompare(b));
const codeBlocks = params.config.code_blocks;
const includeCodeBlocks = (codeBlocks === undefined) ? true : !!codeBlocks;
// Text of automatic hyperlinks is implicitly a URL
const autolinkText = new Set();
filterTokens(params, "inline", (token) => {
let inAutoLink = false;
token.children.forEach((child) => {
const { info, type } = child;
if ((type === "link_open") && (info === "auto")) {
inAutoLink = true;
} else if (type === "link_close") {
inAutoLink = false;
} else if ((type === "text") && inAutoLink) {
autolinkText.add(child);
const exclusions = [];
if (!includeCodeBlocks) {
forEachInlineCodeSpan(
params.lines.join("\n"),
(code, lineIndex, columnIndex) => {
const codeLines = code.split(newLineRe);
// eslint-disable-next-line unicorn/no-for-loop
for (let i = 0; i < codeLines.length; i++) {
exclusions.push(
[ lineIndex + i, columnIndex, codeLines[i].length ]
);
columnIndex = 0;
}
}
);
}
for (const name of names) {
const escapedName = escapeForRegExp(name);
const startNamePattern = /^\W/.test(name) ? "" : "[^\\s([\"]*\\b_*";
const endNamePattern = /\W$/.test(name) ? "" : "_*\\b[^\\s)\\]\"]*";
const namePattern =
`(${startNamePattern})(${escapedName})${endNamePattern}`;
const nameRe = new RegExp(namePattern, "gi");
forEachLine(lineMetadata(), (line, lineIndex, inCode, onFence) => {
if (includeCodeBlocks || (!inCode && !onFence)) {
let match = null;
while ((match = nameRe.exec(line)) !== null) {
const [ fullMatch, leftMatch, nameMatch ] = match;
const index = match.index + leftMatch.length;
const length = nameMatch.length;
if (
(fullMatch.search(bareUrlRe) === -1) &&
exclusions.every((span) => (
(lineIndex !== span[0]) ||
(index + length < span[1]) ||
(index > span[1] + span[2])
))
) {
addErrorDetailIf(
onError,
lineIndex + 1,
name,
nameMatch,
null,
null,
[ index + 1, length ],
{
"editColumn": index + 1,
"deleteCount": length,
"insertText": name
}
);
}
exclusions.push([ lineIndex, index, length ]);
}
}
});
});
// For each proper name...
names.forEach((name) => {
const escapedName = escapeForRegExp(name);
const startNamePattern = startNonWordRe.test(name) ? "" : "\\S*\\b";
const endNamePattern = endNonWordRe.test(name) ? "" : "\\b\\S*";
const namePattern =
`(${startNamePattern})(${escapedName})(${endNamePattern})`;
const anyNameRe = new RegExp(namePattern, "gi");
// eslint-disable-next-line jsdoc/require-jsdoc
function forToken(token) {
if (!autolinkText.has(token)) {
const fenceOffset = (token.type === "fence") ? 1 : 0;
token.content.split(newLineRe).forEach((line, index) => {
let match = null;
while ((match = anyNameRe.exec(line)) !== null) {
const [ fullMatch, leftMatch, nameMatch, rightMatch ] = match;
if (fullMatch.search(bareUrlRe) === -1) {
const wordMatch = fullMatch
.replace(new RegExp(`^\\W{0,${leftMatch.length}}`), "")
.replace(new RegExp(`\\W{0,${rightMatch.length}}$`), "");
if (!names.includes(wordMatch)) {
const lineNumber = token.lineNumber + index + fenceOffset;
const fullLine = params.lines[lineNumber - 1];
const matchLength = wordMatch.length;
const matchIndex = fullLine.indexOf(wordMatch);
const range = (matchIndex === -1) ?
null :
[ matchIndex + 1, matchLength ];
const fixInfo = (matchIndex === -1) ?
null :
{
"editColumn": matchIndex + 1,
"deleteCount": matchLength,
"insertText": name
};
addErrorDetailIf(
onError,
lineNumber,
name,
nameMatch,
null,
null,
range,
fixInfo
);
}
}
}
});
}
}
forEachInlineChild(params, "text", forToken);
if (includeCodeBlocks) {
forEachInlineChild(params, "code_inline", forToken);
filterTokens(params, "code_block", forToken);
filterTokens(params, "fence", forToken);
}
});
}
}
};