JavaScript 的語法解析與抽象語法樹
抽象語法樹(Abstract Syntax Tree)也稱為AST語法樹,指的是源代碼語法所對應的樹狀結構。也就是說,對于一種具體編程語言下的源代碼,通過構建語法樹的形式將源代碼中的語句映射到樹中的每一個節點上。
JavaScript語法解析
什么是語法樹
可以通過一個簡單的例子來看語法樹具體長什么樣子。有如下代碼:
var AST = "is Tree";
我們可以發現,程序代碼本身可以被映射成為一棵語法樹,而通過操縱語法樹,我們能夠精準的獲得程序代碼中的某個節點。例如聲明語句,賦值語句,而這是用正則表達式所不能準確體現的地方。JavaScript的語法解析器 Espsrima 提供了一個 在線解析的工具 ,你可以借助于這個工具,將JavaScript代碼解析為一個JSON文件表示的樹狀結構。
有什么用
抽象語法樹的作用非常的多,比如編譯器、IDE、壓縮優化代碼等。在JavaScript中,雖然我們并不會常常與AST直接打交道,但卻也會經常的涉及到它。例如使用UglifyJS來壓縮代碼,實際這背后就是在對JavaScript的抽象語法樹進行操作。
在一些實際開發過程中,我們也會用到抽象語法樹,下面通過一個小栗子來看看怎么進行JavaScript的語法解析以及對節點的遍歷與操縱。
舉個栗子
小需求
我們將構建一個簡單的靜態分析器,它可以從命令行進行運行。它能夠識別下面幾部分內容:
- 已聲明但沒有被調用的函數
- 調用了未聲明的函數
- 被調用多次的函數 </ul>
現在我們已經知道了可以將代碼映射為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 Done
小結
是時候進行小結,本文主要介紹了如何對JavaScript代碼使用Esprima進行解析,解析后的結果是一棵符合SpiderMonkey解析API的語法樹,然后我們使用Estraverse進行遍歷,在遍歷過程中我們通過識別節點類型來判斷代碼位置。最后,如果你需要修改節點,你可以參考[3]。