基于 AST 的静态分析工具,用于检测 JavaScript 代码中的死代码(未使用的变量和函数)。
- ✅ 准确的作用域分析:正确处理函数作用域、变量提升、嵌套函数
- ✅ 三级分类:
- 🟢 使用中:被引用的变量/函数
- 🟡 可疑:顶层声明但未引用(可能被其他文件使用)
- 🔴 死代码:嵌套声明且未引用(可安全删除)
- ✅ 未声明引用检测:识别未声明就使用的变量/函数(避免运行时错误)
- ✅ 导出识别:自动识别
window.xxx = yyy、cap.xxx = yyy等导出模式 - ✅ 完整的引用追踪:记录每个变量的所有引用位置
- ✅ 可视化界面:清晰展示分析结果,包含代码位置和作用域路径
npm installnode index.js <你的js文件路径>示例:
# 分析示例文件
node index.js examples/test.js
# 分析你自己的文件
node index.js path/to/your-file.js这会在相同目录生成对应的 .json 文件(AST)。
打开 analyzer.html,找到第 1101 行左右:
const response = await fetch('./examples/test.json'); // 修改为你的 JSON 文件路径在浏览器中打开 analyzer.html,查看分析结果。
Stage 1: 构建符号表和作用域链
↓
Stage 2: 收集引用关系
↓
Stage 3: 死代码检测
- 遍历 AST,识别所有声明(
var/let/const/function) - 为每个函数创建独立的作用域
- 构建作用域树(父子关系链)
- 处理 JavaScript 特性:
var变量提升和重复声明合并- 函数表达式的匿名作用域
- 参数声明
- 关联 AST 节点和作用域(用于引用阶段匹配)
关键数据结构:
// 符号表
symbolTable = Map {
"0_x_10" => {
name: "x",
type: "variable",
scope: 0,
scopePath: "global",
references: []
}
}
// 作用域树
scopeTree = {
id: 0,
type: "global",
declarations: Map { "x" => "0_x_10" },
children: [/* 子作用域 */],
parent: null
}- 再次遍历 AST,识别所有标识符引用
- 使用作用域链解析引用(
findSymbolInScope) - 向上查找作用域,模拟 JavaScript 的标识符解析
- 记录每个引用的位置和所在作用域
- 检测未声明的引用
作用域解析示例:
var x = 1;
function test() {
var x = 2;
console.log(x); // 找到局部的 x (2),不是全局的 x (1)
}- 标记规则:
references.length > 0→ 使用中scopeLevel === 0 && references.length === 0→ 可疑(顶层未引用)scopeLevel > 0 && references.length === 0→ 死代码(嵌套未引用)
- 保守策略:保护所有顶层声明,避免误删被其他文件使用的代码
作用域链构建 = 从语法到语义的关键一跃
AST(语法结构) 作用域链(语义结构)
| |
| 遍历 + 语义规则 |
↓ ↓
节点嵌套关系 -------→ 标识符解析规则
如果作用域链错误:
- ❌ 同名变量无法区分
- ❌ 变量提升无法处理
- ❌ 引用解析全部错误
- ❌ 所有后续分析都是错的
variableStatistics/
├── analyzer.html # 🎯 主分析器(在浏览器中打开)
├── index.js # AST 生成工具
├── package.json # 项目依赖
├── .gitignore # Git 忽略配置
├── README.md # 项目文档
└── examples/ # 测试用例目录
├── README.md # 测试用例说明
├── test.js # 基础语法测试
├── test.json # test.js 的 AST(自动生成)
├── test2.js # 99%的真实项目文件测试(加了一点用于测试的代码)
├── test2.json # test2.js 的 AST(自动生成)
├── test3.js # 基础语法测试
└── test3.json # test3js 的 AST(自动生成)
| 文件 | 说明 |
|---|---|
analyzer.html |
🎯 主分析器,在浏览器中打开 |
index.js |
AST 生成工具(使用 acorn 解析 JS 文件) |
examples/ |
测试用例目录,包含 test.js 和 test2.js |
// ❌ 动态属性访问
var name = "getUserName";
obj[name](); // 无法分析 name 是否被使用
// ❌ eval / Function 构造器
eval("var x = 1");
new Function("return x")(); // 无法静态分析
// ❌ with 语句
with(obj) {
prop = 1; // 无法确定 prop 是哪个变量
}原因:静态分析无法预测运行时行为。
// file1.js
function apiHandler() { ... } // 被其他文件调用
// file2.js
<script src="file1.js"></script>
apiHandler(); // 无法追踪跨文件引用当前处理:顶层声明标记为"可疑",提示手动检查。
解决方案:需要实现模块依赖图分析(类似 Webpack)。
var obj = {
name: "test",
getName: function() {
return this.name; // this.name 无法追踪到 obj.name
}
};原因:this 绑定是运行时确定的,静态分析无法处理。
// ❌ 间接调用
var funcName = "myFunc";
window[funcName]();
// ❌ 反射 API
Object.keys(obj).forEach(key => {
obj[key](); // 无法分析哪些方法被调用
});
// ❌ 解构赋值中的复杂模式
var { [computedKey]: value } = obj;function createCounter() {
var count = 0; // ✅ 可以检测
return function() {
return count++; // ✅ 可以检测到引用
};
}
// 但以下场景有限制:
function wrapper(callback) {
var internal = 1;
callback(internal); // ✅ 可以检测
}
wrapper(function(x) {
externalVar = x; // ⚠️ 如果 externalVar 未声明,会报未声明引用
});当前处理:可以检测闭包内的引用,但无法追踪回调函数传递的复杂场景。
// ✅ 完整支持 let/const 块级作用域(v2.1+)
if (true) {
let x = 1; // 正确:x 在 if 块作用域
}
console.log(x); // 正确:报未声明引用
// ✅ for 循环块作用域
for (let i = 0; i < 10; i++) {
// i 在 for 块作用域
}
console.log(i); // 正确:报未声明引用
// ❌ 解构赋值中的嵌套
var { a: { b: c } } = obj; // 可能无法正确处理深层解构
// ❌ import/export
import { foo } from './module'; // 未处理 ES6 模块
// ❌ Class 语法
class MyClass {
#privateField; // 未处理私有字段
}// ❌ Vue 模板引用
<template>
<div>{{ message }}</div> // message 在 JS 中声明,但被模板使用
</template>
// ❌ React JSX
function Component() {
const handleClick = () => {};
return <button onClick={handleClick} />; // JSX 需要特殊处理
}
// ❌ jQuery 插件模式
$.fn.myPlugin = function() { ... };当前处理:这些需要框架特定的分析器。
// ⚠️ 浏览器全局对象
document.getElementById('app'); // document 被标记为未声明引用
console.log("test"); // console 被标记为未声明引用
// ⚠️ Node.js 全局对象
require('./module'); // require 被标记为未声明引用
process.env.NODE_ENV; // process 被标记为未声明引用当前处理:会报告为"未声明引用"。
改进方案:维护全局对象白名单(window, document, console, require, etc.)。
var obj = {
get value() {
sideEffect(); // 访问 obj.value 时会调用
return this._value;
}
};
var x = obj.value; // 看起来只是读取,实际执行了 sideEffect()原因:静态分析无法检测属性访问的副作用。
// 超大文件(10000+ 行)
// 深度嵌套(20+ 层函数)
// 大量声明(1000+ 变量)当前处理:纯前端分析,大文件可能导致浏览器卡顿。
改进方案:
- 移到 Node.js 环境(Worker 多线程)
- 增量分析
- 缓存中间结果
- 单文件分析:独立的工具函数、业务逻辑文件
- 传统 JS 代码:ES5/ES6 基础语法,无复杂框架依赖
- 代码清理前的参考:识别明显的死代码
- 代码 Review:发现未使用的辅助函数
- 顶层"可疑"代码需手动确认:可能被其他文件引用
- 未声明引用需检查:可能是全局对象或拼写错误
- 结果仅供参考:删除前务必测试
- 配合其他工具:ESLint、TypeScript 等
这个项目是我毕业一年时,在公司为甲方维护旧代码、但必须依赖他们的低代码内网平台时抽空研究出来的。test2.js 里的源码其实就是当年项目的真实逻辑(具体内容只能从提交历史里看)。他们的平台前端只会把顶层声明的函数罗列出来,完全看不出哪些已经废弃、哪些仍在使用,于是我才萌生了做这个项目的想法:目标很简单——辨认那些明明声明了却肯定不会被用到的函数,好让我心里踏实地删掉它们。
只可惜后来项目忙、能力有限,再加上换了工作,这个计划就半途而废。四年里我一直耿耿于怀。直到最近有空,又赶上 AI 编程的大发展,我才重新拾起提交。AI 的思路和我当初差不多,但写得又快又准;也许我准备的测试样本还不够多,肉眼看没问题,却可能还有遗漏,测试覆盖也可能不够全面。但对于四年前我定下的目标,它已经帮我圆满实现了。我相信AI编程是不可逆的趋势。
MIT License