eslint 开发指南


https://eslint.bootcss.com/docs/developer-guide/architecture

架构

  • bin:ESLint 安装后可用的可执行文件
    • bin/eslint.js:这个是命令行应用程序实际上执行的文件. 它仅仅是个封装,用来启动 ESLint,并向 cli 传递命令行参数。该文件有意保持很小,以免需要大量的测试。
  • lib:包含源码
    • lib/cli.js:这个是 ESLint CLI 的核心。它需要一个参数数组,然后使用 eslint 去执行相应的命令。
      • 注释:提供 cli 对象
      • 通过保持这个文件作为一个单独的应用程序,它允许其他人在另外的 Node.js 程序中有效的调用 ESLint,就好像是在命令行上操作的一样。
      • 它最重要的函数是 cli.execute() 。它也扮演着读取文件、遍历目录,输入和输出的角色。该方法接收一个代表命令行选项的字符串数组
      • 对象的职责包括使用一个格式化器
    • lib/api.js:输出一个对象,包括 Linter、CLIEngine、RuleTester 和 SourceCode。
      • 注释:看起来像是一个统一出口
      • CLIEngine:代表 CLI 的核心功能,读取配置文件和源码文件,同时也管理传递到 eslint 对象的环境
        • 接受很多(不是全部)传递给 CLI 的参数
        • 的主要方法是 executeOnFiles(),接收一个要被检查的文件和目录名的数组
        • 注释:eslint对象应该就是Linter
    • lib/linter.js:这是核心的 Linter 类,它根据配置选项进行代码验证。
      • 该文件不进行文件 I/O 操作,也不直接与命令行进行交互。
      • 对于其他需要进行 JavaScript 文本验证的 Node.js 程序,可以直接使用此接口。
      • Linter:
        • eslint 对象的主要方法是 verify(),接收两个参数:要验证的源码文本和一个配置对象
          • 该方法首先使用 espree(或配置的解析器) 解析获取的文本,检索 AST
          • AST 用来产生行/列和范围的位置,对报告问题的位置和检索与 AST 节点有关的源文本很有帮助
        • estraverse 被用来从上到下遍历 AST,在每个节点,eslint对象触发与该节点类型同名的一个事件(即 “Identifier”,”WithStatement” 等)
          • 在回退到子树上时,一个带有 AST 类型名称和 “:exit” 后缀的事件被触发,比如 “Identifier:exit” - 这允许规则在正向和逆向遍历开始起作用
        • 对象的职责包括:报告执行的结果
    • lib/testers/rule-tester.js:对 Mocha 进行了包装,以便对规则进行单元测试。
      • 这个类允许我们对每个规则编写一致的格式化的测试,以确保规则运行正常。
      • RuleTester 接口参照了 Mocha,兼容 Mocha 全局测试方法。RuleTester 也可以与其他测试框架一起使用。
    • lib/util/source-code.js:它包含了一个 SourceCode 类,用来展现解析后的源代码。它使用源码和 AST 作为参数。
      • 疑问:展现源码?

源码

  • ESLint 的目录和文件结构如下:
    • bin - ESLint 安装后可用的可执行文件
    • conf - 默认配置信息
    • docs - 项目文档
    • lib - 包含源码
      • formatters - 定义 formatter 的所有源文件
      • rules - 定义规则的所有源文件
    • tests - 单元测试文件夹
      • lib - 源码的测试
      • formatters - formatter 的测试
      • rules - 规则的测试

开发环境

创建规则

  • ESLint 中的每个规则都有三个文件,以它的 ID 命名(例如,no-extra-semi)。
    • lib/rules 目录:源码文件 (例如,no-extra-semi.js)
    • tests/lib/rules 目录:测试文件 (例如,no-extra-semi.js)
  • 一个规则的源码文件的基本格式
    • 一个规则可以使用当前节点和它周围的树,报告或修复问题
/**
 * @fileoverview Rule to disallow unnecessary semicolons 文件说明
 * @author Nicholas C. Zakas 作者
 */

"use strict";

//------------------------------------------------------------------------------
// Rule Definition 规则定义
//------------------------------------------------------------------------------

