如何编写一个自定义的 eslint 规则?

准备

正文

开始编写 eslint 插件

官方推荐使用 yeoman generator-eslint 脚手架快速生成 eslint plugin模版,不过已经3年没有更新了,感兴趣可以看看这个脚手架的源码。

编写规则

编写一个规则,其实就是在 eslint 提供的 AST 中,找到我们需要检查的 AST 节点,对其进行判断,如何符合条件,就抛出相关提示。

遍历 AST 节点

一个最简单的 rule 对象,只需要声明一个 create 属性即可。参考以下代码:

module.exports = {
    create: function(context) {
        // declare the state of the rule
        return {
            ReturnStatement: function(node) {
                // at a ReturnStatement node while going down
            },
            // at a function expression node while going up:
            "FunctionExpression:exit": checkLASTSegment,
            "ArrowFunctionExpression:exit": checkLASTSegment,
            onCodePathStart: function (codePath, node) {
                // at the start of analyzing a code path
            },
            onCodePathEnd: function(codePath, node) {
                // at the end of analyzing a code path
            }
        };
    }
};

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

上述代码中,create 方法返回的对象,就是 AST 节点 “遍历器” 的集合。

我们可以直接声明 estree 中支持的 AST types。下面这段代码的作用,就是遍历所有的 return 语句,并执行定义的回调函数。

{
    ReturnStatement: function(node) {
        // at a ReturnStatement node while going down
    }
}
1
2
3
4
5

同时,eslint 所依赖的 espree,也支持使用类似 css 选择器的 selector 语法,对 AST types 进行组装。

下面的代码会遍历 new 语句中的字面量参数。

{
    "NewExpression > Literal": function(node) {
        // at a Literal node in a NewExpression
    }
}
1
2
3
4
5

如果对特定代码片段的 AST 结构非常陌生,可以考虑编写一段 demo 代码,使用 espree 进行解析,可以根据得到的 AST 来编写“遍历器”。

context 相关 api 可以参考官方文档的说明。

抛出错误提示

context 对象提供了一个 report 方法,用于抛出警告和错误提示。

最基本的用法如下:

context.report({
    node: node,
    message: "Unexpected identifier"
});
1
2
3
4

report 方法也支持变量插值:

context.report({
    node: node,
    message: "Unexpected identifier: {{ identifier }}",
    data: {
        identifier: node.name
    }
});
1
2
3
4
5
6
7
Example

让我们来实战一下,现在我们需要限制用户调用 new Date() 方法。

  1. 如果你不知道一个 new Date() AST 节点是什么样子的,可以试试编写demo代码,得到一个初始化的 AST 结构

    // demo code
    const code = `
    const a = new Date();
    const b = new Date('adwad');
    `;
    
    const espree = require('espree');
    const fs = require('fs');
    const ast = espree.parse(code, {
      ecmaVersion: 6
    });
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    AST
      {
      "type": "Program",
      "start": 0,
      "end": 52,
      "body": [
        {
          "type": "VariableDeclaration",
          "start": 1,
          "end": 22,
          "declarations": [
            {
              "type": "VariableDeclarator",
              "start": 7,
              "end": 21,
              "id": {
                "type": "Identifier",
                "start": 7,
                "end": 8,
                "name": "a"
              },
              "init": {
                "type": "NewExpression",
                "start": 11,
                "end": 21,
                "callee": {
                  "type": "Identifier",
                  "start": 15,
                  "end": 19,
                  "name": "Date"
                },
                "arguments": []
              }
            }
          ],
          "kind": "const"
        },
        {
          "type": "VariableDeclaration",
          "start": 23,
          "end": 51,
          "declarations": [
            {
              "type": "VariableDeclarator",
              "start": 29,
              "end": 50,
              "id": {
                "type": "Identifier",
                "start": 29,
                "end": 30,
                "name": "b"
              },
              "init": {
                "type": "NewExpression",
                "start": 33,
                "end": 50,
                "callee": {
                  "type": "Identifier",
                  "start": 37,
                  "end": 41,
                  "name": "Date"
                },
                "arguments": [
                  {
                    "type": "Literal",
                    "start": 42,
                    "end": 49,
                    "value": "adwad",
                    "raw": "'adwad'"
                  }
                ]
              }
            }
          ],
          "kind": "const"
        }
      ],
      "sourceType": "script"
    }
      

    可以发现,new Date() 方法的调用,在 AST 中的 type 为 NewExpression。这里的 init其实就是 eslint 中的 node 节点。

  2. 现在我们需要判断,一个 AST 节点,是否是 new Date()

    {
       NewExpression(node) {
         const { callee } = node;
         if (callee.name === 'Date') {
           // your code here
         }
       }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
  3. 找到正确的代码语句后,我们需要抛出提示

    {
      NewExpression(node) {
        const { callee } = node;
        if (callee.name === 'Date') {
          const message = 'Do not using `new Date()` at all!';
          context.report(node, message);
        }
      }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
  4. 现在你会发现,一个自定义的规则就大功告成了

    module.exports = {
      rules: {
        'no-date-string-construct': {
          create: function(context) {
            return {
              NewExpression(node) {
                const { callee } = node;
                if (callee.name === 'Date') {
                  const message = 'Do not using `new Date()` at all!';
                  context.report(node, message);
                }
              }
            };
          }
        }
      }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17

使用规则

plugins + rules

eslint 提供了接入 plugin 的方式,使用 plugins字段即可。需要注意的是,这里配置的 plugin 名称,不需要再声明 eslint-plugin 的前缀。

引入 plugin 后,就可以直接在 rules 中添加该插件中定义的各个规则。与 eslint 内置的规则不同,使用 plugin 的规则,需要声明命名空间,采用 'namespace/rule-name' 的形式。

// eslintrc.js
module.exports = {
  plugins: ['my-rules'],
  rules: {
    "my-rules/my-rule": 2
  }
}
1
2
3
4
5
6
7

如果你编写的 plugin 尚未发布 npm 包,可以使用以下方式把一个开发中的包加入到当前工作目录下的 node_modules 中

yarn add -D file:path/to/eslint-plugin-my-rules
1
extends

一个插件可以提供预设好的多个规则,让使用者直接引入。

plugin内的配置如下:

// eslint-plugin-my-rules
module.exports = {
    configs: {
    recommended: {
      plugins: ['my-rules'],
      rules: {
        'my-rules/my-rule': 1
      }
    }
  }
}
1
2
3
4
5
6
7
8
9
10
11

使用者的配置如下:

// eslintrc.js
module.exports = {
  extends: ['plugin:my-rules/recommended']
}
1
2
3
4

参考内容