5000字带你深入理解ESLint

1,912 阅读15分钟

这篇文章详细介绍了 ESLint 相关的一些知识,主要分成三大部分:

  • ESLint 基本介绍与使用
  • ESLint 运行原理与 AST
  • 如何编写 ESLint 插件

什么是 ESLint

ESLint 是一个开源的 JavaScript 代码检查工具,由 Nicholas C. Zakas 于2013年6月创建。代码检查是一种静态的分析,常用于寻找有问题的模式或者代码,并且不依赖于具体的编码风格。对大多数编程语言来说都会有代码检查,一般来说编译程序会内置检查工具。

JavaScript 是一个动态的弱类型语言,在开发中比较容易出错。因为没有编译程序,为了寻找 JavaScript 代码错误通常需要在执行过程中不断调试。ESLint 可以让程序员在编码的过程中发现问题而不是在执行的过程中。

ESLint 的初衷是为了让程序员可以创建自己的检测规则。ESLint 的所有规则都被设计成可插拔的。ESLint 的默认规则与其他的插件并没有什么区别,规则本身和测试可以依赖于同样的模式。为了便于人们使用,ESLint 内置了一些规则,当然,你可以在使用过程中自定义规则。

安装与使用

你可以使用 npm 或者 yarn 安装 ESLint,本文会使用 yarn。首先创建一个目录eslint-start,初始化package.json文件,然后安装 eslint

yarn init

yarn add eslint --dev

安装完成之后需要设置一个配置文件,可以通过命令行工具直接生成:

yarn create @eslint/config

在这个过程中,ESLint 会让你选择一些选项:

✔ How would you like to use ESLint? · problems
✔ What type of modules does your project use? · esm
✔ Which framework does your project use? · none
✔ Does your project use TypeScript? · No / Yes
✔ Where does your code run? · browser
✔ What format do you want your config file to be in? · JSON

之后会得到一个.eslintrc.json文件,内容如下:

{
  // 指定环境,比如是浏览器还是 Node,会提供一些预定义的全局变量
  "env": {
    "browser": true,
    "es2021": true
  },
  "extends": "eslint:recommended", // 要扩展的配置文件
  "parserOptions": {
    "ecmaVersion": "latest", // 指定你要使用的 ECMAScript 语法版本,"latest" 表示始终启用最新的 ECMAScript 版本
    "sourceType": "module" // "script" (默认值) 或 "module"(如果你的代码是 ECMAScript 模块)
  },
  // 配置规则
  "rules": {}
}

JSON 和 YAML 配置文件是支持注释的,ESLint 会 ignore 配置文件中的注释

现在你可以在任何文件或目录上运行 ESLint。

例子

下面来看一个简单的例子,首先添加一条规则到.eslintrc.json中的rules部分:"prefer-const": "error",这条规则要求声明后没有被重新赋值的变量必须使用const,否则会报错。

ESLint 官方提供的所有规则都可以在这个页面找到:eslint.org/docs/rules/

{
  // ...
  "rules": {
    "prefer-const": "error"
  }
}

之后在项目根目录创建一个index.js文件,并把以下内容写入到文件中:

let myName = 'dapangmao';
console.log(myName);

接下来就可以运行 ESLint 了:

yarn run eslint index.js

命令执行完之后,会在控制台看到以下错误:

也可以通过在上面命令的基础上添加--fix,这样 ESLint 会尝试去修复错误,对于上图中的错误,ESLint 会自动把let替换为const,感兴趣的读者可以自行尝试。

rules

ESLint 中有两个重要的部分:rules 和 plugins。

在上一个例子中,我们使用了键值对的形式来添加一个规则,键是规则的名称,值是错误级别,这一类的规则是没有属性的,只需要开启或者关闭。

Rule 的错误级别可以是以下值之一:

  • off或者0:关闭规则
  • warn或者1:开启规则,使用警告级别的错误
  • error或者2:开启规则,使用错误级别的错误
{
  "no-debugger": "error",
  "no-delete-var": "warn",
  "no-dupe-args": "off"
}

除了键值对形式的规则外,还有一部分规则除了需要开启或关闭,还需要配置属性。

"rules": {
  "quotes": ["error", "single"], // 如果不是单引号,则报错
  "one-var": [
    "error",
    {
      "var": "always", // 每个函数作用域中,只允许 1 个 var 声明
      "let": "never", // 每个块作用域中,允许多个 let 声明
      "const": "never" // 每个块作用域中,允许多个 const 声明
    }
  ]
}

plugins

