为 ESLint v9.0.0 准备自定义规则

即将发布的 ESLint 重大版本包含对规则作者的几项重大更改。继续阅读以了解如何为 ESLint v9.0.0 更新你的规则。

当 ESLint v9.0.0 发布时,它将为规则作者带来一些重大变更。这些变更是实现语言插件工作的一部分所必需的,该功能使 ESLint 能够原生支持对 JavaScript 以外语言的代码进行 lint。我们不得不进行这些更改,因为 ESLint 从一开始就假设它只会用于 lint JavaScript。因此,对于规则用来与源代码交互的方法应该放在哪里,并没有太多考虑。在重新审视语言插件工作的 API 时,我们发现,在仅限 JavaScript 的世界中能够接受的不一致性,在面向语言无关的 ESLint 核心中将无法工作。

🌐 When ESLint v9.0.0 is released, it will ship with several breaking changes for rule authors. These changes are necessary as part of the work to implement language plugins, which gives ESLint first-class support for linting languages other than JavaScript. We’ve had to make these changes because ESLint has, from the start, assumed that it would only ever be used to lint JavaScript. As such, there wasn’t a lot of thought put into where methods that rules used to interact with source code should live. When revisiting the API for the language plugins work we found that the inconsistencies we were able to live with in a JavaScript-only world will not work in a language-agnostic ESLint core.

自动更新你的规则

🌐 Automatically update your rules

在解释 ESLint v9.0.0 引入的所有更改之前,了解本文中描述的大多数更改可以使用 eslint-transforms 工具自动补齐是有帮助的。要使用该工具,首先安装它,然后运行 v9-rule-migration 转换,如下所示:

🌐 Before explaining all of the changes introduced in ESLint v9.0.0, it’s helpful to know that most of the changes described in this post can be automatically made using the eslint-transforms utility. To use the utility, first install it and then run the v9-rule-migration transform, like this:

# 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 更改的完整列表以及推荐的解决方法。

🌐 Not every change can be addressed with eslint-tranforms, though, so below is a complete list of the API changes and recommended ways to address them.

context 方法变成属性

🌐 context methods becoming properties

在我们展望希望其他语言也能拥有的 API 时,我们决定将 context 上的一些方法转换为属性。下表中的方法只是返回一些不会变化的数据,所以它们完全可以作为属性而不是方法。

🌐 As we look towards the API we’d like rules for other languages to have, we decided to convert some methods on context to properties. The methods in the following table just returned some data that didn’t change, so there was no reason they couldn’t be properties instead.

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),因为它们不会阻碍语言插件的工作。下面是一个确保使用正确值的示例:

🌐 We are deprecating the methods in favor of the properties (added in v8.40.0). These methods will be removed in v10.0.0 (not v9.0.0) as they are not blocking language plugins work. Here’s an example that ensures the correct value is used:

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
            }
        }
    }
};

contextSourceCode

🌐 From context to SourceCode

对规则作者而言,大多数重大变更包括将传入规则的 context 对象上的方法移到通过 context.sourceCode 获取的 SourceCode 对象上(或已弃用的 context.getSourceCode();见下文)。在 ESLint 的生命周期中,contextSourceCode 的责任范围发生了变化:一开始,context 是规则所需的所有内容的归宿。随着我们添加了 SourceCode,我们逐渐开始向其添加更多方法。最终的结果是,一些方法存在于 context 上,而一些方法存在于 SourceCode 上,其唯一原因是什么?就是这些方法被添加的时间。

🌐 The majority of the breaking changes for rule authors consist of moving methods off of the context object that is passed into rules and onto the SourceCode object that is retrieved via context.sourceCode (or the deprecated context.getSourceCode(); see below). The area of responsibility for context vs. SourceCode has shifted during the lifetime of ESLint: In the beginning, context was the home for everything rules needed to use. Once we added SourceCode, we slowly started adding more methods to it. The end result was that some methods lived on context and some methods lived on SourceCode, and the only reason why? The time at which the methods were added.

在一个与语言无关的 ESLint 核心中,我们需要重新定义这两个对象的职责。今后,context 是规则需要与核心交互功能的所在,而 SourceCode 是规则需要与被检查代码交互功能的所在。这允许同一个 context 对象在无论检查哪种语言时都能使用,同时也允许语言插件定义它们自己的 SourceCode 类来提供该语言特有的方法。

🌐 In a language-agnostic ESLint core, we need to redefine the responsibilities of these two objects. Going forward, context is the home for functionality that rules need to interact with the core while SourceCode is the home for functionality that rules need to interact with the code being linted. This allows the same context object to be used regardless of the language being linted as well as allowing language plugins to define their own SourceCode class to provide methods that are unique to that language.

所有这些都是为了说明,我们正在弃用 context 上的所有与代码相关的方法,并将它们迁移到 SourceCode。下表显示了 context 上哪些字段将迁移到 SourceCode。请注意,即使名称发生变化,这些方法的签名也保持不变:

