注册
web

手把手教你实现一个自定义 eslint 规则

ESlint 概述

ESLint 是一个代码检查工具,通过静态的分析,寻找有问题的模式或者代码。默认使用 Espree 解析器将代码解析为 AST 抽象语法树,然后再对代码进行检查。

抽象语法树Abstract Syntax Tree,AST),或简称语法树(Syntax tree),是源代码语法结构的一种抽象表示。它以树状的形式表现编程语言的语法结构,树上的每个节点都表示源代码中的一种结构。

随着前端工程化体系的不断发展,Eslint 已经前端工程化不可缺失的开发工具。它解决了前端工程化中团队代码风格不统一的问题,避免了一些由于代码规范而产生的 Bug, 同时它提高了了团队的整体效率。

运行机制

Eslint的内部运行机制不算特别复杂,主要分为以下几个部分:

  • preprocess,把非 js 文本处理成 js

  • 确定 parser(默认是 espree

  • 调用 parser,把源码 parseSourceCodeAST

  • 调用 rules,对 SourceCode 进行检查,返回 linting problems

  • 扫描出注释中的 directives,对 problems 进行过滤

  • postprocess,对 problems 做一次处理

  • 基于字符串替换实现自动 fix

具体描述,这里就不补充了。详细的运行机制推荐大家去学习一下Eslint的底层实现原理和源码。

常用规则

为了让使用者对规则有个更好的理解, Eslint 官方将常用的规则进行了分类并且定义了一个推荐的规则组 "extends": "eslint:recommended"。具体规则详情请见官网

示例规则如下:

  • array-element-newline<string|object>
    "always"(默认) - 需要数组元素之间的换行符
    "never" - 不允许数组元素之间换行
    "consistent" - 数组元素之间保持一致的换行符

配置详解

Eslint 配置我们主要通过.eslintrc配置来描述

extends

extends 的内容为

一个 ESLint配置文件,一旦扩展了(即从外部引入了其他配置包),就能继承另一个配置文件的所有属性(包括rules, plugins, and language option在内),然后通过 merge合并/覆盖所有原本的配置。最终得到的配置是前后继承和覆盖前后配置的并集。

extends属性的值可以是:

  • 定义一个配置的字符串(配置文件的路径、可共享配置的名称,如eslint:recommendedeslint:all)

  • 定义规则组的字符串。plugin:插件名/规则名称 (插件名取eslint-plugin-之后的名称)

 "extends": [
   "eslint:recommended",
   "plugin:react/recommended"
],

parserOptions

指定你想要支持的 JavaScript 语言选项。默认支持 ECMAScript 5 语法。你可以覆盖该设置,以启用对 ECMAScript 其它版本和 JSX 的支持。

"parserOptions": {
 "ecmaVersion": 6,
 "sourceType": "module",
 "ecmaFeatures": {
    "jsx": true
}
}

rules

ESLint 拥有大量的规则。你可以通过配置插件添加更多规则。使用注释或配置文件修改你项目中要使用的规则。要改变一个规则,你必须将规则 ID 设置为下列值之一:

  • "off"0 - 关闭规则

  • "warn"1 - 开启规则,使用警告级别的错误:warn (不会导致程序退出)

  • "error"2 - 开启规则,使用错误级别的错误:error (当被触发的时候,程序会退出)

"plugins": [
 "plugin-demo",
],
"rules": {
 "quotes": ["error", "double"], // 修改eslint recommended中quotes规则
 "plugin-demo/rule1": "error", // 配置eslint-plugin-plugin-demo 下rule1规则
}

对于 Eslint recommended 规则组中你不想使用的规则,也可以在这里进行关闭。

plugin

ESLint 支持使用第三方插件。要使用插件,必须先用 npm进行安装

"plugins": [
  "plugin-demo", // // 配置 eslint-plugin-plugin-demo 插件
],

这里做一下补充,extendsplugin 的区别在于,extendsplugin 的子集。就好比如 Eslint 中除了 recommended 规则组还有其他规则

自定义Eslint插件

团队开发中,我们经常会使用一些 eslint 规则插件来约束代码开发,但偶尔也会有一些个性定制化的团队规范,而这些规范就需要通过一些自定义的 ESlint 插件来实现。

我们先看一段简短的代码:

import { omit } from 'lodash';

上述代码是我们在使用lodash的一个习惯性写法,但是这段代码会导致全量引入lodash,造成工程包体积偏大。

正确的引用方式如下:

import omit from 'lodash/omit';

// 或
import { omit } from 'lodash-es';

我们希望可以通过插件去约束开发者的使用习惯。但是 Eslint 自带的规则对于这个定制化的场景就无法满足了。此时, 就需要去使用 Eslint 提供的开放能力去定制化一个 Eslint 规则。接下来我将从创建到使用去实现一个lodash引用规范的Eslint自定义插件

创建

工程搭建

Eslint 官方提供了脚手架来简化新规则的开发, 如不使用脚手架搭建,只需保证和脚手架一样的结构就可以啦。

创建工程前,先全局安装两个依赖包:

$ npm i -g yo
$ npm i -g generator-eslint

再执行如下命令生成 Eslint 插件工程。

$ yo eslint:plugin

这是一个交互式命令,需要你填写一些基本信息,如下

$ yo eslint:rule
? What is your name? // guming-eslint-plugin-custom-lodash
? What is the plugin ID? // 插件名 (eslint-plugin-xxx)
? Type a short description of this plugin: // 描述你的插件是干啥的
? Does this plugin contain custom ESLint rules? Yes // 是否为自定义Eslint 校验规则
? Does this plugin contain one or more processors? No // 是否需要处理器

658dfbcfce0a410285f01a2e8d8a5534~tplv-k3u1fbpfcp-zoom-in-crop-mark:4536:0:0:0.awebp

接下来我们为插件创建一条规则,执行如下命令:

$ npx yo eslint:rule

这也是一个交互式命令,如下:

? What is your name? // guming-eslint-plugin-custom-lodash
? Where will this rule be published? ESLint Plugin
? What is the rule ID? // 规则名称 lodash-auto-import
? Type a short description of this rule: // 规则的描述
? Type a short example of the code that will fail: // 这里写你这条规则校验不通过的案例代码

35904006a1ca4a8493d8b3a9d5a6000b~tplv-k3u1fbpfcp-zoom-in-crop-mark:4536:0:0:0.awebp

填写完上述信息后, 我们可以得到如下的一个项目目录结构:

guming-eslint
├─ .eslintrc.js
├─ .git
├─ README.md
├─ docs
│ └─ rules
│ └─ lodash-auto-import.md
├─ lib // 规则
│ ├─ index.js
│ └─ rules
│ └─ lodash-auto-import.js
├─ node_modules
├─ package-lock.json
├─ package.json
└─ tests // 单测
└─ lib
└─ rules
└─ lodash-auto-import.js

eslint 规则配置

Eslint 官方制定了一套开发自定义规则的规范。我们只需要根据规范配置相应的内容就可以轻松的实现我们的自定义Eslint规则。具体配置详情可见官网

相关配置的说明如下:

module.exports = {
meta: {
// 规则的类型 problem|suggestion|layout
// problem: 这条规则识别的代码可能会导致错误或让人迷惑。应该优先解决这个问题
// suggestion: 这条规则识别的代码不会导致错误,但是建议用更好的方式
// layout: 表示这条规则主要关心像空格、分号等这种问题
type: "suggestion",
// 对于自定义规则,docs字段是非必须的
docs: {
description: "描述你的规则是干啥的",
// 规则的分类,假如你把这条规则提交到eslint核心规则里,那eslint官网规则的首页会按照这个字段进行分类展示
category: "Possible Errors",
// 假如你把规则提交到eslint核心规则里
// 且像这样extends: ['eslint:recommended']继承规则的时候,这个属性是true,就会启用这条规则
recommended: true,
// 你的规则使用文档的url
url: "https://eslint.org/docs/rules/no-extra-semi",
},
// 定义提示信息文本 error-name为提示文本的名称 定义后我们可以在规则内部使用这个名称
messages: {
"error-name": "这是一个错误的命名"
},
// 标识这条规则是否可以修复,假如没有这属性,即使你在下面那个create方法里实现了fix功能,eslint也不会帮你修复
fixable: "code",
// 这里定义了这条规则需要的参数
// 比如我们是这样使用带参数的rule的时候,rules: { myRule: ['error', param1, param2....]}
// error后面的就是参数,而参数就是在这里定义的
schema: [],
},
create: function (context) {
// 这是最重要的方法,我们对代码的校验就是在这里做的
return {
// callback functions
};
},
};

本次Eslint 校验规则是推荐使用更好的lodash引用方式,所以常见规则类型 typesuggestion

AST 结构

Eslint 的本质是通过代码生成的 AST 树做代码的静态分析,我们可以使用 astexplorer 快速方便地查看解析成 AST 的结构。

我们将如下代码输入

import { omit } from 'lodash'

得到的 AST 结构如下:

{
"type": "Program",
"start": 0,
"end": 29,
"body": [
{
"type": "ImportDeclaration",
"start": 0,
"end": 29,
"specifiers": [
{
"type": "ImportSpecifier",
"start": 9,
"end": 13,
"imported": {
"type": "Identifier",
"start": 9,
"end": 13,
"name": "omit"
},
"local": {
"type": "Identifier",
"start": 9,
"end": 13,
"name": "omit"
}
}
],
"source": {
"type": "Literal",
"start": 21,
"end": 29,
"value": "lodash",
"raw": "'lodash'"
}
}
],
"sourceType": "module"
}

分析 AST 的结构,我们可以知道:

  • type 为 包的引入方式

  • source 为 资源名(依赖包名)

  • specifiers 为导出的模块

节点访问方法

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

  • VariableDeclaration,则返回声明中声明的所有变量。

  • 如果节点是一个 VariableDeclarator,则返回 declarator 中声明的所有变量。

  • 如果节点是 FunctionDeclarationFunctionExpression,除了函数参数的变量外,还返回函数名的变量。

  • 如果节点是一个 ArrowFunctionExpression,则返回参数的变量。

  • 如果节点是 ClassDeclarationClassExpression,则返回类名的变量。

  • 如果节点是一个 CatchClause 子句,则返回异常的变量。

  • 如果节点是 ImportDeclaration,则返回其所有说明符的变量。

  • 如果节点是 ImportSpecifierImportDefaultSpecifierImportNamespaceSpecifier,则返回声明的变量。

本次我们是校验资源导入规范,所以我们使用ImportDeclaration获取我们导入资源的节点结构

代码修复

report()函数返回一个特定结构的对象,它用来发布警告或错误, 我们可以通过配置对象去配置错误AST 节点,错误提示的内容(可使用 meta 配置的 meaasge 名称)以及修复方式

实例配置代码如下

context.report({
node: node,
message: "Missing semicolon",
fix: function(fixer) {
return fixer.insertTextAfter(node, ";");
}
});

编写代码

了解完上述内容,我们就可以开始愉快的编写代码了。

自定义规则代码如下:

 // lib/rules/lodash-auto-import.js

/**
* @fileoverview 这是一个lodash按需引入的eslint规则
* @author guming-eslint
*/
"use strict";

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

/** @type {import('eslint').Rule.RuleModule} */

const SOURCElIST = ["lodash", "lodash-es"];
module.exports = {
// eslint-disable-next-line eslint-plugin/prefer-message-ids
meta: {
type: "suggestion", // `problem`, `suggestion`, or `layout`
docs: {
description: "这是一个lodash按需引入的eslint规则",
recommended: true,
url: null, // URL to the documentation page for this rule
},
messages: {
autoImportLodash: "请使用lodash按需引用",
invalidImport: "lodash 导出依赖不为空",
},
fixable: "code",
schema: [],
},

create: function (context) {

// 获取lodash中导入的函数名称,并返回
function getImportSpecifierArray(specifiers) {
const incluedType = ["ImportSpecifier", "ImportDefaultSpecifier"];
return specifiers
.filter((item) => incluedType.includes(item.type))
.map((item) => {
return item.imported ? item.imported.name : item.local.name;
});
}

// 生成修复文本
function generateFixedImportText(importedList, dependencyName) {
let fixedText = "";
importedList.forEach((importName, index) => {
fixedText += `import ${importName} from "${dependencyName}/${importName}";`;
if (index != importedList.length - 1) fixedText += "\n";
});
return fixedText;
}

return {
ImportDeclaration(node) {
const source = node.source.value;
const hasUseLodash = SOURCElIST.inclues(source);

// 使用lodash
if (hasUseLodash) {
const importedList = getImportSpecifierArray(node.specifiers || []);

if (importedList.length <= 0) {
return context.report({
node,
messageId: "invalidImport",
});
}

const dependencyName = getImportDependencyName(node);
return context.report({
node,
messageId: "autoImportLodash",
fix(fixer) {
return fixer.replaceTextRange(
node.range,
generateFixedImportText(importedList, dependencyName)
);
},
});
}
},
};
},
};

配置规则组

// lib/rules/index.js

const requireIndex = require("requireindex");
// 在这里导入了我们上面写的自定义规则
const rules = requireIndex(__dirname + "/rules");
module.exports = {
// rules是必须的
rules,
// 增加configs配置
configs: {
// 配置了这个之后,就可以在其他项目中像下面这样使用了
// extends: ['plugin:guming-eslint/recommended']
recommended: {
plugins: ['guming-eslint'],
rules: {
'guming-eslint/lodash-auto-import': ['error'],
}
}
}
}

补充测试用例

// tests/lib/rules/lodash-auto-import.js
/**
* @fileoverview 这是一个lodash按需引入的eslint规则
* @author guming-eslint
*/
"use strict";

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

const rule = require("../../../lib/rules/lodash-auto-import"),
RuleTester = require("eslint").RuleTester;

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

const ruleTester = new RuleTester();
ruleTester.run("lodash-auto-import", rule, {
valid: ['import omit from "lodash/omit";', 'import { omit } from "lodash-es";'],

invalid: [
// eslint-disable-next-line eslint-plugin/consistent-output
{
code: 'import {} from "lodash";',
errors: [{ message: "invalidImport" }],
output: 'import xxx from lodash/xxx'
},
{
code: 'import {} from "lodash-es";',
errors: [{ message: "invalidImport" }],
output: 'import { xxx } from lodash-es'
},
{
code: 'import { omit } from "lodash";',
errors: [{ message: "directlyImportLodash" }],
output: 'import omit from "lodash/omit";',
},
{
code: 'import { omit as _omit } from "lodash";',
errors: [{ message: "directlyImportLodash" }],
output: 'import omit from "lodash/omit";',
},
{
code: 'import { omit, debounce } from "lodash";',
errors: [{ message: "directlyImportLodash" }],
output:
'import omit from "lodash/omit"; \n import debounce from "lodash/debounce";',
},
],
});

可输入如下指令,执行测试

$ yarn run test

注意事项

开发这个插件的一些注意事项如下

  • 多个模块导出

  • lodash 具名导出和默认导出

  • 模块别名(as)

使用

插件安装

  • npm 包发布安装调试

$ yarn add eslint-plugin-guming-eslint
  • npm link 本地调试(推荐使用) 插件项目目录执行如下指令

$ npm link

项目目录执行如下指令

$ npm link eslint-plugin-guming-eslint

项目配置

添加你的 plugin 包名(eslint-plugin- 前缀可忽略) 到 .eslintrc 配置文件的 extends 字段。

.eslintrc 配置文件示例:

module.exports = {
// 你的插件
extends: ["plugin:guming-eslint/recommended"],
parserOptions: {
ecmaVersion: 7,
sourceType: "module",
},
};

效果

0c2ff946a93d474a9191ece602bec3ce~tplv-k3u1fbpfcp-zoom-in-crop-mark:4536:0:0:0.awebp9fb3fed81e1941e8bd1305aa98922b92~tplv-k3u1fbpfcp-zoom-in-crop-mark:4536:0:0:0.awebp d6c3e01bab5e40ceabb41e985f100b62~tplv-k3u1fbpfcp-zoom-in-crop-mark:4536:0:0:0.awebp?

作者:古茗前端团队
来源:juejin.cn/post/7202413628807938108

0 个评论

要回复文章请先登录注册