简介《代码分析转换》是前端开发中比较小众的技能树。在迁移架构的过程中,遇到了代码批量转换的问题,于是做了一些原理和工具的研究。最近发现社区里很多讨论这个的文章也引起了大家的关注,所以打算在这里多分享一些我们的经验。事实上,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(`
