
当 ESLint v9.0.0 发布时,它将包含针对规则作者的几个重大更改。这些更改是实施 语言插件 工作的一部分,该插件为 ESLint 提供了对 JavaScript 以外的语言进行 linting 的一流支持。我们不得不做出这些更改,因为 ESLint 从一开始就假定它只会用于 linting JavaScript。因此,对于规则用于与源代码交互的方法应该放在哪里,并没有进行太多思考。当重新审视语言插件工作的 API 时,我们发现,在仅限 JavaScript 的世界中我们能够容忍的不一致性,在语言不可知的 ESLint 核心中将无法工作。
自动更新您的规则
在解释 ESLint v9.0.0 中引入的所有更改之前,了解可以使用 eslint-transforms 实用程序自动完成本文中描述的大部分更改是很有帮助的。要使用该实用程序,请先安装它,然后运行 v9-rule-migration 转换,如下所示
# install the utility
npm install eslint-transforms -g
# apply the transform to one file
eslint-transforms v9-rule-migration rule.js
# apply the transform to all files in a directory
eslint-transforms v9-rule-migration rules/
并非每个更改都可以通过 eslint-tranforms 解决,因此以下是 API 更改的完整列表以及解决这些更改的建议方法。
context 方法变为属性
当我们展望我们希望其他语言的规则拥有的 API 时,我们决定将 context 上的一些方法转换为属性。下表中的方法只是返回一些不会更改的数据,因此没有理由不将它们作为属性。
在 context 上已弃用 |
context 上的属性 |
|---|---|
context.getSourceCode() |
context.sourceCode |
context.getFilename() |
context.filename |
context.getPhysicalFilename() |
context.physicalFilename |
context.getCwd() |
context.cwd |
我们正在弃用这些方法,转而使用属性(在 v8.40.0 中添加)。这些方法将在 v10.0.0(而不是 v9.0.0)中删除,因为它们不会阻止语言插件工作。这是一个示例,确保使用了正确的值
module.exports = {
create(context) {
const sourceCode = context.sourceCode ?? context.getSourceCode();
const cwd = context.cwd ?? context.getCwd();
const filename = context.filename ?? context.getFilename();
const physicalFilename = context.physicalFilename ?? context.getPhysicalFilename();
return {
Program(node) {
// do something
}
}
}
};
从 context 到 SourceCode
规则作者的大部分重大更改包括将方法从传递到规则的 context 对象移动到通过 context.sourceCode (或已弃用的 context.getSourceCode();见下文)检索的 SourceCode 对象。context 与 SourceCode 的职责范围在 ESLint 的生命周期中发生了变化:最初,context 是规则需要使用的所有内容的家。一旦我们添加了 SourceCode,我们就慢慢开始向其添加更多方法。最终结果是,某些方法存在于 context 上,而某些方法存在于 SourceCode 上,而唯一的原因是什么?添加方法的时间。
在语言不可知的 ESLint 核心中,我们需要重新定义这两个对象的职责。展望未来,context 是规则需要与核心交互的功能的家,而 SourceCode 是规则需要与正在 linting 的代码交互的功能的家。这允许相同的 context 对象用于任何正在 linting 的语言,并允许语言插件定义自己的 SourceCode 类,以提供特定于该语言的方法。
所有这些都是为了说明我们正在弃用 context 上的所有与代码相关的方法,并将它们移动到 SourceCode。下表显示了 context 上的哪些字段正在移动到 SourceCode。请注意,即使名称更改,所有这些方法的 方法签名 保持不变
在 context 上已弃用 |
SourceCode 上的替换 |
|---|---|
context.getSource() |
sourceCode.getText() |
context.getSourceLines() |
sourceCode.getLines() |
context.getAllComments() |
sourceCode.getAllComments() |
context.getNodeByRangeIndex() |
sourceCode.getNodeByRangeIndex() |
context.getComments() |
sourceCode.getCommentsBefore(), sourceCode.getCommentsAfter(), sourceCode.getCommentsInside() |
context.getCommentsBefore() |
sourceCode.getCommentsBefore() |
context.getCommentsAfter() |
sourceCode.getCommentsAfter() |
context.getCommentsInside() |
sourceCode.getCommentsInside() |
context.getJSDocComment() |
sourceCode.getJSDocComment() |
context.getFirstToken() |
sourceCode.getFirstToken() |
context.getFirstTokens() |
sourceCode.getFirstTokens() |
context.getLastToken() |
sourceCode.getLastToken() |
context.getLastTokens() |
sourceCode.getLastTokens() |
context.getTokenAfter() |
sourceCode.getTokenAfter() |
context.getTokenBefore() |
sourceCode.getTokenBefore() |
context.getTokenByRangeStart() |
sourceCode.getTokenByRangeStart() |
context.getTokens() |
sourceCode.getTokens() |
context.getTokensAfter() |
sourceCode.getTokensAfter() |
context.getTokensBefore() |
sourceCode.getTokensBefore() |
context.getTokensBetween() |
sourceCode.getTokensBetween() |
context.parserServices |
sourceCode.parserServices |
此表中列出的所有 context 方法将在 ESLint v9.0.0 中删除,而 SourceCode 上的替换方法已经存在六年了,因此您应该可以轻松切换到新方法。(是的,我们弃用了这些方法,然后完全忘记删除它们。)
除了此表中的方法之外,还有其他几种方法也在移动,但需要不同的方法签名。
context.getScope()
context.getScope() 方法用于检索当前遍历节点的 scope 对象。此方法一直有点奇怪,因为它使用 ESLint 的内部遍历状态来确定要用作参考点以检索 scope 对象的节点。这意味着它既有限制性(因为您无法更改参考节点),又令人困惑(因为并不总是清楚引用的是哪个节点)。因此,我们正在弃用此方法,并将在 ESLint v9.0.0 中删除它。
我们引入了一个新的 SourceCode#getScope(node) 方法,该方法要求您传入参考节点。此方法已在 ESLint v8.37.0 中添加,因此在过去六个月中已经存在。为了获得最佳兼容性,您可以检查是否存在此新方法以确定要使用哪一个
module.exports = {
create(context) {
const sourceCode = context.sourceCode ?? context.getSourceCode();
return {
Program(node) {
const scope = sourceCode.getScope
? sourceCode.getScope(node)
: context.getScope();
// do something with scope
}
}
}
};
context.getAncestors()
context.getAncestors() 方法是 context 上的另一个方法,它使用内部遍历状态来返回当前访问节点的祖先。与 context.getScope() 类似,这意味着该方法既有限制性又不明确。我们正在弃用此方法,并将在 v9.0.0 中删除它。替换方法是 SourceCode#getAncestors(node) (在 v8.38.0 中添加),它要求您传入要检索其祖先的节点。这是一个示例,检查要使用的正确方法
module.exports = {
create(context) {
const sourceCode = context.sourceCode ?? context.getSourceCode();
return {
Program(node) {
const ancestors = sourceCode.getAncestors
? sourceCode.getAncestors(node)
: context.getAncestors();
// do something with ancestors
}
}
}
};
context.getDeclaredVariables(node)
context.getDeclaredVariables(node) 返回给定节点声明的所有变量(例如 let 语句中)。我们正在弃用此方法,并将在 v9.0.0 中删除它。我们正在用 SourceCode#getDeclaredVariables(node) (在 v8.38.0 中添加)替换它,其工作方式完全相同。这是一个示例,检查要使用的正确方法
module.exports = {
create(context) {
const sourceCode = context.sourceCode ?? context.getSourceCode();
return {
Program(node) {
const variables = sourceCode.getDeclaredVariables
? sourceCode.getDeclaredVariables(node)
: context.getDeclaredVariables(node);
// do something with variables
}
}
}
};
context.markVariableAsUsed(name)
context.markVariableAsUsed(name) 方法在当前 scope 中查找具有给定名称的变量,并将其标记为已使用,这样它就不会在 no-unused-vars 规则中引起违规。此方法在幕后进行了相当多的魔术,因为它使用当前访问的节点在遍历中检索 scope,然后在该 scope 中搜索具有给定名称的变量。我们正在弃用此方法,并将在 v9.0.0 中删除它。替换方法是 SourceCode#markVariableAsUsed(name, node) (在 v8.39.0 中添加),它要求您传入要搜索 scope 的参考节点。(scope 最终与调用 SourceCode#getScope(node) 相同。)这是一个示例,检查要使用的正确方法
module.exports = {
create(context) {
const sourceCode = context.sourceCode ?? context.getSourceCode();
return {
Program(node) {
const result = sourceCode.markVariableAsUsed
? sourceCode.markVariableAsUsed("foo", node)
: context.markVariableAsUsed("foo");
if (result) {
// the variable was found and marked as used
}
}
}
}
};
CodePath#currentSegments
ESLint 规则的一个鲜为人知的功能是 分析代码路径。ESLint 核心规则在多个规则中使用代码路径分析,不仅验证代码的外观,还验证逻辑如何流动。这是通过访问 CodePath 和 CodePathSegment 对象来完成的。在为语言插件进行研究时,我们发现 CodePath#currentSegments 实际上代表了在规则中公开的另一个遍历状态。具体来说,CodePath#currentSegments 是一个数组,它在整个遍历过程中随着您遇到不同的代码路径段而增长和缩小。由于代码路径分析是 JavaScript 独有的,因此我们不能再让核心跟踪此遍历状态。在评估了几个选项后,我们决定拥有一个既表示代码路径数据又表示遍历状态的对象是不可取的,因此我们正在弃用 CodePath#currentSegments,并将在 v9.0.0 中删除它。我们需要添加两个新的事件处理程序 onUnreachableCodePathSegmentStart 和 onUnreachableCodePathSegmentEnd,以允许访问相同的数据(这些已在 v8.49.0 中添加)。
要重新创建此数据,您需要手动跟踪遍历状态,这可以通过以下代码完成
module.exports = {
meta: {
// ...
},
create(context) {
// tracks the code path we are currently in
let currentCodePath;
// tracks the segments we've traversed in the current code path
let currentSegments;
// tracks all current segments for all open paths
const allCurrentSegments = [];
return {
onCodePathStart(codePath) {
currentCodePath = codePath;
allCurrentSegments.push(currentSegments);
currentSegments = new Set();
},
onCodePathEnd(codePath) {
currentCodePath = codePath.upper;
currentSegments = allCurrentSegments.pop();
},
onCodePathSegmentStart(segment) {
currentSegments.add(segment);
},
onCodePathSegmentEnd(segment) {
currentSegments.delete(segment);
},
onUnreachableCodePathSegmentStart(segment) {
currentSegments.add(segment);
},
onUnreachableCodePathSegmentEnd(segment) {
currentSegments.delete(segment);
}
};
}
};
我们已经在所有 ESLint 核心规则中进行了此更改,以验证该方法是否按预期工作。
context 属性:parserOptions 和 parserPath 将被删除
此外,context.parserOptions 和 context.parserPath 属性已被弃用,并将在 v10.0.0(而不是 v9.0.0)中删除。有一个新的 context.languageOptions 属性,允许规则访问与 context.parserOptions 类似的数据。但总的来说,规则不应依赖 context.parserOptions 或 context.languageOptions 中的信息来确定它们的行为方式。
context.parserPath 属性旨在允许规则通过 require() 检索 ESLint 正在使用的解析器的实例。但是,新的扁平配置系统不知道要加载的解析器模块的位置,因此我们无法提供此数据。此外,由于 JavaScript 生态系统正在转向 ESM,因此从此属性返回的任何值都将无法与 import() 一起使用。此属性是在 ESLint 生命周期的早期添加的,我们通常建议规则不要尝试在其中进一步解析 JavaScript 代码。如有必要,您可以使用 context.languageOptions.parser 访问 ESLint 正在使用的解析器。
结论
ESLint 已经存在十年了,在这段时间里,我们积累了一些 API 冗余,我们需要清理这些冗余,以便为 ESLint 的未来十年做好准备。本文中描述的 API 更改是使 ESLint 能够 linting 非 JavaScript 语言以及更好地将核心功能与特定于语言的功能分离的必要步骤。团队花费了大量时间来规划 ESLint 生命周期中的这个过渡点,我们希望这些更改对生态系统来说只是一个小小的麻烦。如果您需要帮助或对本文中讨论的任何内容有疑问,请发起讨论或访问 Discord 与团队交谈。
更新(2024-06-06): 添加了关于 eslint-tranforms 的部分。
