1、基础扫盲
- Esprima 是一个用于对 JS 代码做词法或者语法分析的工具
- 体验网址
- 只支持js,不支持 flow 或者 typescript 格式
- 当前最新版本是4.0,主要提供两个API:
- parseScript 解析不包含 import 和 export 之类的js 代码
- parseModule 解析含 import 和 export 之类的js 代码
- 4.0 以下的版本仅支持 parse 方法,需自行判断是 script 还是 module
- 语法格式
esprima.parseScript(input, config, delegate)
esprima.parseModule(input, config, delegate)
input 代表原始 js 字符串
config 是如下的配置对象:
- delegate参数
// node 包含节点类型等信息,metadata 包含该节点位置等信息
function (node, metadata) {
console.log(node.type, metadata);
}
2、进阶
Esprima 是用来做词法和语法分析的,这需要对其解析之后的对象结构有清楚的了解,本节分析 Esprima 解析后生成的语法结构树。
总体结构
- 语法树的总体结构就两种
interface Program {
type: 'Program';
sourceType: 'script';
body: StatementListItem[];
}
interface Program {
type: 'Program';
sourceType: 'module';
body: ModuleItem[];
}
- StatementListItem && ModuleItem
其中 ModuleItem 只是比 StatementListItem 多了导入和导出两个module才会用到的类型,这两个类型用的少,所以只用关心 StatementListItem
type StatementListItem = Declaration | Statement;
type ModuleItem = ImportDeclaration | ExportDeclaration | StatementListItem;
- 从 StatementListItem 可看出其只包含 Declaration(变量声明) 和 Statement(执行语句)
枚举 Declaration
type Declaration = ClassDeclaration | FunctionDeclaration | VariableDeclaration;
枚举 Statement
type Statement = BlockStatement | BreakStatement | ContinueStatement |
DebuggerStatement | DoWhileStatement | EmptyStatement |
ExpressionStatement | ForStatement | ForInStatement |
ForOfStatement | FunctionDeclaration | IfStatement |
LabeledStatement | ReturnStatement | SwitchStatement |
ThrowStatement | TryStatement | VariableDeclaration |
WhileStatement | WithStatement;
其中 ExpressionStatement 比较复杂
interface ExpressionStatement {
type: 'ExpressionStatement';
expression: Expression;
directive?: string;
}
// Expression 类型
type Expression = ThisExpression | Identifier | Literal |
ArrayExpression | ObjectExpression | FunctionExpression | ArrowFunctionExpression | ClassExpression |
TaggedTemplateExpression | MemberExpression | Super | MetaProperty |
NewExpression | CallExpression | UpdateExpression | AwaitExpression | UnaryExpression |
BinaryExpression | LogicalExpression | ConditionalExpression |
YieldExpression | AssignmentExpression | SequenceExpression;
3、小结
Esprima 本质上将 js 代码解析成了两大部分:
- 3 种变量声明(函数、变量和类)
- 表达式
其中表达式又被分为了两大类:
- 关键字组成的 statement,如 IfStatement, ForStatement等,这里面的BlockStatement有些特殊,因为其body又是 StatementListItem,产生递归。
- 运算语句(赋值、计算之类的操作)组成的 ExpressionStatement
看个例子:
// 解析
var answer = 6 * 7;
if(true){answer =1}
// 结果
{
"type": "Program",
"sourceType": "script",
"body": [
{
"type": "VariableDeclaration",
"declarations": [
{
"type": "VariableDeclarator",
"id": {
"type": "Identifier",
"name": "answer"
},
"init": {
"type": "BinaryExpression",
"operator": "*",
"left": {
"type": "Literal",
"value": 6,
"raw": "6"
},
"right": {
"type": "Literal",
"value": 7,
"raw": "7"
}
}
}
],
"kind": "var"
},
{
"type": "IfStatement",
"test": {
"type": "Literal",
"value": true,
"raw": "true"
},
"consequent": {
"type": "BlockStatement",
"body": [
{
"type": "ExpressionStatement",
"expression": {
"type": "AssignmentExpression",
"operator": "=",
"left": {
"type": "Identifier",
"name": "answer"
},
"right": {
"type": "Literal",
"value": 1,
"raw": "1"
}
}
}
]
},
"alternate": null
}
]
}
4、应用案例
去除 console.log() 语句,主要利用了 delegate 的第二个参数获取 console.log() 语句的位置,然后做字符串拼接
const esprima = require('esprima');
// console.log(x) or console['error'](y)
function isConsoleCall(node) {
return (node.type === 'CallExpression') &&
(node.callee.type === 'MemberExpression') &&
(node.callee.object.type === 'Identifier') &&
(node.callee.object.name === 'console');
}
function removeCalls(source) {
const entries = [];
esprima.parseScript(source, {}, function (node, meta) {
if (isConsoleCall(node)) {
entries.push({
start: meta.start.offset,
end: meta.end.offset
});
}
});
entries.sort((a, b) => { return b.end - a.end }).forEach(n => {
source = source.slice(0, n.start) + source.slice(n.end);
});
return source;
}