🌐 All of this is to say that we are deprecating all of the code-related methods on context and moving them to SourceCode. The following table shows which fields on context are moving to SourceCode. Note that the method signatures remain unchanged for all of these methods even if the name changes:

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 上的替代方法已经存在六年,因此你切换到新方法应该没有问题。(是的,我们确实弃用了这些方法,但随后完全忘记将它们移除。)

🌐 All of the context methods listed in this table will be removed in ESLint v9.0.0, and the replacement methods on SourceCode have already been in place for six years, so you should have no problem switching to the new methods. (Yes, we deprecated these and then completely forgot to remove them.)

除了表中的方法之外,还有其他几种方法也在移动,但需要不同的方法签名。

🌐 In addition to the methods in this table, there are several other methods that are also moving but required different method signatures.

context.getScope()

context.getScope() 方法用于获取当前遍历节点的作用域对象。这个方法一直有点奇怪,因为它使用 ESLint 的内部遍历状态来确定使用哪个节点作为参考点来获取作用域对象。这意味着它既有限制,因为你不能更改参考节点,又令人困惑,因为不总是清楚引用的是哪个节点。因此,我们正在弃用此方法,并将在 ESLint v9.0.0 中移除它。

🌐 The context.getScope() method is used to retrieve a scope object for the currently-traversed node. This method was always a bit strange because it uses ESLint’s internal traversal state to determine which node to use as a reference point to retrieve a scope object. That meant it was both limited, because you couldn’t change the reference node, and confusing, because it wasn’t always clear what node was being referenced. So, we are deprecating this method and will remove it in ESLint v9.0.0.

我们引入了一个新的 SourceCode#getScope(node) 方法,需要你传入参考节点。该方法在 ESLint v8.37.0 中添加,因此在过去六个月中已经存在。为了获得最佳兼容性,你可以检查此新方法的存在来确定使用哪一个方法:

🌐 We have introduced a new SourceCode#getScope(node) method that requires you to pass in the reference node. This method was added in ESLint v8.37.0 so it has already been in place for the last six months. For best compatibility, you can check for the presence of this new method to determine which one to use:

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 中添加),该方法要求你传入要获取祖级的节点。下面是一个检查使用正确方法的示例:

🌐 The context.getAncestors() method is another method on context that uses the internal traversal state to return the ancestors of the currently visited node. Also similar to context.getScope(), this meant the method was both limited and unclear. We are deprecating this method and will remove it in v9.0.0. The replacement method is SourceCode#getAncestors(node) (added in v8.38.0), which requires you to pass in the node whose ancestors you want to retrieve. Here is an example that checks for the correct method to use:

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 中添加)来替代,它的工作方式完全相同。以下是一个检查要使用的正确方法的示例:

🌐 The context.getDeclaredVariables(node) returns all variables declared by the given node (such as in a let statement). We are deprecating this method and will remove it in v9.0.0. We are replacing it with SourceCode#getDeclaredVariables(node) (added in v8.38.0), which works exactly the same way. Here is an example that checks for the correct method to use:

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) 方法在当前作用域中查找具有给定名称的变量,并将其标记为已使用,以避免在 no-unused-vars 规则中引发违规。该方法在背后有相当多的魔法操作,因为它使用遍历中当前访问的节点来检索作用域,然后在该作用域中搜索具有给定名称的变量。我们正在弃用此方法,并将在 v9.0.0 中移除它。替代方法是 SourceCode#markVariableAsUsed(name, node)(在 v8.39.0 中添加),需要你传入用于搜索的作用域的引用节点。(最后得到的作用域与调用 SourceCode#getScope(node) 得到的作用域相同。)下面是一个检查应使用正确方法的示例:

🌐 The context.markVariableAsUsed(name) method finds a variable with the given name in the current scope and marks it as used so it won’t cause a violation in the no-unused-vars rule. This method has quite a bit of magic going on behind the scenes, as it uses the currently visited node in the traversal to retrieve a scope and then searches that scope for a variable with the given name. We are deprecating this method and will remove it in v9.0.0. The replacement method is SourceCode#markVariableAsUsed(name, node) (added in v8.39.0) and requires you to pass in the reference node for the scope to search. (The scope ends up being the same as calling SourceCode#getScope(node).) Here is an example that checks for the correct method to use:

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 核心规则在多个规则中使用代码路径分析,不仅验证代码的外观,还验证逻辑的流程。这是通过访问 CodePathCodePathSegment 对象来完成的。在为语言插件进行研究时,我们发现 CodePath#currentSegments 实际上代表了规则中暴露的另一个遍历状态。具体来说,CodePath#currentSegments 是一个数组,在遍历过程中会随着遇到不同的代码路径段而增长和缩小。由于代码路径分析是 JavaScript 独有的,我们不能再让核心跟踪这个遍历状态。在评估了几种选项后,我们决定将表示代码路径数据和遍历状态的对象结合在一起是不理想的,所以我们正在废弃 CodePath#currentSegments,并将在 v9.0.0 中移除它。我们需要添加两个新的事件处理器 onUnreachableCodePathSegmentStartonUnreachableCodePathSegmentEnd,以允许访问相同的数据(这些在 v8.49.0 中添加)。

