【实战】自定义 ESLint Plugin

2022-08-01 19:50:23 浏览数 (1)

背景

之前做过一个小分享——【优化】记一次通过工具减少 Git 冲突[1]。主要讲的是通过利用 git hooks 在代码提交之前给相关的代码排序,从而减少合代码时候的冲突。

上次同事提醒说,这个 Eslint 就可以做到。我回去查了一下,还真可以,详情见 sort-keys[2]。假如使用了这条规则,就是要求对象写法要遵循一定的顺序。比如开启这个规则的话,默认情况下下面的代码就会报错:

代码语言:javascript复制
let obj = {a: 1, c: 3, b: 2};

应该为:

代码语言:javascript复制
let obj = {a: 1, b: 2, c: 3};

但是其实我们的诉求中,还有一种场景,那就是对象数组。比如下面这个场景,我需要根据 label 去决定对象在数组中顺序(注意:我们这个场景下数组的顺序对业务是没有影响的),Eslint 这个规则就无能为力了。

代码语言:javascript复制
const FlowList = [
  { value: '5', label: 'a' },
  { value: '2', label: 'C' },
  { value: '1', label: 'B' }
];

另外,我们知道 ESLint 规则可以针对某个文件夹或者某个文件生效,那能不能只针对于某个代码块呢?

那我们如何通过 Eslint 暴露给我们的能力去实现这些点呢?

ESLint 是什么?

官方如下:

ESLint 是在 ECMAScript/JavaScript 代码中识别和报告模式匹配的工具,它的目标是保证代码的一致性和避免错误

ESLint 具有以下特点:

  • 使用 Espree[3] 解析 JavaScript。
  • 使用 AST 去分析代码中的模式。
  • 完全插件化的。每一个规则都是一个插件,提供了足够的可拓展能力,让我们更好的定义使用规则。

ESlint 我们离不开 AST(抽象语法树),我们可以通过 astexplorer[4] 直观看到 Espree 处理后生成的 AST 的结构。比如 var a = 1; 如下所示:

image-20210812220540504

竟然我们知道它的结构,我们就可以直接去检测它合不合法了。

我们来讲一个重要的概念——AST Selectors :它是一个字符串,可用于匹配抽象语法树(AST)中的节点。这对于在代码中描述特定的语法模式非常有用。AST 选择器的语法与 CSS 选择器的语法类似。如果你以前使用过 CSS 选择器,那么 AST 选择器的语法应该很容易理解。这个在我们后面自定义规则的时候非常重要。它的语法可以看官方文档[5]

ESlint 的原理

在开始书写我们的规则,我们看看 ESlint 具体的实现是怎么做的(这里只说明单条的 Rule 是怎么书写的,整体的 ESlint 作用流程这里不展开)。就以之前提到的 sort-keys[6] 为例。

每个规则都会有三个重要的文件:

  • lib/rules 目录中的是源文件,具体的校验逻辑可以在这里写。
  • tests/lib/rules 目录中是测试文件,写具体的测试用例。
  • docs/rules 文档目录。

lib/rules/sort-keys.js 中我们可以找到上面规则相应的源码。规则的源文件导出具有以下属性的对象。类似如下:

代码语言:javascript复制
module.exports = {
  // 包含规则的元数据
  meta: {
    // 规则类型
    type: "suggestion",
    // 文档
    docs: {
      description: "require object keys to be sorted",
      category: "Stylistic Issues",
      recommended: false,
      url: "https://eslint.org/docs/rules/sort-keys",
    },
    schema: [
      // 可以传的一些参数
      {
        enum: ["asc", "desc"],
      }
    ],
    // 提示信息
    messages: {
      sortKeys:
        "Expected object keys to be in {{natural}}{{insensitive}}{{order}}ending order. '{{thisName}}' should be before '{{prevName}}'.",
    },
  },
  create(context) {
    return {};
  },
};

  • meta :代表了这条规则的元数据,如其类别,文档,可接收的参数的 schema 等等,官方文档[7]对其有详细的描述,这里不做赘述。
  • create: meta 表达了我们想做什么,那么 create 则用表达了这条 rule 具体会怎么分析代码。

create 返回的是一个对象,其中 key 就是上面提到的 AST Selector,在 AST Selector 中,我们可以获取对应选中的内容,随后我们可以针对选中的内容作一定的判断,看是否满足我们的规则,如果不满足,可用 context.report()抛出问题,ESLint 会利用我们的配置对抛出的内容做不同的展示。

AST Selector的末尾添加 :exit 将导致在遍历过程中退出匹配节点时调用侦听器,而不是在输入匹配节点时。

