「代碼分析轉換」原本在前端開發中是一個比較小衆的技能樹,我所在的阿里媽媽前端技術團隊(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 代碼替換小訣竅