【从0到1】教你写个最新ESLint 9插件

563 阅读10分钟

1. ESlint是什么

ESLint 是一种静态代码分析工具,用于在编写 JavaScript 和 TypeScript 代码时识别和报告问题。主要有两个功能:代码质量检查、代码格式化。

代码质量检查:可以发现代码中存在的可能错误,如使用未声明变量、声明而未使用的变量、修改 const 变量等等

代码格式化:可以用来统一团队的代码风格,比如加不加分号、使用 tab 还是空格等等。但ESLint 10可能会完全废除所有格式化规则,代码格式化可交给Prettier处理

小结:未来的ESlint只负责一件事,进行代码质量检查

2. 自定义 ESLint 插件场景

虽然@eslint/js中包含了常见的 JavaScript 编程规则,例如对使用console.log进行告警提示。但某些情况下需自定义团队的编程规则。

例如,setTimeout、setInterval的第二个参数,避免直接使用数字,而是通过定义变量加注释的方式使用。如果直接使用数字,ESLint报错提示,防止项目中出现大量不明意义的时间间隔。

3. 使用模板初始化项目

Node版本:^18.18.0、^20.9.0、>=21.1.0

1. 安装

npm i -g yo generator-eslint

yo:Yeoman 是一个用于生成项目骨架的工具,它通过各种生成器(generators)来帮助开发者快速搭建项目结构。

generator-eslint:是一个专门为 ESLint 项目设计的 Yeoman 生成器。它可以帮助你快速生成和配置 ESLint 项目。

2. 新建eslint-plugin-utils文件夹

3. 在新建文件夹中,使用命令行初始化ESLint项目

yo eslint:plugin

交互命令如下所示

1731574979896.png

第一行:作者名称

第二行:插件ID,例如这里输入wjcao-utils,最终生成的package.json的name为eslint-plugin-wjcao-utils

第三行:插件描述

第四行:这个插件是否包含ESlint规则,选Yes

第五行:这个插件是否包含一个或多个处理器。本篇文章主要讲自定义ESlint插件规则,选No

生成的 lib/rules 目录存放自定义规则,tests/lib/rules 目录存放规则对应的单元测试代码。

4. 使用命令行新建一条ESLint规则

yo eslint:rule

交互命令如下所示

1731575005622.png

第一行:作者名称

第二行:规则将在哪里发布,这里选择ESLint Plugin

第三行:规则的id

第四行:规则描述

第五行:输入一个失败例子的代码,这里直接回车

完成上述操作后,会生成两个文件,分别是 lib/rules/set-timeout.js 和 tests/lib/rules/set-timeout.js

4. 文件分析

1. lib/rules/set-timeout.js

"use strict";

