如何编写eslint规则

934 阅读6分钟

一、自定义eslint规则构建步骤

(1)在根目录下创建rules,并在该目录下创建my-rule.js

--myProject
  --src
 +--rules
    --my-rule.js

(2)编写my-rule.js的规则,当前规则是不允许给变量定义为“a”

"use strict";


module.exports = {
    meta: {
        type: "problem",
        fixable: "code",
        docs: {
        },
        schema: [], // no options
        messages: {
            error: "不能以'a'命名变量"
        }
    },
    create: function(context) {
        return {
            VariableDeclaration(node) {
                const { declarations } = node;
                declarations?.forEach((v) => {
                    const { id } = v;
                    if(id?.name === 'a') {
                        context.report({
                            node,
                            messageId: "error",
                            data: {},
                            fix: function(fixer) {
                                return fixer.replaceText(id, "b");
                            }
                        });
                    }
                })
            },
        };
    }
};

(3)修改package.json中的eslint执行脚本,添加rulesdir引入自己的规则

"scripts": {
  ...
  "lint": "eslint --rulesdir ./rules --ext .tsx,.ts,.js src --cache --fix",
}

(4)修改eslintrc.js配置文件

rules: {
  "my-rule": ["error"],
  ...
}

(5)执行命令npm run lint,获取展示结果

 6:1   error  不能以'a'命名变量                                                     my-rule

二、eslint规则是什么

在node_modules/.bin下面可以找到eslint文件,这里找到了eslint下的eslint.js文件

#!/bin/sh
basedir=$(dirname "$(echo "$0" | sed -e 's,\,/,g')")

case `uname` in
    *CYGWIN*) basedir=`cygpath -w "$basedir"`;;
esac

if [ -x "$basedir/node" ]; then
  "$basedir/node"  "$basedir/../eslint/bin/eslint.js" "$@"
  ret=$?
else 
  node  "$basedir/../eslint/bin/eslint.js" "$@"
  ret=$?
fi
exit $ret

跟着源码走,可以看到所有的规则都在./rules的目录下,该目录下包含了多个js文件,如quote.js等。这里的每一个js文件都是一个规则,结构就像我们在“一“中的my-rule.js一样。

const BuiltinRules = require("../rules");

--rules
  --quote.js
  --eqeqeq.js
  ...

三、rule的结构

下面是eslint官网的文档内容。

module.exports = {
    meta: {
        type: "suggestion",

        docs: {
            description: "disallow unnecessary semicolons",
            category: "Possible Errors",
            recommended: true,
            url: "https://eslint.org/docs/rules/no-extra-semi"
        },
        fixable: "code",
        schema: [] // no options
    },
    create: function(context) {
        return {
            // callback functions
        };
    }
};

meta(对象)包含规则的元数据:

  • type (string) 指示规则的类型,值为 "problem""suggestion" 或 "layout"

    • "problem" 指的是该规则识别的代码要么会导致错误,要么可能会导致令人困惑的行为。开发人员应该优先考虑解决这个问题。
    • "suggestion" 意味着规则确定了一些可以用更好的方法来完成的事情,但是如果代码没有更改,就不会发生错误。
    • "layout" 意味着规则主要关心空格、分号、逗号和括号,以及程序中决定代码外观而不是执行方式的所有部分。这些规则适用于AST中没有指定的代码部分。
  • docs (object) 对 ESLint 核心规则来说是必需的:

    • description (字符串) 提供规则的简短描述在规则首页展示
    • category (string) 指定规则在规则首页处于的分类
    • recommended (boolean) 配置文件中的 "extends": "eslint:recommended"属性是否启用该规则
    • url (string) 指定可以访问完整文档的 url。

    在自定义的规则或插件中,你可以省略 docs 或包含你需要的任何属性。

  • deprecated (boolean) 表明规则是已被弃用。如果规则尚未被弃用,你可以省略 deprecated 属性。

  • replacedBy (array) 在不支持规则的情况下,指定替换的规则

1,schema

  • schema (array) 指定该选项 这样的 ESLint 可以避免无效的规则配置 这里的schema为options,这里里面的结构为json schema,在my-rule.js中,我们将”a“写死在代码里,如果我们希望从过配置项传入时,我们需要更改为以下代码
schema: [
    {
       type: "string"
    }
], 
VariableDeclaration(node) {
    const { declarations } = node;
    declarations?.forEach((v) => {
        const { id } = v;
        if(id?.name === context?.options?.[0]) {
            context.report({
                node,
                messageId: "error",
                data: {},
                fix: function(fixer) {
                    return fixer.replaceText(id, "b");
                }
            });
        }
    })
},

