自定义 Eslint 开发
pfan123 opened this issue · 0 comments
对于前端开发者来说,ESLint 是比较常用的代码规范和错误检查工具,ESLint 非常强大不仅提供了大量的常用 rules,还有许多实用的 ESLint 插件可以满足各样需求。但随着项目不断迭代发展,可能会遇到已有 ESLint 插件不能满足现在团队开发的情况。那么这时候,我们就需要自定义 Eslint 开发 ESLint Shareable Config
、ESLint Plugins
。
- ESLint Shareable Config,可分享的扩展配置(
eslint-config-<config-name>
)。 - ESLint Plugins,插件(
eslint-plugin-<plugin-name>
) 。
ESLint Shareable Config 开发
可分享的扩展配置(eslint-config-<config-name>
) 是一个 ESLint 配置对象 npm 包,模块名称以 eslint-config-<config-name>
、@<scope>/eslint-config-<config-name>
命名,创建比较简单导出配置规则即可。
创建扩展配置
创建扩展配置非常简单,创建一个新的 index.js
文件并 export 一个包含配置的对象即可:
module.exports = {
globals: {
MyGlobal: true
},
rules: {
semi: [2, "always"]
}
}
更多配置字段,参考 Configuring ESLint
使用扩展配置
npm 发布扩展包,引入 ESLint 配置:
module.exports = {
// extends: ['antife', 'myconfig'],
extends: ['eslint-config-antife', 'eslint-config-myconfig'],
globals: {
'EVENT': true,
'PAGE': true,
'SCENE': true,
'AlipayJSBridge': true,
},
plugins: [
'babel',
// 'html', // eslint-plugin-html 从 <script> 标记中提取内容,eslint-plugin-vue 需要 <script> 标记和<template> 标记,两者同时存在会冲突
'vue',
]
}
Eslint plugin 开发
插件(eslint-plugin-<plugin-name>
) 是一个命名格式为 eslint-plugin-<plugin-name>
的 npm 包,模块名称以 eslint-plugin-<plugin-name>
、@<scope>/eslint-plugin-<plugin-name>
命名。
Eslint plugin 目录
我们可以利用 yeoman 和 generator-eslint 来构建插件的目录结构进行开发,这里我们选用自定义目录,如下:
├── README.md
├── _tests__
├── docs
├── index.js
└── rules
└── my-rule.js
插件主入口组成部分
-
Rules - 插件必须输出一个
rules
对象,包含规则 ID 和对应规则的一个键值对。 -
Environments - 插件可以暴露额外的环境以在 ESLint 中使用。
-
Processors - 定义插件如何处理校验的文件。
-
Configs - 可以通过配置指定插件打包、编译方式,还可提供多种风格校验配置。
module.exports = {
rules: {
"my-rules": {
create: function (context) {
// rule implementation ...
}
}
},
env: {
jquery: {
globals: {
$: false
}
}
},
configs: {
myConfig: {
parser: require.resolve('vue-eslint-parser'),
parserOptions: {
ecmaVersion: 2018,
sourceType: 'module',
ecmaFeatures: {
jsx: true
}
},
plugins: ["myPlugin"],
env: ["browser"],
rules: {
"myPlugin/my-rule": "error",
}
},
myOtherConfig: {
plugins: ["myPlugin"],
env: ["node"],
rules: {
"myPlugin/my-rule": "off",
}
}
},
processors: {
'.vue': {
// takes text of the file and filename
preprocess: function(text, filename) {
// here, you can strip out any non-JS content
// and split into multiple strings to lint
return [string]; // return an array of strings to lint
},
// takes a Message[][] and filename
postprocess: function(messages, filename) {
// `messages` argument contains two-dimensional array of Message objects
// where each top-level array item contains array of lint messages related
// to the text that was returned in array from preprocess() method
// you need to return a one-dimensional array of the messages you want to keep
return messages[0];
},
supportsAutofix: true // (optional, defaults to false)
}
}
}
Rules 创建
在开始编写新规则之前,请阅读官方的 ESLint指南,了解下 ESLint 的特点:
- ESLint 使用 Espree 进行JavaScript解析。
- ESLint 使用 AST 评估校验代码。
- ESLint 是完全可插入的,每个规则都可以是一个插件。
- ESLint 每条规则相互独立,可以设置禁用
off
、警告warn
⚠️ 和报错error
❌,当然还有正常通过不用给任何提示。
我们可以通过使用 astexplorer.net, 去了解 ESLint 如何使用 AST 评估校验代码,astexplorer.net 非常强大,还支持 Vue 模板。
规则组成部分
-
meta
对象包含规则的元数据 -
create
函数返回 ESLint 调用方法对象,通过该方法访问 JavaScript 代码的抽象语法树(由ESTree定义的AST)节点 -
context
对象包含与规则上下文相关的信息-
属性:
-
方法:
-
getAncestors()
- 返回当前遍历的节点的祖先数组,从AST的根部开始,一直到当前节点的直接父级 -
getCwd()
- 将cwd
传递的内容返回给Linter, 为当前工作目录 -
getDeclaredVariables
- 返回给定节点声明的 -
getFilename()
- 返回与源关联的文件名 -
getScope()
- 返回当前遍历的节点的 scope ,用于跟踪对变量的引用 -
getSourceCode()
- 返回一个SourceCode
对象,可以使用该对象来处理传递给ESLint的源 -
markVariableAsUsed(name)
- 在当前作用域中使用给定名称标记变量 -
report(descriptor)
- 报告代码中的问题
-
-
"use strict";
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
module.exports = {
meta: {
type: "suggestion",
docs: {
description: "disallow unnecessary semicolons",
category: "Possible Errors",
recommended: true,
url: "https://eslint.org/docs/rules/no-extra-semi"
},
fixable: "code",
schema: [] // no options
},
create: function(context) {
context.getScope()
context.report()
return {
Identifier(node) {
if (node.name === "foo") {
context.report({
node,
messageId: "avoidName",
data: {
name: "foo",
}
})
}
},
ExportDefaultDeclaration(node){
context.report({
node,
message: "test",
})
}
}
}
}
若我们需要校验 Vue 模板,这里要注意由于 Vue 中的单个文件组件不是普通的 JavaScript,因此无法使用默认解析器,因此引入了新的解析器 vue-eslint-parser。
要了解更多 vue AST 知识,可以查看
自定义 Processors
ESLint 插件开发,支持自定义处理器来处理 JavaScript 之外的文件,自定义处理器含有两个过程:preprocess
和postprocess
。自定义处理器大体结构如下:
module.exports = {
processors: {
// assign to the file extension you want (.js, .jsx, .html, etc.)
".ext": {
// takes text of the file and filename
preprocess: function(text, filename) {
// here, you can strip out any non-JS content
// and split into multiple strings to lint
return [string]; // return an array of strings to lint
},
// takes a Message[][] and filename
postprocess: function(messages, filename) {
// `messages` argument contains two-dimensional array of Message objects
// where each top-level array item contains array of lint messages related
// to the text that was returned in array from preprocess() method
// you need to return a one-dimensional array of the messages you want to keep
return messages[0];
},
supportsAutofix: true // (optional, defaults to false)
}
}
};
插件测试
ESLint 提供了 RuleTester
实用工具可以轻松地测试你插件中的规则,在 peerDependency
指向 ESLint 0.8.0
或之后的版本。
{
"peerDependencies": {
"eslint": ">=0.8.0"
}
}
peerDependencies 目的是提示宿主环境去安装满足插件peerDependencies所指定依赖的包,然后在插件import或者require所依赖的包的时候,永远都是引用宿主环境统一安装的npm包,最终解决插件与所依赖包不一致的问题。
// in the file to lint:
var foo = 2;
// ^ error: Avoid using variables named 'foo'
// In your tests:
var rule = require("../rules/no-avoid-name")
var RuleTester = require("eslint").RuleTester
var ruleTester = new RuleTester()
ruleTester.run("no-avoid-name", rule, {
valid: ["bar", "baz"], // right data
invalid: [ // error data
{
code: "foo",
errors: [
{
messageId: "avoidName"
}
]
}
]
})
实践开发 Vue 模板 Eslint plugin
在开发之前,这里要注意由于 Vue 中的单个文件组件并不是普通的 JavaScript,导致无法使用默认解析器,因此引入了新的解析器 vue-eslint-parser。
{
"parser": "vue-eslint-parser",
"parserOptions": {
"parser": "babel-eslint",
"sourceType": "module",
"allowImportExportEverywhere": false
}
}
开发 vue eslint 规则
这里要注意,涉及到自定义的解析器的,需要使用context.parserServices
访问该解析器解析的抽象语法树内容。vue-eslint-parser
提供了以下三个处理api 方法:
// 处理 template 语法树遍历方法
// Define handlers to traverse the template body
context.parserServices.defineTemplateBodyVisitor()
// 获取缓存的 template 语法树结构
context.parserServices.getTemplateBodyTokenStore()
// 获取根结点 document fragment.
context.parserServices.getDocumentFragment()
- Vue 插件规则,示例
module.exports = {
meta: {
docs: {
description: 'disallow unnecessary `v-bind` directives',
url: 'https://eslint.vuejs.org/rules/no-useless-v-bind.html'
},
fixable: 'code',
type: 'suggestion'
},
create(context) {
if (context.parserServices.defineTemplateBodyVisitor == null) {
context.report({
loc: { line: 1, column: 0 },
message:
'Use the latest vue-eslint-parser. See also https://eslint.vuejs.org/user-guide/#what-is-the-use-the-latest-vue-eslint-parser-error'
})
return {}
}
return context.parserServices.defineTemplateBodyVisitor({
VElement(node){
if(node.name === "template"){
context.report({
node,
message: "template标签",
})
}
},
Identifier(node){
console.error('Identifier.name', node.name)
}
})
}
}
若是校验 js,无需通过 context.parserServices.defineTemplateBodyVisitor 获取语法树信息
- Vue 插件入口文件示例:
module.exports = {
configs: {
base: {
parser: require.resolve('vue-eslint-parser'),
plugins: ['boilerplate'],
rules: {
'boilerplate/no-avoid-name': 'error',
'boilerplate/no-useless-v-bind': 'error'
}
}
},
env: {
browser: true,
es6: true
},
rules: {
'no-avoid-name': require('./rules/no-avoid-name'),
'no-useless-v-bind': require('./rules/no-useless-v-bind'),
}
}
- 配置使用
module.exports = {
parser: 'vue-eslint-parser',
parserOptions: {
parser: 'babel-eslint',
ecmaVersion: 2018,
sourceType: 'module'
},
// vue 插件要放在 extend 前面,防止出现覆盖,"eslint:recommended" 是默认推荐的规则
extends: ['plugin:vue/recommended', 'plugin:boilerplate/base', "eslint:recommended"],
plugins: [
'babel',
],
}
了解更多 vue AST 知识,可以查看
实践开发 TypeScirpt 模板 Eslint plugin
2019 年 1 月,TypeScirpt 官方决定全面采用 ESLint 作为代码检查的工具,并创建了一个新项目 typescript-eslint,提供了 TypeScript 文件的解析器 @typescript-eslint/parser 和相关的配置选项 @typescript-eslint/eslint-plugin 等。之前的两个 lint 解决方案已弃用:
- typescript-eslint-parser 已停止维护
- TSLint 从2019年起 TSLint 已被弃
我们在开发 TypeScript Eslint,也有很多生态工具,帮助我们快速上手:
TypeScript ESTree —— 将 TypeScript 源代码转换为 ESTree 兼容形式的解析器。
Utils for ESLint Plugins —— TypeScript + ESLint的实用工具。
Name | Description |
---|---|
ASTUtils | 操作 ESTree AST 的工具 |
ESLintUtils | 使用 TypeScript 创建 ESLint 规则的工具 |
JSONSchema | 引入 @types/json-schema 工具,json 形式定义配置 |
TSESLint | TS 的 ESLint 的类型 |
TSESLintScope | 依托 eslint-scope,创建的 TSESLintScope |
TSESTree | @typescript-eslint/typescript-estree 解析出的 TSESTree |
AST_NODE_TYPES | TSESTree 提供的 node type |
AST_TOKEN_TYPES | TSESTree 提供的 token type |
ParserServices | typescript 解析器使用的是 @typescript-eslint/typescript-estreeimport { ESLintUtils, getParserServices } from '@typescript-eslint/experimental-utils';import * as tsutils from 'tsutils';export default ESLintUtils.RuleCreator( name => https://github.com/typescript-eslint/typescript-eslint/blob/v${version}/packages/eslint-plugin/docs/rules/${name}.md ,)({ name: 'await-thenable', meta: { docs: { description: 'Disallows awaiting a value that is not a Thenable', category: 'Best Practices', recommended: 'error', requiresTypeChecking: true, }, messages: { await: 'Unexpected await of a non-Promise (non-"Thenable") value.', }, schema: [], type: 'problem', }, defaultOptions: [], create(context) { const parserServices = getParserServices(context); const checker = parserServices.program.getTypeChecker(); return { AwaitExpression(node): void { const originalNode = parserServices.esTreeNodeToTSNodeMap.get(node); const type = checker.getTypeAtLocation(originalNode.expression); if ( !util.isTypeAnyType(type) && !util.isTypeUnknownType(type) && !tsutils.isThenableType(checker, originalNode.expression, type) ) { context.report({ messageId: 'await', node, }); } }, }; },});module.exports = { configs: { base: { "parser": "@typescript-eslint/parser", "parserOptions": { "sourceType": "module", "allowImportExportEverywhere": false }, plugins: ['@typescript-eslint'], rules: { '@typescript-eslint/adjacent-overload-signatures': 'error', '@typescript-eslint/array-type': 'error', '@typescript-eslint/await-thenable': 'error', } } }, env: { browser: true, es6: true }, rules: { '@typescript-eslint/adjacent-overload-signatures': 'error', '@typescript-eslint/array-type': 'error', '@typescript-eslint/await-thenable': 'error', } }Other Resource扩展eslint-plugin-boilerplate |
可以通过使用 astexplorer.net,选择 @typescript-eslint/parser
解析器,去了解 ESLint 如何使用 评估校验代码 TypeScript 代码,从而方便我们快速开发 rules。
微软 TypeScript 团队开始正式支持通过 Babel 进行的 TypeScript 解析,
@typescript-eslint/parser
解析器后面会逐步被 AST 与 Babel 解析器生成的AST进行替代。目前市面上挺多 TypeScript AST 解析的,如 ts-ast-viewer
- TypeScript 插件规则,示例
import { ESLintUtils, getParserServices } from '@typescript-eslint/experimental-utils';
import * as tsutils from 'tsutils';
export default ESLintUtils.RuleCreator(
name =>
`https://github.com/typescript-eslint/typescript-eslint/blob/v${version}/packages/eslint-plugin/docs/rules/${name}.md`,
)({
name: 'await-thenable',
meta: {
docs: {
description: 'Disallows awaiting a value that is not a Thenable',
category: 'Best Practices',
recommended: 'error',
requiresTypeChecking: true,
},
messages: {
await: 'Unexpected `await` of a non-Promise (non-"Thenable") value.',
},
schema: [],
type: 'problem',
},
defaultOptions: [],
create(context) {
const parserServices = getParserServices(context);
const checker = parserServices.program.getTypeChecker();
return {
AwaitExpression(node): void {
const originalNode = parserServices.esTreeNodeToTSNodeMap.get(node);
const type = checker.getTypeAtLocation(originalNode.expression);
if (
!util.isTypeAnyType(type) &&
!util.isTypeUnknownType(type) &&
!tsutils.isThenableType(checker, originalNode.expression, type)
) {
context.report({
messageId: 'await',
node,
});
}
},
};
},
});
- TypeScript 插件入口文件示例:
module.exports = {
configs: {
base: {
"parser": "@typescript-eslint/parser",
"parserOptions": {
"sourceType": "module",
"allowImportExportEverywhere": false
},
plugins: ['@typescript-eslint'],
rules: {
'@typescript-eslint/adjacent-overload-signatures': 'error',
'@typescript-eslint/array-type': 'error',
'@typescript-eslint/await-thenable': 'error',
}
}
},
env: {
browser: true,
es6: true
},
rules: {
'@typescript-eslint/adjacent-overload-signatures': 'error',
'@typescript-eslint/array-type': 'error',
'@typescript-eslint/await-thenable': 'error',
}
}
eslint-plugin-boilerplate
eslint-plugin-boilerplate —— 快速开发 eslint plugins 模版样例。