ESLint插件开发

211 阅读8分钟

ESLint 是一个用于识别和报告在 ECMAScript/JavaScript 代码中发现的模式的工具,其目标是使代码更加一致并避免错误。

这篇文章是简单介绍一下Eslint的插件和规则是如何开发的,可以帮助我们更深的理解Eslint的运行机制

ESLint 是完全插件化的。每条规则都是一个插件,你可以在运行时添加更多。你还可以添加社区插件、配置来扩展 ESLint 的功能

本篇文档会带大家模拟一个Eslint中自带的比较常用的 eqeqeq 的这个规则,不熟悉的同学可以点击此处查看规则此规则如何使用 eqeqeq - ESLint 中文网

创建项目

我们这边使用generator-eslint来生成eslint的项目

generator-eslint

全局安装 yo generator-eslint

npm install -g yo generator-eslint

创建一个eslint-plugin-custom的文件夹,并且进入

mkdir eslint-plugin-custom

cd eslint-plugin-custom

使用 generator-eslint 命令生成插件的架子

yo eslint:plugin

这是一个交互式命令,下面是所需要填写的信息

? What is your name? wang  ## 你的名字是什么
? What is the plugin ID? eslint-plugin-custom  ## 给你的插件起个名字
? Type a short description of this plugin: eslint rule eqeqeq  ## 描述你的插件
? Does this plugin contain custom ESLint rules? Yes    ##插件需要规则吗
? Does this plugin contain one or more processors? No  ## 需要处理器吗

其实填完之后对应的就是如下的内容

现在我们架子是有了,接下来我们创建一个模板rule

yo eslint:rule

所填写的信息

? What is your name? wang  ## 你的名字
? Where will this rule be published? ESLint Plugin ## 你的规则发布到哪里
? What is the rule ID? eqeqeq ## 你的规则id
? Type a short description of this rule: Rule to flag statements that use != and == instead of !== and ===  ## 描述你的规则
? Type a short example of the code that will fail: a == b ## 错误示例
   create docs/rules/eqeqeq.md
   create lib/rules/eqeqeq.js
   create tests/lib/rules/eqeqeq.js

对应如下,会帮我们创建好文件

至此下面就开始写我们的插件了

这里参照eslint的eqeqeq进行示例

eslint/lib/rules/eqeqeq.js at main · eslint/eslint

基础知识

规则包含的内容

详细版可以看官网 自定义规则 - ESLint 中文网

规则包含meta 对象和create函数 ,meta对象主要是描述规则的一些信息, create函数是主要的执行函数

具体每一项干嘛的,可以看官网,这里列出来常用的一些做说明

// customRule.js

module.exports = {
  // meta 属性包含规则的元信息
  meta: {
    /**
     *  `problem`: 这条规则是检查错误的问题,优先解决
     *  `suggestion`: 这条规则不会导致代码错误,但是可以使用更好的方式
     *  `layout`: 这条规则是有关代码的外观,比如空格没对齐之类的
     */
    type: 'suggestion',

    docs: {
      // 描述这条规则是干嘛的
      description: "Require the use of `===` and `!==`",
      // 方便使用者点击链接去查看该条规则
      url: 'https://eslint.org/docs/latest/rules/eqeqeq',
    },

    // 'code' 表示这条规则可以被修复, 如果 命令行 上的 --fix 选项自动修复规则报告的问题,则为 "code", 修复逻辑写在create方法中
    fixable: 'code',
    // 描述这条规则需要的参数信息
    schema: [],

    // 在上报错误的时候需要使用的字段,key为提示消息错误的id, value为值, value中双花括号可以接收在使用途中传入的字段
    messages: {
      unexpected: "Expected '{{expectedOperator}}' and instead saw '{{actualOperator}}'."
    }
  },

  /**
   * 这是写校验逻辑的主方法
   * @param {*} context 参数有很多属性,详情可以查看官网
   * @returns 返回值是一个 visitor 对象, visitor对象可以参考AST生成的代码树里的结构
   */
  create(context) {
    
    return {
      // 运行到type 为 BinaryExpression节点的时候会调用这个方法
      BinaryExpression(node) {
        
      }
    };
  },
};

