JavaScript的語法解析與抽象語法樹

抽象語法樹(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()方法接收兩種類型的參數:字符串或NodeBuffer對象,它也能夠收附加的選項做爲參數。解析後返回結果即爲抽象語法樹(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,根節點的typeProgramtype也是全部節點都共有的,其餘的屬性依賴於節點的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子屬性即爲函數名。paramsbody分別爲函數的參數列表和函數體。

 

咱們再來看函數調用:

 

"expression": {

    "type": "CallExpression",

        "callee": {

        "type": "Identifier",

            "name": "myAwesomeFunction"

    },

    "arguments": []

}

對函數調用而言,即節點類型爲CallExpressioncallee指向被調用的函數。有了上面的瞭解,咱們能夠繼續完成咱們的程序以下:

 

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

相關文章
相關標籤/搜索