本文介绍 ESLint 插件开发相关基础知识,包括eslint的基础配置、插件规则的工作原理,创建一个eslint插件项目,从开发、测试到发布的整个流程。
使用与原理
先简单介绍 ESLint 的基础使用和工作原理。
配置简介
下面是 ESLint 配置文件的常见字段:
// .eslintrc.json
{
parser : babel-eslint ,
parserOptions : {
ecmaVersion : 2018,
sourceType : module ,
},
extends : [ eslint:recommended ],
plugins : [ @byted/check-css-modules ],
rules : {
no-console : off ,
@byted/check-css-modules/no-unused-class : [2, {
markAsUsed : [ usedClasseA , usedClasseB ],
ignorePattern : ^keyframes
}],
},
settings : {
@byted/check-css-modules : {
include : **/*.module.less
}
}
// ...
}
复制代码
parser
: 解析器,默认使用 Espree。如果你使用了 Babel 或者 TypeScript,则需要配置对应的解析器完成代码解析,比如babel-eslint
、@typescript-eslint/parser
。
parserOptions
: 解析器选项。配置 ESLint 如何解析 JS 代码,ecmaVersion
表示ECMAScript 语法版本,sourceType
表示模块类型,
plugins
: 添加插件。可省略插件名称中的eslint-plugin-
前缀,但@scope
不能省略。添加插件后即可以在rules
字段配置规则。
{
plugins : [
react , // 即 eslint-plugin-react
@typescript-eslint , // 即 @typescript-eslint/eslint-plugin
@byted/check-css-modules // 即 @byted/eslint-plugin-check-css-modules
]
}
复制代码
rules
: 关闭单条规则(off
或0
)、开启规则同时设置错误级别(warn
或1
、error
或2
)、设置配置项。
-
extends
: 用于开启一系列预设的规则。这里可以添加两类预设:- 配置包。配置包用于专门导出共享的配置内容。配置
extends
时名称包含eslint-config-
可省略,比如airbnb
即为eslint-config-airbnb
。 - 插件包。插件包通常输出一系列规则,但同时还能导出一个或多个命名配置供用户选择,这些配置与配置包👆🏻的导出内容一致。配置到
extends
时格式为plugin:包名/配置名称
,包名可省略eslint-plugin-
前缀。
- 配置包。配置包用于专门导出共享的配置内容。配置
{
plugins : [ react ],
extends : [
airbnb , // 即 eslint-config-airbnb
plugin:react/recommended // 即 eslint-plugin-react 中 exports.configs.recommended
],
}
复制代码
settings
: 设置指定插件的配置选项,具体配置选项与插件定义相关。同样可省略eslint-plugin-
前缀。
AST
ESLint 默认使用 espree 解析JS代码生成AST(Abstract Syntax Tree,抽象语法树),AST explorer 可查看生成的 AST:
Rule 如何工作
ESLint 每条规则都是独立的,并可以设置规则级别:关闭off/0
、警告warn/1
或错误error/2
。
每条规则即是如下👇🏻这样一个对象定义,create
方法返回不同 AST 节点类型的访问器函数,ESLint 遍历 AST 节点时会执行同名的访问器,访问器用于检测代码是否符合规则的期望,如果不满足就可以通过 context.report()
想 ESLint 报告问题,然后 ESLint 根据规则级别对上报的问题做不同的展示。
module.exports = {
meta: {
// ...
messages: {
unexpected: Unexpected 'debugger' statement.
}
},
create(context) {
return {
DebuggerStatement(node) {
context.report({
node,
messageId: unexpected
});
}
};
}
};
复制代码
以上面 ESLint no-debugger Rule 源码为例,开启 no-debugger 表示代码中禁止出现 debugger
语句。由于 debugger
语句解析后的 AST 节点类型为 DebuggerStatement
,因此DebuggerStatement
的访问器中上报了 Unexpected 'debugger' statement. 消息。
ESLint 如何工作
ESLint 根据配置文件搜集所有已配置规则的访问器,ESLint 在遍历 AST 过程中会执行执行这些访问器,从而完成检测代码、发现问题与问题上报,并根据规则级别对上报的问题做不同的展示。
具体实现
示例插件
这里以本人的一个CSS Modules ESLint 插件为例,此插件中存在一个 no-unused-class
规则用于检查 CSS Modules 中是否存在未使用到的 classNames。它工作的基本原理为 eslint 遍历 AST Tree 过程中,插件处理const tyles fro``m 'xx.less'
语句对应的 AST Node,读到获取文件完整路径后读取对应的xx.less.d.ts
文件中 className 类型声明,从而搜集到 CSS Modules 中导出的 classNames。而遍历到 styles.a
之类的表达式的 AST node 时,将对应的 className 标记为已使用。eslint 程序退出后报告所有未使用的 classNames。
module.exports = {
plugins: ['@byted/check-css-modules'],
rules: {
// 校验是否存在未使用的className
@byted/check-css-modules/no-unused-class : 'error',
// 添加配置
@byted/check-css-modules/no-unused-class : ['error', {
markAsUsed : ['usedClasseA', 'usedClasseB'], // 对特定的className标记为已使用
}],
},
};
复制代码
项目初始化
# 安装脚手架
npm install -g yo generator-eslint
# 创建项目目录
mkdir eslint-plugin-check-css-modules
cd eslint-plugin-check-css-modules
# 生成 eslint 插件项目
yo eslint:plugin
# 安装项目依赖
npm install
# 添加规则
yo eslint:rule
复制代码
默认初始化后项目并不支持 typescript,添加 typescript 配置文件后的项目结构如下:
├── node_modules
├── dist # 编译输出目录
├── lib # 工作目录
│ ├── rules # eslint规则
│ │ ├── index.ts
│ │ ├── ...
│ │ └── no-unused-class.ts
│ ├── index.ts # 主入口
│ └── types.ts
├── tests # 测试用例目录
│ └── rules
│ └── no-unused-class.test.ts
├── package.json
├── tsconfig.json # 开发与测试时的ts配置文件
├── tsconfig.build.json # 编译时的ts配置文件
└── README.md
复制代码
相关库
estree
:包含了所有 AST 节点的 ts 类型。
ts-node
:Node 不能解析 typescript 格式写的测试用例。
json-schema
:包含了Rule.meta.schema
的 ts 类型。
anymatch
:用于校验字符串与指定的模式(正则/glob/字符串)是否匹配,常用于include/exclude
的校验。
规则实现
在 【Rule 如何工作】部分已经介绍了规则定义需要导出 meta
字段和 create
方法。
module.exports = {
meta: {
type: null,
docs: {
description: Ensures that any referenced class is exported by css files. ,
recommended: false,
url: null,
},
fixable: null,
schema: [],
},
create(context) {
// ...
return {
// visitor functions for different types of nodes
};
},
};
复制代码
meta
meta
属性包含了当前规则定义的元数据信息,下面介绍一些常用的选项,更多选项可查看这里。
-
type
: 设置规则的类型。可选值有:problem
:问题,需要优先解决;suggestion
:建议。表示可以有更好的处理方式;layout
:表示当前规则用于检测代码风格,如空格、分号等。
-
docs
:设置规则的说明信息。description
:规则作用描述。recommended
:ESLint 配置文件添加extends : eslint:recommended
是否启用当前规则。url
:规则文档访问链接。
-
fixable
:执行eslint --fix
时自动修复规则报告的问题。 -
schema
: 规则设置独立配置选项,为避免无效的规则配置,这里可设置配置选项的校验规则。下面会具体介绍👇🏻。
schema
ESLint 内部通过 JSON Schema(中文文档)来使用 schema
的配置,并校验 Rule 开启时实际传入的选项。
module.exports = {
plugins: ['@byted/check-css-modules'],
rules: {
@byted/check-css-modules/no-unused-class : [2, {
markAsUsed : ['usedClasseA', 'usedClasseB'], // 对特定的className标记为已使用
ignorePattern : '^keyframes' // 对正则匹配到特定的className进行忽略((new RegExp(ignorePattern)).test(className))
}],
},
};
复制代码
比如上面示例代码中规则 @byted/check-css-modules/no-unused-class
可以传入两个选项 markAsUsed
和 ignorePattern
。对应的 meta.s``chema
配置为:
module.exports = {
meta: {
// ...
schema: [
{
type: object ,
properties: {
markAsUsed: { type: array },
ignorePattern: { type: string },
},
additionalProperties: false, // 规则配置中仅允许存在 properties 定义的选项
}
],
},
create(context) {
return {
// visitor functions for different types of nodes
};
},
};
复制代码
additionalProperties
也是常用的配置,表示是否允许添加 properties
内以外的属性。
create
通过此方法注册节点访问器完成代码校验,若不符合期望则上报或修复问题。
context
context 对象包含与规则上下文相关的信息,下面是几个常用属性和方法(详细了解查看这里):
options
: 可获取 ESLint 所有插件或单条规则的配置选项。
以下面配置为例:
// .eslintrc.json
{
plugins : [ @byted/check-css-modules ],
rules : {
@byted/check-css-modules/no-unused-class : [2, {
markAsUsed : [ usedClasseA , usedClasseB ],
ignorePattern : ^keyframes
}],
},
settings : {
@byted/check-css-modules : {
include : **/*.module.less ,
exclude : **/node_modules/**/*
}
}
}
复制代码
// 获取规则配置
const markAsUsed = context.options[0]?.markAsUsed || [];
const ignorePattern = context.options[0]?.ignorePattern;
// 获取插件配置
const settings = context.settings[ '@byted/check-css-modules' ];
const { include, exclude } = settings || {};
复制代码
-
getFilename()
: 返回源码关联的文件名。 -
getSourceCode()
:返回一个SourceCode
对象,对象包含了一系列的属性和方法用于处理源代码(详细了解查看这里)。 -
report()
:用于上报或修复问题(详细参数可查看这里),下面是常见上报的示例👇🏻:
仅上报问题:
module.exports = {
meta: {
messages: {
avoidName: Avoid using variables named '{{ name }}' ,
},
},
create(context) {
// ...
return {
Identifier (node) {
if (node.name !== foo ) return;
// 方式1:常规上报
context.report({
node: node,
message: `Avoid using variables named '${node.name}'`,
});
// 方式2:使用占位符
context.report({
node: node,
message: Avoid using variables named '{{ name }} ,
data: { name: node.name },
});
// 方式3:使用 messageIds
context.report({
node: node,
messageId: avoidName ,
data: { name: node.name },
});
}
};
}
};
复制代码
上报并修复问题:
如果需要修复问题,需要添加fix
方法。它的参数接收一个fixer
对象,此对象包含了众多方法用于修复代码问题,比如 insertTextAfter 表示在指定定节点后插入文本👇🏻:
context.report({
node: node,
message: Missing semicolon ,
fix(fixer) {
return fixer.insertTextAfter(node, ; );
}
});
复制代码
上报并提供建议:
这种情况意味着自动修复可能更改功能或其他不可预测的问题。这里不深入介绍,详细了解可查看这里。
result
result 是 create 方法的返回的一系列访问器,访问器的名称是节点类型名称、选择器或事件名称。
create: function(context: Rule.RuleContext) {
// declare the state of the rule
return {
ReturnStatement: function(node: ESTree.Node) {
// ...
},
// at a function expression node while going up:
FunctionExpression:exit : checkLastSegment,
ArrowFunctionExpression:exit : checkLastSegment,
onCodePathStart: function (codePath, node) {
// at the start of analyzing a code path
},
onCodePathEnd: function(codePath, node) {
// at the end of analyzing a code path
}
};
}
复制代码
由于 ESLint 遍历 AST 的时候先向下遍历再向上遍历。所以
- 如果 key 是节点类型或选择器,访问器会在ESLint 向下遍历时调用。如上面示例中的
ReturnStatement
。
- 如果 key 是节点类型或选择器,且添加了
:exit
,则访问器会在ESLint 向上遍历时调用。如上面示例中的FunctionExpression:exit
和ArrowFunctionExpression:exit
。
- 如果 key 是事件名称,ESLint 会调用该访问器进行代码路径分析。如示例中的
onCodePathStart
与onCodePathEnd
。
选择器
AST 选择器用于匹配特定条件下的节点。
var foo = 1;
bar.baz();
复制代码
假设访问器 key 为Identifier(){}
将匹配到所有节点类型为Identifier
的节点。上面的示例代码中则匹配到 foo
、bar
和 baz
,如果仅仅想匹配到 foo
。则可以设置访问器的key设置为 VariableDeclarator > Identifier
,因为 var
的AST 节点类型为 VariableDeclarator
,foo
是 var
的下一个节点。
选择器的语言非常类似 CSS 选择器,更多选择器语言可查看这里。
代码路径分析
代码路径分析是对条件分支语句、循环语句等做分析。这块个人感觉不太常用到。想了解可查看官方文档 code-path-analysis 部分的内容。
测试
RuleTester
ESLint 提供了 RuleTester
工具简化规则测试。每组测试用例包含至少包含一个 有效用例和无效用例(见valid
和 invalid
),每个用例配置中可以设置运行测试的选项(如 parserOptions
和 filename
)下面是一个简单示例:
import { RuleTester } from 'eslint';
import rule from '../../lib/rules/no-unused-class';
import path from 'path';
const filename = path.resolve(__dirname, '../../files/foo.ts');
function test(config) {
return Object.assign({
parserOptions: {
sourceType: 'module',
ecmaVersion: 6,
ecmaFeatures: { jsx: true },
},
filename,
}, config);
};
const ruleTester = new RuleTester();
ruleTester.run('no-unused-class', rule, {
valid: [
test({
code: `
import s from './noUnusedClass1.module.less';
export default Foo = () => (
<div className={s.container}></div>
);
`,
}),
// 其他有效用例
],
invalid: [
test({
code: `
import s from './noUnusedClass1.module.less';
export default Foo = () => (
<div className={s.bar}></div>
);
`,
// 无效用例必须包含 errors 以预测上报的问题
errors: [
{
messageId: 'unusedClassName',
data: { classNames: 'container' },
}
]
}),
// 其他无效用例
],
});
复制代码
导出插件
ESLint 插件包需要在主入口文件规则配置。
Rules
rules 属性导出插件中的所有规则。
// lib/index.ts
const allRules = {
'no-unused-class': require('./lib/rules/no-unused-class'),
'no-undef-class': require('./lib/rules/no-undef-class'),
};
export.exports = {
rules: allRules, // 必须
configs: {/* ... */} // 非必须
}
复制代码
Configs
通常情况下配置包输出预设配置、插件包通常输出规则。如果一个插件包中的规则过多,需要一个个去添加/配置也是非常不便的事情。因此插件包也可以导出一个或多个命名配置供用户选择,使用时以 plugin:包名/配置名称
的格式(包名可省略 eslint-plugin-
)添加到 ESLint 配置文件的 extends
字段中。这里以一个规则较多的插件 eslint-plugin-react
作为示例:
使用:
// .eslintrc.json
{
plugins : [ react ],
extends : [
plugin:react/recommended // 即 eslint-plugin-react 中 exports.configs.recommended
// plugin:react/all // 即 eslint-plugin-react 中 exports.configs.all
],
}
复制代码
配置:
// lib/index.ts
const configs = {
recommended: {
recommended: {
plugins: [
'react',
],
parserOptions: {
ecmaFeatures: {
jsx: true,
},
},
rules: {
'react/display-name': 2,
'react/jsx-key': 2,
'react/jsx-no-comment-textnodes': 2,
// ...
},
},
all: {
plugins: [
'react',
],
parserOptions: {
ecmaFeatures: {
jsx: true,
},
},
rules: activeRulesConfig,
},
// 其他预设配置
},
};
export.exports = {
rules: allRules,
configs: configs
}
复制代码
打包
scripts : {
build : npm run lint && npm run test && rm -rf dist && tsc -p tsconfig.build.json ,
lint : eslint . ,
lint:fix : eslint . --fix ,
test : mocha --require ts-node/register 'tests/**/*.test.ts'
},
复制代码
发布
scripts : {
// ...
release:alpha : npm version prerelease --preid=alpha && npm publish --tag alpha --registry=http://bnpm.byted.org ,
release : npm version patch && npm publish --registry=http://bnpm.byted.org
},
复制代码
推荐阅读 维护一个 npm 包。