最近在分析微信小程序,須要統計之前代碼中所使用過的wx.api。思路爲對js文件進行語法分析,而後找出調用者爲wx的成員表達式。node
首先來看一下什麼是抽象語法樹。抽象語法樹(Abstract Syntax Tree)也稱爲AST語法樹,指的是源代碼語法所對應的樹狀結構。也就是說,對於一種具體編程語言下的源代碼,經過構建語法樹的形式將源代碼中的語句映射到樹中的每個節點上。
能夠經過一個demo來看一下什麼是AST。
js代碼爲express
var a = 1; function main() { console.log('this is a function'); } main();
這段代碼的AST長這樣: npm
能夠發現,代碼被映射成了一顆語法樹,有三個節點,不一樣語句映射成不一樣的節點。那麼咱們即可以經過操做語法樹來準確的得到代碼中的某個節點,對代碼進行分析等。編程
Espsrima是一個較爲成熟的JavaScript語法解析開源項目。使用Espsrima對js代碼進行語法解析的步驟以下:小程序
咱們使用node來構建可以在命令行中運行的js代碼。因此首先確保安裝了node環境。
而後建立項目目錄。微信小程序
cd espsrima
。api
npm install esprima --save npm install estraverse --save
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');
esprima 將代碼解析爲語法樹,Estraverse則用來對節點進行遍歷。咱們仍以這段代碼爲例微信
var a = 1; function main() {} wx.laod();
咱們看下這段代碼節點遍歷的結果,結果太長截圖其中一部分:
編程語言
從圖中看出,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。
首先須要一個對象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);
能夠看到相似如下的處理結果