“代码分析转换”原本在前端开发中是一个比较小众的技能树,我所在的阿里妈妈前端技术团队(MUX)也是在大量业务的迁移架构的过程当中遇到了须要批量转换代码的问题,因此对原理和工具进行了一些研究,最近发现社区里很多对此的讨论的文章也获得了你们的关注,因此也打算在此多分享一些咱们的经验。
其实AST分析的过程与每一位开发同窗的工做都密不可分,小到一次eslint语法检查,大到框架的升级,都涉及于此。简单、个别的转换能够经过人眼辨别、手动修改,批量的简单转换能够经过正则匹配、字符串替换,但更复杂的转换,基于AST是最有效的方案。
javascript
抽象语法树(Abstract Syntax Tree)简称 AST ,是以树状形式表现编程语言的语法结构,树上的每一个节点都表示源代码中的一种结构。JavaScript引擎工做工做的第一步就是将代码解析为AST,Babel、eslint、prettier等工具都基于AST。
将源代码解析为AST分为两步,词法分析和语法分析
1.词法分析,将字符序列转为单词(Token)序列的过程。
2.语法分析,将Token序列组合成各种语法短语,如Program,Statement,Expression等
html
AST会忽略代码风格,将代码解析为最纯粹的语法树,所以基于AST进行的转换是更加准确严谨的,而使用正则表达式来分析转换代码,没法有效的分析一段代码的上下文,即便对简单规则的匹配都须要考虑太多边界状况,兼容各类代码风格。
举个简单的例子,将全部由var定义的变量转为let定义,基于AST能够很容易的完成,而使用正则须要考虑各类状况,书写的表达式也难以调试、读懂。前端
其中一、3在社区都有成熟的工具,能够拿来即用。而第2步须要开发者本身操做AST,社区上有流行的工具,但存在必定问题。
vue
社区流行的方案有Babel、jscodeshift以及esprima、recast、acorn、estraverse等,本文选择最具表明性的Babel和jscodeshift来分析。java
没有Babel就没有JS社区今天在语言规范上的高度繁荣,babel/parser也是很是优秀的解析器。不少开发者进行代码分析转换时都离不开Babel插件,可是我我的认为Babel插件目前的编写方案上存在几个问题 1.上手难度高,学习成本高 2.匹配、生成节点的逻辑复杂,代码量大 3.代码可读性差,不利于维护。具体而言:
node
上手Babel插件开发以前须要深刻了解AST规范,AST节点的类型和属性。参考 babel-types和babel node type,200多个节点类型。babelrc的配置、babel plugin的编写方式是基础,除此以外还要了解visitor、scope、state、excit、enter等概念、babel-types、babel-traverse、builder等工具。
git
匹配节点,须要层层、逐个对比节点类型和属性,若是须要肯定上下文信息会更加复杂。构造节点一样须要严格按照类型和结构来进行。须要在AST的操做上耗费大量时间,没法专一于分析与转换的核心逻辑。
github
MemberExpression(path) {
if (path.node.object.name == 'self' && path.node.property.name == 'doEdit') {
const firstCallExpression = path.findParent(path => path.isCallExpression());
if (!firstCallExpression) {
return;
}
if (!firstCallExpression.node.arguments[0]) {
return;
}
let secondCallExpression = null
if (firstCallExpression.node.arguments[0].type == 'StringLiteral'
&& firstCallExpression.node.arguments[0].value == 'price') {
secondCallExpression = firstCallExpression.findParent(
path => path.isCallExpression()
)
}
if (!secondCallExpression) {
return;
}
if (secondCallExpression.node.arguments.length != 2
|| secondCallExpression.node.arguments[0].type != 'ThisExpression') {
return;
}
const pId = secondCallExpression.node.arguments[0].value;
}
}
复制代码
types.variableDeclaration('var', [
types.variableDeclarator(
//t.variableDeclarator(id, init)
//id就是identifier
//此处的init必须是一个Expression
types.identifier('varName'),
//t.callExpression(callee, arguments)
types.callExpression(
types.identifier('require'),
[types.stringLiteral('moduleName')]
)
),
]);
复制代码
看了上面两段例子,能够发现不只代码量大,可读性也不够好,即便对AST和Babel很是熟悉,也须要仔细逐句进行理解。
正则表达式
相比于Babel而言,jscodeshift的优点是匹配节点更简便一些,链式操做用起来更加顺手。
匹配self.doEdit('price')(this, '100'),写法以下编程
const callExpressions = root.find(j.CallExpression, {
callee: {
callee: {
object: {
name: 'self'
},
property: {
name: 'doEdit'
}
},
arguments: [{
value: 'price'
}]
},
arguments: [{
type: 'ThisExpression'
}, {
value: '100'
}]
})
复制代码
转换和构造节点的方式与Babel写法相似,再也不赘述。能够看出jscodeshift也没有很好的解决上文提到的三个问题。
因而在社区宝贵的经验之上,咱们开发了新的工具GoGoCode。目的就是让开发者可以最高效率最低成本的完成代码分析转换。
GoGoCode是一个操做AST的工具,能够下降使用AST的门槛,帮助开发者从繁琐的AST操做中解放出来,更专一于代码分析转换逻辑的开发。简单的替换甚至不用学习AST,而初步学习了AST节点结构(可参考AST查看器)后就能够完成更复杂的分析转换。
GoGoCode借鉴了JQuery的思想,咱们的使命也是让代码转换像使用JQuery同样简单。JQuery在原生js的基础上大大便利了DOM操做的效率,没有复杂的配置流程,能够拿来即用,并且有不少优秀的设计思想值得借鉴:好比$()实例化、选择器思想、链式操做等。除此以外,咱们将简单的replace的思想应用在AST中,效果也很不错。
使用$(),源代码和AST节点均可以被实例化为AST对象,能够链式调用实例上挂载的任意函数
$(code: string)
$('var a = 1')
$(node: ASTNode)
$({ type: 'Identifier', name: 'a' }).generate()
复制代码
DOM树和AST树都是树结构,JQuery能够用各类选择器匹配节点,AST是否是也能够经过简单的选择器来匹配真实的节点呢?因而咱们定义了“代码选择器”
不管你想找什么样的代码,均可以经过代码选择器直接匹配到
$(code).find('import a from "./a"')
$(code).find('function a(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 $_$1 from $_$2')
.find('console.log()')
.remove()
.root()
.generate()
复制代码
既能够获取也能够修改节点属性,比手动遍历,层层判断来操做属性、节点友好不少
$(code).attr('id.name') // 返回该节点id属性中的name属性值
$(code).attr('declarations.0.id.name', 'c') // 修改name属性值
复制代码
比经过正则进行replace更简单、更强大、更好用。$_$n相似于正则中的捕获组,$$$相似于rest参数
$(code).replace('{ text: $_$1, value: $_$2, $$$ }', '{ name: $_$1, id: $_$2, $$$ }')
$(code).replace(`import { $$$ } from "@alifd/next"`, `import { $$$ } from "antd"`)
$(code).replace(`<View $$$1>$$$2</View>`,`<div $$$1>$$$2</div>`)
$(code).replace(`Page({ $$$1 })`,
`Page({ init() { this.data = {} }, $$$1 })`
)
复制代码
基础api | 获取节点api | 操做节点 |
---|---|---|
$() | .find() | .attr() |
$.loadFile | .parent() | .replace() |
.generate() | .parents() | .replaceBy() |
.siblings() | .after() | |
.next() | .before() | |
.nextAll() | .append() | |
.prev() | .prepend() | |
.prevAll() | .empty() | |
.root() | .remove() | |
.eq() | .clone() | |
.each() |
前文的例子中,匹配 self.doEdit('price')(this, '100')
语句 ,使用GoGoCode写法以下
$(code).find(`self.doEdit('price')(this, '100')`)
复制代码
构造'var varName = require("moduleName")'
,使用GoGoCode写法以下
$('var varName = require("moduleName")')
复制代码
以一个完整的例子将GoGoCode和Babel插件进行对比:
对于如下这段代码,咱们但愿对不一样的 console.log
作不一样的处理
console.log
的调用删除console.log()
做为变量初始值时转换为 void 0
console.log
做为变量初始值时转换为空方法代码经转换的结果以下:
使用GoGoCode实现的代码以下:
$(code)
.replace(`var $_$ = console.log()`, `var $_$ = void 0`)
.replace(`var $_$ = console.log`, `var $_$ = function(){}`)
.find(`console.log()`)
.remove()
.generate();
复制代码
使用Babel实现的核心代码以下:
// 代码来源:https://zhuanlan.zhihu.com/p/32189701
module.exports = function({ types: t }) {
return {
name: "transform-remove-console",
visitor: {
CallExpression(path, state) {
const callee = path.get("callee");
if (!callee.isMemberExpression()) return;
if (isIncludedConsole(callee, state.opts.exclude)) {
// console.log()
if (path.parentPath.isExpressionStatement()) {
path.remove();
} else {
//var a = console.log()
path.replaceWith(createVoid0());
}
} else if (isIncludedConsoleBind(callee, state.opts.exclude)) {
// console.log.bind()
path.replaceWith(createNoop());
}
},
MemberExpression: {
exit(path, state) {
if (
isIncludedConsole(path, state.opts.exclude) &&
!path.parentPath.isMemberExpression()
) {
//console.log = func
if (
path.parentPath.isAssignmentExpression() &&
path.parentKey === "left"
) {
path.parentPath.get("right").replaceWith(createNoop());
} else {
//var a = console.log
path.replaceWith(createNoop());
}
}
}
}
}
};
复制代码
其中 isIncludedConsole、isIncludedConsoleBind、createNoop 等方法还需额外开发引入
能够看出,与社区工具对比,GoGoCode的优点是:
基于GoGoCode第一版本咱们开发了妈妈自研框架Magix的升级套件,包含78个简单规则、30个复杂规则的转换,自动将Magix1代码(左)转换为Magix3代码(右),提高了框架升级效率
其中一个20行左右的转换逻辑咱们曾尝试用Babel写,近200行代码才完成。
俗话说,磨刀不误砍柴工,在这里编写自动化转换规则是磨刀,实施转换是砍柴。若是磨刀的时间接近直接砍柴的时间,那你们会选择放弃磨刀。代码转换常常是解决咱们团队、系统内的特定问题,多数状况下甚至是一次性的,(不能像ES6转ES5那样经过大规模的应用一套通用规则来分摊掉插件开发的成本)这就要求咱们磨刀的效率必须高。
近期咱们在进行支付宝小程序代码转PC框架代码的尝试,团队内对AST了解很少的同窗经一小时就能够快速上手,不到200行代码就完成了80%js逻辑的转换。可见不管是上手难度下降、效率提高仍是代码量减小都是很显著的。
GoGoCode在代码量、可读性、灵活性方面都具备优点,咱们会继续打磨,增强工具健壮性和易用性。但愿经过GoGoCode人人都能理解并操纵抽象语法树,从而完成代码分析转换逻辑,更好的掌控代码,实现一码多端、更顺畅的框架升级......同时但愿在相关领域让更多同窗可以最低成本的参与进来贡献本身的力量,给业界生态提供更好的解决方案。
除了前文提到的语法检查、一码多端、框架升级以外,还有不少场景须要分析和转换代码
若是你须要分析、转换代码,若是你想快速实现Babel现有插件不能知足的需求,欢迎使用和共建GoGoCode。
若是你用 GoGoCode 不方便解决或者出了错,但愿你能提给咱们
QQ群:735216094 钉钉群:34266233
Github:github.com/thx/gogocod… 新项目求 star 支持 o(////▽////)q
官网:gogocode.io
playground:play.gogocode.io/
相关文章:
阿里妈妈出的新工具,给批量修改项目代码减轻了痛苦
「GoGoCode 实战」一口气学会 30 个 AST 代码替换小诀窍