尽管 ESLint 附带了一些很好的规则,但通常它们不足以满足项目的所有需求,特别是如果使用 React、Vue、Angular 等库和框架进行构建时。ESLint 插件允许我们根据项目的需要添加自定义规则。插件作为 npm 模块发布,命名格式为eslint-plugin-<plugin-name>

要使用插件,首先需要通过 npm 安装插件,然后把插件添加到eslintrc配置文件中的plugins中。例如,你想使用一个名为eslint-plugin-my-awesome-plugin的插件,你可以像这样把它添加到你的配置文件中:

{
  "plugins": ["my-awesome-plugin"] // "eslint-plugin" 前缀可以省略
}

需要注意的是,添加了这个插件不意味着这个插件的所有规则都会被自动启用,仍然需要单独应用要与该插件一起使用的每个规则,在配置文件中的 rules 对象上配置。

{
  "rules": {
    "eqeqeq": "off",
    "curly": "error",
  }
}

但是如果每一个规则都需要配置一遍,对开发者来说很不友好,所以 ESLint 提供了另一种方式:可共享的配置。

可共享的配置

ESLint 允许我们通过将配置发布到 npm 来共享配置。与插件的名字类似,可共享的配置以eslint-config-<config-name>的格式发布。

要使用可共享配置,首先也要从 npm 安装,然后可以通过extends部分来扩展项目的 ESLint 配置。

{
  "extends": "standard" // 与插件类似,"eslint-config" 前缀可以省略
}

我们可以通过将多个配置添加到数组中来扩展它们,如果配置修改相同的规则,则前面的配置的规则将被后面的配置覆盖,因此在这些情况下顺序很重要。

需要注意的是,可共享配置不仅仅用于共享规则集,它们可以是具有自己的插件、格式化程序等的完整配置,甚至还可以从其他配置扩展。

eslint-config-standard为例,当我们使用"extends": "standard"时,实际上是使用了这个配置文件

带有配置的插件

除了使用eslint-config-<config-name>来发布可共享配置之外,插件本身也可以附带不同的可共享的配置集,我们可以根据项目需要来选择使用哪一个。如果你以前有配置过 ESLint,很可能见过这样的写法:

{
  "extends": {
    "plugin:prettier/recommended"
  }
}

我们可以通过plugin:前缀使用插件附带的这些配置。例如,我们正在使用一个名为eslint-plugin-my-awesome-plugin的插件,它带有一个名为recommended的配置。然后,我们可以将plugin:my-awesome-plugin/recommended添加到配置中的extends部分,来从该可共享配置扩展。

我们甚至不需要在eslintrc配置文件中把prettier添加到plugins中,因为recommended配置中已经包含了。我们以eslint-plugin-prettier为例,如果查看代码,就会发现这个插件导出了一个recommended配置,内容如下:

module.exports = {
  configs: {
    recommended: {
      extends: ['prettier'],
      plugins: ['prettier'],
      rules: {
        'prettier/prettier': 'error',
        'arrow-body-style': 'off',
        'prefer-arrow-callback': 'off',
      },
    },
  },
};

通过"extends": "eslint:recommended",所有在 rules 页面打钩✔️的 rules 都会被开启。

ESLint 工作原理

了解了 ESLint 基本的使用之后,我们再来了解一下 ESLint 的工作原理,也为接下来的编写 ESLint 插件部分做准备。

在 ESLint 中,默认使用 Espree 来解析 JavaScript,将代码转换成 AST(抽象语法树),然后去拦截检测是否符合我们规定的书写方式,最后让其展示报错、警告或正常通过。

ESLint 的核心就是一系列 rules,而 rules 的核心就是利用 AST 来做校验。在 ESLint 中,一切都是可插拔的,每条规则相互独立。

架构

