前面两节已经对eslint的配置,eslint源码进行了详细的解释,那接下来我们就来手写一个plugin。
该plugin包含自定义rule、自定义processor、共享配置信息。
完整的项目请看eslint-project
初始化项目。每个插件是一个命名格式为 eslint-plugin-
的 npm 模块,你也可以用这样的格式 @
限定在包作用域下。我们的插件就取名为eslint-plugin-myPlugin
mkdir myPlugin
cd myPlugin
npm init
在eslint中,插件可以暴露额外的规则以供使用。为此,插件必须输出一个rules对象,包含规则ID和对应规则的一个键值对。那我们就来自定义一个rule, id取名为replacement。rule解决的问题是将代码块中的XXX替换为Candice。
创建规则需要的属性以及每个属性对应的功能,eslint官网已经给出了详细的解释,这里我就不赘述了,不了解的同学可以移步官网。
创建自定义规则还需要掌握抽象语法树AST(Abstract syntax tree)的知识。
Eslint默认的语法解析工具为Espree,我们可以在AST explorer查看代码解析之后对应的节点类型。
创建myPlugin/rules/replacement.js
"use strict";
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
module.exports = {
// 规则的元数据
meta: {
/**
* type (string) 指示规则的类型,值为 "problem"、"suggestion" 或 "layout"
* "problem": 指的是该规则识别的代码要么会导致错误,要么可能会导致令人困惑的行为。开发人员应该优先考虑解决这个问题。
* "suggestion": 意味着规则确定了一些可以用更好的方法来完成的事情,但是如果代码没有更改,就不会发生错误。
* "layout": 意味着规则主要关心空格、分号、逗号和括号,以及程序中决定代码外观而不是执行方式的所有部分。这些规则适用于AST中没有指定的代码部分。
*/
type: "problem",
// 对 ESLint 核心规则来说是必需的
docs: {
// 展示在eslint规则首页的描述
description: "XXX 不能出现在代码中!",
// eslint规则首页的分类: Possible Errors、Best Practices、Strict Mode、Varibles、Stylistic Issues、ECMAScript 6、Deprecated、Removed
category: "Possible Errors",
// "extends": "eslint:recommended"属性是否启用该规则
recommended: false,
// 指定可以访问完整文档的URL
url: "https://github.com/Candice0718/eslint-project/tree/master/myPlugin/rules"
},
// 该规则是否可以修复
fixable: "code",
// 参数类型
schema: [
{
type: 'string'
}
],
messages: {
unexpected: '错误的字符串XXX, 需要用{{argv}}替换'
}
},
// 返回一个对象,Eslint在遍历抽象语法树AST时,用来访问节点的方法。
/**
*
*/
create: function (context) {
// 获取规则的参数
const str = context.options[0];
function checkLiteral(node) {
if (node.raw && typeof node.raw === 'string') {
if (node.raw.indexOf('XXX') !== -1) {
const result = node.raw.replace('XXX', str);
context.report({ // 发布警告或者错误
node, // 有问题的AST节点
messageId: 'unexpected', // 对应meta.messages.XXX,message可以直接用message替换
data: { // 占位数据
argv: str
},
fix: (fixer) => {
/**
* fixer 对象有一下几个方法:
*
* insertTextAfter(nodeOrToken, text) - 在给定的节点或记号之后插入文本
* insertTextAfterRange(range, text) - 在给定的范围之后插入文本
* insertTextBefore(nodeOrToken, text) - 在给定的节点或记号之前插入文本
* insertTextBeforeRange(range, text) - 在给定的范围之前插入文本
* remove(nodeOrToken) - 删除给定的节点或记号
* removeRange(range) - 删除给定范围内的文本
* replaceText(nodeOrToken, text) - 替换给定的节点或记号内的文本
* replaceTextRange(range, text) - 替换给定范围内的文本
*/
return fixer.replaceText(node, str)
}
})
}
}
}
return {
// 节点类型为字面量
/**
* 如果一个 key 是个节点类型或 selector,在 向下 遍历树时,ESLint 调用 visitor 函数
* 如果一个 key 是个节点类型或 selector,并带有 :exit,在 向上 遍历树时,ESLint 调用 visitor 函数
* 如果一个 key 是个事件名字,ESLint 为代码路径分析调用 handler 函数
*/
Literal: checkLiteral
}
}
};
myPlugin/index.js 引入replacement.js
module.exports = {
rules: {
replacement: require('./rules/replacement.js')
}
}
发布myPlugin这个项目,怎么发布npm我这里就不赘述了。我们是demo,我就本地建个软连接用于测试。怎么使用MyPlugin见在项目中如何使用myPlugin?。
yarn link
新建一个项目用于测试myPlugin。执行下面的语句,准备好一个eslint的执行环境。
mkdir eslint-project
cd eslint-project
npm init
npm install eslint -D
安装myPlugin。
yarn link "eslint-plugin-myplugin"
在根目录配置.eslintrc文件,引入myPlugin。
{
"plugins": ["myPlugin"],
"rules": {
"myPlugin/replacement": ["error", "Candice"]
}
}
我们再来建一个待检测文件/src/index.js。
eslint-project/src/index.js
function say() {
var name = 'XXX'
}
然后来执行eslint命令,看看控制台会报什么错。
npx eslint ./src/*.js
执行到这一步,我们myPlugin离成功也就差最后一步了,那我们再来试试修复功能吧。执行完下面的命令,记得去/src/index.js看看XXX是不是神奇的被替换了。
npx eslint ./src/*.js --fix
插件可以提供处理器。processor提供两个钩子函数preprocess
、postprocess
。preprocess
是在parser之前执行获取检测代码,可以对代码块进行操作。postprocess
是执行完rule之后的执行,可以获取到messageList问题信息。处理器还提供supportsAutofix` 属性控制是否修复问题,该属性优先级大于rule的元数据fixable 。
接下来我们就来手写一个processor,该处理器能够从markdown语法中提取出内联代码块,并用我们的replacement规则来检测它,并支持修复。
首先创建myPlugin/processors/index.js。
module.exports = {
// 待校验代码块的起始位置为了在postprocess恢复原始代码块的位置
forward: 0,
/**
* preprocess 在parser之前执行
* @param {*} text 原代码字符串
* @param {*} filename 文件名
*/
preprocess: (text, filename) => {
const reg = /`{3}(([^`][\s\S])*)`{3}/g;
if (text.match(reg)) {
this.forward = text.indexOf(RegExp.$1);
// 提取待校验的代码块
return [RegExp.$1];
}
},
/**
* postprocess 执行完rules之后
* @param {*} messages 问题节点信息列表
* @param {*} filename 文件名
*/
postprocess: (messages, filename) => {
return messages[0].map((problem) => {
return {
...problem,
fix: problem.fix ? {
// rule里提供的修复属性
...problem.fix,
// *重要:range必须调整错误的位置,使其与原始的未处理的代码中的位置相对应
range: [problem.fix.range[0] + this.forward, problem.fix.range[1] + + this.forward]
}: problem.fix
};
});
},
supportsAutofix: true // 是否提供自动修复功能
}
myPlugin/index.js 导出processor。plugin导出processor有两种方式。
在plugin导出文件中直接写校验的后缀名,那在项目中只要引入plugin就可以使用该处理器。
module.exports = {
processors: {
'.md': require('./processors/index.js')
},
rules: {
replacement: require('./rules/replacement.js')
}
}
项目中.eslintrc文件,引入myPlugin。
{
"plugins": ["myPlugin"],
"rules": {
"myPlugin/replacement": ["error", "Candice"]
}
}
在plugin导出文件中只约定processor的名称,那么在项目中引入plugin还需要定义processor
或者overrides
,二选一即可。
module.exports = {
processors: {
myProcessor: require('./processors/index.js')
},
rules: {
replacement: require('./rules/replacement.js')
}
}
项目中.eslintrc文件,引入myPlugin。
{
"plugins": [
"myPlugin"
],
// "processor": "myPlugin/myProcessor",
"overrides": [
{
"files": [
"*.md"
],
"processor": "myPlugin/myProcessor"
}
],
"rules": {
"myPlugin/replacement": [
"error",
"'Candice'"
]
}
}
我们来建一个待检测文件/src/index.md。
```
function say() {
var name = 'XXX'
}
```
然后来执行eslint命令,来检测.md文件。看看控制台会报什么错。是不是很熟悉?我们replacement rule起作用了。
npx eslint ./src/*.md
再来执行–fix,看看我们.md是不是也完美的修复啦!
npx eslint ./src/*.md --fix
配置信息的 .eslintrc
文件是你的项目中重要的部分,正因为这样,你可能想要将你的配置信息分享给其他项目或人。
我们有两种方式来共享配置:
我们就在我们的plugin中写一个共享配置吧!
首先创建myPlugin/configs/index.js。
module.exports = {
"root": true,
// 共享配置还可以继承其他配置
extends: "eslint:recommended",
"rules": {
"quotes": ["error", "single"]
}
}
在myPlugin/index.js中导出共享配置。
module.exports = {
configs: {
"recommended": require('./configs/index.js')
}
}
项目中.eslintrc文件,引入myPlugin。
{
"plugins": [
"myPlugin"
],
"extends": "plugin:myPlugin/recommended",
}