mcpField
D
detect-static-dependencies

detect-static-dependencies

github.com/dotnet测试与质量5.8k3.5k 次安装1 天前更新
代码仓库

关于 detect-static-dependencies

用于审计 C#/.NET 代码库中难以测试的静态及环境依赖,并按调用点和受影响文件排序,提供可用于改善可测试性的现有抽象建议。

主要功能

  1. 01扫描 C# 文件中的依赖模式
  2. 02汇总并排序调用点
  3. 03报告受影响文件及位置
  4. 04建议 .NET 抽象方案

适用场景

  1. 01编写单元测试前评估可测试性
  2. 02查找遗留代码中的静态耦合
  3. 03确定封装迁移的优先级

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/dotnet/skills --skill detect-static-dependencies

安装后在 Claude Code 中输入/detect-static-dependencies确认已加载。

详细介绍

检测静态依赖

扫描 C# 代码库中对难以测试的静态 API 的调用,并生成一份排名报告,展示出现频率最高的静态 API、受影响最严重的文件,以及 .NET 生态中现有的可替代抽象。

适用场景

  • 在添加单元测试前,审查项目的可测试性
  • 了解旧代码库中静态耦合的范围
  • 确定优先封装哪些静态 API(优先处理出现频率最高的)
  • 制定渐进式提升可测试性的迁移计划

响应指南

  • 根据用户的请求调整响应范围。若用户询问某个特定类别(例如“查找时间相关的静态 API”),应重点提供该类别的文件位置和数量,而不是生成涵盖所有类别的完整报告。
  • 用户提供特定文件或目录路径时,只扫描该范围;除非用户提出要求,否则不要扩展到整个解决方案。
  • 第 4 步的完整结构化报告格式适用于全面审查请求。对于聚焦的问题,只返回相关部分(例如所请求类别的摘要和受影响文件)。

执行约定

  • 提示词中提到相对路径即可开始。使用可用的文件列表工具查找路径并立即扫描;在文件发现和内容搜索均失败前,不要要求用户提供或重新上传文件。
  • 从符合条件的 .cs 文件开始,递归搜索内容并显示行号。不要只搜索 static 关键字:LINQ 表达式、lambda、回调和插值字符串中的环境调用通常没有 static 修饰符。
  • 如果文件读取工具无法读取已通过列表或搜索确认存在的路径,应先判断失败原因再重试。只有在确认属于工具可用性、传输或路径规范化问题后,才能改用其他可用机制(例如 rg -n、grep 或 shell 文件读取器),并且必须先确认规范路径仍在工作区内。若失败原因是内容排除、权限或策略、工作区边界,或原因不明,则停止。搜索输出可用于建立调用记录;只需打开必要的周边代码来核实接收者来源。
  • 加载此技能或宣布扫描计划后,绝不能停止。必须在同一响应中返回已完成的审查结果。如果所有备用方式确实都失败,应报告已核实的部分发现和具体限制;不得捏造发现,也不得用要求用户重新运行来替代审查。

不适用场景

  • 用户希望生成封装器(转交给 generate-testability-wrappers)
  • 用户希望执行机械式迁移(转交给 migrate-static-to-wrapper)
  • 静态 API 已通过接口或 TimeProvider 封装
  • 代码不是 C# / .NET

输入