这张图是 ESLint 官网给出的一个架构图。

  • bin/eslint.js - 这个是命令行应用程序实际上执行的文件,它仅仅是个封装,用来启动 ESLint,并向cli传递命令行参数。
  • lib/api.js - 这个是require("eslint")的入口,导出了一个包含LinterESLintRuleTesterSourceCode的对象。
  • lib/cli.js - 这个是 ESLint CLI 的核心。它接受一个参数数组,然后使用eslint执行命令。通过保持这个文件作为一个单独的应用程序,它允许其他人在另外的 Node.js 程序中有效的调用 ESLint,就好像是在命令行上操作的一样。它最重要的函数是cli.execute()。它也扮演着读取文件、遍历目录,输入和输出的角色。
  • lib/cli-engine/:这个模块是 CLIEngine 类,它查找源代码文件和配置文件,然后使用 Linter 类进行代码验证。这里面包括了配置文件、解析器、插件和格式化程序的加载逻辑。
  • lib/linter/ - 这个模块是基于配置选项进行代码验证的核心 Linter 类。这个文件不与控制台交互,没有 I/O。对于其他需要验证 JavaScript 文本的 Node.js 程序,他们将能够直接使用此接口。
  • lib/rule-tester/ - 这个模块是 RuleTester 类,它是 Mocha 的包装器,因此可以对规则进行单元测试。这个类让我们可以为每个已实现的规则编写格式一致的测试,并确信每个规则都有效。 RuleTester 接口以 Mocha 为模型,与 Mocha 的全局测试方法一起使用。 RuleTester 也可以修改为与其他测试框架一起使用。
  • lib/source-code/ - 这个模块是 SourceCode 类,用于表示解析后的源代码。它接收源代码和代表代码的 AST 节点。
  • lib/rules - 包含了内置规则

AST

如果想要深入了解 ESLint 的工作原理,那么 AST 毫无疑问是极其重要的一部分。AST 是源代码语法结构的一种抽象表示,它以树状的形式表现编程语言的语法结构,树上的每个节点都表示源代码的一种结构。

AST如何生成

JavaScript 执行的第一步是读取文件中的字符流,然后通过词法分析生成 token,之后再通过语法分析( Parser )生成 AST,最后生成机器码执行。

整个解析过程主要分为以下两个步骤:

  • 分词:将整个代码字符串分割成最小语法单元数组
  • 语法分析:在分词基础上建立分析语法单元之间的关系

JS Parser 是 JavaScript 语法解析器,它可以将 JavaScript 源码转成 AST,常见的 Parser 有 EsprimaAcorn

词法分析

词法分析,也称之为扫描(scanner),简单来说就是调用 next() 方法,一个一个字母的来读取字符,然后与定义好的 JavaScript 关键字符做比较,生成对应的 Token。Token 是一个不可分割的最小单元:

例如var这三个字符,它只能作为一个整体,语义上不能再被分解,因此它是一个 Token。

