ESLint 自定义语法规则插件开发

607 阅读4分钟

是不是用了那么久eslint,只知道如何关闭它?

一、介绍

JavaScript 是一个动态的弱类型语言,在开发中比较容易出错。

因为没有编译程序,为了寻找 JavaScript 代码错误通常需要在执行过程中不断调试。

像 ESLint 这样的可以让程序员在编码的过程中发现问题而不是在执行的过程中。

检测并修复 JavaScript 代码中的问题

简介:开源的 JavaScript 代码检查工具
作者:Nicholas C. Zakas

时间:2013年6月
环境:使用 Node.js 编写

ESLint每条规则:

  • 各自独立
  • 可以开启或关闭(没有什么可以被认为“太重要所以不能关闭”)
  • 可以将结果设置成警告或者错误

二、插件开发

功能

  • 当用户使用 getXXX get开头的函数的时候 如果不返回值的话 那么就会报错

  • 可以 fix

  • 用户可以自行配置是否 fix

每个插件是一个命名格式为 eslint-plugin- 的 npm 模块,

比如 eslint-plugin-jquery。

你也可以用这样的格式 @/eslint-plugin- 限定在包作用域下,

比如

2.1、 插件脚手架

创建一个插件最简单的方式是使用 Yeoman generator

我们利用 yeomangenerator-eslint 来构建插件的脚手架代码。安装:

npm install -g yo generator-eslint

本地新建文件夹eslint-plugin-demoget:

mkdir eslint-plugin-pengyuan
cd eslint-plugin-pengyuan

初始化 ESLint 插件的项目结构:

// 搭建一个初始化的目录结构
yo eslint:plugin 

文件目录结构:

├── README.md
├── docs // 使用文档
│   └── rules
│       └── no-console-time.md
├── lib // eslint 规则开发
│   ├── index.js
│   └── rules // 此目录下可以构建多个规则,本文只拿一个规则来讲解
│       └── no-console-time.js
├── package.json
└── tests // 单元测试
    └── lib
        └── rules
            └── no-console-time.js

安装依赖:

npm install

2.2、初始化新规则

# 生成默认 eslint rule 模版文件
yo eslint:rule 

此时项目结构为:

.
├── README.md
├── docs // 使用文档
│   └── rules
│       └── get-return.md
├── lib // eslint 规则开发
│   ├── index.js
│   └── rules // 此目录下可以构建多个规则,本文只拿一个规则来讲解
│       └── get-return.js
├── package.json
└── tests // 单元测试
    └── lib
        └── rules
            └── get-return.js

上面结构中,我们需要在 ./lib/ 目录下去开发 Eslint 插件,这里是定义它的规则的位置。

先运行一次

npm run test

开发热更新调试:

// package.json
"test": "mocha tests --recursive",

pnpm test -- --watch

创建vscode launch.json 便于debugger

{
    // 使用 IntelliSense 了解相关属性。 
    // 悬停以查看现有属性的描述。
    // 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387
    "version": "0.2.0",
    "configurations": [
        {
            "name": "pnpm test",
            "request": "launch",
            "runtimeArgs": [
                "test",
                "--",
                "--watch"
            ],
            "runtimeExecutable": "pnpm",
            "skipFiles": [
                "<node_internals>/**"
            ],
            "type": "node"
        }
    ]
}

测试用例

const ruleTester = new RuleTester({ parserOptions: { ecmaVersion: 2015 } });
 

valid: [
    {
      code: "function getName(){ return 'pengyuan'}",
    },
    {
      code: "function setName(){}",
    },
  ],
  invalid: [
    {
      name: "no fix",
      code: "function getName(){}",
      output: null,
      errors: [{ message: "getXX function name must return a value" }],
    },
  ],

2.3、AST 在 ESLint 中的运用

根据《圣经·旧约·创世记》篇章记载,

当时人类联合起来兴建希望能通往天堂的高塔;为了阻止人类的计划,上帝让人类说不同的语言,使人类相互之间不能沟通,计划因此失败,人类自此各散东西。

astexplorer.net/

AST (Abstract Syntax Tree(抽象语法树))

function getName(){}

上面讲完了 ESLint 和 AST 的关系之后,我们可以正式进入开发具体规则。

2.4、规则实现

先来看之前生成的 lib/rules/get-return.js:

// eslint-disable
/**
 * @fileoverview 人家是规则的描述哦
 * @author songpengyuan
 */
