npx skills add ...
npx skills add dotnet/skills --skill find-untested-sources
MANDATORY for static source-to-test pairing: find or list source files/modules without corresponding tests, or suggest test locations from repository structure. Invoke even for a tiny package; do not substitute manual globbing. Uses Roslyn for C#/.NET and tree-sitter for Python, TS/JS, Go, Java, Rust, Ruby, Kotlin, Swift, PowerShell, and C++. DO NOT USE FOR: real line/branch/Cobertura data, coverage-backed test priorities, CRAP risk, or grading existing tests.
npx skills add dotnet/skills --skill find-untested-sources
Coverage tools answer "which lines were executed?" — they require a green build and a passing test run, which is minutes-to-tens-of-minutes on a real repo. The question this skill answers is different and much cheaper:
Which source files have no test file referencing any of their declared types/symbols?
That's the question an agent asks before writing a new test — and it can be answered statically in a few seconds by parsing source files, with no build, no dependency resolution, and no compilation. The output is a deterministic test-pairing map that lets the agent pick the next file to test without reading the entire codebase first.
This skill ships two interchangeable analyzers with a compatible JSON contract:
| Engine | Script | Use when |
|---|---|---|
| Roslyn (C#) | scripts/Find-UntestedSources.cs | The repo is .NET-only. Parses every .cs file with the Roslyn syntax API and does strict namespace disambiguation, so it is materially more accurate on duplicated short names like Settings or Context. |
| tree-sitter (polyglot) | scripts/find_untested_sources.py | The repo is not exclusively C#, or you want one tool across C#, Python, TypeScript/JavaScript, Go, Java, Rust, Ruby, Kotlin, Swift, PowerShell, and C++. |
For a .NET-only repository, prefer the Roslyn engine — its namespace-aware pairing beats the polyglot engine's identifier overlap.
--include-tested when the answer must distinguish
paired sources from unpaired sources.
"Static pairing only" prohibits compiling the target repository and running
its tests; it does not prohibit launching this skill's parse-only analyzer.
State that distinction briefly when the caller also says "do not build."
Treat analyzer dependencies as environment prerequisites: do not install
packages, try the wrong engine, build the repository, or fall back to a manual
scan when an analyzer invocation fails. Report the prerequisite failure instead.coverage-analysis.coverage-analysis.coverage-analysis.test-gap-analysis (mutation reasoning)
or assertion-quality.dotnet run script.cs). Pinned in the
repo's global.json (SDK 11 preview or later).Microsoft.CodeAnalysis.CSharp on first run.Diagnostics go to stderr; JSON goes to stdout.
bin/, obj/, node_modules/,
.git/, .vs/, packages/, and any dotted subdir. Skips generated files
(.g.cs, .Designer.cs, .AssemblyInfo.cs)..csproj and
marks it a test project if the project name ends in .Tests, .Test,
.UnitTests, .IntegrationTests, .E2E, .EndToEnd, .Spec, .Specs, or
the content references Microsoft.NET.Test.Sdk, MSTest.Sdk,
Microsoft.Testing.Platform, xunit, NUnit, TUnit, or
<IsTestProject>true</IsTestProject>.CSharpSyntaxTree.ParseText (syntax only, no compilation); record every
BaseTypeDeclarationSyntax / DelegateDeclarationSyntax as
(ShortName, EnclosingNamespace, FilePath).using directives +
enclosing namespace, walk every IdentifierToken, look it up in the
short-name index, and disambiguate strictly: an identifier is attributed
only if the declaration's namespace matches one of the test file's using
directives, the enclosing namespace, or a prefix of them. This avoids noise
where common names like Settings or Context match every project.source → [tests]. Build a
production-to-test project map from <ProjectReference> entries; for each
untested source, mirror its in-project relative path under the referencing
test project to suggest a path.pip install tree-sitter-language-pack (single self-contained wheel that
bundles parsers for 300+ languages and the high-level process() API). No
native build, no per-language grammar install.Pass --include-tested to additionally emit tested_sources (omitted by
default to keep the payload small for LLM consumption). Diagnostics go to
stderr; JSON goes to stdout.
File discovery — recursive walk pruning common build/vendor dirs (bin,
obj, node_modules, target, dist, build, vendor, __pycache__,
.venv, .git, …) and generated files (.d.ts, .g.cs, .Designer.cs,
_pb2.py, *.min.js, AssemblyInfo.cs, …).
Language detection — detect_language_from_path maps the extension to a
supported language; unknown extensions are skipped.
Test-vs-source classification — per-language path heuristics:
| Language | Test rule |
|---|---|
| Python | path contains tests//test/; or filename starts with test_ or ends _test.py; or conftest.py. |
| JS/TS/TSX | path contains __tests__, tests, test, spec, e2e; or filename contains .test./.spec.. |
| Go | filename ends _test.go. |
| Java | path contains test/tests; or filename ends Test.java/Tests.java. |
| Rust | path contains tests//benches/. |
| C# | path contains tests/; or project segment ends .Tests/.Test/.UnitTests/.IntegrationTests; or filename ends Tests/Test. |
| Ruby | path contains spec//test/; or filename ends _spec.rb/_test.rb. |
| Kotlin | path contains test//tests//spec/; or filename ends Test.kt/Tests.kt/Spec.kt. |
| Swift | path contains test//tests//uitests//integrationtests/ (case-insensitive); or filename ends Test.swift/Tests.swift. |
| PowerShell | path contains test//tests//pester/; or filename ends .Tests.ps1/.Test.ps1. |
| C++ | path contains test//tests//testing/; or filename starts test_ or ends _test.cpp/_tests.cpp. |
Per-file extraction — process(text, ProcessConfig(structure, imports, symbols)) returns declared items, raw import statements, and a flat declared
-name list.
Pairing — for each test file, union import resolution (per language,
e.g. Python from pkg.mod import x → pkg/mod.py; Java import a.b.C; →
a/b/C.java; C# using is namespace-not-file, so a no-op) with identifier
overlap (word-like tokens, length ≥ 4, matched against declared names).
JSON emit — untested_sources ordered by declaration count descending.
Both engines are static, parse-only heuristics that trade a little accuracy for orders-of-magnitude lower cost than coverage. Known gaps:
var, target-typed new(), pattern matching lose the type token; the
file-level union usually still catches it through other references.id, db, Tag.For these cases, run actual coverage (coverage-analysis) on the unpaired
candidates the agent has already triaged.
Always label the final result as a static pairing heuristic, not evidence of line or branch coverage. Include that caveat even when every requested source file has an obvious matching or missing test.
untested[*].source / untested_sources[*].path — pick the next source file
to test (highest declaration count first).*.suggested_test_path — drop-in target for the new test file; the Roslyn
engine honors the test project that already <ProjectReference>s the source's
project, so dotnet sln add is not needed. The polyglot engine may suggest a
co-located test when no test root is discoverable. When a source sibling is
already paired, its test directory is the established convention and must be
reused for the missing sibling rather than falling back to source co-location.source_to_tests (Roslyn) / --include-tested tested_sources (polyglot) —
verify a newly written test file lands in the list for the intended source.orphan_tests (polyglot) — tests that don't reference any same-language
source file; useful for triaging stale or integration-only tests._