词法分析器里,每个关键字是一个 Token ,每个标识符是一个 Token,每个操作符是一个 Token,每个标点符号也都是一个 Token。除此之外,还会过滤掉源程序中的注释和空白字符(换行符、空格、制表符等。

最终,整个代码将被分割进一个 tokens 列表(或者说一维数组)。

语法分析

语法分析会将词法分析出来的 Token 转化成有语法含义的抽象语法树结构。同时,验证语法,语法如果有错的话,抛出语法错误。

AST Explorer 是一个工具网站,它能查看代码被解析成 AST 的样子。

编写 ESLint 插件

介绍了原理之后,接下来就是我们的实战部分了。有些时候,已有的 Lint 规则并不能满足项目需求,我们可以根据需求创建自己的规则。

接下来我们以一个简单的需求为例,开发一个属于我们自己的 ESLint 插件。

需求:使用const声明基本类型的变量时,变量名不能出现小写字母。

初始化项目

想要创建一个 ESLint rule,首先需要创建一个 ESLint 插件。我们在 plugins 部分有提到过,插件是一个以eslint-plugin开头的 npm 模块,这是 ESLint 官方规定的。

首先初始化package.json,内容如下:

{
  "name": "eslint-plugin-awesome-rules",
  "version": "1.0.0",
  "main": "index.js",
  "author": "dapangmao",
  "license": "MIT"
}

创建规则

之后在根目录创建一个index.js文件,用来存放 rule 的具体逻辑。

const hasLowerCase = (str) => /[a-z]/.test(str);
module.exports = {
  rules: {
    'constant-capitalization': {
      meta: {
        type: 'suggestion',
        docs: {
          description: 'disallow lowercase alphabets in constants declaration',
        },
      },
      create: function (context) {
        return {
          VariableDeclarator: function (node) {
            if (
              node.parent.kind === 'const' &&
              hasLowerCase(node.id.name) &&
              node.init.type === 'Literal'
            ) {
              context.report(
                node,
                'Please use capitalized casing for constants'
              );
            }
          },
        };
      },
    },
  },
};

其实到这里一个基本的插件我们就创建完成了,只包含两个文件:package.jsonindex.js。这个插件只提供了一个规则:constant-capitalization。下面来详细介绍一下 rule 部分的具体内容。

插件中的每个规则都必须包含两条属性:metacreate

  • meta:元数据,包含了规则的通用信息,比如规则的类型,以及一些用来用来描述规则的信息。
  • create:一个函数,它将逐个节点访问整个代码的语法树,并让我们对节点进行操作。参数context包含与规则上下文相关的信息,这个函数返回一个对象,对象的属性是 AST 中的选择器,ESLint 会收集这些选择器,在 AST 遍历过程中会执行所有监听该选择器的回调。

回到我们的规则本身,为了找到符合条件的节点,我们需要观察代码解析成 AST 的结果,下面的截图是在 AST Explorer 中输入const foo = '123';得到的 AST:

通过观察 AST 可以发现通过node.parent.kind === 'const' && hasLowerCase(node.id.name) && node.init.type === 'Literal'就可以过滤出符合条件的节点。对于符合条件的节点,调用context.report来发布警告或错误(取决于你所使用的配置)。该方法只接收一个参数,是个对象。

测试

了解了规则的实现后,让我们通过一个实际的例子,来测试一下我们编写的规则。

首先在当前插件的根目录执行yarn link,你会看到下面类似的输出。

测试项目我们可以继续使用在安装与使用章节创建的项目,首先运行yarn link "eslint-plugin-awesome-rules",把这个模块链接到我们编写的的本地插件。之后运行yarn add eslint-plugin-awesome-rules@link:1.0.0把插件添加到package.json中。

// 在 eslint-plugin-awesome-rules 根目录中
eslint-plugin-awesome-rules % yarn link

// 在测试项目中
eslint-start % yarn link "eslint-plugin-awesome-rules"
eslint-start % yarn add eslint-plugin-awesome-rules@link:1.0.0

我们在前面的 plugins 部分提到过,安装好插件之后,还需要在 ESLint 的配置文件中进行配置。配置好的.eslintrc.json文件长这样:

{
  "env": {
    "browser": true,
    "es2021": true
  },
  "plugins": ["awesome-rules"],
  "parserOptions": {
    "ecmaVersion": "latest",
    "sourceType": "module"
  },
  "rules": {
    "awesome-rules/constant-capitalization": "error"
  }
}

之后我们把index.js文件中的内容改成下面的内容:

const myName = 'dapangmao';

运行命令:yarn run eslint index.js,就会在控制台看到我们期望的输出:

至此,一个最简单的 ESLint 插件就创建完成了。

使用 Yeoman generator

上面我们通过手动创建项目来编写了一个插件,是为了让示例尽量精简,只专注在规则本身。但是如果我们想把编写的插件发布到 npm,更推荐大家使用 Yeoman generator

Yeoman generator 是 ESLint 官方为我们开发 eslint 插件提供的脚手架,用于生成包含指定框架结构的工程化目录结构。

首先全局安装yogenerator-eslint

npm install -g yo generator-eslint

创建项目目录,使用命令行初始化项目:

mkdir eslint-plugin-awesome-rules-yo
cd eslint-plugin-awesome-rules-yo

yo eslint:plugin	# 生成项目骨架

命令行会要求你输入一些插件相关的信息,之后会生成一些必要的文件。

如果要创建一个自定义规则,还需要键入下面这个命令,来添加一些创建 rule 相关的文件。

yo eslint:rule

最终的文件结构长这样:

lib/rules/constant-capitalization.js文件的内容长这样(删掉了不必要的注释):

/**
 * @fileoverview disallow lowercase alphabets in constants declaration
 * @author dapangmao
 */
"use strict";

/**
 * @type {import('eslint').Rule.RuleModule}
 */
module.exports = {
  meta: {
    type: null, // `problem`, `suggestion`, or `layout`
    docs: {
      description: "disallow lowercase alphabets in constants declaration",
      category: "Fill me in",
      recommended: false,
      url: null, // URL to the documentation page for this rule
    },
    fixable: null, // Or `code` or `whitespace`
    schema: [], // Add a schema if the rule has options
  },

  create(context) {
    // variables should be defined here
    return {
      // visitor functions for different types of nodes
    };
  },
};

可以发现,和我们手动创建插件的文件内容很像,这个文件就是我们编写 rule 逻辑代码的地方。

使用yo eslint:rule创建规则时,在docstests/lib文件夹中各有一个和 rule 同名的文件,这是我们写规则文档和测试的地方,如果我们要发布到 npm,文档和完整的测试还是很有必要的。

总结

本篇文章到这里就结束了,我们一步步由浅入深,介绍了 ESLint 的基本使用、工作原理、AST以及如何编写一个插件等,希望这篇文章给你带来了一些收货,让你对 ESLint 有了一个更深入的了解!