HooSharper is an open-source collection of Roslyn analyzers and code fixes for opinionated C# code style. It brings focused, conservative inspections and quick fixes to the standard .NET compiler and IDE analyzer infrastructure.
The package currently ships nine rules covering guard clauses, compact conditionals, redundant control flow, type patterns, boolean expressions, dictionary lookups, null-coalescing assignment, and argument validation.
- A C# project using an SDK-style project file
- A Roslyn-capable editor or build environment, such as Visual Studio, Rider, VS Code with C# tooling, or
dotnet build - This repository itself builds with .NET SDK 10.0.110 or a compatible .NET 10 patch
The analyzer package targets netstandard2.0 so it can run in a broad range of Roslyn hosts. Projects consuming the analyzer do not need to target .NET 10.
Install the package directly from nuget.org:
dotnet add package HooSharper.Analyzers --version 0.3.6For central package management, add the version to Directory.Packages.props:
<Project>
<ItemGroup>
<PackageVersion Include="HooSharper.Analyzers" Version="0.3.6" />
</ItemGroup>
</Project>Then reference it from each project:
<ItemGroup>
<PackageReference Include="HooSharper.Analyzers" PrivateAssets="all" />
</ItemGroup>Without central package management:
<ItemGroup>
<PackageReference Include="HooSharper.Analyzers"
Version="0.3.6"
PrivateAssets="all" />
</ItemGroup>PrivateAssets="all" prevents an application or library from exposing HooSharper as a transitive runtime dependency. The package contains both the analyzer assembly and its code-fix assembly under analyzers/dotnet/cs.
Build the NuGet package:
dotnet build HooSharper.slnx -c Release
dotnet pack src/HooSharper.Analyzers/HooSharper.Analyzers.csproj \
-c Release \
--no-build \
-o artifactsAdd the local package directory as a source and install it into another project:
dotnet nuget add source /absolute/path/to/hoosharper/artifacts \
--name HooSharperLocal
dotnet add package HooSharper.Analyzers \
--version 0.3.6 \
--source /absolute/path/to/hoosharper/artifactsAlternatively, add the local source in NuGet.config:
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<packageSources>
<add key="HooSharperLocal" value="/absolute/path/to/hoosharper/artifacts" />
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" />
</packageSources>
</configuration>After adding the package:
- Restore and build the project.
- Open a C# file in a Roslyn-capable IDE.
- Place the caret on a HooSharper diagnostic.
- Open Quick Actions. In Visual Studio and VS Code this is normally
Ctrl+.; Rider commonly usesAlt+Enter. - Select the HooSharper action.
All current fixers use Roslyn's batch Fix All provider. Where the host supports it, an action can be applied to the document, project, or solution.
Analyzer diagnostics also run during dotnet build. Code fixes are interactive IDE operations; a command-line build reports diagnostics but does not rewrite source files.
HooSharper uses normal Roslyn diagnostic configuration. Add settings under the [*.cs] section of the repository's .editorconfig.
Each rule can use one of these values:
error— fails the buildwarning— appears as a warningsuggestion— appears as an IDE suggestion/build messagesilent— hidden but available to IDE featuresnone— disables the ruledefault— restores the descriptor's default severity
Example configuration:
root = true
[*.cs]
dotnet_diagnostic.HOO1001.severity = warning
dotnet_diagnostic.HOO1002.severity = suggestionAll current diagnostics are enabled by default with Roslyn Info severity.
[*.cs]
dotnet_diagnostic.HOO1002.severity = none[*.cs]
dotnet_diagnostic.HOO1001.severity = errorEditorConfig sections and file hierarchy determine scope. For example, enforce early returns in production code but disable the rule in generated compatibility sources:
[*.cs]
dotnet_diagnostic.HOO1001.severity = warning
[src/Compatibility/**/*.cs]
dotnet_diagnostic.HOO1001.severity = nonePrefer correcting the code or configuring the rule by scope. When a single occurrence must remain, use a standard diagnostic suppression:
#pragma warning disable HOO1001
// Intentionally nested control flow.
#pragma warning restore HOO1001A project-wide MSBuild suppression also works, but is less visible than .editorconfig:
<PropertyGroup>
<NoWarn>$(NoWarn);HOO1001</NoWarn>
</PropertyGroup>- Category:
HooSharper.CodeStyle - Default severity: Info
- Enabled by default: Yes
- Code fix: Invert condition and return early
- Fix All: Yes
HOO1001 reports a final if statement that wraps the remaining work of a void method.
Before:
void Run(bool enabled)
{
Prepare();
if (enabled)
{
Execute();
Finish();
}
}After:
void Run(bool enabled)
{
Prepare();
if (!enabled)
return;
Execute();
Finish();
}The fixer simplifies common negations and comparisons. Examples include:
if (!disabled) // becomes: if (disabled) return;
if (value == 0) // becomes: if (value != 0) return;
if (value < 10) // becomes: if (!(value < 10)) return;The current implementation is intentionally conservative. A diagnostic is reported only when all of these conditions hold:
- The containing declaration is a method with the exact return type
void. - The
ifis the last statement in the method body. - The
ifhas noelsebranch. - The body is a nonempty block.
It currently does not report for:
- Value-returning methods
- Local functions, constructors, accessors, operators, or lambdas
- An
iffollowed by another statement - An
ifwith anelse - Empty blocks
Configure it with:
[*.cs]
dotnet_diagnostic.HOO1001.severity = warning- Category:
HooSharper.CodeStyle - Default severity: Info
- Enabled by default: Yes
- Code fix: Remove braces
- Fix All: Yes
HOO1002 reports braces around a safe single-statement if or else branch.
Before:
if (enabled)
{
Execute();
}After:
if (enabled)
Execute();It also handles an else branch:
if (enabled)
Execute();
else
Finish();The analyzer does not remove braces when the block:
- Contains zero or multiple statements
- Declares a local variable
- Declares a local function
- Contains preprocessor directives
- Is the block of an
else if; the nestedifis analyzed independently
These restrictions avoid changing declaration scope or damaging conditional-compilation structure.
Configure it with:
[*.cs]
dotnet_diagnostic.HOO1002.severity = suggestionThe standard .NET style option expresses the same general brace preference for built-in IDE tooling:
csharp_prefer_braces = false:warningHOO1002 remains independently configurable through dotnet_diagnostic.HOO1002.severity. The built-in option and HooSharper rule may both report in hosts that enable both analyzers. Disable one diagnostic if duplicate suggestions appear.
All rules below use category HooSharper.CodeStyle, default to Info severity, are enabled by default, and support Fix All.
| ID | Rule | Example |
|---|---|---|
HOO1003 |
Remove a redundant else after a terminating branch |
if (!valid) return; else Work(); → if (!valid) return; Work(); |
HOO1004 |
Replace a final nested loop condition with an early continue |
if (valid) { Work(); } → if (!valid) continue; Work(); |
HOO1005 |
Combine an as cast and null check into a type pattern |
var x = value as T; if (x is not null) → if (value is T x) |
HOO1006 |
Simplify comparisons with boolean literals | ready == false → !ready |
HOO1007 |
Replace ContainsKey plus index access with TryGetValue |
map.ContainsKey(key) and map[key] → map.TryGetValue(key, out var value) |
HOO1008 |
Replace a null check and assignment with ??= |
if (cache is null) cache = Create(); → cache ??= Create(); |
HOO1009 |
Replace a classic argument null guard with ThrowIfNull |
if (value is null) throw new ArgumentNullException(nameof(value)); → ArgumentNullException.ThrowIfNull(value); |
HOO1010 |
Merge nested if statements |
if (a) { if (b) Work(); } → if (a && b) Work(); |
HOO1011 |
Use Dictionary.TryAdd |
if (!map.ContainsKey(k)) map.Add(k, v); → map.TryAdd(k, v); |
HOO1012 |
Use the result of HashSet.Add |
if (!set.Contains(v)) { set.Add(v); Work(); } → if (set.Add(v)) Work(); |
HOO1013 |
Use a terminal using declaration | using (var x = Open()) { Work(x); } → using var x = Open(); Work(x); |
HOO1014 |
Use a null-coalescing expression | x is null ? fallback : x → x ?? fallback |
HOO1015 |
Use null-conditional access | x is null ? null : x.Value → x?.Value |
HOO1016 |
Use string.Contains for a presence test |
text.IndexOf(value) >= 0 → text.Contains(value) |
HOO1017 |
Simplify opposite boolean returns | if (ready) return true; return false; → return ready; |
HOO1018 |
Remove a redundant guard around ?. |
if (x is not null) x?.Run(); → x?.Run(); |
HOO1019 |
Use a not pattern |
!(x is string) → x is not string |
HOO1020 |
Wrap long fluent chains | Place every continuation . at the start of its own line. |
The analyzers deliberately skip ambiguous transformations, including overloaded equality operators, nullable boolean comparisons, unstable expressions with side effects, directive-containing regions, mismatched symbols, changed disposal scope, unavailable framework APIs, unsupported C# language versions, expression-tree-incompatible syntax, scope-expanding declaration collisions, dictionary indexer writes/by-reference uses, and collection arguments that comparer callbacks could mutate. Dictionary and set rules are restricted to standard framework types and callback-stable receivers, keys, and values.
HOO1020 reports a single-line fluent chain whose visual end column exceeds 140 characters and fixes it with leading continuation dots. Tabs, tab_width, indent_style, indent_size, CRLF line endings, and Unicode surrogate pairs are accounted for:
var result = source
.Where(IsValid)
.Select(Convert)
.ToArray();Configure the limit in .editorconfig. The HooSharper-specific key takes precedence over the standard key:
[*.cs]
max_line_length = 140
hoosharper_max_line_length = 140Conditional-access chains, directive-containing expressions, and chains that are already multiline are not rewritten.
Configure any rule independently:
[*.cs]
dotnet_diagnostic.HOO1003.severity = warning
dotnet_diagnostic.HOO1007.severity = suggestion
dotnet_diagnostic.HOO1009.severity = noneA reasonable starting point is:
[*.cs]
# Prefer guard clauses but introduce them gradually.
dotnet_diagnostic.HOO1001.severity = suggestion
# Enforce compact single-statement conditionals.
dotnet_diagnostic.HOO1002.severity = warning
csharp_prefer_braces = false:warning
# Enable the conservative automatic-fix rules as suggestions.
dotnet_analyzer_diagnostic.category-HooSharper.CodeStyle.severity = suggestionFor CI enforcement:
[*.cs]
dotnet_diagnostic.HOO1001.severity = warning
dotnet_diagnostic.HOO1002.severity = warningProjects using <TreatWarningsAsErrors>true</TreatWarningsAsErrors> will fail when any HooSharper rule is configured as a warning.
Clone and build:
git clone https://github.com/openhoo/hoosharper.git
cd hoosharper
bun install --frozen-lockfile
dotnet restore HooSharper.slnx
dotnet build HooSharper.slnx
dotnet test HooSharper.slnxbun install activates the Husky commit-msg hook. Every local commit is checked by Commitlint using @commitlint/config-conventional; CI validates the complete pull-request or push commit range again. Run it manually with:
echo "feat(analyzers): add a rule" | bunx --no-install commitlint
bunx --no-install commitlint --from origin/main --to HEAD --verboseCommitlint enforces message structure, while Hooversion applies OpenHoo's release semantics and package routing. Both checks must pass.
Run a release build and package:
dotnet build HooSharper.slnx -c Release
dotnet test HooSharper.slnx -c Release --no-build
dotnet pack src/HooSharper.Analyzers/HooSharper.Analyzers.csproj \
-c Release \
--no-build \
-o artifactsCheck dependency updates:
dotnet list HooSharper.slnx package --outdatedsrc/HooSharper.Analyzers/ DiagnosticAnalyzer implementations
src/HooSharper.CodeFixes/ CodeFixProvider implementations
tests/HooSharper.Analyzers.Tests/ Analyzer and code-fix tests
benchmarks/HooSharper.Performance/ BenchmarkDotNet performance suite
artifacts/ Local NuGet packages; ignored by Git
Each rule has:
- One analyzer file
- One code-fix provider file
- One dedicated test file
- One entry in
AnalyzerReleases.Unshipped.md, moved toAnalyzerReleases.Shipped.mdwhen released
Analyzer rules use the HOO diagnostic prefix.
Tests use Microsoft's generic Roslyn testing packages with DefaultVerifier, xUnit v3, .NET 10 reference assemblies, and the latest C# parse mode.
A code-fix test should verify:
- Exact diagnostic ID
- Exact diagnostic location using Roslyn markup such as
{|#0:if|} - Exact fixed source text
- That fixable diagnostics are removed
- Incremental application
- Fix All behavior when supported
- Negative cases where no diagnostic should be emitted
Run one test class while developing:
dotnet test tests/HooSharper.Analyzers.Tests/HooSharper.Analyzers.Tests.csproj \
--filter FullyQualifiedName~PreferEarlyReturnAnalyzerTestsThe custom test verifier is in tests/HooSharper.Analyzers.Tests/AnalyzerVerifier.cs.
The BenchmarkDotNet project measures real Roslyn analyzer and code-fix execution. It creates deterministic compilations with 100 or 1,000 candidate groups and records both execution time and managed allocations.
Code-fix application benchmarks create a fresh Roslyn workspace, diagnostic, and action for every measured invocation. Their result therefore includes fixture analysis and action registration as well as action computation and application; registration-only benchmarks isolate RegisterCodeFixesAsync.
List the available benchmarks:
dotnet run --project benchmarks/HooSharper.Performance/HooSharper.Performance.csproj \
-c Release \
-- --list flatRun the complete suite:
dotnet run --project benchmarks/HooSharper.Performance/HooSharper.Performance.csproj \
-c Release \
--no-buildUseful focused runs:
# All analyzers together and collection-heavy rules
dotnet run --project benchmarks/HooSharper.Performance/HooSharper.Performance.csproj \
-c Release --no-build -- --filter '*AnalyzerBenchmarks*'
# One benchmark case for every HOO1001-HOO1020 analyzer
dotnet run --project benchmarks/HooSharper.Performance/HooSharper.Performance.csproj \
-c Release --no-build -- --filter '*IndividualAnalyzerBenchmarks*'
# Code-action registration and application
dotnet run --project benchmarks/HooSharper.Performance/HooSharper.Performance.csproj \
-c Release --no-build -- --filter '*CodeFixBenchmarks*'The stable local job uses five warmup iterations, twelve measured iterations, and a 200 ms minimum iteration time. Reports are exported as JSON and GitHub-flavored Markdown under BenchmarkDotNet.Artifacts/results/. The artifact directory is local output and should not be committed.
Compare performance using the same benchmark sources, SDK, build configuration, parameters, and machine state on both revisions. Treat overlapping confidence intervals as inconclusive; do not infer improvements from mean values alone.
HooSharper.Analyzers.csproj produces HooSharper.Analyzers.<version>.nupkg. The package contains:
analyzers/dotnet/cs/HooSharper.Analyzers.dll
analyzers/dotnet/cs/HooSharper.CodeFixes.dll
README.md
The analyzer assembly is loaded by the compiler and IDE. The code-fix assembly is used by IDE hosts that discover Roslyn code-fix providers. Neither assembly is a runtime application dependency.
Inspect a locally produced package with:
unzip -l artifacts/HooSharper.Analyzers.0.2.1.nupkgCI follows the shared OpenHoo release model:
.github/workflows/ci.ymlruns on pull requests and pushes tomain.- Hooversion validates every commit against the Conventional Commits format.
- The solution is restored, built, tested, and packed with the SDK pinned in
global.json. - A successful non-release push to
maintriggers.github/workflows/release.yml. - Hooversion calculates the next semantic version, updates
versionandCHANGELOG.md, creates achore(release):commit andv<version>tag, pushes both, and creates the GitHub Release. - The tagged source is rebuilt and published to GitHub Packages.
- After that succeeds, the release workflow automatically dispatches the trusted-publishing job in
.github/workflows/ci.ymlfor the same tag and publishes the package to nuget.org.
Version changes are derived from Conventional Commits:
feat:creates a minor release.fix:andperf:create a patch release.- A
!after the type or scope, or aBREAKING CHANGE:footer, creates a major release. docs:,test:,ci:,chore:, and other non-release types do not create a version by themselves.
Examples:
feat(analyzers): add expression-bodied member rule
fix(code-fixes): preserve comments around removed braces
feat!: rename diagnostic categories
The package version has one source of truth: the repository-root version file. Directory.Build.props reads it into MSBuild's VersionPrefix, and Hooversion owns release-time updates. Do not manually edit version, CHANGELOG.md, release tags, or release commits for a normal release.
The release workflow uses the repository GITHUB_TOKEN for GitHub Packages. nuget.org publishing uses OpenID Connect trusted publishing for the OpenHoo account, so neither registry requires a long-lived package publishing secret.
To preview a release without creating commits, tags, releases, or packages, run the Release workflow manually with dry_run enabled. For local inspection, check out the same pinned Hooversion revision used by CI:
git clone https://github.com/openhoo/hooversion.git /tmp/hooversion
git -C /tmp/hooversion checkout 799ecf4a9c29e8ce4d5ad7055a6030adf665cf82
(cd /tmp/hooversion && bun install --frozen-lockfile)
bun /tmp/hooversion/src/cli.ts plan
bun /tmp/hooversion/src/cli.ts release --dry-runThe Hooversion action and installation source are pinned to the audited v0.2.0 commit. Update that SHA deliberately in both workflows when upgrading Hooversion.
- Choose the next
HOOdiagnostic ID. - Add one analyzer under
src/HooSharper.Analyzers. - Add one code-fix provider under
src/HooSharper.CodeFixeswhen the transformation is safe and deterministic. - Add a dedicated test file under
tests/HooSharper.Analyzers.Tests. - Register the rule in
src/HooSharper.Analyzers/AnalyzerReleases.Unshipped.md; move it toAnalyzerReleases.Shipped.mdwhen the release ships. - Document the rule and its configuration in this README.
- Run the full build, tests, and package verification.
New fixers should preserve trivia, request Roslyn formatting only for changed nodes, support Fix All when transformations are independent, and avoid offering fixes that could change behavior.
HooSharper is licensed under the MIT License.
