Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions docs/processors/markdown.md
Comment thread
xbinaryx marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,8 @@ Comment bodies are passed through unmodified, so the plugin supports any [config

This example enables the `alert` global variable, disables the `no-alert` rule, and configures the `quotes` rule to prefer single quotes:

<!-- eslint-skip -->

````markdown
<!-- global alert -->
<!-- eslint-disable no-alert -->
Expand All @@ -195,6 +197,8 @@ alert('Hello, world!');

Each code block in a file is linted separately, so configuration comments apply only to the code block that immediately follows.

<!-- eslint-skip -->

````markdown
Assuming `no-alert` is enabled in `eslint.config.js`, the first code block will have no error from `no-alert`:

Expand Down
99 changes: 88 additions & 11 deletions src/processor.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import { fromMarkdown } from "mdast-util-from-markdown";
/**
* @import { Node, Parent, Code, Html } from "mdast";
* @import { Linter, Rule, AST } from "eslint";
* @import { Block, RangeMap } from "./types.js";
* @import { Block, Comment, RangeMap } from "./types.js";
* @typedef {Linter.LintMessage} Message
* @typedef {Rule.Fix} Fix
* @typedef {AST.Range} Range
Expand All @@ -33,6 +33,8 @@ const UNSATISFIABLE_RULES = new Set([
const SUPPORTS_AUTOFIX = true;

const BOM = "\uFEFF";
const unusedDirectiveMessagePattern =
/^Unused eslint-(?:disable|enable) directive/u;

/**
* @type {Map<string, Block[]>}
Expand Down Expand Up @@ -141,7 +143,7 @@ function getIndentText(text, node) {
* delta at the beginning of each line.
* @param {string} text The text of the file.
* @param {Code} node A Markdown code block AST node.
* @param {string[]} comments List of configuration comment strings that will be
* @param {Comment[]} comments List of configuration comment objects that will be
* inserted at the beginning of the code block.
* @returns {RangeMap[]} 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 @@ -179,7 +181,7 @@ function getBlockRangeMap(text, node, comments) {
* of the linted JS and start the JS offset lookup keys at this index.
*/
const commentLength = comments.reduce(
(len, comment) => len + comment.length + 1,
(len, comment) => len + comment.text.length + 1,
0,
);

Expand Down Expand Up @@ -240,6 +242,74 @@ function getBlockRangeMap(text, node, comments) {
return rangeMap;
}

/**
* Determines whether a message reports an unused directive.
* @param {Message} message The message to check.
* @returns {boolean} True if the message reports an unused directive.
*/
function isUnusedDirectiveMessage(message) {
return (
message.ruleId === null &&
unusedDirectiveMessagePattern.test(message.message)
);
}

/**
* Finds the inserted comment that contains the given generated line.
* @param {Block} block The code block to search.
* @param {number} line The generated JS line number.
* @returns {Comment | undefined} The matching inserted comment location.
*/
function findCommentLocation(block, line) {
let currentJsLine = 1;
Comment thread
xbinaryx marked this conversation as resolved.
Outdated
let foundComment;

for (const comment of block.comments) {
const commentLines = comment.text.split("\n").length;
Comment thread
xbinaryx marked this conversation as resolved.
Outdated

if (line >= currentJsLine && line < currentJsLine + commentLines) {
foundComment = comment;
break;
}

currentJsLine += commentLines;
}

return foundComment;
}

/**
* Adjusts an unused directive message in an inserted JS comment.
* @param {Block} block The code block containing the inserted comment.
* @param {Message} message The message to adjust.
* @returns {Message} The adjusted message, if it can be mapped.
*/
function adjustCommentMessage(block, message) {
const comment = findCommentLocation(block, message.line);

if (!comment) {
return message;
}

const { start, end } = comment.position;
const { fix, ...messageWithoutFix } = message;

const adjustedMessage = /** @type {Message} */ ({
...messageWithoutFix,
line: start.line,
column: start.column,
});

if (fix) {
adjustedMessage.fix = {
range: [start.offset, end.offset],
Comment thread
xbinaryx marked this conversation as resolved.
Outdated
text: fix.text,
};
}

return adjustedMessage;
}

const codeBlockFileNameRegex = /filename=(?<quote>["'])(?<filename>.*?)\1/u;

/**
Expand Down Expand Up @@ -281,7 +351,7 @@ function preprocess(sourceText, filename) {
* 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[]}
* @type {Comment[]}
*/
let htmlComments = [];

Expand All @@ -297,16 +367,19 @@ function preprocess(sourceText, filename) {
*/
code(node) {
if (node.lang) {
/** @type {string[]} */
/** @type {Comment[]} */
const comments = [];

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

comments.push(`/*${comment}*/`);
comments.push({
text: `/*${comment.text}*/`,
position: comment.position,
});
}

htmlComments = [];
Expand All @@ -329,7 +402,7 @@ function preprocess(sourceText, filename) {
const comment = getComment(node.value);

if (comment) {
htmlComments.push(comment);
htmlComments.push({ text: comment, position: node.position });
} else {
htmlComments = [];
}
Expand All @@ -348,7 +421,9 @@ function preprocess(sourceText, filename) {

return {
filename: fileNameFromMeta(block) ?? `${index}.${fileExtension}`,
text: [...block.comments, block.value, ""].join("\n"),
text: [...block.comments.map(c => c.text), block.value, ""].join(
"\n",
),
};
});
}
Expand Down Expand Up @@ -390,7 +465,7 @@ function adjustFix(block, fix) {
*/
function adjustBlock(block) {
const leadingCommentLines = block.comments.reduce(
(count, comment) => count + comment.split("\n").length,
(count, comment) => count + comment.text.split("\n").length,
0,
);

Expand All @@ -413,7 +488,9 @@ function adjustBlock(block) {
const lineInCode = message.line - leadingCommentLines;

if (lineInCode < 1 || lineInCode >= block.rangeMap.length) {
return null;
return isUnusedDirectiveMessage(message)
? adjustCommentMessage(block, message)
: null;
}

/** @type {Pick<Message, "line" | "column" | "endLine" | "suggestions">} */
Expand Down
8 changes: 7 additions & 1 deletion src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import type {
Yaml,
} from "mdast";
import type { InlineMath, Math } from "mdast-util-math";
import type { Position } from "unist";
import type {
LanguageContext,
LanguageOptions,
Expand All @@ -64,9 +65,14 @@ export interface RangeMap {
md: number;
}

export interface Comment {
text: string;
position: Position;
}

export interface BlockBase {
baseIndentText: string;
comments: string[];
comments: Comment[];
Comment thread
xbinaryx marked this conversation as resolved.
Outdated
rangeMap: RangeMap[];
}

Expand Down
3 changes: 3 additions & 0 deletions tests/fixtures/eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ export default [
languageOptions: {
globals: globals.browser,
},
linterOptions: {
reportUnusedDisableDirectives: "off",
},
rules: {
"eol-last": "error",
"no-console": "error",
Expand Down
67 changes: 67 additions & 0 deletions tests/plugin.test.js
Comment thread
xbinaryx marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -1569,6 +1569,73 @@ describe("FlatESLint", () => {
);
assert.strictEqual(results[0].messages[0].line, 5);
});

describe("unused disable directives", () => {
Comment thread
DMartens marked this conversation as resolved.
let unusedDisableESLint;

beforeEach(() => {
unusedDisableESLint = initFlatESLint("eslint.config.js", {
overrideConfig: {
linterOptions: {
reportUnusedDisableDirectives: "error",
},
},
});
});

it("reports unused disable directives", async () => {
const code = [
"<!-- eslint-disable no-console -->",
"",
"```js",
"const answer = 42;",
"```",
].join("\n");

const results = await unusedDisableESLint.lintText(code, {
filePath: "test.md",
});

assert.strictEqual(results.length, 1);
assert.strictEqual(results[0].messages.length, 1);
assert.strictEqual(
results[0].messages[0].message,
"Unused eslint-disable directive (no problems were reported from 'no-console').",
);
assert.strictEqual(results[0].messages[0].line, 1);
assert.strictEqual(results[0].messages[0].column, 1);
assert.deepStrictEqual(results[0].messages[0].fix, {
range: [0, 34],
text: " ",
});
});

it("reports unused disable directives correctly among multiple comments", async () => {
const code = [
"# Title",
"",
"<!-- eslint-disable quotes -->",
"<!-- eslint-disable no-console -->",
"",
"```js",
"const message = 'single quotes';",
"```",
].join("\n");

const results = await unusedDisableESLint.lintText(code, {
filePath: "test.md",
});

assert.strictEqual(results.length, 1);
assert.strictEqual(results[0].messages.length, 1);
assert.strictEqual(
results[0].messages[0].message,
"Unused eslint-disable directive (no problems were reported from 'no-console').",
);
assert.strictEqual(results[0].messages[0].line, 4);
assert.strictEqual(results[0].messages[0].column, 1);
});
});
Comment thread
xbinaryx marked this conversation as resolved.
});

describe("should fix code", () => {
Expand Down
15 changes: 13 additions & 2 deletions tests/types/types.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import type {
Json,
RangeMap,
Block,
Comment,
} from "@eslint/markdown";
import type { Plugin, SourceLocation, SourceRange } from "@eslint/core";
import type { Linter } from "eslint";
Expand Down Expand Up @@ -58,7 +59,15 @@ const validBlock: Block = {

// `BlockBase` properties
baseIndentText: " ",
comments: ["// A comment"],
comments: [
{
text: "eslint-disable",
position: {
start: { line: 1, column: 1, offset: 0 },
end: { line: 1, column: 15, offset: 14 },
},
},
],
rangeMap: [{ indent: 2, js: 0, md: 4 }],
};

Expand All @@ -72,7 +81,9 @@ validBlock.data satisfies CodeData | undefined;

// Verify `Block` has `BlockBase` properties
validBlock.baseIndentText satisfies string;
validBlock.comments satisfies string[];
validBlock.comments satisfies Comment[];
Comment thread
xbinaryx marked this conversation as resolved.
Outdated
validBlock.comments[0].text satisfies string;
validBlock.comments[0].position satisfies Position;
validBlock.rangeMap satisfies RangeMap[];

// Verify `RangeMap` structure
Expand Down
Loading