关于javascript:最近很火的AST分析原理社区方案比较

6次阅读

共计 7534 个字符,预计需要花费 19 分钟才能阅读完成。

引言

“代码剖析转换”原本在前端开发中是一个比拟小众的技能树,我所在的阿里妈妈前端技术团队(MUX)也是在大量业务的迁徙架构的过程中遇到了须要批量转换代码的问题,所以对原理和工具进行了一些钻研,最近发现社区里不少对此的探讨的文章也失去了大家的关注,所以也打算在此多分享一些咱们的教训。
其实 AST 剖析的过程与每一位开发同学的工作都密不可分,小到一次 eslint 语法查看,大到框架的降级,都波及于此。简略、个别的转换能够通过人眼分别、手动批改,批量的简略转换能够通过正则匹配、字符串替换,但更简单的转换,基于 AST 是最无效的计划。

基于 AST 的代码剖析转换

AST 简要介绍

形象语法树(Abstract Syntax Tree)简称 AST,是以树状模式体现编程语言的语法结构,树上的每个节点都示意源代码中的一种构造。JavaScript 引擎工作工作的第一步就是将代码解析为 AST,Babel、eslint、prettier 等工具都基于 AST。
将源代码解析为 AST 分为两步,词法剖析和语法分析
1. 词法剖析,将字符序列转为单词(Token)序列的过程。

2. 语法分析,将 Token 序列组合成各类语法短语,如 Program,Statement,Expression 等

基于 AST 进行剖析转换的劣势

AST 会疏忽代码格调,将代码解析为最纯正的语法树,因而基于 AST 进行的转换是更加精确谨严的,而应用正则表达式来剖析转换代码,无奈无效的剖析一段代码的上下文,即便对简略规定的匹配都须要思考太多边界状况,兼容各种代码格调。
举个简略的例子,将所有由 var 定义的变量转为 let 定义,基于 AST 能够很容易的实现,而应用正则须要思考各种状况,书写的表达式也难以调试、读懂。

基于 AST 转换的过程

  1. parse 源代码通过词法剖析、语法分析解析为 AST
  2. transform 对 AST 进行剖析变换
  3. generate 将最终的 AST 输入为代码

其中 1、3 在社区都有成熟的工具,能够拿来即用。而第 2 步须要开发者本人操作 AST,社区上有风行的工具,但存在肯定问题。

社区风行计划现状及问题

社区风行的计划有 Babel、jscodeshift 以及 esprima、recast、acorn、estraverse 等,本文抉择最具代表性的 Babel 和 jscodeshift 来剖析。

Babel 插件计划的分析

没有 Babel 就没有 JS 社区明天在语言标准上的高度凋敝,babel/parser 也是十分优良的解析器。很多开发者进行代码剖析转换时都离不开 Babel 插件,然而我集体认为 Babel 插件目前的编写计划上存在几个问题 1. 上手难度高,学习老本高 2. 匹配、生成节点的逻辑简单,代码量大 3. 代码可读性差,不利于保护。具体而言:

1. 上手难度、学习老本较高

上手 Babel 插件开发之前须要深刻理解 AST 标准,AST 节点的类型和属性。参考 babel-types 和 babel node type,200 多个节点类型。babelrc 的配置、babel plugin 的编写形式是根底,除此之外还要理解 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') {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;
  }
}
复制代码
  • 应用 Babel 结构 ’var varName = require(“moduleName”)’,写法如下
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')]
    )
  ),
]);
复制代码

3. 代码可读性差,不利于保护

看了下面两段例子,能够发现不仅代码量大,可读性也不够好,即便对 AST 和 Babel 十分相熟,也须要认真逐句进行了解。

jscodeshift 的分析

相比于 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

概述

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()
复制代码

办法重载:.attr()

既能够获取也能够批改节点属性,比手动遍历,层层判断来操作属性、节点敌对很多

$(code).attr('id.name')  // 返回该节点 id 属性中的 name 属性值

$(code).attr('declarations.0.id.name', 'c') // 批改 name 属性值
复制代码

简略的 replace

比通过正则进行 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

获取节点 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  做不同的解决

  1. console.log 的调用删除
  2. console.log() 作为变量初始值时转换为 void 0 
  3. 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 的劣势是:

  1. 上手难度低:不须要对所有 AST 节点标准一目了然,不须要晓得遍历、拜访 AST 的各种阶段,不须要额定的工具,只需浏览简略的 GoGoCode 文档。GoGoCode 是惟一面向开发者而不是面向 AST 构造的 AST 解决工具。
  2. 代码量非常少:让你专一剖析与转换的外围逻辑,不在 AST 的操作上消耗大量工夫。无论是匹配、批改还是结构节点都很简略,几行代码就能搞定。
  3. 可读性很强:比照能够看出,基于 GoGoCode 写进去的代码很直观,很容易看懂,也更便于短暂的保护。
  4. 灵活性强:应用 Babel 插件和 jscodeshift 能做到的,GoGoCode 都能够更便捷的做到。除了 js 之外,GoGoCode 同时反对对 html 的解决,对 vue 的解决,这是社区其余风行工具所不具备的。

应用成果

基于 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:https://github.com/thx/gogocode 新我的项目求 star 反对 o(_////▽////_)q
官网:gogocode.io
playground:play.gogocode.io/
相干文章:
阿里妈妈出的新工具,给批量批改我的项目代码加重了苦楚
「GoGoCode 实战」一口气学会 30 个 AST 代码替换小窍门

作者:阿里妈妈前端快爆
链接:https://juejin.cn/post/694566…
起源:掘金
著作权归作者所有。商业转载请分割作者取得受权,非商业转载请注明出处。

正文完
 0