"use strict";

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

/** @type {import('eslint').Rule.RuleModule} */
module.exports = {
  meta: {
    type: "suggestion", // `problem`, `suggestion`, or `layout`
    docs: {
      description: "人家是规则的描述哦",
      recommended: false,
      url: 'https://okki.com/', // URL to the documentation page for this rule
    },
    schema: [],
    messages: {
      errorMessageId: "getXX function name must return a value",
    },
  },

  create(context) {
    
    function reportError(node) {
      context.report({
        node,
        messageId: "errorMessageId",
      });
    }

    return {
      FunctionDeclaration(node) {
        if (node.id.name.startsWith("get")) {
          const blockStatementBody = node.body.body;
          const lastNode = blockStatementBody[blockStatementBody.length - 1];
          if (!lastNode || lastNode.type !== "ReturnStatement") {
            reportError(node);
          }
        }
      },
    };
  },
};

再修改 lib/index.js:

const requireIndex = require("requireindex");

//------------------------------------------------------------------------------
// Plugin Definition
//------------------------------------------------------------------------------

// import all rules in lib/rules
module.exports = {
  configs: {
    recommended: {
      plugins: ["pengyuan"],
      rules: {
        "pengyuan/get-return": ["warn", true],  // 可以省略 eslint-plugin 前缀
      },
    },
  },
  rules: requireIndex(__dirname + "/rules"),
};

rule:get 命名开头的函数,必须有返回值 fix可自动添加 return‘

可以通过 options 的形式控制 fix 的行为

在项目中测试下·~~

三、npm 发布

"files": [
  "lib"
],
# 登录
npm login

# 发布
npm publish

四、如何在项目中使用

安装依赖包:eslint-plugin-pengyuan

npm i eslint-plugin-pengyuan

# 没有发布,先使用本地的
pnpm i ../eslint-plugin-pengyuan

然后在 .eslintrc.js 中配置:

module.exports = {
  root: true,
  plugins: ["pengyuan"],
  // extends: ['plugin:pengyuan/recommended'],
  rules: {
    "pengyuan/get-return": ["warn"],
  },
  env: {
    node: true,
  },
  overrides: [
    {
      files: ["tests/**/*.js"],
      env: { mocha: true },
    },
  ],
};

此时,如果在当前项目的 JS 文件中书写 get开头的函数没有返回值,会出现如下效果:

五、 快速修复 fix

meta: {
   fixable: "code", // Or `code` or `whitespace`
   schema: [
      {
        type: "boolean",
      },
    ],
    // ...
}
 valid: [
    {
      code: "function getName(){ return 'pengyuan'}",
    },
    {
      code: "function setName(){}",
    },
  ],
  invalid: [
    {
      name: "should throw error when function doesn't return value",
      code: "function getName(){}",
      output: "function getName(){return ''\r}",
      errors: [{ message: "getXX function name must return a value" }],
    },
    {
      name: "should throw error when function doesn't return value",
      code: "function getName(){ const name = 'cxr'}",
      errors: [{ message: "getXX function name must return a value" }],
      output: `function getName(){ const name = 'cxr';return ''\r}`,
    },
    {
      name: "no fix",
      code: "function getName(){}",
      output: null,
      options: [false],
      errors: [{ message: "getXX function name must return a value" }],
    },
  ],
const isFix = context.options[0];



context.report({
    node,
    messageId: "errorMessageId",
    fix: (fixer) => {
      if (isFix === false) return fixer.insertTextAfter(node, "");
      const endPosition = node.range[1] - 1;
      const prefix = node.body.body.length === 0 ? "" : ";";
      return {
        range: [endPosition, endPosition],
        text: `${prefix}return ''\r`,
      };
   }
});

五、扩展:

1. eslint-plugin-vue

提供vue-eslint-parser了一些有用的解析器服务,以帮助遍历生成的 AST 和访问模板的令牌:

  • context.parserServices.defineTemplateBodyVisitor(visitor, scriptVisitor)
  • context.parserServices.getTemplateBodyTokenStore()
  • ......

六、参考资料

  1. Eslint 中文文档
  2. bilibili: 实现 ESLint Plugin 扩展自己的 Rule--阿崔cxr
  3. eslint-plugin-react
  4. awesome-eslint
  5. eslint-config-alloy
  6. eslint-config-imweb,基于 eslint-config-airbnb 封装
  7. code-guide