Skip to content

feat: add support for language plugins #257

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
Apr 12, 2025
Merged
Show file tree
Hide file tree
Changes from all 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
109 changes: 83 additions & 26 deletions lib/internal/disabled-area.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,25 +8,7 @@ const utils = require("./utils")
const DELIMITER = /[\s,]+/gu
const pool = new WeakMap()

module.exports = class DisabledArea {
/**
* Get singleton instance for the given source code.
*
* @param {eslint.SourceCode} sourceCode - The source code to get.
* @returns {DisabledArea} The singleton object for the source code.
*/
static get(sourceCode) {
let retv = pool.get(sourceCode.ast)

if (retv == null) {
retv = new DisabledArea()
retv._scan(sourceCode)
pool.set(sourceCode.ast, retv)
}

return retv
}

class DisabledArea {
/**
* Constructor.
*/
Expand All @@ -45,7 +27,7 @@ module.exports = class DisabledArea {
* @param {string[]|null} ruleIds - The ruleId names to disable.
* @param {string} kind - The kind of disable-comments.
* @returns {void}
* @private
* @protected
*/
_disable(comment, location, ruleIds, kind) {
if (ruleIds) {
Expand Down Expand Up @@ -85,7 +67,7 @@ module.exports = class DisabledArea {
* @param {string[]|null} ruleIds - The ruleId names to enable.
* @param {string} kind - The kind of disable-comments.
* @returns {void}
* @private
* @protected
*/
_enable(comment, location, ruleIds, kind) {
const relatedDisableDirectives = new Set()
Expand Down Expand Up @@ -159,13 +141,62 @@ module.exports = class DisabledArea {

return null
}
}

class DisabledAreaForLanguagePlugin extends DisabledArea {
/**
* Scan the source code and setup disabled area list.
*
* @param {import('@eslint/core').TextSourceCode} sourceCode - The source code to scan.
* @returns {void}
*/
_scan(sourceCode) {
const disableDirectives = sourceCode.getDisableDirectives()
for (const directive of disableDirectives.directives) {
if (
![
"disable",
"enable",
"disable-line",
"disable-next-line",
].includes(directive.type)
) {
continue
}
const ruleIds = directive.value
? directive.value.split(DELIMITER)
: null

const loc = sourceCode.getLoc(directive.node)
if (directive.type === "disable") {
this._disable(directive.node, loc.start, ruleIds, "block")
} else if (directive.type === "enable") {
this._enable(directive.node, loc.start, ruleIds, "block")
} else if (directive.type === "disable-line") {
const line = loc.start.line
const start = { line, column: 0 }
const end = { line: line + 1, column: -1 }

this._disable(directive.node, start, ruleIds, "line")
this._enable(directive.node, end, ruleIds, "line")
} else if (directive.type === "disable-next-line") {
const line = loc.start.line
const start = { line: line + 1, column: 0 }
const end = { line: line + 2, column: -1 }

this._disable(directive.node, start, ruleIds, "line")
this._enable(directive.node, end, ruleIds, "line")
}
}
}
}

class DisabledAreaForLegacy extends DisabledArea {
/**
* Scan the source code and setup disabled area list.
*
* @param {eslint.SourceCode} sourceCode - The source code to scan.
* @returns {void}
* @private
*/
_scan(sourceCode) {
for (const comment of sourceCode.getAllComments()) {
Expand All @@ -176,10 +207,12 @@ module.exports = class DisabledArea {

const kind = directiveComment.kind
if (
kind !== "eslint-disable" &&
kind !== "eslint-enable" &&
kind !== "eslint-disable-line" &&
kind !== "eslint-disable-next-line"
![
"eslint-disable",
"eslint-enable",
"eslint-disable-line",
"eslint-disable-next-line",
].includes(kind)
) {
continue
}
Expand Down Expand Up @@ -209,3 +242,27 @@ module.exports = class DisabledArea {
}
}
}

module.exports = {
/**
* Get singleton instance for the given rule context.
*
* @param {import("@eslint/core").RuleContext} context - The rule context code to get.
* @returns {DisabledArea} The singleton object for the rule context.
*/
getDisabledArea(context) {
const sourceCode = context.sourceCode || context.getSourceCode()
let retv = pool.get(sourceCode.ast)

if (retv == null) {
retv =
typeof sourceCode.getDisableDirectives === "function"
? new DisabledAreaForLanguagePlugin()
: new DisabledAreaForLegacy()
retv._scan(sourceCode)
pool.set(sourceCode.ast, retv)
}

return retv
},
}
154 changes: 154 additions & 0 deletions lib/internal/get-all-directive-comments.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
"use strict"

const utils = require("./utils")

/**
* @typedef {object} DirectiveComment
* @property {string} kind The kind of directive comment.
* @property {string} [value] The directive value if it is `eslint-` comment.
* @property {string} description The description of the directive comment.
* @property {object} node The node of the directive comment.
* @property {import("@eslint/core").SourceRange} range The range of the directive comment.
* @property {import("@eslint/core").SourceLocation} loc The location of the directive comment.
*/

const pool = new WeakMap()

/**
* @param {import('eslint').SourceCode} sourceCode - The source code to scan.
* @returns {DirectiveComment[]} The directive comments.
*/
function getAllDirectiveCommentsFromAllComments(sourceCode) {
return sourceCode
.getAllComments()
.map((comment) => ({
comment,
directiveComment: utils.parseDirectiveComment(comment),
}))
.filter(({ directiveComment }) => Boolean(directiveComment))
.map(
({ comment, directiveComment }) =>
/** @type {DirectiveComment} */ ({
kind: directiveComment.kind,
value: directiveComment.value,
description: directiveComment.description,
node: comment,
range: comment.range,
loc: comment.loc,
})
)
}

/**
* @param {import('@eslint/core').TextSourceCode} sourceCode - The source code to scan.
* @returns {DirectiveComment[]} The directive comments.
*/
function getAllDirectiveCommentsFromInlineConfigNodes(sourceCode) {
const result = sourceCode.getDisableDirectives().directives.map(
(directive) =>
/** @type {DirectiveComment} */ ({
kind: `eslint-${directive.type}`,
value: directive.value,
description: directive.justification,
node: directive.node,
range: sourceCode.getRange(directive.node),
get loc() {
return sourceCode.getLoc(directive.node)
},
})
)

return result.concat(
sourceCode
.getInlineConfigNodes()
.map((node) => ({
node,
range: sourceCode.getRange(node),
}))
.filter(
({ range }) =>
// The node has intersection of the directive comment.
// So, we need to skip it.
!result.some(
(comment) =>
comment.range[0] <= range[1] &&
range[0] <= comment.range[1]
)
)
.map(({ node, range }) => {
const nodeText = sourceCode.text.slice(range[0], range[1])
const commentValue = extractCommentContent(nodeText)
const directiveComment = utils.parseDirectiveText(commentValue)

return {
directiveComment,
node,
range,
}
})
.filter(
({ directiveComment }) =>
directiveComment != null &&
![
"eslint-disable",
"eslint-disable-line",
"eslint-disable-next-line",
"eslint-enable",
].includes(directiveComment.kind)
)
.map(
({ directiveComment, node, range }) =>
/** @type {DirectiveComment} */ ({
kind: directiveComment.kind,
value: directiveComment.value,
description: directiveComment.description,
node,
range,
get loc() {
return sourceCode.getLoc(node)
},
})
)
)
}

function extractCommentContent(text) {
// Extract comment content from the comment text.
// The comment format was based on the language comment definition in vscode-eslint.
// See https://github.com/microsoft/vscode-eslint/blob/c0e753713ea9935667e849d91e549adbff213e7e/server/src/languageDefaults.ts#L14
return text.startsWith("/*") && text.endsWith("*/")
? text.slice(2, -2)
: text.startsWith("//")
? text.slice(2)
: text.startsWith("<!--") && text.endsWith("-->")
? text.slice(4, -3)
: text.startsWith("###") && text.endsWith("###")
? text.slice(3, -3)
: text.startsWith("#")
? text.slice(1)
: text
}

module.exports = {
/**
* Get all directive comments for the given rule context.
*
* @param {import("@eslint/core").RuleContext} context - The rule context to get.
* @returns {DirectiveComment[]} The all directive comments object for the rule context.
*/
getAllDirectiveComments(context) {
const sourceCode = context.sourceCode || context.getSourceCode()
let result = pool.get(sourceCode.ast)

if (result == null) {
result =
typeof sourceCode.getInlineConfigNodes === "function" &&
typeof sourceCode.getDisableDirectives === "function"
? getAllDirectiveCommentsFromInlineConfigNodes(sourceCode)
: getAllDirectiveCommentsFromAllComments(sourceCode)
pool.set(sourceCode.ast, result)
}

return result
},
}
Loading