图中可点击的eqeqeq就是meta.docs.url对应的信息

create方法中的返回值是一个 visitor 对象,那什么是 visitor 对象呢?

该对象包含一系列以 AST 节点类型为键的函数。这些函数会在 ESLint 遍历代码生成的 AST 时被调用,针对每种类型的节点执行相应的检查或转换逻辑

AST又是什么呢?

AST

AST是: Abstract Syntax Tree的简称,中文叫做:抽象语法树。

将代码抽象成树状数据结构,方便后续分析检测代码

astexplorer.net是一个工具网站:它能查看代码被解析成AST的样子

图中框起来的内容就是 a === b 被解析后的结果

包含 type start end 等等参数, 其中type对应的类型就是visitor对象对应的key, 当遍历到对应的key时候,会调用对应的函数

exporession 就是主要内容,里面同样也有start type end 等参数 left 就是表达式对应的左边内容, right对应右边 opreator 就是对应的操作符

知道这个概念后我们就可以开始写逻辑了

开发规则

meta

我们先声明一下规则中meta方法里面的内容

meta: {
    /**
     *  `problem`: 这条规则是检查错误的问题,优先解决
     *  `suggestion`: 这条规则不会导致代码错误,但是可以使用更好的方式
     *  `layout`: 这条规则是有关代码的外观,比如空格没对齐之类的
     */
    type: 'suggestion',

    docs: {
      // 描述这条规则是干嘛的
      description: "Require the use of `===` and `!==`",
      // 方便使用者点击链接去查看该条规则
      url: 'https://eslint.org/docs/latest/rules/eqeqeq',
    },

    // 'code' 表示这条规则可以被修复, 如果 命令行 上的 --fix 选项自动修复规则报告的问题,则为 "code", 修复逻辑写在create方法中
    fixable: 'code',
    // 描述这条规则需要的参数信息
    schema: [
      {
        // anyOf 是一个数组类型,代表此参数满足数组中的某一项就可以
        anyOf: [
          {
            // 代表第一种情况是一个数组类型
            type: 'array',
            // 数组的子项
            items: [
              {
                // 数组的第一项能填的选项
                enum: ['always'],
              },
              {
                // 数组的第二个选项是对象类型
                type: 'object',
                properties: {
                  // 声明对象里的子项参数,参数名称为null
                  null: {
                   // always(默认) - 始终使用 === 或 !==。
                   // never  切勿将 === 或 !== 与 null 一起使用。
                   // ignore  不要将此规则应用于 null。
                    enum: ["always", "never", "ignore"]
                  }
                },
                // 否允许JSON对象中出现未在 properties 中声明的额外属性
                additionalProperties: false,
              }
            ],
            // 是否允许出现在items里未出现的类型
            additionalItems: false
          },
          {
            type: 'array',
            items: [
              {
                // smart 为智能比较  allow-null 废弃了
                enum: ['smart', 'allow-null']
              }
            ],
            additionalItems: false
          }
        ]
      }
    ],

    // 在上报错误的时候需要使用的字段,key为提示消息错误的id, value为值, value中双花括号可以接收在使用途中传入的字段
    messages: {
      unexpected: "Expected '{{expectedOperator}}' and instead saw '{{actualOperator}}'."
    }
  },

其他参数都有介绍过了,其中schema 参数里的信息比较复杂, 对应的就是规则参数声明

create

create 方法