自定义 ESlint 插件

基于 `Yeoman generator`[8] (一个快速帮你搭建工程的脚手架工具),可以快速创建 ESLint plugin 项目。

代码语言:javascript复制
npm i -g yo
npm i -g generator-eslint
// 创建一个plugin
yo eslint:plugin
// 创建一个规则
yo eslint:rule

我创建的目录结构如下:

代码语言:javascript复制
├── README.md
├── docs # 文档
│   └── rules
│       ├── array-sort-object.md
│       └── sort.md
├── lib # 源代码,规则文件
│   ├── index.js
│   └── rules
│       ├── array-sort-object.js
│       └── sort.js
├── package.json
├── tests # 单元测试文件
│   └── lib
│       └── rules
│           ├── array-sort-object.js
│           └── sort.js
└── yarn.lock

如何做到只检测部分代码?

我们知道 ESlint 的检测可以指定到文件维度,但是我们希望只针对部分的代码进行检测。要不然像对象数组顺序,假如都开了检测,将会有很多报错或者警告。

方法是有的,我们发现,ESlint 是可以通过 getCommentsInside 方法获取到某个 AST Selector 中的注释,返回给定节点内所有注释标记的数组。比如以下:

代码语言:javascript复制
const FlowList = [
  // eslint sortBy:'label'
  { value: '5', label: 'a' },
  { value: '2', label: 'C' },
  { value: '1', label: 'B' }
];
代码语言:javascript复制
create: function (context) {
    // 获取到顺序的配置,默认是升序
    const order = context.options[0] || "asc";
    // variables should be defined here
  return {
    ArrayExpression: (node) => {
      console.log('getCommentsBefore:', context.getCommentsInside(node))
    }
  }

打印出来的结果如下,我们就可以利用这个信息进行处理。只有评论命中某个规则的时候,才去处理这段代码

image-20210812231108912

实现对象数组排序

整体的实现代码如下,实现上并不难。整体思路:

是先获取到要比较的字段(比如上面例子中的 label)。

代码语言:javascript复制
// 获取到 comment
const comment = context.getCommentsInside(node);
if (!comment) return;
// 获取到排序的字段
const field = comment[0] && comment[0].value && comment[0].value.split("'")[1];
if (!field) return;

拿到数组中每一项目标字段对应的值([ 'a', 'C', 'B' ])。

代码语言:javascript复制
// 取每一项排序对象中值
let fieldValueArr = node.elements.map(item => {
  const target = (item.properties.find((prop) => {
    return prop.key.name === field
  }) || { value: '' });
  return target.value && target.value.value;
});

再对该数组进行前后顺序的检测,假如不符合我们就报错。

代码语言:javascript复制
// 默认按照升序排序
for (let i = 1; i < fieldValueArr.length; i  ) {
  let reportError = false;
  if (order === 'asc' && String(fieldValueArr[i]).localeCompare(String(fieldValueArr[i - 1])) < 0) {
    reportError = true;
  } else if (order === 'desc' && String(fieldValueArr[i]).localeCompare(String(fieldValueArr[i - 1])) > 0) {
    reportError = true;
  }
  // 判断是否是降序
  if (reportError) {
    context.report({
      node,
      message: `数组排序不正确。请根据 ${field} 字段排序`,
    });
    break;
  }
}

总结

Eslint 对于一个团队的代码规范是非常重要的,Eslint 自身带有很多有用的规则,本文介绍了 ESlint 的基础原理以及如何自定义 Eslint 插件来解决对象数组排序的问题,除此之外,我们可能还有其他的场景可以进行尝试,欢迎大家参与讨论~

参考

  • ESLint 工作原理探讨[9]
  • 自定义 ESLint 规则,让代码持续美丽

参考资料

[1]【优化】记一次通过工具减少 Git 冲突: https://juejin.cn/post/6895534290411454477

[2]sort-keys: https://eslint.org/docs/rules/sort-keys#rule-details

[3]Espree: https://github.com/eslint/espree

[4]astexplorer: https://astexplorer.net/

[5]官方文档: https://eslint.org/docs/developer-guide/selectors

[6]sort-keys: https://eslint.org/docs/rules/sort-keys#rule-details

[7]官方文档: https://link.juejin.cn?target=http://link.zhihu.com/?target=https://eslint.org/docs/developer-guide/working-with-rules#rule-basics

[8]Yeoman generator: https://yeoman.io/authoring/

[9]ESLint 工作原理探讨: https://juejin.cn/post/6844903749886935053#heading-5

0 人点赞