ESLint机制分析与简单插件实践
作者:YePeng前言
代码是写给人看的,所以一份好的代码,是要让水平不一的阅读者,都能够理解代码的本意。每个人的代码风格是不可能完全相同的,例如在一个文件里,有的以两个空格做缩进,有的以四个空格做缩进,有的使用下划线,有的使用驼峰,那么它的阅读体验就会变得很差。
所以如何来对代码进行约束,使团队的代码风格尽量统一,不产生更多的理解成本,是一个需要解决的问题。众所周知,懒是社会生产力进步的源动力,所以。。。
在前端工程化的标准中有一项就是自动化,自动化当中就包括了代码规范自动化。实现代码规范自动化可以解放团队生产力,提升团队生产效率。。。所以ESLint、TSLint、StyleLint这些工程化插件应运而生。
而最近在笔者团队也在统一不同的项目之间的规范差异,相信大家也都遇到了大段飘红的现象,今天咱来简单探究一下背后涉及到的原理。WhatisESLintLint?
首先,提到ESLint,应该会想到两种东西,一个是ESLint的npm包,也就是我们devDep里面的,另一个是我们所安装的比如VSCode的ESLint插件,那么这两个东西有什么联系呢。
npm包:是实际的Lint规则以及我们执行Lint的时候,控制代码如何去进行格式化的。
VSCode插件:实际指向我们项目的nodemoduleseslint或者全局的eslint,通过ESLint的规则,告诉IDE,哪些地方需要飘红。也就是说插件是在解析我们的打开的文件,同时和规则对比,是否存在ESLint问题。以及可以通过我们的IDE配置,在不同的时机去执行我们的Lint,比如保存自动格式化。
总而言之,ESLint规则就是对我们的代码风格和代码中潜在的一些错误和不规范用法的一个约束,通过npm包的形式引入项目,同时通过IDE的插件,读取npm包规则,对我们的代码进行错误提示。HowtoUseit?
如何在项目配置ESLint就不在本文赘述了。大多数脚手架其实都会给你初始化好基本的ESLint。涉及到的工具不同可能会有些许的不一样,不过都大差不差。这段讲一下ESLint中的主要配置项。如果有兴趣深一步研究,可以移步ESLint的官网文档〔1〕,对ESLint默认的规则集〔2〕感兴趣也可移步官网文档一同查看。
打开一个eslintrc文件,一般来说,有几个选项。这里以json为例,来简单说明下每个字段。{extends:,规则集继承自某个规则集root:true,找到这后,不再向上级目录寻找解析选项parserOptions:{ecmaVersion:6,指定你想要使用的ECMAScript版本356789sourceType:module,script(default)ormodule,标明你的代码是模块还是scriptecmaFeatures:{是否支持某些feature,默认均为falseglobalReturn:true,是否允许全局returnimpliedStrict:true,是否为全局严格模式jsx:true}},自定义解析器,官方支持下列四种,也可以自己定义解析器。parse:espreeesprimaBabelESLinttypescripteslintparser,plugins:〔aplugin〕,第三方插件aprocessor:apluginaprocessor,制定处理器为插件a的处理器rules:{eqeqeq:error}指定一些全局变量,类似于global。d。ts的作用globals:{var1:writable,var2:readonly}忽略哪些文件ignorePatterns:〔src。test。ts,srcfrontendgenerated〕}
ESLint支持以下几种格式的配置文件,如果同一个目录下有多个配置文件,ESLint只会使用一个。优先级顺序如下:。eslintrc。js。eslintrc。yaml。eslintrc。yml。eslintrc。json。eslintrcpackage。json
同时ESLint也支持对每个目录配置不一样的规则,对于mono仓库下,可能每个repo的ESLint都有些许的区别,这个时候我们就可以采用下面的目录格式,根目录下存在基本规则,子app下存在特定的规则。子rc是对父rc的一个override,但是如果我们在app。eslintrc。js中设置了root:true,那么对于test。js,父目录中rc使用的规则,在app中不会生效。packagespackage。json。eslintrc。jslibtest。jsapp。eslintrc。jstest。jsWhydoesitwork?
AST
他是为什么能够生效的。这里就要提到我们前端方方面面都要涉及到的AST了,感谢新时代。
ESLint是基于抽象语法树来进行工作的,ESLint默认使用的编译器(parse)是Espree〔3〕,通过它来解析我们的JS代码生成AST,基于AST,我们就可以对我们的代码进行检查和修改了。
通常我们的Babel编译分为下图这几步,编译转换生成。ESlint和它对比,只有第一步是一致的,因为我们只需要拿到AST中的部分信息,同时直接在源码中进行提示和操作就行,并不需要transform和后续的生成代码。
解析
现在我们通过demo来探究他背后的原理以及转换的方式。首先,我们需要加载和解析我们的源代码。这就是编译器将我们的代码转换成AST树的一个过程。因为已经全面拥抱typescript(主要是因为espree没有类型注解,我难受),所以本文使用typescripteslintparser来作为我们的编译器。这里有个小坑,如果在VSCode安装了importcost插件的话,他去解析这个parser会特别卡,所以可以暂时禁用。constfooanthonyconstbardstimportfsfromfs;importpathfrompath;importastsParserfromtypescripteslintparser;constfilePathpath。resolve(。srctest。ts)consttextfs。readFileSync(filePath,utf8)编译成AST这里是不是和eslint的配置项对上了,没错就是透传而已constasttsParser。parse(text,{comment:true,创建包含所有注释的顶级注释数组ecmaVersion:6,JS版本指定其他语言功能,ecmaFeatures:{jsx:true,启用JSX解析globalReturn:true在全局范围内启用return(当sourceType为commonjs时自动设置为true)},loc:true,将行列位置信息附加到每个节点range:true,将范围信息附加到每个节点tokens:true创建包含所有标记的顶级标记数组})
然后我们将获得的AST打印一下,简单从下图可以看到主要包含的内容。本地打印出来可能不太方便阅读,也可以使用在线的工具〔4〕,将解析器设置为typescripteslintparser。相对于espree来说,ts解析多出来的部分中,比较关键的就是图中这段,决定我们如何去解析他的类型。
AST就是记录了读取源文件之后的文本内容的各个单位的位置信息,这样我们就可以通过操作AST修改需要修改的内容,然后再根据修改后的AST信息进行修改对应的文本内容。比如我们把上文中的const关键字修改成let,那么我们就先对AST对应的const内容进行修改为let,得到修改之后的AST数据,再根据修改后的AST数据去修改对应的文本内容。所谓的修改就是字符串替换,因为我们已经知道了对应的位置信息。
SourceCode
但是根据上面我们可以看到,直接根据AST去查找然后比对替换,效率是很低的,而且嵌套比较深。这个时候ESlint是怎么干的呢?他生成了一个新的结构用于我们操作,也就是SourceCode。有兴趣进一步探究可以自行查阅源码的sourcecodesourcecode。js部分。简单来说,就是构建了一个SourceCode实例,接受两个参数,原文text和解析后的AST,然后返回我们一个包含茫茫多方法的实例对象。
我们在demo项目中装一个eslint然后引入SourceCode,看看构造后的对象是个什么玩意。import{SourceCode}fromeslint;。。。。constsourceCodenewSourceCode(text,ast);这打个断点,看看sourceCode结构
简单来讲解(摘抄)一下实例对象里面的一些属性和proto上的方法,完整属性可以查阅官网源码类型注解。hasBOM:是否含有unicodebom〔5〕;lines:将我们的每一行切割,分行形成的一个array;linsStartIndices:每行的开始位置;tokenAndComments:token和comment的一个有序集合;getText(node?:ESTree。Node,beforeCount?:number,afterCount?:number):string;isSpaceBetweenTokens(first:AST。Token,second:AST。Token):boolean;两个token间是否有空格;visitorKeys:存在的key值。
OK,前置的一些知识我们已经介绍的差不多了。接下来结合实际的rulesdemo来进行讲解。
规则模版
相信如果有写过VSCode插件的同学应该对Yeoman不陌生,ESLint也有提供基于Yeoman的一套脚手架用于生成模版。
首先全局安装eslint的脚手架,npminstallgyogeneratoreslint,然后通过下面的一些交互式命令行操作来初始化我们的操作。
通过初始化,我们可以看到一个以下的文件的壳子,我们在里面添加一些我们上面所讲到的东西。打开我们生成的规则模版文件,同时在里面添加一些规则和提示(注意,这里我的写法不规范,我将两种无关规则放在了一个规则文件里)。usestrict;type{import(eslint)。Rule。RuleModule}module。exports{meta:{type:problem,problem,suggestion,orlayoutdocs:{description:xxxx,recommended:false,url:null,URLtothedocumentationpageforthisrule},messages:{temp:不样你用字面量作为函数的参数传入,novar:不样你用var声明,noExport:退出时执行这个},fixable:code,Orcodeorwhitespaceschema:〔〕,Addaschemaiftherulehasoptions},create(context){variablesshouldbedefinedhereconstsourceCodecontext。getSourceCode();return{ArrowFunctionExpression:(node){if(node。callee。name!abcd)return;if(!node。arguments)return;node。arguments。forEach((argNode,index){argNode。typeLiteralcontext。report({node,messageId:temp,fix(fixer){constvalargNode。value;conststatementStringconstval{index}{val};return〔fixer。replaceTextRange(node。arguments〔index〕。range,val{index}),fixer。insertTextBeforeRange(node。range,statementString)〕}})})},Program:exit(node){context。report({node,messageId:noExport,});},VariableDeclaration(node){if(node。kindvar){context。report({node,messageId:novar,fix(fixer){constvarTokensourceCode。getFirstToken(node)returnfixer。replaceText(varToken,let)}})}}};},};
关键函数
在这个demo里面,我们看到几个东西,一个是create函数的参数context以及他的返回值,还有就是context上提供的report方法以及report接受的fix参数。这几个加起来,形成了我们一条规则的校验逻辑,通过遍历,我们到了某个AST节点,如果某个AST节点满足了我们所写的某条规则,我们进行report,同时提供一个修复函数,修复函数通过token或者range来决定对某处进行文本替换。
接下来,挨个来讲解这些东西,首先是context的上下文形成,这个没有什么好说的,其实就是创建了一个对象,然后提供了一些一些方法,供我们在插件中访问上下文使用,然后对于每个rule都在createRuleListener中都创建了一个listener,这里我们在后面串整体流程时还会再过一遍。
接着是report方法,简单分析下这块代码,其实就是通过一系列的操作,然后往lintingProblem这个数组里面推了一个problem。这个problem包含一些错误信息,AST信息等等。
最后是我们的fix,我们上面用到的所有replace方法,其实都殊途同归,最后回到了这里,大道至简,简单的slice和完成了我们的修复动作。
基本上一个插件涉及到的核心几个东西,都简单解释了下。现在我们来串一串整体检测和修复的流程,也就是源码中linter。js中的runRules方法。
整体流程
我们在跑规则的时候,肯定需要的是对AST进行遍历,同时做一些操作。首先做了一个什么操作呢,调用了一个实例方法Traverser。traverse,传入了ast和一个对象,包含enter、leave和visitorKeys。这个函数的作用就是进行一个递归遍历,同时在遍历的时候通过enter和leave我们在队列中存储了两个相同的节点,一个是进入时,一个是退出时,方便我们后续处理。这里涉及到一个设计模式,访问者模式(用于数据和操作解耦),通过在遍历时加上isEntering,可以让我们决定是在进入时还是退出时执行访问者逻辑。
接着我们需要把我们的所有规则都给像上面讲的给创建成ruleListener,然后在我们的nodeQueue后续遍历时,触发某些逻辑。当然,这里大家可能都想到了订阅发布模式,这个也是在我们整个逻辑中比较重要的一环,遍历时,通过emit推送消息,然后让ruleListener决定是否需要执行某些逻辑,所以,我们需要对Listener订阅上某些事件。
接下来,就是对我们的nodeQueue遍历了,通过我们节点上打上的标,来决定是在执行进入逻辑还是离开逻辑。这里我就不展开讲具体的细节了,其实简单理解就是通过enter和leave的时候去触发不同的visitor的动作。
后语
本文仅是初步的探索了背后的原理,根据原理,后续可以做的一些例如ESLint插件等等并没有详细的阐述,大家可以自行探索。
最后送大家一句话,linus说的,也是我比较信奉的一句话:talkischeap,showmethecode,想了解一个东西,最好的办法就是简单实现它。我相信大家在解析完它的流程后,都能够简单实现一个ESLint的小demo,以及能够上手写一写eslintplugin。参考资料
〔1〕ESLint的官网文档:https:eslint。orgdocslatestuserguideconfiguringconfigurationfiles
〔2〕ESLint默认的规则集:https:eslint。orgdocslatestrules
〔3〕Espree:https:github。comeslintespree
〔4〕在线的工具:https:astexplorer。net
〔5〕unicodebom:https:en。wikipedia。orgwikiByteordermark
今生我爱,一生相依,今生我爱,不离不弃今生遇到了你,你就是我一世的深情,我怀念青丝缕缕的青葱岁月,我怀念充满爱的繁花小径,我怀念雨中你撑着伞的倩影,我夜夜在心底种植思念,我夜夜期盼月圆期盼你的爱恋。玫瑰开了一年又一年,
揭秘!只要半杯水,真假酒立马现原形?爱喝酒的人一定要看看揭秘!只要半杯水,真假酒立马现原形?爱喝酒的人一定要看看俗话说,人在江湖飘,哪有不挨刀。尤其是爱酒之人,平日最怕买到的就是假酒,喝了不仅头疼恶心,甚至第二天还会难受,所以如何鉴别出
北京十大特色小吃1驴打滚豆面糕又称驴打滚,是北京小吃中的古老品种之一,它的原料是用黄米面加水蒸熟,和面时稍多加水和软些。另将黄豆炒熟后,轧成粉面。制作时将蒸熟发黄米面外面沾上黄豆粉面擀成片,然后抹
知乎者也丨刘少辉磻溪捻面小时候,经常到姑婆家做客,姑婆家离老家磻溪村不远,在一个山谷中,沿着平路走去,约半个小时,就到了姑婆家。姑婆家是一栋两层楼的平房,厅堂建在南边,中间有一个铺满石头的长方形的天井,正
年过60岁还能喝白酒吗?行家能喝,但要牢记这3点,喝着踏实经历过第一波之后,许多康复的酒友开始恢复了正常的饮酒频率,但是对于超过60岁的酒友来说,再这样特殊的情况之下还能喝白酒吗?为此小编咨询了一位喝酒几十年的行家,他表示酒喝是能喝,但要
冬天,聪明人经常吃这菜,好吃不贵,营养又解腻,全家人都爱吃头条创作挑战赛年关将近,冬天也要接近尾声,马上要迎来春节,最近是很多人返程的高峰期,回家后都是鸡鸭鱼肉,各种美味佳肴都吃腻了,我们也可以试一试家常小菜雪里蕻炒猪肉末,雪里蕻是农村非
供不应求!梅州客家预制菜节前销售火爆销售额比平时翻了好几倍。搭乘电商快车,梅州客家预制菜抢滩行业新风口,迎来节前爆单。日前,记者走访多家预制菜企业,发现客家盐焗食品已成为电商爆款,不少企业直呼供不应求。堆积如山的订单
若觉人生虚幻无意义,不妨看看尼采这一生有人问周国平,人们拼命工作,追求物质享受,梦想财物自由,却从未考虑过心灵自由,在这样的时代背景下,阅读尼采有什么意义?这问题问得很有现实意义,因为这就是无数人的问题。很多人盯着物质
直播预告今夜,舞蹈群星顶峰相会,带来精彩演出今晚1930,群星闪耀,顶峰相会。由中国舞蹈家协会主办中国文联舞蹈艺术中心承办国家大剧院支持的2022中国顶尖舞者之夜将在线上播出,艺绽视频号将进行同步直播。本场演出汇聚了刘岩李德
莫科球队伤员多又碰到辽宁这种强队年轻球员拼搏劲头值得表扬直播吧1月14日讯CBA常规赛,四川87105不敌辽宁。赛后四川主教练莫科和球员代表肖傈仁出席了新闻发布会。莫科点评本场比赛道今天由于我们队出现了很多的伤病,有两个队员已经回到成都
过完年准备就业的年轻人请进,希望对您有所帮助(文伤感岁月)一个人的命运和国家的命运其实是息息相关的,这一点可能很多年轻人体会不是很深,但随着年龄和阅历的增长,对这一点的体会就会越深。认识到这点,在规划自己的事业和职业的时候,就应该努力把握