关于 find-untested-sources
用于在编写测试前查找未与测试文件配对的源文件的静态分析技能。提供 Roslyn 和 tree-sitter 两种解析引擎,无需构建或运行测试即可生成配对清单。
主要功能
- 01提供 Roslyn 与 tree-sitter 引擎
- 02支持 C# 及多种语言
- 03静态解析源文件与测试文件
- 04输出配对和未配对清单
- 05提供建议的测试文件路径
适用场景
- 01为新增测试确定候选源文件
- 02检查生成的测试是否关联到源文件
- 03在多语言仓库中整理测试缺口
- 04识别没有对应源文件的测试
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ dotnet/ skills --skill find-untested-sources安装后在 Claude Code 中输入/find-untested-sources确认已加载。
附带文件scripts/find_untested_sources.pyscripts/Find-UntestedSources.cs
详细介绍
查找未经测试的源文件
用途
覆盖率工具回答的是“哪些代码行执行过?”,它们要求构建成功且测试运行通过;在真实代码仓库中,这可能需要几分钟到几十分钟。本技能要回答的问题则不同,而且成本低得多:
哪些源文件没有任何测试文件引用其中声明的类型/符号?
这是 Agent 在编写新测试之前会提出的问题;只需解析源文件,就能在几秒内通过静态分析得出答案,无需构建、解析依赖项或编译。输出是一份确定性的测试配对映射,让 Agent 无需先通读整个代码库,就能选出下一个要测试的文件。
两种引擎——任选其一
此技能提供两个可互换的分析器,且采用兼容的 JSON 契约:
| 引擎 | 脚本 | 适用情况 |
|---|---|---|
| Roslyn(C#) | scripts/Find-UntestedSources.cs | 代码仓库仅包含 .NET。使用 Roslyn 语法 API 解析每个 .cs 文件,并进行严格的命名空间消歧,因此在 Settings 或 Context 这类短名称重复的情况下,准确性明显更高。 |
| tree-sitter(多语言) | scripts/find_untested_sources.py | 代码仓库不只包含 C#,或者你希望使用一个工具处理 C#、Python、TypeScript/JavaScript、Go、Java、Rust、Ruby、Kotlin、Swift、PowerShell 和 C++。 |
对于仅包含 .NET 的代码仓库,优先使用 Roslyn 引擎——它基于命名空间的配对比多语言引擎的标识符重叠配对更准确。
必需的工作流
- 使用调用者指定的最小范围代码仓库或包根目录。如果请求指定了子目录,不要扫描父级工作区。
- 执行一次适用的分析器。不要用手动 glob 匹配、文件名匹配或目视检查来代替执行分析器。
进行多语言分析时,如果答案必须区分已配对和未配对的源文件,请传入
--include-tested。 “仅进行静态配对”禁止编译目标代码仓库或运行其中的测试;但不禁止启动此技能的纯解析分析器。如果调用者还说“不要构建”,请简要说明这一区别。 将分析器依赖视为环境前置条件:不要安装软件包、尝试使用错误的引擎、构建代码仓库,也不要在分析器调用失败时退而进行手动扫描。应报告前置条件失败。 - 根据分析器的 JSON 得出结果。保留其配对/未配对分类和建议的相对路径;不要猜测其他路径。
- 如果调用者指定了子目录,请在分析器生成的相对路径前加上该子目录,使报告中的路径相对于工作区。
- 报告所请求的结果,并说明静态配对覆盖率的注意事项。不要附加构建、安装软件包、运行测试或覆盖率命令。如果存在已配对的源文件,请列出覆盖它们的测试文件,以便核查未配对分类。
适用情况
- 用户询问“根据源文件配对情况,我应该在哪里添加测试?”、“哪些文件没有测试?”、“查找未配对的源文件”或“给我一份静态测试缺口清单”。
- 在调用测试生成 Agent 之前,生成源文件配对任务清单。
- 生成测试之后,验证每个新测试文件是否与源文件配对。
- 枚举“弱配对”的源文件(只有一个引用它的测试),以便后续深入检查。
不适用情况
- 行/分支覆盖率——使用
coverage-analysis。 - 基于真实覆盖率数据确定优先级——使用
coverage-analysis。 - CRAP 分数/风险热点——使用
coverage-analysis。 - 现有测试是否足够强?——使用
test-gap-analysis(变异推理)或assertion-quality。
Roslyn 引擎(C#)
前提条件
- 支持基于文件的应用(
dotnet run script.cs)的 .NET SDK。版本由仓库中的global.json固定(SDK 11 预览版或更高版本)。 - 除首次运行时还原
Microsoft.CodeAnalysis.CSharp的 NuGet 包外,无需访问互联网。
用法
# From the skill folder
dotnet run scripts/Find-UntestedSources.cs -- <repo-root> [--top N]
# Save the report
dotnet run scripts/Find-UntestedSources.cs -- <repo-root> > pairing.json
# Iterate the untested list, highest-API-surface first
$report = Get-Content pairing.json | ConvertFrom-Json
$report.untested | Select-Object -First 10 source, decl_count, suggested_test_path
诊断信息输出到 stderr;JSON 输出到 stdout。
输出架构
{
"repo": "<absolute path>",
"elapsed_ms": 8883,
"counts": {
"source_files": 3036,
"test_files": 867,
"untested_files": 1852,
"paired_files": 1184
},
"untested": [
{
"source": "src/Foo/Bar.cs",
"decl_count": 8, // # of type declarations in the file
"suggested_test_path": // mirror of source under a discovered test project
"tests/Foo.Tests/Bar/BarTests.cs"
}
],
"source_to_tests": {
"src/Foo/Baz.cs": [
"tests/Foo.Tests/BazTests.cs",
"tests/Foo.IntegrationTests/Scenarios/BazScenarios.cs"
]
}
}
工作原理
- 文件发现 — 递归遍历目录,排除
bin/、obj/、node_modules/、.git/、.vs/、packages/以及所有名称以点开头的子目录。跳过生成的文件 (.g.cs、.Designer.cs、.AssemblyInfo.cs)。 - 测试与源代码分类 — 向上遍历至最近的
.csproj,如果项目名称以.Tests、.Test、.UnitTests、.IntegrationTests、.E2E、.EndToEnd、.Spec、.Specs结尾, 或内容引用了Microsoft.NET.Test.Sdk、MSTest.Sdk、Microsoft.Testing.Platform、xunit、NUnit、TUnit,或包含<IsTestProject>true</IsTestProject>,则将其标记为测试项目。 - 源代码索引(并行) — 使用
CSharpSyntaxTree.ParseText解析每个源文件 (仅语法,不进行编译);将每个BaseTypeDeclarationSyntax/DelegateDeclarationSyntax记录为(ShortName, EnclosingNamespace, FilePath)。 - 测试扫描(并行) — 解析每个测试文件,收集
using指令和 封闭命名空间,遍历所有IdentifierToken,并在短名称索引中查找;然后严格消歧: 只有当声明的命名空间与测试文件的某个using指令、封闭命名空间或其前缀匹配时, 才会将标识符归属到该声明。这可避免Settings或Context等常见名称与每个项目都匹配所产生的噪声。 - 配对与建议 — 反转映射,得到
source → [tests]。根据<ProjectReference>条目构建 生产项目到测试项目的映射;对每个未经测试的源文件,将其项目内相对路径映射到引用它的测试项目下,以此建议路径。 - 输出 JSON — 按声明数量降序排列,然后按字母顺序排列。
多语言引擎(tree-sitter)
前提条件
- Python 3.10+。
pip install tree-sitter-language-pack(一个自包含的 wheel,内置 300 多种语言的解析器和高层process()API)。无需本地构建,也无需逐个安装语言语法。
用法
# From the skill folder
python scripts/find_untested_sources.py <repo-root>
# Restrict to a language (repeatable)
python scripts/find_untested_sources.py <repo-root> --lang python --lang typescript
# Truncate the report (top 20 by declared API surface)
python scripts/find_untested_sources.py <repo-root> --limit-untested 20 > pairing.json
# Iterate, highest-API-surface first
$report = Get-Content pairing.json | ConvertFrom-Json
$report.untested_sources | Select-Object -First 10 path, declaration_count, suggested_test_path
传入 --include-tested 可额外输出 tested_sources(默认省略,以减小供大模型使用的载荷)。诊断信息输出到 stderr;JSON 输出到 stdout。
输出架构
{
"repo_root": "<absolute path>",
"summary": {
"source_files": 3138,
"test_files": 761,
"tested_source_files": 1419,
"untested_source_files": 1719,
"orphan_test_files": 15,
"languages": ["csharp"]
},
"untested_sources": [
{
"path": "src/Foo/Bar.cs",
"language": "csharp",
"declaration_count": 8,
"declarations": ["Bar", "BarOptions", "IBar", "..."],
"suggested_test_path": "src/Foo/BarTests.cs"
}
],
"orphan_tests": [
{ "path": "tests/SomeIntegrationTest.cs", "language": "csharp" }
]
}
工作原理
-
文件发现 — 递归遍历目录,并跳过常见的构建/供应商目录(
bin、obj、node_modules、target、dist、build、vendor、__pycache__、.venv、.git、……)和生成文件(.d.ts、.g.cs、.Designer.cs、_pb2.py、*.min.js、AssemblyInfo.cs、……)。 -
语言检测 —
detect_language_from_path将扩展名映射到受支持的语言;跳过未知扩展名。 -
测试与源文件分类 — 各语言对应的路径启发式规则:
语言 测试规则 Python 路径包含 tests//test/;或文件名以test_开头或以_test.py结尾;或文件名为conftest.py。JS/TS/TSX 路径包含 __tests__、tests、test、spec、e2e;或文件名包含.test./.spec.。Go 文件名以 _test.go结尾。Java 路径包含 test/tests;或文件名以Test.java/Tests.java结尾。Rust 路径包含 tests//benches/。C# 路径包含 tests/;或项目路径段以.Tests/.Test/.UnitTests/.IntegrationTests结尾;或文件名以Tests/Test结尾。Ruby 路径包含 spec//test/;或文件名以_spec.rb/_test.rb结尾。Kotlin 路径包含 test//tests//spec/;或文件名以Test.kt/Tests.kt/Spec.kt结尾。Swift 路径包含 test//tests//uitests//integrationtests/(不区分大小写);或文件名以Test.swift/Tests.swift结尾。PowerShell 路径包含 test//tests//pester/;或文件名以.Tests.ps1/.Test.ps1结尾。C++ 路径包含 test//tests//testing/;或文件名以test_开头,或以_test.cpp/_tests.cpp结尾。 -
逐文件提取 —
process(text, ProcessConfig(structure, imports, symbols))返回已声明项、原始导入语句以及扁平的已声明名称列表。 -
配对 — 对每个测试文件,将导入解析(按语言处理,例如 Python 的
from pkg.mod import x→pkg/mod.py;Java 的import a.b.C;→a/b/C.java;C# 的using表示命名空间而非文件,因此不执行操作)与标识符重叠(类单词的 token,长度 ≥ 4,与已声明名称匹配)合并。 -
输出 JSON —
untested_sources按声明数量降序排列。
局限性(请如实告知 Agent)
两种引擎都是静态、仅解析的启发式方法;与覆盖率分析相比,它们以少量准确性为代价,大幅降低了成本。已知缺漏:
- 反射/通过 DI 解析的类型:如果仅通过字符串名称或容器解析引用,则无法检测到;测试源代码中不会出现该类型的短名称。
- 扩展方法:以实例方法形式调用时(C#),不会写出声明该方法的静态类名,因此不会将其文件计入。
var、目标类型推断的new()、模式匹配:会丢失类型 token;文件级合并通常仍可通过其他引用捕获该文件。- 短标识符名称:为避免对
id、db、Tag等名称产生噪声配对,会忽略多语言代码中的长度小于 4 的名称。 - Monorepo 路径别名:不会解析 TS 路径映射、Java module-info;如果两个文件具有相同的路径后缀,后缀匹配回退可能会选错源文件。
遇到这些情况时,请对 Agent 已完成初步筛选、但尚未配对的候选项运行实际覆盖率分析(coverage-analysis)。
最终结果始终应标注为静态配对启发式方法,而非行覆盖率或分支覆盖率的证据。即使每个请求的源文件都有明显匹配或明显缺失的测试,也必须注明这一点。
Agent 应读取的输出
untested[*].source/untested_sources[*].path— 选择下一个要测试的源文件(按声明数量从高到低排列)。*.suggested_test_path— 新测试文件的即插即用目标路径;Roslyn 引擎会使用已通过<ProjectReference>引用该源文件所在项目的测试项目,因此无需执行dotnet sln add。如果找不到测试根目录,polyglot 引擎可能会建议将测试文件与源文件放在同一目录。若某个源文件的同目录文件已有对应测试,则该测试目录已成为约定目录;缺少对应测试的同目录文件必须沿用该目录,而不是退而选择与源文件放在同一目录。source_to_tests(Roslyn)/--include-testedtested_sources(polyglot)— 确认新编写的测试文件已列入预期源文件对应的列表。orphan_tests(polyglot)— 未引用任何同语言源文件的测试;可用于排查过时测试或仅用于集成测试的测试。