输入必填说明
目标路径否要扫描的文件、目录、项目(.csproj)或解决方案(.sln)。默认为当前工作区。
排除模式否要跳过的 glob 模式(例如 **/obj/**、**/Migrations/**)
类别筛选否限定特定类别:time、filesystem、environment、network、console、process

工作流

步骤 1:确定扫描范围

将目标解析为一组 .cs 文件:

  • 将提示词中指定的工作区相对路径视为目标;应查找该路径,而不是要求用户提供绝对路径。
  • 如果未指定,则扫描当前工作区下所有符合条件的 .cs 文件;不要擅自只选择一个项目而遗漏其他项目。
  • 如果目标是 .cs 文件,则只扫描该文件。
  • 如果目标是目录,则递归扫描其中所有 .cs 文件(排除 obj/、bin/)。
  • 如果目标是 .csproj,则找到其所在目录并扫描其中的 .cs 文件。
  • 如果目标是 .sln,则解析该文件,找到所有项目目录,并扫描所有项目中的 .cs 文件。

始终排除 obj/、bin/ 以及用户指定的排除模式。

步骤 2:搜索静态依赖模式

扫描每个文件,查找符合以下类别的调用:

将匹配的模式视为候选项,而非问题。在统计实例调用之前,请追踪其接收方如何进入类。通过构造函数、参数、属性或依赖注入(DI)提供的协作者已经是一个测试接缝。尤其是,注入的 HttpClient 可以通过受控的 HttpMessageHandler 进行测试;不要仅仅因为注入的类型是具体类型就统计其调用或建议替换它。

类别要搜索的模式建议的替代方案
时间DateTime.Now、DateTime.UtcNow、DateTime.Today、DateTimeOffset.Now、DateTimeOffset.UtcNow、Task.Delay(、new CancellationTokenSource(TimeSpanTimeProvider(.NET 8+)
文件系统File.ReadAllText(、File.WriteAllText(、File.Exists(、File.Delete(、File.Copy(、File.Move(、Directory.Exists(、Directory.CreateDirectory(、Directory.GetFiles(、Directory.Delete(、Path.GetTempPath(,以及会访问磁盘的实例成员(new FileInfo(...)、new DirectoryInfo(...)、.LastWriteTimeUtc、new StreamReader(path))IFileSystem(System.IO.Abstractions NuGet)
随机性 / 标识new Random(、Random.Shared、Guid.NewGuid(类似 TimeProvider 的接缝:注入 Random / 一个 IGuidProvider
区域性 / 序列化CultureInfo.CurrentCulture、CultureInfo.CurrentUICulture、JsonSerializer.Serialize(、JsonSerializer.Deserialize(显式传递区域性 / 选项,或注入序列化器抽象
环境Environment.GetEnvironmentVariable(、Environment.SetEnvironmentVariable(、Environment.MachineName、Environment.UserName、Environment.CurrentDirectory、Environment.Exit(自定义 IEnvironmentProvider
网络new HttpClient(、.GetAsync(、.PostAsync(、.SendAsync((确认接收方是 HttpClient;排除接收方为注入对象或由注入工厂创建的调用)注入 HttpClient(通常由 IHttpClientFactory 提供)
控制台Console.WriteLine(、Console.ReadLine(、Console.Write(、Console.ReadKey(IConsole 包装器或 ILogger
进程Process.Start(、Process.GetCurrentProcess(、Process.GetProcessesByName(自定义 IProcessRunner

对于时间调用,除计数外,还要检查其用法。在一个逻辑操作中读取两次环境时钟,意味着有两个调用点,并会导致一致性缺陷:例如,分别读取 DateTime.UtcNow 来设置 CreatedAt 和 ExpiresAt = DateTime.UtcNow.AddDays(30),两者可能产生偏差。建议捕获一个统一的时间点。使用 TimeProvider 时,尽可能保留 DateTimeOffset;当现有成员要求 UTC DateTime 时,使用 GetUtcNow().UtcDateTime,绝不要使用 .DateTime,因为它会丢失 UTC 类型。将捕获一个时间点视为可选的行为层面后续改进:机械式包装迁移必须逐一保留原有读取,除非用户另行批准这项语义更改。

步骤 3:汇总并排序结果

统计整个扫描范围内的每个调用点,包括以下规则涵盖的实例成员调用点,而不只是 static 调用点。

计数规则——报告与临时扫描结果不一致的主要原因是计数不准确:

  • 撰写说明前,先建立一份调用记录表。 为每个纳入统计的调用点恰好建立一行,包含类别、精确模式、file:line 和建议的抽象边界。所有类别、模式和每个文件的计数,都要通过对同一份记录表进行分组得出;撰写表格时,绝不要独立重新计数。
  • 将三个计数范围分开。 Files scanned 包含所有符合条件的源文件;affected files 仅包含记录表中有条目的文件;call sites 是记录表的行数。绝不要用其中一个替代另一个。
  • 只保留一个权威总计。 你发现的每个调用点都必须计入类别汇总和总计。绝不要把实际发现项放进总计中未涵盖的“其他观察”部分。
  • 按成员访问的对象分类,而不是按它是否为 static 分类。 访问相同不可测试资源的实例成员也要计数,并归入相应类别(new FileInfo(path).LastWriteTimeUtc → 文件系统;new HttpClient().GetAsync(...) → 网络)。如果成员是实例调用,请使用“隐藏依赖”而不是“static”。
  • 统计实例调用前,先检查接收方的来源。 只有当被测代码自行获取或构造依赖时,才统计资源访问。构造函数注入、参数注入、属性注入和 DI 注入的协作者(包括具体的 HttpClient 实例)不计入“需要包装”的总数。
  • 从“需要包装”的总数中排除确定性的纯辅助方法。 Path.Combine、Path.GetExtension、Path.GetFileName 和 Math.*/string.* 静态方法不会读取任何环境输入,因此很容易测试。如果要列出它们,请放在单独的“无需处理”备注中——绝不要将其作为可测试性障碍。
  • 报告前涵盖所有类别——时间、文件系统、环境、网络、控制台、进程、随机数(new Random()、Guid.NewGuid())、区域性(CultureInfo.CurrentCulture)以及序列化/静态方法,例如 JsonSerializer。如果某个存在的类别被遗漏,就会导致计数偏低。
  • 为每个调用点提供 file:line,方便用户直接跳转到相应位置。
  • 发布前核对。 类别总数、模式排名表和每个文件的表格,合计必须与总数一致。
  • 将排除项视为范围决定,而不是一个类别。 建立记录表前,先移除 obj/、bin/、生成文件和用户指定排除的文件。任何报告计数都不得包含这些文件或调用点。集中说明一次排除项,不要将排除的候选项混入计算。
  • 注明截断的排名。 在全面审计中,如需核对总数,应列出所有不同模式。如果用户只要求 Top-N 子集,请将其标注为子集,不要暗示表中各行之和等于总数。

生成一份包含以下内容的摘要:

  1. 类别汇总——每个类别的调用点总数(时间、文件系统、环境等)
  2. 排名靠前的模式——按计数排序的 10 个最常见单独模式
  3. 受影响最多的文件——静态依赖数量最多的文件
  4. 现有可用抽象——针对每个类别,注明建议使用的 .NET 抽象:
    • 时间 → TimeProvider(.NET 8 起内置)
    • 文件系统 → System.IO.Abstractions(NuGet 包)
    • HTTP → IHttpClientFactory(内置)
    • 环境 → 自定义 IEnvironmentProvider
    • 控制台 → 自定义 IConsole 或 ILogger
    • 进程 → 自定义 IProcessRunner
步骤 4:呈现报告

将输出格式化为结构化报告:

## Static Dependency Report

**Scope**: <project/solution name>
**Files scanned**: <count>
**Total static call sites**: <count>

### Category Summary
| Category     | Call Sites | Recommended Abstraction |
|-------------|-----------|------------------------|
| Time         | 42        | TimeProvider (.NET 8+) |
| File System  | 31        | System.IO.Abstractions |
| Environment  | 12        | IEnvironmentProvider   |
| ...          | ...       | ...                    |

### Top 10 Patterns
| # | Pattern             | Count | Files |
|---|---------------------|-------|-------|
| 1 | DateTime.UtcNow     | 28    | 14    |
| 2 | File.ReadAllText    | 18    | 9     |
| ...                                      |

### Most Affected Files
| File                          | Static Calls | Categories          |
|-------------------------------|-------------|---------------------|
| Services/OrderProcessor.cs    | 12          | Time, FileSystem    |
| ...                                                               |

### Migration Priority
1. **Time** (42 sites) — Use `TimeProvider`, zero NuGet dependencies on .NET 8+
2. **File System** (31 sites) — Use `System.IO.Abstractions` NuGet package
3. ...
第 5 步:建议后续步骤

根据报告,建议优先处理数量最多且内置支持最好的类别。简要说明即可。

只有在用户的下一步明确需要时,才提及 generate-testability-wrappers 或 migrate-static-to-wrapper——将其作为交接说明,而非推销。审计结尾不要附上会削弱调查结果的宣传性后续建议。

验证

  • 已扫描范围内的所有 .cs 文件(核对数量)
  • 报告包含各类别总数、主要模式和受影响文件
  • 各类别总数、主要模式和每个文件的计数,与同一总数一致
  • 分别报告已扫描文件、受影响文件和调用点的数量
  • 所有汇总数据均基于同一份出现记录清单得出,而非分别重新计数
  • 每个出现项都带有 file:line 位置
  • 没有在“其他”部分列出未计入总数的发现
  • 调用注入的协作者时,不计入“需要封装”的总数
  • 不将确定性的纯辅助函数(Path.Combine、Math.*)计为可测试性障碍
  • 为每种检测到的模式列出了建议的替代方案
  • 已排除 obj/ 和 bin/ 目录
  • 迁移优先级按影响程度(数量 × 替换难度)排序

常见问题

问题解决方案
扫描 obj/ 或生成的代码始终排除 obj/、bin/ 和 *.Designer.cs
将对注入协作者的调用计入追踪接收者:注入的 HttpClient、TimeProvider、接口或其他由调用方提供的依赖已经具备接缝,无需替换
漏掉 lambda/LINQ 中的静态调用搜索会覆盖 .cs 文件中的所有代码,包括 lambda
在 < .NET 8 的项目中建议使用 TimeProvider检查 .csproj 中的 TargetFramework——如果低于 net8.0,则建议使用 NodaTime.IClock 或自定义的 ISystemClock
忽略测试项目只扫描生产代码——从扫描中排除 *.Tests.csproj 项目
将发现项放到其他位置而导致计数不足实际调用点应计入类别总数,而不是放在被总数忽略的末尾“另有发现”段落中
将实例成员误称为静态成员new FileInfo(p).LastWriteTimeUtc 是实例调用,但仍依赖隐藏的文件系统——将其计入“文件系统”,并准确描述
建议封装 Path.Combine纯粹且确定性的辅助函数无需接缝;将其列为障碍会导致建议有误

评论

支持 Markdown