并在eslintrc.js文件中传入,这里将原来的"a"转为"c"

"my-rule": ["error", "c"],

2,create的返回

create (function) 返回一个对象,其中包含了 ESLint 在遍历 JavaScript 代码的抽象语法树 AST (ESTree 定义的 AST) 时,用来访问节点的方法。

  • 如果一个 key 是个节点类型或 selector,在 向下 遍历树时,ESLint 调用 visitor 函数
  • 如果一个 key 是个节点类型或 selector,并带有 :exit,在 向上 遍历树时,ESLint 调用 visitor 函数
  • 如果一个 key 是个事件名字,ESLint 为代码路径分析调用 handler 函数 简单的说,这里的key可以是ast中的statement或者expression,具体可以看到这里javascript中的ast, 在my-rule.js的例子中我们使用了VariableDeclaration,这里会返回所有的VariableDeclaration的节点,如果这里写成"ImportDeclaration"就会返回所以import的节点。

3,抛出report

当我们发现规则有不符合的地方时,我们通过report的方式将错误抛出,这里的messageId为meta中的message的key

context.report({
    node,
    messageId: "error",
    data: {},
});

4,自动化fix

  • Important:  Without the fixable property, ESLint does not apply fixes even if the rule implements fix functions. Omit the fixable property if the rule is not fixable. 重要: 如果没有 fixable 属性,即使规则实现了 fix 功能,ESLint 也不会进行修复。如果规则不是可修复的,就省略 fixable 属性。 首先需要在meta中添加fixable
 meta: {
        type: "suggestion",

        docs: {
        },
        fixable: "code",
        schema: [] // no options
    },

并在report中添加fix的key,将代码自动修复,下面就将不符合的变量名"a"直接改变成"c"

context.report({
    node,
    messageId: "error",
    data: {},
    fix: function(fixer) {
        return fixer.replaceText(id, "b");
    }
});

fixer具有以下一些方法 在这里,fix() 函数被用来在该节点之后插入一个分号。注意,此函数并不立即进行修复,如果与其它修复程序有冲突,可能根本就不进行修复。在应用修复之后,ESLint 将在所有启用的规则再次运行修复的代码,以应用更多的修复。这个过程将最多重复10次,直到找到更多的可修复的问题。之后,其他问题将照常进行报告。

重要: 除非规则输出 meta.fixable 属性,ESLint 不会进行修复,即使该规则实现了 fix 函数。

fixer 对象有一下几个方法:

  • insertTextAfter(nodeOrToken, text) - 在给定的节点或记号之后插入文本
  • insertTextAfterRange(range, text) - 在给定的范围之后插入文本
  • insertTextBefore(nodeOrToken, text) - 在给定的节点或记号之前插入文本
  • insertTextBeforeRange(range, text) - 在给定的范围之前插入文本
  • remove(nodeOrToken) - 删除给定的节点或记号
  • removeRange(range) - 删除给定范围内的文本
  • replaceText(nodeOrToken, text) - 替换给定的节点或记号内的文本
  • replaceTextRange(range, text) - 替换给定范围内的文本

以上方法返回一个 fixing 对象。

fix() 函数可以返回下面的值:

  • 一个 fixing 对象。
  • 一个包含 fixing 对象的数组。
  • 一个可迭代的对象,用来枚举 fixing 对象。特别是,fix() 可以是一个生成器。

如果你让一个 fix() 函数返回多个 fixing 对象,那么这些 fixing 对象不能重叠。

修复的最佳实践:

  1. 避免任何可能改变代码运行时行为和导致其停止工作的修复。

  2. 做尽可能小的修复。那些不必要的修复可能会与其他修复发生冲突,应该避免。

  3. 使每条消息只有一个修复。这是强制的,因为你必须从 fix() 返回修复操作的结果。

  4. 由于所有的规则只第一轮修复之后重新运行,所以规则就没必要去检查一个修复的代码风格是否会导致另一个规则报告错误。

    • 比如,假如修复一个对象的键周周围的引号,但不确定用户是喜欢单引号还是双引号。

        ({ foo : 1 })
      
        // should get fixed to either
      
        ({ 'foo': 1 })
      
        // or
      
        ({ "foo": 1 })
      
    • 修复程序将可以随意选择一种引号类型。如果猜错了,quotes 规则将会自动报告和修复。

四、参考资料

1,eslint
2,ast