Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
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
106 changes: 95 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,81 @@ 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)
);
}

/**
* 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) {
Comment thread
xbinaryx marked this conversation as resolved.
Outdated
let currentLine = 1;
let jsOffset = 0;
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 (
message.line >= currentLine &&
message.line < currentLine + commentLines
) {
foundComment = comment;
break;
}

currentLine += commentLines;
jsOffset += comment.text.length + 1;
}

if (!foundComment) {
return message;
}

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

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

if (fix) {
const isFullRemoval =
fix.range[0] <= jsOffset &&
fix.range[1] >= jsOffset + foundComment.text.length;

if (isFullRemoval) {
adjustedMessage.fix = {
range: [start.offset, end.offset],
text: fix.text,
};
} else {
// '4' is the length of '<!--' and '2' is the length of '/*'.
const offsetDelta = start.offset + 4 - (jsOffset + 2);
Comment thread
xbinaryx marked this conversation as resolved.

adjustedMessage.fix = {
range: [fix.range[0] + offsetDelta, fix.range[1] + offsetDelta],
text: fix.text,
};
}
}

return adjustedMessage;
}

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

/**
Expand Down Expand Up @@ -281,7 +358,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 +374,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 +409,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 +428,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 +472,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 +495,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
Loading
Loading