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 20 rules covering guard clauses, compact conditionals, redundant control flow, type patterns, boolean expressions, collection lookups, null handling, argument validation, using declarations, string presence tests, and fluent-chain formatting.
- 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 requests .NET SDK 10.0.109 in
global.jsonand allowsrollForward: latestPatchwithin the .NET 10.0.1xx feature band
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.10For central package management, add the version to Directory.Packages.props:
<Project>
<ItemGroup>
<PackageVersion Include="HooSharper.Analyzers" Version="0.3.10" />
</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.10"
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.10 \
--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 |
Prefer an early continue when a final if wraps the remaining loop body |
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 runs Commitlint and Hooversion over the complete pull-request or pushed commit range. Workflow dispatches validate the checked-out commit. Run Commitlint 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.
Benchmark methods report distinct timing scopes. Registration measures only RegisterCodeFixesAsync against a prepared fixture; ComputeOperations measures only code-action operation computation, with fixture creation, analysis, registration, and application outside the measured method; ApplyOperations measures only application, with fixture creation, analysis, registration, and operation computation outside the measured method. DiscoverRegisterApply measures the end-to-end fixture discovery, registration, operation computation, and application path. DocumentFixAll measures Fix All operation computation and application after fixture discovery, analysis, registration, and Fix All action creation in iteration setup. Registration-only timing therefore does not include analyzer discovery or fixture construction.
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
hoosharper-logo.png
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.*.nupkgCI follows the shared OpenHoo release model:
.github/workflows/ci.ymlruns on pull requests, pushes tomain, and manual dispatches.- Commitlint and Hooversion validate every commit in the complete pull-request or pushed commit range; manual dispatches validate the checked-out commit.
- The analyzer test project and its dependencies are restored and built with the SDK policy in
global.json, its tests run with a line-coverage gate, and the analyzer package is packed. - 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, published to GitHub Packages, and attached to the GitHub Release with an SPDX SBOM, checksums, Sigstore bundles, and GitHub artifact attestations.
- After that succeeds, the release workflow automatically dispatches the
trusted-publishing job in
.github/workflows/ci.ymlfor the same tag. That job downloads the already signed GitHub Release package, verifies its checksum, then publishes that exact artifact to nuget.org without rebuilding it.
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 ac503b23b9b36ebbf39ee6103713c81f1f18b64d
(cd /tmp/hooversion && go build -o /tmp/hooversion-bin ./cmd/hooversion)
/tmp/hooversion-bin plan
/tmp/hooversion-bin release --dry-runThe Hooversion action and installation source are pinned to the audited
v1.1.1 commit. Update its immutable SHA and matching version input
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 Apache License 2.0.
