ESLint 是一个用于识别和报告在 ECMAScript/JavaScript 代码中发现的模式的工具,其目标是使代码更加一致并避免错误。
这篇文章是简单介绍一下Eslint的插件和规则是如何开发的,可以帮助我们更深的理解Eslint的运行机制
ESLint 是完全插件化的。每条规则都是一个插件,你可以在运行时添加更多。你还可以添加社区插件、配置来扩展 ESLint 的功能
本篇文档会带大家模拟一个Eslint中自带的比较常用的 eqeqeq 的这个规则,不熟悉的同学可以点击此处查看规则此规则如何使用 eqeqeq - ESLint 中文网
创建项目
我们这边使用generator-eslint来生成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"
}
}