module.exports = {
    meta: { // 包含规则的元数据
        type: "suggestion", // 规则的类型,值为 "problem"(问题、错误)、"suggestion"(建议) 或 "layout"(格式)

        docs: {
            description: "disallow unnecessary semicolons", // 描述
            category: "Possible Errors", // 分类 https://eslint.bootcss.com/docs/rules/
            recommended: true, // "extends": "eslint:recommended"属性是否启用该规则
            url: "https://eslint.org/docs/rules/no-extra-semi" // 完整文档的 url
        },
        fixable: "code", // 如果没有 fixable 属性,即使规则实现了 fix 功能,ESLint 也不会进行修复
        schema: [] // no options 可选项
        // deprecated (boolean) 表明规则是已被弃用
        // replacedBy (array) 在不支持规则的情况下,指定替换的规则
    },
    create: function(context) { // 返回一个对象,其中包含了 ESLint 在遍历 JavaScript 代码的抽象语法树 AST (ESTree 定义的 AST) 时,用来访问节点的方法
        return {
            // callback functions
            // 如果一个 key 是个节点类型或 selector,在 向下 遍历树时,ESLint 调用 visitor 函数
              // 注释:节点类型,根据 ESTree 定义的 AST 规范
              // 注释:selector
                // 注释:用于查询AST节点,一个字符串,可用于匹配抽象语法树(AST)中的节点。
                // 注释:有固定的选择器字符串,例如 Identifier 将匹配所有 Identifier 的节点
                // 注释:VariableDeclarator > Identifier 将匹配父节点是 VariableDeclarator 的 Identifier 子节点
              // 注释:visitor 函数,节点或 selector 对应的函数
            // 如果一个 key 是个节点类型或 selector,并带有 :exit,在 向上 遍历树时,ESLint 调用 visitor 函数
            // 例如:有3个以上参数的函数节点
            "FunctionDeclaration[params.length>3]": function(functionDeclarationNode) {
              // ...your logic here
            },
            // 如果一个 key 是个事件名字,ESLint 为代码路径分析调用 handler 函数
            onCodePathStart: function (codePath, node) {
                // at the start of analyzing a code path
            },
        };
    }
};
  • selector:支持的选择器 https://eslint.bootcss.com/docs/developer-guide/selectors

    • ForStatement AST节点类型
    • *:通配符,匹配所有节点
    • [attr]
    • [attr="foo"] or [attr=123]
    • [attr=/foo.*/]
    • [attr!="foo"], [attr>2], [attr<3], [attr>=2], or [attr<=3]
    • [attr.level2="foo"] 嵌套属性?
    • FunctionDeclaration > Identifier.id 具有id属性?
    • :first-child or :last-child
    • :nth-child(2)
    • :nth-last-child(1)
    • FunctionExpression ReturnStatement 后代选择器
    • UnaryExpression > Literal
    • ArrayExpression > Literal + SpreadElement 兄弟选择器
    • VariableDeclaration ~ VariableDeclaration 后续选择器,兄弟选择器只选择相邻,后续选择元素之后的匹配元素
    • :not(ForStatement) 反选,非 ForStatement 节点
    • :matches([attr] > :first-child, :last-child) 匹配任何?
    • :statement, :expression, :declaration, :function, or :pattern AST节点类?
    • 注释:如果有两个选择器匹配节点,将按照类css选择器的权重执行
      • 注释:如果权重相同,则按子母顺序
      • 注释:有些规则允许传入选择器字符串作为规则的参数,例如:no-restricted-syntax 规则用于限制js的某些特性,就允许接收选择器字符串"no-restricted-syntax": ["error", "IfStatement > :not(BlockStatement).consequent"]
  • 事件名字 https://eslint.bootcss.com/docs/developer-guide/code-path-analysis

    • "onCodePathStart": function(codePath, node) 在分析代码路径开始时调用的。此时,代码路径对象只有初始段。
    • "onCodePathEnd": function(codePath, node) 在分析代码路径的末尾调用的。此时,代码路径对象已完成。
    • "onCodePathSegmentStart": function(segment, node) 这在创建代码路径段时调用。这意味着代码路径被分叉或合并。在这段时间内,该段具有之前的段,并且已被判断为可到达或不可到达。segment 新的代码路径
    • "onCodePathSegmentEnd": function(segment, node) 这在代码路径段离开时调用。此时,该段还没有下一段。segment 留下的代码路径段。
    • "onCodePathSegmentLoop": function(fromSegment, toSegment, node) 当循环代码路径段时调用。在创建段时,段具有每个先前的段,每循环一次就添加一个新的段
      • fromSegment 源代码的代码路径段
      • toSegment 目的地的代码路径段。
  • 代码路径对象有两种 https://eslint.bootcss.com/docs/developer-guide/code-path-analysis

    • 注释:程序可以由若干 code path 表达,一个 code path 可能包括两种类型的对象 CodePath 和 CodePathSegment
    • CodePath 代码路径表示一条代码路径的整体。每个函数和全局函数都存在此对象。具有以下属性
      • id (string) 相应的规则可以使用id为每个代码路径保存附加信息?
      • initialSegment (CodePathSegment) 此代码路径的初始段
      • finalSegments (CodePathSegment[]) 包括返回和抛出的最后一段。
      • returnedSegments (CodePathSegment[]) 只包含返回的最后一段。
      • thrownSegments (CodePathSegment[]) 最后的片段只包括抛出的片段。
      • currentSegments (CodePathSegment[]) 当前位置的段?
      • upper (CodePath|null) 上层函数/全局作用域的代码路径。
      • childCodePaths (CodePath[]) 此代码路径包含的函数的代码路径。
    • CodePathSegment 代码路径由多个CodePathSegment对象表示,具有以下属性
      • id (string) A unique string. Respective rules can use id to save additional information for each segment.
      • nextSegments (CodePathSegment[]) 接下来的部分。如果分叉,则有两个或更多。如果是最终的,什么都没有。
      • prevSegments (CodePathSegment[]) 前面的部分。如果合并,则有两个或更多。如果是首段,则什么都没有。
      • reachable (boolean) 显示是否可以到达的标志。当前面加上return、throw、break或continue时,该值变为false。
    • 以上两个对象实例共享给每个规则,因此,规则不能修改这些实例。
  • 代码路径相关例子

    • no-unreachable 这条规则不允许可达代码后return,throw,continue,和break语句。https://github.com/eslint/eslint/blob/main/lib/rules/no-unreachable.js
    • no-fallthrough 强制每个case语句以throw,return,break或comment结尾 https://github.com/eslint/eslint/blob/main/lib/rules/no-fallthrough.js
    • consistent-return 某个函数中的任何代码路径显式返回一个值,但某些代码路径不会显式返回值,则可能是输入错误 https://github.com/eslint/eslint/blob/main/lib/rules/consistent-return.js
    • constructor-super 该规则检查是否存在有效的super()调用。https://github.com/eslint/eslint/blob/main/lib/rules/constructor-super.js
    • no-this-before-super 在派生类的构造函数中,如果在super调用之前使用this则会报错 https://github.com/eslint/eslint/blob/main/lib/rules/no-this-before-super.js
  • context对象包含与规则上下文相关的信息

    • parserOptions - 解析器选项
    • id - 规则 ID
    • options - 此规则的 已配置的选项 数组。此数组不包含规则严重性。
    • settings - 配置中的共享的设置。
    • parserPath - 配置中的 parser(解析器) 的名称
    • parserServices - 包含由解析器为规则提供的服务的对象。
      • 默认解析器不提供任何服务。然而,如果规则打算与自定义解析器一起使用,则可以使用 parserServices 访问该解析器提供的任何内容。(例如,TypeScript 解析器可以提供获取给定节点的计算类型的能力。)
    • getAncestors() - 返回当前遍历节点的祖先数组,从 AST 的根节点开始,一直到当前节点的直接父节点。
    • getDeclaredVariables(node) - 返回由给定节点声明的变量列表。此信息可用于跟踪对变量的引用
      • 如果节点是 VariableDeclaration,则返回声明中声明的所有变量。
      • 如果节点是一个 VariableDeclarator,则返回 declarator 中声明的所有变量。
      • 如果节点是 FunctionDeclaration 或 FunctionExpression,除了函数参数的变量外,还返回函数名的变量。
      • 如果节点是一个 ArrowFunctionExpression,则返回参数的变量。
      • 如果节点是 ClassDeclaration 或 ClassExpression,则返回类名的变量。
      • 如果节点是一个 CatchClause 子句,则返回异常的变量。
      • 如果节点是 ImportDeclaration,则返回其所有说明符的变量。
      • 如果节点是 ImportSpecifier,ImportDefaultSpecifier 或 ImportNamespaceSpecifier,则返回声明的变量。
    • getFilename() - 返回与源文件关联的文件名。
    • getScope() - 返回当前遍历节点的 scope。此信息可用于跟踪对变量的引用。
      • Scope:作用域对象,对象具有作用域中的所有变量和引用。
      • 注释:只返回以下作用域(节点类型:scope类型)
        • Program:global
        • FunctionDeclaration:function
        • FunctionExpression:function
        • ArrowFunctionExpression:function
        • ClassDeclaration:class
        • ClassExpression:class
        • BlockStatement ※1:block
        • SwitchStatement ※1:switch
        • ForStatement ※2:for
        • ForInStatement ※2:for
        • ForOfStatement ※2:for
        • WithStatement:with
        • CatchClause:catch
        • others ※3
        • ※1 仅当配置的解析器提供块作用域特性时才使用。parserOptions.ecmaVersion不小于 6 。
        • ※2 只有当 for 语句将迭代变量定义为块作用域的变量时 (例如,for (let i = 0;?? {})。
        • ※3 具有自己作用域的最近祖先节点的作用域。如果最近的祖先节点有多个作用域,那么它选择最内部的作用域(例如,如果Program#sourceType 是 "module",则 Program 节点有一个 global 作用域和一个 module 作用域。最内层的作用域是 "module" 作用域。)。
    • getSourceCode() - 返回一个SourceCode对象,你可以使用该对象处理传递给 ESLint 的源代码。
      • SourceCode:源码对象
        • hasBOM - 标记源码中是否含有 Unicode BOM。(Unicode BOM一种编码格式)
        • text - 被检查的代码全文,Unicode BOM 已经从该文本中剥离。
        • ast - AST 的 Program(程序) 节点,用于代码检查
        • visitorKeys - the visitor keys to traverse this AST.(疑问:遍历 AST 的?)
        • lines - 一个包含所有行的数组,是根据规范中的换行符的定义划分的。
        • getText(node) - 返回给定节点的源码。省略 node,返回整个源码。
        • getAllComments() - 返回一个包含源中所有注释的数组。
        • getCommentsBefore(nodeOrToken) - 返回一个在给定的节点或 token 之前的注释的数组。
        • getCommentsAfter(nodeOrToken) - 返回一个在给定的节点或 token 之后的注释的数组。
        • getCommentsInside(node) - 返回一个在给定的节点内的注释的数组。
        • getJSDocComment(node) - 返回给定节点的 JSDoc 注释,如果没有则返回 null。
        • isSpaceBetweenTokens(first, second) - 如果两个记号之间有空白,返回 true(疑问:记号是什么?)
        • getFirstToken(node, skipOptions) - 返回代表给定节点的第一个token。(疑问:token是什么?)
          • skipOptions 是个对象,包含三个属性;skip、includeComments 和 filter。默认是 {skip: 0, includeComments: false, filter: null}
            • skip 是个正整数,表示要跳过的 token 的数量。如果同时给出了 filter 选项,过滤掉的 token 不计入此值。
            • includeComments 是个布尔值,标记是否把注释 token 包含进返回结果中。
            • filter 是个函数,用一个 token 作为第一个参数,如果该函数返回 false,那么返回的结果将不包含那个 token。
        • getFirstTokens(node, countOptions) - 返回代表给定节点的第一个 count 数量的 token。(注释:返回指定数量的 token)
          • countOptions 是个对象包含三个属性;count、includeComments 和 filter。默认为 {count: 0, includeComments: false, filter: null}。
            • count 是个正整数,返回的 token 的最大数量。
            • includeComments 是个布尔值,标记是否把注释 token 包含进返回结果中。
            • filter 是个函数,用一个 token 作为第一个参数,如果该函数返回 false,那么返回的结果将不包含那个 token。
        • getLastToken(node, skipOptions) - 返回代表给定节点最后一个token。
        • getLastTokens(node, countOptions) - 返回代表给定节点的最后一个 count 数量的 token。
        • getTokenAfter(nodeOrToken, skipOptions) - 返回给定的节点或记号之后的第一个token。
        • getTokensAfter(nodeOrToken, countOptions) - 返回给定节点或记号之后的 count 数量的 token。
        • getTokenBefore(nodeOrToken, skipOptions) - 返回给定的节点或记号之前的第一个 token。
        • getTokensBefore(nodeOrToken, countOptions) - 返回给定节点或记号之前的 count 数量的 token。
        • getFirstTokenBetween(nodeOrToken1, nodeOrToken2, skipOptions) - 返回两个节点或 token 之间的第一个 token。
        • getFirstTokensBetween(nodeOrToken1, nodeOrToken2, countOptions) - 返回两个节点或 token 之间的第一个 count 数量的 token。
        • getLastTokenBetween(nodeOrToken1, nodeOrToken2, skipOptions) - 返回两个节点或 token 之间的最后一个 token。
        • getLastTokensBetween(nodeOrToken1, nodeOrToken2, countOptions) - 返回两个节点或 token 之间的最后一个 count 数量的 token。
        • getTokens(node) - 返回给定节点的所有 token。
        • getTokensBetween(nodeOrToken1, nodeOrToken2) - 返回两个节点间 token。
        • getTokenByRangeStart(index, rangeOptions) - 返回源中范围从给定的索引开始的token。(疑问:源?)
          • rangeOptions 是个对象,包含一个属性: includeComments
            • includeComments 是个布尔值,标记是否把注释 token 包含进返回结果中。
        • getNodeByRangeIndex(index) - 返回 AST 中包含给定的源的索引的最深节点。
        • getLocFromIndex(index) - 返回一个包含 line 和 column 属性的对象,对应给定的源的索引的位置。line 从 1 开始,column 从 0 开始。
        • getIndexFromLoc(loc) - 返回一个源码中的给定的位置的索引,loc 是个对象,包含一个从 1 开始的 line 键和一个从 0 开始的 column 键。
        • commentsExistBetween(nodeOrToken1, nodeOrToken2) - 如果两个节点间存在注释,返回 true。
    • markVariableAsUsed(name) - 在当前作用域内用给定的名称标记一个变量。
      • 这将影响 no-unused-vars规则
      • 如果找到一个具有给定名称的变量并将其标记为已使用,则返回 true,否则返回 false。(疑问)
    • report(descriptor) - 报告问题的代码
      • descriptor 包含以下属性
        • message - 有问题的消息,message: "Unexpected identifier: {{ identifier }}"
        • node - (可选的) 与问题有关的 AST 节点。如果存在且没有指定 loc,那么该节点的开始位置被用来作为问题的位置。
        • loc - (可选的) 用来指定问题位置的一个对象。如果同时指定的了 loc 和 node,那么位置将从loc获取而非node。
          • start - 开始位置
            • line - 问题发生的行号,从 1 开始。
            • column - 问题发生的列号,从 0 开始。
          • end - 结束位置(属性同上)
        • data - (可选的) message的占位符对象。
          • 占位符:即模板字符串中变量对应值
        • fix - (可选的) 一个用来解决问题的修复函数

开发插件

  • 创建一个插件最简单的方式是使用 Yeoman generator:https://www.npmjs.com/package/generator-eslint
    • 安装 Yeoman 和 generator-eslintnpm i -g yo npm i -g generator-eslint
    • 创建新的 ESLint 插件yo eslint:plugin
    • 创建新的 ESLint 规则,请确保您位于 ESLint 存储库克隆或 ESLint 插件的顶级目录中yo eslint:rule
  • 每个插件是一个命名格式为 eslint-plugin- 的 npm 模块
  • 在 ESLint 中,插件可以暴露额外的规则以供使用。为此,插件必须输出一个 rules对象,包含规则 ID 和对应规则的一个键值对。
module.exports = {
    rules: {
        "dollar-sign": {
            create: function (context) {
                // rule implementation ...
            }
        }
    }
};
  • 要在 ESLint 中使用插件中的规则,你可以使用不带前缀的插件名,后跟一个 /,然后是规则名。插件eslint-plugin-myplugin,使用插件规则"rules": {"myplugin/dollar-sign": "error"}
  • 插件可以暴露额外的环境以在 ESLint 中使用。为此,插件必须输出一个 environments 对象
    • 注释:jquery:true 允许使用全局变量 $
    • environments 对象的 key 是不同环境提供的名字,值是不同环境的设置
module.exports = {
    environments: {
        jquery: {
            globals: {
                $: false
            }
        }
    }
};
  • 在 ESLint 中使用这个环境,你可以使用不带前缀的插件名,后跟一个 /,然后是环境名。myplugin/jquery
  • 插件环境定义以下对象
    • globals - 同配置文件中的 globals 一样。key 是全局变量的名字,值为 true允许全局变量被覆盖,false 不允许覆盖。
    • parserOptions - 同配置文件中的 parserOptions 一样(注释:parserOptions 解析器配置项)
      • 疑问:解析器的参数?
  • 可以创建插件告诉 ESLint 如何处理 JavaScript 之外的文件。为了创建一个处理器,从你的模块中输出的对象必须符合以下接口
module.exports = {
    processors: {

        // assign to the file extension you want (.js, .jsx, .html, etc.) 文件扩展名
        ".ext": {
            // takes text of the file and filename 文件的文本和文件名
            preprocess: function(text, filename) {
                // here, you can strip out any non-JS content 去掉非js的内容
                // and split into multiple strings to lint 

                return [string];  // return an array of strings to lint 返回字符串数组使用规则校验,例如:.html 文件通过该方法提取 js 部分,然后使用 js 规则校验
            },

            // takes a Message[][] and filename 接收 Message[][] 和文件名
            // Message 包括
            // {
            //    line: number,
            //    column: number,
            //    endLine?: number,
            //    endColumn?: number,
            //    message?: string // 错误消息(postprocess返回时)
            // }
            postprocess: function(messages, filename) {
                // `messages` argument contains two-dimensional array of Message objects
                // where each top-level array item contains array of lint messages related
                // to the text that was returned in array from preprocess() method // messages 每个顶级数组项都包含与 preprocess()方法在数组中返回的文本相关的lint消息数组

                // you need to return a one-dimensional array of the messages you want to keep // 返回要保留的消息的一维数组
                return messages[0];
            },

            supportsAutofix: true // (optional, defaults to false) // 支持自动修复
        }
    }
};
  • ESLint 不会执行自动修复,即使在命令行上启用了 --fix 标志。要允许 ESLint 在使用处理器时自动修复代码,你应该采取以下额外步骤:
    • 更新 postprocess 方法,以额外转换报告问题的 fix 属性。所有可自动修复的问题都有一个 fix 属性,它是一个对象
      • range 属性在代码中包含两个索引,表示将被替换的连续文本段的开始和结束位置。
      • text 属性引用将替换给定范围的文本。
    • 向处理器添加 supportsAutofix: true 属性
    • 注释:postprocess 方法返回的对象添加以下属性使 eslint 具有修复功能
{
    range: [number, number],
    text: string
}
  • 可以在一个插件中在 configs 键下指定打包的配置
    • 不可能为给定的插件指定默认配置,当用户想要使用一个插件时,必须在配置文件中指定
    • 注释:配置就是 extends
// eslint-plugin-myPlugin

module.exports = {
    configs: {
        myConfig: {
            plugins: ["myPlugin"],
            env: ["browser"],
            rules: {
                semi: "error",
                "myPlugin/my-rule": "error",
                "eslint-plugin-myPlugin/another-rule": "error"
            }
        },
        myOtherConfig: {
            plugins: ["myPlugin"],
            env: ["node"],
            rules: {
                "myPlugin/my-rule": "off",
                "eslint-plugin-myPlugin/another-rule": "off"
                "eslint-plugin-myPlugin/yet-another-rule": "error"
            }
        }
    }
};
  • 如果上面的示例插件名为 eslint-plugin-myPlugin,那么 myConfig 和 myOtherConfig 配置可以分别从 "plugin:myPlugin/myConfig" 和 "plugin:myPlugin/myOtherConfig" 扩展出来
{
    "extends": ["plugin:myPlugin/myConfig"]
}

备注

  • eslint 执行过程
    • 遍历依据源码生成的 AST ,将每一个 node 传入 nodeQueue 队列中,每个会被传入两次;
    • 遍历所有将被应用的规则,为规则中所有的选择器添加监听事件;(注释:同一个选择器执行多个规则)
    • 遍历第一步获取到的 nodeQueue,触发其中包含的事件,将问题 push 到 lintingProblems 中(注释:匹配所有选择器,执行对应的规则列表)
    • 返回 lintingProblems