抽象語法樹(Abstract Syntax Tree)也稱爲AST語法樹,指的是源代碼語法所對應的樹狀結構。也就是說,對於一種具體編程語言下的源代碼,經過構建語法樹的形式將源代碼中的語句映射到樹中的每個節點上。javascript
JavaScript語法解析java
什麼是語法樹node
能夠經過一個簡單的例子來看語法樹具體長什麼樣子。有以下代碼:git
var AST = "is Tree";github
咱們能夠發現,程序代碼自己能夠被映射成爲一棵語法樹,而經過操縱語法樹,咱們可以精準的得到程序代碼中的某個節點。例如聲明語句,賦值語句,而這是用正則表達式所不能準確體現的地方。JavaScript的語法解析器Espsrima提供了一個在線解析的工具,你能夠藉助於這個工具,將JavaScript代碼解析爲一個JSON文件表示的樹狀結構。正則表達式
有什麼用express
抽象語法樹的做用很是的多,好比編譯器、IDE、壓縮優化代碼等。在JavaScript中,雖然咱們並不會經常與AST直接打交道,但卻也會常常的涉及到它。例如使用UglifyJS來壓縮代碼,實際這背後就是在對JavaScript的抽象語法樹進行操做。npm
在一些實際開發過程當中,咱們也會用到抽象語法樹,下面經過一個小栗子來看看怎麼進行JavaScript的語法解析以及對節點的遍歷與操縱。編程
舉個例子編程語言
小需求
咱們將構建一個簡單的靜態分析器,它能夠從命令行進行運行。它可以識別下面幾部份內容:
已聲明但沒有被調用的函數
調用了未聲明的函數
被調用屢次的函數
如今咱們已經知道了能夠將代碼映射爲AST進行語法解析,從而找到這些節點。可是,咱們仍然須要一個語法解析器才能順利的進行工做,在JavaScript的語法解析領域,一個流行的開源項目是Esprima,咱們能夠利用這個工具來完成任務。此外,咱們須要藉助Node來構建可以在命令行運行的JS代碼。b
完整代碼地址:
https://github.com/wwsun/awesome-javascript/tree/master/src/day05
準備工做
爲了可以完成後面的工做,你須要確保安裝了Node環境。首先建立項目的基本目錄結構,以及初始化NPM。
mkdir esprima-tutorial
cd esprima-tutorial
npm install esprima --save
在根目錄新建index.js文件,初試代碼以下:
var fs = require('fs'),
esprima = require('esprima');
function analyzeCode(code) {
// 1
}
// 2
if (process.argv.length < 3) {
console.log('Usage: index.js file.js');
process.exit(1);
}
// 3
var filename = process.argv[2];
console.log('Reading ' + filename);
var code = fs.readFileSync(filename);
analyzeCode(code);
console.log('Done');
在上面的代碼中:
函數analyzeCode用於執行主要的代碼分析工做,這裏咱們暫時預留下來這部分工做待後面去解決。
咱們須要確保用戶在命令行中指定了分析文件的具體位置,這能夠經過查看process.argv的長度來獲得。爲何?你能夠參考Node的官方文檔:
The first element will be ‘node’, the second element will be the name of the JavaScript file. The next elements will be any additional command line arguments.
獲取文件,並將文件傳入到analyzeCode函數中進行處理
解析代碼和遍歷AST
藉助Esprima解析代碼很是簡單,只要使用一個方法便可:
var ast = esprima.parse(code);
esprima.parse()方法接收兩種類型的參數:字符串或Node的Buffer對象,它也能夠收附加的選項做爲參數。解析後返回結果即爲抽象語法樹(AST),AST遵照Mozilla SpiderMonkey的解析器API。例如代碼:
6 * 7
解析後的結果爲:
{
"type": "Program",
"body": [
{
"type": "ExpressionStatement",
"expression": {
"type": "BinaryExpression",
"operator": "*",
"left": {
"type": "Literal",
"value": 6,
"raw": "6"
},
"right": {
"type": "Literal",
"value": 7,
"raw": "7"
}
}
}
]
}
咱們能夠發現每一個節點都有一個type,根節點的type爲Program。type也是全部節點都共有的,其餘的屬性依賴於節點的type。例如上面實例的程序中,咱們能夠發現根節點下面的子節點的類型爲EspressionStatement,依此類推。
爲了可以分析代碼,咱們須要對獲得的AST進行遍歷,咱們能夠藉助Estraverse進行節點的遍歷。執行以下命令進行安裝該NPM包:
npm install estraverse --save
基本用法以下:
function analyzeCode(code) {
var ast = esprima.parse(code);
estraverse.traverse(ast, {
enter: function (node) {
console.log(node.type);
}
});
}
上面的代碼會輸出遇到的語法樹上每一個節點的類型。
獲取分析數據
爲了完成需求,咱們須要遍歷語法樹,並統計每一個函數調用和聲明的次數。所以,咱們須要知道兩種節點類型。首先是函數聲明:
{
"type": "FunctionDeclaration",
"id": {
"type": "Identifier",
"name": "myAwesomeFunction"
},
"params": [
...
],
"body": {
"type": "BlockStatement",
"body": [
...
]
}
}
對函數聲明而言,其節點類型爲FunctionDeclaration,函數的標識符(即函數名)存放在id節點中,其中name子屬性即爲函數名。params和body分別爲函數的參數列表和函數體。
咱們再來看函數調用:
"expression": {
"type": "CallExpression",
"callee": {
"type": "Identifier",
"name": "myAwesomeFunction"
},
"arguments": []
}
對函數調用而言,即節點類型爲CallExpression,callee指向被調用的函數。有了上面的瞭解,咱們能夠繼續完成咱們的程序以下:
function analyzeCode(code) {
var ast = esprima.parse(code);
var functionsStats = {}; //1
var addStatsEntry = function (funcName) { //2
if (!functionsStats[funcName]) {
functionsStats[funcName] = { calls: 0, declarations: 0 };
}
};
// 3
estraverse.traverse(ast, {
enter: function (node) {
if (node.type === 'FunctionDeclaration') {
addStatsEntry(node.id.name); //4
functionsStats[node.id.name].declarations++;
} else if (node.type === 'CallExpression' && node.callee.type === 'Identifier') {
addStatsEntry(node.callee.name);
functionsStats[node.callee.name].calls++; //5
}
}
});
}
咱們建立了一個對象functionStats用來存放函數的調用和聲明的統計信息,函數名做爲key。
函數addStatsEntry用於實現存放統計信息。
遍歷AST
若是發現了函數聲明,增長一次函數聲明
若是發現了函數調用,增長一次函數調用
處理結果
最後進行結果的處理,咱們只須要遍歷查看functionStats中的信息就能夠獲得結果。建立結果處理函數以下:
function processResults(results) {
for (var name in results) {
if (results.hasOwnProperty(name)) {
var stats = results[name];
if (stats.declarations === 0) {
console.log('Function', name, 'undeclared');
} else if (stats.declarations > 1) {
console.log('Function', name, 'decalred multiple times');
} else if (stats.calls === 0) {
console.log('Function', name, 'declared but not called');
}
}
}
}
而後,在analyzeCode函數的末尾調用該函數便可,以下:
processResults(functionsStats);
測試
建立測試文件demo.js以下:
function declaredTwice() {
}
function main() {
undeclared();
}
function unused() {
}
function declaredTwice() {
}
main();
執行以下命令:
$ node index.js demo.js
你將獲得以下的處理結果:
Reading test.js
Function declaredTwice decalred multiple times
Function undeclared undeclared
Function unused declared but not called