JavaScript语法解析与抽象语法树(AST)----Espsrima的使用方法

 阅读约 9 分钟

前言

最近在分析微信小程序,需要统计以前代码中所使用过的wx.api。思路为对js文件进行语法分析,然后找出调用者为wx的成员表达式。

JavaScript语法解析

首先来看一下什么是抽象语法树。抽象语法树(Abstract Syntax Tree)也称为AST语法树,指的是源代码语法所对应的树状结构。也就是说,对于一种具体编程语言下的源代码,通过构建语法树的形式将源代码中的语句映射到树中的每一个节点上。
可以通过一个demo来看一下什么是AST。
js代码为

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

这段代码的AST长这样:

image

可以发现,代码被映射成了一颗语法树,有三个节点,不同语句映射成不同的节点。那么我们便可以通过操作语法树来准确的获得代码中的某个节点,对代码进行分析等。

Espsrima

Espsrima是一个较为成熟的JavaScript语法解析开源项目。使用Espsrima对js代码进行语法解析的步骤如下:

1. 准备环境

我们使用node来构建能够在命令行中运行的js代码。所以首先确保安装了node环境。
然后创建项目目录。

  • 新建一个文件夹,比如我新建一个‘espsrima’的文件夹。
  • 进入该文件夹

cd espsrima

  • 安装库接下来要用到的库
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

阅读 3.9k更新于 2018-03-31
推荐阅读
贝贝的前端
用户专栏

爱健身的前端狗

8 人关注
15 篇文章
专栏主页
目录