create(context) {
    // context.options 可以拿到使用的时候传递过来的参数 默认为always
    const config = context.options[0] || "always";
    const options = context.options[1] || {};

    // sourceCode 可以拿到当前的源代码对象
    const sourceCode = context.sourceCode;

    // config === 'always' 代表我们上面声明类型的第一种情况    可以看到第一种情况 null的默认值为always  第二种情况null的默认值为ignore
    const nullOption = (config === 'always') ? (options.null || 'always') : 'ignore';

    const enforceRuleForNull = (nullOption === "always");

    const enforceInverseRuleForNull = (nullOption === "never");

    /**
     * 检查表达式是不是typeof类型的
     * 后面有针对typeof类型做特出处理
     */
    function isTypeOf(node) {
      return node.type === "UnaryExpression" && node.operator === "typeof";
    }

    /**
     * 检查操作符任意一边是不是typeof的
     */
    function isTypeOfBinary(node) {
      return isTypeOf(node.left) || isTypeOf(node.right);
    }
    /**
     * 检查操作符的两边类型是否一致
     */
    function areLiteralsAndSameType(node) {
      return node.left.type === "Literal" && node.right.type === "Literal" &&
        typeof node.left.value === typeof node.right.value;
    }
    /**
     * 确定当前节点是否是null
     */
    function isNullLiteral(node) {
      return node.type === "Literal" && node.value === null;
    }
    /**
     * 检查操作符两边是否有一边为null
     */
    function isNullCheck(node) {
      return isNullLiteral(node.right) || isNullLiteral(node.left);
    }

    /**
     * 封装report方法,方便上报错误信息
     */
    function report(node, expectedOperator) {
      // 查找当前节点中满足条件的 操作符
      const operatorToken = sourceCode.getFirstTokenBetween(
        node.left,
        node.right,
        token => token.value === node.operator
      );
      // 上报错误的主方法
      context.report({
        // 与问题相关的ast节点
        node,
        // 指定问题的位置
        loc: operatorToken.loc,
        // 对应meta.message中写的错误信息
        messageId: "unexpected",
        // 会传递给meta.message的模板语法的参数
        data: { expectedOperator, actualOperator: node.operator },
        fix(fixer) {

          // 如果两边有一边是typeof 类型的 或者 两边的类型是一致的可以安全修复
          if (isTypeOfBinary(node) || areLiteralsAndSameType(node)) {
            // 替换当前节点
            return fixer.replaceText(operatorToken, expectedOperator);
          }
          return null;
        }
      });
    }
    return {
      // visitor functions for different types of nodes
      // 运行到type 为 BinaryExpression节点的时候会调用这个方法
      BinaryExpression(node) {
        const isNull = isNullCheck(node);

        // 如果操作符不是'==' 和'!='
        if (node.operator !== '==' && node.operator !== '!=') {
          // 如果强制声明不要与null比较,同时对比的参数有null
          if (enforceInverseRuleForNull && isNull) {
            report(node, node.operator.slice(0, -1))
          }
          // 验证通过
          return;
        }
        // 剩下的场景都是操作符为 '==' 或者 '!=' 了

        // 配置的选项为smart, 同时有typeof 或者 类型相等,则不需要报错, 比如 typeof foo == 'undefined' 或者 true == true
        if (config === 'smart' && (isTypeOfBinary(node) || areLiteralsAndSameType(node))) {
          return;
        }
        
        // 忽略和null的比较
        if (!enforceRuleForNull && isNull) {
          return;
        }
        
        report(node, `${node.operator}=`)
      },
    };
  },

验证规则

test/lib/rules/eqeqeq.js

/**
 * @fileoverview Rule to flag statements that use != and == instead of !== and ===
 * @author wang
 */
"use strict";

//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------

const rule = require("../../../lib/rules/eqeqeq"),
  RuleTester = require("eslint").RuleTester;


//------------------------------------------------------------------------------
// Tests
//------------------------------------------------------------------------------

const ruleTester = new RuleTester();
ruleTester.run("eqeqeq", rule, {
  // 正确的代码
  valid: [
    {
      code: "a === b",
    },
  ],
  // 错误的代码
  invalid: [
    {
      code: "a == b",
      errors: [{ message: "Expected '===' and instead saw '=='.", type: "BinaryExpression" }],
    },
  ],
});

通过 node命令 执行

执行这段程序,如果没报错说明 验证成功

vscode 调试

选择node.js

确认路径没错

开始调试

没报错说明没错

也可以打debugger进行调试,更方便我们理解

使用插件

我们可以在npm上发布当前插件,发布之后在自己的项目可以通过配置进行使用

// .eslintrc.js
module.exports = {
  plugins: ['eslint-plugin-custom'],
  rules: { 
    "eslint-plugin-custom/eqeqeq": "error"
 }
}