ExhaustiveMatching.Modern
1.0.1
dotnet add package ExhaustiveMatching.Modern --version 1.0.1
NuGet\Install-Package ExhaustiveMatching.Modern -Version 1.0.1
<PackageReference Include="ExhaustiveMatching.Modern" Version="1.0.1" />
<PackageVersion Include="ExhaustiveMatching.Modern" Version="1.0.1" />
<PackageReference Include="ExhaustiveMatching.Modern" />
paket add ExhaustiveMatching.Modern --version 1.0.1
#r "nuget: ExhaustiveMatching.Modern, 1.0.1"
#:package ExhaustiveMatching.Modern@1.0.1
#addin nuget:?package=ExhaustiveMatching.Modern&version=1.0.1
#tool nuget:?package=ExhaustiveMatching.Modern&version=1.0.1
ExhaustiveMatching.Modern
ExhaustiveMatching.Modern is a Roslyn analyzer that reports missing cases in
C# switch statements and switch expressions. It supports enums and closed class
or interface hierarchies, including modern logical patterns such as or,
and, not, and parenthesized patterns.
This project is a maintained, modernized fork of
WalkerCodeRanger/ExhaustiveMatching.
It keeps the existing ExhaustiveMatching namespace so applications can migrate
without changing their source API.
Install
dotnet add package ExhaustiveMatching.Modern
Install the package in every project that contains exhaustive switches or
declares, inherits, or implements types marked with ClosedAttribute.
The package supports projects targeting .NET 8 and .NET 10. The runtime API and analyzer target .NET Standard 2.0, and the analyzer supports Roslyn 4.8 or newer.
Quick start
Mark a switch as intentionally exhaustive by throwing the exception returned by
ExhaustiveMatch.Failed from its fallback case:
using ExhaustiveMatching;
public enum CoinFlip
{
Heads,
Tails,
}
// EM0001: Enum value not handled by switch: Tails
var result = coinFlip switch
{
CoinFlip.Heads => "Heads!",
_ => throw ExhaustiveMatch.Failed(coinFlip),
};
Modern pattern examples
or patterns
Combine multiple declared enum values in one arm:
var category = coinFlip switch
{
CoinFlip.Heads or CoinFlip.Tails => "Known outcome",
_ => throw ExhaustiveMatch.Failed(coinFlip),
};
Nullable enum switches can combine logical patterns with an explicit null
case:
var message = nullableCoinFlip switch
{
null => "No result",
CoinFlip.Heads or CoinFlip.Tails => "Known result",
_ => throw ExhaustiveMatch.Failed(nullableCoinFlip),
};
Relational and and patterns
Relational patterns are evaluated against the enum's declared values:
public enum Priority
{
Low = 1,
Medium = 2,
High = 3,
Critical = 4,
}
var queue = priority switch
{
<= Priority.Medium => "Normal",
> Priority.Medium and <= Priority.Critical => "Urgent",
_ => throw ExhaustiveMatch.Failed(priority),
};
The analyzer unions or coverage, intersects and coverage, and complements
safe not patterns. Parentheses can be used to control grouping:
// EM0001: Enum value not handled by switch: CoinFlip.Heads
var result = coinFlip switch
{
not (CoinFlip.Heads) => "Not heads",
_ => throw ExhaustiveMatch.Failed(coinFlip),
};
Closed type hierarchies
Use ClosedAttribute to define the direct cases of a class or interface:
using ExhaustiveMatching;
[Closed(typeof(Circle), typeof(Rectangle), typeof(Triangle))]
public abstract record Shape;
public sealed record Circle(double Radius) : Shape;
public sealed record Rectangle(double Width, double Height) : Shape;
public sealed record Triangle(double Base, double Height) : Shape;
The analyzer reports any concrete case not covered by the switch:
// EM0003: Subtype not handled by switch: Triangle
var area = shape switch
{
Circle circle => CalculateArea(circle),
Rectangle rectangle => CalculateArea(rectangle),
_ => throw ExhaustiveMatch.Failed(shape),
};
Logical patterns may cover multiple cases:
var kind = shape switch
{
Circle or Rectangle => "round or rectangular",
Triangle => "triangular",
_ => throw ExhaustiveMatch.Failed(shape),
};
A not pattern contributes the safe complement of its operand for class
hierarchies:
// EM0003: Subtype not handled by switch: Triangle
var kind = shape switch
{
not Triangle => "Not a triangle",
_ => throw ExhaustiveMatch.Failed(shape),
};
Total positional and property patterns can capture data while covering their whole subtype:
var dimensions = shape switch
{
Circle(var radius) => $"radius={radius}",
Rectangle { Width: var width, Height: var height }
=> $"{width} x {height}",
Triangle(var @base, var height) => $"base={@base}, height={height}",
_ => throw ExhaustiveMatch.Failed(shape),
};
Value constraints are intentionally conservative because they do not cover every instance of a subtype:
// EM0101: this constrained pattern cannot prove full subtype coverage
// EM0003: Circle is still considered unhandled
var description = shape switch
{
Circle { Radius: > 0 } => "Non-empty circle",
Rectangle => "Rectangle",
Triangle => "Triangle",
_ => throw ExhaustiveMatch.Failed(shape),
};
A case may also cover a closed branch higher in a nested hierarchy. All concrete leaf types below that branch count as handled.
Supported patterns
The analyzer evaluates patterns against the finite set of declared enum values or concrete leaves in a closed hierarchy.
| Pattern | Enum | Closed hierarchy |
|---|---|---|
| Constant and type/declaration | Yes | Yes |
or |
Yes | Yes |
and |
Yes | Yes |
not |
Yes | When the complement is provably safe |
| Parenthesized | Yes | Yes |
| Relational | Yes | No |
null |
Yes | Ignored by design |
var and discard |
Yes | Yes |
| Property/positional/recursive | When constraints are provably total | When constraints are provably total |
| List and slice | Conservative | Conservative |
Patterns whose coverage cannot be proven produce EM0101 and do not suppress a
missing-case diagnostic. A when guard produces EM0100 and its pattern does
not count toward exhaustiveness because the guard may be false.
For nullable enums, null must be handled. For closed reference hierarchies,
the analyzer preserves the original behavior and does not require a null case.
Exhaustive switch statements
Switch statements use the same analysis when their default section throws:
switch (shape)
{
case Circle or Rectangle:
RenderSimpleShape(shape);
break;
case Triangle triangle:
RenderTriangle(triangle);
break;
default:
throw ExhaustiveMatch.Failed(shape);
}
The analyzer also recognizes the original enum convention using
InvalidEnumArgumentException.
Diagnostics
| ID | Description |
|---|---|
| EM0001 | An enum switch is missing a declared value |
| EM0002 | A nullable enum switch is missing a null case |
| EM0003 | A closed-type switch is missing a concrete subtype |
| EM0011 | A direct concrete subtype is missing from its parent's Closed cases |
| EM0012 | A listed case is an indirect rather than direct subtype |
| EM0013 | A listed case is not a subtype |
| EM0014 | A concrete subtype is not covered by a closed case |
| EM0015 | A direct open interface is missing from its parent's Closed cases |
| EM0100 | A guarded case cannot prove exhaustive coverage |
| EM0101 | A case pattern cannot be analyzed safely |
| EM0102 | The switched type is neither an enum nor closed |
| EM0103 | A case type is outside the closed hierarchy |
| EM0104 | A type has duplicate Closed attributes |
| EM0105 | A Closed attribute lists a case more than once |
Building
The repository uses the .NET 10 SDK. Install both the .NET 8 and .NET 10 runtimes to run the full test matrix:
dotnet test ExhaustiveMatch.sln --configuration Release
dotnet pack ExhaustiveMatching.Analyzer\ExhaustiveMatching.Analyzer.csproj `
--configuration Release `
--output artifacts
License and attribution
This project remains licensed under the BSD 3-Clause License. The original copyright notice and license conditions are retained. The original author's and contributors' names are not used to endorse this fork.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net5.0 was computed. net5.0-windows was computed. net6.0 was computed. net6.0-android was computed. net6.0-ios was computed. net6.0-maccatalyst was computed. net6.0-macos was computed. net6.0-tvos was computed. net6.0-windows was computed. net7.0 was computed. net7.0-android was computed. net7.0-ios was computed. net7.0-maccatalyst was computed. net7.0-macos was computed. net7.0-tvos was computed. net7.0-windows was computed. net8.0 was computed. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 was computed. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 was computed. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
| .NET Core | netcoreapp2.0 was computed. netcoreapp2.1 was computed. netcoreapp2.2 was computed. netcoreapp3.0 was computed. netcoreapp3.1 was computed. |
| .NET Standard | netstandard2.0 is compatible. netstandard2.1 was computed. |
| .NET Framework | net461 was computed. net462 was computed. net463 was computed. net47 was computed. net471 was computed. net472 was computed. net48 was computed. net481 was computed. |
| MonoAndroid | monoandroid was computed. |
| MonoMac | monomac was computed. |
| MonoTouch | monotouch was computed. |
| Tizen | tizen40 was computed. tizen60 was computed. |
| Xamarin.iOS | xamarinios was computed. |
| Xamarin.Mac | xamarinmac was computed. |
| Xamarin.TVOS | xamarintvos was computed. |
| Xamarin.WatchOS | xamarinwatchos was computed. |
-
.NETStandard 2.0
- No dependencies.
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
Adds .NET 8 support alongside .NET 10, with compatibility for Roslyn 4.8 and newer.