ts,依赖分析统计你的代码使用情况

ts,依赖分析统计你的代码使用情况

一、背景与问题

在大型 TypeScript 项目中,代码模块间的依赖关系往往变得复杂且难以追踪。开发者可能需要统计:某个模块被多少个文件引用、哪些模块未被使用、哪些模块存在循环依赖等问题。传统方式需要手动查看代码,但随着项目规模扩大,这种方式效率极低。

TypeScript 提供了强大的类型系统和编译器 API,我们可以利用这些特性构建依赖分析工具。通过解析 AST(抽象语法树)或利用类型检查信息,可以实现自动化统计。但这种方案需要深入理解 TypeScript 编译流程,同时要处理符号引用、模块路径解析等复杂问题。

二、基本原理

TypeScript 的依赖分析核心是其 ts 编译器 API,它提供了完整的 AST 解析能力。当我们编译 TypeScript 代码时,编译器会构建完整的符号表(Symbol Table),记录所有模块的导入/导出关系。通过遍历 AST,我们可以提取:

  1. 模块的导入路径(import/require)
  2. 模块的导出符号(export)
  3. 模块的使用位置(变量/函数/类的引用)
  4. 模块的类型信息(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.ts

2. 实现代码

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
--------------------

六、源码解析

  1. 依赖图构建逻辑:

    • 使用 ts-morph 遍历所有源文件
    • 通过 getImportStatements() 获取所有导入语句
    • 通过 getExportedSymbols() 获取所有导出符号
    • 构建模块到导入/导出的映射关系
  2. 关键优化点:

    • 使用 Set 避免重复记录
    • 只处理有模块路径的文件
    • 忽略未导入的文件
  3. 类型安全:

    • 使用类型断言确保访问正确属性
    • 通过 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. 推荐使用场景

  1. 代码重构:分析模块使用情况,确定可删除的代码
  2. 依赖管理:识别未使用的依赖项
  3. 测试覆盖:确定未测试的代码路径
  4. 文档生成:自动生成模块依赖图
  5. 代码质量:检测循环依赖和未使用的符号

2. 不推荐使用场景

  1. 小型项目:维护成本高于收益
  2. 动态代码:无法静态分析的运行时代码
  3. 第三方库:可能包含不规范的代码
  4. 需要运行时分析:需要动态执行的场景
  5. 频繁变更:需要实时分析的开发环境

3. 推荐方案

方案适用场景优点缺点
AST 解析静态代码分析准确复杂
装饰器代码标记简单有限
构建工具编译时分析集成灵活
脚本工具自定义分析灵活重复

十一、总结

通过 TypeScript 的编译器 API 和 AST 解析能力,我们可以构建强大的依赖分析工具。这种方案不仅能统计代码使用情况,还能检测循环依赖、未使用的符号等关键问题。在实际开发中,这种工具特别适用于大型项目和复杂的代码结构。

需要注意的是,这种方案需要权衡性能和准确性,对于小型项目或需要运行时分析的场景可能不适用。同时,要特别注意安全问题,确保分析过程不会引入代码注入风险。

通过合理使用 AST 解析、符号表管理和依赖图构建,我们可以显著提高代码维护效率,为团队提供更清晰的代码结构视图。在实际项目中,建议结合 CI/CD 流程进行自动化分析,确保代码质量持续提升。

none
最后修改于:2026年09月20日 07:05

评论已关闭

推荐阅读

AIGC实战——Transformer模型
2024年12月01日
Socket TCP 和 UDP 编程基础(Python)
2024年11月30日
python , tcp , udp
如何使用 ChatGPT 进行学术润色?你需要这些指令
2024年12月01日
AI
最新 Python 调用 OpenAi 详细教程实现问答、图像合成、图像理解、语音合成、语音识别(详细教程)
2024年11月24日
ChatGPT 和 DALL·E 2 配合生成故事绘本
2024年12月01日
omegaconf,一个超强的 Python 库!
2024年11月24日
【视觉AIGC识别】误差特征、人脸伪造检测、其他类型假图检测
2024年12月01日
[超级详细]如何在深度学习训练模型过程中使用 GPU 加速
2024年11月29日
Python 物理引擎pymunk最完整教程
2024年11月27日
MediaPipe 人体姿态与手指关键点检测教程
2024年11月27日
深入了解 Taipy:Python 打造 Web 应用的全面教程
2024年11月26日
基于Transformer的时间序列预测模型
2024年11月25日
Python在金融大数据分析中的AI应用(股价分析、量化交易)实战
2024年11月25日
AIGC Gradio系列学习教程之Components
2024年12月01日
Python3 `asyncio` — 异步 I/O,事件循环和并发工具
2024年11月30日
llama-factory SFT系列教程:大模型在自定义数据集 LoRA 训练与部署
2024年12月01日
Python 多线程和多进程用法
2024年11月24日
Python socket详解,全网最全教程
2024年11月27日
python之plot()和subplot()画图
2024年11月26日
理解 DALL·E 2、Stable Diffusion 和 Midjourney 工作原理
2024年12月01日