Loading...
Loading...
Published on 2026-05-21
系统讲解AST基本结构与遍历、Babel插件开发、ESLint规则编写、Source Map VLQ编码及代码覆盖率实现原理

AST(抽象语法树)的基本结构与 Babel 插件开发流程;ESLint 规则的自定义编写;Source Map 的原理(VLQ 编码、mappings 字段解析);代码覆盖率的实现原理(Istanbul 的插桩机制)。
抽象语法树(Abstract Syntax Tree)是源代码的结构化树形表示,去除了语法噪声(括号、分号等),保留语义结构。
// 源代码
const add = (a, b) => a + b;
// 对应的 AST(简化版)
{
"type": "Program",
"body": [{
"type": "VariableDeclaration",
"kind": "const",
"declarations": [{
"type": "VariableDeclarator",
"id": { "type": "Identifier", "name": "add" },
"init": {
"type": "ArrowFunctionExpression",
"params": [
{ "type": "Identifier", "name": "a" },
{ "type": "Identifier", "name": "b" }
],
"body": {
"type": "BinaryExpression",
"operator": "+",
"left": { "type": "Identifier", "name": "a" },
"right": { "type": "Identifier", "name": "b" }
}
}
}]
}]
}
源代码 → [Parser] → AST → [Transformer] → 新 AST → [Generator] → 目标代码
↑ ↑ ↑ ↑
@babel/parser @babel/traverse 插件逻辑 @babel/generator
访问 astexplorer.net 可实时查看任意代码的 AST 结构,是开发 Babel 插件的必备工具。
// 各类节点示意
const nodeTypes = {
// 声明
VariableDeclaration: { kind: 'const|let|var', declarations: [] },
FunctionDeclaration: { id, params, body },
ClassDeclaration: { id, superClass, body },
// 表达式
CallExpression: { callee, arguments: [] },
MemberExpression: { object, property, computed },
BinaryExpression: { operator, left, right },
AssignmentExpression:{ operator, left, right },
ArrowFunctionExpression: { params, body, expression },
// 语句
IfStatement: { test, consequent, alternate },
ReturnStatement: { argument },
ExpressionStatement: { expression },
// 字面量
Identifier: { name },
StringLiteral: { value },
NumericLiteral:{ value },
BooleanLiteral:{ value },
NullLiteral: {},
};
// my-babel-plugin.js
module.exports = function(babel) {
const { types: t } = babel; // t 是 @babel/types 工具库
return {
name: 'my-babel-plugin', // 插件名称(调试用)
pre(state) {
// 插件初始化(每个文件处理前)
},
visitor: {
// visitor 模式:声明对哪些节点感兴趣
// key 是节点类型,value 是访问函数
// 进入节点时调用
FunctionDeclaration(path, state) {
// path:节点路径(包含节点本身及其上下文)
// state:当前文件状态(含插件选项)
},
// enter/exit 两个时机
CallExpression: {
enter(path) { /* 进入时 */ },
exit(path) { /* 离开时 */ },
},
},
post(state) {
// 插件收尾(每个文件处理后)
},
};
};
visitor: {
Identifier(path) {
// 节点信息
path.node // 当前 AST 节点
path.parent // 父节点
path.parentPath // 父节点路径
path.scope // 作用域信息
// 遍历
path.get('name') // 获取子路径
path.findParent(p => ...) // 向上查找祖先
path.getSibling(index) // 兄弟节点
// 判断
path.isIdentifier() // 等同 t.isIdentifier(path.node)
path.isReferenced() // 是否被引用
path.inScope('varName') // 变量是否在作用域中
// 修改
path.replaceWith(newNode) // 替换为新节点
path.replaceWithMultiple([n1, n2]) // 替换为多个节点
path.insertBefore(node) // 在前插入
path.insertAfter(node) // 在后插入
path.remove() // 删除节点
// 遍历控制
path.skip() // 跳过子树
path.stop() // 停止整个遍历
}
}
// 目标:
// 输入:console.log('hello')
// 输出:console.log('[functionName]', 'hello')
module.exports = function({ types: t }) {
// 获取函数名的工具函数
function getFunctionName(path) {
let current = path.parentPath;
while (current) {
if (current.isFunctionDeclaration()) {
return current.node.id?.name ?? 'anonymous';
}
if (current.isFunctionExpression() || current.isArrowFunctionExpression()) {
// 赋值表达式:const foo = () => {}
const parent = current.parentPath;
if (parent.isVariableDeclarator()) {
return parent.node.id?.name ?? 'anonymous';
}
// 对象属性:{ foo: () => {} }
if (parent.isObjectProperty()) {
return parent.node.key?.name ?? 'anonymous';
}
return 'anonymous';
}
if (current.isClassMethod()) {
return current..?. ?? ;
}
current = current.;
}
;
}
{
: ,
: {
(path) {
{ node } = path;
callee = node.;
isConsoleCall =
t.(callee) &&
t.(callee., { : }) &&
t.(callee.) &&
[, , , ].(callee..);
(!isConsoleCall) ;
firstArg = node.[];
(t.(firstArg) && firstArg..()) ;
fnName = (path);
label = t.();
node..(label);
}
}
};
};
() {
.(, user);
}
= () => {
.();
};
// eslint-plugin-custom/rules/no-var-declaration.js
module.exports = {
meta: {
type: 'suggestion',
docs: {
description: '禁止使用 var 声明,请使用 let 或 const',
category: 'Best Practices',
recommended: true,
},
fixable: 'code', // 支持 --fix 自动修复
schema: [], // 无选项
messages: {
noVar: '请使用 "{{ preferred }}" 替代 "var"',
},
},
create(context) {
return {
VariableDeclaration(node) {
if (node.kind !== 'var') return;
// 判断变量是否被重新赋值(决定用 let 还是 const)
const isReassigned = node.declarations.some((decl) => {
const scope = context.getScope();
const variable = scope.variables.find(v => v.name === decl.id.name);
return variable?.references.some( => ref.() && ref. !== decl.);
});
preferred = isReassigned ? : ;
context.({
node,
: ,
: { preferred },
() {
fixer.(
{ : [node.[], node.[] + ] },
preferred
);
},
});
}
};
}
};
{ } = ();
rule = ();
tester = ({ : { : } });
tester.(, rule, {
: [
,
,
],
: [
{
: ,
: [{ : , : { : } }],
: ,
},
{
: ,
: [{ : , : { : } }],
: ,
},
],
});
{
"version": 3,
"file": "bundle.js",
"sourceRoot": "",
"sources": ["src/app.js", "src/utils.js"],
"sourcesContent": ["...", "..."],
"names": ["add", "a", "b"],
"mappings": "AAAA,SAAS,GAAGA,CAAC,CAACC,CAAD,CAACC,CAAD"
}
mappings 是分号和逗号分隔的 Base64 VLQ 编码序列:
;:分隔行(输出文件中的换行),:分隔同一行中的不同映射[生成列, 源文件索引, 源行, 源列, 名称索引]// VLQ 解码实现
const BASE64_MAP = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
function decodeVLQ(str) {
const result = [];
let i = 0;
while (i < str.length) {
let value = 0, shift = 0, digit;
do {
digit = BASE64_MAP.indexOf(str[i++]);
value |= (digit & 0x1F) << shift;
shift += 5;
} while (digit & 0x20); // 继续位(第6位)
// 最低位是符号位(VLQ signed)
result.push(value & 1 ? -(value >> 1) : value >> 1);
}
return result;
}
// 解析 mappings
function parseMappings(mappings) {
const lines = mappings.split(';');
const result = [];
for (let line = 0; line < lines.length; line++) {
const segments = lines[line].split(',').filter(Boolean);
let prevCol = 0, prevSrc = 0, prevSrcLine = , prevSrcCol = ;
( seg segments) {
[genCol, srcIdx, srcLine, srcCol] = (seg).( {
(i === ) { prevCol += v; prevCol; }
(i === ) { prevSrc += v; prevSrc; }
(i === ) { prevSrcLine += v; prevSrcLine; }
(i === ) { prevSrcCol += v; prevSrcCol; }
});
result.({ : line, genCol, srcIdx, srcLine, srcCol });
}
}
result;
}
Istanbul(现为 nyc/c8)通过 AST 转换在代码中插入计数器:
// 原始代码
function add(a, b) {
if (a > 0) {
return a + b;
}
return b;
}
// 插桩后(伪代码示意)
const __cov = global.__coverage__['src/add.js'] = {
s: { 0: 0, 1: 0, 2: 0 }, // 语句计数
b: { 0: [0, 0] }, // 分支计数 [true分支, false分支]
f: { 0: 0 }, // 函数计数
};
function add(a, b) {
__cov.f[0]++; // 函数被调用
__cov.s[0]++; // 语句 0 执行
if (a > 0) {
__cov.b[0][0]++; // 分支 true
__cov.s[1]++;
return a + b;
}
__cov.b[0][1]++; // 分支 false
__cov.s[2]++;
return b;
}
语句覆盖率 (Statement) = 执行过的语句数 / 总语句数
分支覆盖率 (Branch) = 执行过的分支数 / 总分支数(if/else/三元各算两个)
函数覆盖率 (Function) = 调用过的函数数 / 总函数数
行覆盖率 (Line) = 执行过的行数 / 总行数
Q1:Babel 的三阶段(Parse → Transform → Generate)各做什么?
@babel/parser(原 babylon)将源码解析为 AST,支持 TypeScript、JSX 等语法扩展。@babel/generator 将修改后的 AST 转换回代码字符串,同时生成 Source Map。Q2:Source Map 中 mappings 字段为何使用 VLQ 编码而非直接存偏移量?
VLQ(Variable-length quantity,可变长度量)的好处:
Q3:ESLint 规则的 fixable 和 hasSuggestions 有何区别?
fixable: 'code':可通过 --fix 自动修复,无需用户确认,适合确定性修改(如 var → const)。
hasSuggestions: true:提供建议(suggestions),用户在 IDE 中手动选择应用,适合可能有多种修复方案或影响语义的情况。建议通过 context.report({ suggest: [...] }) 提供。
Q4:Istanbul 的插桩会影响运行时性能吗?
会,但通常可接受:
--coverage),零插桩,性能损耗更小