module.exports = {
  meta: {
    type: null, // `problem`, `suggestion`, or `layout`
    docs: {
      description: "setTimeout第二个参数不能直接使用数字",
      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
    messages: {}, // Add messageId and message
  },

  create(context) {
    return {
      // visitor functions for different types of nodes
    };
  },
};

meta:规则的元数据信息

type:规则类型,problem表示报错、suggestion表示建议修改、layout表示该规则关注的是代码格式

docs:文档信息

  • description:表示规则描述

  • recommended:表示规则是否被推荐,为true说明该规则是重要的,建议在多数项目中启用

  • url:表示该规则的文档页面。

fixable:是否可自动修复。当值为code时,还需在create函数中提供自动修复的代码

schema:定义规则所需要的外部参数

messages:定义规则报告错误时使用的消息 ID 和消息内容

create:规则的主要入口点,它接收一个 context 上下文对象,返回一个对象,该对象的属性名表示节点类型,在向下遍历树时,当遍历到和属性名匹配的节点时,ESLint 会调用属性名对应的函数。

2. tests/lib/rules/set-timeout.js

"use strict";

const rule = require("../../../lib/rules/typeof-limit"),
  RuleTester = require("eslint").RuleTester;

const ruleTester = new RuleTester();
ruleTester.run("set-timeout", rule, {
  valid: [
    // give me some code that won't trigger a warning
  ],

  invalid: [
    {
      code: "",
      errors: [{ messageId: "Fill me in.", type: "Me too" }],
    },
  ],
});

run第一个参数:表示规则名称,也就是之前使用命令行创建ESLint规则时的rule ID

run第二个参数:表示具体的规则,也就是lib/rules/set-timeout.js文件内容

run第三个参数:是一个对象,valid包含不会触发警告的代码片段,invalid包含会触发警告的代码片段

5. AST抽象语法树

ESLint 使用 Espree 作为默认的 JavaScript 解析器。解析器将源代码转换成一个抽象语法树(AST)。访问AST转换平台:astexplorer.net/,将以下函数输入,右侧将出现转换结果

setTimeout(()=>{},1000)

右侧JSON结构:

{
  "type": "Program",
  "start": 0,
  "end": 24,
  "body": [
    {
      "type": "ExpressionStatement",
      "start": 0,
      "end": 23,
      "expression": {
        "type": "CallExpression",
        "start": 0,
        "end": 23,
        "callee": {
          "type": "Identifier",
          "start": 0,
          "end": 10,
          "name": "setTimeout"
        },
        "arguments": [
          {
            "type": "ArrowFunctionExpression",
            "start": 11,
            "end": 17,
            "id": null,
            "expression": false,
            "generator": false,
            "async": false,
            "params": [],
            "body": {
              "type": "BlockStatement",
              "start": 15,
              "end": 17,
              "body": []
            }
          },
          {
            "type": "Literal",
            "start": 18,
            "end": 22,
            "value": 1000,
            "raw": "1000"
          }
        ],
        "optional": false
      }
    }
  ],
  "sourceType": "module"
}

分析AST抽象语法树主要看type。

Program:程序的根节点

ExpressionStatement:一个表达式语句

CallExpression:函数表达式

Identifier:标识符,可以简单理解为变量名,函数名,属性名

ArrowFunctionExpression:箭头函数表达式

BlockStatement:一个代码块

Literal:一个字面量,可以理解为一个具体的值

这段AST抽象语法树表示:一个名为setTimeout的函数表达式,有两个参数,第一个参数是一个箭头函数,第二个参数是一个具体的值。

6. ESLint运行流程

ESLint运行流程可以简单概括为:解析、遍历、触发回调

解析:ESLint使用JavaScript解析器Espree把JS代码解析成AST

遍历:对AST抽象语法树进行遍历

触发回调:每当匹配到对应的节点,触发rule监听器上的回调

我们主要的工作是定义规则,编写create函数

7. setTimeout第二个参数检测

  1. 修改lib/.../set-timeout.js
/**
 * @fileoverview setTimeout第二个参数不能直接使用数字
 * @author wjcao
 */
"use strict";

/** @type {import('eslint').Rule.RuleModule} */
module.exports = {
  meta: {
    type: "problem", //报错提示
    docs: {
      description: "setTimeout第二个参数不能直接使用数字",
      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
    messages: {
      setTimeoutError: "setTimeout第二个参数不能直接使用数字",
    },
  },

  create(context) {
    return {
      //匹配函数调用
      CallExpression: (node) => {
        if (node.callee.name === "setTimeout") {
          const timeNode = node.arguments && node.arguments[1]; // 获取第二个参数
          if (timeNode) {
            // 检测报错第二个参数是数字 报错
            if (timeNode.type === "Literal" && typeof timeNode.value === "number") {
              context.report({
                node,
                messageId: "setTimeoutError",
              });
            }
          }
        }
      },
    };
  },
};

在元信息meta中,定义提示类型type为problem,定义提示消息messages,messages是一个对象,key值是messages的ID,value值是具体的提示消息

在create函数中,匹配CallExpression函数调用,函数名为setTimeout则进一步判断第二个参数是否为number类型,如果是,则使用context.report抛出错误提示。

2. 修改tests/.../set-timeout.js,进行单元测试

"use strict";

const rule = require("../../../lib/rules/set-timeout"),
  RuleTester = require("eslint").RuleTester;

const ruleTester = new RuleTester();
ruleTester.run("set-timeout", rule, {
  valid: [
    {
      code: "const delay = 1000; setTimeout(()=>{},delay)",
    },
  ],

  invalid: [
    {
      code: "setTimeout(()=>{},1000)",
      errors: [{ messageId: "setTimeoutError", type: "CallExpression" }],
    },
  ],
});

valid中定义合法的单元测试,invalid定义不合法的测试。

写好单元测试后,执行“npm test”命令即可运行测试,如果看到如图所示的输出,则表示单元测试通过了。

1731575209949.png

8. setInterval第二个参数检测

1. 使用命令行新建一条ESLint规则

yo eslint:rule

交互命令如下所示

1731575239500.png

2. 参照set-timeout.js,修改lib/rules/set-interval.js

"use strict";

//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------

/** @type {import('eslint').Rule.RuleModule} */
module.exports = {
  meta: {
    type: "problem", // `problem`, `suggestion`, or `layout`
    docs: {
      description: "setInterval第二个参数不能是数字",
      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
    messages: {
      setIntervalError: "setInterval第二个参数不能直接使用数字",
    }, // Add messageId and message
  },

  create(context) {
    return {
      //匹配函数调用
      CallExpression: (node) => {
        if (node.callee.name === "setInterval") {
          const timeNode = node.arguments && node.arguments[1]; // 获取第二个参数
          if (timeNode) {
            // 检测报错第二个参数是数字 报错
            if (timeNode.type === "Literal" && typeof timeNode.value === "number") {
              context.report({
                node,
                messageId: "setIntervalError",
              });
            }
          }
        }
      },
    };
  },
};

3. 修改tests/.../set-interval.js,进行单元测试

"use strict";

const rule = require("../../../lib/rules/set-interval"),
  RuleTester = require("eslint").RuleTester;

const ruleTester = new RuleTester();
ruleTester.run("set-interval", rule, {
  valid: [
    {
      code: "const delay = 1000; setInterval(()=>{},delay)",
    },
  ],

  invalid: [
    {
      code: "setInterval(()=>{},1000)",
      errors: [{ messageId: "setIntervalError", type: "CallExpression" }],
    },
  ],
});

写好单元测试后,执行“npm test”命令即可运行测试

9. Plugins与Extends

在使用自定义插件前,需明白Plugins与Extends的使用场景

1. Plugins

Plugins负责加载插件,以扩展ESLint的功能。例如校验React Hooks,则使用eslint-plugin-react-hooks。

plugins: {   "react-hooks": reactHooks, },

Plugins只是加载了插件,还需在rules中开启对应的规则。例如开启eslint-plugin-react-hooks推荐的全部规则

rules: {   ...reactHooks.configs.recommended.rules, },

2. extends

extends 用于继承现有的配置文件,简单理解为是plugins与rules的结合。例如开启ESLint JavaScript代码推荐规则

extends: [js.configs.recommended],

10. 在React项目中使用自定义ESLint规则

在发布插件前,先通过link的方式进行软件包测试

  1. 确保VSCode安装了ESLint插件

  2. 在eslint-plugin-utils项目下运行

npm link
  1. 使用vite,新搭建一个React项目,项目模板自带了ESLint
npm create vite\@latest vite-react-ts-seed -- --template react-ts
  1. 在根目录下执行下面的命令,这会在 node_modules 目录下创建一个软链接
npm link eslint-plugin-wjcao-utils

link后面的eslint-plugin-wjcao-utils为eslint-plugin-utils项目中的package.json的name值

  1. 修改项目的eslint.config.js
import js from "@eslint/js";
import globals from "globals";
import reactHooks from "eslint-plugin-react-hooks";
import reactRefresh from "eslint-plugin-react-refresh";
import tseslint from "typescript-eslint";
import myLint from "eslint-plugin-wjcao-utils"; //新增

export default tseslint.config(
  { ignores: ["dist"] },
  {
    extends: [js.configs.recommended, ...tseslint.configs.recommended],
    files: ["**/*.{ts,tsx}"],
    languageOptions: {
      ecmaVersion: 2020,
      globals: globals.browser,
    },
    plugins: {
      "react-hooks": reactHooks,
      "react-refresh": reactRefresh,
      "my-limit": myLint, //新增
    },
    rules: {
      ...reactHooks.configs.recommended.rules,
      "react-refresh/only-export-components": ["warn", { allowConstantExport: true }],
      "my-limit/set-timeout": "error", //新增
      "my-limit/set-interval": "error", //新增
    },
  }
);

  1. 测试:修改App.tsx
import { useEffect } from "react";

function App() {
  useEffect(() => {
    setTimeout(() => {
      console.log("setTimeout");
    }, 1000);

    setInterval(() => {
      console.log("setInterval");
    }, 2000);
  }, []);
  return <></>;
}

export default App;

编辑器直接飘红报错提示:

1731575407830.png

  1. 运行 npm run lint,也能在终端中得到报错信息

11. 在Vue项目中使用自定义ESLint规则

在发布插件前,先通过link的方式进行软件包测试

  1. 确保VSCode安装了ESLint插件

  2. 在eslint-plugin-utils项目下运行

npm link
  1. 使用vite,新搭建一个Vue项目
npm create vite@latest vite-vue-ts-seed -- --template vue-ts
  1. 项目模板目前没有自带ESLint,需手动安装
npm init @eslint/config

根据提示进行安装

1731575455887.png

  1. 在根目录下执行下面的命令,这会在 node_modules 目录下创建一个软链接
npm link eslint-plugin-wjcao-utils

link后面的eslint-plugin-wjcao-utils为eslint-plugin-utils项目中的package.json的name值

  1. 修改项目的eslint.config.js
import globals from "globals";
import pluginJs from "@eslint/js";
import tseslint from "typescript-eslint";
import pluginVue from "eslint-plugin-vue";
import myLint from "eslint-plugin-wjcao-utils"; //新增

export default [
  { files: ["**/*.{js,mjs,cjs,ts,vue}"] },
  { languageOptions: { globals: globals.browser } },
  pluginJs.configs.recommended,
  ...tseslint.configs.recommended,
  ...pluginVue.configs["flat/essential"],
  { files: ["**/*.vue"], languageOptions: { parserOptions: { parser: tseslint.parser } } },
  //新增
  {
    plugins: {
      "my-limit": myLint,
    },
    rules: {
      "my-limit/set-timeout": "error",
      "my-limit/set-interval": "error",
    },
  },
];
  1. 测试:修改App.vue
<template>
  <div></div>
</template>

<script setup lang="ts">
setTimeout(() => {
  console.log("setTimeout");
}, 1000);

setInterval(() => {
  console.log("setInterval");
}, 2000);
</script>

编辑器直接飘红报错提示:

1731575521031.png

  1. 修改package.json,在script中新增一条命令
"lint": "eslint src"

运行 npm run lint,也能在终端中得到报错信息

12. 插件规则集成

之前需要手动一条一条配置规则,例如

"my-limit/set-timeout": "error", //新增 
"my-limit/set-interval": "error", //新增

如果自定义规则多了之后,这样一条条引入肯定不现实

  1. 修改lib/index.js,将规则放到recommended字段中
"use strict";

const requireIndex = require("requireindex");

const plugin = {
  configs: {},
  rules: requireIndex(__dirname + "/rules"),
};

Object.assign(plugin.configs, {
  recommended: {
    plugins: {
      "my-limit": plugin,
    },
    rules: {
      "my-limit/set-timeout": "error",
      "my-limit/set-interval": "error",
    },
  },
});

module.exports = plugin;
  1. 修改项目的eslint.config.js(React项目)
//...
import myLint from "eslint-plugin-wjcao-utils"; //新增

export default tseslint.config(
  { ignores: ["dist"] },
  {
    extends: [
     js.configs.recommended,
     ...tseslint.configs.recommended,
     myLint.configs.recommended], //新增一个recommended
    //...
  }
);

  1. 修改项目的eslint.config.js(Vue项目)
//...
import myLint from "eslint-plugin-wjcao-utils"; //新增

export default [
  { files: ["**/*.{js,mjs,cjs,ts,vue}"] },
  { languageOptions: { globals: globals.browser } },
  pluginJs.configs.recommended,
  myLint.configs.recommended, //新增
  //...省略
];

使用VSCode重新打开项目,使ESLint校验规则生效

13. 发包

发包比较简单,两个命令npm login与npm publish

之前介绍过发包过程,具体可参考:发布到npm

结尾

本篇文章限于篇幅只介绍了ESLint部分用法,如果对ESLint感兴趣,可在评论区留言。如果感兴趣的小伙伴多的话,我再继续更新ESlint相关的文章。

创作不易,欢迎点赞+收藏支持!!!

参考文章:juejin.cn/post/684490…