AST
全称 Abstract Syntax Tree
,翻译为抽象语法树,是前端工程化绕不开的一个名词,目前前端常用的一些插件或者工具,比如说javascript
转译、代码压缩、css
预处理器、eslint
、pretiier
等功能的实现,都是建立在 AST
的基础之上,webpack
、eslint
等很多工具库的核心都是通过抽象语法书这个概念来实现对代码的检查、分析等操作。本文将会介绍 AST
的概念、原理以及用途,并通过开发一个 ESLint
插件来加深对 AST
概念的理解。
基本概念
抽象语法树(Abstract Syntax Tree,AST),或简称语法树(Syntax tree),是源代码语法结构的一种抽象表示。它以树状的形式表现编程语言的语法结构,树上的每个节点都表示源代码中的一种结构。
我们先看一下如下代码
import React from 'react';
再看一下抽象语法树转换后的代码
{
"type": "Program",
"start": 0,
"end": 207,
"body": [
{
"type": "ImportDeclaration",
"start": 179,
"end": 205,
"specifiers": [
{
"type": "ImportDefaultSpecifier",
"start": 186,
"end": 191,
"local": {
"type": "Identifier",
"start": 186,
"end": 191,
"name": "React"
}
}
],
"source": {
"type": "Literal",
"start": 197,
"end": 204,
"value": "react",
"raw": "'react'"
}
}
],
"sourceType": "module"
}
AST
的本质就是一个树形结构的对象,AST
是对 JS
源码的抽象,字面量、标识符、表达式、语句、模块语法、class 语法 都有各自的抽象语法树节点(Node)。一个 AST 可以由单一的节点或是成百上千个节点构成,它们组合在一起可以描述用于静态分析的程序语法。
上例中,在最外层的 type
、start
、end
、body
、sourceType
中,我们主要看中间 body 部分,这里的 body
的第一个对象内容就是对应import语句,这是一个 type
为 ImportDeclaration
类型的对象,每个对象都有 type
、start
、end
、这几个字段。
- 其中 type 表达当前块的类型,比如
ImportDefaultSpecifier
表示声明语句,FunctionDeclaration
表示函数定义,Identifier
表示标识符、BlockStatement
表示块语句、ReturnStatement
表示返回语句等。 start
表示该块开始的位置,end
表示该块结束的位置。specifiers
是一个数组,描述的导入的具体的成员变量,source
字段标识引入的库信息,其中value
值为引入的变量名
使用 astexplorer 可以在线将任意对象,表达式转换为 AST 语法树。
AST的用途
- 常用各类转义、编译的插件中。比如最典型的
ES6
转换为ES5
工具 、JSX
语法转换为JavaScript
语法,即babel
模块。 - 代码语法的检查,比如代码规范工具
ESLint
模块。 - 各类
JS/CSS/HTML
压缩工具。 - 语法检查、代码风格检查、格式化代码、语法高亮、错误提示、自动补全等
比如说,有个函数 function a() {}
我想把它变成 function b() {}
,或者在 webpack 中代码编译完成后 require('a') --> __webapck__require__("*/**/a.js")
,这些都是 AST
的用途。
将代码转换成AST
一个对象生成 AST
的关键所在是词法分析和语法分析。对于JavaScript
而言,可以通过JS Parser
将JS
代码转换成AST
,目前比较常见的JS Parser
如下:
- esprima(流行库)
- Babylon(babel中使用)
- acorn(webpack中使用)
- espree(在acorn基础上衍生而来,eslint中使用)
- astexplorer(在线生成工具,可选不同的JS Parser实时查看)
词法分析
JavaScript
编译执行流程:js
执行的第一步是读取js
文件中的字符流,然后通过词法分析生成token
,之后再通过语法分析生成AST(Abstract Syntax Tree)
,最后生成机器码执行。
词法分析指的是将对象逐个扫描(scanner),调用 next()
方法,一个字母一个字母的来读取字符,然后与定义好的 JavaScript
关键字符做比较,生成对应的 Token
。所谓 Token
是最小的不可分割单元,像是下例中的 const
就已经无法被分割了,它就是一个 Token
。类似 const
这样,每个关键字、标识符、操作符、标点符号等都是一个 Token
,词法分析器会过滤掉源程序中的注释和空白字符(换行符、空格、制表符等)。最终词法分析会生成由对象组成的一维数组(Tokens列表)。
const a = 5;
//词法分析
[
{ value:'const', type:'keyword' },
{ value:'a', type:'identifier' }
...
]
每一个 type 有一组属性来描述该令牌:
{
type: {
label: 'name',
keyword: undefined,
beforeExpr: false,
startsExpr: true,
rightAssociative: false,
isLoop: false,
isAssign: false,
prefix: false,
postfix: false,
binop: null,
updateContext: null
},
...
}
语法分析
语法分析指的是将有关联的对象整合成树形结构的表达形式。语法分析会将词法分析出来的 Token 转化成有语法含义的抽象语法树结构。同时,验证语法,语法如果有错的话,抛出语法错误。
const a = 5;
//语法分析
{
"type": "Program",
"start": 0,
"end": 12,
"body": [
{
"type": "VariableDeclaration",
"start": 0,
"end": 12,
"declarations": [
{
"type": "VariableDeclarator",
"start": 6,
"end": 11,
"id": {
"type": "Identifier",
"start": 6,
"end": 7,
"name": "a"
},
"init": {
"type": "Literal",
"start": 10,
"end": 11,
"value": 5,
"raw": "5"
}
}
],
"kind": "const"
}
],
"sourceType": "module"
}
实践转换过程
在这一章节,我们站在巨人的肩膀上,使用esprima完成从 let a = 1;
到 let b = 2;
的过程。
第一步,我们使用 astexplorer 观察一下 let a = 1;
对应的AST:
{
"type": "Program",
"start": 0,
"end": 190,
"body": [
{
"type": "VariableDeclaration",
"start": 179,
"end": 189,
"declarations": [
{
"type": "VariableDeclarator",
"start": 183,
"end": 188,
"id": {
"type": "Identifier",
"start": 183,
"end": 184,
"name": "a"
},
"init": {
"type": "Literal",
"start": 187,
"end": 188,
"value": 1,
"raw": "1"
}
}
],
"kind": "let"
}
],
"sourceType": "module"
}
第二步,我们使用 astexplorer 观察一下 let b = 2;
对应的AST:
{
"type": "Program",
"start": 0,
"end": 190,
"body": [
{
"type": "VariableDeclaration",
"start": 179,
"end": 189,
"declarations": [
{
"type": "VariableDeclarator",
"start": 183,
"end": 188,
"id": {
"type": "Identifier",
"start": 183,
"end": 184,
"name": "b"
},
"init": {
"type": "Literal",
"start": 187,
"end": 188,
"value": 2,
"raw": "2"
}
}
],
"kind": "let"
}
],
"sourceType": "module"
}
第三步,对比一下发现前后不同在于type
为Identifier
的id
的name
属性值不一样,以及type
为Literal
的init
的value
属性值不一样。接下来,我们需要安装estraverse(遍历AST)和escodegen(根据AST生成JS)这两个包,遍历AST树并修改 Node
属性,便可以完成转换过程。
const esprima = require('esprima');
const estraverse = require('estraverse');
const escodegen = require('escodegen');
const program = "let a = 1;";
const ASTree = esprima.parseScript(program);
function changeAToB(node) {
if (node.type === 'Identifier') {
node.name = 'b';
}
if (node.type === 'Literal') {
node.value = 2
}
}
estraverse.traverse(ASTree, {
enter(node) {
changeAToB(node);
}
});
const ASTreeAfterChange = escodegen.generate(ASTree);
console.log(ASTreeAfterChange); // let b = 2;
常见的AST节点
Identifier
interface Identifier <: Expression, Pattern {
type: "Identifier";
name: string;
}
标识符,就是我们写 JS 时自定义的名称,如变量名,函数名,属性名,都归为标识符,值存放于字段name
中。
ImportSpecifier
interface ImportSpecifier <: ModuleSpecifier {
type: "ImportSpecifier";
imported: Identifier;
}
单独导出的成员变量,描述的是例如 import { Message as MyMessage } from 'element-ui'
中的Message。 主要关注的是local
和imported
字段,其都是 identitifier
类型,其 name
属性在没有进行别名处理时相同,如果使用了别名则 local
中的 name
则为别名,imported
为原始成员变量名。
// import { Message as MyMessage } from 'element-ui'
{
"body": [
{
"specifiers": [
{
"imported": {
"type": "Identifier",
"name": "Message" // imported 为原始成员变量名。
},
"local": {
"type": "Identifier",
"name": "MyMessage" // 使用了别名则 `local` 中的 `name` 则为别名
}
}
],
}
],
}
ImportDefaultSpecifier
interface ImportDefaultSpecifier <: ModuleSpecifier {
type: "ImportDefaultSpecifier";
}
描述的是形如import _ from "lodash"
中的_
// import _ from "lodash"
{
"body": [
{
"type": "ImportDeclaration",
"specifiers": [
{
"type": "ImportDefaultSpecifier", // type是ImportDefaultSpecifier
"local": {
"type": "Identifier",
"name": "_" // 描述"_"
}
}
],
}
],
}
CallExpression
interface CallExpression <: Expression {
type: "CallExpression";
callee: Expression | Super | Import;
arguments: [ Expression | SpreadElement ];
}
函数调用表达式,比如:setTimeout(()=>{})
,callee
属性是一个表达式节点,表示函数,arguments
是一个数组,元素是表达式节点,表示函数参数列表。
// setTimeout(()=>{})
{
"type": "Program",
"body": [
{
"type": "ExpressionStatement",
"expression": {
"type": "CallExpression",
"callee": { // 一个表达式节点,表示函数
"type": "Identifier",
"name": "setTimeout"
},
"arguments": [ // 数组,元素是表达式节点,表示函数参数列表
{
"type": "ArrowFunctionExpression",
"params": [],
"body": {
"type": "BlockStatement",
"body": []
}
}
],
"optional": false
}
}
],
}
MemberExpression
interface MemberExpression <: Expression, Pattern {
type: "MemberExpression";
object: Expression | Super;
property: Expression;
computed: boolean;
}
成员表达式节点,即表示引用对象成员的语句,object
是引用对象的表达式节点,property
是表示属性名称,computed 如果为 false,是表示 . 来引用成员,property 应该为一个 Identifier 节点,如果 computed 属性为 true,则是 [] 来进行引用,即 property 是一个 Expression 节点,名称是表达式的结果值
// window.setTimeout
{
"type": "Program",
"body": [
{
"type": "ExpressionStatement",
"expression": {
"type": "MemberExpression",
"object": { // 引用对象的表达式节点
"type": "Identifier",
"name": "window"
},
"property": { // 表示属性名称
"type": "Identifier",
"name": "setTimeout"
},
"computed": false, // computed如果为 false,是表示 . 来引用成员,property 应该为一个 Identifier 节点,如果 computed 属性为 true,则是 [] 来进行引用,即 property 是一个 Expression 节点,名称是表达式的结果值
"optional": false
}
}
],
}
AssignmentExpression
interface AssignmentExpression <: Expression {
type: "AssignmentExpression";
operator: AssignmentOperator;
left: Pattern | Expression;
right: Expression;
}
赋值表达式节点,operator
属性表示一个赋值运算符,left
和 right
是赋值运算符左右的表达式。
// let a; a = 20;
{
"type": "Program",
"body": [
{
"type": "VariableDeclaration",
"declarations": [
{
"type": "VariableDeclarator",
"id": {
"type": "Identifier",
"name": "a"
},
"init": null
}
],
"kind": "let"
},
{
"type": "ExpressionStatement",
"expression": {
"type": "AssignmentExpression",
"operator": "=",
"left": {
"type": "Identifier",
"name": "a"
},
"right": {
"type": "Literal",
"value": 20,
"raw": "20"
}
}
}
],
}
ArrayExpression
interface ArrayExpression <: Expression {
type: "ArrayExpression";
elements: [ Expression | SpreadElement | null ];
}
数组表达式节点,const array = [d,2,3]
, elements
属性是一个数组,表示数组的多个元素,每一个元素都是一个表达式节点。
// const array = [d,2,3]
{
"type": "Program",
"body": [
{
"type": "VariableDeclaration",
"declarations": [
{
"type": "VariableDeclarator",
"id": {
"type": "Identifier",
"name": "array"
},
"init": {
"type": "ArrayExpression",
"elements": [ // 表示数组的多个元素,每一个元素都是一个表达式节点
{
"type": "Identifier",
"name": "d"
},
{
"type": "Literal",
"value": 2,
"raw": "2"
},
{
"type": "Literal",
"value": 3,
"raw": "3"
}
]
}
}
],
"kind": "const"
}
],
}
VariableDeclaration
interface VariableDeclaration <: Declaration {
type: "VariableDeclaration";
declarations: [ VariableDeclarator ];
kind: "var" | "let" | "const";
}
变量声明表达式,kind
属性表示是什么类型的声明,值可能是var/const/let
。declarations
表示声明的多个描述,因为我们可以这样:let a = 1, b = 2
// let a = 1, b = 2
{
"type": "Program",
"body": [
{
"type": "VariableDeclaration",
"declarations": [
{
"type": "VariableDeclarator",
},
{
"type": "VariableDeclarator",
}
],
"kind": "let"
}
],
"sourceType": "module"
}
LogicalExpression
interface LogicalExpression <: Expression {
type: "LogicalExpression";
operator: LogicalOperator;
left: Expression;
right: Expression;
}
逻辑运算表达式, ||
或者 &&
。
ConditionalExpression
interface ConditionalExpression <: Expression {
type: "ConditionalExpression";
test: Expression;
alternate: Expression;
consequent: Expression;
}
条件表达式即三元运算表达式:boolean ? true : false
。
IfStatement
interface IfStatement <: Statement {
type: "IfStatement";
test: Expression;
consequent: Statement;
alternate: Statement | null;
}
if
表达式,if(true)
,test
属性表示 if (...)
括号中的表达式。 consequent
属性是表示条件为 true 时的执行语句,通常会是一个块语句。 alternate
属性则是用来表示 else 后跟随的语句节点,通常也会是块语句,但也可以又是一个 if
语句节点,即类似这样的结构:if (a) { ... } else if (b) { ... }
。alternate
当然也可以为 null
。
ESLint简要介绍
ESLint
由 JavaScript
红宝书 作者 Nicholas C. Zakas 编写, 2013 年发布第一个版本。ESLint
是以可扩展、每条规则独立、不内置编码风格为理念编写的一个 lint 工具。
ESLint 主要有以下特点:
- 默认规则包含所有 JSLint、JSHint 中存在的规则,易迁移;
- 规则可配置性高:可设置「警告」、「错误」两个 error 等级,或者直接禁用;
- 包含代码风格检测的规则
- 支持插件扩展、自定义规则
ESLint
详尽使用参见 官方文档 。
ESLint
的核心在于约束,通过各种规则 rule
进行约束。JS是一个随心所欲的动态语言,我们需要各类规则约束代码,使其可读性更高,代码更健壮,工程更可靠。规则的本质就是前人的经验总结,将经验抽象为规则使团队成员共享最佳实践。
ESLint基本配置
我们通常在 eslintrc
文件中配置 eslint
,其中包括以下这些配置项。
parser
功能:将代码转换为 eslint 能理解的 AST 语法树,支持我们对非标准 JS
语法添加 Lint
。
parser
是解析器,其功能对应编译原理中的词法分析、语法分析。如果需要其他功能,则可以使用额外的 parse 配置,如解析 TS
代码时使用 typescript-eslint/parser
,使用了 babel
时使用 @babel/eslint-parser
等。
module.exports = {
"parser": "@typescript-eslint/parser",
}
parserOptions
对于解析器的配置。常用的属性有以下几个:
ecmaVersion: latest
: 配置代码的js
版本,告知ESLint
我们想要支持什么版本的 JS 语法sourceType: 'module'
: 源码类型,是否允许使用export
/import
ecmaFeatures: { jsx: true }
:告知ESLint
是否使用jsx
rules
一条 rule 就是单独的一条规则,针对一个具体问题,例如不允许使用 console.time()
。
{
"rules": {
"semi": ["error", "always"],
"quotes": ["error", "double"]
}
}
semi
和 quotes
是 rules 的名字,第一个值是规则的错误等级,有一下三类:
"off"
或0
- 关闭规则"warn"
或1
- 将规则视为警告"error"
或2
- 将规则视为错误
三个错误等级让我们可以方便控制 ESLint
如何应用这些规则,此外还可以传入 options
,用于灵活的配置规则,本文将会编写的规则便需要自定义 options
。
我们在 .eslintrc.*
中配置的 rules
字段是规则,我们在代码中写的注释,例如 // eslint-disable-next-line @typescript-eslint/no-unused-vars
也是规则,两部分合并起来得到了最终的 rule
。
得到了最终 rule
之后,深度优先遍历源码生成的 AST
,将 node
存放到 ESLint
内部维护的队列中,然后遍历所有规则,为规则中所有的选择器添加监听事件,在触发时执行,触发队列中包含的事件就会返回显示给用户的 lintingProblems
。在 ESLint
中应用 rule
是一个事件驱动的好范例。
plugins
针对特殊语法自定义的那些规则我们称之为eslint
插件,插件为一系列规则 rules
的合集,一般作为一个 plugin
,如一个 plugin
是用来规范变量命名的,那么它可能包含对于普通变量的 rule
,对于导出常量的 rule
,对于组件命名的 rule
,对于样式命名的 rule
等等。
常见的插件有: eslint-plugin-import
、eslint-plugin-promise
、eslint-plugin-react
。ESLint 本身规则只会去支持标准的 ECMAScript 语法,如果我们想在 React 中也使用 ESLint 则需要自己去定义一些规则,就有了 eslint-plugin-react 。
extends
一系列 plugins
的合集,如 Google 的 gts
。根据要求,extends
的命名都以eslint-config-
为开头,在配置 extends
时可以省略这个前缀。引入的 extends
可以是 npm
包,也可以是本地路径。plugins
的配置仅仅代表在项目中引入了哪些规则,并没有指明该规则是警告、报错、忽略,extends
要做的就是引入 eslint
推荐的规则设置。按照 eslint
插件的开发规范,每个插件的入口文件都会导出一个对象,其中就有一个 configs
字段,该字段是个对象,他可以把该插件已有的规则分成不同的风格。
/** .eslintrc.js */
module.exports = {
// eslint-config-airbnb https://www.npmjs.com/package/eslint-config-airbnb
"extends": "airbnb"
};
Airbnb JavaScript Style Guide 是最出名的社区实践之一,上述 airbnb
为 eslint-config-airbng
的缩写,这里的意思是让 ESLint
将 eslint-config-airbng
的规则做为拓展引用到我们自己的项目中来。
overrides
若要对某些文件进行更细致的定制化,则在overrides
字段中进行配置。
settings
用于配置全局共享的设置,官方文档。
env
设置代码的运行环境,如 node 环境还是 browser 环境。在设置 env 后可以使用对应的全局变量。 除了预定义的字段外,也可以开启某个 plugin 中的某种环境
"env": {
"browser": true, // 可以使用window
"es6": true, // 可以使用Map、Set等新的数据类型,并自动开启es6语法
"node": true, // 可以使用global
},
编写自己的ESLint插件
此插件的目的在于对 import
项分组整理,达成以下效果:
// react
import React, {useEffect} from 'react';
// style
import styles from './index.module.scss'
// layout
import Footer from "@layout/footer";
import Header from "@layout/header";
开发过程
新建项目
我们使用官方推荐的 Yeoman generator 开发插件,依次运行以下指令
npm i -g yo
npm i -g generator-eslint
yo eslint:plugin // create a new ESLint plugin, make sure you're in the top-level directory
yo eslint:rule // create a new ESLint rule, make sure you're in the top-level directory
运行完上述指令后,我们会得到如下所示的文件树
.
├── README.md
├── docs
│ └── rules
│ └── import-order-demo005.md
├── lib
│ ├── index.js // 此处将规则进行导出
│ └── rules
│ └── import-order-demo005.js // 这里是我们的规则主体内容
├── package-lock.json
├── package.json
└── tests
└── lib
└── rules
└── import-order-demo005.js // 在此处编写我们的测试用例
编写自定义规则
解析context
对象需要用到 eslint
提供的多个方法,我们主要参考官方中文文档:cn.eslint.org/docs/develo…,具体涉及到的方法用途写在了代码注释中。working-with-plugins 和 working-with-rules 这两篇官方文档能够帮助我们更好地理解如何编写自定义规则。
// lib/rules/eslint-plugin-group-import-decorations.js
/**
* @fileoverview Group import decorations in React projects
* @author fl427
* eslint开发方法集合:https://cn.eslint.org/docs/developer-guide/working-with-rules
*/
"use strict";
//------------------------------------------------------------------------------
// Rule Definition
//------------------------------------------------------------------------------
/** @type {import('eslint').Rule.RuleModule} */
module.exports = {
meta: {
type: 'suggestion', // `problem`, `suggestion`, or `layout`
docs: {
description: "Group import decorations in React projects",
recommended: false,
url: null, // URL to the documentation page for this rule
},
fixable: 'code',
schema: [{
type: 'object',
}],
messages: {
sort: '运行ESLint规则来整理import分组',
nameComment: '分组注释错误',
intervalComment: '同组的节点之间存在多余注释'
},
},
create(context) {
// 拿到配置对象,对象的key即为组名(ex: 'react'),对象的value即为该组对应正则(ex: '^react')
const options = context.options[0] || {
"react": "^react",
"style": ".(css|scss|less|sass)$",
"common": ".(png|jpg|jpeg|svg|gif|json)$",
"hooks":"/hooks/",
"src": "@src/",
};
const groups = [];
// 遍历配置对象,构造分组数组
for (let name in options) {
const reg = options[name];
groups.push({
name,
reg,
imports: []
})
}
// 用户没有指定other则将无法分类的import项统一放入other分组中
if (!options['other']) {
groups.push({
name: 'other',
rules: '.*',
imports: [],
})
}
// group前注释错误
function isGroupNameError(node, name) {
// getSourceCode返回一个SourceCode对象,可以使用该对象处理传递给 ESLint 的源代码。
const sourceCode = context.getSourceCode();
// getCommentsBefore返回一个在给定的节点或 token 之前的注释的数组
const commentsBefore = sourceCode.getCommentsBefore(node);
// 没有注释 或者 最近的一行注释和指定的group名字不匹配
return {
// 没有注释
noNameComment: !commentsBefore.length,
// 有注释但注释错误
groupNameIsError: commentsBefore.length && commentsBefore[commentsBefore.length - 1].value !== ` ${name}`
}
}
return {
Program: programNode => {
// SourceCode 是获取被检查源码的更多信息的主要对象。可以使用 getSourceCode()在任何时间检索 SourceCode 对象。
const sourceCode = context.getSourceCode();
// importNodes存放所有的import node
const importNodes = [];
// 将import node放到正则匹配的数组中
for (const node of programNode.body) {
if (node.type === 'ImportDeclaration') {
// 是引用语句
importNodes.push(node);
// 遍历每个分组
for (let group of groups) {
const regex = new RegExp(group.reg, 'ig');
// 这个node符合该rule,放到对应的数组中
if (regex.test(node.source.value)) {
if (Boolean(group.imports) === false) {
group.imports = [];
}
group.imports.push({
// 每次推入一个node,group.imports的长度就+1,我们将当前node的序号记录下来,方便后续判断哪一个import node是当前组的第一个,从而添加组前注释
idx: group.imports.length,
name: group.name,
node,
});
break;
}
}
}
}
// 平铺存放按顺序排好的{idx, name, node}结构体,后续与原始的import nodes对比
let orderImport = [];
for (let group of groups) {
orderImport = orderImport.concat(group.imports);
}
// 遍历所有的import nodes,利用fixer让import nodes的顺序与整理好的group-imports一致(通过替换文本实现)
for (let index = 0; index < importNodes.length; index++) {
// 原始node
const node = importNodes[index];
const prevNode = importNodes[index - 1];
// {idx, name, node}结构体
const orderedNode = orderImport[index];
const prevOrderedNode = orderImport[index - 1];
// getText返回给定节点的源码。省略 node,返回整个源码。
const nowText = sourceCode.getText(node);
const orderedNodeText = sourceCode.getText(orderedNode.node);
if (nowText !== orderedNodeText) {
context.report({
node,
messageId: 'sort',
// replaceText替换给定的节点或记号内的文本
fix: fixer => fixer.replaceText(node, orderedNodeText),
});
} else {
// 是该组的第一个节点,判断这个节点的注释是否正确,不正确则在前面插入注释
const { noNameComment, groupNameIsError } = isGroupNameError(orderedNode.node, orderedNode.name)
// 是该组的第一个节点,没有注释或者注释错误都要插入一行注释
const isFirstNodeAndCommentErr = orderedNode.idx === 0 && (noNameComment || groupNameIsError);
// 不是第一个节点,但是它之前有注释且注释错误,需要插入一行注释来纠正错误,我们不会在没有注释的情况下插入注释,因为我们只希望注释放在第一个节点前面
const otherNodeAndCommentErr = orderedNode.idx != 0 && groupNameIsError
if (isFirstNodeAndCommentErr || otherNodeAndCommentErr) {
context.report({
node,
messageId: 'nameComment',
// insertTextBeforeRange在给定的节点或记号之前插入文本
fix: fixer => fixer.insertTextBeforeRange(node.range, `// ${orderedNode.name}\n`),
});
}
// 如果前后两个node是同一组,而它们之间存在注释,则去除注释
if (prevNode && prevOrderedNode && sourceCode.commentsExistBetween(prevNode, node) && prevOrderedNode.name === orderedNode.name) {
context.report({
node,
messageId: 'intervalComment',
// removeRange删除给定范围内的文本
fix: fixer => fixer.removeRange([prevNode.range[1] + 1, node.range[0] - 1]),
});
}
}
}
}
};
},
};
编写测试用例
'use strict';
/**
* @fileoverview import order
* @author fl427
*/
'use strict';
const rule = require("../../../lib/rules/eslint-plugin-group-import-decorations")
const { RuleTester } = require('eslint');
const ruleTester = new RuleTester({
parserOptions: {
ecmaVersion: 2020,
sourceType: 'module',
},
});
ruleTester.run('eslint-plugin-group-import-decorations', rule, {
valid: [
{
code:
`
// react
import React, { ReactNode, useEffect, useMemo, useRef, useState } from 'react';
import { useLocation } from 'react-router-dom';
// style
import './index.scss';
// hooks
import useInterval from 'src/hooks/useInterval';
// common
import noneImg from '../../common/imgs/empty.png';
`,
errors: 1,
options: [{
"react": "^react",
"style": ".(css|scss|less|sass)$",
"src": "@src/",
"common": ".(png|jpg|jpeg|svg|gif|json)$",
"hooks":"/hooks/",
}],
}
],
invalid: [
{
code: `
import React, { ReactNode, useEffect, useMemo, useRef, useState } from 'react';
import { useLocation } from 'react-router-dom';
import './index.scss';
`,
errors: 1,
options: [{
"react": "^react",
"style": ".(css|scss|less|sass)$",
"src": "@src/",
"common": ".(png|jpg|jpeg|svg|gif|json)$",
"hooks":"/hooks/",
}],
},
],
});
导出并发布
我们的插件至此开发完成,接下来需要编写对 eslint
暴露这个模块的代码。
// lib/index.js
/**
* @fileoverview Group Import Decorations in React Projects
* @author fl427
*/
"use strict";
const requireIndex = require("requireindex");
// import all rules in lib/rules
module.exports.rules = requireIndex(__dirname + "/rules");
module.exports = {
rules: {
'group-import-decorations': require('./rules/eslint-plugin-group-import-decorations')
},
configs: {
recommended: {
rules: {
'group-import-decorations/group-import-decorations': 2
}
}
}
}
完成了对于模块的导出之后,我们将其发布到 npm
方便使用。
npm config set registry https://registry.npmjs.org/ // 修改registry,确保我们的npm源为官方源
npm login // 发布npm包需要是登录状态
npm publish // 发布npm包
至此整个流程发布完毕。
项目中引用
我们编写的是 ESLint
插件,所以为了让其生效,我们首先需要在 VSCode
或者 WebStorm
中启用对 ESLint
的支持,对 VSCode
,我们需要安装 ESLint
插件,而 WebStorm
内置了 ESLint
,我们在 Preference
中搜索 eslint
并打开即可。
在 devDependencies
装好 npm
包之后在 .eslintrc.js
文件中配置如下内容,指定我们的 rules
。
// .eslintrc.js
module.exports = {
"env": {
"browser": true,
"es2021": true,
"node": true,
},
"extends": [
"eslint:recommended",
"plugin:react/recommended",
"plugin:@typescript-eslint/recommended",
"plugin:group-import-decorations/recommended"
],
"overrides": [
],
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaVersion": "latest",
"sourceType": "module"
},
"plugins": [
"react",
"@typescript-eslint",
'import',
"group-import-decorations",
],
// group-import-decorations/group-import-decorations,前面的是我们的plugin的名字,后面的是module.exports的rules的key值
"rules": {
'@typescript-eslint/no-var-requires': 0,
"group-import-decorations/group-import-decorations": [
2,
{
"react": "^react",
"style": ".(css|scss|less|sass)$",
"layout": "@layout/",
"pages": "@pages/",
"common": ".(png|jpg|jpeg|svg|gif|json)$",
"hooks":"/hooks/",
}
],
}
}
配套源码地址
npm
github
参考
Working with Rules - ESLint - Pluggable JavaScript Linter
Working with Plugins - ESLint - Pluggable JavaScript Linter