🌐 A little-known ability of ESLint rules is analyzing code paths. ESLint core rules use code path analysis in multiple rules to validate not just what the code looks like but also how the logic flows. This is done through accessing CodePath and CodePathSegment objects. In doing our research for language plugins, we discovered that CodePath#currentSegments actually represents another traversal state that is exposed in rules. Specifically, CodePath#currentSegments is an array that grows and shrinks throughout traversal as you encounter different code path segments. Because code path analysis is unique to JavaScript, we can’t have the core tracking this traversal state any longer. After evaluating several options, we decided that having an object that represented both code path data and traversal state was undesirable, so we are deprecating CodePath#currentSegments and will remove it in v9.0.0. We needed to add two new event handlers, onUnreachableCodePathSegmentStart and onUnreachableCodePathSegmentEnd, to allow access to the same data (these were added in v8.49.0).

要重新创建这些数据,你需要手动跟踪遍历状态,这可以通过以下代码实现:

🌐 To recreate this data, you’ll need to track the traversal state manually, which can be accomplished with the following code:

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 核心规则中进行了此更改,以验证该方法是否按预期工作。

🌐 We have already made this change in all of the ESLint core rules to validate that the approach works as expected.

context 属性:parserOptionsparserPath 被移除

🌐 context properties: parserOptions and parserPath being removed

此外,context.parserOptionscontext.parserPath 属性已被弃用,并将在 v10.0.0(不是 v9.0.0)中移除。新增了 context.languageOptions 属性,允许规则访问与 context.parserOptions 类似的数据。不过,一般来说,规则不应依赖 context.parserOptionscontext.languageOptions 中的信息来决定它们的行为。

🌐 Additionally, the context.parserOptions and context.parserPath properties are deprecated and will be removed in v10.0.0 (not v9.0.0). There is a new context.languageOptions property that allows rules to access similar data as context.parserOptions. In general, though, rules should not depend on information either in context.parserOptions or context.languageOptions to determine how they should behave.

context.parserPath 属性旨在允许规则通过 require() 获取 ESLint 使用的解析器实例。然而,新的扁平配置系统不知道要加载的解析器模块的位置,因此我们无法提供此数据。此外,由于 JavaScript 生态系统正在向 ESM 迁移,从此属性返回的任何值都无法与 import() 一起使用。此属性是在 ESLint 早期阶段添加的,我们通常建议规则不要在其中尝试进一步解析 JavaScript 代码。如有必要,你可以使用 context.languageOptions.parser 来访问 ESLint 正在使用的解析器。

🌐 The context.parserPath property was intended to allow rules to retrieve an instance of the parser that ESLint is using via require(). However, the new flat config system does not know the location of the parser module to load, so we are unable to provide this data. Further, because the JavaScript ecosystem is moving to ESM, any value returned from this property will not work with import(). This property was added early on in ESLint’s life and we generally recommend that rules not try to further parse JavaScript code inside of them. If necessary, you can use context.languageOptions.parser to access the parser ESLint is using.

结论

🌐 Conclusion

ESLint 已经存在十年了,在这段时间里,我们积累了一些需要清理的 API 冗余,以便为 ESLint 的下一个十年做准备。本文中描述的 API 变更是使 ESLint 能够检查非 JavaScript 语言并更好地将核心功能与特定语言功能分离的必要步骤。团队花了很多时间规划 ESLint 生命周期中的这一过渡点,我们希望这些变化对生态系统来说只是一个小小的不便。如果你需要帮助,或者对本文讨论的内容有任何疑问,请 开始讨论 或访问 Discord 与团队交流。

🌐 ESLint has been around for ten years, and in that time, we have collected some API cruft that we need to clean up in order to prepare ESLint for the next ten years. The API changes described in this post are a necessary step towards enabling ESLint to lint non-JavaScript languages and to better separate core functionality from language-specific functionality. The team spent a lot of time planning this transition point in ESLint’s lifecycle and we hope that these changes are just a small inconvenience for the ecosystem. If you need help with, or have questions about, any of what was discussed in this post, please start a discussion or stop by Discord to talk with the team.

更新(2024-06-06): 添加了关于 eslint-tranforms 的部分。

最新的 ESLint 新闻、案例研究、教程和资源。

ESLint v10.3.0 发布
1 min read

ESLint v10.3.0 发布

我们刚刚发布了 ESLint v10.3.0,这是 ESLint 的一次小版本升级。此版本添加了一些新功能,并修复了上一版本中发现的几个错误。

ESLint v10.2.1 发布
1 min read

ESLint v10.2.1 发布

我们刚刚发布了 ESLint v10.2.1,这是 ESLint 的一个补丁版本升级。本次发布修复了上一版本中发现的几个错误。

ESLint v10.2.0 发布
2 min read

ESLint v10.2.0 发布

我们刚刚发布了 ESLint v10.2.0,这是 ESLint 的一次小版本升级。此版本添加了一些新功能,并修复了上一版本中发现的几个错误。