国庆极客实验:用 AST 分析器自动扫描项目中未使用的导出函数并自动修剪
在大型前端工程的漫长迭代岁月中通用工具库如utils、helpers、api等目录往往是最容易藏污纳垢的“数字荒原”。伴随着产品业务的数轮重构与技术栈换代大量曾立下汗马功劳的公共导出函数逐渐失去了所有的消费者。然而由于代码库庞大复杂、模块之间盘根错节团队中的任何一名工程师都不敢轻易手动删除一个看似无用的export function formatLegacyDate()生怕在哪个未知的角落引发灾难性的运行时调用崩溃。很多人会下意识地认为“只要我们开启了 Rollup 或 Vite 的 Tree Shaking这些没用到的导出自然会被编译器摇树消除掉。”但这只是理想状态下的美好设想。在实际工程中由于模块顶级作用域中可能存在的隐式副作用Side Effects、某些对象引用的逃逸分析不完全、或者项目自身被打包为面向下游消费的 SDK打包器往往出于安全稳妥的原则将这些死代码原封不动地打包进最终的生产产物中造成线上包体积的慢性水肿。趁着国庆假期的宽裕时间我们展开了一场极客实验手写一个基于 AST 抽象语法树的全项目符号引用分析器自动扫描出那些沉睡已久的“僵尸导出”并执行确定性的安全自动化修剪。为什么静态文本搜索Grep/Ripgrep无法胜任此任务在没有专用工具前许多工程师习惯用全局正则搜索或ripgrep去搜寻某个函数名。这种简陋的做法在工业级项目中会引发大量的误报与漏报同名属性碰撞搜索parse会匹配到JSON.parse、url.parse、对象属性{ parse: 1 }导致误以为该函数仍在被广泛使用重命名导入导出形如import { format as f } from ./date或export { a as b }的别名语法纯正则根本无法追溯其符号绑定的真实血缘动态导入与重导出陷阱形如export * from ./submodule的星号重导出会彻底斩断基于文本的静态追踪链路。只有深入到底层词法与语法分析层面构建项目的抽象语法树AST与符号作用域拓扑图Symbol Scope Graph才能做到百分之百的确定性裁决。扫描器架构双向符号图谱的构建我们的分析器基于 TypeScript 官方 Compiler API 构建遵循双向追踪闭环导出符号收集Export Inventory遍历项目中所有的源文件精准提取所有的具名导出Named Exports、默认导出Default Exports以及聚合重导出记录其声明文件路径与在 AST 节点中的精确行号偏移量。导入消费拓扑Import Cross-Reference再次遍历全量源码解析所有的ImportDeclaration、动态import()以及require调用精准映射其引用的目标文件与具体的符号名。差集判定与安全修剪Diff Auto-Pruning比对导出字典与导入拓扑凡是消费计数器为零且未被外部白名单豁免的符号即被标记为僵尸代码。修剪引擎可以进一步通过 AST 操作直接将export function foo()安全剥离为普通局部函数或者直接删除该 AST 节点并重新格式化源码。核心分析器实现代码下面是核心扫描与引用图谱构建脚本的精炼实现import ts from typescript; import * as path from path; import * as fs from fs; interface ExportedSymbolInfo { filePath: string; symbolName: string; node: ts.Node; isUsed: boolean; } export class DeadCodeScanner { private program: ts.Program; private checker: ts.TypeChecker; private exportedMap: Mapstring, ExportedSymbolInfo new Map(); // key: filePath#symbolName private importedSet: Setstring new Set(); // targetFilePath#symbolName constructor(tsConfigPath: string) { const configFile ts.readConfigFile(tsConfigPath, ts.sys.readFile); const parsedConfig ts.parseJsonConfigFileContent( configFile.config, ts.sys, path.dirname(tsConfigPath) ); this.program ts.createProgram(parsedConfig.fileNames, parsedConfig.options); this.checker this.program.getTypeChecker(); } public analyze(): ExportedSymbolInfo[] { const sourceFiles this.program.getSourceFiles().filter( (sf) !sf.isDeclarationFile !sf.fileName.includes(node_modules) ); // 第一阶段收集全部导出符号 for (const sf of sourceFiles) { this.collectExports(sf); } // 第二阶段收集全局消费引用 for (const sf of sourceFiles) { this.collectImports(sf); } // 第三阶段计算差集 const deadSymbols: ExportedSymbolInfo[] []; this.exportedMap.forEach((info, key) { if (!this.importedSet.has(key)) { deadSymbols.push(info); } }); return deadSymbols; } private collectExports(sourceFile: ts.SourceFile): void { const filePath path.resolve(sourceFile.fileName); ts.forEachChild(sourceFile, (node) { // 处理 export function / export const if (ts.canHaveModifiers(node)) { const modifiers ts.getModifiers(node); const hasExport modifiers?.some((m) m.kind ts.SyntaxKind.ExportKeyword); if (hasExport) { if (ts.isFunctionDeclaration(node) node.name) { const name node.name.text; this.exportedMap.set(${filePath}#${name}, { filePath, symbolName: name, node, isUsed: false, }); } else if (ts.isVariableStatement(node)) { for (const decl of node.declarationList.declarations) { if (ts.isIdentifier(decl.name)) { const name decl.name.text; this.exportedMap.set(${filePath}#${name}, { filePath, symbolName: name, node: decl, isUsed: false, }); } } } } } }); } private collectImports(sourceFile: ts.SourceFile): void { const currentDir path.dirname(sourceFile.fileName); ts.forEachChild(sourceFile, (node) { if (ts.isImportDeclaration(node) node.importClause) { const moduleSpecifier (node.moduleSpecifier as ts.StringLiteral).text; // 解析模块的绝对路径 const resolvedPath this.resolveModulePath(currentDir, moduleSpecifier); if (!resolvedPath) return; const namedBindings node.importClause.namedBindings; if (namedBindings ts.isNamedImports(namedBindings)) { for (const element of namedBindings.elements) { const importedName element.propertyName ? element.propertyName.text : element.name.text; this.importedSet.add(${resolvedPath}#${importedName}); } } } }); } private resolveModulePath(dir: string, specifier: string): string | null { if (!specifier.startsWith(.)) return null; // 排除外部三方包 const extensions [.ts, .tsx, .js, .jsx, /index.ts, /index.js]; const base path.resolve(dir, specifier); for (const ext of extensions) { const full base ext; if (fs.existsSync(full)) return full; } return null; } }自动修剪器Auto-Pruner的安全执行策略在揪出了死代码清单之后最为关键的步骤是执行“安全修剪”。贸然物理删除代码可能会因为开发人员正在本地分支临时编写的代码引发冲突。因此我们的修剪引擎采用“渐进式降级修剪”策略去导出化De-exporting对于仅在该文件内部被同模块调用的函数AST 转换器仅移除其前面的export修饰符将其收敛为模块私有成员避免符号泄露给打包器彻底剔除无副作用死函数对于在整个项目包括其定义文件自身都未被调用的纯函数利用 TypeScript 的 Printer API 重新生成代码将对应的函数节点彻底剥离并连带清除无用的 JSDoc 注释自动化测试与门禁验证修剪完成后自动触发单元测试与类型检查tsc --noEmit确保没有破坏现有的类型推演与业务断言。实验成果与总结在我们的核心工具库中运行该实验脚本成果令人惊喜在短短 3.2 秒的 AST 遍历分析后脚本在 140 多个工具文件中精准识别出了 47 个从未被任何模块引用的僵尸导出函数。自动修剪后不仅源码库减少了近三千行无用代码最终经过 Vite 7.0 打包后的生产环境 Javascript 产物体积更是直接缩减了 18.6KBGzip 后压缩了 5.2KB。这种利用 AST 工具武装前端交付流水线的实践将原本高度依赖人工审查与心理直觉的“代码清理难题”转化成了确定、优雅且自动化运转的数学确定性。它不仅是应对前端体积水肿的终极解药更是工程卓越性在微观代码层面的鲜明印记。

相关新闻

11、事件分发目标查找:findFocusedWindowTargets()与findTouchedWindowTargets()

11、事件分发目标查找:findFocusedWindowTargets()与findTouchedWindowTargets()

11.1 为什么需要两个查找方法?你想想看,键盘事件和触摸事件的性质完全不同。键盘事件:用户按下一个键,系统得知道当前哪个窗口获得了焦点。焦点窗口只有一个,所以查找逻辑相对简单。触摸事件:用户手指点下去…

2026/10/11 10:48:25 阅读更多 →
Lighthouse 绿分不代表真实体验:CrUX 现场数据与实验室数据的口径差异解读

Lighthouse 绿分不代表真实体验:CrUX 现场数据与实验室数据的口径差异解读

Lighthouse 绿分不代表真实体验:CrUX 现场数据与实验室数据的口径差异解读适用读者:做外贸独立站、在意 Google 收录与排名的开发者和站长;正在用 Lighthouse 和 PageSpeed Insights 给站点做性能体检,但发现分数跟海外用户反馈对…

2026/10/11 15:55:59 阅读更多 →
Python Agent 踩坑实录:第一次写 Agent,我忘了加一行代码,循环跑了 200 次

Python Agent 踩坑实录:第一次写 Agent,我忘了加一行代码,循环跑了 200 次

Python Agent 踩坑实录:第一次写 Agent,我忘了加一行代码,循环跑了 200 次 摘要:本文从一次"初学者写 Agent 忘了加最大步数限制、循环跑了 200 次才停"的经历出发,分析 Agent 循环失控的三个常见原因&#…

2026/10/9 16:50:48 阅读更多 →

最新新闻

风电水电联合调度优化算法与工程实践

风电水电联合调度优化算法与工程实践

1. 项目背景与核心价值去年参与某省级电网清洁能源消纳项目时,我第一次接触到风-水联合调度这个课题。当时面对风电出力的强随机性和水电调节的滞后性,团队在制定优化运行方案时遇到了不少挑战。这个经历让我深刻认识到:在双碳目标下&#xf…

2026/10/14 2:21:24 阅读更多 →
从PowerDesigner迁移到PDMan:开源数据库建模与Git版本管理实践

从PowerDesigner迁移到PDMan:开源数据库建模与Git版本管理实践

1. 为什么我最终把数据库建模从商业软件迁到了PDMan做后端开发时间长了,数据库建模这件事几乎绕不开。早些年团队里清一色用PowerDesigner,画物理模型、生成DDL、导出字段文档,一套流程走下来确实顺手。但问题也很明显:正版授权费…

2026/10/14 2:21:24 阅读更多 →
微信小程序仿智行火车票开发实战:云开发+数据离线化方案

微信小程序仿智行火车票开发实战:云开发+数据离线化方案

简介:本资源是一套完整的微信小程序仿智行火车票12306系统实战项目,面向Java后端与小程序前端初学者及全栈学习者,聚焦铁路订票核心业务场景,涵盖用户查询、车次管理、订单生成等全流程功能实现。项目采用SSM(SpringSp…

2026/10/14 2:21:23 阅读更多 →
RTMP Flash播放插件替代方案:flv.js迁移实战与参数调优

RTMP Flash播放插件替代方案:flv.js迁移实战与参数调优

简介:这份资源是面向需要在网页中播放RTMP流媒体的开发者与运维人员的一套Flash播放插件实现方案,核心解决旧系统或特定场景下通过Flash技术播放RTMP直播、点播流的问题。压缩包共5个文件,约1.15MB,包含2个swf播放器与安装检测组件…

2026/10/14 2:21:23 阅读更多 →
AI编程助手代码质量救星:TypeScript静态类型约束实战

AI编程助手代码质量救星:TypeScript静态类型约束实战

AI 编程助手越帮越乱,这个话题我最近真的是感同身受。我们团队接入 AI 编程助手之后,合并请求里的代码量倒是上去了,可编译期错误非但没减少,反而冒出来一批"语法完全合法、类型对不上、一跑就崩"的代码。后来大家坐下来…

2026/10/14 2:21:23 阅读更多 →
LaTeX 2025环境配置:texlive + TeXstudio

LaTeX 2025环境配置:texlive + TeXstudio

文章目录Latex2025环境配置一、LaTeX 简介1.1 什么是 LaTeX?1.2 为什么使用 LaTeX?1.3 如何开始使用 LaTeX?二、texlive下载安装2.1 texlive下载2.2 texlive安装2.3 常见问题2.3.1 用管理员权限打开install-tl-windows.bat 报错2.3.2 Latex安…

2026/10/14 2:20:23 阅读更多 →

日新闻

AnyPS5通用适配层设计:兼容性与性能的工程取舍

AnyPS5通用适配层设计:兼容性与性能的工程取舍

1. 从"AnyPS5"这个名字说起:它到底想解决什么问题第一次看到"AnyPS5"这个标题,我脑子里冒出来的第一个念头是:这大概率是一个围绕"跨平台运行"或者"通用适配"做文章的项目。名字里的"Any"…

2026/10/14 0:00:41 阅读更多 →
Flutter跨平台开发实战:二手交易应用与HarmonyOS适配

Flutter跨平台开发实战:二手交易应用与HarmonyOS适配

1. 项目概述:Flutter跨平台二手交易应用开发作为一名长期从事移动应用开发的工程师,我最近完成了一个基于Flutter框架的二手物品交易平台项目。这个项目最吸引我的地方在于它完美结合了Flutter的跨平台优势与HarmonyOS的生态特性,实现了"…

2026/10/14 0:00:41 阅读更多 →
DFS算法解决飞机降落调度问题实战

DFS算法解决飞机降落调度问题实战

1. 项目背景与核心思路飞机降落问题是一个经典的调度优化场景,也是算法竞赛和面试中的高频题目。这个题目要求我们合理安排多架飞机的降落顺序和时间,确保所有飞机都能安全着陆而不发生冲突。在实际航空管制中,这类问题直接关系到飞行安全和机…

2026/10/14 0:00:41 阅读更多 →

周新闻

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

简介:基于 ARIMA、LSTM、Transformer 等模型的流感时间序列预测 Python 源码,面向计算机相关专业课程设计与期末大作业学生,以及项目实战学习者。内容覆盖预处理、平稳性检验、定阶、残差分析、多模型对比预测的完整时序建模流程,…

2026/10/13 2:54:56 阅读更多 →
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别 做影刀RPA自动化,十个新手有八个栽在"往输入框里填东西"这件事上:要么填不进去,要么填了一半,要么直接把原来内容追加在后面。这背后的根因&…

2026/10/13 16:22:04 阅读更多 →
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容 1. 认识影刀:什么场景该用RPA采小说数据 起点中文网的页面结构相对稳定——分类榜单、书籍详情、章节内容三块独立页面,跳转链路清晰。这种场景非常适合影刀自动化&#x…

2026/10/13 1:58:08 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/13 22:01:27 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/13 14:57:39 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/12 18:11:17 阅读更多 →