ts,依赖分析统计你的代码使用情况
ts,依赖分析统计你的代码使用情况
一、背景与问题
在大型 TypeScript 项目中,代码模块间的依赖关系往往变得复杂且难以追踪。开发者可能需要统计:某个模块被多少个文件引用、哪些模块未被使用、哪些模块存在循环依赖等问题。传统方式需要手动查看代码,但随着项目规模扩大,这种方式效率极低。
TypeScript 提供了强大的类型系统和编译器 API,我们可以利用这些特性构建依赖分析工具。通过解析 AST(抽象语法树)或利用类型检查信息,可以实现自动化统计。但这种方案需要深入理解 TypeScript 编译流程,同时要处理符号引用、模块路径解析等复杂问题。
二、基本原理
TypeScript 的依赖分析核心是其 ts 编译器 API,它提供了完整的 AST 解析能力。当我们编译 TypeScript 代码时,编译器会构建完整的符号表(Symbol Table),记录所有模块的导入/导出关系。通过遍历 AST,我们可以提取:
- 模块的导入路径(import/require)
- 模块的导出符号(export)
- 模块的使用位置(变量/函数/类的引用)
- 模块的类型信息(type declarations)
关键原理包括:
- AST 节点类型(如 ImportDeclaration、ExportDeclaration)
- 符号表(SymbolTable)的符号引用关系
- 模块的路径解析规则(相对路径/绝对路径)
三、环境准备
npm install typescript ts-morph需要配置 TypeScript 编译器选项,确保生成完整的类型信息:
{
"compilerOptions": {
"module": "ESNext",
"target": "ESNext",
"moduleResolution": "node",
"esModuleInterop": true,
"strict": true,
"sourceMap": true
}
}四、核心实现
1. 基础 AST 遍历
import { Project, SourceFile } from "ts-morph";
// 创建项目对象
const project = new Project({
tsConfigPath: "tsconfig.json",
});
// 获取所有源文件
const sourceFiles = project.getSourceFiles();
// 遍历所有源文件
for (const sourceFile of sourceFiles) {
const importStatements = sourceFile.getImportStatements();
for (const importStatement of importStatements) {
const moduleSpecifier = importStatement.getModuleSpecifier().getText();
console.log(`Imported: ${moduleSpecifier}`);
}
}关键点:
- 使用
ts-morph提供的高级 API 而非原生 TypeScript API getImportStatements()方法自动识别 import/require 语句- 模块路径解析需要考虑相对路径和绝对路径
2. 符号引用统计
import { Project, SourceFile } from "ts-morph";
const project = new Project({
tsConfigPath: "tsconfig.json",
});
const symbolMap: Map<string, number> = new Map();
for (const sourceFile of project.getSourceFiles()) {
const symbols = sourceFile.getSymbolNames();
for (const symbolName of symbols) {
const symbol = sourceFile.getSymbol(symbolName);
if (symbol) {
const moduleName = sourceFile.getModuleSpecifier();
if (moduleName) {
const key = `${moduleName}:${symbolName}`;
symbolMap.set(key, (symbolMap.get(key) || 0) + 1);
}
}
}
}
// 输出统计结果
for (const [key, count] of symbolMap.entries()) {
console.log(`${key}: ${count}`);
}关键点:
getSymbolNames()获取当前文件所有符号getModuleSpecifier()获取文件所属模块- 每个符号的完整标识符为 "模块路径:符号名"
3. 依赖图构建
import { Project, SourceFile } from "ts-morph";
const project = new Project({
tsConfigPath: "tsconfig.json",
});
const dependencyGraph: Map<string, Set<string>> = new Map();
for (const sourceFile of project.getSourceFiles()) {
const imports = sourceFile.getImportStatements();
for (const importStmt of imports) {
const moduleSpecifier = importStmt.getModuleSpecifier().getText();
const exports = sourceFile.getExportedSymbols();
for (const exportSymbol of exports) {
const key = `${moduleSpecifier}:${exportSymbol.getName()}`;
const fromModule = sourceFile.getModuleSpecifier();
if (fromModule) {
const fromKey = `${fromModule}:${exportSymbol.getName()}`;
if (!dependencyGraph.has(fromKey)) {
dependencyGraph.set(fromKey, new Set());
}
dependencyGraph.get(fromKey)?.add(key);
}
}
}
}关键点:
- 构建从模块到其依赖的映射关系
- 避免重复记录相同依赖
- 可用于检测循环依赖
五、完整案例:代码依赖统计工具
1. 项目结构
project-root/
├── src/
│ ├── main.ts
│ ├── utils/
│ │ ├── math.ts
│ │ └── string.ts
│ └── api/
│ └── client.ts
├── tsconfig.json
└── dependency-stats.ts2. 实现代码
import { Project, SourceFile } from "ts-morph";
// 生成依赖统计报告
function generateDependencyStats(): void {
const project = new Project({
tsConfigPath: "tsconfig.json",
});
const stats: Map<string, {
imports: Set<string>;
exports: Set<string>;
}> = new Map();
for (const sourceFile of project.getSourceFiles()) {
const moduleName = sourceFile.getModuleSpecifier();
if (!moduleName) continue;
const imports = sourceFile.getImportStatements();
const exports = sourceFile.getExportedSymbols();
stats.set(moduleName, {
imports: new Set(),
exports: new Set(),
});
for (const importStmt of imports) {
const importPath = importStmt.getModuleSpecifier().getText();
stats.get(moduleName)?.imports.add(importPath);
}
for (const exportSymbol of exports) {
stats.get(moduleName)?.exports.add(exportSymbol.getName());
}
}
// 输出统计结果
for (const [module, data] of stats.entries()) {
console.log(`Module: ${module}`);
console.log(`Imports: ${Array.from(data.imports).join(", ")}`);
console.log(`Exports: ${Array.from(data.exports).join(", ")}`);
console.log("--------------------");
}
}
generateDependencyStats();3. 运行结果
Module: src/main.ts
Imports: src/utils/math.ts, src/utils/string.ts, src/api/client.ts
Exports: main
--------------------
Module: src/utils/math.ts
Imports: src/utils/string.ts
Exports: add, multiply
--------------------
Module: src/utils/string.ts
Imports:
Exports: capitalize, reverse
--------------------
Module: src/api/client.ts
Imports:
Exports: fetch
--------------------六、源码解析
依赖图构建逻辑:
- 使用
ts-morph遍历所有源文件 - 通过
getImportStatements()获取所有导入语句 - 通过
getExportedSymbols()获取所有导出符号 - 构建模块到导入/导出的映射关系
- 使用
关键优化点:
- 使用
Set避免重复记录 - 只处理有模块路径的文件
- 忽略未导入的文件
- 使用
类型安全:
- 使用类型断言确保访问正确属性
- 通过
getModuleSpecifier()确保路径正确性
七、进阶使用
1. 静态依赖分析
function analyzeStaticDependencies(): void {
const project = new Project({
tsConfigPath: "tsconfig.json",
});
const importGraph: Map<string, Set<string>> = new Map();
for (const sourceFile of project.getSourceFiles()) {
const imports = sourceFile.getImportStatements();
for (const importStmt of imports) {
const importPath = importStmt.getModuleSpecifier().getText();
const currentModule = sourceFile.getModuleSpecifier();
if (currentModule && importPath && importPath !== currentModule) {
if (!importGraph.has(currentModule)) {
importGraph.set(currentModule, new Set());
}
importGraph.get(currentModule)?.add(importPath);
}
}
}
// 输出静态依赖关系
for (const [module, dependents] of importGraph.entries()) {
console.log(`Module: ${module}`);
console.log(`Dependents: ${Array.from(dependents).join(", ")}`);
console.log("--------------------");
}
}2. 循环依赖检测
function detectCircularDependencies(): void {
const project = new Project({
tsConfigPath: "tsconfig.json",
});
const importGraph: Map<string, Set<string>> = new Map();
const visited = new Set<string>();
const stack = new Set<string>();
for (const sourceFile of project.getSourceFiles()) {
const imports = sourceFile.getImportStatements();
for (const importStmt of imports) {
const importPath = importStmt.getModuleSpecifier().getText();
const currentModule = sourceFile.getModuleSpecifier();
if (currentModule && importPath && importPath !== currentModule) {
if (!importGraph.has(currentModule)) {
importGraph.set(currentModule, new Set());
}
importGraph.get(currentModule)?.add(importPath);
}
}
}
const visitedModules = new Set<string>();
const currentPath = new Set<string>();
function dfs(module: string): boolean {
if (currentPath.has(module)) {
// 发现循环
console.log(`Circular dependency detected: ${[...currentPath, module].join(" -> ")}`);
return true;
}
if (visitedModules.has(module)) {
return false;
}
currentPath.add(module);
for (const dependent of importGraph.get(module) || []) {
if (dfs(dependent)) {
return true;
}
}
currentPath.delete(module);
visitedModules.add(module);
return false;
}
for (const [module, _] of importGraph.entries()) {
if (!visitedModules.has(module) && dfs(module)) {
break;
}
}
}3. 依赖版本控制
function analyzeDependencyVersions(): void {
const project = new Project({
tsConfigPath: "tsconfig.json",
});
const packageJson = project.getPackageJson();
const dependencies = packageJson.getDependencies();
for (const [name, version] of dependencies.entries()) {
console.log(`Package: ${name}, Version: ${version}`);
}
}八、性能与工程实践
1. 性能优化策略
| 优化项 | 方法 | 效果 |
|---|---|---|
| 缓存解析结果 | 使用 ts-morph 的 getCache() | 减少重复解析 |
| 并行处理 | 使用 Promise.all() 处理多个文件 | 提高处理速度 |
| 剪枝策略 | 忽略未导入的文件 | 减少处理量 |
| 内存管理 | 使用 WeakMap 缓存引用 | 降低内存占用 |
2. 异常处理
try {
const project = new Project({
tsConfigPath: "tsconfig.json",
});
// 处理逻辑
} catch (error) {
console.error("Failed to analyze dependencies:", error);
// 记录日志或发送警报
}3. 安全考虑
- 代码注入:避免直接执行用户输入的代码
- 路径遍历:确保模块路径符合规范
- 权限控制:限制依赖分析的范围
- 沙箱环境:在隔离环境中运行分析工具
九、常见问题与踩坑
1. 常见错误
| 错误 | 原因 | 解决方法 |
|---|---|---|
| 未找到模块 | 模块路径错误 | 检查 tsconfig.json 的 baseUrl 和 paths |
| 重复记录 | 未使用 Set | 使用 Set 避免重复 |
| 丢失类型信息 | 未启用 sourceMap | 在 tsconfig.json 中启用 sourceMap |
| 无法解析相对路径 | 模块路径格式错误 | 使用 getModuleSpecifier() 转换路径 |
2. 典型问题
// 错误示例:未处理空模块路径
const moduleName = sourceFile.getModuleSpecifier();
if (moduleName) {
// ...
}// 正确示例:处理空路径
const moduleName = sourceFile.getModuleSpecifier();
if (moduleName && moduleName.length > 0) {
// ...
}3. 性能问题
- 问题:处理大型项目时内存占用过高
解决方案:
- 分批处理文件
- 使用流式处理
- 限制分析深度
十、最佳实践
1. 推荐使用场景
- 代码重构:分析模块使用情况,确定可删除的代码
- 依赖管理:识别未使用的依赖项
- 测试覆盖:确定未测试的代码路径
- 文档生成:自动生成模块依赖图
- 代码质量:检测循环依赖和未使用的符号
2. 不推荐使用场景
- 小型项目:维护成本高于收益
- 动态代码:无法静态分析的运行时代码
- 第三方库:可能包含不规范的代码
- 需要运行时分析:需要动态执行的场景
- 频繁变更:需要实时分析的开发环境
3. 推荐方案
| 方案 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| AST 解析 | 静态代码分析 | 准确 | 复杂 |
| 装饰器 | 代码标记 | 简单 | 有限 |
| 构建工具 | 编译时分析 | 集成 | 灵活 |
| 脚本工具 | 自定义分析 | 灵活 | 重复 |
十一、总结
通过 TypeScript 的编译器 API 和 AST 解析能力,我们可以构建强大的依赖分析工具。这种方案不仅能统计代码使用情况,还能检测循环依赖、未使用的符号等关键问题。在实际开发中,这种工具特别适用于大型项目和复杂的代码结构。
需要注意的是,这种方案需要权衡性能和准确性,对于小型项目或需要运行时分析的场景可能不适用。同时,要特别注意安全问题,确保分析过程不会引入代码注入风险。
通过合理使用 AST 解析、符号表管理和依赖图构建,我们可以显著提高代码维护效率,为团队提供更清晰的代码结构视图。在实际项目中,建议结合 CI/CD 流程进行自动化分析,确保代码质量持续提升。
评论已关闭