Skip to content

Commit

Permalink
Fix: More reliable comment attachment (fixes #76) (#177)
Browse files Browse the repository at this point in the history
Comment attachment was sensitive to whitespace around the code block and
preceding comments. In some cases, the parser would place comments as
descendants of code blocks' preceding sibling nodes. However, a
depth-first traversal of the tree will still encounter the comments in
linear order, which is sufficient for our purposes.
  • Loading branch information
btmills authored Mar 30, 2021
1 parent 610cffd commit 79be776
Show file tree
Hide file tree
Showing 2 changed files with 71 additions and 24 deletions.
64 changes: 40 additions & 24 deletions lib/processor.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,25 +22,27 @@ let blocks = [];
* Performs a depth-first traversal of the Markdown AST.
* @param {ASTNode} node A Markdown AST node.
* @param {Object} callbacks A map of node types to callbacks.
* @param {Object} [parent] The node's parent AST node.
* @returns {void}
*/
function traverse(node, callbacks, parent) {
function traverse(node, callbacks) {
if (callbacks[node.type]) {
callbacks[node.type](node, parent);
callbacks[node.type](node);
} else {
callbacks["*"]();
}

if (typeof node.children !== "undefined") {
for (let i = 0; i < node.children.length; i++) {
traverse(node.children[i], callbacks, node);
traverse(node.children[i], callbacks);
}
}
}

/**
* Converts leading HTML comments to JS block comments.
* Extracts `eslint-*` or `global` comments from HTML comments if present.
* @param {string} html The text content of an HTML AST node.
* @returns {string[]} An array of JS block comments.
* @returns {string} The comment's text without the opening and closing tags or
* an empty string if the text is not an ESLint HTML comment.
*/
function getComment(html) {
const commentStart = "<!--";
Expand Down Expand Up @@ -116,7 +118,7 @@ function getIndentText(text, node) {
* delta at the beginning of each line.
* @param {string} text The text of the file.
* @param {ASTNode} node A Markdown code block AST node.
* @param {comments} comments List of configuration comment strings that will be
* @param {string[]} comments List of configuration comment strings that will be
* inserted at the beginning of the code block.
* @returns {Object[]} A list of offset-based adjustments, where lookups are
* done based on the `js` key, which represents the range in the linted JS,
Expand Down Expand Up @@ -209,45 +211,59 @@ function getBlockRangeMap(text, node, comments) {
}

/**
* Extracts lintable JavaScript code blocks from Markdown text.
* Extracts lintable code blocks from Markdown text.
* @param {string} text The text of the file.
* @returns {string[]} Source code strings to lint.
* @returns {Array<{ filename: string, text: string }>} Source code blocks to lint.
*/
function preprocess(text) {
const ast = markdown.parse(text);

/**
* During the depth-first traversal, keep track of any sequences of HTML
* comment nodes containing `eslint-*` or `global` comments. If a code
* block immediately follows such a sequence, insert the comments at the
* top of the code block. Any non-ESLint comment or other node type breaks
* and empties the sequence.
* @type {string[]}
*/
let htmlComments = [];

blocks = [];
traverse(ast, {
code(node, parent) {
const comments = [];

"*"() {
htmlComments = [];
},
code(node) {
if (node.lang) {
let index = parent.children.indexOf(node) - 1;
let previousNode = parent.children[index];

while (previousNode && previousNode.type === "html") {
const comment = getComment(previousNode.value);

if (!comment) {
break;
}
const comments = [];

for (const comment of htmlComments) {
if (comment.trim() === "eslint-skip") {
htmlComments = [];
return;
}

comments.unshift(`/*${comment}*/`);
index--;
previousNode = parent.children[index];
comments.push(`/*${comment}*/`);
}

htmlComments = [];

blocks.push({
...node,
baseIndentText: getIndentText(text, node),
comments,
rangeMap: getBlockRangeMap(text, node, comments)
});
}
},
html(node) {
const comment = getComment(node.value);

if (comment) {
htmlComments.push(comment);
} else {
htmlComments = [];
}
}
});

Expand Down
31 changes: 31 additions & 0 deletions tests/lib/processor.js
Original file line number Diff line number Diff line change
Expand Up @@ -454,6 +454,37 @@ describe("processor", () => {
].join("\n"));
});

// https://github.com/eslint/eslint-plugin-markdown/issues/76
it("should insert comments inside list items", () => {
const code = [
"* List item followed by a blank line",
"",
"<!-- eslint-disable no-console -->",
"```js",
"console.log(\"Blank line\");",
"```",
"",
"* List item without a blank line",
"<!-- eslint-disable no-console -->",
"```js",
"console.log(\"No blank line\");",
"```"
].join("\n");
const blocks = processor.preprocess(code);

assert.strictEqual(blocks.length, 2);
assert.strictEqual(blocks[0].text, [
"/* eslint-disable no-console */",
"console.log(\"Blank line\");",
""
].join("\n"));
assert.strictEqual(blocks[1].text, [
"/* eslint-disable no-console */",
"console.log(\"No blank line\");",
""
].join("\n"));
});

it("should ignore non-eslint comments", () => {
const code = [
"<!-- eslint-env browser -->",
Expand Down

0 comments on commit 79be776

Please sign in to comment.