JavaScript語法解析與抽象語法樹(AST)----Espsrima的使用方法

前言

最近在分析微信小程序,須要統計之前代碼中所使用過的wx.api。思路爲對js文件進行語法分析,而後找出調用者爲wx的成員表達式。node

JavaScript語法解析

首先來看一下什麼是抽象語法樹。抽象語法樹(Abstract Syntax Tree)也稱爲AST語法樹,指的是源代碼語法所對應的樹狀結構。也就是說,對於一種具體編程語言下的源代碼,經過構建語法樹的形式將源代碼中的語句映射到樹中的每個節點上。
能夠經過一個demo來看一下什麼是AST。
js代碼爲express

var a = 1;
function main() {
    console.log('this is a function');
}
main();

這段代碼的AST長這樣: npm

image

能夠發現,代碼被映射成了一顆語法樹,有三個節點,不一樣語句映射成不一樣的節點。那麼咱們即可以經過操做語法樹來準確的得到代碼中的某個節點,對代碼進行分析等。編程

Espsrima

Espsrima是一個較爲成熟的JavaScript語法解析開源項目。使用Espsrima對js代碼進行語法解析的步驟以下:小程序

1. 準備環境

咱們使用node來構建可以在命令行中運行的js代碼。因此首先確保安裝了node環境。
而後建立項目目錄。微信小程序

  • 新建一個文件夾,好比我新建一個‘espsrima’的文件夾。
  • 進入該文件夾

cd espsrimaapi

  • 安裝庫接下來要用到的庫
npm install esprima --save  
npm install estraverse --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');

2. 解析js代碼並分析數據

esprima 將代碼解析爲語法樹,Estraverse則用來對節點進行遍歷。咱們仍以這段代碼爲例微信

var a = 1;
function main() {}
wx.laod();

咱們看下這段代碼節點遍歷的結果,結果太長截圖其中一部分:
image編程語言

從圖中看出,type表明節點類型,如函數聲明FunctionDeclaration和函數調用 CallExpression。咱們的目的是找出全部的wx.xxx的函數,因此咱們主要關注函數調用類型。咱們來看基本的函數調用代碼:函數

"expression": {  
    "type": "CallExpression",  
    "callee": {  
        "type": "Identifier",  
        "name": "myAwesomeFunction"  
    },  
    "arguments": []  
}

對函數調用而言,即節點類型爲 CallExpression,callee指向被調用函數。
咱們再來看下形如 'wx.xxx'的代碼

CallExpression: {
  type: 'CallExpression',
  callee: 
   StaticMemberExpression {
     type: 'MemberExpression',
     computed: false,
     object: Identifier { type: 'Identifier', name: 'wx' },
     property: Identifier { type: 'Identifier', name: 'laod' } },
  arguments: [] 
    
}

能夠看到,節點類型仍然爲 CallExpression,callee的類型爲 MemberExpression, callee的object name爲wx。可依此提出wx.xxx。

3. 輔助函數

首先須要一個對象functionsStats,用來存儲提取的api。其次,須要一個'對象去重函數addStatsEntry'.最後須要一個函數對functionsStats進行處理,將結果寫到本地。 通過以上的分析,最終的代碼以下:

var fs = require('fs'),  
    esprima = require('esprima'),
    estraverse = require('estraverse');

function addStatsEntry(funcName) {
    if (!functionsStats[funcName]) {  
        functionsStats[funcName] = { calls: 0 };  
    } 
}
// 寫文件
function writeResult(str) {
    fs.writeFileSync('result.txt', str, 'utf8', (err) => {
        if (err) throw err;
        console.log('It\'s saved!');
    });
}

// 結果處理函數
function processResults(results) {  
    var str = '';
    for (var name in results) {  
        if (results.hasOwnProperty(name)) {  
            var stats = results[name];
            var apiName = 'wx.'+ name;
            console.log('name:', apiName);
            str += apiName+ '\n';
        }  
    }  
    writeResult(str);
} 

function analyzeCode(code) {  
    var ast = esprima.parse(code);  
    var functionsStats = {}; //1  
  
    estraverse.traverse(ast, {  
        enter: function (node) { 
            if (node.type === 'CallExpression' && node.callee.type === 'MemberExpression') {  
                if(node.callee.object.name === 'wx') {
                    addStatsEntry(node.callee.property.name);
                    functionsStats[node.callee.property.name].calls++;
                }
            }  
        }  
    }); 
    processResults(functionsStats);  
}   

 
if (process.argv.length < 3) {  
    console.log('Usage: index.js file.js');  
    process.exit(1);  
}  
  
var filename = process.argv[2];  
console.log('Reading ' + filename);  
var code = fs.readFileSync(filename, { encoding: 'utf8' });  
  
analyzeCode(code);

能夠看到相似如下的處理結果

image

相關文章
相關標籤/搜索