当前位置: 首页 > Web前端 > HTML

最近流行的AST分析原理&社区解决方案对比

时间:2023-04-03 00:40:51 HTML

简介《代码分析转换》是前端开发中比较小众的技能树。在迁移架构的过程中,遇到了代码批量转换的问题,于是做了一些原理和工具的研究。最近发现社区里很多讨论这个的文章也引起了大家的关注,所以打算在这里多分享一些我们的经验。事实上,AST分析的过程离不开每个开发者的工作,小到eslint语法检查,大到框架升级。简单和个性化的转换可以被人眼识别并手动修改。简单的批量转换可以通过正则匹配和字符串替换来完成。然而,对于更复杂的转换,AST是最有效的解决方案。基于AST的代码分析转换AST简介抽象语法树(AbstractSyntaxTree)简称AST,是一种程序设计语言语法结构的树状表示,树上的每个节点代表源代码中的一个结构。JavaScript引擎工作的第一步是将代码解析为AST。Babel、eslint、prettier等工具都是基于AST的。将源代码解析成AST分为两步,词法分析和句法分析1.词法分析,将字符序列转换为词(Token)序列的过程。2.语法分析,将Token序列组合成各种语法短语,如Program,Statement,Expression等。基于AST分析转换的优点AST会忽略代码风格,将代码解析成最纯粹的语法树,所以AST-based的转换更加准确和严谨,但是使用正则表达式来分析转换后的代码并不能有效分析一段代码的上下文。即使是简单规则的匹配,也需要考虑太多的边界条件,兼容各种代码风格。举个简单的例子,将var定义的所有变量都转换为let定义,基于AST可以很容易做到,但是使用正则表达式需要考虑各种情况,写出来的表达式也很难调试和理解。基于AST转换的过程,将源码通过词法分析和句法分析解析成ASTtransform,对AST进行分析转换生成,将最终的AST输出为代码。其中1和3在社区有成熟的工具,可以立即使用。第二步需要开发者自己操作AST。社区中有流行的工具,但也存在一定的问题。社区流行方案现状及问题社区流行方案有Babel、jscodeshift、esprima、recast、acorn、estraverse等,本文选取最具代表性的Babel和jscodeshift进行分析。Babel插件方案解析没有Babel,就没有今天JS社区在语言规范上的高度繁荣,而babel/parser也是一个非常优秀的解析器。很多开发者进行代码分析和转换都离不开Babel插件,但是我个人认为目前Babel插件的编写方案存在几个问题。1、入门难,学习成本高。体积大3.代码可读性差,不利于维护。具体来说:1.难度大、学习成本高在开始Babel插件开发之前,需要对AST规范、AST节点的类型和属性有深入的了解。参考babel-types和babel节点类型,200多种节点类型。babelrc的配置和babelplugin的写法是基础。此外,还需要了解visitor、scope、state、excit、enter等概念,以及babel-types、babel-traverse、builder等工具。2、匹配构建节点逻辑复杂,代码量大。匹配节点需要逐层逐一比较节点类型和属性。如果需要确定上下文信息,那就更复杂了。构建节点也需要严格按照类型和结构进行。操作AST需要花费大量时间,不能专注于分析和转换的核心逻辑。使用Babel匹配self.doEdit('price')(this,'100'),写法如下MemberExpression(path){if(path.node.object.name=='self'&&path.node.property.name=='doEdit'){constfirstCallExpression=path.findParent(path=>path.isCallExpression());如果(!firstCallExpression){返回;}if(!firstCallExpression.node.arguments[0]){返回;}letsecondCallExpression=nullif(firstCallExpression.node.arguments[0].type=='StringLiteral'&&firstCallExpression.node.arguments[0].value=='price'){secondCallExpression=firstCallExpression.findParent(path=>path.isCallExpression())}如果(!secondCallExpression){返回;}if(secondCallExpression.node.arguments.length!=2||secondCallExpression.node.arguments[0].type!='ThisExpression'){返回;}constpId=secondCallExpression.node.arguments[0].value;}}复制代码使用Babel构造'varvarName=require("moduleName")',写法如下types.variableDeclaration('var',[types.variableDeclarator(//t.variableDeclarator(id,init)//id是标识符//这里的init必须是一个Expressiontypes.identifier('varName'),//t.callExpression(callee,arguments)types.callExpression(types.identifier('require'),[types.stringLiteral('moduleName')])),]);复制代码3.代码可读性能差,不利于维护看完上面两个例子,你会发现不仅代码量大,而且可读性也不够好,即使你对AST和Babel非常熟悉,也需要仔细体会jscodeshift对比babel的分析,jscodeshift的优势在于更容易匹配节点,链式操作使用起来更方便。matchself.doEdit('price')(this,'100'),写法如下constcallExpressions=root.find(j.CallExpression,{callee:{callee:{object:{name:'self'},property:{name:'doEdit'}},arguments:[{value:'price'}]},arguments:[{type:'ThisExpression'},{value:'100'}]})复制代码转换构造节点的方式和babel的写法类似,不再赘述.可见jscodeshift并没有解决上面提到的三个问题。于是在社区的宝贵经验的基础上,我们开发了一个新的工具GoGoCode。目的是让开发者以最高的效率和最低的成本完成代码分析转换。AnotherSolutionGoGoCode概述GoGoCode是一款操作AST的工具,可以降低AST的使用门槛,帮助开发者从繁琐的AST操作中解脱出来,更专注于代码分析和转换逻辑的开发。简单的替换甚至不需要学习AST,更复杂的分析和转换在初步学习AST节点结构后就可以完成(参考AST查看器)。理念GoGoCode借鉴了JQuery的思想,我们的使命是让代码转换像使用JQuery一样简单。jQuery在原生js的基础上极大的方便了DOM操作的效率。没有复杂的配置过程,开箱即用,还有很多优秀的设计思想值得学习:比如$()实例化、选择器思想、链式操作等。此外,我们应用了简单替换成AST,效果也很好。$()实例化方法使用$(),源代码和AST节点都可以被实例化为AST对象,任何挂载在实例上的函数都可以链接起来$(code:string)$('vara=1')$(node:ASTNode)$({type:'Identifier',name:'a'}).generate()copycodecodeselectorDOM树和AST树都是树结构,JQuery可以匹配各种选择器Node,可以AST匹配通过简单选择器的真实节点?所以我们定义了“代码选择器”。无论你想找什么样的代码,都可以通过代码选择器直接匹配到$(code).find('importafrom"./a"')$(code)。find('functiona(b,c){}')$(code).find('if(a&&sth){}')复制代码如果你要匹配的代码包含不确定部分,把不确定部分用通配符代替,用$_$表示。恭喜发财o(*≧▽≦)ツ$(code).find('import$_$from"./a"')$(code).find('function$_$(b,c){}')$(code).find('if($_$&&sth){}')复制代码链式操作GoGoCode提供的大部分API都可以链式使用,让代码更简洁,优雅。我们对整个代码应用多个转换规则更方便$(sourceCode).replace('const$_$1=require($_$2)','import$_$1from$_$2').find('console.log()').remove().root().generate()复制代码方法重载:.attr()可以获取或修改节点属性,优于手动遍历逐层判断操作属性和节点非常友好$(code).attr('id.name')//返回节点id属性中name属性的值$(code).attr('declarations.0.id.name','c')//修改name属性的值复制代码简单替换比常规替换更简单、更强大、更好用。$_$n类似于正则中的捕获组,$$$类似于rest参数$(code).replace('{text:$_$1,value:$_$2,$$$}','{name:$_$1,id:$_$2,$$$}')$(code).replace(`import{$$$}from"@alifd/next"`,`import{$$$}来自“antd”`)$(code).replace(`$$$2`,`$$$2

`)$(code)。replace(`Page({$$$1})`,`Page({init(){this.data={}},$$$1})`)复制代码CoreAPIBasicAPIGetNodeAPIOperationNode$().find().attr()$.loadFile.parent().replace().generate().parents().replaceBy().siblings().after().next().before().nextAll(.append().prev().prepend().prevAll().empty().root().remove().eq().clone().each()与前面示例中流行的社区解决方案相比,匹配自我。doEdit('price')(this,'100')语句,使用GoGoCode编写如下$(code).find(`self.doEdit('price')(this,'100')`)复制代码构造'varvarName=require("moduleName")',用GoGoCode写如下,我们希望对不同的console.log做不同的处理。调用console.log时,删除console.log()作为变量初始值,转换为void0console.log作为变量初值时,转换为空方法代码。转换后的结果如下:使用GoGoCode实现的代码如下:$(code).replace(`var$_$=console.log()`,`var$_$=void0`).replace(`var$_$=console.log`,`var$_$=function(){}`).find(`console.log()`).remove()。产生();复制代码Babel实现的核心代码如下://代码来源:https://zhuanlan.zhihu.com/p/32189701module.exports=function({types:t}){return{name:"transform-remove-console",visitor:{CallExpression(path,state){constcallee=path.get("callee");如果(!callee.isMemberExpression())返回;if(isIncludedConsole(callee,state.opts.exclude)){//console.log()if(path.parentPath.isExpressionStatement()){path.remove();}else{//vara=console.log()path.replaceWith(createVoid0());}}elseif(isIncludedConsoleBind(callee,state.opts.exclude)){//console.log.bind()path.replaceWith(createNoop());}},成员表达式sion:{exit(path,state){if(isIncludedConsole(path,state.opts.exclude)&&!path.parentPath.isMemberExpression()){//console.log=funcif(path.parentPath.isAssignmentExpression()&&path.parentKey==="left"){path.parentPath.get("right").replaceWith(createNoop());}else{//vara=console.logpath.replaceWith(createNoop());}}}}}};复制代码其中,isIncludedConsole、isIncludedConsoleBind、createNoop等方法需要额外开发引入。可以看出,与社区工具相比,GoGoCode的优势在于:上手难度低:无需熟知所有AST节点规范,无需了解遍历访问AST的各个阶段,无需额外工具,只需阅读简单的GoGoCode文档GoGoCode是唯一面向开发人员而非AST结构的AST处理工具。代码量极少:让你专注于分析和转换的核心逻辑,而不用花费大量时间在AST操作上。无论是匹配、修改还是构造节点,都非常简单,几行代码就可以搞定。可读性强:通过对比可以看出,基于GoGoCode编写的代码非常直观、易懂,也更容易长期维护。灵活性强:Babel插件和jscodeshift能做的,GoGoCode做起来更方便。除了js,GoGoCode还支持html和vue的处理,这是社区其他流行工具所不具备的。我们在原版GoGoCode的基础上,开发了自研框架Magix的升级包,包括78条简单规则和30条复杂规则转换,自动将Magix1代码(左)转换为Magix3代码(右),完善了框架升级效率我们尝试在Babel中写了20行左右的转换逻辑,花了将近200行代码才完成。俗话说,磨刀不误砍柴工。这里编写自动转换规则是磨刀,实现转换是砍柴。如果磨刀的时间和直接砍木头的时间接近,那么大家就会选择放弃磨刀。代码转换往往是为了解决我们团队和系统内部的具体问题,大多数情况下甚至是一次性的。我们磨刀的效率一定很高。最近在尝试将支付宝小程序代码转PC框架代码。团队中对AST了解不多的同学,一个小时就能快速上手,不到200行代码就完成了80%的js逻辑转换。可见无论是上手难度的降低,效率的提升还是代码量的减少,都是非常显着的。总结GoGoCode在代码大小、可读性和灵活性方面具有优势。我们将继续完善和增强该工具的稳健性和易用性。希望大家通过GoGoCode,能够理解和操作抽象语法树,从而完成代码解析和转换逻辑,更好的控制代码,实现一个代码,多个终端,更平滑的框架升级……同时,希望让更多相关领域的同学能够以最低的成本参与进来,贡献自己的力量,为行业生态提供更好的解决方案。除了上面提到的语法检查,一码多端,框架升级,还有很多场景需要分析转换代码分析页面或视图与异步请求关联分析模块复杂度分析模块依赖清洗无用代码埋点代码自动生成单测插桩文件生成自动修正代码问题...如果您需要分析和转换代码,如果您想快速实现Babel现有插件无法满足的需求,欢迎使用和共建GoGo代码。如果大家使用GoGoCode不方便解决或者出错,希望大家提一下QQ群:735216094钉钉群:34266233Github:https://github.com/thx/gogocode新项目求star支持o(_////▽////_)q官网:gogocode.ioplayground:play.gogocode.io/相关文章:阿里妈妈开发的新工具,解除批量修改项目代码之痛《GoGoCode实战》学习30个AST代码一口气更换小技巧作者:阿里妈妈前端快爆链接:https://juejin.cn/post/694566...来源:掘金版权归作者所有。商业转载请联系作者授权,非商业转载请注明出处。