Find the dead weight in your Reqnroll / SpecFlow BDD suite.
Unused code, unused step definitions, and feature-file data errors — pure static analysis, no AI, no network.
Point it at your solution folder and it reports what's rotting: C# no one calls, step definitions no scenario uses, and feature files with broken data. Pure static analysis — no network, no AI, no test run required.
▶ View a live sample report — generated from the tiny demo in samples/demo, it shows a finding in each check.
| Check | What it finds |
|---|---|
| Unused code | Dead C# — methods, classes, and interfaces with no references, using Roslyn's semantic model (symbol-accurate, not text matching). |
| Unused step definitions | [Given]/[When]/[Then] bindings that no scenario uses, matched with the same Cucumber-expression / regex semantics Reqnroll uses at runtime — so no false "unused". |
| Data errors | Feature-file problems: undefined <placeholders>, a Scenario that has an Examples: table (should be a Scenario Outline), malformed data tables, unresolved @DataSource CSVs. |
The easiest way — as a .NET global tool:
dotnet tool install -g SpecHygiene
spechygiene /path/to/your/solutionUpdate later with dotnet tool update -g SpecHygiene.
Requires the .NET SDK (8 or later) to install. The tool runs on .NET 8, 9, and 10 — and it analyses source, so it works on codebases targeting any of them, including .NET 10 / C# 14.
Run from source instead
git clone https://github.com/Karzone/SpecHygiene.git
cd SpecHygiene
dotnet build -c Release
dotnet run --project src/SpecHygiene -- /path/to/your/solutionTry it on the bundled demo first:
spechygiene ./samples/demo # if installed as a tool
# or
dotnet run --project src/SpecHygiene -- ./samples/demospechygiene <path> [checks] [--out <dir>]
Checks (default: all):
--unused-code Roslyn dead-code (unused methods/classes/interfaces)
--unused-steps Step definitions no scenario uses
--data-errors Feature-file data errors (undefined placeholders, etc.)
--all Run every check (default when none specified)
Options:
--out <dir> Output directory (default ./reports)
-h, --help Show this help
Examples:
# Just the unused-step-definition check
dotnet run --project src/SpecHygiene -- /path/to/solution --unused-steps
# Two checks, custom output folder
dotnet run --project src/SpecHygiene -- /path/to/solution --unused-code --data-errors --out ./hygieneTip: after
dotnet build, run the compiled binary directly:./src/SpecHygiene/bin/Release/net8.0/spechygiene <path>
Every run writes to the output directory (default ./reports):
| File | What it is |
|---|---|
spechygiene-report.html |
A self-contained HTML report — summary cards per check plus a table of every finding with file/line locations. Light- & dark-mode aware. |
summary.txt |
A plain-text summary of the counts — handy for CI logs. |
The console prints the same summary as it runs.
How the HTML is generated — it's produced automatically on every run (unless you set
Output.GenerateHtml to false in appsettings.json). It's a single file with all CSS
inlined and no JavaScript or external assets, so it works completely offline and is safe to
email or archive.
How to view it — just open the file in any browser:
# macOS
open reports/spechygiene-report.html
# Windows
start reports\spechygiene-report.html
# Linux
xdg-open reports/spechygiene-report.htmlOr see the live sample report rendered straight from this repo.
Defaults live in src/SpecHygiene/appsettings.json and can
be tuned — scan paths, feature-file patterns, step-definition globs, exclusion lists, thresholds,
and which checks are enabled. Command-line flags win for the scan path and which checks run.
{
"Analysis": { "FeatureFilePattern": "*.feature" },
"CoverageAnalysis": { "Enabled": true, "UseSyntacticDiscovery": true },
"UnusedCodeAnalysis": { "Enabled": true, "IncludePublicMembers": false }
}
UnusedCodeAnalysis.IncludePublicMembersis off by default: in solutions with DI, controllers, serialization, or reflection, public members are often reached by call paths no static analyzer can see, so flagging them is noisy. Private + internal members are assembly-scoped — that's where dead-code detection is sound. Turn it on for a stricter sweep.
- Unused step definitions — parse your
*Steps.cswith Roslyn to discover bindings (honouring[Binding]inheritance and bare method-name-convention bindings), then match every feature-file step against them using the same expression semantics Reqnroll uses at runtime — Cucumber expressions, regex, optional text(s), alternationa/b, and parameter types. A step that binds is "used"; a binding no step reaches is reported as unused. - Unused code — build a Roslyn compilation and walk symbol references, so an overload that merely looks called by name is judged correctly. Test methods, hooks, controllers, and serialization entry points are excluded by attribute so they aren't false-flagged.
- Data errors come from a single cheap feature-file parse pass.
dotnet build # build the solution
dotnet test # run the test suite (217 tests)src/SpecHygiene # the CLI + analysis engine
tests/SpecHygiene.Tests # xUnit tests, incl. a matcher eval corpus
samples/demo # a tiny solution that triggers every check
The matcher tests include an eval corpus — binding/step cases whose expected results are pinned to real Reqnroll runtime behaviour, so changes to the matching logic are gated against ground truth rather than intuition.
Issues and pull requests are welcome. Please run dotnet test before opening a PR.
MIT © Karthik Kalaiyarasu